diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp index ec76ef6..4e9d42d 100644 --- a/src/bridge/bridge.cpp +++ b/src/bridge/bridge.cpp @@ -14,6 +14,7 @@ #include "result_set_management.hpp" #include #include +#include using namespace std::string_literals; @@ -221,6 +222,64 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std:: std::vector lines = utils::split(body, '\n', true); if (lines.empty()) return ; + + if (irc->has_capability("draft/multiline")) + { + std::string uuid; + const auto xmpp_body = this->make_xmpp_body(body); +#ifdef USE_DATABASE + if (this->record_history) + uuid = Database::store_muc_message(this->get_bare_jid(), iid.get_local(), iid.get_server(), std::chrono::system_clock::now(), + std::get<0>(xmpp_body), irc->get_own_nick()); +#else + uuid = utils::gen_uuid(); +#endif + + IrcChannel* channel = irc->get_channel(iid.get_local()); + if (!channel->joined) + { + log_warning("Cannot send message to channel ", iid.get_local(), ", it is not joined"); + return; + } + + std::vector messages; + for (const std::string& line: lines) + { + constexpr auto max_username_size = 10; + constexpr auto max_hostname_size = 63; + const auto line_size = 512 - 1 - ::strlen("batch=") - uuid.length() - 1 - ::strlen("draft/multiline-concat") - 1 - + irc->get_own_nick().size() - max_username_size - max_hostname_size - + ::strlen(":!@ PRIVMSG ") - iid.get_local().length() - ::strlen(" :\r\n"); + const auto privmsgs = cut(line, line_size); + bool first = true; + for (const auto& privmsg: privmsgs) + { + if (first) + { + messages.push_back(IrcMessage({}, std::move(std::string()), "PRIVMSG", {iid.get_local(), privmsg})); + first = false; + } + else + messages.push_back(IrcMessage({{"draft/multiline-concat", {}}}, std::move(std::string()), "PRIVMSG", {iid.get_local(), privmsg})); + } + } + + MessageCallback mirror_to_all_resources = [this, iid, uuid, nodes_to_reflect, body](const IrcClient* irc, const IrcMessage&) { + for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) + { + auto stanza = this->xmpp.make_muc_message(std::to_string(iid), irc->get_own_nick(), this->make_xmpp_body(body), + this->user_jid + "/" + + resource, uuid, uuid); + for (const auto& node: nodes_to_reflect) + stanza.add_child(node); + this->xmpp.send_stanza(stanza); + } + }; + + irc->send_batch(uuid, "draft/multiline", {iid.get_local()}, messages, std::move(mirror_to_all_resources)); + return; + } + bool first = true; for (const std::string& line: lines) { diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp index 72da2b4..a1b972c 100644 --- a/src/irc/irc_client.cpp +++ b/src/irc/irc_client.cpp @@ -83,6 +83,7 @@ static const std::unordered_mapsend_gateway_message("Connected to IRC server"s + (this->use_tls ? " (encrypted)": "") + "."); + this->capabilities["batch"] = {[]{}, []{}}; + this->capabilities["message-tags"] = {[]{}, []{}}; this->capabilities["multi-prefix"] = {[]{}, []{}}; + this->capabilities["draft/multiline"] = {[]{}, []{}}; #ifdef USE_DATABASE auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(), @@ -416,12 +420,34 @@ void IrcClient::parse_in_buffer(const size_t) "”: ", args_size); else { - const auto& cb = it->second.first; - try { - (this->*(cb))(message); - } catch (const std::exception& e) { - log_error("Unhandled exception: ", e.what()); - } + bool added_to_batch = false; + if (this->has_capability("batch")) + { + auto it_tags = message.tags.find("batch"); + if (it_tags != message.tags.end()) + if (it_tags->second.has_value()) + { + auto it_batches = this->batches.find(it_tags->second.value()); + if (it_batches != this->batches.end()) + { + auto message_tags = message.tags; + auto message_prefix = message.prefix; + auto message_command = message.command; + auto message_arguments = message.arguments; + it_batches->second.second.emplace_back(std::move(message_tags), std::move(message_prefix), std::move(message_command), std::move(message_arguments)); + added_to_batch = true; + } + } + } + if (!added_to_batch) + { + const auto& cb = it->second.first; + try { + (this->*(cb))(message); + } catch (const std::exception& e) { + log_error("Unhandled exception: ", e.what()); + } + } } } else @@ -441,6 +467,16 @@ void IrcClient::actual_send(std::pair&& message_pai const MessageCallback& callback = message_pair.second; log_debug("IRC SENDING: (", this->get_hostname(), ") ", message); std::string res; + if (!message.tags.empty()) + { + res += "@"; + for (auto& tag: message.tags) + if (tag.second.has_value()) + res += tag.first + "=" + tag.second.value() + ";"; + else + res += tag.first + ";"; + res.back() = ' '; + } if (!message.prefix.empty()) res += ":" + message.prefix + " "; res += message.command; @@ -606,6 +642,22 @@ void IrcClient::send_ping_command() this->send_message(IrcMessage("PING", {"biboumi"})); } +void IrcClient::send_batch(const std::string& reference_tag, const std::string& type, const std::vector& parameters, std::vector& messages, MessageCallback callback) +{ + std::vector args; + args.push_back("+" + reference_tag); + args.push_back(type); + for (const std::string& parameter: parameters) + args.push_back(parameter); + this->send_message(IrcMessage("BATCH", std::move(args))); + for (IrcMessage& message: messages) + { + message.tags["batch"] = reference_tag; + this->send_message(std::move(message)); + } + this->send_message(IrcMessage("BATCH", {"-" + reference_tag}), callback); +} + void IrcClient::forward_server_message(const IrcMessage& message) { const std::string from = message.prefix; @@ -1351,11 +1403,16 @@ void IrcClient::on_cap(const IrcMessage &message) } Capability& capability = it->second; if (sub_command == "ACK") - capability.on_ack(); + { + capability.on_ack(); + this->enabled_capabilities.insert(it->first); + } else if (sub_command == "NACK") - capability.on_nack(); + capability.on_nack(); this->capabilities.erase(it); } + for (const auto& batch: this->batches) + log_debug("Batch: ", batch.first, " exists"); if (this->capabilities.empty()) this->cap_end(); } @@ -1419,3 +1476,85 @@ void IrcClient::cap_end() this->send_message({"CAP", {"END"}}); this->bridge.on_irc_client_connected(this->get_hostname()); } + +bool IrcClient::has_capability(const std::string& capability) +{ + auto it = this->enabled_capabilities.find(capability); + return it != this->enabled_capabilities.end(); +} + +void IrcClient::on_batch(const IrcMessage& message) +{ + if (message.arguments[0][0] == '+') + { + if (message.arguments.size() < 2) + return; + if (message.arguments[1] == "draft/multiline" && message.arguments.size() != 3) + return; + this->batches.emplace(message.arguments[0].substr(1), std::make_pair(std::make_pair(message.prefix, message.arguments), std::vector())); + } + else if (message.arguments[0][0] == '-') + { + const std::string reference_tag = message.arguments[0].substr(1); + this->process_batch(reference_tag); + this->batches.erase(reference_tag); + } + return; +} + +void IrcClient::process_batch(const std::string& reference_tag) +{ + auto it = this->batches.find(reference_tag); + if (it == this->batches.end()) + return; + if (it->second.first.second[1] == "draft/multiline") + { + const IrcUser user(it->second.first.first); + const std::string nick = user.nick; + Iid iid; + iid.set_local(it->second.first.second[2]); + iid.set_server(this->hostname); + bool muc = true; + if (!this->get_channel(iid.get_local())->joined) + { + iid.type = Iid::Type::User; + iid.set_local(nick); + muc = false; + } + else + iid.type = Iid::Type::Channel; + + std::string body; + for (const IrcMessage& message: it->second.second) + { + auto it_tags = message.tags.find("draft/multiline-concat"); + if (it_tags == message.tags.end()) + body += "\n" + message.arguments[1]; + else + body += message.arguments[1]; + } + if (body.find_first_not_of("\n") == std::string::npos) + // Empty message + return; + this->bridge.send_message(iid, nick, body, muc); + } + else + { + for (const IrcMessage& message: it->second.second) + { + auto it_callback = irc_callbacks.find(message.command); + if (it_callback == irc_callbacks.end()) + continue; + const auto& cb = it_callback->second.first; + try + { + (this->*(cb))(message); + } + catch (const std::exception& e) + { + log_error("Unhandled exception: ", e.what()); + } + } + } + return; +} diff --git a/src/irc/irc_client.hpp b/src/irc/irc_client.hpp index 3423228..4090ab1 100644 --- a/src/irc/irc_client.hpp +++ b/src/irc/irc_client.hpp @@ -26,6 +26,7 @@ #include #include #include +#include class IrcClient; @@ -155,6 +156,10 @@ public: * Send the QUIT irc command */ void send_quit_command(const std::string& reason); + /** + * Send a batch of messages + */ + void send_batch(const std::string& reference_tag, const std::string& type, const std::vector& parameters, std::vector& messages, MessageCallback callback); /** * Send a message to the gateway user, not generated by the IRC server, * but that might be useful because we want to be verbose (for example we @@ -244,6 +249,7 @@ public: void on_cap(const IrcMessage& message); private: void cap_end(); + void process_batch(const std::string& reference_tag); public: #ifdef WITH_SASL void on_authenticate(const IrcMessage& message); @@ -306,6 +312,7 @@ public: */ void on_channel_mode(const IrcMessage& message); void on_quit(const IrcMessage& message); + void on_batch(const IrcMessage& message); void on_unknown_message(const IrcMessage& message); /** * Return the number of joined channels @@ -326,6 +333,8 @@ public: * Store the history limit that the client asked when joining this room. */ HistoryLimit history_limit; + + bool has_capability(const std::string& capability); private: /** * The hostname of the server we are connected to. @@ -387,6 +396,7 @@ private: SaslState sasl_state{SaslState::unneeded}; #endif std::map capabilities; + std::unordered_set enabled_capabilities; /** * See http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt section 3.3 * We store the possible chanmodes in this object. @@ -431,6 +441,7 @@ private: Resolver dns_resolver; TokensBucket tokens_bucket; long int get_throttle_limit() const; + std::unordered_map>, std::vector>> batches; }; diff --git a/src/irc/irc_message.cpp b/src/irc/irc_message.cpp index 62fe9a7..c056a8c 100644 --- a/src/irc/irc_message.cpp +++ b/src/irc/irc_message.cpp @@ -1,8 +1,27 @@ #include #include +#include +#include +#include IrcMessage::IrcMessage(std::stringstream ss) { + if (ss.peek() == '@') + { + std::string raw_tags; + ss.ignore(); + ss >> raw_tags; + std::vector tags = utils::split(raw_tags, ';', false); + for (auto &tag: tags) + { + auto pos = tag.find('='); + if (pos == std::string::npos) + this->tags[tag] = {}; + else + this->tags[tag.substr(0, pos)] = tag.substr(pos+1); + } + ss.ignore(); + } if (ss.peek() == ':') { ss.ignore(); @@ -30,6 +49,7 @@ IrcMessage::IrcMessage(std::stringstream ss) IrcMessage::IrcMessage(std::string&& prefix, std::string&& command, std::vector&& args): + tags(), prefix(std::move(prefix)), command(std::move(command)), arguments(std::move(args)) @@ -38,16 +58,36 @@ IrcMessage::IrcMessage(std::string&& prefix, IrcMessage::IrcMessage(std::string&& command, std::vector&& args): + tags(), prefix(), command(std::move(command)), arguments(std::move(args)) { } +IrcMessage::IrcMessage(std::unordered_map>&& tags, + std::string&& prefix, + std::string&& command, + std::vector&& args): + tags(std::move(tags)), + prefix(std::move(prefix)), + command(std::move(command)), + arguments(std::move(args)) +{ +} + std::ostream& operator<<(std::ostream& os, const IrcMessage& message) { os << "IrcMessage"; - os << "[" << message.command << "]"; + os << "{"; + for (auto& tag: message.tags) + { + os << tag.first; + if (tag.second.has_value()) + os << "=" << tag.second.value(); + os << ";"; + } + os << "}[" << message.command << "]"; for (const std::string& arg: message.arguments) { os << "{" << arg << "}"; diff --git a/src/irc/irc_message.hpp b/src/irc/irc_message.hpp index 5475fd2..17d1c39 100644 --- a/src/irc/irc_message.hpp +++ b/src/irc/irc_message.hpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include class IrcMessage { @@ -13,6 +15,7 @@ public: IrcMessage(std::string str): IrcMessage{std::stringstream{str}} {} IrcMessage(std::string&& prefix, std::string&& command, std::vector&& args); IrcMessage(std::string&& command, std::vector&& args); + IrcMessage(std::unordered_map>&& tags, std::string&& prefix, std::string&& command, std::vector&& args); ~IrcMessage() = default; IrcMessage(const IrcMessage&) = delete; @@ -20,6 +23,7 @@ public: IrcMessage& operator=(const IrcMessage&) = delete; IrcMessage& operator=(IrcMessage&&) = default; + std::unordered_map> tags; std::string prefix; std::string command; std::vector arguments;