diff options
Diffstat (limited to 'src/xmpp')
-rw-r--r-- | src/xmpp/biboumi_adhoc_commands.cpp | 179 | ||||
-rw-r--r-- | src/xmpp/biboumi_adhoc_commands.hpp | 5 | ||||
-rw-r--r-- | src/xmpp/biboumi_component.cpp | 384 | ||||
-rw-r--r-- | src/xmpp/biboumi_component.hpp | 28 |
4 files changed, 510 insertions, 86 deletions
diff --git a/src/xmpp/biboumi_adhoc_commands.cpp b/src/xmpp/biboumi_adhoc_commands.cpp index eec930d..003b901 100644 --- a/src/xmpp/biboumi_adhoc_commands.cpp +++ b/src/xmpp/biboumi_adhoc_commands.cpp @@ -1,9 +1,13 @@ #include <xmpp/biboumi_adhoc_commands.hpp> #include <xmpp/biboumi_component.hpp> +#include <utils/scopeguard.hpp> +#include <bridge/bridge.hpp> #include <config/config.hpp> #include <utils/string.hpp> #include <utils/split.hpp> #include <xmpp/jid.hpp> +#include <algorithm> +#include <iomanip> #include <biboumi.h> @@ -11,9 +15,9 @@ #include <database/database.hpp> #endif -#include <louloulibs.h> - -#include <algorithm> +#ifndef HAS_PUT_TIME +#include <ctime> +#endif using namespace std::string_literals; @@ -114,6 +118,96 @@ void DisconnectUserStep2(XmppComponent& xmpp_component, AdhocSession& session, X } #ifdef USE_DATABASE + +void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node) +{ + const Jid owner(session.get_owner_jid()); + const Jid target(session.get_target_jid()); + + auto options = Database::get_global_options(owner.bare()); + + XmlNode x("jabber:x:data:x"); + x["type"] = "form"; + XmlNode title("title"); + title.set_inner("Configure some global default settings."); + x.add_child(std::move(title)); + XmlNode instructions("instructions"); + instructions.set_inner("Edit the form, to configure your global settings for the component."); + x.add_child(std::move(instructions)); + + XmlNode required("required"); + + XmlNode max_histo_length("field"); + max_histo_length["var"] = "max_history_length"; + max_histo_length["type"] = "text-single"; + max_histo_length["label"] = "Max history length"; + max_histo_length["desc"] = "The maximum number of lines in the history that the server sends when joining a channel"; + + XmlNode value("value"); + value.set_inner(std::to_string(options.maxHistoryLength.value())); + max_histo_length.add_child(std::move(value)); + x.add_child(std::move(max_histo_length)); + + XmlNode record_history("field"); + record_history["var"] = "record_history"; + record_history["type"] = "boolean"; + record_history["label"] = "Record history"; + record_history["desc"] = "Whether to save the messages into the database, or not"; + + value.set_name("value"); + if (options.recordHistory.value()) + value.set_inner("true"); + else + value.set_inner("false"); + record_history.add_child(std::move(value)); + x.add_child(std::move(record_history)); + + command_node.add_child(std::move(x)); +} + +void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node) +{ + BiboumiComponent& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component); + + const XmlNode* x = command_node.get_child("x", "jabber:x:data"); + if (x) + { + const Jid owner(session.get_owner_jid()); + auto options = Database::get_global_options(owner.bare()); + for (const XmlNode* field: x->get_children("field", "jabber:x:data")) + { + const XmlNode* value = field->get_child("value", "jabber:x:data"); + + if (field->get_tag("var") == "max_history_length" && + value && !value->get_inner().empty()) + options.maxHistoryLength = value->get_inner(); + else if (field->get_tag("var") == "record_history" && + value && !value->get_inner().empty()) + { + options.recordHistory = to_bool(value->get_inner()); + Bridge* bridge = biboumi_component.find_user_bridge(owner.bare()); + if (bridge) + bridge->set_record_history(options.recordHistory.value()); + } + } + + options.update(); + + command_node.delete_all_children(); + XmlNode note("note"); + note["type"] = "info"; + note.set_inner("Configuration successfully applied."); + command_node.add_child(std::move(note)); + return; + } + XmlNode error(ADHOC_NS":error"); + error["type"] = "modify"; + XmlNode condition(STANZA_NS":bad-request"); + error.add_child(std::move(condition)); + command_node.add_child(std::move(error)); + session.terminate(); +} + void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node) { const Jid owner(session.get_owner_jid()); @@ -315,7 +409,7 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com } else if (field->get_tag("var") == "verify_cert" && value - && !value->get_inner().empty()) + && !value->get_inner().empty()) { auto val = to_bool(value->get_inner()); options.verifyCert = val; @@ -381,7 +475,7 @@ void ConfigureIrcChannelStep1(XmppComponent&, AdhocSession& session, XmlNode& co { const Jid owner(session.get_owner_jid()); const Jid target(session.get_target_jid()); - const Iid iid(target.local); + const Iid iid(target.local, {}); auto options = Database::get_irc_channel_options_with_server_default(owner.local + "@" + owner.domain, iid.get_server(), iid.get_local()); @@ -434,7 +528,7 @@ void ConfigureIrcChannelStep2(XmppComponent&, AdhocSession& session, XmlNode& co { const Jid owner(session.get_owner_jid()); const Jid target(session.get_target_jid()); - const Iid iid(target.local); + const Iid iid(target.local, {}); auto options = Database::get_irc_channel_options(owner.local + "@" + owner.domain, iid.get_server(), iid.get_local()); for (const XmlNode* field: x->get_children("field", "jabber:x:data")) @@ -442,7 +536,7 @@ void ConfigureIrcChannelStep2(XmppComponent&, AdhocSession& session, XmlNode& co const XmlNode* value = field->get_child("value", "jabber:x:data"); if (field->get_tag("var") == "encoding_out" && - value && !value->get_inner().empty()) + value && !value->get_inner().empty()) options.encodingOut = value->get_inner(); else if (field->get_tag("var") == "encoding_in" && @@ -633,3 +727,74 @@ void DisconnectUserFromServerStep3(XmppComponent& xmpp_component, AdhocSession& note.set_inner(msg); command_node.add_child(std::move(note)); } + +void GetIrcConnectionInfoStep1(XmppComponent& component, AdhocSession& session, XmlNode& command_node) +{ + BiboumiComponent& biboumi_component = static_cast<BiboumiComponent&>(component); + + const Jid owner(session.get_owner_jid()); + const Jid target(session.get_target_jid()); + + std::string message{}; + + // As the function is exited, set the message in the response. + utils::ScopeGuard sg([&message, &command_node]() + { + command_node.delete_all_children(); + XmlNode note("note"); + note["type"] = "info"; + note.set_inner(message); + command_node.add_child(std::move(note)); + }); + + Bridge* bridge = biboumi_component.get_user_bridge(owner.bare()); + if (!bridge) + { + message = "You are not connected to anything."; + return; + } + + std::string hostname; + if ((hostname = Config::get("fixed_irc_server", "")).empty()) + hostname = target.local; + + IrcClient* irc = bridge->find_irc_client(hostname); + if (!irc || !irc->is_connected()) + { + message = "You are not connected to the IRC server "s + hostname; + return; + } + + std::ostringstream ss; + ss << "Connected to IRC server " << irc->get_hostname() << " on port " << irc->get_port(); + if (irc->is_using_tls()) + ss << " (using TLS)"; + const std::time_t now_c = std::chrono::system_clock::to_time_t(irc->connection_date); +#ifdef HAS_PUT_TIME + ss << " since " << std::put_time(std::localtime(&now_c), "%F %T"); +#else + constexpr std::size_t timestamp_size{10 + 1 + 8 + 1}; + char buf[timestamp_size] = {}; + const auto res = std::strftime(buf, timestamp_size, "%F %T", std::localtime(&now_c)); + if (res > 0) + ss << " since " << buf; +#endif + ss << " (" << std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now() - irc->connection_date).count() << " seconds ago)."; + + for (const auto& it: bridge->resources_in_chan) + { + const auto& channel_key = it.first; + const auto& irc_hostname = std::get<1>(channel_key); + const auto& resources = it.second; + + if (irc_hostname == irc->get_hostname() && !resources.empty()) + { + const auto& channel_name = std::get<0>(channel_key); + ss << "\n" << channel_name << " from " << resources.size() << " resource" << (resources.size() > 1 ? "s": "") << ": "; + for (const auto& resource: resources) + ss << resource << " "; + } + } + + message = ss.str(); +} diff --git a/src/xmpp/biboumi_adhoc_commands.hpp b/src/xmpp/biboumi_adhoc_commands.hpp index 2763a9f..b5fce61 100644 --- a/src/xmpp/biboumi_adhoc_commands.hpp +++ b/src/xmpp/biboumi_adhoc_commands.hpp @@ -10,6 +10,9 @@ class XmppComponent; void DisconnectUserStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node); void DisconnectUserStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node); +void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node); +void ConfigureGlobalStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node); + void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node); void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node); @@ -20,4 +23,4 @@ void DisconnectUserFromServerStep1(XmppComponent&, AdhocSession& session, XmlNod void DisconnectUserFromServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node); void DisconnectUserFromServerStep3(XmppComponent&, AdhocSession& session, XmlNode& command_node); - +void GetIrcConnectionInfoStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node); diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp index a6aac21..d6782e2 100644 --- a/src/xmpp/biboumi_component.cpp +++ b/src/xmpp/biboumi_component.cpp @@ -8,23 +8,27 @@ #include <xmpp/biboumi_adhoc_commands.hpp> #include <bridge/list_element.hpp> #include <config/config.hpp> -#include <xmpp/jid.hpp> #include <utils/sha1.hpp> +#include <utils/time.hpp> +#include <xmpp/jid.hpp> #include <stdexcept> #include <iostream> -#include <stdio.h> +#include <cstdlib> #include <louloulibs.h> #include <biboumi.h> -#include <uuid.h> +#include <uuid/uuid.h> #ifdef SYSTEMD_FOUND # include <systemd/sd-daemon.h> #endif +#include <database/database.hpp> +#include <bridge/result_set_management.hpp> + using namespace std::string_literals; static std::set<std::string> kickable_errors{ @@ -53,33 +57,30 @@ BiboumiComponent::BiboumiComponent(std::shared_ptr<Poller> poller, const std::st this->stanza_handlers.emplace("iq", std::bind(&BiboumiComponent::handle_iq, this,std::placeholders::_1)); - this->adhoc_commands_handler.get_commands() = { - {"ping", AdhocCommand({&PingStep1}, "Do a ping", false)}, - {"hello", AdhocCommand({&HelloStep1, &HelloStep2}, "Receive a custom greeting", false)}, - {"disconnect-user", AdhocCommand({&DisconnectUserStep1, &DisconnectUserStep2}, "Disconnect selected users from the gateway", true)}, - {"disconnect-from-irc-servers", AdhocCommand({&DisconnectUserFromServerStep1, &DisconnectUserFromServerStep2, &DisconnectUserFromServerStep3}, "Disconnect from the selected IRC servers", false)}, - {"reload", AdhocCommand({&Reload}, "Reload biboumi’s configuration", true)} - }; + this->adhoc_commands_handler.add_command("ping", {{&PingStep1}, "Do a ping", false}); + this->adhoc_commands_handler.add_command("hello", {{&HelloStep1, &HelloStep2}, "Receive a custom greeting", false}); + this->adhoc_commands_handler.add_command("disconnect-user", {{&DisconnectUserStep1, &DisconnectUserStep2}, "Disconnect selected users from the gateway", true}); + this->adhoc_commands_handler.add_command("disconnect-from-irc-server", {{&DisconnectUserFromServerStep1, &DisconnectUserFromServerStep2, &DisconnectUserFromServerStep3}, "Disconnect from the selected IRC servers", false}); + this->adhoc_commands_handler.add_command("reload", {{&Reload}, "Reload biboumi’s configuration", true}); + + AdhocCommand get_irc_connection_info{{&GetIrcConnectionInfoStep1}, "Returns various information about your connection to this IRC server.", false}; + if (!Config::get("fixed_irc_server", "").empty()) + this->adhoc_commands_handler.add_command("get-irc-connection-info", get_irc_connection_info); + else + this->irc_server_adhoc_commands_handler.add_command("get-irc-connection-info", get_irc_connection_info); #ifdef USE_DATABASE AdhocCommand configure_server_command({&ConfigureIrcServerStep1, &ConfigureIrcServerStep2}, "Configure a few settings for that IRC server", false); + AdhocCommand configure_global_command({&ConfigureGlobalStep1, &ConfigureGlobalStep2}, "Configure a few settings", false); + if (!Config::get("fixed_irc_server", "").empty()) - { - this->adhoc_commands_handler.get_commands().emplace(std::make_pair("configure", - configure_server_command)); - } -#endif + this->adhoc_commands_handler.add_command("configure", configure_server_command); + else + this->adhoc_commands_handler.add_command("configure", configure_global_command); - this->irc_server_adhoc_commands_handler.get_commands() = { -#ifdef USE_DATABASE - {"configure", configure_server_command}, + this->irc_server_adhoc_commands_handler.add_command("configure", configure_server_command); + this->irc_channel_adhoc_commands_handler.add_command("configure", {{&ConfigureIrcChannelStep1, &ConfigureIrcChannelStep2}, "Configure a few settings for that IRC channel", false}); #endif - }; - this->irc_channel_adhoc_commands_handler.get_commands() = { -#ifdef USE_DATABASE - {"configure", AdhocCommand({&ConfigureIrcChannelStep1, &ConfigureIrcChannelStep2}, "Configure a few settings for that IRC channel", false)}, -#endif - }; } void BiboumiComponent::shutdown() @@ -126,7 +127,7 @@ void BiboumiComponent::handle_presence(const Stanza& stanza) Bridge* bridge = this->get_user_bridge(from_str); Jid to(to_str); Jid from(from_str); - Iid iid(to.local); + Iid iid(to.local, bridge); // An error stanza is sent whenever we exit this function without // disabling this scopeguard. If error_type and error_name are not @@ -142,7 +143,7 @@ void BiboumiComponent::handle_presence(const Stanza& stanza) }); try { - if (iid.is_channel && !iid.get_server().empty()) + if (iid.type == Iid::Type::Channel && !iid.get_server().empty()) { // presence toward a MUC that corresponds to an irc channel, or a // dummy channel if iid.chan is empty if (type.empty()) @@ -163,7 +164,7 @@ void BiboumiComponent::handle_presence(const Stanza& stanza) } else { - // An user wants to join an invalid IRC channel, return a presence error to him + // A user wants to join an invalid IRC channel, return a presence error to him/her if (type.empty()) this->send_invalid_room_error(to.local, to.resource, from_str); } @@ -180,29 +181,30 @@ void BiboumiComponent::handle_presence(const Stanza& stanza) void BiboumiComponent::handle_message(const Stanza& stanza) { - std::string from = stanza.get_tag("from"); + std::string from_str = stanza.get_tag("from"); std::string id = stanza.get_tag("id"); std::string to_str = stanza.get_tag("to"); std::string type = stanza.get_tag("type"); - if (from.empty()) + if (from_str.empty()) return; if (type.empty()) type = "normal"; - Bridge* bridge = this->get_user_bridge(from); + Bridge* bridge = this->get_user_bridge(from_str); + Jid from(from_str); Jid to(to_str); - Iid iid(to.local); + Iid iid(to.local, bridge); std::string error_type("cancel"); std::string error_name("internal-server-error"); utils::ScopeGuard stanza_error([&](){ - this->send_stanza_error("message", from, to_str, id, + this->send_stanza_error("message", from_str, to_str, id, error_type, error_name, ""); }); const XmlNode* body = stanza.get_child("body", COMPONENT_NS); try { // catch IRCNotConnected exceptions - if (type == "groupchat" && iid.is_channel) + if (type == "groupchat" && iid.type == Iid::Type::Channel) { if (body && !body->get_inner().empty()) { @@ -216,7 +218,7 @@ void BiboumiComponent::handle_message(const Stanza& stanza) { const XmlNode* error = stanza.get_child("error", COMPONENT_NS); // Only a set of errors are considered “fatal”. If we encounter one of - // them, we purge (we disconnect the user from all the IRC servers). + // them, we purge (we disconnect that resource from all the IRC servers) // We consider this to be true, unless the error condition is // specified and is not in the kickable_errors set bool kickable_error = true; @@ -227,38 +229,49 @@ void BiboumiComponent::handle_message(const Stanza& stanza) kickable_error = false; } if (kickable_error) - bridge->shutdown("Error from remote client"); + bridge->remove_resource(from.resource, "Error from remote client"); } else if (type == "chat") { if (body && !body->get_inner().empty()) { // a message for nick!server - if (iid.is_user && !iid.get_local().empty()) + if (iid.type == Iid::Type::User && !iid.get_local().empty()) { bridge->send_private_message(iid, body->get_inner()); bridge->remove_preferred_from_jid(iid.get_local()); } - else if (!iid.is_user && !to.resource.empty()) + else if (iid.type != Iid::Type::User && !to.resource.empty()) { // a message for chan%server@biboumi/Nick or // server@biboumi/Nick // Convert that into a message to nick!server - Iid user_iid(utils::tolower(to.resource) + "!" + iid.get_server()); + Iid user_iid(utils::tolower(to.resource), iid.get_server(), Iid::Type::User); bridge->send_private_message(user_iid, body->get_inner()); bridge->set_preferred_from_jid(user_iid.get_local(), to_str); } - else if (!iid.is_user && !iid.is_channel) + else if (iid.type == Iid::Type::Server) { // Message sent to the server JID // Convert the message body into a raw IRC message bridge->send_raw_message(iid.get_server(), body->get_inner()); } } } - else if (iid.is_user) - this->send_invalid_user_error(to.local, from); + else if (type == "normal" && iid.type == Iid::Type::Channel) + { + if (const XmlNode* x = stanza.get_child("x", MUC_USER_NS)) + if (const XmlNode* invite = x->get_child("invite", MUC_USER_NS)) + { + const auto invite_to = invite->get_tag("to"); + if (!invite_to.empty()) + { + bridge->send_irc_invitation(iid, invite_to); + } + } + + } } catch (const IRCNotConnected& ex) { - this->send_stanza_error("message", from, to_str, id, + this->send_stanza_error("message", from_str, to_str, id, "cancel", "remote-server-not-found", "Not connected to IRC server "s + ex.hostname, true); @@ -321,7 +334,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) std::string affiliation = child->get_tag("affiliation"); if (!nick.empty()) { - Iid iid(to.local); + Iid iid(to.local, {}); if (role == "none") { // This is a kick std::string reason; @@ -345,15 +358,17 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) // Depending on the 'to' jid in the request, we use one adhoc // command handler or an other - Iid iid(to.local); + Iid iid(to.local, {}); AdhocCommandsHandler* adhoc_handler; - if (!to.local.empty() && !iid.is_user && !iid.is_channel) - adhoc_handler = &this->irc_server_adhoc_commands_handler; - else if (!to.local.empty() && iid.is_channel) - adhoc_handler = &this->irc_channel_adhoc_commands_handler; - else + if (to.local.empty()) adhoc_handler = &this->adhoc_commands_handler; - + else + { + if (iid.type == Iid::Type::Server) + adhoc_handler = &this->irc_server_adhoc_commands_handler; + else + adhoc_handler = &this->irc_channel_adhoc_commands_handler; + } // Execute the command, if any, and get a result XmlNode that we // insert in our response XmlNode inner_node = adhoc_handler->handle_request(from, to_str, *query); @@ -365,15 +380,23 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) this->send_stanza(response); stanza_error.disable(); } +#ifdef USE_DATABASE + else if ((query = stanza.get_child("query", MAM_NS))) + { + if (this->handle_mam_request(stanza)) + stanza_error.disable(); + } +#endif } else if (type == "get") { const XmlNode* query; if ((query = stanza.get_child("query", DISCO_INFO_NS))) { // Disco info + Iid iid(to.local, {'#', '&'}); + const std::string node = query->get_tag("node"); if (to_str == this->served_hostname) { - const std::string node = query->get_tag("node"); if (node.empty()) { // On the gateway itself @@ -381,16 +404,32 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) stanza_error.disable(); } } + else if (iid.type == Iid::Type::Server) + { + if (node.empty()) + { + this->send_irc_server_disco_info(id, from, to_str); + stanza_error.disable(); + } + } + else if (iid.type == Iid::Type::Channel) + { + if (node == MUC_TRAFFIC_NS) + { + this->send_irc_channel_muc_traffic_info(id, from, to_str); + stanza_error.disable(); + } + } } else if ((query = stanza.get_child("query", VERSION_NS))) { - Iid iid(to.local); - if (iid.is_user || - (iid.is_channel && !to.resource.empty())) + Iid iid(to.local, bridge); + if ((iid.type == Iid::Type::Channel && !to.resource.empty()) || + (iid.type == Iid::Type::User)) { // Get the IRC user version std::string target; - if (iid.is_user) + if (iid.type == Iid::Type::User) target = iid.get_local(); else target = to.resource; @@ -406,7 +445,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) } else if ((query = stanza.get_child("query", DISCO_ITEMS_NS))) { - Iid iid(to.local); + Iid iid(to.local, bridge); const std::string node = query->get_tag("node"); if (node == ADHOC_NS) { @@ -419,7 +458,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) this->adhoc_commands_handler); stanza_error.disable(); } - else if (!iid.is_user && !iid.is_channel) + else if (iid.type == Iid::Type::Server) { // Get the server's adhoc commands this->send_adhoc_commands_list(id, from, to_str, (Config::get("admin", "") == @@ -427,7 +466,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) this->irc_server_adhoc_commands_handler); stanza_error.disable(); } - else if (!iid.is_user && iid.is_channel) + else if (iid.type == Iid::Type::Channel) { // Get the channel's adhoc commands this->send_adhoc_commands_list(id, from, to_str, (Config::get("admin", "") == @@ -436,21 +475,36 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) stanza_error.disable(); } } - else if (node.empty() && !iid.is_user && !iid.is_channel) + else if (node.empty() && iid.type == Iid::Type::Server) { // Disco on an IRC server: get the list of channels - bridge->send_irc_channel_list_request(iid, id, from); + ResultSetInfo rs_info; + const XmlNode* set_node = query->get_child("set", RSM_NS); + if (set_node) + { + const XmlNode* after = set_node->get_child("after", RSM_NS); + if (after) + rs_info.after = after->get_inner(); + const XmlNode* before = set_node->get_child("before", RSM_NS); + if (before) + rs_info.before = before->get_inner(); + const XmlNode* max = set_node->get_child("max", RSM_NS); + if (max) + rs_info.max = std::atoi(max->get_inner().data()); + + } + bridge->send_irc_channel_list_request(iid, id, from, std::move(rs_info)); stanza_error.disable(); } } else if ((query = stanza.get_child("ping", PING_NS))) { - Iid iid(to.local); - if (iid.is_user) + Iid iid(to.local, bridge); + if (iid.type == Iid::Type::User) { // Ping any user (no check on the nick done ourself) bridge->send_irc_user_ping_request(iid.get_server(), iid.get_local(), id, from, to_str); } - else if (iid.is_channel && !to.resource.empty()) + else if (iid.type == Iid::Type::Channel && !to.resource.empty()) { // Ping a room participant (we check if the nick is in the room) bridge->send_irc_participant_ping_request(iid, to.resource, id, from, to_str); @@ -481,7 +535,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) version = version_node->get_inner(); if (os_node) os = os_node->get_inner(); - const Iid iid(to.local); + const Iid iid(to.local, bridge); bridge->send_xmpp_version_to_irc(iid, name, version, os); } else @@ -508,6 +562,96 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) error_name = "feature-not-implemented"; } +#ifdef USE_DATABASE +bool BiboumiComponent::handle_mam_request(const Stanza& stanza) +{ + std::string id = stanza.get_tag("id"); + Jid from(stanza.get_tag("from")); + Jid to(stanza.get_tag("to")); + + const XmlNode* query = stanza.get_child("query", MAM_NS); + std::string query_id; + if (query) + query_id = query->get_tag("queryid"); + + Iid iid(to.local, {'#', '&'}); + if (iid.type == Iid::Type::Channel && to.resource.empty()) + { + std::string start; + std::string end; + const XmlNode* x = query->get_child("x", DATAFORM_NS); + if (x) + { + const XmlNode* value; + const auto fields = x->get_children("field", DATAFORM_NS); + for (const auto& field: fields) + { + if (field->get_tag("var") == "start") + { + value = field->get_child("value", DATAFORM_NS); + if (value) + start = value->get_inner(); + } + else if (field->get_tag("var") == "end") + { + value = field->get_child("value", DATAFORM_NS); + if (value) + end = value->get_inner(); + } + } + } + const auto lines = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(), -1, start, end); + for (const db::MucLogLine& line: lines) + { + if (!line.nick.value().empty()) + this->send_archived_message(line, to.full(), from.full(), query_id); + } + this->send_iq_result_full_jid(id, from.full(), to.full()); + return true; + } + return false; +} + +void BiboumiComponent::send_archived_message(const db::MucLogLine& log_line, const std::string& from, const std::string& to, + const std::string& queryid) +{ + Stanza message("message"); + message["from"] = from; + message["to"] = to; + + XmlNode result("result"); + result["xmlns"] = MAM_NS; + if (!queryid.empty()) + result["queryid"] = queryid; + result["id"] = log_line.uuid.value(); + + XmlNode forwarded("forwarded"); + forwarded["xmlns"] = FORWARD_NS; + + XmlNode delay("delay"); + delay["xmlns"] = DELAY_NS; + delay["stamp"] = utils::to_string(log_line.date.value().timeStamp()); + + forwarded.add_child(std::move(delay)); + + XmlNode submessage("message"); + submessage["xmlns"] = CLIENT_NS; + submessage["from"] = from + "/" + log_line.nick.value(); + submessage["type"] = "groupchat"; + + XmlNode body("body"); + body.set_inner(log_line.body.value()); + submessage.add_child(std::move(body)); + + forwarded.add_child(std::move(submessage)); + result.add_child(std::move(forwarded)); + message.add_child(std::move(result)); + + this->send_stanza(message); +} + +#endif + Bridge* BiboumiComponent::get_user_bridge(const std::string& user_jid) { auto bare_jid = Jid{user_jid}.bare(); @@ -517,8 +661,7 @@ Bridge* BiboumiComponent::get_user_bridge(const std::string& user_jid) } catch (const std::out_of_range& exception) { - this->bridges.emplace(bare_jid, std::make_unique<Bridge>(bare_jid, *this, this->poller)); - return this->bridges.at(bare_jid).get(); + return this->bridges.emplace(bare_jid, std::make_unique<Bridge>(bare_jid, *this, this->poller)).first->second.get(); } } @@ -557,7 +700,32 @@ void BiboumiComponent::send_self_disco_info(const std::string& id, const std::st identity["type"] = "irc"; identity["name"] = "Biboumi XMPP-IRC gateway"; query.add_child(std::move(identity)); - for (const char* ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS}) + for (const char* ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS}) + { + XmlNode feature("feature"); + feature["var"] = ns; + query.add_child(std::move(feature)); + } + iq.add_child(std::move(query)); + this->send_stanza(iq); +} + +void BiboumiComponent::send_irc_server_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from) +{ + Jid from(jid_from); + Stanza iq("iq"); + iq["type"] = "result"; + iq["id"] = id; + iq["to"] = jid_to; + iq["from"] = jid_from; + XmlNode query("query"); + query["xmlns"] = DISCO_INFO_NS; + XmlNode identity("identity"); + identity["category"] = "conference"; + identity["type"] = "irc"; + identity["name"] = "IRC server "s + from.local + " over Biboumi"; + query.add_child(std::move(identity)); + for (const char* ns: {DISCO_INFO_NS, ADHOC_NS, PING_NS, VERSION_NS}) { XmlNode feature("feature"); feature["var"] = ns; @@ -567,6 +735,25 @@ void BiboumiComponent::send_self_disco_info(const std::string& id, const std::st this->send_stanza(iq); } +void BiboumiComponent::send_irc_channel_muc_traffic_info(const std::string id, const std::string& jid_from, const std::string& jid_to) +{ + Stanza iq("iq"); + iq["type"] = "result"; + iq["id"] = id; + iq["from"] = jid_from; + iq["to"] = jid_to; + + XmlNode query("query"); + query["xmlns"] = DISCO_INFO_NS; + query["node"] = MUC_TRAFFIC_NS; + // We drop all “special” traffic (like xhtml-im, chatstates, etc), so + // don’t include any <feature/> + iq.add_child(std::move(query)); + + this->send_stanza(iq); + +} + void BiboumiComponent::send_iq_version_request(const std::string& from, const std::string& jid_to) { @@ -604,15 +791,16 @@ void BiboumiComponent::send_ping_request(const std::string& from, "the response mismatches the 'from' of the request"); } else - bridge->send_irc_ping_result(from, id); + bridge->send_irc_ping_result({from, bridge}, id); }; this->waiting_iq[id] = result_cb; } -void BiboumiComponent::send_iq_room_list_result(const std::string& id, - const std::string& to_jid, - const std::string& from, - const std::vector<ListElement>& rooms_list) +void BiboumiComponent::send_iq_room_list_result(const std::string& id, const std::string& to_jid, + const std::string& from, const ChannelList& channel_list, + std::vector<ListElement>::const_iterator begin, + std::vector<ListElement>::const_iterator end, + const ResultSetInfo& rs_info) { Stanza iq("iq"); iq["from"] = from + "@" + this->served_hostname; @@ -621,12 +809,60 @@ void BiboumiComponent::send_iq_room_list_result(const std::string& id, iq["type"] = "result"; XmlNode query("query"); query["xmlns"] = DISCO_ITEMS_NS; - for (const auto& room: rooms_list) + + for (auto it = begin; it != end; ++it) { XmlNode item("item"); - item["jid"] = room.channel + "%" + from + "@" + this->served_hostname; + item["jid"] = it->channel + "@" + this->served_hostname; query.add_child(std::move(item)); } + + if ((rs_info.max >= 0 || !rs_info.after.empty() || !rs_info.before.empty())) + { + XmlNode set_node("set"); + set_node["xmlns"] = RSM_NS; + + if (begin != channel_list.channels.cend()) + { + XmlNode first_node("first"); + first_node["index"] = std::to_string(std::distance(channel_list.channels.cbegin(), begin)); + first_node.set_inner(begin->channel + "@" + this->served_hostname); + set_node.add_child(std::move(first_node)); + } + if (end != channel_list.channels.cbegin()) + { + XmlNode last_node("last"); + last_node.set_inner(std::prev(end)->channel + "@" + this->served_hostname); + set_node.add_child(std::move(last_node)); + } + if (channel_list.complete) + { + XmlNode count_node("count"); + count_node.set_inner(std::to_string(channel_list.channels.size())); + set_node.add_child(std::move(count_node)); + } + query.add_child(std::move(set_node)); + } + iq.add_child(std::move(query)); this->send_stanza(iq); } + +void BiboumiComponent::send_invitation(const std::string& room_target, + const std::string& jid_to, + const std::string& author_nick) +{ + Stanza message("message"); + message["from"] = room_target + "@" + this->served_hostname; + message["to"] = jid_to; + XmlNode x("x"); + x["xmlns"] = MUC_USER_NS; + XmlNode invite("invite"); + if (author_nick.empty()) + invite["from"] = room_target + "@" + this->served_hostname; + else + invite["from"] = room_target + "@" + this->served_hostname + "/" + author_nick; + x.add_child(std::move(invite)); + message.add_child(std::move(x)); + this->send_stanza(message); +} diff --git a/src/xmpp/biboumi_component.hpp b/src/xmpp/biboumi_component.hpp index 24d768a..999001f 100644 --- a/src/xmpp/biboumi_component.hpp +++ b/src/xmpp/biboumi_component.hpp @@ -9,6 +9,10 @@ #include <string> #include <map> +namespace db +{ +class MucLogLine; +} struct ListElement; /** @@ -58,6 +62,15 @@ public: */ void send_self_disco_info(const std::string& id, const std::string& jid_to); /** + * Send a result IQ with the disco informations regarding IRC server JIDs. + */ + void send_irc_server_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from); + /** + * Sends the allowed namespaces in MUC message, according to + * http://xmpp.org/extensions/xep-0045.html#impl-service-traffic + */ + void send_irc_channel_muc_traffic_info(const std::string id, const std::string& jid_from, const std::string& jid_to); + /** * Send an iq version request */ void send_iq_version_request(const std::string& from, @@ -71,9 +84,10 @@ public: /** * Send the channels list in one big stanza */ - void send_iq_room_list_result(const std::string& id, const std::string& to_jid, - const std::string& from, - const std::vector<ListElement>& rooms_list); + void send_iq_room_list_result(const std::string& id, const std::string& to_jid, const std::string& from, + const ChannelList& channel_list, std::vector<ListElement>::const_iterator begin, + std::vector<ListElement>::const_iterator end, const ResultSetInfo& rs_info); + void send_invitation(const std::string& room_target, const std::string& jid_to, const std::string& author_nick); /** * Handle the various stanza types */ @@ -81,13 +95,19 @@ public: void handle_message(const Stanza& stanza); void handle_iq(const Stanza& stanza); -private: +#ifdef USE_DATABASE + bool handle_mam_request(const Stanza& stanza); + void send_archived_message(const db::MucLogLine& log_line, const std::string& from, const std::string& to, + const std::string& queryid); +#endif + /** * Return the bridge associated with the bare JID. Create a new one * if none already exist. */ Bridge* get_user_bridge(const std::string& user_jid); +private: /** * A map of id -> callback. When we want to wait for an iq result, we add * the callback to this map, with the iq id as the key. When an iq result |