diff --git a/CMakeLists.txt b/CMakeLists.txt index afd8311..e8f7d11 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,13 +7,29 @@ project(jay set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -# Find gloox library +cmake_policy(SET CMP0076 NEW) + find_package(PkgConfig) pkg_search_module(GLOOX REQUIRED gloox) -add_executable(jay main.cpp jay.cpp logger.cpp) +find_package(yaml-cpp REQUIRED) -target_include_directories(jay PRIVATE ${GLOOX_INCLUDE_DIRS}) -target_link_libraries(jay ${GLOOX_LIBRARIES}) +set(EXEC_NAME "jay") -install(TARGETS jay RUNTIME DESTINATION bin) +add_executable(${EXEC_NAME} + main.cpp + jay.cpp + logger.cpp + config.cpp +) + +add_subdirectory(handlers) + +target_include_directories(${EXEC_NAME} PRIVATE ${GLOOX_INCLUDE_DIRS}) +target_include_directories(${EXEC_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(${EXEC_NAME} PRIVATE + ${GLOOX_LIBRARIES} + yaml-cpp +) + +install(TARGETS ${EXEC_NAME} RUNTIME DESTINATION bin) diff --git a/config.cpp b/config.cpp new file mode 100644 index 0000000..bdd7f30 --- /dev/null +++ b/config.cpp @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2024 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "config.h" + +Config::Config(const std::string& path): + root(YAML::LoadFile(path)) +{} + +std::string Config::getBareJID() const { + return root["jid"].as(); +} + +std::string Config::getPassword() const { + return root["password"].as(); +} + +std::string Config::getResource() const { + return root["resource"].as("bot"); +} + +std::string Config::getFullJID() const { + return getBareJID() + "/" + getResource(); +} + +gloox::LogLevel Config::getLogLevel() const { + std::string level = root["logLevel"].as("warning"); + + if (level == "debug") + return gloox::LogLevelDebug; + else if (level == "error") + return gloox::LogLevelError; + else + return gloox::LogLevelWarning; +} + +gloox::TLSPolicy Config::getTLSPolicy() const { + std::string level = root["tls"].as("optional"); + + if (level == "disabled") + return gloox::TLSDisabled; + else if (level == "required") + return gloox::TLSRequired; + else + return gloox::TLSOptional; +} + + +std::set Config::getOwners() const { + std::set result; + YAML::Node owners = root["resource"]; + if (!owners.IsSequence()) + return result; + + for (const YAML::Node& node : owners) + result.insert(node.as()); + + return result; +} + +bool Config::isValid() const { + return !getBareJID().empty() && !getPassword().empty(); +} diff --git a/config.h b/config.h new file mode 100644 index 0000000..0c61043 --- /dev/null +++ b/config.h @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +#include "gloox/gloox.h" + +#include "yaml-cpp/yaml.h" + +class Config { +public: + Config(const std::string& path); + + bool isValid() const; + + std::string getBareJID() const; + std::string getFullJID() const; + std::string getPassword() const; + std::string getResource() const; + std::set getOwners() const; + gloox::LogLevel getLogLevel() const; + gloox::TLSPolicy getTLSPolicy() const; + +private: + YAML::Node root; +}; diff --git a/example.config.yml b/example.config.yml new file mode 100644 index 0000000..f2e2ef9 --- /dev/null +++ b/example.config.yml @@ -0,0 +1,8 @@ +jid: bot@xmpp.server +password: "supersecret" +logLevel: debug +tls: required +resource: bot +owners: + - user1@xmpp.server + - user2@xmpp.server diff --git a/handlers/CMakeLists.txt b/handlers/CMakeLists.txt new file mode 100644 index 0000000..1c5525d --- /dev/null +++ b/handlers/CMakeLists.txt @@ -0,0 +1,11 @@ +set(SOURCES + message.cpp + connection.cpp +) + +set(HEADERS + message.h + connection.h +) + +target_sources(${EXEC_NAME} PRIVATE ${SOURCES}) diff --git a/handlers/connection.cpp b/handlers/connection.cpp new file mode 100644 index 0000000..63efaa5 --- /dev/null +++ b/handlers/connection.cpp @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2024 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "connection.h" + +Connection::Connection(const std::shared_ptr& config, const std::shared_ptr& client): + config(config), + client(client) +{ + client->registerConnectionListener(this); +} + +Connection::~Connection() { + if (std::shared_ptr cl = client.lock()) + cl->removeConnectionListener(this); +} + + +void Connection::onConnect() {} + +void Connection::onDisconnect(gloox::ConnectionError e) {} + +bool Connection::onTLSConnect(const gloox::CertInfo&) { + return true; +} + + diff --git a/handlers/connection.h b/handlers/connection.h new file mode 100644 index 0000000..c67a48a --- /dev/null +++ b/handlers/connection.h @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2024 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "gloox/client.h" +#include "gloox/connectionlistener.h" + +#include "config.h" + +class Connection : public gloox::ConnectionListener { +public: + Connection(const std::shared_ptr& config, const std::shared_ptr& client); + ~Connection(); + + void onConnect() override; + void onDisconnect(gloox::ConnectionError e) override; + bool onTLSConnect(const gloox::CertInfo&) override; + +private: + std::weak_ptr config; + std::weak_ptr client; +}; diff --git a/handlers/message.cpp b/handlers/message.cpp new file mode 100644 index 0000000..0525884 --- /dev/null +++ b/handlers/message.cpp @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2024 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "message.h" + +#include + +Message::Message(const std::shared_ptr& config, const std::shared_ptr& client): + config(config), + client(client) +{ + client->registerMessageHandler(this); +} + +Message::~Message() { + if (std::shared_ptr cl = client.lock()) + cl->removeMessageHandler(this); +} + +void Message::handleMessage(const gloox::Message& message, gloox::MessageSession* session) { + std::shared_ptr cfg = config.lock(); + if (!cfg) + return; + + if (cfg->getOwners().count(message.from().bare())) { + std::cout << "Received message from owner: " << message.body() << std::endl; + } else { + std::cout << "Received message: " << message.body() << std::endl; + } +} diff --git a/handlers/message.h b/handlers/message.h new file mode 100644 index 0000000..e44172a --- /dev/null +++ b/handlers/message.h @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2024 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include +#include +#include + +#include "config.h" + +class Message : public gloox::MessageHandler { +public: + Message(const std::shared_ptr& config, const std::shared_ptr& client); + ~Message(); + + void handleMessage(const gloox::Message& message, gloox::MessageSession* session = 0) override; + +private: + std::weak_ptr config; + std::weak_ptr client; +}; diff --git a/jay.cpp b/jay.cpp index 78760c2..48774bc 100644 --- a/jay.cpp +++ b/jay.cpp @@ -3,59 +3,57 @@ #include "jay.h" -Jay::Jay(const std::string& jid, const std::string& password) : - client(jid, password), - loggers(), - owners() -{ - client.registerMessageHandler(this); - client.registerConnectionListener(this); - - client.setTls(gloox::TLSPolicy::TLSOptional); - client.setSASLMechanisms(gloox::SaslMechScramSha1); -} +Jay::Jay(const std::string& configPath): + runMutex(), + config(std::make_shared(configPath)), + client(), + messageHandler(), + connectionHandler(), + loggers() +{} Jay::~Jay() {} +bool Jay::isConfigValid() const { + return config->isValid(); +} + + void Jay::run() { - if (client.connect(false)) { - // Run the event loop - gloox::ConnectionError ce = gloox::ConnNoError; - while (ce == gloox::ConnNoError) - ce = client.recv(); + std::lock_guard lock(runMutex); - std::cout << "Connection terminated with error: " << ce << std::endl; - } - std::cout << "Out of loop" << std::endl; + initialize(); + client->connect(true); } -void Jay::handleMessage(const gloox::Message& message, gloox::MessageSession* session) { - if (owners.count(message.from().bare())) { - std::cout << "Received message from owner: " << message.body() << std::endl; - } else { - std::cout << "Received message: " << message.body() << std::endl; - } +void Jay::initialize() { + createClient(); + + if (!messageHandler) + messageHandler = std::make_unique(config, client); + + if (!connectionHandler) + connectionHandler = std::make_unique(config, client); } -void Jay::onConnect() { - for (const std::string& owner : owners) - client.send(gloox::Message(gloox::Message::Chat, owner, "I'm online!")); -} +void Jay::createClient() { + if (client) + return; -void Jay::onDisconnect(gloox::ConnectionError e) { - std::cout << "Disconnected: " << e << std::endl; -} + client = std::make_shared(config->getFullJID(), config->getPassword()); + addLogger(config->getLogLevel()); -bool Jay::onTLSConnect(const gloox::CertInfo&) { - return true; + gloox::Disco* disco = client->disco(); + + disco->setVersion("Jay", "0.0.1"); + disco->setIdentity("client", "bot"); + + client->setTls(config->getTLSPolicy()); + client->setSASLMechanisms(gloox::SaslMechAll); + client->setStreamManagement(true, true); } -Logger* Jay::addLogger(gloox::LogLevel level) { - loggers.emplace_back(std::make_unique(client.logInstance(), level)); - return loggers.back().get(); -} - -void Jay::addOwner(const std::string& jid) { - owners.insert(jid); +void Jay::addLogger(gloox::LogLevel level) { + loggers.emplace_back(std::make_unique(client->logInstance(), level)); } diff --git a/jay.h b/jay.h index f0fe925..e35f353 100644 --- a/jay.h +++ b/jay.h @@ -4,35 +4,39 @@ #pragma once #include -#include #include #include #include +#include #include -#include +#include #include -#include #include "logger.h" +#include "config.h" +#include "handlers/message.h" +#include "handlers/connection.h" -class Jay : public gloox::MessageHandler, public gloox::ConnectionListener { +class Jay { public: - Jay(const std::string& jid, const std::string& password); + Jay(const std::string& configPath); ~Jay(); - void handleMessage(const gloox::Message& message, gloox::MessageSession* session = 0) override; - void onConnect() override; - void onDisconnect(gloox::ConnectionError e) override; - bool onTLSConnect(const gloox::CertInfo&) override; + bool isConfigValid() const; void run(); - Logger* addLogger(gloox::LogLevel level); - void addOwner(const std::string& jid); +private: + void addLogger(gloox::LogLevel level); + void initialize(); + void createClient(); private: - gloox::Client client; + std::mutex runMutex; + std::shared_ptr config; + std::shared_ptr client; + std::unique_ptr messageHandler; + std::unique_ptr connectionHandler; std::vector> loggers; - std::set owners; }; diff --git a/main.cpp b/main.cpp index 6562218..a6c1e7d 100644 --- a/main.cpp +++ b/main.cpp @@ -16,23 +16,17 @@ std::string readEnv(const std::string& key, const std::string& defaultValue = "" return defaultValue; } -int main(int argc, char** argv) { - std::string jid = readEnv("JID"); - std::string password = readEnv("PASSWORD"); - - if (jid.empty() || password.empty()) { - std::cout << "You need to provide JID and PASSWORD environment variables" << std::endl; - return - 1; +int main(int argc, char* argv[]) { + if (argc < 2) { + std::cerr << "Usage: " << argv[0] << " " << std::endl; + return EXIT_FAILURE; } - Jay bot(jid, password); - bot.addLogger(gloox::LogLevelDebug); - - std::string owners = readEnv("OWNERS"); - std::stringstream ss(owners); - std::string owner; - while (std::getline(ss, owner, ',')) - bot.addOwner(owner); + Jay bot(argv[1]); + if (!bot.isConfigValid()) { + std::cerr << "Invalid config, can not proceed, quitting" << std::endl; + return EXIT_FAILURE; + } bot.run();