diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a8623953..94799d4b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -88,11 +88,9 @@ if (USE_SUBMODULE_FMT) add_subdirectory(extern/fmt) endif () -option(ENABLE_GUI "Build the graphical user interface" OFF) +# option(ENABLE_GUI "Build the graphical user interface" OFF) add_subdirectory(libtego) -if (ENABLE_GUI) - add_subdirectory(libtego_ui) - add_subdirectory(ricochet-refresh) -endif () +add_subdirectory(libtego_ui) +add_subdirectory(ricochet-refresh) add_subdirectory(irc) diff --git a/src/irc/CMakeLists.txt b/src/irc/CMakeLists.txt index 1101400e..187ed8c4 100644 --- a/src/irc/CMakeLists.txt +++ b/src/irc/CMakeLists.txt @@ -1,3 +1,34 @@ +# Ricochet Refresh - https://ricochetrefresh.net/ +# Copyright (C) 2021, Blueprint For Free Speech +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# +# * Neither the names of the copyright owners nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + cmake_minimum_required(VERSION 3.16) project(ricochet-irc LANGUAGES CXX) @@ -15,7 +46,10 @@ if (FORCE_QT5) NAMES Qt5 COMPONENTS Core + Gui Network + Quick + Widgets REQUIRED) else () find_package( @@ -24,14 +58,20 @@ else () Qt6 Qt5 COMPONENTS Core + Gui Network + Quick + Widgets REQUIRED) endif () find_package( Qt${QT_VERSION_MAJOR} COMPONENTS Core + Gui Network + Quick + Widgets REQUIRED) # Require Qt >5.15 @@ -48,42 +88,47 @@ if (APPLE) REQUIRED) endif () -include(FindOpenSSL) - -add_executable(ricochet-irc) - -if (DEFINED ENV{RICOCHET_REFRESH_VERSION}) - add_compile_definitions(TEGO_VERSION=$ENV{RICOCHET_REFRESH_VERSION}) -endif () - -target_sources(ricochet-irc PRIVATE - main.cpp ${RICOCHET_QML_RES} ${RICOCHET_QM_RES} +add_executable( + ricochet-irc + main.cpp + utils/Useful.h + utils/Settings.cpp + utils/Settings.h + libtego_callbacks.cpp + shims/UserIdentity.h + shims/ContactsManager.cpp + shims/TorCommand.h + shims/UserIdentity.cpp + shims/ContactUser.h + shims/ContactsManager.h + shims/OutgoingContactRequest.h + shims/ContactIDValidator.h + shims/TorControl.h + shims/TorManager.h + shims/TorCommand.cpp + shims/TorControl.cpp + shims/IncomingContactRequest.h + shims/OutgoingContactRequest.cpp + shims/ConversationModel.cpp + shims/ContactIDValidator.cpp + shims/IncomingContactRequest.cpp + shims/ContactUser.cpp + shims/ConversationModel.h + shims/TorManager.cpp + libtego_callbacks.hpp IrcChannel.cpp + IrcChannel.h IrcConnection.cpp + IrcConnection.h + IrcConstants.h IrcServer.cpp + IrcServer.h IrcUser.cpp + IrcUser.h RicochetIrcServer.cpp + RicochetIrcServer.h RicochetIrcServerTask.cpp - # Not nice, but we can't link against libtego_ui - # if we want to avoid GUI dependencies: - ../libtego_ui/libtego_callbacks.cpp - ../libtego_ui/shims/ContactIDValidator.cpp - ../libtego_ui/shims/ContactsManager.cpp - ../libtego_ui/shims/ContactUser.cpp - ../libtego_ui/shims/ConversationModel.cpp - ../libtego_ui/shims/IncomingContactRequest.cpp - ../libtego_ui/shims/OutgoingContactRequest.cpp - ../libtego_ui/shims/TorCommand.cpp - ../libtego_ui/shims/TorControl.cpp - ../libtego_ui/shims/TorManager.cpp - ../libtego_ui/shims/UserIdentity.cpp - ../libtego_ui/utils/Settings.cpp -) -if (STATIC_QT) - include(qmake_static) - target_generate_static_qml_plugins(ricochet-irc) - target_generate_static_qt_plugins(ricochet-irc) -endif () + RicochetIrcServerTask.h) target_precompile_headers(ricochet-irc PRIVATE precomp.hpp) @@ -94,22 +139,26 @@ setup_compiler(ricochet-irc) target_compile_features(ricochet-irc PRIVATE cxx_std_20) -target_link_libraries(ricochet-irc PUBLIC tego) +# Since ricochet-refresh includes libtego_callbacks.hpp as a system header file, we export the include directory twice, +# once as local, once as system TODO: perhaps there's a cleaner way to go about this +target_include_directories(ricochet-irc PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(ricochet-irc SYSTEM PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -# Ugly, but we need bits of libtego_ui without linking to it, -# in order to avoid GUI dependencies. -target_include_directories(ricochet-irc PUBLIC ../libtego_ui/) +target_link_libraries(ricochet-irc PUBLIC tego) if (NOT USE_SUBMODULE_FMT) find_package(fmt REQUIRED) endif () target_link_libraries(ricochet-irc PRIVATE fmt::fmt-header-only) -target_link_libraries(ricochet-irc PRIVATE OpenSSL::Crypto) +# QT target_link_libraries( ricochet-irc PRIVATE Qt${QT_VERSION_MAJOR}::Core - Qt${QT_VERSION_MAJOR}::Network) + Qt${QT_VERSION_MAJOR}::Widgets + Qt${QT_VERSION_MAJOR}::Network + Qt${QT_VERSION_MAJOR}::Qml + Qt${QT_VERSION_MAJOR}::Quick) if (APPLE) target_link_libraries(ricochet-irc PRIVATE Qt${QT_VERSION_MAJOR}::MacExtras) endif () @@ -117,17 +166,3 @@ endif () if ("${CMAKE_BUILD_TYPE}" MATCHES "Rel.*" OR "${CMAKE_BUILD_TYPE}" STREQUAL "MinSizeRel") target_compile_definitions(ricochet-irc PRIVATE QT_NO_DEBUG_OUTPUT QT_NO_WARNING_OUTPUT) endif () - -# Linux / Cygwin -if (UNIX) - # Again, not sure if this needs to be UNIX AND NOT WIN32, or if we should - # install to /bin on Cygwin like it does now - install(TARGETS ricochet-irc DESTINATION usr/bin) -endif () - -# Move our final binary to a bin dir inside the output dir. This makes it -# easier for integration with ricochet-build -set_target_properties(ricochet-irc - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/ricochet-irc/" -) diff --git a/src/irc/IrcConnection.cpp b/src/irc/IrcConnection.cpp index 8ed99108..abc52a42 100644 --- a/src/irc/IrcConnection.cpp +++ b/src/irc/IrcConnection.cpp @@ -237,6 +237,7 @@ void IrcConnection::handle_PASS(QList params) } if(!have_pass) { + // TODO: constant time comparison if(password == params[0]) { have_pass = true; diff --git a/src/irc/IrcUser.h b/src/irc/IrcUser.h new file mode 100644 index 00000000..e9a78c89 --- /dev/null +++ b/src/irc/IrcUser.h @@ -0,0 +1,60 @@ +/* QtLocalIRCD - part of https://github.com/wfr/ricochet-irc/ + * Copyright (C) 2016, Wolfgang Frisch + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the names of the copyright owners nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#ifndef IRCUSER_H +#define IRCUSER_H + +#include + +class IrcServer; + +class IrcUser : public QObject +{ + Q_OBJECT +public: + explicit IrcUser(QObject *ircserver = 0); + + QString nick, user, hostname, realname; + virtual QString getPrefix(); + +signals: + void privmsg(IrcUser *user, const QString& msgtarget, const QString& text); + void joined(IrcUser* user, const QString& channel); + +public slots: + +protected: + IrcServer *ircserver; + +private: +}; + +#endif // IRCUSER_H diff --git a/src/irc/RicochetIrcServer.cpp b/src/irc/RicochetIrcServer.cpp index 101c6722..4e845f52 100644 --- a/src/irc/RicochetIrcServer.cpp +++ b/src/irc/RicochetIrcServer.cpp @@ -68,10 +68,6 @@ RicochetIrcServer::~RicochetIrcServer() */ void RicochetIrcServer::initRicochet() { - connect(shims::TorManager::torManager, - &shims::TorManager::configurationNeededChanged, - this, - &RicochetIrcServer::torConfigurationNeededChanged); connect(shims::TorManager::torManager, &shims::TorManager::runningChanged, this, @@ -80,6 +76,10 @@ void RicochetIrcServer::initRicochet() &shims::TorManager::errorChanged, this, &RicochetIrcServer::torErrorChanged); + connect(shims::TorControl::torControl, + &shims::TorControl::statusChanged, + this, + &RicochetIrcServer::onTorControlStatusChanged); auto userIdentity = shims::UserIdentity::userIdentity; auto contactsManager = shims::UserIdentity::userIdentity->getContacts(); @@ -171,35 +171,14 @@ void RicochetIrcServer::stopRicochet() shims::TorControl::torControl->setConfiguration(conf); } - -/** - * @brief RicochetIrcServer::torConfigurationNeededChanged Hardcoded Tor configuration - * - * BUG: This slot is currently never called because the signal in TorManager - * is emitted in its constructor, before this class is instantiated. - */ -void RicochetIrcServer::torConfigurationNeededChanged() -{ -// auto& torControl = shims::TorControl::torControl; -// qDebug() << "==== IRC torConfigurationNeededChanged() ===="; -// QVariantMap conf = { -// { QStringLiteral("disableNetwork"), 0 }, -// { QStringLiteral("bridges"), QStringList() } -// }; -// torControl->setConfiguration(conf); -// auto command = qobject_cast(torControl->setConfiguration(conf)); -// QObject::connect(command, &shims::TorControlCommand::finished, this, &RicochetIrcServer::torConfigurationFinished); -} - - void RicochetIrcServer::torConfigurationFinished() { - qDebug() << "==== IRC torConfigurationFinished() ===="; + qDebug() << "=== torConfigurationFinished"; - auto& torControl = shims::TorControl::torControl; - if (torControl->hasOwnership()) { - torControl->saveConfiguration(); - } + //auto& torControl = shims::TorControl::torControl; + //if (torControl->hasOwnership()) { + // torControl->saveConfiguration(); + //} } @@ -208,24 +187,18 @@ void RicochetIrcServer::torRunningChanged() { auto& torManager = shims::TorManager::torManager; auto& torControl = shims::TorControl::torControl; - qDebug() << "==== IRC torRunningChanged: " << torManager->running(); + qDebug() << "=== torRunningChanged " << torManager->running(); - if (torManager->configurationNeeded()) { - if (torManager->running() == "Yes") { - QVariantMap conf = { - { QStringLiteral("disableNetwork"), 0 }, - { QStringLiteral("bridges"), QStringList() } - }; - torControl->setConfiguration(conf); - torControl->saveConfiguration(); - } - } - - if (torManager->running() == "Yes") { - qDebug() << "=== Tor ready ==="; - QString topic = userIdentity->contactID(); - getChannel(control_channel_name)->setTopic(ricochet_user, topic); - } + // //if (torManager->configurationNeeded()) { + // if (torManager->running() == "Yes") { + // QVariantMap conf = { + // { QStringLiteral("disableNetwork"), 0 }, + // { QStringLiteral("bridges"), QStringList() } + // }; + // torControl->setConfiguration(conf); + // // torControl->saveConfiguration(); + // } + // //} if (clients.count() > 0) { auto contactsManager = userIdentity->getContacts(); @@ -243,6 +216,18 @@ void RicochetIrcServer::torErrorChanged() { qDebug() << "=== NOT-IMPLEMENTED: IRC torErrorChanged() ==="; } +void RicochetIrcServer::onTorControlStatusChanged(int status) { + qDebug() << "=== onTorControlStatusChanged" << status; + auto& userIdentity = shims::UserIdentity::userIdentity; + switch (status) { + case shims::TorControl::Status::Connected: + qDebug() << "=== Tor connected"; + QString topic = userIdentity->contactID(); + getChannel(control_channel_name)->setTopic(ricochet_user, topic); + break; + } +} + void RicochetIrcServer::torLogMessage(const QString &message) { (void)message; qDebug() << "=== NOT-IMPLEMENTED: IRC torLogMessage() ==="; @@ -429,7 +414,7 @@ void RicochetIrcServer::cmdHelp() echo(QStringLiteral("| _ (_)__ ___ __| |_ ___| |_ |_ _| _ \\/ __| __ _|__ /")); echo(QStringLiteral("| / / _/ _ \\/ _| ' \\/ -_) _| | || / (__ \\ V /|_ \\")); echo(QStringLiteral("|_|_\\_\\__\\___/\\__|_||_\\___|\\__| |___|_|_\\\\___| \\_/|___/")); - echo(QStringLiteral("%1, based on libtego 3.0.11") + echo(QStringLiteral("%1, based on libtego_ui 3.0.29") .arg(QCoreApplication::applicationVersion())); echo(QLatin1String("")); echo(QStringLiteral("COMMANDS:")); @@ -470,7 +455,8 @@ void RicochetIrcServer::cmdAdd(const QStringList& args) message.append(QStringLiteral(" %1").arg(args[i])); } shims::ContactIDValidator contactIdValidator; - if(contactIdValidator.isValidID(id)) + int pos; + if(contactIdValidator.validate(id, pos) == QValidator::Acceptable) { echo(QStringLiteral("sending contact request to user `%1` with id: %2 with message: %3").arg(nickname).arg(id).arg(message)); // TODO: my nickname diff --git a/src/irc/RicochetIrcServer.h b/src/irc/RicochetIrcServer.h index 9572ea64..3ead917b 100644 --- a/src/irc/RicochetIrcServer.h +++ b/src/irc/RicochetIrcServer.h @@ -65,8 +65,8 @@ public slots: const QString getWelcomeMessage() override; private slots: - void torConfigurationNeededChanged(); void torConfigurationFinished(); + void onTorControlStatusChanged(int status); void torLogMessage(const QString &message); void torRunningChanged(); void torErrorChanged(); diff --git a/src/irc/RicochetIrcServerTask.cpp b/src/irc/RicochetIrcServerTask.cpp index 9a8ad9fe..33193690 100644 --- a/src/irc/RicochetIrcServerTask.cpp +++ b/src/irc/RicochetIrcServerTask.cpp @@ -1,7 +1,7 @@ #include "RicochetIrcServerTask.h" #include "RicochetIrcServer.h" -#include "ui/ContactsModel.h" +// #include "ui/ContactsModel.h" #include "utils/Settings.h" @@ -17,6 +17,7 @@ #include "shims/IncomingContactRequest.h" #include +#include RicochetIrcServerTask::RicochetIrcServerTask(QCoreApplication* app) : QObject(app), irc_server(nullptr) @@ -24,6 +25,7 @@ RicochetIrcServerTask::RicochetIrcServerTask(QCoreApplication* app) SettingsObject settings; uint16_t port = static_cast(settings.read("irc.port", 6667).toInt()); QString password = settings.read("irc.password", QLatin1String("")).toString(); + assert(password.length()); irc_server = new RicochetIrcServer(this, port, password); } @@ -46,6 +48,7 @@ void RicochetIrcServerTask::run() std::cout << std::endl; std::cout << "### WeeChat client setup:" << std::endl; std::cout << "/server add ricochet 127.0.0.1/" << irc_server->port() << std::endl; + std::cout << "/set irc.server.ricochet.tls \"off\"" << std::endl; std::cout << "/set irc.server.ricochet.password \"" << irc_server->password().toUtf8().data() << "\"" << std::endl; std::cout << "/set logger.level.irc.ricochet 0" << std::endl; std::cout << std::endl; diff --git a/src/irc/RicochetIrcServerTask.h b/src/irc/RicochetIrcServerTask.h index 18ae2dde..8e32138e 100644 --- a/src/irc/RicochetIrcServerTask.h +++ b/src/irc/RicochetIrcServerTask.h @@ -1,6 +1,7 @@ #pragma once class RicochetIrcServer; +class QCoreApplication; class RicochetIrcServerTask : public QObject { diff --git a/src/irc/libtego_callbacks.cpp b/src/irc/libtego_callbacks.cpp new file mode 100644 index 00000000..2feab7a7 --- /dev/null +++ b/src/irc/libtego_callbacks.cpp @@ -0,0 +1,676 @@ +#include "utils/Settings.h" +#include "utils/Useful.h" +#include "shims/TorControl.h" +#include "shims/TorManager.h" +#include "shims/UserIdentity.h" +#include "shims/ConversationModel.h" +#include "shims/OutgoingContactRequest.h" + +namespace +{ + constexpr int consumeInterval = 10; + + // this holds a callback which can be called and then deletes the underlying data + // replaces std::function beause std::function cannot be move constructed >:[ + class run_once_task + { + public: + run_once_task() = default; + run_once_task(run_once_task&& that) + : run_once_task() + { + *this = std::move(that); + } + + // no copying allowed + run_once_task(const run_once_task&) = delete; + run_once_task& operator=(run_once_task const&) = delete; + + // ensure move does not overwrite existing callback data + run_once_task& operator=(run_once_task&& that) + { + Q_ASSERT(exec == nullptr && callable == nullptr); + exec = that.exec; + callable = that.callable; + + that.exec = nullptr; + that.callable = nullptr; + + return *this; + } + + template + run_once_task(LAMBDA&& lambda) + { + // convertible to raw ptr + if constexpr (std::is_convertible::value) + { + exec = [](run_once_task* self) -> void + { + auto fn = reinterpret_cast(self->callable); + fn(); + + self->exec = nullptr; + self->callable = nullptr; + }; + callable = reinterpret_cast(static_cast(lambda)); + } + // otherwise make a heap copy + else + { + exec = [](run_once_task* self) -> void + { + auto fn = reinterpret_cast(self->callable); + (*fn)(); + delete fn; + + self->exec = nullptr; + self->callable = nullptr; + }; + callable = new LAMBDA(std::move(lambda)); + } + } + + // just ensure we're not messing anything up + ~run_once_task() + { + Q_ASSERT(exec == nullptr && callable == nullptr); + } + + + void operator()() + { + exec(this); + } + + private: + void(*exec)(run_once_task*) = nullptr; + void* callable = nullptr; + }; + + // data + std::vector taskQueue; + std::mutex taskQueueLock; + + void consume_tasks() + { + // get sole access to the task queue + static decltype(taskQueue) localTaskQueue; + { + std::lock_guard lock(taskQueueLock); + std::swap(taskQueue, localTaskQueue); + } + + // consume all of our tasks + for(auto& task : localTaskQueue) + { + try + { + task(); + } + catch(std::exception& ex) + { + qDebug() << "Exception thrown from task: " << ex.what(); + } + } + + // clear out our queue + localTaskQueue.clear(); + + // schedule us to run again + QTimer::singleShot(consumeInterval, &consume_tasks); + } + + template + void push_task(FUNC&& func) + { + // acquire lock on the queue and push our received functor + std::lock_guard lock(taskQueueLock); + taskQueue.push_back(std::move(func)); + } + + QString serviceIdToContactId(const QString& serviceId) + { + return QStringLiteral("ricochet:%1").arg(serviceId); + } + + QString tegoUserIdToServiceId(const tego_user_id_t* user) + { + std::unique_ptr serviceId; + tego_user_id_get_v3_onion_service_id(user, tego::out(serviceId), tego::throw_on_error()); + + char serviceIdRaw[TEGO_V3_ONION_SERVICE_ID_SIZE] = {0}; + tego_v3_onion_service_id_to_string(serviceId.get(), serviceIdRaw, sizeof(serviceIdRaw), tego::throw_on_error()); + + auto contactId = QString::fromUtf8(serviceIdRaw, TEGO_V3_ONION_SERVICE_ID_LENGTH); + return contactId; + } + + // converts the our tego_user_id_t to ricochet's contactId in the form ricochet:serviceidserviceidserviceid... + QString tegoUserIdToContactId(const tego_user_id_t* user) + { + return serviceIdToContactId(tegoUserIdToServiceId(user)); + } + + shims::ContactUser* contactUserFromContactId(const QString& contactId) + { + auto userIdentity = shims::UserIdentity::userIdentity; + auto contactsManager = userIdentity->getContacts(); + + auto contactUser = contactsManager->getShimContactByContactId(contactId); + return contactUser; + } + + // + // libtego callbacks + // + + void on_tor_error_occurred( + tego_context_t*, + tego_tor_error_origin_t origin, + const tego_error_t* error) + { + // route the error message to the appropriate component + QString errorMsg = tego_error_get_message(error); + logger::println("tor error : {}", errorMsg); + push_task([=]() -> void + { + switch(origin) + { + case tego_tor_error_origin_control: + { + shims::TorControl::torControl->setErrorMessage(errorMsg); + } + break; + case tego_tor_error_origin_manager: + { + shims::TorManager::torManager->setErrorMessage(errorMsg); + } + break; + } + }); + } + + void on_update_tor_daemon_config_succeeded( + tego_context_t*, + tego_bool_t success) + { + push_task([=]() -> void + { + logger::println("tor daemon config succeeded : {}", success); + auto torControl = shims::TorControl::torControl; + if (torControl->m_setConfigurationCommand != nullptr) + { + torControl->m_setConfigurationCommand->onFinished(success); + torControl->m_setConfigurationCommand = nullptr; + } + }); + } + + void on_tor_control_status_changed( + tego_context_t*, + tego_tor_control_status_t status) + { + push_task([=]() -> void + { + logger::println("new control status : {}", status); + shims::TorControl::torControl->setStatus(static_cast(status)); + }); + } + + void on_tor_process_status_changed( + tego_context_t*, + tego_tor_process_status_t status) + { + push_task([=]() -> void + { + logger::println("new process status : {}", status); + auto torManager = shims::TorManager::torManager; + switch(status) + { + case tego_tor_process_status_running: + torManager->setRunning("Yes"); + break; + case tego_tor_process_status_external: + torManager->setRunning("External"); + break; + default: + torManager->setRunning("No"); + break; + } + }); + } + + void on_tor_network_status_changed( + tego_context_t*, + tego_tor_network_status_t status) + { + push_task([=]() -> void + { + logger::println("new network status : {}", status); + auto torControl = shims::TorControl::torControl; + switch(status) + { + case tego_tor_network_status_unknown: + torControl->setTorStatus(shims::TorControl::TorUnknown); + break; + case tego_tor_network_status_ready: + torControl->setTorStatus(shims::TorControl::TorReady); + break; + case tego_tor_network_status_offline: + torControl->setTorStatus(shims::TorControl::TorOffline); + break; + } + }); + } + + void on_tor_bootstrap_status_changed( + tego_context_t*, + int32_t progress, + tego_tor_bootstrap_tag_t tag) + { + push_task([=]() -> void + { + logger::println("bootstrap status : {{ progress : {}, tag : {} }}", progress, static_cast(tag)); + + auto tagSummary = tego_tor_bootstrap_tag_to_summary( + tag, + tego::throw_on_error()); + + auto torControl = shims::TorControl::torControl; + torControl->setBootstrapStatus(progress, tag, QString(tagSummary)); + + }); + } + + void on_tor_log_received( + tego_context_t*, + const char* message, + size_t messageLength) + { + auto messageString = QString::fromUtf8(message, safe_cast(messageLength)); + push_task([=]()-> void + { + auto torManager = shims::TorManager::torManager; + emit torManager->logMessage(messageString); + }); + } + + void on_host_onion_service_state_changed( + tego_context_t*, + tego_host_onion_service_state_t state) + { + push_task([=]() -> void + { + auto userIdentity = shims::UserIdentity::userIdentity; + userIdentity->setHostOnionServiceState(state); + }); + } + + void on_chat_request_received( + tego_context_t*, + const tego_user_id_t* userId, + const char* message, + size_t messageLength) + { + logger::println("Received chat request from {}", tegoUserIdToServiceId(userId)); + logger::println("Message : {}", message); + + auto hostname = tegoUserIdToServiceId(userId) + ".onion"; + auto messageString = QString::fromUtf8(message, safe_cast(messageLength)); + + push_task([=]() -> void + { + auto userIdentity = shims::UserIdentity::userIdentity; + userIdentity->createIncomingContactRequest(hostname, messageString); + }); + } + + void on_chat_request_response_received( + tego_context_t*, + const tego_user_id_t* userId, + tego_bool_t requestAccepted) + { + logger::trace(); + + auto serviceId = tegoUserIdToServiceId(userId); + + push_task([=]() -> void + { + auto userIdentity = shims::UserIdentity::userIdentity; + auto contactsManager = userIdentity->getContacts(); + auto contact = contactsManager->getShimContactByContactId(serviceIdToContactId(serviceId)); + auto outgoingContactRequest = contact->contactRequest(); + + logger::trace(); + + if (requestAccepted) + { + outgoingContactRequest->setAccepted(); + } + else + { + outgoingContactRequest->setRejected(); + contact->setStatus(shims::ContactUser::RequestRejected); + } + }); + } + + void on_user_status_changed( + tego_context_t*, + const tego_user_id_t* userId, + tego_user_status_t status) + { + logger::trace(); + auto serviceId = tegoUserIdToServiceId(userId); + + logger::println("user status changed -> service id : {}, status : {}", serviceId, static_cast(status)); + + push_task([=]() -> void + { + auto userIdentity = shims::UserIdentity::userIdentity; + auto contactsManager = userIdentity->getContacts(); + auto contact = contactsManager->getShimContactByContactId(serviceIdToContactId(serviceId)); + if (contact != nullptr) + { + auto conversation = contact->conversation(); + switch(status) + { + case tego_user_status_online: + contact->setStatus(shims::ContactUser::Online); + contactsManager->setContactStatus(contact, shims::ContactUser::Online); + conversation->setStatus(shims::ContactUser::Online); + break; + case tego_user_status_offline: + contact->setStatus(shims::ContactUser::Offline); + contactsManager->setContactStatus(contact, shims::ContactUser::Offline); + conversation->setStatus(shims::ContactUser::Offline); + break; + default: + break; + } + } + }); + } + + void on_message_received( + tego_context_t*, + const tego_user_id_t* sender, + tego_time_t timestamp, + tego_message_id_t messageId, + const char* message, + size_t messageLength) + { + auto contactId = tegoUserIdToContactId(sender); + auto messageString = QString::fromUtf8(message, safe_cast(messageLength)); + + push_task([=]() -> void + { + auto contactUser = contactUserFromContactId(contactId); + Q_ASSERT(contactUser != nullptr); + auto conversationModel = contactUser->conversation(); + Q_ASSERT(conversationModel != nullptr); + + conversationModel->messageReceived(messageId, QDateTime::fromMSecsSinceEpoch(safe_cast(timestamp)), messageString); + }); + } + + void on_message_acknowledged( + tego_context_t*, + const tego_user_id_t* userId, + tego_message_id_t messageId, + tego_bool_t messageAccepted) + { + logger::trace(); + logger::println(" userId : {}", static_cast(userId)); + logger::println(" messageId : {}", messageId); + logger::println(" messageAccepted : {}", messageAccepted); + + QString contactId = tegoUserIdToContactId(userId); + push_task([=]() -> void + { + logger::trace(); + auto contactsManager = shims::UserIdentity::userIdentity->getContacts(); + auto contactUser = contactsManager->getShimContactByContactId(contactId); + auto conversationModel = contactUser->conversation(); + conversationModel->messageAcknowledged(messageId, static_cast(messageAccepted)); + }); + } + + void on_file_transfer_request_received( + tego_context_t*, + tego_user_id_t const* sender, + tego_file_transfer_id_t id, + char const* fileName, + size_t fileNameLength, + tego_file_size_t fileSize, + tego_file_hash_t const* fileHash) + { + auto contactId = tegoUserIdToContactId(sender); + QString fileNameCopy = QString::fromUtf8(fileName, safe_cast(fileNameLength)); + auto hashStr = tego::to_string(fileHash); + + push_task([=,fileName=std::move(fileNameCopy)]() -> void + { + auto contactUser = contactUserFromContactId(contactId); + Q_ASSERT(contactUser != nullptr); + auto conversationModel = contactUser->conversation(); + Q_ASSERT(conversationModel != nullptr); + + conversationModel->fileTransferRequestReceived(id, fileName, QString::fromStdString(hashStr), fileSize); + }); + } + + void on_file_transfer_request_acknowledged( + tego_context_t*, + tego_user_id_t const* receiver, + tego_file_transfer_id_t id, + tego_bool_t ack) + { + auto contactId = tegoUserIdToContactId(receiver); + + push_task([=]() -> void + { + auto contactUser = contactUserFromContactId(contactId); + Q_ASSERT(contactUser != nullptr); + auto conversationModel = contactUser->conversation(); + Q_ASSERT(conversationModel != nullptr); + + conversationModel->fileTransferRequestAcknowledged(id, ack); + }); + } + + void on_file_transfer_request_response_received( + tego_context_t*, + tego_user_id_t const* receiver, + tego_file_transfer_id_t id, + tego_file_transfer_response_t response) + { + auto contactId = tegoUserIdToContactId(receiver); + + push_task([=]() -> void + { + auto contactUser = contactUserFromContactId(contactId); + Q_ASSERT(contactUser != nullptr); + auto conversationModel = contactUser->conversation(); + Q_ASSERT(conversationModel != nullptr); + + conversationModel->fileTransferRequestResponded(id, response); + }); + } + + void on_file_transfer_progress( + tego_context_t*, + const tego_user_id_t* userId, + tego_file_transfer_id_t id, + tego_file_transfer_direction_t direction, + tego_file_size_t bytesComplete, + tego_file_size_t bytesTotal) + { + auto contactId = tegoUserIdToContactId(userId); + + push_task([=]() -> void + { + auto contactUser = contactUserFromContactId(contactId); + Q_ASSERT(contactUser != nullptr); + auto conversationModel = contactUser->conversation(); + Q_ASSERT(conversationModel != nullptr); + + conversationModel->fileTransferRequestProgressUpdated(id, bytesComplete); + }); + + + logger::println( + "File Progress id : {}, direction : {}, transferred : {} bytes, total : {} bytes", + id, + direction == tego_file_transfer_direction_sending ? "sending" : "receiving", + bytesComplete, + bytesTotal); + } + + void on_file_transfer_complete( + tego_context_t*, + const tego_user_id_t* userId, + tego_file_transfer_id_t id, + tego_file_transfer_direction_t, + tego_file_transfer_result_t result) + { + auto contactId = tegoUserIdToContactId(userId); + + push_task([=]() -> void + { + auto contactUser = contactUserFromContactId(contactId); + Q_ASSERT(contactUser != nullptr); + auto conversationModel = contactUser->conversation(); + Q_ASSERT(conversationModel != nullptr); + + conversationModel->fileTransferRequestCompleted(id, result); + }); + } + + void on_new_identity_created( + tego_context_t*, + const tego_ed25519_private_key_t* privateKey) + { + // convert privateKey to KeyBlob + char rawKeyBlob[TEGO_ED25519_KEYBLOB_SIZE] = {0}; + tego_ed25519_keyblob_from_ed25519_private_key( + rawKeyBlob, + sizeof(rawKeyBlob), + privateKey, + tego::throw_on_error()); + + QString keyBlob(rawKeyBlob); + + push_task([=]() -> void + { + SettingsObject so(QStringLiteral("identity")); + so.write("privateKey", keyBlob); + }); + } +} + +void init_libtego_callbacks(tego_context_t* context) +{ + // start triggering our consume queue + QTimer::singleShot(consumeInterval, &consume_tasks); + + // + // register each of our callbacks with libtego + // + + tego_context_set_tor_error_occurred_callback( + context, + &on_tor_error_occurred, + tego::throw_on_error()); + + tego_context_set_update_tor_daemon_config_succeeded_callback( + context, + &on_update_tor_daemon_config_succeeded, + tego::throw_on_error()); + + tego_context_set_tor_control_status_changed_callback( + context, + &on_tor_control_status_changed, + tego::throw_on_error()); + + tego_context_set_tor_process_status_changed_callback( + context, + &on_tor_process_status_changed, + tego::throw_on_error()); + + tego_context_set_tor_network_status_changed_callback( + context, + &on_tor_network_status_changed, + tego::throw_on_error()); + + tego_context_set_tor_bootstrap_status_changed_callback( + context, + &on_tor_bootstrap_status_changed, + tego::throw_on_error()); + + tego_context_set_tor_log_received_callback( + context, + &on_tor_log_received, + tego::throw_on_error()); + + tego_context_set_host_onion_service_state_changed_callback( + context, + &on_host_onion_service_state_changed, + tego::throw_on_error()); + + tego_context_set_chat_request_received_callback( + context, + &on_chat_request_received, + tego::throw_on_error()); + + tego_context_set_chat_request_response_received_callback( + context, + &on_chat_request_response_received, + tego::throw_on_error()); + + tego_context_set_file_transfer_request_received_callback( + context, + &on_file_transfer_request_received, + tego::throw_on_error()); + + tego_context_set_file_transfer_request_acknowledged_callback( + context, + &on_file_transfer_request_acknowledged, + tego::throw_on_error()); + + tego_context_set_file_transfer_request_response_received_callback( + context, + &on_file_transfer_request_response_received, + tego::throw_on_error()); + + tego_context_set_file_transfer_progress_callback( + context, + &on_file_transfer_progress, + tego::throw_on_error()); + + tego_context_set_file_transfer_complete_callback( + context, + &on_file_transfer_complete, + tego::throw_on_error()); + + tego_context_set_user_status_changed_callback( + context, + &on_user_status_changed, + tego::throw_on_error()); + + tego_context_set_message_received_callback( + context, + &on_message_received, + tego::throw_on_error()); + + tego_context_set_message_acknowledged_callback( + context, + &on_message_acknowledged, + tego::throw_on_error()); + + tego_context_set_new_identity_created_callback( + context, + &on_new_identity_created, + tego::throw_on_error()); +} diff --git a/src/irc/libtego_callbacks.hpp b/src/irc/libtego_callbacks.hpp new file mode 100644 index 00000000..eac1fa17 --- /dev/null +++ b/src/irc/libtego_callbacks.hpp @@ -0,0 +1,3 @@ +#pragma once + +void init_libtego_callbacks(tego_context_t* context); \ No newline at end of file diff --git a/src/irc/main.cpp b/src/irc/main.cpp index 3d42a7cc..5f061086 100644 --- a/src/irc/main.cpp +++ b/src/irc/main.cpp @@ -1,84 +1,25 @@ -/* Ricochet-IRC - https://github.com/wfr/ricochet-irc/ - * Wolfgang Frisch - * - * Derived from: - * Ricochet Refresh - https://ricochetrefresh.net/ - * Copyright (C) 2019, Blueprint For Free Speech - * - * Ricochet - https://ricochet.im/ - * Copyright (C) 2014, John Brooks - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following disclaimer - * in the documentation and/or other materials provided with the - * distribution. - * - * * Neither the names of the copyright owners nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#include "utils/Settings.h" - -#include - -// shim replacements +#include +#include +#include +#include + +#include "libtego_callbacks.hpp" + #include "shims/TorControl.h" #include "shims/TorManager.h" #include "shims/UserIdentity.h" + #include "RicochetIrcServer.h" #include "RicochetIrcServerTask.h" -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -bool verbose_output = false; - +bool verbose_output = true; +static QString randomPassword(size_t len = 32); +static void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg); static bool initSettings(SettingsFile *settings, QLockFile **lockFile, QString &errorMessage); -static void initTranslation(); -static QString randomPassword(size_t len = 14); -void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg); -int main(int argc, char *argv[]) try -{ - /* Disable rwx memory. - This will also ensure full PAX/Grsecurity protections. */ - qputenv("QV4_FORCE_INTERPRETER", "1"); - qputenv("QT_ENABLE_REGEXP_JIT", "0"); +int main(int argc, char *argv[]) +{ QCoreApplication a(argc, argv); qInstallMessageHandler(myMessageOutput); @@ -91,19 +32,24 @@ int main(int argc, char *argv[]) try init_libtego_callbacks(tegoContext); + a.setApplicationVersion(QLatin1String(TEGO_VERSION_STR)); + qDebug() << "tego version:" << TEGO_VERSION_STR; + QScopedPointer settings(new SettingsFile); SettingsObject::setDefaultFile(settings.data()); QString error; QLockFile *lock = 0; if (!initSettings(settings.data(), &lock, error)) { - qCritical() << error; + if (error.isEmpty()) { + return 0; + } + qCritical() << "Ricochet Error:" << error; return 1; } QScopedPointer lockFile(lock); - initTranslation(); // init our tor shims shims::TorControl::torControl = new shims::TorControl(tegoContext); @@ -124,115 +70,136 @@ int main(int argc, char *argv[]) try tego_context_start_tor(tegoContext, launchConfig.get(), tego::throw_on_error()); } + /* Identities */ // init our shims shims::UserIdentity::userIdentity = new shims::UserIdentity(tegoContext); - auto contactsManager = shims::UserIdentity::userIdentity->getContacts(); - - auto privateKeyString = SettingsObject("identity").read("privateKey"); - if (privateKeyString.isEmpty()) - { - tego_context_start_service( - tegoContext, - nullptr, - nullptr, - nullptr, - 0, - tego::throw_on_error()); - } - else - { - // construct privatekey from privateKey keyblob - std::unique_ptr privateKey; - auto keyBlob = privateKeyString.toUtf8(); - - tego_ed25519_private_key_from_ed25519_keyblob( - tego::out(privateKey), - keyBlob.data(), - static_cast(keyBlob.size()), - tego::throw_on_error()); - // load all of our user objects - std::vector userIds; - std::vector userTypes; - auto userIdCleanup = tego::make_scope_exit([&]() -> void - { - std::for_each(userIds.begin(), userIds.end(), &tego_user_id_delete); - }); - - // map strings saved in json with tego types - const static QMap stringToUserType = - { - {QString("allowed"), tego_user_type_allowed}, - {QString("requesting"), tego_user_type_requesting}, - {QString("blocked"), tego_user_type_blocked}, - {QString("pending"), tego_user_type_pending}, - {QString("rejected"), tego_user_type_rejected}, - }; - - auto usersJson = SettingsObject("users").data(); - for(auto it = usersJson.begin(); it != usersJson.end(); ++it) - { - // get the user's service id - const auto serviceIdString = it.key(); - const auto serviceIdRaw = serviceIdString.toUtf8(); - - std::unique_ptr serviceId; - tego_v3_onion_service_id_from_string( - tego::out(serviceId), - serviceIdRaw.data(), - static_cast(serviceIdRaw.size()), - tego::throw_on_error()); - - std::unique_ptr userId; - tego_user_id_from_v3_onion_service_id( - tego::out(userId), - serviceId.get(), - tego::throw_on_error()); - userIds.push_back(userId.release()); - - // load relevant data - const auto& userData = it.value().toObject(); - auto typeString = userData.value("type").toString(); - - Q_ASSERT(stringToUserType.contains(typeString)); - auto type = stringToUserType.value(typeString); - userTypes.push_back(type); - - if (type == tego_user_type_allowed || - type == tego_user_type_pending || - type == tego_user_type_rejected) - { - const auto nickname = userData.value("nickname").toString(); - auto contact = contactsManager->addContact(serviceIdString, nickname); - switch(type) + // wait until a control connection has been established before attempting + // to send configuration info to the daemon + QObject::connect( + shims::TorControl::torControl, + &shims::TorControl::statusChanged, + [&](int newStatus, int) -> void { + if (newStatus == tego_tor_control_status_connected) { + + // send configuration down to tor daemon + auto networkSettings = SettingsObject().read("tor").toObject(); + shims::TorControl::torControl->setConfiguration(networkSettings); + + // at this point we could configure Tor, + // however we're happy with a default Tor config for now. + shims::TorControl::torControl->beginBootstrap(); + + // start up our onion service + auto privateKeyString = SettingsObject("identity").read("privateKey"); + if (privateKeyString.isEmpty()) { - case tego_user_type_allowed: - contact->setStatus(shims::ContactUser::Offline); - break; - case tego_user_type_pending: - contact->setStatus(shims::ContactUser::RequestPending); - break; - case tego_user_type_rejected: - contact->setStatus(shims::ContactUser::RequestRejected); - break; - default: - break; + tego_context_start_service( + tegoContext, + nullptr, + nullptr, + nullptr, + 0, + tego::throw_on_error()); + } + else + { + auto contactsManager = shims::UserIdentity::userIdentity->getContacts(); + + // construct privatekey from privateKey keyblob + std::unique_ptr privateKey; + auto keyBlob = privateKeyString.toUtf8(); + + tego_ed25519_private_key_from_ed25519_keyblob( + tego::out(privateKey), + keyBlob.data(), + static_cast(keyBlob.size()), + tego::throw_on_error()); + + // load all of our user objects + std::vector userIds; + std::vector userTypes; + auto userIdCleanup = tego::make_scope_exit([&]() -> void + { + std::for_each(userIds.begin(), userIds.end(), &tego_user_id_delete); + }); + + // map strings saved in json with tego types + const static QMap stringToUserType = + { + {QString("allowed"), tego_user_type_allowed}, + {QString("requesting"), tego_user_type_requesting}, + {QString("blocked"), tego_user_type_blocked}, + {QString("pending"), tego_user_type_pending}, + {QString("rejected"), tego_user_type_rejected}, + }; + + auto usersJson = SettingsObject("users").data(); + for(auto it = usersJson.begin(); it != usersJson.end(); ++it) + { + // get the user's service id + const auto serviceIdString = it.key(); + const auto serviceIdRaw = serviceIdString.toUtf8(); + + std::unique_ptr serviceId; + tego_v3_onion_service_id_from_string( + tego::out(serviceId), + serviceIdRaw.data(), + static_cast(serviceIdRaw.size()), + tego::throw_on_error()); + + std::unique_ptr userId; + tego_user_id_from_v3_onion_service_id( + tego::out(userId), + serviceId.get(), + tego::throw_on_error()); + userIds.push_back(userId.release()); + + // load relevant data + const auto& userData = it.value().toObject(); + auto typeString = userData.value("type").toString(); + + Q_ASSERT(stringToUserType.contains(typeString)); + auto type = stringToUserType.value(typeString); + userTypes.push_back(type); + + if (type == tego_user_type_allowed || + type == tego_user_type_pending || + type == tego_user_type_rejected) + { + const auto nickname = userData.value("nickname").toString(); + auto contact = contactsManager->addContact(serviceIdString, nickname); + switch(type) + { + case tego_user_type_allowed: + contact->setStatus(shims::ContactUser::Offline); + break; + case tego_user_type_pending: + contact->setStatus(shims::ContactUser::RequestPending); + break; + case tego_user_type_rejected: + contact->setStatus(shims::ContactUser::RequestRejected); + break; + default: + break; + } + } + } + Q_ASSERT(userIds.size() == userTypes.size()); + const size_t userCount = userIds.size(); + + tego_context_start_service( + tegoContext, + privateKey.get(), + userIds.data(), + userTypes.data(), + userCount, + tego::throw_on_error()); } } - } - Q_ASSERT(userIds.size() == userTypes.size()); - const size_t userCount = userIds.size(); - - tego_context_start_service( - tegoContext, - privateKey.get(), - userIds.data(), - userTypes.data(), - userCount, - tego::throw_on_error()); - } + }); // Start the IRC server auto task = new RicochetIrcServerTask(&a); @@ -242,30 +209,43 @@ int main(int argc, char *argv[]) try return a.exec(); } -catch(std::exception& re) -{ - qDebug() << "Caught Exception: " << re.what(); - return -1; + + +QString randomPassword(size_t len) { + QString result; + const QString alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"); + auto rng = QRandomGenerator::securelySeeded(); + for(size_t i = 0; i < len; i++) { + result.append(alphabet.at(rng.bounded(alphabet.length()))); + } + return result; } -#ifdef Q_OS_MAC -// returns the directory to place the config.ricochet directory on macOS -// no trailing '/' -static QString appBundlePath() + +void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) { - QString path = QApplication::applicationDirPath(); - // if user left the binaries insidie the app bundle - int p = path.lastIndexOf(QLatin1String(".app/")); - if (p >= 0) - { - // just some binaries floating around somewhere - p = path.lastIndexOf(QLatin1Char('/'), p); - path = path.left(p); + QByteArray localMsg = msg.toLocal8Bit(); + const char *file = context.file ? context.file : ""; + const char *function = context.function ? context.function : ""; + switch (type) { + case QtDebugMsg: + if (::verbose_output) { + fprintf(stderr, "%s\n", localMsg.constData()); + } + break; + case QtInfoMsg: + case QtWarningMsg: + fprintf(stderr, "%s\n", localMsg.constData()); + break; + case QtCriticalMsg: + fprintf(stderr, "CRITICAL: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function); + break; + case QtFatalMsg: + fprintf(stderr, "FATAL: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function); + break; } - - return path; } -#endif + // Writes default settings to settings object. Does not care about any // preexisting values, therefore this is best used on a fresh object. @@ -274,9 +254,10 @@ static void loadDefaultSettings(SettingsFile *settings) settings->root()->write("ui.combinedChatWindow", true); } + static bool initSettings(SettingsFile *settings, QLockFile **lockFile, QString &errorMessage) { - /* ricochet-refresh by default loads and saves configuration files from QStandardPaths::AppLocalDataLocation + /* ricochet-refresh by default loads and saves configuration files from QStandardPaths::AppConfigLocation * * Linux: ~/.config/ricochet-refresh * Windows: C:/Users//AppData/Local/ricochet-refresh @@ -284,12 +265,15 @@ static bool initSettings(SettingsFile *settings, QLockFile **lockFile, QString & * * ricochet-refresh can also load configuration files from a custom directory passed in as the first argument */ + QString defaultConfigPath = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); + // Parse command-line arguments QCommandLineParser parser; parser.setApplicationDescription(QCoreApplication::translate("main","Anonymous peer-to-peer instant messaging, IRC gateway")); QCommandLineOption opt_config_path(QStringLiteral("config"), QCoreApplication::translate("main", "Select configuration directory."), - QStringLiteral("config-path")); + QStringLiteral("config-path"), + defaultConfigPath); parser.addOption(opt_config_path); QCommandLineOption opt_irc_port(QStringLiteral("port"), QCoreApplication::translate("irc", "Set IRC server port."), @@ -300,86 +284,25 @@ static bool initSettings(SettingsFile *settings, QLockFile **lockFile, QString & QCoreApplication::translate("irc", "Generate random IRC password.")); parser.addOption(opt_irc_password); QCommandLineOption opt_verbose(QStringList() << "debug" << "verbose", - QCoreApplication::translate("irc", "Verbose output")); + QCoreApplication::translate("irc", "Verbose output")); parser.addOption(opt_verbose); parser.addHelpOption(); parser.addVersionOption(); parser.process(qApp->arguments()); - QString configPath; - const QStringList args = parser.positionalArguments(); - if(args.count() > 0) { + if(parser.positionalArguments().count() > 0) { parser.showHelp(1); + return false; } if (parser.isSet(opt_verbose)) { ::verbose_output = true; } - if(parser.isSet(opt_config_path)) { - configPath = parser.value(opt_config_path); - } else { - // TODO: remove this profile migration after sufficient time has passed (EOY 2021) - auto legacyConfigPath = []() -> QString { - QString configPath; -#ifdef Q_OS_MAC - // if the user has installed it to /Applications - if (qApp->applicationDirPath().contains(QStringLiteral("/Applications"))) { - // ~Library/Application Support/Ricochet/Ricochet-Refresh - configPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/Ricochet/Ricochet-Refresh"); - } else { - configPath = appBundlePath() + QStringLiteral("/config.ricochet"); - } -#else - configPath = qApp->applicationDirPath() + QStringLiteral("/config"); -#endif - return configPath; - }(); - auto v3_0_10ConfigPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); - configPath = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); - - logger::println("configPath : {}", configPath); - logger::println("legacyConfigPath : {}", legacyConfigPath); - bool migrate = false; - - // only put up migration UX when - if (// old path differs from new path - configPath != legacyConfigPath && - // the old path exists - QFile::exists(legacyConfigPath) && - // the new path does not exist - !QFile::exists(configPath)) { - - std::cout << "Ricochet Refresh has detected an existing legacy profile." << std::endl; - std::cout << "Old profile: " << legacyConfigPath.toStdString() << std::endl; - std::cout << "New profile: " << configPath.toStdString() << std::endl; - - if (migrate) { - if(!QDir().rename(legacyConfigPath, configPath)) { - errorMessage = QStringLiteral("Unable to migrate profile"); - return false; - } - std::cout << "migrated to the new profile path: " << configPath.toStdString() << std::endl; - } - // auto migrate if we have a 3.0.10 config but no 3.0.11+ config - } else if (// 3.0.10 path exists - QFile::exists(v3_0_10ConfigPath) && - // but 3.0.11+ path does not - !QFile::exists(configPath)) { - - /// just automatically move the directory - if(!QDir().rename(v3_0_10ConfigPath, configPath)) { - // on failure use the old path - configPath = v3_0_10ConfigPath; - } else { - // use old profile path - configPath = legacyConfigPath; - std::cout << "using the old profile path: " << configPath.toStdString() << std::endl; - } - } - } + // Load/initialize config + QDir dir(parser.value(opt_config_path)); + logger::println("configPath : {}", dir); - QDir dir(configPath); if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) { errorMessage = QStringLiteral("Cannot create directory: %1").arg(dir.path()); return false; @@ -420,6 +343,7 @@ static bool initSettings(SettingsFile *settings, QLockFile **lockFile, QString & loadDefaultSettings(settings); } + // IRC settings if(parser.isSet(opt_irc_port)) { bool ok; int port = parser.value(QStringLiteral("port")).toInt(&ok); @@ -432,84 +356,11 @@ static bool initSettings(SettingsFile *settings, QLockFile **lockFile, QString & qDebug() << "IRC server port is" << port; } - if(parser.isSet(opt_irc_password) || settings->root()->read("irc.password", QStringLiteral("")) == QStringLiteral("")) { + if(parser.isSet(opt_irc_password) + || settings->root()->read("irc.password", QStringLiteral("")) == QStringLiteral("")) { settings->root()->write("irc.password", randomPassword()); } return true; } -static void initTranslation() -{ - QTranslator *translator = new QTranslator; - - bool ok = false; - QString appPath = qApp->applicationDirPath(); - QString resPath = QLatin1String(":/lang/"); - - QLocale locale = QLocale::system(); - if (!qgetenv("RICOCHET_LOCALE").isEmpty()) { - locale = QLocale(QString::fromLatin1(qgetenv("RICOCHET_LOCALE"))); - qDebug() << "Forcing locale" << locale << "from environment" << locale.uiLanguages(); - } - - SettingsObject settings; - QString settingsLanguage(settings.read("ui.language").toString()); - - if (!settingsLanguage.isEmpty()) { - locale = settingsLanguage; - } else { - //write an empty string to get "System default" language selected automatically in preferences - settings.write(QStringLiteral("ui.language"), QString()); - } - - ok = translator->load(locale, QStringLiteral("ricochet"), QStringLiteral("_"), appPath); - if (!ok) - ok = translator->load(locale, QStringLiteral("ricochet"), QStringLiteral("_"), resPath); - - if (ok) { - qApp->installTranslator(translator); - - QTranslator *qtTranslator = new QTranslator; - ok = qtTranslator->load(QStringLiteral("qt_") + locale.name(), QLibraryInfo::location(QLibraryInfo::TranslationsPath)); - if (ok) - qApp->installTranslator(qtTranslator); - else - delete qtTranslator; - } else - delete translator; -} - -QString randomPassword(size_t len) { - QString result; - const QString alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"); - auto rng = QRandomGenerator::securelySeeded(); - for(size_t i = 0; i < len; i++) { - result.append(alphabet.at(rng.bounded(alphabet.length()))); - } - return result; -} - -void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) -{ - QByteArray localMsg = msg.toLocal8Bit(); - const char *file = context.file ? context.file : ""; - const char *function = context.function ? context.function : ""; - switch (type) { - case QtDebugMsg: - if (::verbose_output) { - fprintf(stderr, "%s\n", localMsg.constData()); - } - break; - case QtInfoMsg: - case QtWarningMsg: - fprintf(stderr, "%s\n", localMsg.constData()); - break; - case QtCriticalMsg: - fprintf(stderr, "CRITICAL: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function); - break; - case QtFatalMsg: - fprintf(stderr, "FATAL: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function); - break; - } -} diff --git a/src/irc/pluggables.hpp b/src/irc/pluggables.hpp new file mode 100644 index 00000000..a8222ddb --- /dev/null +++ b/src/irc/pluggables.hpp @@ -0,0 +1,6 @@ +#pragma once +// The contents of this file are populated in ricochet-build with bridge strings that +// are retrieved from tor-browser-build. +const QMap> defaultBridges = {}; +const QString recommendedBridgeType = ""; + diff --git a/src/irc/precomp.hpp b/src/irc/precomp.hpp index 462582bb..70609c52 100644 --- a/src/irc/precomp.hpp +++ b/src/irc/precomp.hpp @@ -1,27 +1,47 @@ +#include + // C headers -// openssl -#include +// standard library +#include // C++ headers #ifdef __cplusplus -// Qt +// standard library +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// fmt +#include +#include -#include -#include +// Qt +#include +#include #include -#include -#include #include +#include #include -#include +#include #include -#include -#include +#include +#include +#include // QtGui +#include #include -#include -#include +#include +#ifdef Q_OS_MAC +# include +#endif // Q_OS_MAC // tego #include diff --git a/src/irc/shims/ContactIDValidator.cpp b/src/irc/shims/ContactIDValidator.cpp new file mode 100644 index 00000000..c59dd2e3 --- /dev/null +++ b/src/irc/shims/ContactIDValidator.cpp @@ -0,0 +1,84 @@ +#include "UserIdentity.h" +#include "ContactIDValidator.h" + +namespace shims +{ + ContactIDValidator::ContactIDValidator(QObject *parent) + : QRegularExpressionValidator(parent) + { + QRegularExpressionValidator::setRegularExpression(QRegularExpression(QStringLiteral("ricochet:([a-z2-7]{56})"))); + } + + void ContactIDValidator::fixup(QString &text) const + { + logger::trace(); + text = text.trimmed().toLower(); + } + + QValidator::State ContactIDValidator::validate(QString &text, int &pos) const + { + fixup(text); + + const auto result = QRegularExpressionValidator::validate(text, pos); + switch(result) { + case QValidator::Acceptable: + if(isValidID(text)) { + if (auto contact = matchingContact(text); contact != nullptr) { + emit matchesContact(contact->getNickname()); + logger::println(" - matches contact"); + return QValidator::Invalid; + } + if (matchesIdentity(text)) { + emit matchesSelf(); + logger::println(" - matches self"); + return QValidator::Invalid; + } + emit acceptable(); + logger::println(" - valid service id!"); + return QValidator::Acceptable; + } else { + emit invalidServiceId(); + logger::println(" - invalid service id!"); + return QValidator::Invalid; + } + case QValidator::Intermediate: + emit intermediate(); + return QValidator::Intermediate; + default: + return result; + } + } + + shims::ContactUser* ContactIDValidator::matchingContact(const QString &text) const + { + auto contactsManager = UserIdentity::userIdentity->getContacts(); + return contactsManager->getShimContactByContactId(text); + } + + bool ContactIDValidator::matchesIdentity(const QString &text) const + { + auto context = UserIdentity::userIdentity->getContext(); + + std::unique_ptr userId; + tego_context_get_host_user_id(context, tego::out(userId), tego::throw_on_error()); + + std::unique_ptr serviceId; + tego_user_id_get_v3_onion_service_id(userId.get(), tego::out(serviceId), tego::throw_on_error()); + + char serviceIdString[TEGO_V3_ONION_SERVICE_ID_SIZE] = {0}; + tego_v3_onion_service_id_to_string(serviceId.get(), serviceIdString, sizeof(serviceIdString), tego::throw_on_error()); + + auto utf8Text = text.mid(tego::static_strlen("ricochet:")).toUtf8(); + auto utf8ServiceId = QByteArray(serviceIdString, TEGO_V3_ONION_SERVICE_ID_LENGTH); + + return utf8Text == utf8ServiceId; + } + + bool ContactIDValidator::isValidID(const QString &serviceID) const + { + auto strippedID = serviceID.mid(tego::static_strlen("ricochet:")).toUtf8(); + bool valid = tego_v3_onion_service_id_string_is_valid(strippedID.constData(), static_cast(strippedID.size()), nullptr) == TEGO_TRUE; + + return valid; + } +} diff --git a/src/irc/shims/ContactIDValidator.h b/src/irc/shims/ContactIDValidator.h new file mode 100644 index 00000000..7f4812cc --- /dev/null +++ b/src/irc/shims/ContactIDValidator.h @@ -0,0 +1,32 @@ +#pragma once + +namespace shims +{ + class ContactUser; + class ContactIDValidator : public QRegularExpressionValidator + { + Q_OBJECT + Q_DISABLE_COPY(ContactIDValidator) + public: + ContactIDValidator(QObject *parent = 0); + + virtual void fixup(QString &text) const; + virtual State validate(QString &text, int &pos) const; + + signals: + // fired when the service id is a new valid contact + void acceptable() const; + // fired when user input is maybe a valid contact + void intermediate() const; + // fired when the service-id matches the regex but is not a valid service id + void invalidServiceId() const; + // fired when the service-id is already a contact + void matchesContact(QString) const; + // fired when the service-id is ourself + void matchesSelf() const; + private: + bool isValidID(const QString &serviceID) const; + shims::ContactUser *matchingContact(const QString &text) const; + bool matchesIdentity(const QString &text) const; + }; +} diff --git a/src/irc/shims/ContactUser.cpp b/src/irc/shims/ContactUser.cpp new file mode 100644 index 00000000..65eed623 --- /dev/null +++ b/src/irc/shims/ContactUser.cpp @@ -0,0 +1,127 @@ +#include "UserIdentity.h" +#include "ContactUser.h" +#include "ConversationModel.h" +#include "OutgoingContactRequest.h" + +// TODO: wire up the slots in here, figure out how to properly wire up unread count, status change +// populating the contacts manager on boot, keeping libtego's internal contacts synced with frontend's +// put all of the SettingsObject stuff into one settings manager + +namespace shims +{ + ContactUser::ContactUser(const QString& serviceId_, const QString& nickname_) + : conversationModel(new shims::ConversationModel(this)) + , outgoingContactRequest(new shims::OutgoingContactRequest()) + , status(ContactUser::Offline) + , serviceId(serviceId_) + , nickname() + , settings(QString("users.%1").arg(serviceId_)) + { + Q_ASSERT(serviceId.size() == TEGO_V3_ONION_SERVICE_ID_LENGTH); + conversationModel->setContact(this); + + + this->setNickname(nickname_); + } + + QString ContactUser::getNickname() const + { + return nickname; + } + + QString ContactUser::getContactID() const + { + return QString("ricochet:") + serviceId; + } + + ContactUser::Status ContactUser::getStatus() const + { + return status; + } + + void ContactUser::setStatus(ContactUser::Status newStatus) + { + if (this->status != newStatus) + { + this->status = newStatus; + switch(this->status) + { + case ContactUser::Online: + case ContactUser::Offline: + settings.write("type", "allowed"); + break; + case ContactUser::RequestPending: + settings.write("type", "pending"); + break; + case ContactUser::RequestRejected: + settings.write("type", "rejected"); + break; + default: + break; + } + emit this->statusChanged(); + } + } + + shims::OutgoingContactRequest* ContactUser::contactRequest() + { + return outgoingContactRequest; + } + + shims::ConversationModel* ContactUser::conversation() + { + return conversationModel; + } + + void ContactUser::setNickname(const QString& newNickname) + { + if (this->nickname != newNickname) + { + this->nickname = newNickname; + settings.write("nickname", newNickname); + emit this->nicknameChanged(); + } + } + + void ContactUser::deleteContact() + { + auto userIdentity = shims::UserIdentity::userIdentity; + + auto context = userIdentity->getContext(); + auto userId = this->toTegoUserId(); + + tego_context_forget_user(context, userId.get(), tego::throw_on_error()); + + settings.undefine(); + emit this->contactDeleted(this); + } + + void ContactUser::sendFile() + { + this->conversationModel->sendFile(); + } + + bool ContactUser::exportConversation() + { + return this->conversationModel->exportConversation(); + } + + std::unique_ptr ContactUser::toTegoUserId() const + { + logger::println("serviceId : {}", this->serviceId); + + auto serviceIdRaw = this->serviceId.toUtf8(); + + // ensure valid service id + std::unique_ptr onionServiceId; + tego_v3_onion_service_id_from_string(tego::out(onionServiceId), serviceIdRaw.data(), static_cast(serviceIdRaw.size()), tego::throw_on_error()); + + logger::trace(); + + // create user id object from service id + std::unique_ptr userId; + tego_user_id_from_v3_onion_service_id(tego::out(userId), onionServiceId.get(), tego::throw_on_error()); + + return userId; + } +} diff --git a/src/irc/shims/ContactUser.h b/src/irc/shims/ContactUser.h new file mode 100644 index 00000000..2ecd86b3 --- /dev/null +++ b/src/irc/shims/ContactUser.h @@ -0,0 +1,67 @@ +#pragma once + +#include "utils/Settings.h" + +namespace shims +{ + class ContactsManager; + class ConversationModel; + class OutgoingContactRequest; + class ContactUser : public QObject + { + Q_OBJECT + Q_DISABLE_COPY(ContactUser) + Q_ENUMS(Status) + + Q_PROPERTY(QString nickname READ getNickname WRITE setNickname NOTIFY nicknameChanged) + Q_PROPERTY(QString contactID READ getContactID CONSTANT) + Q_PROPERTY(Status status READ getStatus NOTIFY statusChanged) + Q_PROPERTY(shims::OutgoingContactRequest* contactRequest READ contactRequest NOTIFY statusChanged) + Q_PROPERTY(shims::ConversationModel* conversation READ conversation CONSTANT) + public: + enum Status + { + Online, + Offline, + RequestPending, + RequestRejected, + Outdated + }; + + ContactUser(const QString& serviceId, const QString& nickname); + + QString getNickname() const; + QString getContactID() const; + Status getStatus() const; + void setStatus(Status status); + shims::OutgoingContactRequest *contactRequest(); + shims::ConversationModel *conversation(); + + Q_INVOKABLE void deleteContact(); + Q_INVOKABLE void sendFile(); + Q_INVOKABLE bool exportConversation(); + + std::unique_ptr toTegoUserId() const; + + public slots: + void setNickname(const QString &nickname); + + signals: + void nicknameChanged(); + void statusChanged(); + void contactDeleted(shims::ContactUser *user); + + private: + shims::ConversationModel* conversationModel; + shims::OutgoingContactRequest* outgoingContactRequest; + + Status status; + QString serviceId; + QString nickname; + + SettingsObject settings; + + friend class shims::ContactsManager; + friend class shims::ConversationModel; + }; +} \ No newline at end of file diff --git a/src/irc/shims/ContactsManager.cpp b/src/irc/shims/ContactsManager.cpp new file mode 100644 index 00000000..718ec36d --- /dev/null +++ b/src/irc/shims/ContactsManager.cpp @@ -0,0 +1,101 @@ +#include "ContactsManager.h" +#include "ContactUser.h" + +namespace shims +{ + ContactsManager::ContactsManager(tego_context_t* context_) + : context(context_) + , contactsList({}) + { } + + shims::ContactUser* ContactsManager::createContactRequest( + const QString &contactID, + const QString &nickname, + const QString &myNickname, + const QString &message) + { + logger::println("{{ contactID : {}, nickname : {}, myNickname : {}, message : {} }}", + contactID, nickname, myNickname, message); + + auto serviceId = contactID.mid(tego::static_strlen("ricochet:")).toUtf8(); + + // check that the service id is valid before anything else + if (tego_v3_onion_service_id_string_is_valid(serviceId.constData(), static_cast(serviceId.size()), nullptr) != TEGO_TRUE) + { + return nullptr; + } + + auto shimContact = this->addContact(serviceId, nickname); + + auto userId = shimContact->toTegoUserId(); + auto rawMessage = message.toUtf8(); + + tego_context_send_chat_request(this->context, userId.get(), rawMessage.data(), static_cast(rawMessage.size()), tego::throw_on_error()); + + shimContact->setStatus(shims::ContactUser::RequestPending); + + return shimContact; + } + + shims::ContactUser* ContactsManager::addContact(const QString& serviceId, const QString& nickname) + { + // creates a new contact from service id and nickname + auto shimContact = new shims::ContactUser(serviceId, nickname); + contactsList.push_back(shimContact); + + // remove our reference and ready for deleting when contactDeleted signal is fireds + connect(shimContact, &shims::ContactUser::contactDeleted, [self=this](shims::ContactUser* user) -> void + { + // find the given user in our internal list and remove, mark for deletion + auto it = std::find(self->contactsList.begin(), self->contactsList.end(), user); + if (it != self->contactsList.end()) { + self->contactsList.erase(it); + user->deleteLater(); + } + + }); + + emit this->contactAdded(shimContact); + return shimContact; + } + + shims::ContactUser* ContactsManager::getShimContactByContactId(const QString& contactId) const + { + logger::trace(); + for(auto& cu : contactsList) + { + logger::println("cu : {}", static_cast(cu)); + if (cu->getContactID() == contactId) + { + logger::trace(); + return cu; + } + } + return nullptr; + } + + shims::ContactUser* ContactsManager::getShimContactByNickname(const QString& nickname) const { + for(auto cu : contactsList) { + if (cu->getNickname() == nickname) + { + return cu; + } + } + return nullptr; + } + + const QList& ContactsManager::contacts() const + { + return contactsList; + } + + void ContactsManager::setUnreadCount(shims::ContactUser* user, int unreadCount) + { + emit this->unreadCountChanged(user, unreadCount); + } + + void ContactsManager::setContactStatus(shims::ContactUser* user, int status) + { + emit this->contactStatusChanged(user, status); + } +} diff --git a/src/irc/shims/ContactsManager.h b/src/irc/shims/ContactsManager.h new file mode 100644 index 00000000..3c6fb8b3 --- /dev/null +++ b/src/irc/shims/ContactsManager.h @@ -0,0 +1,35 @@ +#pragma once + +#include "ContactUser.h" + +namespace shims +{ + class ContactsManager : public QObject + { + Q_OBJECT + Q_DISABLE_COPY(ContactsManager) + public: + ContactsManager(tego_context_t* context); + + Q_INVOKABLE shims::ContactUser* createContactRequest( + const QString &contactID, + const QString &nickname, + const QString &myNickname, + const QString &message); + shims::ContactUser* addContact(const QString& serviceId, const QString& nickname); + const QList& contacts() const; + shims::ContactUser* getShimContactByContactId(const QString& contactId) const; + shims::ContactUser* getShimContactByNickname(const QString& nickname) const; + + void setUnreadCount(shims::ContactUser* user, int unreadCount); + void setContactStatus(shims::ContactUser* user, int status); + + signals: + void contactAdded(shims::ContactUser *user); + void unreadCountChanged(shims::ContactUser *user, int unreadCount); + void contactStatusChanged(shims::ContactUser* user, int status); + private: + tego_context_t* context; + mutable QList contactsList; + }; +} diff --git a/src/irc/shims/ConversationModel.cpp b/src/irc/shims/ConversationModel.cpp new file mode 100644 index 00000000..f45808e8 --- /dev/null +++ b/src/irc/shims/ConversationModel.cpp @@ -0,0 +1,695 @@ +#include "ContactUser.h" +#include "ConversationModel.h" +#include "UserIdentity.h" +#include "utils/Useful.h" + +namespace shims +{ + ConversationModel::ConversationModel(QObject *parent) + : QAbstractListModel(parent) + , contactUser(nullptr) + , messages({}) + , unreadCount(0) + { + connect(this, &ConversationModel::unreadCountChanged, [self=this](int prevCount, int currentCount) -> void + { + static int globalUnreadCount = 0; + + const auto delta = currentCount - prevCount; + globalUnreadCount += delta; + + qDebug() << "globalUnreadCount:" << globalUnreadCount; +#ifdef Q_OS_MAC + QtMac::setBadgeLabelText(globalUnreadCount == 0 ? QString() : QString::number(globalUnreadCount)); +#endif + }); + } + + QHash ConversationModel::roleNames() const + { + QHash roles; + roles[Qt::DisplayRole] = "text"; + roles[TimestampRole] = "timestamp"; + roles[IsOutgoingRole] = "isOutgoing"; + roles[StatusRole] = "status"; + roles[SectionRole] = "section"; + roles[TimespanRole] = "timespan"; + roles[TypeRole] = "type"; + roles[TransferRole] = "transfer"; + return roles; + } + + int ConversationModel::rowCount(const QModelIndex &parent) const + { + if (parent.isValid()) + return 0; + return messages.size(); + } + + QVariant ConversationModel::data(const QModelIndex &index, int role) const + { + if (!index.isValid() || index.row() >= messages.size()) + return QVariant(); + + const MessageData &message = messages[index.row()]; + + switch (role) { + case Qt::DisplayRole: + if (message.type == TextMessage) + { + return message.text; + } + else + { + return QStringLiteral("not a text message"); + } + + case TimestampRole: return message.time; + case IsOutgoingRole: return message.status != Received; + case StatusRole: return message.status; + + case SectionRole: { + if (contact()->getStatus() == ContactUser::Online) + return QString(); + if (index.row() < messages.size() - 1) { + const MessageData &next = messages[index.row()+1]; + if (next.status != Received && next.status != Delivered) + return QString(); + } + for (int i = 0; i <= index.row(); i++) { + if (messages[i].status == Received || messages[i].status == Delivered) + return QString(); + } + return QStringLiteral("offline"); + } + case TimespanRole: { + if (index.row() < messages.size() - 1) + return messages[index.row() + 1].time.secsTo(messages[index.row()].time); + else + return -1; + } + case TypeRole: { + if (message.type == TextMessage) { + return QStringLiteral("text"); + } + else if (message.type == TransferMessage) { + return QStringLiteral("transfer"); + } + else { + return QStringLiteral("invalid"); + } + case TransferRole: + if (message.type == TransferMessage) + { + QVariantMap transfer; + transfer["file_name"] = message.fileName; + transfer["file_size"] = message.fileSize; + transfer["file_hash"] = message.fileHash; + transfer["id"] = message.identifier; + transfer["status"] = message.transferStatus; + transfer["statusString"] = [=]() + { + switch(message.transferStatus) + { + case Pending: return tr("Pending"); + case Accepted: return tr("Accepted"); + case Rejected: return tr("Rejected"); + case InProgress: + { + const auto locale = QLocale::system(); + return QString("%1 / %2").arg(locale.formattedDataSize(safe_cast(message.bytesTransferred)), locale.formattedDataSize(message.fileSize)); + } + case Cancelled: return tr("Cancelled"); + case Finished: return tr("Complete"); + case UnknownFailure: return tr("Unkown Failure"); + case BadFileHash: return tr("Bad File Hash"); + case NetworkError: return tr("Network Error"); + case FileSystemError: return tr("File System Error"); + + default: return tr("Invalid"); + } + }(); + transfer["progressPercent"] = double(message.bytesTransferred) / double(message.fileSize); + transfer["direction"] = message.transferDirection; + + return transfer; + } + } + } + + return QVariant(); + } + + shims::ContactUser* ConversationModel::contact() const + { + return contactUser; + } + + void ConversationModel::setContact(shims::ContactUser *contact) + { + this->contactUser = contact; + emit contactChanged(); + } + + int ConversationModel::getUnreadCount() const + { + return unreadCount; + } + + void ConversationModel::resetUnreadCount() + { + this->setUnreadCount(0); + } + + void ConversationModel::setUnreadCount(int count) + { + Q_ASSERT(count >= 0); + + const auto oldUnreadCount = this->unreadCount; + if(oldUnreadCount != count) + { + this->unreadCount = count; + emit unreadCountChanged(oldUnreadCount, unreadCount); + + auto userIdentity = shims::UserIdentity::userIdentity; + auto contactsManager = userIdentity->getContacts(); + contactsManager->setUnreadCount(this->contactUser, count); + } + } + + void ConversationModel::sendMessage(const QString &text) + { + logger::println("sendMessage : {}", text); + auto userIdentity = shims::UserIdentity::userIdentity; + auto context = userIdentity->getContext(); + + auto utf8Str = text.toUtf8(); + if (utf8Str.size() == 0) + { + return; + } + + const auto userId = this->contactUser->toTegoUserId(); + tego_message_id_t messageId = 0; + + // send message and save off the id associated with it + tego_context_send_message( + context, + userId.get(), + utf8Str.data(), + static_cast(utf8Str.size()), + &messageId, + tego::throw_on_error()); + + // store data locally for UI + MessageData md; + md.type = TextMessage; + md.text = text; + md.time = QDateTime::currentDateTime(); + md.identifier = messageId; + md.status = Queued; + + this->beginInsertRows(QModelIndex(), 0, 0); + this->messages.prepend(std::move(md)); + this->endInsertRows(); + this->addEventFromMessage(indexOfOutgoingMessage(messageId)); + } + + void ConversationModel::sendFile() + { + // Not implemented + } + + void ConversationModel::deserializeTextMessageEventToFile(const EventData &event, std::ofstream &ofile) const + { + auto &md = this->messages[this->messages.size() - safe_cast(event.messageData.reverseIndex)]; + switch (md.status) + { + case Received: + fmt::print(ofile, "[{}] <{}>: {}\n", + md.time.toString().toStdString(), + this->contact()->getNickname().toStdString(), + md.text.toStdString()); break; + case Delivered: + fmt::print(ofile, "[{}] <{}>: {}\n", + md.time.toString().toStdString(), + tr("me").toStdString(), + md.text.toStdString()); break; + default: + // messages we sent that weren't delivered + fmt::print(ofile, "[{}] <{}> ({}): {}\n", + md.time.toString().toStdString(), + tr("me").toStdString(), + getMessageStatusString(md.status), + md.text.toStdString()); break; + } + } + + void ConversationModel::deserializeTransferMessageEventToFile(const EventData &event, std::ofstream &ofile) const + { + auto &md = this->messages[this->messages.size() - safe_cast(event.transferData.reverseIndex)]; + + if (md.transferDirection == InvalidDirection) + return; + + std::string sender = md.transferDirection == Uploading + ? tr("me").toStdString() + : this->contact()->getNickname().toStdString(); + + switch (event.transferData.status) + { + case Pending: //FALLTHROUGH + case Accepted: //FALLTHROUGH + case Rejected: //FALLTHROUGH + case Cancelled: //FALLTHROUGH + case Finished: + fmt::print(ofile, "[{}] file '{}' from <{}> (hash: {}, size: {:L} bytes): {}\n", + event.time.toString().toStdString(), + md.fileName.toStdString(), + sender, + md.fileHash.toStdString(), + md.fileSize, + getTransferStatusString(event.transferData.status)); break; + case UnknownFailure: //FALLTHROUGH + case BadFileHash: //FALLTHROUGH + case NetworkError: //FALLTHROUGH + case FileSystemError: + fmt::print(ofile, "[{}] file '{}' from <{}> (hash: {}, size: {:L} bytes): Error: {}, bytes transferred: {:L} bytes\n", + event.time.toString().toStdString(), + md.fileName.toStdString(), + sender, + md.fileHash.toStdString(), + md.fileSize, + getTransferStatusString(event.transferData.status), + event.transferData.bytesTransferred); break; + default: + qWarning() << "Invalid transfer status in events"; + break; + } + } + + void ConversationModel::deserializeUserStatusUpdateEventToFile(const EventData &event, std::ofstream &ofile) const + { + if (event.userStatusData.target == UserTargetNone) + return; + + std::string sender = event.userStatusData.target == UserTargetClient + ? tr("me").toStdString() + : this->contact()->getNickname().toStdString(); + + switch (event.userStatusData.status) + { + case ContactUser::Status::Online: + fmt::print(ofile, "[{}] <{}> is now online\n", + event.time.toString().toStdString(), + this->contact()->getNickname().toStdString()); break; + case ContactUser::Status::Offline: + fmt::print(ofile, "[{}] <{}> is now offline\n", + event.time.toString().toStdString(), + this->contact()->getNickname().toStdString()); break; + case ContactUser::Status::RequestPending: + fmt::print(ofile, "[{}] New contact request to <{}>\n", + event.time.toString().toStdString(), + this->contact()->getNickname().toStdString()); break; + case ContactUser::Status::RequestRejected: + fmt::print(ofile, "[{}] Outgoing request to <{}> was rejected\n", + event.time.toString().toStdString(), + this->contact()->getNickname().toStdString()); break; + default: + break; + } + } + + void ConversationModel::deserializeEventToFile(const EventData &event, std::ofstream &ofile) const + { + switch (event.type) + { + case TextMessageEvent: + deserializeTextMessageEventToFile(event, ofile); break; + case TransferMessageEvent: + deserializeTransferMessageEventToFile(event, ofile); break; + case UserStatusUpdateEvent: + deserializeUserStatusUpdateEventToFile(event, ofile); break; + default: + qWarning() << "Unknown event type in events list"; + break; + } + } + + bool ConversationModel::hasEventsToExport() { + return events.size() > 0; + } + + bool ConversationModel::exportConversation() + { + // Not implemented + return false; + } + + void ConversationModel::tryAcceptFileTransfer(quint32 id) + { + // Not implemented + return; + } + + void ConversationModel::cancelFileTransfer(tego_file_transfer_id_t id) + { + // we get the cancelled callback if we cancel or if the other user cancelled, + // so ensure we only do work if it was the other preson cancelling + auto row = this->indexOfMessage(id); + if (row < 0) + { + return; + } + + MessageData &data = messages[row]; + if (data.transferStatus != Cancelled) + { + data.transferStatus = Cancelled; + emitDataChanged(row); + this->addEventFromMessage(row); + + auto userIdentity = shims::UserIdentity::userIdentity; + auto context = userIdentity->getContext(); + const auto userId = this->contactUser->toTegoUserId(); + + try + { + tego_context_cancel_file_transfer( + context, + userId.get(), + id, + tego::throw_on_error()); + } + catch(const std::runtime_error& err) + { + qWarning() << err.what(); + } + } + } + + void ConversationModel::rejectFileTransfer(quint32 id) + { + auto row = this->indexOfIncomingMessage(id); + if (row < 0) + { + return; + } + + auto& data = messages[row]; + + auto userIdentity = shims::UserIdentity::userIdentity; + auto context = userIdentity->getContext(); + const auto sender = this->contactUser->toTegoUserId(); + + try + { + tego_context_respond_file_transfer_request( + context, + sender.get(), + id, + tego_file_transfer_response_reject, + nullptr, + 0, + tego::throw_on_error()); + } + catch(const std::runtime_error& err) + { + qWarning() << err.what(); + } + + data.transferStatus = Rejected; + emitDataChanged(row); + this->addEventFromMessage(row); + } + + void ConversationModel::fileTransferRequestReceived(tego_file_transfer_id_t id, QString fileName, QString fileHash, quint64 fileSize) + { + MessageData md; + md.type = TransferMessage; + md.identifier = id; + md.time = QDateTime::currentDateTime(); + md.status = Received; + + md.fileName = std::move(fileName); + md.fileHash = std::move(fileHash); + md.fileSize = safe_cast(fileSize); + md.transferDirection = Downloading; + md.transferStatus = Pending; + + this->beginInsertRows(QModelIndex(), 0, 0); + this->messages.prepend(std::move(md)); + this->endInsertRows(); + + this->setUnreadCount(this->unreadCount + 1); + this->addEventFromMessage(indexOfIncomingMessage(id)); + } + + void ConversationModel::fileTransferRequestAcknowledged(tego_file_transfer_id_t id, bool accepted) + { + auto row = this->indexOfOutgoingMessage(id); + Q_ASSERT(row >= 0); + + MessageData &data = messages[row]; + data.status = accepted ? Delivered : Error; + emitDataChanged(row); + } + + void ConversationModel::fileTransferRequestResponded(tego_file_transfer_id_t id, tego_file_transfer_response_t response) + { + auto row = this->indexOfOutgoingMessage(id); + Q_ASSERT(row >= 0); + + MessageData &data = messages[row]; + switch(response) + { + case tego_file_transfer_response_accept: + data.transferStatus = Accepted; + break; + case tego_file_transfer_response_reject: + data.transferStatus = Rejected; + break; + default: + return; + } + + emitDataChanged(row); + this->addEventFromMessage(row); + } + + void ConversationModel::fileTransferRequestProgressUpdated(tego_file_transfer_id_t id, quint64 bytesTransferred) + { + auto row = this->indexOfMessage(id); + if (row >= 0) + { + MessageData &data = messages[row]; + data.bytesTransferred = bytesTransferred; + data.transferStatus = InProgress; + + emitDataChanged(row); + } + } + + void ConversationModel::fileTransferRequestCompleted( + tego_file_transfer_id_t id, tego_file_transfer_result_t result) + { + auto row = this->indexOfMessage(id); + if (row >= 0) + { + auto &data = messages[row]; + switch(result) + { + case tego_file_transfer_result_success: + data.transferStatus = Finished; + break; + case tego_file_transfer_result_failure: + data.transferStatus = UnknownFailure; + break; + case tego_file_transfer_result_cancelled: + data.transferStatus = Cancelled; + break; + case tego_file_transfer_result_rejected: + data.transferStatus = Rejected; + break; + case tego_file_transfer_result_bad_hash: + data.transferStatus = BadFileHash; + break; + case tego_file_transfer_result_network_error: + data.transferStatus = NetworkError; + break; + case tego_file_transfer_result_filesystem_error: + data.transferStatus = FileSystemError; + break; + default: + data.transferStatus = InvalidTransfer; + break; + } + emitDataChanged(row); + this->addEventFromMessage(row); + } + } + + void ConversationModel::clear() + { + if (messages.isEmpty()) + { + return; + } + + beginRemoveRows(QModelIndex(), 0, messages.size()-1); + messages.clear(); + endRemoveRows(); + + resetUnreadCount(); + } + + void ConversationModel::messageReceived(tego_message_id_t messageId, QDateTime timestamp, const QString& text) + { + MessageData md; + md.type = TextMessage; + md.text = text; + md.time = timestamp; + md.identifier = messageId; + md.status = Received; + + this->beginInsertRows(QModelIndex(), 0, 0); + this->messages.prepend(std::move(md)); + this->endInsertRows(); + + this->setUnreadCount(this->unreadCount + 1); + this->addEventFromMessage(indexOfIncomingMessage(messageId)); + } + + void ConversationModel::messageAcknowledged(tego_message_id_t messageId, bool accepted) + { + if (messages.size() == 0) { + // Reached when the model is cleared after an outgoing message was sent, + // but before it is acknowledged. + // https://github.com/blueprint-freespeech/ricochet-refresh/issues/150 + return; + } + + auto row = this->indexOfOutgoingMessage(messageId); + Q_ASSERT(row >= 0); + + MessageData &data = messages[row]; + data.status = accepted ? Delivered : Error; + emitDataChanged(row); + } + + void ConversationModel::addEventFromMessage(int row) + { + EventData ed; + + if (row < 0) + return; + + auto &md = this->messages[row]; + switch (md.type) + { + case TextMessage: + ed.type = TextMessageEvent; + ed.messageData.reverseIndex = static_cast(this->messages.size() - row); + break; + case TransferMessage: + ed.type = TransferMessageEvent; + ed.transferData.reverseIndex = static_cast(this->messages.size() - row); + ed.transferData.status = md.transferStatus; + ed.transferData.bytesTransferred = safe_cast(md.bytesTransferred); + break; + default: + return; + } + ed.time = QDateTime::currentDateTime(); + + this->events.append(std::move(ed)); + emit this->conversationEventCountChanged(); + } + + void ConversationModel::setStatus(ContactUser::Status status) + { + EventData ed; + + ed.type = UserStatusUpdateEvent; + ed.userStatusData.status = status; + ed.userStatusData.target = UserTargetPeer; + ed.time = QDateTime::currentDateTime(); + + this->events.append(std::move(ed)); + emit this->conversationEventCountChanged(); + } + + void ConversationModel::emitDataChanged(int row) + { + Q_ASSERT(row >= 0); + emit dataChanged(index(row, 0), index(row, 0)); + } + + int ConversationModel::indexOfMessage(quint32 identifier) const + { + for (int i = 0; i < messages.size(); i++) { + const auto& currentMessage = messages[i]; + + if (currentMessage.identifier == identifier) + return i; + } + return -1; + } + + int ConversationModel::indexOfOutgoingMessage(quint32 identifier) const + { + for (int i = 0; i < messages.size(); i++) { + const auto& currentMessage = messages[i]; + + if (currentMessage.identifier == identifier && (currentMessage.status != Received)) + return i; + } + return -1; + } + + int ConversationModel::indexOfIncomingMessage(quint32 identifier) const + { + for (int i = 0; i < messages.size(); i++) { + const auto& currentMessage = messages[i]; + + if (currentMessage.identifier == identifier && (currentMessage.status == Received)) + return i; + } + return -1; + } + + const char* ConversationModel::getMessageStatusString(const MessageStatus status) + { + constexpr static const char* statusList[] = + { + "None", + "Received", + "Queued", + "Sending", + "Delivered", + "Error" + }; + + return statusList[static_cast(status)]; + } + + const char* ConversationModel::getTransferStatusString(const TransferStatus status) + { + constexpr static const char* statusList[] = + { + "Invalid Transfer", + "Pending", + "Accepted", + "Rejected", + "In Progress", + "Cancelled", + "Finished", + "Unknown Failure", + "Bad File Hash", + "Network Error", + "Filesystem Error" + }; + + return statusList[static_cast(status)]; + } +} diff --git a/src/irc/shims/ConversationModel.h b/src/irc/shims/ConversationModel.h new file mode 100644 index 00000000..187296a4 --- /dev/null +++ b/src/irc/shims/ConversationModel.h @@ -0,0 +1,190 @@ +#pragma once + +#include "ContactUser.h" + +namespace shims +{ + class ContactUser; + class ConversationModel : public QAbstractListModel + { + Q_OBJECT + Q_ENUMS(MessageStatus) + + Q_PROPERTY(shims::ContactUser* contact READ contact WRITE setContact NOTIFY contactChanged) + Q_PROPERTY(int unreadCount READ getUnreadCount RESET resetUnreadCount NOTIFY unreadCountChanged) + Q_PROPERTY(int conversationEventCount READ getConversationEventCount NOTIFY conversationEventCountChanged) + public: + ConversationModel(QObject *parent = 0); + + enum { + TimestampRole = Qt::UserRole, + IsOutgoingRole, + StatusRole, + SectionRole, + TimespanRole, + TypeRole, + TransferRole, + }; + + enum MessageStatus { + None, + Received, + Queued, + Sending, + Delivered, + Error + }; + + enum MessageDataType + { + InvalidMessage = -1, + TextMessage, + TransferMessage, + }; + + enum TransferStatus + { + InvalidTransfer, + Pending, + Accepted, + Rejected, + InProgress, + Cancelled, + Finished, + UnknownFailure, + BadFileHash, + NetworkError, + FileSystemError, + }; + Q_ENUM(TransferStatus); + + enum TransferDirection + { + InvalidDirection, + Uploading, + Downloading, + }; + Q_ENUM(TransferDirection); + + enum EventType { + InvalidEvent, + TextMessageEvent, + TransferMessageEvent, + UserStatusUpdateEvent + }; + + enum UserStatusTarget { + UserTargetNone, + UserTargetClient, + UserTargetPeer + }; + + // impl QAbstractListModel + virtual QHash roleNames() const; + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const; + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + + shims::ContactUser *contact() const; + void setContact(shims::ContactUser *contact); + int getUnreadCount() const; + Q_INVOKABLE void resetUnreadCount(); + + void sendFile(); + bool hasEventsToExport(); + Q_INVOKABLE int getConversationEventCount() const { return this->events.size(); } + bool exportConversation(); + // invokable function neeeds to use a Qt type since it is invokable from QML + static_assert(std::is_same_v); + Q_INVOKABLE void tryAcceptFileTransfer(quint32 id); + Q_INVOKABLE void cancelFileTransfer(quint32 id); + Q_INVOKABLE void rejectFileTransfer(quint32 id); + + + void setStatus(ContactUser::Status status); + + void fileTransferRequestReceived(tego_file_transfer_id_t id, QString fileName, QString fileHash, quint64 fileSize); + void fileTransferRequestAcknowledged(tego_file_transfer_id_t id, bool accepted); + void fileTransferRequestResponded(tego_file_transfer_id_t id, tego_file_transfer_response_t response); + void fileTransferRequestProgressUpdated(tego_file_transfer_id_t id, quint64 bytesTransferred); + void fileTransferRequestCompleted(tego_file_transfer_id_t id, tego_file_transfer_result_t result); + + void messageReceived(tego_message_id_t messageId, QDateTime timestamp, const QString& text); + void messageAcknowledged(tego_message_id_t messageId, bool accepted); + + public slots: + void sendMessage(const QString &text); + void clear(); + + signals: + void contactChanged(); + void unreadCountChanged(int prevCount, int currentCount); + void conversationEventCountChanged(); + private: + void setUnreadCount(int count); + + shims::ContactUser* contactUser = nullptr; + + struct MessageData + { + MessageDataType type = InvalidMessage; + QString text = {}; + QDateTime time = {}; + static_assert(std::is_same_v); + static_assert(std::is_same_v); + quint32 identifier = 0; + MessageStatus status = None; + quint8 attemptCount = 0; + // file transfer data + QString fileName = {}; + qint64 fileSize = 0; + QString fileHash = {}; + quint64 bytesTransferred = 0; + TransferDirection transferDirection = InvalidDirection; + TransferStatus transferStatus = InvalidTransfer; + }; + + struct EventData + { + EventType type = InvalidEvent; + union { + struct { + size_t reverseIndex = 0; + } messageData; + struct { + size_t reverseIndex = 0; + TransferStatus status = InvalidTransfer; + qint64 bytesTransferred = 0; // we care about this for when a transfer is cancelled midway + } transferData; + struct { + ContactUser::Status status = ContactUser::Status::Offline; + UserStatusTarget target = UserTargetNone; // when the protocol is eventually fixed and users + // are notified of being blocked, this will be needed + } userStatusData; + }; + QDateTime time = {}; + + EventData() {} + }; + + QList messages; + QList events; + + void addEventFromMessage(int row); + + void deserializeTextMessageEventToFile(const EventData &event, std::ofstream &ofile) const; + void deserializeTransferMessageEventToFile(const EventData &event, std::ofstream &ofile) const; + void deserializeUserStatusUpdateEventToFile(const EventData &event, std::ofstream &ofile) const; + void deserializeEventToFile(const EventData &event, std::ofstream &ofile) const; + + int unreadCount = 0; + + void emitDataChanged(int row); + + int indexOfMessage(quint32 identifier) const; + int indexOfOutgoingMessage(quint32 identifier) const; + int indexOfIncomingMessage(quint32 identifier) const; + + static const char* getMessageStatusString(const MessageStatus status); + static const char* getTransferStatusString(const TransferStatus status); + }; +} diff --git a/src/irc/shims/IncomingContactRequest.cpp b/src/irc/shims/IncomingContactRequest.cpp new file mode 100644 index 00000000..e3e71928 --- /dev/null +++ b/src/irc/shims/IncomingContactRequest.cpp @@ -0,0 +1,68 @@ +#include "IncomingContactRequest.h" +#include "UserIdentity.h" + +namespace shims +{ + IncomingContactRequest::IncomingContactRequest(const QString& hostname, const QString& msg) + : serviceIdString(hostname.chopped(tego::static_strlen(".onion"))) + , nickname() + , message(msg) + , userId() + { + auto serviceIdRaw = serviceIdString.toUtf8(); + + std::unique_ptr serviceId; + tego_v3_onion_service_id_from_string(tego::out(serviceId), serviceIdRaw.data(), static_cast(serviceIdRaw.size()), tego::throw_on_error()); + tego_user_id_from_v3_onion_service_id(tego::out(userId), serviceId.get(), tego::throw_on_error()); + + // save our request to disk + SettingsObject settings(QString("users.%1").arg(serviceIdString)); + settings.write("type", "requesting"); + } + + QString IncomingContactRequest::getHostname() const + { + return serviceIdString + QString(".onion"); + } + + QString IncomingContactRequest::getContactId() const + { + return QString("ricochet:") + serviceIdString; + } + + void IncomingContactRequest::setNickname(const QString& newNickname) + { + logger::println("setNickname : '{}'", newNickname); + this->nickname = newNickname; + emit this->nicknameChanged(); + } + + void IncomingContactRequest::accept() + { + auto userIdentity = shims::UserIdentity::userIdentity; + auto context = userIdentity->getContext(); + auto contactManager = userIdentity->getContacts(); + + tego_context_acknowledge_chat_request(context, userId.get(), tego_chat_acknowledge_accept, tego::throw_on_error()); + + userIdentity->removeIncomingContactRequest(this); + + contactManager->addContact(serviceIdString, nickname); + + SettingsObject settings(QString("users.%1").arg(serviceIdString)); + settings.write("type", "allowed"); + } + + void IncomingContactRequest::reject() + { + auto userIdentity = shims::UserIdentity::userIdentity; + auto context = userIdentity->getContext(); + + tego_context_acknowledge_chat_request(context, userId.get(), tego_chat_acknowledge_block, tego::throw_on_error()); + + userIdentity->removeIncomingContactRequest(this); + + SettingsObject settings(QString("users.%1").arg(serviceIdString)); + settings.write("type", "blocked"); + } +} diff --git a/src/irc/shims/IncomingContactRequest.h b/src/irc/shims/IncomingContactRequest.h new file mode 100644 index 00000000..7539416f --- /dev/null +++ b/src/irc/shims/IncomingContactRequest.h @@ -0,0 +1,35 @@ +#pragma once + +namespace shims +{ + class IncomingContactRequest : public QObject + { + Q_OBJECT + Q_DISABLE_COPY(IncomingContactRequest) + + Q_PROPERTY(QString hostname READ getHostname CONSTANT) + Q_PROPERTY(QString nickname READ getNickname WRITE setNickname NOTIFY nicknameChanged) + Q_PROPERTY(QString contactId READ getContactId CONSTANT) + Q_PROPERTY(QString message READ getMessage CONSTANT) + public: + IncomingContactRequest(const QString& hostname, const QString& message); + + QString getHostname() const; + QString getNickname() const { return nickname; } + void setNickname(const QString&); + QString getContactId() const; + QString getMessage() const { return message; } + + public slots: + void accept(); + void reject(); + signals: + void nicknameChanged(); + + private: + const QString serviceIdString; + QString nickname; + const QString message; + std::unique_ptr userId; + }; +} \ No newline at end of file diff --git a/src/irc/shims/OutgoingContactRequest.cpp b/src/irc/shims/OutgoingContactRequest.cpp new file mode 100644 index 00000000..8cab7066 --- /dev/null +++ b/src/irc/shims/OutgoingContactRequest.cpp @@ -0,0 +1,33 @@ +#include "OutgoingContactRequest.h" + +namespace shims +{ + OutgoingContactRequest::Status OutgoingContactRequest::getStatus() const + { + logger::trace(); + return this->status; + } + + void OutgoingContactRequest::setStatus(Status newStatus) + { + if (this->status != newStatus) + { + emit this->statusChanged(this->status, newStatus); + this->status = newStatus; + } + } + + void OutgoingContactRequest::setAccepted() + { + this->setStatus(Acknowledged); + this->setStatus(Accepted); + } + + void OutgoingContactRequest::setRejected() + { + this->setStatus(Acknowledged); + this->setStatus(Rejected); + + emit this->rejected(); + } +} diff --git a/src/irc/shims/OutgoingContactRequest.h b/src/irc/shims/OutgoingContactRequest.h new file mode 100644 index 00000000..7e681cdf --- /dev/null +++ b/src/irc/shims/OutgoingContactRequest.h @@ -0,0 +1,41 @@ +#pragma once + +namespace shims +{ + class ContactUser; + class OutgoingContactRequest : public QObject + { + Q_OBJECT + Q_DISABLE_COPY(OutgoingContactRequest) + Q_ENUMS(Status) + + Q_PROPERTY(Status status READ getStatus NOTIFY statusChanged) + public: + enum Status + { + Pending, + Acknowledged, + Accepted, + Error, + Rejected, + FirstResult = Accepted + }; + + OutgoingContactRequest() = default; + + Status getStatus() const; + + void setStatus(Status status); + + void setAccepted(); + void setRejected(); + + + signals: + void statusChanged(int newStatus, int oldStatus); + void rejected(); + + private: + Status status = shims::OutgoingContactRequest::Pending; + }; +} \ No newline at end of file diff --git a/src/irc/shims/TorCommand.cpp b/src/irc/shims/TorCommand.cpp new file mode 100644 index 00000000..b7a5df41 --- /dev/null +++ b/src/irc/shims/TorCommand.cpp @@ -0,0 +1,16 @@ +#include "TorCommand.h" + +namespace shims +{ + void TorControlCommand::onFinished(bool success) + { + this->m_successful = success; + emit this->finished(success); + this->deleteLater(); + } + + bool TorControlCommand::isSuccessful() const + { + return m_successful; + } +} \ No newline at end of file diff --git a/src/irc/shims/TorCommand.h b/src/irc/shims/TorCommand.h new file mode 100644 index 00000000..16e0dba6 --- /dev/null +++ b/src/irc/shims/TorCommand.h @@ -0,0 +1,23 @@ +#pragma once + +namespace shims +{ + class TorControlCommand : public QObject + { + Q_OBJECT + Q_DISABLE_COPY(TorControlCommand) + + Q_PROPERTY(bool successful READ isSuccessful CONSTANT) + public: + TorControlCommand() = default; + void onFinished(bool success); + + bool isSuccessful() const; + + signals: + void finished(bool success); + + private: + bool m_successful = false; + }; +} \ No newline at end of file diff --git a/src/irc/shims/TorControl.cpp b/src/irc/shims/TorControl.cpp new file mode 100644 index 00000000..dcc70829 --- /dev/null +++ b/src/irc/shims/TorControl.cpp @@ -0,0 +1,426 @@ +#include "TorControl.h" +#include "utils/Settings.h" + +#include "pluggables.hpp" + +namespace shims +{ + TorControl* TorControl::torControl = nullptr; + + TorControl::TorControl(tego_context_t* context_) + : context(context_) + { } + + // callable from QML + // see TorConfigurationPage.qml + QObject* TorControl::setConfiguration(const QVariantMap &options) + { + QJsonObject json = QJsonObject::fromVariantMap(options); + return this->setConfiguration(json); + } + + QObject* TorControl::setConfiguration(const QJsonObject &config) try + { + Q_ASSERT(this->m_setConfigurationCommand == nullptr); + + std::unique_ptr daemonConfig; + tego_tor_daemon_config_initialize( + tego::out(daemonConfig), + tego::throw_on_error()); + + // generate own json to save to settings + QJsonObject tor; + + // proxy + if (auto proxyIt = config.find("proxy"); proxyIt != config.end()) + { + auto proxyObj = proxyIt->toObject(); + auto typeIt = proxyObj.find("type"); + TEGO_THROW_IF_EQUAL(typeIt, proxyObj.end()); + + auto typeString = typeIt->toString().toStdString(); + if (typeString != "none") + { + TEGO_THROW_IF_FALSE( + typeString == "socks4" || + typeString == "socks5" || + typeString == "https"); + + auto addressIt = proxyObj.find("address"); + TEGO_THROW_IF_EQUAL(addressIt, proxyObj.end()); + auto addressQString = addressIt->toString(); + auto address = addressQString.toStdString(); + TEGO_THROW_IF(address.size() == 0); + + auto portIt = proxyObj.find("port"); + TEGO_THROW_IF_EQUAL(portIt, proxyObj.end()); + auto port = portIt->toInt(); + TEGO_THROW_IF_FALSE(port > 0 && port < 65536); + + QJsonObject proxy; + proxy["address"] = addressQString; + proxy["port"] = port; + + if (typeString == "socks4") + { + tego_tor_daemon_config_set_proxy_socks4( + daemonConfig.get(), + address.data(), + address.size(), + static_cast(port), + tego::throw_on_error()); + + proxy["type"] = "socks4"; + } + else + { + auto usernameIt = proxyObj.find("username"); + auto passwordIt = proxyObj.find("password"); + + auto usernameQString = (usernameIt == proxyObj.end()) ? QString() : usernameIt->toString(); + auto passwordQString = (passwordIt == proxyObj.end()) ? QString() : passwordIt->toString(); + auto username = usernameQString.toStdString(); + auto password = passwordQString.toStdString(); + + proxy["username"] = usernameQString; + proxy["password"] = passwordQString; + + if (typeString == "socks5") + { + tego_tor_daemon_config_set_proxy_socks5( + daemonConfig.get(), + address.data(), + address.size(), + static_cast(port), + username.data(), + username.size(), + password.data(), + password.size(), + tego::throw_on_error()); + + proxy["type"] = "socks5"; + + } + else + { + TEGO_THROW_IF_FALSE(typeString == "https"); + tego_tor_daemon_config_set_proxy_https( + daemonConfig.get(), + address.data(), + address.size(), + static_cast(port), + username.data(), + username.size(), + password.data(), + password.size(), + tego::throw_on_error()); + + proxy["type"] = "https"; + } + } + tor["proxy"] = proxy; + } + } + // firewall + if (auto allowedPortsIt = config.find("allowedPorts"); allowedPortsIt != config.end()) + { + auto allowedPortsArray = allowedPortsIt->toArray(); + + std::vector allowedPorts; + for(auto value : allowedPortsArray) { + auto port = value.toInt(); + TEGO_THROW_IF_FALSE(port > 0 && port < 65536); + + // don't add duplicates + if (std::find(allowedPorts.begin(), allowedPorts.end(), port) == allowedPorts.end()) + { + allowedPorts.push_back(static_cast(port)); + } + } + std::sort(allowedPorts.begin(), allowedPorts.end()); + + if (allowedPorts.size() > 0) + { + tego_tor_daemon_config_set_allowed_ports( + daemonConfig.get(), + allowedPorts.data(), + allowedPorts.size(), + tego::throw_on_error()); + + tor["allowedPorts"] = ([&]() -> QJsonArray { + QJsonArray retval; + for(auto port : allowedPorts) { + retval.push_back(port); + } + return retval; + })(); + } + } + // bridges + if (auto bridgeTypeIt = config.find("bridgeType"); bridgeTypeIt != config.end() && *bridgeTypeIt != "none") + { + auto bridgeType = bridgeTypeIt->toString(); + + // sets list of bridge strings + const auto tegoTorDaemonConfigSetBridges = [&](const std::vector& bridgeStrings) -> void { + + // convert strings to std::string + const auto bridgeCount = static_cast(bridgeStrings.size()); + + // allocate buffers to pass to tego + auto rawBridges = std::make_unique(bridgeCount); + auto rawBridgeLengths = std::make_unique(bridgeCount); + + for(size_t i = 0; i < bridgeCount; ++i) { + const auto& bridgeString = bridgeStrings[i]; + rawBridges[i] = bridgeString.c_str(); + rawBridgeLengths[i] = bridgeString.size(); + } + + tego_tor_daemon_config_set_bridges( + daemonConfig.get(), + const_cast(rawBridges.get()), + rawBridgeLengths.get(), + bridgeCount, + tego::throw_on_error()); + }; + + if (bridgeType == "custom") + { + auto bridgeStringsIt = config.find("bridgeStrings"); + TEGO_THROW_IF_EQUAL(bridgeStringsIt, config.end()); + + std::vector bridgeStrings; + QJsonArray bridgeStringsArray; + for(auto entry : bridgeStringsIt->toArray()) { + auto bridgeString = entry.toString(); + logger::println("adding: {}", bridgeString); + bridgeStrings.push_back(bridgeString.toStdString()); + bridgeStringsArray.push_back(bridgeString); + } + tegoTorDaemonConfigSetBridges(bridgeStrings); + + tor["bridgeType"] = "custom"; + tor["bridgeStrings"] = bridgeStringsArray; + } + else if (auto bridgeStrings = this->getBridgeStringsForType(bridgeType); + bridgeStrings.size() > 0) + { + // ensure the bridges are ordered randomly per user to distribute to all users evenly + // but keep the seed per user consistent so their individual experience is consistent + auto seedJson = SettingsObject().read("tor.seed"); + uint32_t seed = 0; + + // ensure the signed -> unsigned conversion does as we expect + typedef decltype(seedJson.toInt()) json_int_t; + static_assert(std::numeric_limits::max() <= std::numeric_limits::max()); + + if (auto val = seedJson.toInt(-1); val >= 0) + { + seed = static_cast(val); + } + else + { + // fill seed w/ random bytes + tego_get_random_bytes( + this->context, + reinterpret_cast(&seed), + sizeof(seed), + tego::throw_on_error()); + + // now ensure we can save this value as an json_int_t + seed = seed % static_cast(std::numeric_limits::max()); + } + + // save seed to settings + tor["seed"] = static_cast(seed); + + // shuffle the bridge list so that users don't all select the first one + std::minstd_rand rand; + rand.seed(seed); + std::shuffle(bridgeStrings.begin(), bridgeStrings.end(), rand); + + tegoTorDaemonConfigSetBridges(bridgeStrings); + tor["bridgeType"] = bridgeType; + } + } + tego_context_update_tor_daemon_config( + context, + daemonConfig.get(), + tego::throw_on_error()); + + // after config is confirmed updated then save our settings + auto setConfigurationCommand = std::make_unique(); + // QQmlEngine::setObjectOwnership(setConfigurationCommand.get(), QQmlEngine::CppOwnership); + + this->m_setConfigurationCommand = setConfigurationCommand.release(); + connect( + this->m_setConfigurationCommand, + &shims::TorControlCommand::finished, + [tor=std::move(tor)](bool successful) -> void { + SettingsObject settings; + // only persist settings if config was set successfully + if (successful) { + settings.write("tor", tor); + } else { + settings.unset("tor"); + } + }); + + return this->m_setConfigurationCommand; + } catch (std::exception& ex) { + logger::println("Exception: {}", ex.what()); + return nullptr; + } + + QJsonObject TorControl::getConfiguration() + { + return SettingsObject().read("tor").toObject(); + } + + QObject* TorControl::beginBootstrap() try + { + tego_context_update_disable_network_flag( + context, + TEGO_FALSE, + tego::throw_on_error()); + + auto setConfigurationCommand = std::make_unique(); + // QQmlEngine::setObjectOwnership(setConfigurationCommand.get(), QQmlEngine::CppOwnership); + this->m_setConfigurationCommand = setConfigurationCommand.release(); + + return this->m_setConfigurationCommand; + } catch (std::exception& ex) { + logger::println("Exception: {}", ex.what()); + return nullptr; + } + + QList TorControl::getBridgeTypes() + { + auto types = defaultBridges.keys(); + if (auto it = std::find(types.begin(), types.end(), recommendedBridgeType); it != types.end()) { + std::iter_swap(it, types.begin()); + } + return types; + } + + std::vector TorControl::getBridgeStringsForType(const QString &bridgeType) + { + if (auto it = defaultBridges.find(bridgeType); it != defaultBridges.end()) { + return *it; + } + return {}; + } + + // for now we just assume we always have ownership, + // as we have no way in config to setup usage of + // an existing tor process + bool TorControl::hasOwnership() const + { + logger::trace(); + return true; + } + + bool TorControl::hasBootstrappedSuccessfully() const + { + auto value= SettingsObject().read("tor.bootstrappedSuccessfully"); + return value.isBool() ? value.toBool() : false; + } + + QString TorControl::torVersion() const + { + logger::trace(); + return tego_context_get_tor_version_string( + context, + tego::throw_on_error()); + } + + TorControl::Status TorControl::status() const + { + tego_tor_control_status_t status; + tego_context_get_tor_control_status( + context, + &status, + tego::throw_on_error()); + + logger::trace(); + return static_cast(status); + } + + TorControl::TorStatus TorControl::torStatus() const + { + tego_tor_network_status_t status; + tego_context_get_tor_network_status( + context, + &status, + tego::throw_on_error()); + + switch(status) + { + case tego_tor_network_status_unknown: + return TorControl::TorUnknown; + case tego_tor_network_status_ready: + return TorControl::TorReady; + case tego_tor_network_status_offline: + return TorControl::TorOffline; + default: + return TorControl::TorError; + } + } + + QVariantMap TorControl::bootstrapStatus() const + { + QVariantMap retval; + retval["progress"] = this->m_bootstrapProgress; + retval["done"] = (this->m_bootstrapTag == tego_tor_bootstrap_tag_done); + retval["summary"] = this->m_bootstrapSummary; + return retval; + } + + QString TorControl::errorMessage() const + { + return m_errorMessage; + } + + void TorControl::setStatus(Status status) + { + auto oldStatus = m_status; + if (oldStatus == status) return; + + m_status = status; + emit this->statusChanged( + static_cast(status), + static_cast(oldStatus)); + } + + void TorControl::setTorStatus(TorStatus status) + { + auto oldStatus = m_torStatus; + if (oldStatus == status) return; + + m_torStatus = status; + emit this->torStatusChanged( + static_cast(status), + static_cast(oldStatus)); + } + + void TorControl::setErrorMessage(const QString& msg) + { + m_errorMessage = msg; + this->setStatus(TorControl::Error); + } + + void TorControl::setBootstrapStatus(int32_t progress, tego_tor_bootstrap_tag_t tag, QString&& summary) + { + TEGO_THROW_IF_FALSE(progress >= 0 && progress <= 100); + this->m_bootstrapProgress = static_cast(progress); + this->m_bootstrapTag = tag; + this->m_bootstrapSummary = std::move(summary); + + emit torControl->bootstrapStatusChanged(); + + if (tag == tego_tor_bootstrap_tag_done) { + SettingsObject().write("tor.bootstrappedSuccessfully", true); + } + } +} diff --git a/src/irc/shims/TorControl.h b/src/irc/shims/TorControl.h new file mode 100644 index 00000000..94898acc --- /dev/null +++ b/src/irc/shims/TorControl.h @@ -0,0 +1,86 @@ +#pragma once + +#include "TorCommand.h" + +namespace shims +{ + // shim version of Tor::ToControl with just the functionality requried by the UI + class TorControl : public QObject + { + Q_OBJECT + Q_ENUMS(Status TorStatus) + + Q_PROPERTY(bool hasOwnership READ hasOwnership CONSTANT) + Q_PROPERTY(QString torVersion READ torVersion CONSTANT) + // Status of the control connection + Q_PROPERTY(Status status READ status NOTIFY statusChanged) + // Status of Tor (and whether it believes it can connect) + Q_PROPERTY(TorStatus torStatus READ torStatus NOTIFY torStatusChanged) + Q_PROPERTY(QVariantMap bootstrapStatus READ bootstrapStatus NOTIFY bootstrapStatusChanged) + // uses statusChanged like actual backend implementation + Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY statusChanged) + Q_PROPERTY(bool hasBootstrappedSuccessfully READ hasBootstrappedSuccessfully CONSTANT) + public: + enum Status + { + Error = -1, + NotConnected, + Connecting, + Authenticating, + Connected + }; + + enum TorStatus + { + TorError = -1, + TorUnknown, + TorOffline, + TorReady + }; + + Q_INVOKABLE QObject *setConfiguration(const QVariantMap &options); + QObject* setConfiguration(const QJsonObject& options); + Q_INVOKABLE QJsonObject getConfiguration(); + Q_INVOKABLE QObject *beginBootstrap(); + + // QVariant(Map) is not needed here, since QT handles the conversion to + // a JS array for us: see https://doc.qt.io/qt-5/qtqml-cppintegration-data.html#sequence-type-to-javascript-array + Q_INVOKABLE QList getBridgeTypes(); + std::vector getBridgeStringsForType(const QString &bridgeType); + + TorControl(tego_context_t* context); + + /* Ownership means that tor is managed by this socket, and we + * can shut it down, own its configuration, etc. */ + bool hasOwnership() const; + bool hasBootstrappedSuccessfully() const; + + QString torVersion() const; + Status status() const; + TorStatus torStatus() const; + QVariantMap bootstrapStatus() const; + QString errorMessage() const; + + void setStatus(Status); + void setTorStatus(TorStatus); + void setErrorMessage(const QString&); + void setBootstrapStatus(int32_t progress, tego_tor_bootstrap_tag_t tag, QString&& summary); + + static TorControl* torControl; + TorControlCommand* m_setConfigurationCommand = nullptr; + Status m_status = NotConnected; + TorStatus m_torStatus = TorUnknown; + QString m_errorMessage; + int m_bootstrapProgress = 0; + tego_tor_bootstrap_tag_t m_bootstrapTag = tego_tor_bootstrap_tag_invalid; + QString m_bootstrapSummary; + + signals: + void statusChanged(int newStatus, int oldStatus); + void torStatusChanged(int newStatus, int oldStatus); + void bootstrapStatusChanged(); + + private: + tego_context_t* context; + }; +} \ No newline at end of file diff --git a/src/irc/shims/TorManager.cpp b/src/irc/shims/TorManager.cpp new file mode 100644 index 00000000..e6f7ad8e --- /dev/null +++ b/src/irc/shims/TorManager.cpp @@ -0,0 +1,57 @@ +#include "TorManager.h" +#include "utils/Useful.h" + +namespace shims +{ + TorManager* TorManager::torManager = nullptr; + + TorManager::TorManager(tego_context_t* context) + : m_context(context) + { } + + QStringList TorManager::logMessages() const + { + const auto bufferSize = tego_context_get_tor_logs_size( + m_context, + tego::throw_on_error()); + auto buffer = std::make_unique(bufferSize); + + // NOTE: it is possible for a new log entry to have been received in-between + // getting the required buffer size and the data + + const auto written = tego_context_get_tor_logs( + m_context, + buffer.get(), + bufferSize, + tego::throw_on_error()); + + return QString::fromUtf8(buffer.get(), safe_cast(written)).split('\n'); + } + + QString TorManager::running() const + { + return this->m_running; + } + + void TorManager::setRunning(const QString& running) + { + this->m_running = running; + emit this->runningChanged(); + } + + bool TorManager::hasError() const + { + return !this->m_errorMessage.isEmpty(); + } + + QString TorManager::errorMessage() const + { + return this->m_errorMessage; + } + + void TorManager::setErrorMessage(const QString& msg) + { + this->m_errorMessage = msg; + emit this->errorChanged(); + } +} diff --git a/src/irc/shims/TorManager.h b/src/irc/shims/TorManager.h new file mode 100644 index 00000000..a77bf03d --- /dev/null +++ b/src/irc/shims/TorManager.h @@ -0,0 +1,34 @@ +#pragma once + +namespace shims +{ + class TorManager : public QObject + { + Q_OBJECT + + Q_PROPERTY(QStringList logMessages READ logMessages CONSTANT) + Q_PROPERTY(QString running READ running NOTIFY runningChanged) + Q_PROPERTY(bool hasError READ hasError NOTIFY errorChanged) + Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY errorChanged) + + public: + TorManager(tego_context_t*); + static TorManager* torManager; + + QStringList logMessages() const; + QString running() const; + void setRunning(const QString& running); + bool hasError() const; + QString errorMessage() const; + void setErrorMessage(const QString& message); + signals: + void logMessage(const QString &message); + void runningChanged(); + void errorChanged(); + + private: + tego_context_t* m_context; + QString m_errorMessage; + QString m_running; + }; +} \ No newline at end of file diff --git a/src/irc/shims/UserIdentity.cpp b/src/irc/shims/UserIdentity.cpp new file mode 100644 index 00000000..118ed3de --- /dev/null +++ b/src/irc/shims/UserIdentity.cpp @@ -0,0 +1,93 @@ +#include "ContactsManager.h" +#include "UserIdentity.h" +#include "IncomingContactRequest.h" + +shims::UserIdentity* shims::UserIdentity::userIdentity = nullptr; + +namespace shims +{ + UserIdentity::UserIdentity(tego_context_t* context_) + : contacts(context_) + , context(context_) + { } + + void UserIdentity::createIncomingContactRequest(const QString& hostname, const QString& message) + { + auto incomingContactRequest = new shims::IncomingContactRequest(hostname, message); + this->requests.push_back(incomingContactRequest); + + emit this->requestAdded(incomingContactRequest); + emit this->requestsChanged(); + } + + void UserIdentity::removeIncomingContactRequest(shims::IncomingContactRequest* incomingContactRequest) + { + auto it = std::find(this->requests.begin(), this->requests.end(), incomingContactRequest); + Q_ASSERT(it != this->requests.end()); + + this->requests.erase(it); + emit this->requestsChanged(); + + incomingContactRequest->deleteLater(); + } + + QList UserIdentity::getRequests() const + { + logger::trace(); + QList retval; + retval.reserve(requests.size()); + for(auto currentRequest: requests) + { + retval.push_back(currentRequest); + } + return retval; + } + + bool UserIdentity::isServiceOnline() const + { + auto state = tego_host_onion_service_state_none; + tego_context_get_host_onion_service_state(this->context, &state, tego::throw_on_error()); + + return state == tego_host_onion_service_state_service_published; + } + + QString UserIdentity::contactID() const try + { + // get host user id and convert to the ricochet:blahlah format + std::unique_ptr userId; + tego_context_get_host_user_id(this->context, tego::out(userId), tego::throw_on_error()); + + std::unique_ptr serviceId; + tego_user_id_get_v3_onion_service_id(userId.get(), tego::out(serviceId), tego::throw_on_error()); + + char serviceIdString[TEGO_V3_ONION_SERVICE_ID_SIZE] = {0}; + tego_v3_onion_service_id_to_string(serviceId.get(), serviceIdString, sizeof(serviceIdString), tego::throw_on_error()); + + QString contactId; + QTextStream(&contactId) << "ricochet:" << serviceIdString; + + return contactId; + } catch (const std::exception& ex){ + qDebug() << "Exception:" << ex.what(); + return QString(""); + } + + shims::ContactsManager* UserIdentity::getContacts() + { + logger::trace(); + return &contacts; + } + + void UserIdentity::setHostOnionServiceState(tego_host_onion_service_state_t state) { + TEGO_THROW_IF_FALSE( + state == tego_host_onion_service_state_none || + state == tego_host_onion_service_state_service_added || + state == tego_host_onion_service_state_service_published); + + const auto newState = static_cast(state); + if (newState != this->hostOnionServiceState) { + this->hostOnionServiceState = newState; + emit this->hostOnionServiceStateChanged(newState); + } + } +} diff --git a/src/irc/shims/UserIdentity.h b/src/irc/shims/UserIdentity.h new file mode 100644 index 00000000..3ab1e675 --- /dev/null +++ b/src/irc/shims/UserIdentity.h @@ -0,0 +1,65 @@ +#pragma once + +#include "ContactsManager.h" +namespace shims +{ + class IncomingContactRequest; + class UserIdentity : public QObject + { + Q_OBJECT + Q_DISABLE_COPY(UserIdentity) + + Q_ENUMS(HostOnionServiceState) + + // needed by createDialog("ContactRequestDialog.qml",...) in main.qml + Q_PROPERTY(QList requests READ getRequests NOTIFY requestsChanged) + // used in TorPreferences.qml + Q_PROPERTY(HostOnionServiceState hostOnionServiceState READ getHostOnionServiceState NOTIFY hostOnionServiceStateChanged) + // this originally had a contactIDChanged signal + Q_PROPERTY(QString contactID READ contactID CONSTANT) + // needed in MainWindow.qml + Q_PROPERTY(shims::ContactsManager *contacts READ getContacts CONSTANT) + public: + enum HostOnionServiceState + { + HostOnionServiceState_None, + HostOnionServiceState_Added, + HostOnionServiceState_Published, + }; + + UserIdentity(tego_context_t* context); + + void createIncomingContactRequest(const QString& hostname, const QString& message); + void removeIncomingContactRequest(shims::IncomingContactRequest* incomingContactRequest); + QList getRequests() const; + bool isServiceOnline() const; + QString contactID() const; + shims::ContactsManager* getContacts(); + + void setOnline(bool); + + static shims::UserIdentity* userIdentity; + shims::ContactsManager contacts; + + tego_context_t* getContext() { return context; } + + void setHostOnionServiceState(tego_host_onion_service_state_t state); + HostOnionServiceState getHostOnionServiceState() const {return this->hostOnionServiceState;} + + signals: + void hostOnionServiceStateChanged(HostOnionServiceState newState); + + // used in main.qml + void requestAdded(shims::IncomingContactRequest *request); + void requestsChanged(); + // used in MainWindow.qml + void unreadCountChanged(ContactUser *user, int unreadCount); + void contactStatusChanged(ContactUser* user, int status); + + private: + QList requests; + + tego_context_t *context; + HostOnionServiceState hostOnionServiceState = HostOnionServiceState_None; + }; +} \ No newline at end of file diff --git a/src/irc/utils/Settings.cpp b/src/irc/utils/Settings.cpp new file mode 100644 index 00000000..3183bb43 --- /dev/null +++ b/src/irc/utils/Settings.cpp @@ -0,0 +1,542 @@ +/* Ricochet - https://ricochet.im/ + * Copyright (C) 2014, John Brooks + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the names of the copyright owners nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Settings.h" + +class SettingsFilePrivate : public QObject +{ + Q_OBJECT + +public: + SettingsFile *q; + QString filePath; + QString errorMessage; + QTimer syncTimer; + QJsonObject jsonRoot; + SettingsObject *rootObject; + + SettingsFilePrivate(SettingsFile *qp); + virtual ~SettingsFilePrivate(); + + void reset(); + void setError(const QString &message); + bool checkDirPermissions(const QString &path); + bool readFile(); + bool writeFile(); + + static QStringList splitPath(const QString &input, bool &ok); + QJsonValue read(const QJsonObject &base, const QStringList &path); + bool write(const QStringList &path, const QJsonValue &value); + +signals: + void modified(const QStringList &path, const QJsonValue &value); + +private slots: + void sync(); +}; + +SettingsFile::SettingsFile(QObject *parent) + : QObject(parent), d(new SettingsFilePrivate(this)) +{ + d->rootObject = new SettingsObject(this, QString()); +} + +SettingsFile::~SettingsFile() +{ +} + +SettingsFilePrivate::SettingsFilePrivate(SettingsFile *qp) + : QObject(qp) + , q(qp) + , rootObject(0) +{ + syncTimer.setInterval(0); + syncTimer.setSingleShot(true); + connect(&syncTimer, &QTimer::timeout, this, &SettingsFilePrivate::sync); +} + +SettingsFilePrivate::~SettingsFilePrivate() +{ + if (syncTimer.isActive()) + sync(); + delete rootObject; +} + +void SettingsFilePrivate::reset() +{ + filePath.clear(); + errorMessage.clear(); + + jsonRoot = QJsonObject(); + emit modified(QStringList(), jsonRoot); +} + +QString SettingsFile::filePath() const +{ + return d->filePath; +} + +bool SettingsFile::setFilePath(const QString &filePath) +{ + if (d->filePath == filePath) + return hasError(); + + d->reset(); + d->filePath = filePath; + + QFileInfo fileInfo(filePath); + QDir dir(fileInfo.path()); + if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) { + d->setError(QStringLiteral("Cannot create directory: %1").arg(dir.path())); + return false; + } + d->checkDirPermissions(fileInfo.path()); + + if (!d->readFile()) + return false; + + return true; +} + +QString SettingsFile::errorMessage() const +{ + return d->errorMessage; +} + +bool SettingsFile::hasError() const +{ + return !d->errorMessage.isEmpty(); +} + +void SettingsFilePrivate::setError(const QString &message) +{ + errorMessage = message; + emit q->error(); +} + +bool SettingsFilePrivate::checkDirPermissions(const QString &path) +{ + static QFile::Permissions desired = QFileDevice::ReadUser | QFileDevice::WriteUser | QFileDevice::ExeUser; + static QFile::Permissions ignored = QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner; + + QFile file(path); + if ((file.permissions() & ~ignored) != desired) { + qDebug() << "Correcting permissions on configuration directory"; + if (!file.setPermissions(desired)) { + qWarning() << "Correcting permissions on configuration directory failed"; + return false; + } + } + + return true; +} + +SettingsObject *SettingsFile::root() +{ + return d->rootObject; +} + +const SettingsObject *SettingsFile::root() const +{ + return d->rootObject; +} + +void SettingsFilePrivate::sync() +{ + if (filePath.isEmpty()) + return; + + syncTimer.stop(); + writeFile(); +} + +bool SettingsFilePrivate::readFile() +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadWrite)) { + setError(file.errorString()); + return false; + } + + QByteArray data = file.readAll(); + if (data.isEmpty() && (file.error() != QFileDevice::NoError || file.size() > 0)) { + setError(file.errorString()); + return false; + } + + if (data.isEmpty()) { + jsonRoot = QJsonObject(); + return true; + } + + QJsonParseError parseError; + QJsonDocument document = QJsonDocument::fromJson(data, &parseError); + if (document.isNull()) { + setError(parseError.errorString()); + return false; + } + + if (!document.isObject()) { + setError(QStringLiteral("Invalid configuration file (expected object)")); + return false; + } + + jsonRoot = document.object(); + + emit modified(QStringList(), jsonRoot); + return true; +} + +bool SettingsFilePrivate::writeFile() +{ + QSaveFile file(filePath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + setError(file.errorString()); + return false; + } + + QJsonDocument document(jsonRoot); + QByteArray data = document.toJson(); + if (data.isEmpty() && !document.isEmpty()) { + setError(QStringLiteral("Encoding failure")); + return false; + } + + if (file.write(data) < data.size() || !file.commit()) { + setError(file.errorString()); + return false; + } + + return true; +} + +QStringList SettingsFilePrivate::splitPath(const QString &input, bool &ok) +{ + QStringList components = input.split(QLatin1Char('.')); + + // Allow a leading '.' to simplify concatenation + if (!components.isEmpty() && components.first().isEmpty()) + components.takeFirst(); + + // No other empty components, including a trailing . + foreach (const QString &word, components) { + if (word.isEmpty()) { + ok = false; + return QStringList(); + } + } + + ok = true; + return components; +} + +QJsonValue SettingsFilePrivate::read(const QJsonObject &base, const QStringList &path) +{ + QJsonValue current = base; + + foreach (const QString &key, path) { + QJsonObject object = current.toObject(); + if (object.isEmpty() || (current = object.value(key)).isUndefined()) + return QJsonValue::Undefined; + } + + return current; +} + +// Compare two QJsonValue to find keys that have changed, +// recursing into objects and building paths as necessary. +typedef QList > ModifiedList; +static void findModifiedRecursive(ModifiedList &modified, const QStringList &path, const QJsonValue &oldValue, const QJsonValue &newValue) +{ + if (oldValue.isObject() || newValue.isObject()) { + // If either is a non-object type, this returns an empty object + QJsonObject oldObject = oldValue.toObject(); + QJsonObject newObject = newValue.toObject(); + + // Iterate keys of the original object and compare to new + for (QJsonObject::iterator it = oldObject.begin(); it != oldObject.end(); it++) { + QJsonValue newSubValue = newObject.value(it.key()); + if (*it == newSubValue) + continue; + + if ((*it).isObject() || newSubValue.isObject()) + findModifiedRecursive(modified, QStringList() << path << it.key(), *it, newSubValue); + else + modified.append(qMakePair(QStringList() << path << it.key(), newSubValue)); + } + + // Iterate keys of the new object that may not be in original + for (QJsonObject::iterator it = newObject.begin(); it != newObject.end(); it++) { + if (oldObject.contains(it.key())) + continue; + + if ((*it).isObject()) + findModifiedRecursive(modified, QStringList() << path << it.key(), QJsonValue::Undefined, it.value()); + else + modified.append(qMakePair(QStringList() << path << it.key(), it.value())); + } + } else + modified.append(qMakePair(path, newValue)); +} + +bool SettingsFilePrivate::write(const QStringList &path, const QJsonValue &value) +{ + typedef QVarLengthArray > ObjectStack; + ObjectStack stack; + QJsonValue current = jsonRoot; + QJsonValue originalValue; + QString currentKey; + + foreach (const QString &key, path) { + const QJsonObject &parent = current.toObject(); + stack.append(qMakePair(currentKey, parent)); + current = parent.value(key); + currentKey = key; + } + + // Stack now contains parent objects starting with the root, and current + // is the old value. Write back changes in reverse. + if (current == value) + return false; + originalValue = current; + current = value; + + for (ObjectStack::const_reverse_iterator it = stack.rbegin(); it != stack.rend(); ++it) + { + QJsonObject update = it->second; + update.insert(currentKey, current); + current = update; + currentKey = it->first; + } + + // current is now the updated jsonRoot + jsonRoot = current.toObject(); + syncTimer.start(); + + ModifiedList modified; + findModifiedRecursive(modified, path, originalValue, value); + + for (ModifiedList::iterator it = modified.begin(); it != modified.end(); it++) + emit this->modified(it->first, it->second); + + return true; +} + +class SettingsObjectPrivate : public QObject +{ + Q_OBJECT + +public: + explicit SettingsObjectPrivate(SettingsObject *q); + + SettingsObject *q; + SettingsFile *file; + QStringList path; + QJsonObject object; + bool invalid; + + void setFile(SettingsFile *file); + +public slots: + void modified(const QStringList &absolutePath, const QJsonValue &value); +}; + +SettingsObject::SettingsObject(QObject *parent) + : QObject(parent) + , d(new SettingsObjectPrivate(this)) +{ + d->setFile(defaultFile()); + if (d->file) + setPath(QString()); +} + +SettingsObject::SettingsObject(const QString &path, QObject *parent) + : QObject(parent) + , d(new SettingsObjectPrivate(this)) +{ + d->setFile(defaultFile()); + setPath(path); +} + +SettingsObject::SettingsObject(SettingsFile *file, const QString &path, QObject *parent) + : QObject(parent) + , d(new SettingsObjectPrivate(this)) +{ + d->setFile(file); + setPath(path); +} + +SettingsObject::SettingsObject(SettingsObject *base, const QString &path, QObject *parent) + : QObject(parent) + , d(new SettingsObjectPrivate(this)) +{ + d->setFile(base->d->file); + setPath(base->path() + QLatin1Char('.') + path); +} + +SettingsObjectPrivate::SettingsObjectPrivate(SettingsObject *qp) + : QObject(qp) + , q(qp) + , file(0) + , invalid(true) +{ +} + +void SettingsObjectPrivate::setFile(SettingsFile *value) +{ + if (file == value) + return; + + if (file) + disconnect(file, 0, this, 0); + file = value; + if (file) + connect(file->d, &SettingsFilePrivate::modified, this, &SettingsObjectPrivate::modified); +} + +// Emit SettingsObject::modified with a relative path if path is matched +void SettingsObjectPrivate::modified(const QStringList &key, const QJsonValue &value) +{ + if (key.size() < path.size()) + return; + + for (int i = 0; i < path.size(); i++) { + if (path[i] != key[i]) + return; + } + + object = file->d->read(file->d->jsonRoot, path).toObject(); + emit q->modified(QStringList(key.mid(path.size())).join(QLatin1Char('.')), value); + emit q->dataChanged(); +} + +static QPointer defaultObjectFile; + +SettingsFile *SettingsObject::defaultFile() +{ + return defaultObjectFile; +} + +void SettingsObject::setDefaultFile(SettingsFile *file) +{ + defaultObjectFile = file; +} + +QString SettingsObject::path() const +{ + return d->path.join(QLatin1Char('.')); +} + +void SettingsObject::setPath(const QString &input) +{ + bool ok = false; + QStringList newPath = SettingsFilePrivate::splitPath(input, ok); + if (!ok) { + d->invalid = true; + d->path.clear(); + d->object = QJsonObject(); + + emit pathChanged(); + emit dataChanged(); + return; + } + + if (!d->invalid && d->path == newPath) + return; + + d->path = newPath; + if (d->file) { + d->invalid = false; + d->object = d->file->d->read(d->file->d->jsonRoot, d->path).toObject(); + emit dataChanged(); + } + + emit pathChanged(); +} + +QJsonObject SettingsObject::data() const +{ + return d->object; +} + +void SettingsObject::setData(const QJsonObject &input) +{ + if (d->invalid || d->object == input) + return; + + d->object = input; + d->file->d->write(d->path, d->object); +} + +QJsonValue SettingsObject::read(const QString &key, const QJsonValue &defaultValue) const +{ + bool ok = false; + QStringList splitKey = SettingsFilePrivate::splitPath(key, ok); + if (d->invalid || !ok || splitKey.isEmpty()) { + qDebug() << "Invalid settings read of path" << key; + return defaultValue; + } + + QJsonValue ret = d->file->d->read(d->object, splitKey); + if (ret.isUndefined()) + ret = defaultValue; + return ret; +} + +void SettingsObject::write(const QString &key, const QJsonValue &value) +{ + bool ok = false; + QStringList splitKey = SettingsFilePrivate::splitPath(key, ok); + if (d->invalid || !ok || splitKey.isEmpty()) { + qDebug() << "Invalid settings write of path" << key; + return; + } + + splitKey = d->path + splitKey; + d->file->d->write(splitKey, value); +} + +void SettingsObject::unset(const QString &key) +{ + write(key, QJsonValue()); +} + +void SettingsObject::undefine() +{ + if (d->invalid) + return; + + d->object = QJsonObject(); + d->file->d->write(d->path, QJsonValue::Undefined); +} + +#include "Settings.moc" diff --git a/src/irc/utils/Settings.h b/src/irc/utils/Settings.h new file mode 100644 index 00000000..d39bd3e9 --- /dev/null +++ b/src/irc/utils/Settings.h @@ -0,0 +1,258 @@ +/* Ricochet - https://ricochet.im/ + * Copyright (C) 2014, John Brooks + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the names of the copyright owners nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef SETTINGS_H +#define SETTINGS_H + +class SettingsObject; +class SettingsFilePrivate; +class SettingsObjectPrivate; + +/* SettingsFile represents a JSON-encoded configuration file. + * + * SettingsFile is an API for reading, writing, and change notification + * on JSON-encoded settings files. + * + * Data is accessed via SettingsObject, either using the root property + * or by creating a SettingsObject, optionally using a base path. + */ +class SettingsFile : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(SettingsFile) + + Q_PROPERTY(SettingsObject *root READ root CONSTANT) + Q_PROPERTY(QString filePath READ filePath WRITE setFilePath NOTIFY filePathChanged) + Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY error) + Q_PROPERTY(bool hasError READ hasError NOTIFY error) + +public: + explicit SettingsFile(QObject *parent = 0); + virtual ~SettingsFile(); + + QString filePath() const; + bool setFilePath(const QString &filePath); + + QString errorMessage() const; + bool hasError() const; + + SettingsObject *root(); + const SettingsObject *root() const; + +signals: + void filePathChanged(); + void error(); + +private: + SettingsFilePrivate *d; + + friend class SettingsObject; + friend class SettingsObjectPrivate; +}; + +/* SettingsObject reads and writes data within a SettingsFile + * + * A SettingsObject is associated with a SettingsFile and represents an object + * tree within that file. It refers to the JSON object tree using a path + * notation with keys separated by '.'. For example: + * + * { + * "one": { + * "two": { + * "three": "value" + * } + * } + * } + * + * With this data, a SettingsObject with an empty path can read with the path + * "one.two.three", and a SettingsObject with a path of "one.two" can simply + * read or write on "three". + * + * Multiple SettingsObjects may be created for the same path, and will be kept + * synchronized with changes. The modified signal is emitted for all changes + * affecting keys within a path, including writes of object trees and from other + * instances. + */ +class SettingsObject : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(SettingsObject) + + Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) + Q_PROPERTY(QJsonObject data READ data WRITE setData NOTIFY dataChanged) + +public: + explicit SettingsObject(QObject *parent = 0); + explicit SettingsObject(const QString &path, QObject *parent = 0); + explicit SettingsObject(SettingsFile *file, const QString &path, QObject *parent = 0); + explicit SettingsObject(SettingsObject *base, const QString &path, QObject *parent = 0); + + /* Specify a SettingsFile to use by default on SettingsObject instances. + * + * After calling setDefaultFile, a SettingsObject created without any file, e.g.: + * + * SettingsObject settings; + * SettingsObject animals(QStringLiteral("animals")); + * + * Will use the specified SettingsFile instance by default. This is a convenience + * over passing around instances of SettingsFile in application use cases, and is + * particularly useful for QML. + */ + static SettingsFile *defaultFile(); + static void setDefaultFile(SettingsFile *file); + + QString path() const; + void setPath(const QString &path); + + QJsonObject data() const; + void setData(const QJsonObject &data); + + Q_INVOKABLE QJsonValue read(const QString &key, const QJsonValue &defaultValue = QJsonValue::Undefined) const; + template T read(const QString &key) const; + Q_INVOKABLE void write(const QString &key, const QJsonValue &value); + template void write(const QString &key, const T &value); + Q_INVOKABLE void unset(const QString &key); + + // const char* key overloads + QJsonValue read(const char *key, const QJsonValue &defaultValue = QJsonValue::Undefined) const + { + return read(QString::fromLatin1(key), defaultValue); + } + template T read(const char *key) const + { + return read(QString::fromLatin1(key)); + } + void write(const char *key, const QJsonValue &value) + { + write(QString::fromLatin1(key), value); + } + template void write(const char *key, const T &value) + { + write(QString::fromLatin1(key), value); + } + template void write_container(K&& key, const T& values) + { + QJsonArray array; + for (auto value : values) { + array.push_back(value); + } + write(std::move(key), array); + } + void unset(const char *key) + { + unset(QString::fromLatin1(key)); + } + + Q_INVOKABLE void undefine(); + +signals: + void pathChanged(); + void dataChanged(); + + void modified(const QString &path, const QJsonValue &value); + +private: + SettingsObjectPrivate *d; +}; + +template inline void SettingsObject::write(const QString &key, const T &value) +{ + write(key, QJsonValue(value)); +} + +template<> inline QString SettingsObject::read(const QString &key) const +{ + return read(key).toString(); +} + +template<> inline QJsonArray SettingsObject::read(const QString &key) const +{ + return read(key).toArray(); +} + +template<> inline QJsonObject SettingsObject::read(const QString &key) const +{ + return read(key).toObject(); +} + +template<> inline double SettingsObject::read(const QString &key) const +{ + return read(key).toDouble(); +} + +template<> inline int SettingsObject::read(const QString &key) const +{ + return read(key).toInt(); +} + +template<> inline bool SettingsObject::read(const QString &key) const +{ + return read(key).toBool(); +} + +template<> inline QDateTime SettingsObject::read(const QString &key) const +{ + QString value = read(key).toString(); + if (value.isEmpty()) + return QDateTime(); + return QDateTime::fromString(value, Qt::ISODate).toLocalTime(); +} + +template<> inline void SettingsObject::write(const QString &key, const QDateTime &value) +{ + write(key, QJsonValue(value.toUTC().toString(Qt::ISODate))); +} + +// Explicitly store value encoded as base64. Decodes and casts implicitly to QByteArray for reads. +class Base64Encode +{ +public: + explicit Base64Encode(const QByteArray &value) : d(value) { } + operator QByteArray() { return d; } + QByteArray encoded() const { return d.toBase64(); } + +private: + QByteArray d; +}; + +template<> inline Base64Encode SettingsObject::read(const QString &key) const +{ + return Base64Encode(QByteArray::fromBase64(read(key).toString().toLatin1())); +} + +template<> inline void SettingsObject::write(const QString &key, const Base64Encode &value) +{ + write(key, QJsonValue(QString::fromLatin1(value.encoded()))); +} + +#endif + diff --git a/src/irc/utils/Useful.h b/src/irc/utils/Useful.h new file mode 100644 index 00000000..3e4cce06 --- /dev/null +++ b/src/irc/utils/Useful.h @@ -0,0 +1,83 @@ +/* Ricochet - https://ricochet.im/ + * Copyright (C) 2014, John Brooks + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the names of the copyright owners nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef UTILS_USEFUL_H +#define UTILS_USEFUL_H + +/* Print a warning for bug conditions, and assert on a debug build. + * + * This should be used in place of Q_ASSERT for bug conditions, along + * with a proper error case for release-mode builds. For example: + * + * if (!connection || !user) { + * TEGO_BUG() << "Request" << request << "should have a connection and user"; + * return false; + * } + * + * Do not confuse bugs with actual error cases; TEGO_BUG() should never be + * triggered unless the code or logic is wrong. + */ +#if !defined(QT_NO_DEBUG) || defined(QT_FORCE_ASSERTS) +# define TEGO_BUG() Explode(__FILE__,__LINE__), qWarning() << "BUG:" +namespace { +class Explode +{ +public: + const char *file; + int line; + Explode(const char *file, int line) : file(file), line(line) { } + ~Explode() { + qt_assert("something broke!", file, line); + } +}; +} +#else +# define TEGO_BUG() qWarning() << "BUG:" +#endif + +/* + * helper function for safely casting to QT sizes (generally an int) + * throws if the conversion overflows the target conversion type + */ +template +T safe_cast(F from) +{ + if (from >= std::numeric_limits::max()) + { + TEGO_BUG() << "Invalid safe_cast. Value: " << from + << "; Max value of target type: " << std::numeric_limits::max(); + } + return static_cast(from); +} + +#endif +