diff --git a/CMakeLists.txt b/CMakeLists.txt index 939e3bc..770b4a9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,11 +9,13 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) cmake_policy(SET CMP0076 NEW) -find_package(PkgConfig) +find_package(PkgConfig REQUIRED) pkg_search_module(GLOOX REQUIRED gloox) find_package(yaml-cpp REQUIRED) +pkg_check_modules(UUID REQUIRED uuid) + set(EXEC_NAME "jay") add_executable(${EXEC_NAME} main.cpp jay.cpp) @@ -25,9 +27,11 @@ add_subdirectory(shared) target_include_directories(${EXEC_NAME} PRIVATE ${GLOOX_INCLUDE_DIRS}) target_include_directories(${EXEC_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(${EXEC_NAME} PRIVATE ${UUID_INCLUDE_DIRS}) target_link_libraries(${EXEC_NAME} PRIVATE ${GLOOX_LIBRARIES} yaml-cpp + ${UUID_LIBRARIES} ) install(TARGETS ${EXEC_NAME} RUNTIME DESTINATION bin) diff --git a/component/core.cpp b/component/core.cpp index cb3aa39..5f2dfa9 100644 --- a/component/core.cpp +++ b/component/core.cpp @@ -51,6 +51,16 @@ void Core::setGroup(const std::string& jid, const std::string& group) { config.setActors(actors); } +void Core::publish(const std::string& service, const std::string& node, const std::string& title, const std::string& body) { + std::shared_ptr cn = connection.lock(); + if (!cn) { + logger.log(Shared::Logger::warning, "Couldn't publish to " + node + "@" + service + ", connection is not available", {"Core"}); + return; + } + + cn->publish(service, node, title, body); +} + void Core::initializeActors() { for (const std::pair& pair : config.getActors()) { logger.log(Shared::Logger::info, "registering actor " + pair.first + " as " + pair.second, {"Core"}); diff --git a/component/core.h b/component/core.h index 3272694..46aa93e 100644 --- a/component/core.h +++ b/component/core.h @@ -19,6 +19,7 @@ public: void send(const std::string& jid, const std::string& body); void initialize(const std::shared_ptr& connection); void setGroup(const std::string& jid, const std::string& group); + void publish(const std::string& service, const std::string& node, const std::string& title, const std::string& body); public: Config config; diff --git a/component/router.cpp b/component/router.cpp index b094c68..5e265af 100644 --- a/component/router.cpp +++ b/component/router.cpp @@ -41,7 +41,7 @@ void Router::routeMessage(const std::string& sender, const std::string& body) { if (aItr == actors.end()) aItr = actors.emplace(sender, std::make_shared(sender, defaultGroup)).first; - std::vector args = Module::Module::split(body); + Shared::Strings args = Shared::split(body); std::string moduleAlias = Module::Module::lower(args[0]); Modules::iterator mItr = modules.find(moduleAlias); diff --git a/component/router.h b/component/router.h index bacb578..583659a 100644 --- a/component/router.h +++ b/component/router.h @@ -11,6 +11,7 @@ #include "shared/result.h" #include "shared/loggable.h" +#include "shared/utils.h" #include "actor.h" namespace Module { diff --git a/connection/connection.cpp b/connection/connection.cpp index e3b2519..bbcd202 100644 --- a/connection/connection.cpp +++ b/connection/connection.cpp @@ -7,7 +7,8 @@ Connection::Connection(const std::shared_ptr& core): Shared::Loggable(core->logger, {"Connection"}), state(initial), core(core), - gloox() + gloox(), + pubsub() {} Connection::~Connection() noexcept { @@ -33,6 +34,8 @@ void Connection::initialize() { gloox->setSASLMechanisms(gloox::SaslMechAll); gloox->setStreamManagement(true, true); + pubsub = std::make_unique(gloox.get()); + state = disconnected; } @@ -65,6 +68,42 @@ void Connection::send(const std::string& jid, const std::string& body) { gloox->send(gloox::Message(gloox::Message::Chat, jid, body)); } +void Connection::publish(const std::string& service, const std::string& node, const std::string& title, const std::string& body) { + debug("publishing an article \"" + title + "\" to " + node + "@" + service); + + gloox::Tag* entry = new gloox::Tag("entry"); + entry->setXmlns("http://www.w3.org/2005/Atom"); + + entry->addChild(new gloox::Tag("id", "urn:uuid:" + Shared::getUUID())); + entry->addChild(new gloox::Tag("title", title)); + entry->addChild(new gloox::Tag("summary", body)); + entry->addChild(new gloox::Tag("updated", Shared::getISOTimestamp())); + entry->addChild(new gloox::Tag("published", Shared::getISOTimestamp())); + + gloox::PubSub::Item* item = new gloox::PubSub::Item(); + item->setPayload(entry); + gloox::PubSub::ItemList list({item}); + + pubsub->publishItem(service, node, list, nullptr, this); +} + +std::string Connection::errorTypeToString(gloox::StanzaErrorType err) { + switch (err) { + case gloox::StanzaErrorTypeAuth: + return "Authentication"; + case gloox::StanzaErrorTypeCancel: + return "Cancel"; + case gloox::StanzaErrorTypeContinue: + return "Continue"; + case gloox::StanzaErrorTypeModify: + return "Modify"; + case gloox::StanzaErrorTypeWait: + return "Wait"; + case gloox::StanzaErrorTypeUndefined: + return "Undefined"; + } +} + void Connection::handleMessage(const gloox::Message& message, gloox::MessageSession* session) { if (message.subtype() != gloox::Message::Chat) return; @@ -78,9 +117,21 @@ void Connection::handleMessage(const gloox::Message& message, gloox::MessageSess core->router.routeMessage(jid, body); } +void Connection::handleItemPublication(const std::string& id, const gloox::JID& service, const std::string& node, const gloox::PubSub::ItemList& itemList, const gloox::Error* err) { + std::string srv(node + "@" + service.full()); + + if (err) { + error("Publish failed to " + srv + ", Error: [" + errorTypeToString(err->type()) + "]"); + return; + } + + info("Publish successful to " + srv + ", ID: " + id); +} + void Connection::onConnect() { info("connection established"); } + void Connection::onDisconnect(gloox::ConnectionError e) { std::string error; @@ -148,6 +199,7 @@ void Connection::onDisconnect(gloox::ConnectionError e) { else Loggable::error("disconnected: " + error); } + bool Connection::onTLSConnect(const gloox::CertInfo&) { info("TLS established"); return true; diff --git a/connection/connection.h b/connection/connection.h index c079ddb..0e4395e 100644 --- a/connection/connection.h +++ b/connection/connection.h @@ -10,14 +10,19 @@ #include #include #include +#include +#include +#include #include "shared/loggable.h" +#include "shared/utils.h" #include "component/core.h" class Connection: private Shared::Loggable, public gloox::ConnectionListener, - public gloox::MessageHandler + public gloox::MessageHandler, + public gloox::PubSub::ResultHandler { public: enum State { @@ -34,6 +39,9 @@ public: void deinitialize(); void connect(); void send(const std::string& jid, const std::string& body); + void publish(const std::string& service, const std::string& node, const std::string& title, const std::string& body); + + static std::string errorTypeToString(gloox::StanzaErrorType err); public: void onConnect() override; @@ -41,9 +49,33 @@ public: bool onTLSConnect(const gloox::CertInfo&) override; void handleMessage(const gloox::Message& message, gloox::MessageSession* session = 0) override; + void handleItemPublication(const std::string& id, const gloox::JID& service, const std::string& node, const gloox::PubSub::ItemList& itemList, const gloox::Error* error = 0) override; + + // All other methods are not needed; make them no-op + void handleItem(const gloox::JID&, const std::string&, const gloox::Tag*) override {} + void handleItems(const std::string&, const gloox::JID&, const std::string&, const gloox::PubSub::ItemList&, const gloox::Error* = 0) override {} + void handleItemDeletion(const std::string&, const gloox::JID&, const std::string&, const gloox::PubSub::ItemList&, const gloox::Error* = 0) override {} + void handleSubscriptionResult(const std::string&, const gloox::JID&, const std::string&, const std::string&, const gloox::JID&, const gloox::PubSub::SubscriptionType, const gloox::Error* = 0) override {} + void handleUnsubscriptionResult(const std::string&, const gloox::JID&, const gloox::Error* = 0) override {} + void handleSubscriptionOptions(const std::string&, const gloox::JID&, const gloox::JID&, const std::string&, const gloox::DataForm*, const std::string& = gloox::EmptyString, const gloox::Error* = 0) override {} + void handleSubscriptionOptionsResult(const std::string&, const gloox::JID&, const gloox::JID&, const std::string&, const std::string& = gloox::EmptyString, const gloox::Error* = 0) override {} + void handleSubscribers(const std::string&, const gloox::JID&, const std::string&, const gloox::PubSub::SubscriptionList&, const gloox::Error* = 0) override {} + void handleSubscribersResult(const std::string&, const gloox::JID&, const std::string&, const gloox::PubSub::SubscriberList*, const gloox::Error* = 0) override {} + void handleAffiliates(const std::string&, const gloox::JID&, const std::string&, const gloox::PubSub::AffiliateList*, const gloox::Error* = 0) override {} + void handleAffiliatesResult(const std::string&, const gloox::JID&, const std::string&, const gloox::PubSub::AffiliateList*, const gloox::Error* = 0) override {} + void handleNodeConfig(const std::string&, const gloox::JID&, const std::string&, const gloox::DataForm*, const gloox::Error* = 0) override {} + void handleNodeConfigResult(const std::string&, const gloox::JID&, const std::string&, const gloox::Error* = 0) override {} + void handleNodeCreation(const std::string&, const gloox::JID&, const std::string&, const gloox::Error* = 0) override {} + void handleNodeDeletion(const std::string&, const gloox::JID&, const std::string&, const gloox::Error* = 0) override {} + void handleNodePurge(const std::string&, const gloox::JID&, const std::string&, const gloox::Error* = 0) override {} + void handleSubscriptions(const std::string&, const gloox::JID&, const gloox::PubSub::SubscriptionMap&, const gloox::Error* = 0) override {} + void handleAffiliations(const std::string&, const gloox::JID&, const gloox::PubSub::AffiliationMap&, const gloox::Error* = 0) override {} + void handleDefaultNodeConfig(const std::string&, const gloox::JID&, const gloox::DataForm*, const gloox::Error* = 0) override {} + private: State state; std::shared_ptr core; std::unique_ptr gloox; + std::unique_ptr pubsub; }; diff --git a/example.config.yml b/example.config.yml index f63d888..ba1eba9 100644 --- a/example.config.yml +++ b/example.config.yml @@ -17,6 +17,12 @@ modules: permissions: read: [Owner, User] write: [Owner] + + publish: + alias: publish + enabled: true + permissions: + publish: [Owner] replies: success: [] diff --git a/jay.cpp b/jay.cpp index 7c47a7a..a0b811b 100644 --- a/jay.cpp +++ b/jay.cpp @@ -4,6 +4,7 @@ #include "jay.h" #include "module/actor.h" +#include "module/publish.h" static const std::map< std::string, @@ -17,7 +18,11 @@ static const std::map< {"actor", []( const std::shared_ptr& core, const Shared::Permissions& permissions - ) { return std::make_shared(core, permissions); }} + ) { return std::make_shared(core, permissions); }}, + {"publish", []( + const std::shared_ptr& core, + const Shared::Permissions& permissions + ) { return std::make_shared(core, permissions); }} }; Jay::Jay(const std::string& configPath): diff --git a/module/CMakeLists.txt b/module/CMakeLists.txt index ab70f89..0224a26 100644 --- a/module/CMakeLists.txt +++ b/module/CMakeLists.txt @@ -1,11 +1,13 @@ set(SOURCES module.cpp actor.cpp + publish.cpp ) set(HEADERS module.h actor.h + publish.h ) target_sources(${EXEC_NAME} PRIVATE ${SOURCES}) diff --git a/module/module.cpp b/module/module.cpp index 7c4f47f..6dc68ae 100644 --- a/module/module.cpp +++ b/module/module.cpp @@ -23,21 +23,6 @@ bool Module::Module::hasPermission(const std::string& permission, const std::sha Module::Module::~Module() noexcept {} -std::vector Module::Module::split(const std::string& string, const std::string& delimiter) { - std::vector result; - - std::size_t last = 0; - std::size_t next = string.find(delimiter, last); - while (next != std::string::npos) { - result.emplace_back(string.substr(last, next - last)); - last = next + 1; - next = string.find(delimiter, last); - } - result.emplace_back(string.substr(last)); - - return result; -} - std::string Module::Module::lower(const std::string& text) { return std::ranges::to(text | std::views::transform(::tolower)); } diff --git a/module/module.h b/module/module.h index 2881a5e..e835b74 100644 --- a/module/module.h +++ b/module/module.h @@ -26,7 +26,6 @@ protected: public: virtual ~Module() noexcept; - static Shared::Strings split(const std::string& string, const std::string& delimiter = " "); static std::string lower(const std::string& text); virtual Shared::Result message(const std::shared_ptr<::Actor>& actor, const Shared::Strings& args) = 0; diff --git a/module/publish.cpp b/module/publish.cpp new file mode 100644 index 0000000..bb1af3d --- /dev/null +++ b/module/publish.cpp @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2024 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "publish.h" + +#include + +Module::Publish::Publish(const std::shared_ptr& core, const Shared::Permissions& permissions): + Module(core, permissions, "Actor") +{} + +Module::Publish::~Publish() noexcept {} + +Shared::Result Module::Publish::message(const std::shared_ptr<::Actor>& actor, const Shared::Strings& args) { + if (args.front() == "to") { + if (!hasPermission("publish", actor)) + return Shared::forbidden; + + if (args.size() < 3) + return Shared::error; + + return to(args[1], args[2]); + } + + return Shared::unhandled; +} + +Shared::Result Module::Publish::to(const std::string& address, const std::string& body) { + Shared::Strings parts = Shared::split(address, "@"); + if (parts.size() != 2) { + warn("Malformed address in \"to\" method"); + return Shared::error; + } + + try { + core->publish(parts[1], parts[0], "Completely testing stuff, early stages, ignore please", body); + return Shared::success; + } catch (const std::exception& e) { + error("Exception in \"to\" method: " + std::string(e.what())); + } catch (...) { + error("Unhandled exception in \"to\" method"); + } + + return Shared::error; +} diff --git a/module/publish.h b/module/publish.h new file mode 100644 index 0000000..463a393 --- /dev/null +++ b/module/publish.h @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2024 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "shared/definitions.h" +#include "shared/result.h" +#include "shared/utils.h" + +#include "module.h" + +namespace Module { + +class Publish : public Module { +public: + Publish(const std::shared_ptr& core, const Shared::Permissions& permissions); + ~Publish() noexcept; + + virtual Shared::Result message(const std::shared_ptr<::Actor>& actor, const Shared::Strings& args) override; + +private: + Shared::Result to(const std::string& address, const std::string& body); +}; + +} \ No newline at end of file diff --git a/shared/CMakeLists.txt b/shared/CMakeLists.txt index d08bcb0..e590cf4 100644 --- a/shared/CMakeLists.txt +++ b/shared/CMakeLists.txt @@ -1,6 +1,7 @@ set(SOURCES logger.cpp loggable.cpp + utils.cpp ) set(HEADERS @@ -8,6 +9,7 @@ set(HEADERS loggable.h definitions.h result.h + utils.h ) target_sources(${EXEC_NAME} PRIVATE ${SOURCES}) \ No newline at end of file diff --git a/shared/utils.cpp b/shared/utils.cpp new file mode 100644 index 0000000..c4de6f5 --- /dev/null +++ b/shared/utils.cpp @@ -0,0 +1,32 @@ +#include "utils.h" + +std::string Shared::getISOTimestamp() { + std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); + std::chrono::time_point time = std::chrono::floor(now); + + return std::format("{:%FT%TZ}", time); +} + +std::string Shared::getUUID() { + uuid_t uuid; + uuid_generate_random(uuid); + char uuid_str[37]; + uuid_unparse_lower(uuid, uuid_str); + + return std::string(uuid_str); +} + +Shared::Strings Shared::split(const std::string& string, const std::string& delimiter) { + Strings result; + + std::size_t last = 0; + std::size_t next = string.find(delimiter, last); + while (next != std::string::npos) { + result.emplace_back(string.substr(last, next - last)); + last = next + 1; + next = string.find(delimiter, last); + } + result.emplace_back(string.substr(last)); + + return result; +} diff --git a/shared/utils.h b/shared/utils.h new file mode 100644 index 0000000..6810843 --- /dev/null +++ b/shared/utils.h @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2024 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +#include + +#include "definitions.h" + +namespace Shared { + std::string getISOTimestamp(); + std::string getUUID(); + Shared::Strings split(const std::string& string, const std::string& delimiter = " "); +} \ No newline at end of file