summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/bridge/bridge.cpp907
-rw-r--r--src/bridge/bridge.hpp293
-rw-r--r--src/bridge/colors.cpp170
-rw-r--r--src/bridge/colors.hpp56
-rw-r--r--src/bridge/list_element.hpp19
-rw-r--r--src/database/database.cpp87
-rw-r--r--src/database/database.hpp53
-rw-r--r--src/irc/iid.cpp118
-rw-r--r--src/irc/iid.hpp79
-rw-r--r--src/irc/irc_channel.cpp60
-rw-r--r--src/irc/irc_channel.hpp70
-rw-r--r--src/irc/irc_client.cpp1120
-rw-r--r--src/irc/irc_client.hpp383
-rw-r--r--src/irc/irc_message.cpp61
-rw-r--r--src/irc/irc_message.hpp28
-rw-r--r--src/irc/irc_user.cpp57
-rw-r--r--src/irc/irc_user.hpp33
-rw-r--r--src/main.cpp202
-rw-r--r--src/utils/empty_if_fixed_server.hpp26
-rw-r--r--src/utils/reload.cpp34
-rw-r--r--src/utils/reload.hpp4
-rw-r--r--src/xmpp/biboumi_adhoc_commands.cpp635
-rw-r--r--src/xmpp/biboumi_adhoc_commands.hpp23
-rw-r--r--src/xmpp/biboumi_component.cpp632
-rw-r--r--src/xmpp/biboumi_component.hpp109
25 files changed, 5259 insertions, 0 deletions
diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp
new file mode 100644
index 0000000..17d3ec6
--- /dev/null
+++ b/src/bridge/bridge.cpp
@@ -0,0 +1,907 @@
+#include <bridge/bridge.hpp>
+#include <bridge/list_element.hpp>
+#include <xmpp/biboumi_component.hpp>
+#include <network/poller.hpp>
+#include <utils/empty_if_fixed_server.hpp>
+#include <utils/encoding.hpp>
+#include <utils/tolower.hpp>
+#include <logger/logger.hpp>
+#include <utils/revstr.hpp>
+#include <utils/split.hpp>
+#include <xmpp/jid.hpp>
+#include <database/database.hpp>
+
+using namespace std::string_literals;
+
+static const char* action_prefix = "\01ACTION ";
+
+
+static std::string in_encoding_for(const Bridge& bridge, const Iid& iid)
+{
+#ifdef USE_DATABASE
+ const auto jid = bridge.get_bare_jid();
+ auto options = Database::get_irc_channel_options_with_server_default(jid, iid.get_server(), iid.get_local());
+ return options.encodingIn.value();
+#else
+ return {"ISO-8859-1"};
+#endif
+}
+
+Bridge::Bridge(const std::string& user_jid, BiboumiComponent& xmpp, std::shared_ptr<Poller> poller):
+ user_jid(user_jid),
+ xmpp(xmpp),
+ poller(poller)
+{
+}
+
+/**
+ * Return the role and affiliation, corresponding to the given irc mode
+ */
+static std::tuple<std::string, std::string> get_role_affiliation_from_irc_mode(const char mode)
+{
+ if (mode == 'a' || mode == 'q'){
+ return std::make_tuple("moderator", "owner");}
+ else if (mode == 'o')
+ return std::make_tuple("moderator", "admin");
+ else if (mode == 'h')
+ return std::make_tuple("moderator", "member");
+ else if (mode == 'v')
+ return std::make_tuple("participant", "member");
+ else
+ return std::make_tuple("participant", "none");
+}
+
+void Bridge::shutdown(const std::string& exit_message)
+{
+ for (auto it = this->irc_clients.begin(); it != this->irc_clients.end(); ++it)
+ {
+ it->second->send_quit_command(exit_message);
+ it->second->leave_dummy_channel(exit_message);
+ }
+}
+
+void Bridge::clean()
+{
+ auto it = this->irc_clients.begin();
+ while (it != this->irc_clients.end())
+ {
+ IrcClient* client = it->second.get();
+ if (!client->is_connected() && !client->is_connecting() &&
+ !client->get_resolver().is_resolving())
+ it = this->irc_clients.erase(it);
+ else
+ ++it;
+ }
+}
+
+const std::string& Bridge::get_jid() const
+{
+ return this->user_jid;
+}
+
+std::string Bridge::get_bare_jid() const
+{
+ Jid jid(this->user_jid);
+ return jid.local + "@" + jid.domain;
+}
+
+Xmpp::body Bridge::make_xmpp_body(const std::string& str, const std::string& encoding)
+{
+ std::string res;
+ if (utils::is_valid_utf8(str.c_str()))
+ res = str;
+ else
+ res = utils::convert_to_utf8(str, encoding.data());
+ return irc_format_to_xhtmlim(res);
+}
+
+IrcClient* Bridge::make_irc_client(const std::string& hostname, const std::string& nickname)
+{
+ try
+ {
+ return this->irc_clients.at(hostname).get();
+ }
+ catch (const std::out_of_range& exception)
+ {
+ auto username = nickname;
+ auto realname = nickname;
+ Jid jid(this->user_jid);
+ if (Config::get("realname_from_jid", "false") == "true")
+ {
+ username = jid.local;
+ realname = this->get_bare_jid();
+ }
+ this->irc_clients.emplace(hostname,
+ std::make_shared<IrcClient>(this->poller, hostname,
+ nickname, username,
+ realname, jid.domain,
+ *this));
+ std::shared_ptr<IrcClient> irc = this->irc_clients.at(hostname);
+ return irc.get();
+ }
+}
+
+IrcClient* Bridge::get_irc_client(const std::string& hostname)
+{
+ try
+ {
+ return this->irc_clients.at(hostname).get();
+ }
+ catch (const std::out_of_range& exception)
+ {
+ throw IRCNotConnected(hostname);
+ }
+}
+
+IrcClient* Bridge::find_irc_client(const std::string& hostname)
+{
+ try
+ {
+ return this->irc_clients.at(hostname).get();
+ }
+ catch (const std::out_of_range& exception)
+ {
+ return nullptr;
+ }
+}
+
+bool Bridge::join_irc_channel(const Iid& iid, const std::string& nickname, const std::string& password,
+ const std::string& resource)
+{
+ const auto hostname = iid.get_server();
+ IrcClient* irc = this->make_irc_client(hostname, nickname);
+ this->add_resource_to_server(hostname, resource);
+ auto res_in_chan = this->is_resource_in_chan(ChannelKey{iid.get_local(), hostname}, resource);
+ if (!res_in_chan)
+ this->add_resource_to_chan(ChannelKey{iid.get_local(), hostname}, resource);
+ if (iid.get_local().empty())
+ { // Join the dummy channel
+ if (irc->is_welcomed())
+ {
+ if (irc->get_dummy_channel().joined)
+ return false;
+ // Immediately simulate a message coming from the IRC server saying that we
+ // joined the channel
+ const IrcMessage join_message(irc->get_nick(), "JOIN", {""});
+ irc->on_channel_join(join_message);
+ const IrcMessage end_join_message(std::string(iid.get_server()), "366",
+ {irc->get_nick(),
+ "", "End of NAMES list"});
+ irc->on_channel_completely_joined(end_join_message);
+ }
+ else
+ {
+ irc->get_dummy_channel().joining = true;
+ irc->start();
+ }
+ return true;
+ }
+ if (irc->is_channel_joined(iid.get_local()) == false)
+ {
+ irc->send_join_command(iid.get_local(), password);
+ return true;
+ } else if (!res_in_chan) {
+ this->generate_channel_join_for_resource(iid, resource);
+ }
+ return false;
+}
+
+void Bridge::send_channel_message(const Iid& iid, const std::string& body)
+{
+ if (iid.get_server().empty())
+ {
+ for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
+ this->xmpp.send_stanza_error("message", this->user_jid + "/" + resource, std::to_string(iid), "",
+ "cancel", "remote-server-not-found",
+ std::to_string(iid) + " is not a valid channel name. "
+ "A correct room jid is of the form: #<chan>%<server>",
+ false);
+ return;
+ }
+ IrcClient* irc = this->get_irc_client(iid.get_server());
+
+ // Because an IRC message cannot contain \n, we need to convert each line
+ // of text into a separate IRC message. For conveniance, we also cut the
+ // message into submessages on the XMPP side, this way the user of the
+ // gateway sees what was actually sent over IRC. For example if an user
+ // sends “hello\n/me waves”, two messages will be generated: “hello” and
+ // “/me waves”. Note that the “command” handling (messages starting with
+ // /me, /mode, etc) is done for each message generated this way. In the
+ // above example, the /me will be interpreted.
+ std::vector<std::string> lines = utils::split(body, '\n', true);
+ if (lines.empty())
+ return ;
+ for (const std::string& line: lines)
+ {
+ if (line.substr(0, 5) == "/mode")
+ {
+ std::vector<std::string> args = utils::split(line.substr(5), ' ', false);
+ irc->send_mode_command(iid.get_local(), args);
+ continue; // We do not want to send that back to the
+ // XMPP user, that’s not a textual message.
+ }
+ else if (line.substr(0, 4) == "/me ")
+ irc->send_channel_message(iid.get_local(), action_prefix + line.substr(4) + "\01");
+ else
+ irc->send_channel_message(iid.get_local(), line);
+ for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
+ this->xmpp.send_muc_message(std::to_string(iid), irc->get_own_nick(),
+ this->make_xmpp_body(line), this->user_jid + "/" + resource);
+ }
+}
+
+void Bridge::forward_affiliation_role_change(const Iid& iid, const std::string& nick,
+ const std::string& affiliation,
+ const std::string& role)
+{
+ IrcClient* irc = this->get_irc_client(iid.get_server());
+ IrcChannel* chan = irc->get_channel(iid.get_local());
+ if (!chan || !chan->joined)
+ return;
+ IrcUser* user = chan->find_user(nick);
+ if (!user)
+ return;
+ // For each affiliation or role, we have a “maximal” mode that we want to
+ // set. We must remove any superior mode at the same time. For example if
+ // the user already has +o mode, and we set its affiliation to member, we
+ // remove the +o mode, and add +v. For each “superior” mode (for example,
+ // for +v, the superior modes are 'h', 'a', 'o' and 'q') we check if that
+ // user has it, and if yes we remove that mode
+
+ std::size_t nb = 1; // the number of times the nick must be
+ // repeated in the argument list
+ std::string modes; // The string of modes to
+ // add/remove. For example "+v-aoh"
+ std::vector<char> modes_to_remove; // List of modes to check for removal
+ if (affiliation == "none")
+ {
+ modes = "";
+ nb = 0;
+ modes_to_remove = {'v', 'h', 'o', 'a', 'q'};
+ }
+ else if (affiliation == "member")
+ {
+ modes = "+v";
+ modes_to_remove = {'h', 'o', 'a', 'q'};
+ }
+ else if (role == "moderator")
+ {
+ modes = "+h";
+ modes_to_remove = {'o', 'a', 'q'};
+ }
+ else if (affiliation == "admin")
+ {
+ modes = "+o";
+ modes_to_remove = {'a', 'q'};
+ }
+ else if (affiliation == "owner")
+ {
+ modes = "+a";
+ modes_to_remove = {'q'};
+ }
+ else
+ return;
+ for (const char mode: modes_to_remove)
+ if (user->modes.find(mode) != user->modes.end())
+ {
+ modes += "-"s + mode;
+ nb++;
+ }
+ if (modes.empty())
+ return;
+ std::vector<std::string> args(nb, nick);
+ args.insert(args.begin(), modes);
+ irc->send_mode_command(iid.get_local(), args);
+}
+
+void Bridge::send_private_message(const Iid& iid, const std::string& body, const std::string& type)
+{
+ if (iid.get_local().empty() || iid.get_server().empty())
+ {
+ this->xmpp.send_stanza_error("message", this->user_jid, std::to_string(iid), "",
+ "cancel", "remote-server-not-found",
+ std::to_string(iid) + " is not a valid channel name. "
+ "A correct room jid is of the form: #<chan>%<server>",
+ false);
+ return;
+ }
+ IrcClient* irc = this->get_irc_client(iid.get_server());
+ std::vector<std::string> lines = utils::split(body, '\n', true);
+ if (lines.empty())
+ return ;
+ for (const std::string& line: lines)
+ {
+ if (line.substr(0, 4) == "/me ")
+ irc->send_private_message(iid.get_local(), action_prefix + line.substr(4) + "\01", type);
+ else
+ irc->send_private_message(iid.get_local(), line, type);
+ }
+}
+
+void Bridge::send_raw_message(const std::string& hostname, const std::string& body)
+{
+ IrcClient* irc = this->get_irc_client(hostname);
+ irc->send_raw(body);
+}
+
+void Bridge::leave_irc_channel(Iid&& iid, std::string&& status_message, const std::string& resource)
+{
+ IrcClient* irc = this->get_irc_client(iid.get_server());
+ const auto key = iid.to_tuple();
+ if (!this->is_resource_in_chan(key, resource))
+ return ;
+
+ const auto resources = this->number_of_resources_in_chan(key);
+ if (resources == 1)
+ {
+ irc->send_part_command(iid.get_local(), status_message);
+ // Since there are no resources left in that channel, we don't
+ // want to receive private messages using this room's JID
+ this->remove_all_preferred_from_jid_of_room(iid.get_local());
+ }
+ else
+ {
+ IrcChannel* chan = irc->get_channel(iid.get_local());
+ if (chan)
+ {
+ auto nick = chan->get_self()->nick;
+ this->remove_resource_from_chan(key, resource);
+ this->send_muc_leave(std::move(iid), std::move(nick),
+ "Biboumi note: "s + std::to_string(resources - 1) + " resources are still in this channel.",
+ true, resource);
+ if (this->number_of_channels_the_resource_is_in(iid.get_server(), resource) == 0)
+ this->remove_resource_from_server(iid.get_server(), resource);
+ }
+ }
+}
+
+void Bridge::send_irc_nick_change(const Iid& iid, const std::string& new_nick)
+{
+ IrcClient* irc = this->get_irc_client(iid.get_server());
+ irc->send_nick_command(new_nick);
+}
+
+void Bridge::send_irc_channel_list_request(const Iid& iid, const std::string& iq_id,
+ const std::string& to_jid)
+{
+ IrcClient* irc = this->get_irc_client(iid.get_server());
+
+ irc->send_list_command();
+
+ std::vector<ListElement> list;
+
+ irc_responder_callback_t cb = [this, iid, iq_id, to_jid, list=std::move(list)](const std::string& irc_hostname,
+ const IrcMessage& message) mutable -> bool
+ {
+ if (irc_hostname != iid.get_server())
+ return false;
+ if (message.command == "263" || message.command == "RPL_TRYAGAIN" ||
+ message.command == "ERR_TOOMANYMATCHES" || message.command == "ERR_NOSUCHSERVER")
+ {
+ std::string text;
+ if (message.arguments.size() >= 2)
+ text = message.arguments[1];
+ this->xmpp.send_stanza_error("iq", to_jid, std::to_string(iid), iq_id,
+ "wait", "service-unavailable", text, false);
+ return true;
+ }
+ else if (message.command == "322" || message.command == "RPL_LIST")
+ { // Add element to list
+ if (message.arguments.size() == 4)
+ list.emplace_back(message.arguments[1], message.arguments[2],
+ message.arguments[3]);
+ return false;
+ }
+ else if (message.command == "323" || message.command == "RPL_LISTEND")
+ { // Send the iq response with the content of the list
+ this->xmpp.send_iq_room_list_result(iq_id, to_jid, std::to_string(iid), list);
+ return true;
+ }
+ return false;
+ };
+ this->add_waiting_irc(std::move(cb));
+}
+
+void Bridge::send_irc_kick(const Iid& iid, const std::string& target, const std::string& reason,
+ const std::string& iq_id, const std::string& to_jid)
+{
+ IrcClient* irc = this->get_irc_client(iid.get_server());
+
+ irc->send_kick_command(iid.get_local(), target, reason);
+ irc_responder_callback_t cb = [this, target, iq_id, to_jid, iid](const std::string& irc_hostname,
+ const IrcMessage& message) -> bool
+ {
+ if (irc_hostname != iid.get_server())
+ return false;
+ if (message.command == "KICK" && message.arguments.size() >= 2)
+ {
+ const std::string target_later = message.arguments[1];
+ const std::string chan_name_later = utils::tolower(message.arguments[0]);
+ if (target_later != target || chan_name_later != iid.get_local())
+ return false;
+ this->xmpp.send_iq_result(iq_id, to_jid, std::to_string(iid));
+ }
+ else if (message.command == "401" && message.arguments.size() >= 2)
+ {
+ const std::string target_later = message.arguments[1];
+ if (target_later != target)
+ return false;
+ std::string error_message = "No such nick";
+ if (message.arguments.size() >= 3)
+ error_message = message.arguments[2];
+ this->xmpp.send_stanza_error("iq", to_jid, std::to_string(iid), iq_id, "cancel", "item-not-found",
+ error_message, false);
+ }
+ else if (message.command == "482" && message.arguments.size() >= 2)
+ {
+ const std::string chan_name_later = utils::tolower(message.arguments[1]);
+ if (chan_name_later != iid.get_local())
+ return false;
+ std::string error_message = "You're not channel operator";
+ if (message.arguments.size() >= 3)
+ error_message = message.arguments[2];
+ this->xmpp.send_stanza_error("iq", to_jid, std::to_string(iid), iq_id, "cancel", "not-allowed",
+ error_message, false);
+ }
+ return true;
+ };
+ this->add_waiting_irc(std::move(cb));
+}
+
+void Bridge::set_channel_topic(const Iid& iid, const std::string& subject)
+{
+ IrcClient* irc = this->get_irc_client(iid.get_server());
+ irc->send_topic_command(iid.get_local(), subject);
+}
+
+void Bridge::send_xmpp_version_to_irc(const Iid& iid, const std::string& name, const std::string& version, const std::string& os)
+{
+ std::string result(name + " " + version + " " + os);
+
+ this->send_private_message(iid, "\01VERSION "s + result + "\01", "NOTICE");
+}
+
+void Bridge::send_irc_ping_result(const Iid& iid, const std::string& id)
+{
+ this->send_private_message(iid, "\01PING "s + utils::revstr(id) + "\01", "NOTICE");
+}
+
+void Bridge::send_irc_user_ping_request(const std::string& irc_hostname, const std::string& nick,
+ const std::string& iq_id, const std::string& to_jid,
+ const std::string& from_jid)
+{
+ Iid iid(nick + "!" + irc_hostname);
+ this->send_private_message(iid, "\01PING " + iq_id + "\01");
+
+ irc_responder_callback_t cb = [this, nick=utils::tolower(nick), iq_id, to_jid, irc_hostname, from_jid](const std::string& hostname, const IrcMessage& message) -> bool
+ {
+ if (irc_hostname != hostname || message.arguments.size() < 2)
+ return false;
+ IrcUser user(message.prefix);
+ const std::string body = message.arguments[1];
+ if (message.command == "NOTICE" && utils::tolower(user.nick) == nick
+ && body.substr(0, 6) == "\01PING ")
+ {
+ const std::string id = body.substr(6, body.size() - 7);
+ if (id != iq_id)
+ return false;
+ this->xmpp.send_iq_result_full_jid(iq_id, to_jid, from_jid);
+ return true;
+ }
+ if (message.command == "401" && message.arguments[1] == nick)
+ {
+ std::string error_message = "No such nick";
+ if (message.arguments.size() >= 3)
+ error_message = message.arguments[2];
+ this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "service-unavailable",
+ error_message, true);
+ return true;
+ }
+
+ return false;
+ };
+ this->add_waiting_irc(std::move(cb));
+}
+
+void Bridge::send_irc_participant_ping_request(const Iid& iid, const std::string& nick,
+ const std::string& iq_id, const std::string& to_jid,
+ const std::string& from_jid)
+{
+ IrcClient* irc = this->get_irc_client(iid.get_server());
+ IrcChannel* chan = irc->get_channel(iid.get_local());
+ if (!chan->joined)
+ {
+ this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "not-allowed",
+ "", true);
+ return;
+ }
+ if (chan->get_self()->nick != nick && !chan->find_user(nick))
+ {
+ this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "item-not-found",
+ "Recipient not in room", true);
+ return;
+ }
+
+ // The user is in the room, send it a direct PING
+ this->send_irc_user_ping_request(iid.get_server(), nick, iq_id, to_jid, from_jid);
+}
+
+void Bridge::on_gateway_ping(const std::string& irc_hostname, const std::string& iq_id, const std::string& to_jid,
+ const std::string& from_jid)
+{
+ Jid jid(from_jid);
+ if (irc_hostname.empty() || this->find_irc_client(irc_hostname))
+ this->xmpp.send_iq_result(iq_id, to_jid, jid.local);
+ else
+ this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "service-unavailable",
+ "", true);
+}
+
+void Bridge::send_irc_version_request(const std::string& irc_hostname, const std::string& target,
+ const std::string& iq_id, const std::string& to_jid,
+ const std::string& from_jid)
+{
+ Iid iid(target + "!" + irc_hostname);
+ this->send_private_message(iid, "\01VERSION\01");
+ // TODO, add a timer to remove that waiting iq if the server does not
+ // respond with a matching command before n seconds
+ irc_responder_callback_t cb = [this, target, iq_id, to_jid, irc_hostname, from_jid](const std::string& hostname, const IrcMessage& message) -> bool
+ {
+ if (irc_hostname != hostname)
+ return false;
+ IrcUser user(message.prefix);
+ if (message.command == "NOTICE" && user.nick == target &&
+ message.arguments.size() >= 2 && message.arguments[1].substr(0, 9) == "\01VERSION ")
+ {
+ // remove the "\01VERSION " and the "\01" parts from the string
+ const std::string version = message.arguments[1].substr(9, message.arguments[1].size() - 10);
+ this->xmpp.send_version(iq_id, to_jid, from_jid, version);
+ return true;
+ }
+ if (message.command == "401" && message.arguments.size() >= 2
+ && message.arguments[1] == target)
+ {
+ std::string error_message = "No such nick";
+ if (message.arguments.size() >= 3)
+ error_message = message.arguments[2];
+ this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "item-not-found",
+ error_message, true);
+ return true;
+ }
+ return false;
+ };
+ this->add_waiting_irc(std::move(cb));
+}
+
+void Bridge::send_message(const Iid& iid, const std::string& nick, const std::string& body, const bool muc)
+{
+ const auto encoding = in_encoding_for(*this, iid);
+ if (muc)
+ {
+ for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
+ {
+ this->xmpp.send_muc_message(std::to_string(iid), nick,
+ this->make_xmpp_body(body, encoding), this->user_jid + "/" + resource);
+ }
+ }
+ else
+ {
+ std::string target = std::to_string(iid);
+ const auto it = this->preferred_user_from.find(iid.get_local());
+ if (it != this->preferred_user_from.end())
+ {
+ const auto chan_name = Iid(Jid(it->second).local).get_local();
+ for (const auto& resource: this->resources_in_chan[ChannelKey{chan_name, iid.get_server()}])
+ this->xmpp.send_message(it->second, this->make_xmpp_body(body, encoding),
+ this->user_jid + "/" + resource, "chat", true);
+ }
+ else
+ {
+ for (const auto& resource: this->resources_in_server[iid.get_server()])
+ this->xmpp.send_message(std::to_string(iid), this->make_xmpp_body(body, encoding),
+ this->user_jid + "/" + resource, "chat", false);
+ }
+ }
+}
+
+void Bridge::send_presence_error(const Iid& iid, const std::string& nick,
+ const std::string& type, const std::string& condition,
+ const std::string& error_code, const std::string& text)
+{
+ this->xmpp.send_presence_error(std::to_string(iid), nick, this->user_jid, type, condition, error_code, text);
+}
+
+void Bridge::send_muc_leave(Iid&& iid, std::string&& nick, const std::string& message, const bool self, const std::string& resource)
+{
+ if (!resource.empty())
+ this->xmpp.send_muc_leave(std::to_string(iid), std::move(nick), this->make_xmpp_body(message), this->user_jid + "/" + resource,
+ self);
+ else
+ for (const auto& res: this->resources_in_chan[iid.to_tuple()])
+ this->xmpp.send_muc_leave(std::to_string(iid), std::move(nick), this->make_xmpp_body(message), this->user_jid + "/" + res,
+ self);
+ IrcClient* irc = this->find_irc_client(iid.get_server());
+ if (irc && irc->number_of_joined_channels() == 0)
+ irc->send_quit_command("");
+}
+
+void Bridge::send_nick_change(Iid&& iid,
+ const std::string& old_nick,
+ const std::string& new_nick,
+ const char user_mode,
+ const bool self)
+{
+ std::string affiliation;
+ std::string role;
+ std::tie(role, affiliation) = get_role_affiliation_from_irc_mode(user_mode);
+
+ for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
+ this->xmpp.send_nick_change(std::to_string(iid),
+ old_nick, new_nick, affiliation, role, this->user_jid + "/" + resource, self);
+}
+
+void Bridge::send_xmpp_message(const std::string& from, const std::string& author, const std::string& msg)
+{
+ std::string body;
+ if (!author.empty())
+ {
+ IrcUser user(author);
+ body = "\u000303"s + user.nick + (user.host.empty()?
+ "\u0003: ":
+ (" (\u000310" + user.host + "\u000303)\u0003: ")) + msg;
+ }
+ else
+ body = msg;
+
+ const auto encoding = in_encoding_for(*this, {from});
+ for (const auto& resource: this->resources_in_server[from])
+ {
+ this->xmpp.send_message(from, this->make_xmpp_body(body, encoding), this->user_jid + "/" + resource, "chat");
+ }
+}
+
+void Bridge::send_user_join(const std::string& hostname, const std::string& chan_name,
+ const IrcUser* user, const char user_mode, const bool self)
+{
+ for (const auto& resource: this->resources_in_chan[ChannelKey{chan_name, hostname}])
+ this->send_user_join(hostname, chan_name, user, user_mode, self, resource);
+}
+
+void Bridge::send_user_join(const std::string& hostname, const std::string& chan_name,
+ const IrcUser* user, const char user_mode,
+ const bool self, const std::string& resource)
+{
+ std::string affiliation;
+ std::string role;
+ std::tie(role, affiliation) = get_role_affiliation_from_irc_mode(user_mode);
+
+ std::string encoded_chan_name(chan_name);
+ xep0106::encode(encoded_chan_name);
+
+ this->xmpp.send_user_join(encoded_chan_name + utils::empty_if_fixed_server("%" + hostname), user->nick, user->host,
+ affiliation, role, this->user_jid + "/" + resource, self);
+}
+
+void Bridge::send_topic(const std::string& hostname, const std::string& chan_name, const std::string& topic, const std::string& who)
+{
+ for (const auto& resource: this->resources_in_chan[ChannelKey{chan_name, hostname}])
+ {
+ this->send_topic(hostname, chan_name, topic, who, resource);
+ }
+}
+
+void Bridge::send_topic(const std::string& hostname, const std::string& chan_name,
+ const std::string& topic, const std::string& who,
+ const std::string& resource)
+{
+ std::string encoded_chan_name(chan_name);
+ xep0106::encode(encoded_chan_name);
+ const auto encoding = in_encoding_for(*this, {encoded_chan_name + '%' + hostname});
+ this->xmpp.send_topic(encoded_chan_name + utils::empty_if_fixed_server(
+ "%" + hostname), this->make_xmpp_body(topic, encoding), this->user_jid + "/" + resource, who);
+
+}
+
+std::string Bridge::get_own_nick(const Iid& iid)
+{
+ IrcClient* irc = this->find_irc_client(iid.get_server());
+ if (irc)
+ return irc->get_own_nick();
+ return "";
+}
+
+size_t Bridge::active_clients() const
+{
+ return this->irc_clients.size();
+}
+
+void Bridge::kick_muc_user(Iid&& iid, const std::string& target, const std::string& reason, const std::string& author)
+{
+ for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
+ this->xmpp.kick_user(std::to_string(iid), target, reason, author, this->user_jid + "/" + resource);
+}
+
+void Bridge::send_nickname_conflict_error(const Iid& iid, const std::string& nickname)
+{
+ for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
+ this->xmpp.send_presence_error(std::to_string(iid), nickname, this->user_jid + "/" + resource, "cancel", "conflict", "409", "");
+}
+
+void Bridge::send_affiliation_role_change(const Iid& iid, const std::string& target, const char mode)
+{
+ std::string role;
+ std::string affiliation;
+
+ std::tie(role, affiliation) = get_role_affiliation_from_irc_mode(mode);
+ for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
+ this->xmpp.send_affiliation_role_change(std::to_string(iid), target, affiliation, role, this->user_jid + "/" + resource);
+}
+
+void Bridge::send_iq_version_request(const std::string& nick, const std::string& hostname)
+{
+ const auto resources = this->resources_in_server[hostname];
+ if (resources.begin() != resources.end())
+ this->xmpp.send_iq_version_request(utils::tolower(nick) + "!" + utils::empty_if_fixed_server(hostname), this->user_jid + "/" + *resources.begin());
+}
+
+void Bridge::send_xmpp_ping_request(const std::string& nick, const std::string& hostname,
+ const std::string& id)
+{
+ // Use revstr because the forwarded ping to target XMPP user must not be
+ // the same that the request iq, but we also need to get it back easily
+ // (revstr again)
+ // Forward to the first resource (arbitrary, based on the “order” of the std::set) only
+ const auto resources = this->resources_in_server[hostname];
+ if (resources.begin() != resources.end())
+ this->xmpp.send_ping_request(utils::tolower(nick) + "!" + utils::empty_if_fixed_server(hostname), this->user_jid + "/" + *resources.begin(), utils::revstr(id));
+}
+
+void Bridge::set_preferred_from_jid(const std::string& nick, const std::string& full_jid)
+{
+ auto it = this->preferred_user_from.find(nick);
+ if (it == this->preferred_user_from.end())
+ this->preferred_user_from.emplace(nick, full_jid);
+ else
+ this->preferred_user_from[nick] = full_jid;
+}
+
+void Bridge::remove_preferred_from_jid(const std::string& nick)
+{
+ auto it = this->preferred_user_from.find(nick);
+ if (it != this->preferred_user_from.end())
+ this->preferred_user_from.erase(it);
+}
+
+void Bridge::remove_all_preferred_from_jid_of_room(const std::string& channel_name)
+{
+ for (auto it = this->preferred_user_from.begin(); it != this->preferred_user_from.end();)
+ {
+ Iid iid(Jid(it->second).local);
+ if (iid.get_local() == channel_name)
+ it = this->preferred_user_from.erase(it);
+ else
+ ++it;
+ }
+}
+
+void Bridge::add_waiting_irc(irc_responder_callback_t&& callback)
+{
+ this->waiting_irc.emplace_back(std::move(callback));
+}
+
+void Bridge::trigger_on_irc_message(const std::string& irc_hostname, const IrcMessage& message)
+{
+ auto it = this->waiting_irc.begin();
+ while (it != this->waiting_irc.end())
+ {
+ if ((*it)(irc_hostname, message) == true)
+ it = this->waiting_irc.erase(it);
+ else
+ ++it;
+ }
+}
+
+std::unordered_map<std::string, std::shared_ptr<IrcClient>>& Bridge::get_irc_clients()
+{
+ return this->irc_clients;
+}
+
+void Bridge::add_resource_to_chan(const Bridge::ChannelKey& channel, const std::string& resource)
+{
+ auto it = this->resources_in_chan.find(channel);
+ if (it == this->resources_in_chan.end())
+ this->resources_in_chan[channel] = {resource};
+ else
+ it->second.insert(resource);
+}
+
+void Bridge::remove_resource_from_chan(const Bridge::ChannelKey& channel, const std::string& resource)
+{
+ auto it = this->resources_in_chan.find(channel);
+ if (it != this->resources_in_chan.end())
+ {
+ it->second.erase(resource);
+ if (it->second.empty())
+ this->resources_in_chan.erase(it);
+ }
+}
+
+bool Bridge::is_resource_in_chan(const Bridge::ChannelKey& channel, const std::string& resource) const
+{
+ auto it = this->resources_in_chan.find(channel);
+ if (it != this->resources_in_chan.end())
+ if (it->second.count(resource) == 1)
+ return true;
+ return false;
+}
+
+void Bridge::add_resource_to_server(const Bridge::IrcHostname& irc_hostname, const std::string& resource)
+{
+ auto it = this->resources_in_server.find(irc_hostname);
+ if (it == this->resources_in_server.end())
+ this->resources_in_server[irc_hostname] = {resource};
+ else
+ it->second.insert(resource);
+}
+
+void Bridge::remove_resource_from_server(const Bridge::IrcHostname& irc_hostname, const std::string& resource)
+{
+ auto it = this->resources_in_server.find(irc_hostname);
+ if (it != this->resources_in_server.end())
+ {
+ it->second.erase(resource);
+ if (it->second.empty())
+ this->resources_in_server.erase(it);
+ }
+}
+
+bool Bridge::is_resource_in_server(const Bridge::IrcHostname& irc_hostname, const std::string& resource) const
+{
+ auto it = this->resources_in_server.find(irc_hostname);
+ if (it != this->resources_in_server.end())
+ if (it->second.count(resource) == 1)
+ return true;
+ return false;
+}
+
+std::size_t Bridge::number_of_resources_in_chan(const Bridge::ChannelKey& channel_key) const
+{
+ auto it = this->resources_in_chan.find(channel_key);
+ if (it == this->resources_in_chan.end())
+ return 0;
+ return it->second.size();
+}
+
+std::size_t Bridge::number_of_channels_the_resource_is_in(const std::string& irc_hostname, const std::string& resource) const
+{
+ std::size_t res = 0;
+ for (auto pair: this->resources_in_chan)
+ {
+ if (std::get<0>(pair.first) == irc_hostname && pair.second.count(resource) != 0)
+ res++;
+ }
+ return res;
+}
+
+void Bridge::generate_channel_join_for_resource(const Iid& iid, const std::string& resource)
+{
+ IrcClient* irc = this->get_irc_client(iid.get_server());
+ IrcChannel* channel = irc->get_channel(iid.get_local());
+ const auto self = channel->get_self();
+
+ // Send the occupant list
+ for (const auto& user: channel->get_users())
+ {
+ if (user->nick != self->nick)
+ {
+ log_debug(user->nick);
+ this->send_user_join(iid.get_server(), iid.get_encoded_local(),
+ user.get(), user->get_most_significant_mode(irc->get_sorted_user_modes()),
+ false, resource);
+ }
+ }
+ this->send_user_join(iid.get_server(), iid.get_encoded_local(),
+ self, self->get_most_significant_mode(irc->get_sorted_user_modes()),
+ true, resource);
+ this->send_topic(iid.get_server(), iid.get_encoded_local(), channel->topic, channel->topic_author, resource);
+}
diff --git a/src/bridge/bridge.hpp b/src/bridge/bridge.hpp
new file mode 100644
index 0000000..69b7bd5
--- /dev/null
+++ b/src/bridge/bridge.hpp
@@ -0,0 +1,293 @@
+#pragma once
+
+
+#include <irc/irc_message.hpp>
+#include <irc/irc_client.hpp>
+#include <bridge/colors.hpp>
+#include <irc/irc_user.hpp>
+#include <irc/iid.hpp>
+
+#include <unordered_map>
+#include <functional>
+#include <exception>
+#include <string>
+#include <memory>
+
+class BiboumiComponent;
+class Poller;
+
+/**
+ * A callback called for each IrcMessage we receive. If the message triggers
+ * a response, it must send ore or more iq and return true (in that case it
+ * is removed from the list), otherwise it must do nothing and just return
+ * false.
+ */
+using irc_responder_callback_t = std::function<bool(const std::string& irc_hostname, const IrcMessage& message)>;
+
+/**
+ * One bridge is spawned for each XMPP user that uses the component. The
+ * bridge spawns IrcClients when needed (when the user wants to join a
+ * channel on a new server) and does the translation between the two
+ * protocols.
+ */
+class Bridge
+{
+public:
+ explicit Bridge(const std::string& user_jid, BiboumiComponent& xmpp, std::shared_ptr<Poller> poller);
+ ~Bridge() = default;
+
+ Bridge(const Bridge&) = delete;
+ Bridge(Bridge&& other) = delete;
+ Bridge& operator=(const Bridge&) = delete;
+ Bridge& operator=(Bridge&&) = delete;
+ /**
+ * QUIT all connected IRC servers.
+ */
+ void shutdown(const std::string& exit_message);
+ /**
+ * Remove all inactive IrcClients
+ */
+ void clean();
+ /**
+ * Return the jid of the XMPP user using this bridge
+ */
+ const std::string& get_jid() const;
+ std::string get_bare_jid() const;
+
+ static Xmpp::body make_xmpp_body(const std::string& str, const std::string& encoding = "ISO-8859-1");
+ /***
+ **
+ ** From XMPP to IRC.
+ **
+ **/
+
+ /**
+ * Try to join an irc_channel, does nothing and return true if the channel
+ * was already joined.
+ */
+ bool join_irc_channel(const Iid& iid, const std::string& nickname, const std::string& password, const std::string& resource);
+
+ void send_channel_message(const Iid& iid, const std::string& body);
+ void send_private_message(const Iid& iid, const std::string& body, const std::string& type="PRIVMSG");
+ void send_raw_message(const std::string& hostname, const std::string& body);
+ void leave_irc_channel(Iid&& iid, std::string&& status_message, const std::string& resource);
+ void send_irc_nick_change(const Iid& iid, const std::string& new_nick);
+ void send_irc_kick(const Iid& iid, const std::string& target, const std::string& reason,
+ const std::string& iq_id, const std::string& to_jid);
+ void set_channel_topic(const Iid& iid, const std::string& subject);
+ void send_xmpp_version_to_irc(const Iid& iid, const std::string& name, const std::string& version,
+ const std::string& os);
+ void send_irc_ping_result(const Iid& iid, const std::string& id);
+ void send_irc_version_request(const std::string& irc_hostname, const std::string& target,
+ const std::string& iq_id, const std::string& to_jid,
+ const std::string& from_jid);
+ void send_irc_channel_list_request(const Iid& iid, const std::string& iq_id,
+ const std::string& to_jid);
+ void forward_affiliation_role_change(const Iid& iid, const std::string& nick,
+ const std::string& affiliation, const std::string& role);
+ /**
+ * Directly send a CTCP PING request to the IRC user
+ */
+ void send_irc_user_ping_request(const std::string& irc_hostname, const std::string& nick,
+ const std::string& iq_id, const std::string& to_jid,
+ const std::string& from_jid);
+ /**
+ * First check if the participant is in the room, before sending a direct
+ * CTCP PING request to the IRC user
+ */
+ void send_irc_participant_ping_request(const Iid& iid, const std::string& nick,
+ const std::string& iq_id, const std::string& to_jid,
+ const std::string& from_jid);
+ /**
+ * Directly send back a result if it's a gateway ping or if we are
+ * connected to the given IRC server, an error otherwise.
+ */
+ void on_gateway_ping(const std::string& irc_hostname, const std::string& iq_id, const std::string& to_jid,
+ const std::string& from_jid);
+
+ /***
+ **
+ ** From IRC to XMPP.
+ **
+ **/
+
+ /**
+ * Send a message corresponding to a server NOTICE, the from attribute
+ * should be juste the server hostname.
+ */
+ void send_xmpp_message(const std::string& from, const std::string& author, const std::string& msg);
+ /**
+ * Send the presence of a new user in the MUC.
+ */
+ void send_user_join(const std::string& hostname, const std::string& chan_name,
+ const IrcUser* user, const char user_mode,
+ const bool self, const std::string& resource);
+ void send_user_join(const std::string& hostname, const std::string& chan_name,
+ const IrcUser* user, const char user_mode,
+ const bool self);
+
+ /**
+ * Send the topic of the MUC to the user
+ */
+ void send_topic(const std::string& hostname, const std::string& chan_name, const std::string& topic, const std::string& who);
+ void send_topic(const std::string& hostname, const std::string& chan_name, const std::string& topic, const std::string& who, const std::string& resource);
+ /**
+ * Send a MUC message from some participant
+ */
+ void send_message(const Iid& iid, const std::string& nick, const std::string& body, const bool muc);
+ /**
+ * Send a presence of type error, from a room.
+ */
+ void send_presence_error(const Iid& iid, const std::string& nick, const std::string& type, const std::string& condition, const std::string& error_code, const std::string& text);
+ /**
+ * Send an unavailable presence from this participant
+ */
+ void send_muc_leave(Iid&& iid, std::string&& nick, const std::string& message, const bool self, const std::string& resource="");
+ /**
+ * Send presences to indicate that an user old_nick (ourself if self ==
+ * true) changed his nick to new_nick. The user_mode is needed because
+ * the xmpp presence needs ton contain the role and affiliation of the
+ * user.
+ */
+ void send_nick_change(Iid&& iid,
+ const std::string& old_nick,
+ const std::string& new_nick,
+ const char user_mode,
+ const bool self);
+ void kick_muc_user(Iid&& iid, const std::string& target, const std::string& reason, const std::string& author);
+ void send_nickname_conflict_error(const Iid& iid, const std::string& nickname);
+ /**
+ * Send a role/affiliation change, matching the change of mode for that user
+ */
+ void send_affiliation_role_change(const Iid& iid, const std::string& target, const char mode);
+ /**
+ * Send an iq version request coming from nick!hostname@
+ */
+ void send_iq_version_request(const std::string& nick, const std::string& hostname);
+ /**
+ * Send an iq ping request coming from nick!hostname@
+ */
+ void send_xmpp_ping_request(const std::string& nick, const std::string& hostname,
+ const std::string& id);
+ /**
+ * Misc
+ */
+ std::string get_own_nick(const Iid& iid);
+ /**
+ * Get the number of server to which this bridge is connected or connecting.
+ */
+ size_t active_clients() const;
+ /**
+ * Add (or replace the existing) <nick, jid> into the preferred_user_from map
+ */
+ void set_preferred_from_jid(const std::string& nick, const std::string& full_jid);
+ /**
+ * Remove the preferred jid for the given IRC nick
+ */
+ void remove_preferred_from_jid(const std::string& nick);
+ /**
+ * Given a channel_name, remove all preferred from_jid that come
+ * from this chan.
+ */
+ void remove_all_preferred_from_jid_of_room(const std::string& channel_name);
+ /**
+ * Add a callback to the waiting list of irc callbacks.
+ */
+ void add_waiting_irc(irc_responder_callback_t&& callback);
+ /**
+ * Iter over all the waiting_iq, call the iq_responder_filter_t for each,
+ * whenever one of them returns true: call the corresponding
+ * iq_responder_callback_t and remove the callback from the list.
+ */
+ void trigger_on_irc_message(const std::string& irc_hostname, const IrcMessage& message);
+ std::unordered_map<std::string, std::shared_ptr<IrcClient>>& get_irc_clients();
+
+private:
+ /**
+ * Returns the client for the given hostname, create one (and use the
+ * username in this case) if none is found, and connect that newly-created
+ * client immediately.
+ */
+ IrcClient* make_irc_client(const std::string& hostname, const std::string& nickname);
+ /**
+ * This version does not create the IrcClient if it does not exist, throws
+ * a IRCServerNotConnected error in that case.
+ */
+ IrcClient* get_irc_client(const std::string& hostname);
+ /**
+ * Idem, but returns nullptr if the server does not exist.
+ */
+ IrcClient* find_irc_client(const std::string& hostname);
+ /**
+ * The bare JID of the user associated with this bridge. Messages from/to this
+ * JID are only managed by this bridge.
+ */
+ const std::string user_jid;
+ /**
+ * One IrcClient for each IRC server we need to be connected to.
+ * The pointer is shared by the bridge and the poller.
+ */
+ std::unordered_map<std::string, std::shared_ptr<IrcClient>> irc_clients;
+ /**
+ * To communicate back with the XMPP component
+ */
+ BiboumiComponent& xmpp;
+ /**
+ * Poller, to give it the IrcClients that we spawn, to make it manage
+ * their sockets.
+ */
+ std::shared_ptr<Poller> poller;
+ /**
+ * A map of <nick, full_jid>. For example if this map contains <"toto",
+ * "#somechan%server@biboumi/ToTo">, whenever a private message is
+ * received from the user "toto", instead of forwarding it to XMPP with
+ * from='toto!server@biboumi', we use instead
+ * from='#somechan%server@biboumi/ToTo'
+ */
+ std::unordered_map<std::string, std::string> preferred_user_from;
+ /**
+ * A list of callbacks that are waiting for some IrcMessage to trigger a
+ * response. We add callbacks in this list whenever we received an IQ
+ * request and we need a response from IRC to be able to provide the
+ * response iq.
+ */
+ std::vector<irc_responder_callback_t> waiting_irc;
+
+ /**
+ * Resources to IRC channel/server mapping:
+ */
+ using Resource = std::string;
+ using ChannelName = std::string;
+ using IrcHostname = std::string;
+ using ChannelKey = std::tuple<ChannelName, IrcHostname>;
+ std::map<ChannelKey, std::set<Resource>> resources_in_chan;
+ std::map<IrcHostname, std::set<Resource>> resources_in_server;
+ /**
+ * Manage which resource is in which channel
+ */
+ void add_resource_to_chan(const ChannelKey& channel_key, const std::string& resource);
+ void remove_resource_from_chan(const ChannelKey& channel_key, const std::string& resource);
+ bool is_resource_in_chan(const ChannelKey& channel_key, const std::string& resource) const;
+ std::size_t number_of_resources_in_chan(const ChannelKey& channel_key) const;
+
+ void add_resource_to_server(const IrcHostname& irc_hostname, const std::string& resource);
+ void remove_resource_from_server(const IrcHostname& irc_hostname, const std::string& resource);
+ bool is_resource_in_server(const IrcHostname& irc_hostname, const std::string& resource) const;
+ size_t number_of_channels_the_resource_is_in(const std::string& irc_hostname, const std::string& resource) const;
+
+ /**
+ * Generate all the stanzas to be sent to this resource, simulating a join on this channel.
+ * This means sending the whole user list, the topic, etc
+ * TODO: send message history
+ */
+ void generate_channel_join_for_resource(const Iid& iid, const std::string& resource);
+};
+
+struct IRCNotConnected: public std::exception
+{
+ IRCNotConnected(const std::string& hostname):
+ hostname(hostname) {}
+ const std::string hostname;
+};
+
+
diff --git a/src/bridge/colors.cpp b/src/bridge/colors.cpp
new file mode 100644
index 0000000..66f51ee
--- /dev/null
+++ b/src/bridge/colors.cpp
@@ -0,0 +1,170 @@
+#include <bridge/colors.hpp>
+#include <xmpp/xmpp_stanza.hpp>
+
+#include <algorithm>
+#include <iostream>
+
+#include <string.h>
+
+using namespace std::string_literals;
+
+static const char IRC_NUM_COLORS = 16;
+
+static const char* irc_colors_to_css[IRC_NUM_COLORS] = {
+ "white",
+ "black",
+ "blue",
+ "green",
+ "indianred",
+ "red",
+ "magenta",
+ "brown",
+ "yellow",
+ "lightgreen",
+ "cyan",
+ "lightcyan",
+ "lightblue",
+ "lightmagenta",
+ "gray",
+ "white",
+};
+
+#define XHTML_NS "http://www.w3.org/1999/xhtml"
+
+struct styles_t
+{
+ bool strong;
+ bool underline;
+ bool italic;
+ int fg;
+ int bg;
+};
+
+/** We keep the currently-applied CSS styles in a structure. Each time a tag
+ * is found, update this style list, then close the current span XML element
+ * (if it is open), then reopen it with all the new styles in it. This is
+ * done this way because IRC formatting does not map well with XML
+ * (hierarchical tags), it’s a lot easier and cleaner to remove all styles
+ * and reapply them for each tag, instead of trying to keep a consistent
+ * hierarchy of span, strong, em etc tags. The generated XML is one-level
+ * deep only.
+*/
+Xmpp::body irc_format_to_xhtmlim(const std::string& s)
+{
+ if (s.find_first_of(irc_format_char) == std::string::npos)
+ // there is no special formatting at all
+ return std::make_tuple(s, nullptr);
+
+ std::string cleaned;
+
+ styles_t styles = {false, false, false, -1, -1};
+
+ std::unique_ptr<XmlNode> result = std::make_unique<XmlNode>("body");
+ (*result)["xmlns"] = XHTML_NS;
+
+ std::unique_ptr<XmlNode> current_node_up;
+ XmlNode* current_node = result.get();
+
+ std::string::size_type pos_start = 0;
+ std::string::size_type pos_end;
+
+ while ((pos_end = s.find_first_of(irc_format_char, pos_start)) != std::string::npos)
+ {
+ const std::string txt = s.substr(pos_start, pos_end-pos_start);
+ cleaned += txt;
+ if (current_node->has_children())
+ current_node->get_last_child()->add_to_tail(txt);
+ else
+ current_node->add_to_inner(txt);
+
+ if (s[pos_end] == IRC_FORMAT_BOLD_CHAR)
+ styles.strong = !styles.strong;
+ else if (s[pos_end] == IRC_FORMAT_NEWLINE_CHAR)
+ {
+ current_node->add_child(std::make_unique<XmlNode>("br"));
+ cleaned += '\n';
+ }
+ else if (s[pos_end] == IRC_FORMAT_UNDERLINE_CHAR)
+ styles.underline = !styles.underline;
+ else if (s[pos_end] == IRC_FORMAT_ITALIC_CHAR)
+ styles.italic = !styles.italic;
+ else if (s[pos_end] == IRC_FORMAT_RESET_CHAR)
+ styles = {false, false, false, -1, -1};
+ else if (s[pos_end] == IRC_FORMAT_REVERSE_CHAR)
+ { } // TODO
+ else if (s[pos_end] == IRC_FORMAT_REVERSE2_CHAR)
+ { } // TODO
+ else if (s[pos_end] == IRC_FORMAT_FIXED_CHAR)
+ { } // TODO
+ else if (s[pos_end] == IRC_FORMAT_COLOR_CHAR)
+ {
+ size_t pos = pos_end + 1;
+ styles.fg = -1;
+ styles.bg = -1;
+ // get the first number following the format char
+ if (pos < s.size() && s[pos] >= '0' && s[pos] <= '9')
+ { // first digit
+ styles.fg = s[pos++] - '0';
+ if (pos < s.size() && s[pos] >= '0' && s[pos] <= '9')
+ // second digit
+ styles.fg = styles.fg * 10 + s[pos++] - '0';
+ }
+ if (pos < s.size() && s[pos] == ',')
+ { // get bg color after the comma
+ pos++;
+ if (pos < s.size() && s[pos] >= '0' && s[pos] <= '9')
+ { // first digit
+ styles.bg = s[pos++] - '0';
+ if (pos < s.size() && s[pos] >= '0' && s[pos] <= '9')
+ // second digit
+ styles.bg = styles.bg * 10 + s[pos++] - '0';
+ }
+ }
+ pos_end = pos - 1;
+ }
+
+ // close opened span, if any
+ if (current_node != result.get())
+ {
+ result->add_child(std::move(current_node_up));
+ current_node = result.get();
+ }
+ // Take all currently-applied style and create a new span with it
+ std::string styles_str;
+ if (styles.strong)
+ styles_str += "font-weight:bold;";
+ if (styles.underline)
+ styles_str += "text-decoration:underline;";
+ if (styles.italic)
+ styles_str += "font-style:italic;";
+ if (styles.fg != -1)
+ styles_str += "color:"s +
+ irc_colors_to_css[styles.fg % IRC_NUM_COLORS] + ";";
+ if (styles.bg != -1)
+ styles_str += "background-color:"s +
+ irc_colors_to_css[styles.bg % IRC_NUM_COLORS] + ";";
+ if (!styles_str.empty())
+ {
+ current_node_up = std::make_unique<XmlNode>("span");
+ current_node = current_node_up.get();
+ (*current_node)["style"] = styles_str;
+ }
+
+ pos_start = pos_end + 1;
+ }
+
+ // If some text remains, without any format char, just append that text at
+ // the end of the current node
+ const std::string txt = s.substr(pos_start, pos_end-pos_start);
+ cleaned += txt;
+ if (current_node->has_children())
+ current_node->get_last_child()->add_to_tail(txt);
+ else
+ current_node->add_to_inner(txt);
+
+ if (current_node != result.get())
+ result->add_child(std::move(current_node_up));
+
+ Xmpp::body body_res = std::make_tuple(cleaned, std::move(result));
+ return body_res;
+}
diff --git a/src/bridge/colors.hpp b/src/bridge/colors.hpp
new file mode 100644
index 0000000..e2c8a87
--- /dev/null
+++ b/src/bridge/colors.hpp
@@ -0,0 +1,56 @@
+#pragma once
+
+
+/**
+ * A module handling the conversion between IRC colors and XHTML-IM, and
+ * vice versa.
+ */
+
+#include <string>
+#include <memory>
+#include <tuple>
+
+class XmlNode;
+
+namespace Xmpp
+{
+// Contains:
+// - an XMPP-valid UTF-8 body
+// - an XML node representing the XHTML-IM body, or null
+ using body = std::tuple<const std::string, std::unique_ptr<XmlNode>>;
+}
+
+#define IRC_FORMAT_BOLD_CHAR '\x02' // done
+#define IRC_FORMAT_COLOR_CHAR '\x03' // done
+#define IRC_FORMAT_RESET_CHAR '\x0F' // done
+#define IRC_FORMAT_FIXED_CHAR '\x11' // ??
+#define IRC_FORMAT_REVERSE_CHAR '\x12' // maybe one day
+#define IRC_FORMAT_REVERSE2_CHAR '\x16' // wat
+#define IRC_FORMAT_ITALIC_CHAR '\x1D' // done
+#define IRC_FORMAT_UNDERLINE_CHAR '\x1F' // done
+#define IRC_FORMAT_NEWLINE_CHAR '\n' // done
+
+static const char irc_format_char[] = {
+ IRC_FORMAT_BOLD_CHAR,
+ IRC_FORMAT_COLOR_CHAR,
+ IRC_FORMAT_RESET_CHAR,
+ IRC_FORMAT_FIXED_CHAR,
+ IRC_FORMAT_REVERSE_CHAR,
+ IRC_FORMAT_REVERSE2_CHAR,
+ IRC_FORMAT_ITALIC_CHAR,
+ IRC_FORMAT_UNDERLINE_CHAR,
+ IRC_FORMAT_NEWLINE_CHAR,
+ '\x00'
+};
+
+/**
+ * Convert the passed string into an XML tree representing the XHTML version
+ * of the message, converting the IRC colors symbols into xhtml-im
+ * formatting.
+ *
+ * Returns the body cleaned from any IRC formatting (but without any xhtml),
+ * and the body as XHTML-IM
+ */
+Xmpp::body irc_format_to_xhtmlim(const std::string& str);
+
+
diff --git a/src/bridge/list_element.hpp b/src/bridge/list_element.hpp
new file mode 100644
index 0000000..1eff2ee
--- /dev/null
+++ b/src/bridge/list_element.hpp
@@ -0,0 +1,19 @@
+#pragma once
+
+
+#include <string>
+
+struct ListElement
+{
+ ListElement(const std::string& channel, const std::string& nb_users,
+ const std::string& topic):
+ channel(channel),
+ nb_users(nb_users),
+ topic(topic){}
+
+ std::string channel;
+ std::string nb_users;
+ std::string topic;
+};
+
+
diff --git a/src/database/database.cpp b/src/database/database.cpp
new file mode 100644
index 0000000..61e1b47
--- /dev/null
+++ b/src/database/database.cpp
@@ -0,0 +1,87 @@
+#include "biboumi.h"
+#ifdef USE_DATABASE
+
+#include <database/database.hpp>
+#include <logger/logger.hpp>
+#include <string>
+
+using namespace std::string_literals;
+
+std::unique_ptr<db::BibouDB> Database::db;
+
+void Database::open(const std::string& filename, const std::string& db_type)
+{
+ try
+ {
+ auto new_db = std::make_unique<db::BibouDB>(db_type,
+ "database="s + filename);
+ if (new_db->needsUpgrade())
+ new_db->upgrade();
+ Database::db.reset(new_db.release());
+ } catch (const litesql::DatabaseError& e) {
+ log_error("Failed to open database ", filename, ". ", e.what());
+ throw;
+ }
+}
+
+void Database::set_verbose(const bool val)
+{
+ Database::db->verbose = val;
+}
+
+db::IrcServerOptions Database::get_irc_server_options(const std::string& owner,
+ const std::string& server)
+{
+ try {
+ auto options = litesql::select<db::IrcServerOptions>(*Database::db,
+ db::IrcServerOptions::Owner == owner &&
+ db::IrcServerOptions::Server == server).one();
+ return options;
+ } catch (const litesql::NotFound& e) {
+ db::IrcServerOptions options(*Database::db);
+ options.owner = owner;
+ options.server = server;
+ // options.update();
+ return options;
+ }
+}
+
+db::IrcChannelOptions Database::get_irc_channel_options(const std::string& owner,
+ const std::string& server,
+ const std::string& channel)
+{
+ try {
+ auto options = litesql::select<db::IrcChannelOptions>(*Database::db,
+ db::IrcChannelOptions::Owner == owner &&
+ db::IrcChannelOptions::Server == server &&
+ db::IrcChannelOptions::Channel == channel).one();
+ return options;
+ } catch (const litesql::NotFound& e) {
+ db::IrcChannelOptions options(*Database::db);
+ options.owner = owner;
+ options.server = server;
+ options.channel = channel;
+ return options;
+ }
+}
+
+db::IrcChannelOptions Database::get_irc_channel_options_with_server_default(const std::string& owner,
+ const std::string& server,
+ const std::string& channel)
+{
+ auto coptions = Database::get_irc_channel_options(owner, server, channel);
+ auto soptions = Database::get_irc_server_options(owner, server);
+ if (coptions.encodingIn.value().empty())
+ coptions.encodingIn = soptions.encodingIn;
+ if (coptions.encodingOut.value().empty())
+ coptions.encodingOut = soptions.encodingOut;
+
+ return coptions;
+}
+
+void Database::close()
+{
+ Database::db.reset(nullptr);
+}
+
+#endif
diff --git a/src/database/database.hpp b/src/database/database.hpp
new file mode 100644
index 0000000..7173bcd
--- /dev/null
+++ b/src/database/database.hpp
@@ -0,0 +1,53 @@
+#pragma once
+
+
+#include <biboumi.h>
+#ifdef USE_DATABASE
+
+#include "biboudb.hpp"
+
+#include <memory>
+
+#include <litesql.hpp>
+
+class Database
+{
+public:
+ Database() = default;
+ ~Database() = default;
+
+ Database(const Database&) = delete;
+ Database(Database&&) = delete;
+ Database& operator=(const Database&) = delete;
+ Database& operator=(Database&&) = delete;
+
+ static void set_verbose(const bool val);
+
+ template<typename PersistentType>
+ static size_t count()
+ {
+ return litesql::select<PersistentType>(*Database::db).count();
+ }
+ /**
+ * Return the object from the db. Create it beforehand (with all default
+ * values) if it is not already present.
+ */
+ static db::IrcServerOptions get_irc_server_options(const std::string& owner,
+ const std::string& server);
+ static db::IrcChannelOptions get_irc_channel_options(const std::string& owner,
+ const std::string& server,
+ const std::string& channel);
+ static db::IrcChannelOptions get_irc_channel_options_with_server_default(const std::string& owner,
+ const std::string& server,
+ const std::string& channel);
+
+ static void close();
+ static void open(const std::string& filename, const std::string& db_type="sqlite3");
+
+
+private:
+ static std::unique_ptr<db::BibouDB> db;
+};
+#endif /* USE_DATABASE */
+
+
diff --git a/src/irc/iid.cpp b/src/irc/iid.cpp
new file mode 100644
index 0000000..0e2841e
--- /dev/null
+++ b/src/irc/iid.cpp
@@ -0,0 +1,118 @@
+#include <utils/tolower.hpp>
+#include <config/config.hpp>
+
+#include <irc/iid.hpp>
+
+#include <utils/encoding.hpp>
+
+Iid::Iid(const std::string& iid):
+ is_channel(false),
+ is_user(false)
+{
+ const std::string fixed_irc_server = Config::get("fixed_irc_server", "");
+ if (fixed_irc_server.empty())
+ this->init(iid);
+ else
+ this->init_with_fixed_server(iid, fixed_irc_server);
+}
+
+
+void Iid::init(const std::string& iid)
+{
+ const std::string::size_type sep = iid.find_first_of("%!");
+ if (sep != std::string::npos)
+ {
+ if (iid[sep] == '%')
+ this->is_channel = true;
+ else
+ this->is_user = true;
+ this->set_local(iid.substr(0, sep));
+ this->set_server(iid.substr(sep + 1));
+ }
+ else
+ this->set_server(iid);
+}
+
+void Iid::init_with_fixed_server(const std::string& iid, const std::string& hostname)
+{
+ this->set_server(hostname);
+
+ const std::string::size_type sep = iid.find("!");
+
+ // Without any separator, we consider that it's a channel
+ if (sep == std::string::npos)
+ {
+ this->is_channel = true;
+ this->set_local(iid);
+ }
+ else // A separator can be present to differenciate a channel from a user,
+ // but the part behind it (the hostname) is ignored
+ {
+ this->set_local(iid.substr(0, sep));
+ this->is_user = true;
+ }
+}
+
+Iid::Iid():
+ is_channel(false),
+ is_user(false)
+{
+}
+
+void Iid::set_local(const std::string& loc)
+{
+ std::string local(utils::tolower(loc));
+ xep0106::decode(local);
+ this->local = local;
+}
+
+void Iid::set_server(const std::string& serv)
+{
+ this->server = utils::tolower(serv);
+}
+
+const std::string& Iid::get_local() const
+{
+ return this->local;
+}
+
+const std::string Iid::get_encoded_local() const
+{
+ std::string local(this->local);
+ xep0106::encode(local);
+ return local;
+}
+
+const std::string& Iid::get_server() const
+{
+ return this->server;
+}
+
+std::string Iid::get_sep() const
+{
+ if (this->is_channel)
+ return "%";
+ else if (this->is_user)
+ return "!";
+ return "";
+}
+
+namespace std {
+ const std::string to_string(const Iid& iid)
+ {
+ if (Config::get("fixed_irc_server", "").empty())
+ return iid.get_encoded_local() + iid.get_sep() + iid.get_server();
+ else
+ {
+ if (iid.get_sep() == "!")
+ return iid.get_encoded_local() + iid.get_sep();
+ else
+ return iid.get_encoded_local();
+ }
+ }
+}
+
+std::tuple<std::string, std::string> Iid::to_tuple() const
+{
+ return std::make_tuple(this->get_local(), this->get_server());
+}
diff --git a/src/irc/iid.hpp b/src/irc/iid.hpp
new file mode 100644
index 0000000..3b11470
--- /dev/null
+++ b/src/irc/iid.hpp
@@ -0,0 +1,79 @@
+#pragma once
+
+
+#include <string>
+
+/**
+ * A name representing an IRC channel on an IRC server, or an IRC user on an
+ * IRC server, or just an IRC server.
+ *
+ * The separator for an user is '!', for a channel it's '%'. If no separator
+ * is present, it's just an irc server.
+ * It’s possible to have an empty-string server, but it makes no sense in
+ * the biboumi context.
+ *
+ * #test%irc.example.org has :
+ * - local: "#test" (the # is part of the name, it could very well be absent, or & (for example) instead)
+ * - server: "irc.example.org"
+ * - is_channel: true
+ * - is_user: false
+ *
+ * %irc.example.org:
+ * - local: ""
+ * - server: "irc.example.org"
+ * - is_channel: true
+ * - is_user: false
+ * Note: this is the special empty-string channel, used internal in biboumi
+ * but has no meaning on IRC.
+ *
+ * foo!irc.example.org
+ * - local: "foo"
+ * - server: "irc.example.org"
+ * - is_channel: false
+ * - is_user: true
+ * Note: the empty-string user (!irc.example.org) has no special meaning in biboumi
+ *
+ * irc.example.org:
+ * - local: ""
+ * - server: "irc.example.org"
+ * - is_channel: false
+ * - is_user: false
+ */
+class Iid
+{
+public:
+ Iid(const std::string& iid);
+ Iid();
+ Iid(const Iid&) = default;
+
+ Iid(Iid&&) = delete;
+ Iid& operator=(const Iid&) = delete;
+ Iid& operator=(Iid&&) = delete;
+
+ void set_local(const std::string& loc);
+ void set_server(const std::string& serv);
+ const std::string& get_local() const;
+ const std::string get_encoded_local() const;
+ const std::string& get_server() const;
+
+ bool is_channel;
+ bool is_user;
+
+ std::string get_sep() const;
+
+ std::tuple<std::string, std::string> to_tuple() const;
+
+private:
+
+ void init(const std::string& iid);
+ void init_with_fixed_server(const std::string& iid, const std::string& hostname);
+
+ std::string local;
+ std::string server;
+};
+
+namespace std {
+ const std::string to_string(const Iid& iid);
+}
+
+
diff --git a/src/irc/irc_channel.cpp b/src/irc/irc_channel.cpp
new file mode 100644
index 0000000..e769245
--- /dev/null
+++ b/src/irc/irc_channel.cpp
@@ -0,0 +1,60 @@
+#include <irc/irc_channel.hpp>
+#include <algorithm>
+
+IrcChannel::IrcChannel():
+ joined(false),
+ self(nullptr)
+{
+}
+
+void IrcChannel::set_self(const std::string& name)
+{
+ this->self = std::make_unique<IrcUser>(name);
+}
+
+IrcUser* IrcChannel::add_user(const std::string& name,
+ const std::map<char, char>& prefix_to_mode)
+{
+ this->users.emplace_back(std::make_unique<IrcUser>(name, prefix_to_mode));
+ return this->users.back().get();
+}
+
+IrcUser* IrcChannel::get_self() const
+{
+ return this->self.get();
+}
+
+IrcUser* IrcChannel::find_user(const std::string& name) const
+{
+ IrcUser user(name);
+ for (const auto& u: this->users)
+ {
+ if (u->nick == user.nick)
+ return u.get();
+ }
+ return nullptr;
+}
+
+void IrcChannel::remove_user(const IrcUser* user)
+{
+ const auto nick = user->nick;
+ const auto it = std::find_if(this->users.begin(), this->users.end(),
+ [nick](const std::unique_ptr<IrcUser>& u)
+ {
+ return nick == u->nick;
+ });
+ if (it != this->users.end())
+ this->users.erase(it);
+}
+
+void IrcChannel::remove_all_users()
+{
+ this->users.clear();
+ this->self.reset();
+}
+
+DummyIrcChannel::DummyIrcChannel():
+ IrcChannel(),
+ joining(false)
+{
+}
diff --git a/src/irc/irc_channel.hpp b/src/irc/irc_channel.hpp
new file mode 100644
index 0000000..2bcefaf
--- /dev/null
+++ b/src/irc/irc_channel.hpp
@@ -0,0 +1,70 @@
+#pragma once
+
+
+#include <irc/irc_user.hpp>
+#include <memory>
+#include <string>
+#include <vector>
+#include <map>
+
+/**
+ * Keep the state of a joined channel (the list of occupants with their
+ * informations (mode, etc), the modes, etc)
+ */
+class IrcChannel
+{
+public:
+ explicit IrcChannel();
+
+ IrcChannel(const IrcChannel&) = delete;
+ IrcChannel(IrcChannel&&) = delete;
+ IrcChannel& operator=(const IrcChannel&) = delete;
+ IrcChannel& operator=(IrcChannel&&) = delete;
+
+ bool joined;
+ std::string topic;
+ std::string topic_author;
+ void set_self(const std::string& name);
+ IrcUser* get_self() const;
+ IrcUser* add_user(const std::string& name,
+ const std::map<char, char>& prefix_to_mode);
+ IrcUser* find_user(const std::string& name) const;
+ void remove_user(const IrcUser* user);
+ void remove_all_users();
+ const std::vector<std::unique_ptr<IrcUser>>& get_users() const
+ { return this->users; }
+
+protected:
+ std::unique_ptr<IrcUser> self;
+ std::vector<std::unique_ptr<IrcUser>> users;
+};
+
+/**
+ * A special channel that is not actually linked to any real irc
+ * channel. This is just a channel representing a connection to the
+ * server. If an user wants to maintain the connection to the server without
+ * having to be on any irc channel of that server, he can just join this
+ * dummy channel.
+ * It’s not actually dummy because it’s useful and it does things, but well.
+ */
+class DummyIrcChannel: public IrcChannel
+{
+public:
+ explicit DummyIrcChannel();
+ DummyIrcChannel(const DummyIrcChannel&) = delete;
+ DummyIrcChannel(DummyIrcChannel&&) = delete;
+ DummyIrcChannel& operator=(const DummyIrcChannel&) = delete;
+ DummyIrcChannel& operator=(DummyIrcChannel&&) = delete;
+
+ /**
+ * This flag is at true whenever the user wants to join this channel, but
+ * he is not yet connected to the server. When the connection is made, we
+ * check that flag and if it’s true, we inform the user that he has just
+ * joined that channel.
+ * If the user is already connected to the server when he tries to join
+ * the channel, we don’t use that flag, we just join it immediately.
+ */
+ bool joining;
+};
+
+
diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp
new file mode 100644
index 0000000..dd83307
--- /dev/null
+++ b/src/irc/irc_client.cpp
@@ -0,0 +1,1120 @@
+#include <utils/timed_events.hpp>
+#include <database/database.hpp>
+#include <irc/irc_message.hpp>
+#include <irc/irc_client.hpp>
+#include <bridge/bridge.hpp>
+#include <irc/irc_user.hpp>
+
+#include <logger/logger.hpp>
+#include <config/config.hpp>
+#include <utils/tolower.hpp>
+#include <utils/split.hpp>
+#include <utils/string.hpp>
+
+#include <sstream>
+#include <iostream>
+#include <stdexcept>
+#include <cstring>
+
+#include <chrono>
+#include <string>
+
+#include "biboumi.h"
+#include "louloulibs.h"
+
+using namespace std::string_literals;
+using namespace std::chrono_literals;
+
+/**
+ * Define a map of functions to be called for each IRC command we can
+ * handle.
+ */
+using IrcCallback = void (IrcClient::*)(const IrcMessage&);
+
+static const std::unordered_map<std::string,
+ std::pair<IrcCallback, std::pair<std::size_t, std::size_t>>> irc_callbacks = {
+ {"NOTICE", {&IrcClient::on_notice, {2, 0}}},
+ {"002", {&IrcClient::forward_server_message, {2, 0}}},
+ {"003", {&IrcClient::forward_server_message, {2, 0}}},
+ {"004", {&IrcClient::on_server_myinfo, {4, 0}}},
+ {"005", {&IrcClient::on_isupport_message, {0, 0}}},
+ {"RPL_LISTSTART", {&IrcClient::on_rpl_liststart, {0, 0}}},
+ {"321", {&IrcClient::on_rpl_liststart, {0, 0}}},
+ {"RPL_LIST", {&IrcClient::on_rpl_list, {0, 0}}},
+ {"322", {&IrcClient::on_rpl_list, {0, 0}}},
+ {"RPL_LISTEND", {&IrcClient::on_rpl_listend, {0, 0}}},
+ {"323", {&IrcClient::on_rpl_listend, {0, 0}}},
+ {"RPL_NOTOPIC", {&IrcClient::on_empty_topic, {0, 0}}},
+ {"331", {&IrcClient::on_empty_topic, {0, 0}}},
+ {"RPL_MOTDSTART", {&IrcClient::empty_motd, {0, 0}}},
+ {"375", {&IrcClient::empty_motd, {0, 0}}},
+ {"RPL_MOTD", {&IrcClient::on_motd_line, {2, 0}}},
+ {"372", {&IrcClient::on_motd_line, {2, 0}}},
+ {"RPL_MOTDEND", {&IrcClient::send_motd, {0, 0}}},
+ {"376", {&IrcClient::send_motd, {0, 0}}},
+ {"JOIN", {&IrcClient::on_channel_join, {1, 0}}},
+ {"PRIVMSG", {&IrcClient::on_channel_message, {2, 0}}},
+ {"353", {&IrcClient::set_and_forward_user_list, {4, 0}}},
+ {"332", {&IrcClient::on_topic_received, {2, 0}}},
+ {"TOPIC", {&IrcClient::on_topic_received, {2, 0}}},
+ {"333", {&IrcClient::on_topic_who_time_received, {4, 0}}},
+ {"RPL_TOPICWHOTIME", {&IrcClient::on_topic_who_time_received, {4, 0}}},
+ {"366", {&IrcClient::on_channel_completely_joined, {2, 0}}},
+ {"396", {&IrcClient::on_own_host_received, {2, 0}}},
+ {"432", {&IrcClient::on_erroneous_nickname, {2, 0}}},
+ {"433", {&IrcClient::on_nickname_conflict, {2, 0}}},
+ {"438", {&IrcClient::on_nickname_change_too_fast, {2, 0}}},
+ {"001", {&IrcClient::on_welcome_message, {1, 0}}},
+ {"PART", {&IrcClient::on_part, {1, 0}}},
+ {"ERROR", {&IrcClient::on_error, {1, 0}}},
+ {"QUIT", {&IrcClient::on_quit, {0, 0}}},
+ {"NICK", {&IrcClient::on_nick, {1, 0}}},
+ {"MODE", {&IrcClient::on_mode, {1, 0}}},
+ {"PING", {&IrcClient::send_pong_command, {1, 0}}},
+ {"PONG", {&IrcClient::on_pong, {0, 0}}},
+ {"KICK", {&IrcClient::on_kick, {3, 0}}},
+
+ {"401", {&IrcClient::on_generic_error, {2, 0}}},
+ {"402", {&IrcClient::on_generic_error, {2, 0}}},
+ {"403", {&IrcClient::on_generic_error, {2, 0}}},
+ {"404", {&IrcClient::on_generic_error, {2, 0}}},
+ {"405", {&IrcClient::on_generic_error, {2, 0}}},
+ {"406", {&IrcClient::on_generic_error, {2, 0}}},
+ {"407", {&IrcClient::on_generic_error, {2, 0}}},
+ {"408", {&IrcClient::on_generic_error, {2, 0}}},
+ {"409", {&IrcClient::on_generic_error, {2, 0}}},
+ {"410", {&IrcClient::on_generic_error, {2, 0}}},
+ {"411", {&IrcClient::on_generic_error, {2, 0}}},
+ {"412", {&IrcClient::on_generic_error, {2, 0}}},
+ {"414", {&IrcClient::on_generic_error, {2, 0}}},
+ {"421", {&IrcClient::on_generic_error, {2, 0}}},
+ {"422", {&IrcClient::on_generic_error, {2, 0}}},
+ {"423", {&IrcClient::on_generic_error, {2, 0}}},
+ {"424", {&IrcClient::on_generic_error, {2, 0}}},
+ {"431", {&IrcClient::on_generic_error, {2, 0}}},
+ {"436", {&IrcClient::on_generic_error, {2, 0}}},
+ {"441", {&IrcClient::on_generic_error, {2, 0}}},
+ {"442", {&IrcClient::on_generic_error, {2, 0}}},
+ {"443", {&IrcClient::on_generic_error, {2, 0}}},
+ {"444", {&IrcClient::on_generic_error, {2, 0}}},
+ {"446", {&IrcClient::on_generic_error, {2, 0}}},
+ {"451", {&IrcClient::on_generic_error, {2, 0}}},
+ {"461", {&IrcClient::on_generic_error, {2, 0}}},
+ {"462", {&IrcClient::on_generic_error, {2, 0}}},
+ {"463", {&IrcClient::on_generic_error, {2, 0}}},
+ {"464", {&IrcClient::on_generic_error, {2, 0}}},
+ {"465", {&IrcClient::on_generic_error, {2, 0}}},
+ {"467", {&IrcClient::on_generic_error, {2, 0}}},
+ {"470", {&IrcClient::on_generic_error, {2, 0}}},
+ {"471", {&IrcClient::on_generic_error, {2, 0}}},
+ {"472", {&IrcClient::on_generic_error, {2, 0}}},
+ {"473", {&IrcClient::on_generic_error, {2, 0}}},
+ {"474", {&IrcClient::on_generic_error, {2, 0}}},
+ {"475", {&IrcClient::on_generic_error, {2, 0}}},
+ {"476", {&IrcClient::on_generic_error, {2, 0}}},
+ {"477", {&IrcClient::on_generic_error, {2, 0}}},
+ {"481", {&IrcClient::on_generic_error, {2, 0}}},
+ {"482", {&IrcClient::on_generic_error, {2, 0}}},
+ {"483", {&IrcClient::on_generic_error, {2, 0}}},
+ {"484", {&IrcClient::on_generic_error, {2, 0}}},
+ {"485", {&IrcClient::on_generic_error, {2, 0}}},
+ {"487", {&IrcClient::on_generic_error, {2, 0}}},
+ {"491", {&IrcClient::on_generic_error, {2, 0}}},
+ {"501", {&IrcClient::on_generic_error, {2, 0}}},
+ {"502", {&IrcClient::on_generic_error, {2, 0}}},
+};
+
+IrcClient::IrcClient(std::shared_ptr<Poller> poller, const std::string& hostname,
+ const std::string& nickname, const std::string& username,
+ const std::string& realname, const std::string& user_hostname,
+ Bridge& bridge):
+ TCPSocketHandler(poller),
+ hostname(hostname),
+ user_hostname(user_hostname),
+ username(username),
+ realname(realname),
+ current_nick(nickname),
+ bridge(bridge),
+ welcomed(false),
+ chanmodes({"", "", "", ""}),
+ chantypes({'#', '&'})
+{
+ this->dummy_channel.topic = "This is a virtual channel provided for "
+ "convenience by biboumi, it is not connected "
+ "to any actual IRC channel of the server '" + this->hostname +
+ "', and sending messages in it has no effect. "
+ "Its main goal is to keep the connection to the IRC server "
+ "alive without having to join a real channel of that server. "
+ "To disconnect from the IRC server, leave this room and all "
+ "other IRC channels of that server.";
+#ifdef USE_DATABASE
+ auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
+ this->get_hostname());
+ std::vector<std::string> ports = utils::split(options.ports, ';', false);
+ for (auto it = ports.rbegin(); it != ports.rend(); ++it)
+ this->ports_to_try.emplace(*it, false);
+# ifdef BOTAN_FOUND
+ ports = utils::split(options.tlsPorts, ';', false);
+ for (auto it = ports.rbegin(); it != ports.rend(); ++it)
+ this->ports_to_try.emplace(*it, true);
+# endif // BOTAN_FOUND
+
+#else // not USE_DATABASE
+ this->ports_to_try.emplace("6667", false); // standard non-encrypted port
+# ifdef BOTAN_FOUND
+ this->ports_to_try.emplace("6670", true); // non-standard but I want it for some servers
+ this->ports_to_try.emplace("6697", true); // standard encrypted port
+# endif // BOTAN_FOUND
+#endif // USE_DATABASE
+}
+
+IrcClient::~IrcClient()
+{
+ // This event may or may not exist (if we never got connected, it
+ // doesn't), but it's ok
+ TimedEventsManager::instance().cancel("PING"s + this->hostname + this->bridge.get_jid());
+}
+
+void IrcClient::start()
+{
+ if (this->is_connecting() || this->is_connected())
+ return;
+ std::string port;
+ bool tls;
+ std::tie(port, tls) = this->ports_to_try.top();
+ this->ports_to_try.pop();
+ this->bridge.send_xmpp_message(this->hostname, "", "Connecting to "s +
+ this->hostname + ":" + port + " (" +
+ (tls ? "encrypted" : "not encrypted") + ")");
+
+ this->bind_addr = Config::get("outgoing_bind", "");
+
+#ifdef BOTAN_FOUND
+# ifdef USE_DATABASE
+ auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
+ this->get_hostname());
+ this->credential_manager.set_trusted_fingerprint(options.trustedFingerprint);
+# endif
+#endif
+ this->connect(this->hostname, port, tls);
+}
+
+void IrcClient::on_connection_failed(const std::string& reason)
+{
+ this->bridge.send_xmpp_message(this->hostname, "",
+ "Connection failed: "s + reason);
+
+ if (this->hostname_resolution_failed)
+ while (!this->ports_to_try.empty())
+ this->ports_to_try.pop();
+
+ if (this->ports_to_try.empty())
+ {
+ // Send an error message for all room that the user wanted to join
+ for (const auto& tuple: this->channels_to_join)
+ {
+ Iid iid(std::get<0>(tuple) + "%" + this->hostname);
+ this->bridge.send_presence_error(iid, this->current_nick,
+ "cancel", "item-not-found",
+ "", reason);
+ }
+ }
+ else // try the next port
+ this->start();
+}
+
+void IrcClient::on_connected()
+{
+ const auto webirc_password = Config::get("webirc_password", "");
+ static std::string resolved_ip;
+
+ if (!webirc_password.empty())
+ {
+ if (!resolved_ip.empty())
+ this->send_webirc_command(webirc_password, resolved_ip);
+ else
+ { // Start resolving the hostname of the user, and call
+ // on_connected again when it’s done
+ this->dns_resolver.resolve(this->user_hostname, "5222",
+ [this](const struct addrinfo* addr)
+ {
+ resolved_ip = addr_to_string(addr);
+ // Only continue the process if we
+ // didn’t get connected while we were
+ // resolving
+ if (this->is_connected())
+ this->on_connected();
+ },
+ [this](const char* error_msg)
+ {
+ if (this->is_connected())
+ {
+ this->on_connection_close("Could not resolve hostname "s + this->user_hostname +
+ ": " + error_msg);
+ this->send_quit_command("");
+ }
+ });
+ return;
+ }
+ }
+
+ this->send_message({"CAP", {"REQ", "multi-prefix"}});
+ this->send_message({"CAP", {"END"}});
+
+#ifdef USE_DATABASE
+ auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
+ this->get_hostname());
+ if (!options.pass.value().empty())
+ this->send_pass_command(options.pass.value());
+#endif
+
+ this->send_nick_command(this->current_nick);
+
+#ifdef USE_DATABASE
+ if (Config::get("realname_customization", "true") == "true")
+ {
+ if (!options.username.value().empty())
+ this->username = options.username.value();
+ if (!options.realname.value().empty())
+ this->realname = options.realname.value();
+ this->send_user_command(username, realname);
+ }
+ else
+ this->send_user_command(this->username, this->realname);
+#else
+ this->send_user_command(this->username, this->realname);
+#endif
+ this->send_gateway_message("Connected to IRC server"s + (this->use_tls ? " (encrypted)": "") + ".");
+ this->send_pending_data();
+}
+
+void IrcClient::on_connection_close(const std::string& error_msg)
+{
+ std::string message = "Connection closed";
+ if (!error_msg.empty())
+ message += ": " + error_msg;
+ else
+ message += ".";
+ const IrcMessage error{"ERROR", {message}};
+ this->on_error(error);
+ log_warning(message);
+}
+
+IrcChannel* IrcClient::get_channel(const std::string& n)
+{
+ if (n.empty())
+ return &this->dummy_channel;
+ const std::string name = utils::tolower(n);
+ try
+ {
+ return this->channels.at(name).get();
+ }
+ catch (const std::out_of_range& exception)
+ {
+ this->channels.emplace(name, std::make_unique<IrcChannel>());
+ return this->channels.at(name).get();
+ }
+}
+
+bool IrcClient::is_channel_joined(const std::string& name)
+{
+ IrcChannel* channel = this->get_channel(name);
+ return channel->joined;
+}
+
+std::string IrcClient::get_own_nick() const
+{
+ return this->current_nick;
+}
+
+void IrcClient::parse_in_buffer(const size_t)
+{
+ while (true)
+ {
+ auto pos = this->in_buf.find("\r\n");
+ if (pos == std::string::npos)
+ break ;
+ IrcMessage message(this->in_buf.substr(0, pos));
+ this->in_buf = this->in_buf.substr(pos + 2, std::string::npos);
+ log_debug("IRC RECEIVING: (", this->get_hostname(), ") ", message);
+
+ // Call the standard callback (if any), associated with the command
+ // name that we just received.
+ auto it = irc_callbacks.find(message.command);
+ if (it != irc_callbacks.end())
+ {
+ const auto& limits = it->second.second;
+ // Check that the Message is well formed before actually calling
+ // the callback. limits.first is the min number of arguments,
+ // second is the max
+ if (message.arguments.size() < limits.first ||
+ (limits.second > 0 && message.arguments.size() > limits.second))
+ log_warning("Invalid number of arguments for IRC command “", message.command,
+ "”: ", message.arguments.size());
+ else
+ {
+ const auto& cb = it->second.first;
+ try {
+ (this->*(cb))(message);
+ } catch (const std::exception& e) {
+ log_error("Unhandled exception: ", e.what());
+ }
+ }
+ }
+ else
+ {
+ log_info("No handler for command ", message.command,
+ ", forwarding the arguments to the user");
+ this->on_unknown_message(message);
+ }
+ // Try to find a waiting_iq, which response will be triggered by this IrcMessage
+ this->bridge.trigger_on_irc_message(this->hostname, message);
+ }
+}
+
+void IrcClient::send_message(IrcMessage&& message)
+{
+ log_debug("IRC SENDING: (", this->get_hostname(), ") ", message);
+ std::string res;
+ if (!message.prefix.empty())
+ res += ":" + std::move(message.prefix) + " ";
+ res += std::move(message.command);
+ for (const std::string& arg: message.arguments)
+ {
+ if (arg.find(" ") != std::string::npos ||
+ (!arg.empty() && arg[0] == ':'))
+ {
+ res += " :" + arg;
+ break;
+ }
+ res += " " + arg;
+ }
+ res += "\r\n";
+ this->send_data(std::move(res));
+}
+
+void IrcClient::send_raw(const std::string& txt)
+{
+ log_debug("IRC SENDING (raw): (", this->get_hostname(), ") ", txt);
+ this->send_data(txt + "\r\n");
+}
+
+void IrcClient::send_user_command(const std::string& username, const std::string& realname)
+{
+ this->send_message(IrcMessage("USER", {username, this->user_hostname, "ignored", realname}));
+}
+
+void IrcClient::send_nick_command(const std::string& nick)
+{
+ this->send_message(IrcMessage("NICK", {nick}));
+}
+
+void IrcClient::send_pass_command(const std::string& password)
+{
+ this->send_message(IrcMessage("PASS", {password}));
+}
+
+void IrcClient::send_webirc_command(const std::string& password, const std::string& user_ip)
+{
+ this->send_message(IrcMessage("WEBIRC", {password, "biboumi", this->user_hostname, user_ip}));
+}
+
+void IrcClient::send_kick_command(const std::string& chan_name, const std::string& target, const std::string& reason)
+{
+ this->send_message(IrcMessage("KICK", {chan_name, target, reason}));
+}
+
+void IrcClient::send_list_command()
+{
+ this->send_message(IrcMessage("LIST", {}));
+}
+
+void IrcClient::send_topic_command(const std::string& chan_name, const std::string& topic)
+{
+ this->send_message(IrcMessage("TOPIC", {chan_name, topic}));
+}
+
+void IrcClient::send_quit_command(const std::string& reason)
+{
+ this->send_message(IrcMessage("QUIT", {reason}));
+}
+
+void IrcClient::send_join_command(const std::string& chan_name, const std::string& password)
+{
+ if (this->welcomed == false)
+ this->channels_to_join.emplace_back(chan_name, password);
+ else if (password.empty())
+ this->send_message(IrcMessage("JOIN", {chan_name}));
+ else
+ this->send_message(IrcMessage("JOIN", {chan_name, password}));
+ this->start();
+}
+
+bool IrcClient::send_channel_message(const std::string& chan_name, const std::string& body)
+{
+ IrcChannel* channel = this->get_channel(chan_name);
+ if (channel->joined == false)
+ {
+ log_warning("Cannot send message to channel ", chan_name, ", it is not joined");
+ return false;
+ }
+ // The max size is 512, taking into account the whole message, not just
+ // the text we send.
+ // This includes our own nick, username and host (because this will be
+ // added by the server into our message), in addition to the basic
+ // components of the message we send (command name, chan name, \r\n et)
+ // : + NICK + ! + USER + @ + HOST + <space> + PRIVMSG + <space> + CHAN + <space> + : + \r\n
+ const auto line_size = 512 -
+ this->current_nick.size() - this->username.size() - this->own_host.size() -
+ ::strlen(":!@ PRIVMSG ") - chan_name.length() - ::strlen(" :\r\n");
+ const auto lines = cut(body, line_size);
+ for (const auto& line: lines)
+ this->send_message(IrcMessage("PRIVMSG", {chan_name, line}));
+ return true;
+}
+
+void IrcClient::send_private_message(const std::string& username, const std::string& body, const std::string& type)
+{
+ std::string::size_type pos = 0;
+ while (pos < body.size())
+ {
+ this->send_message(IrcMessage(std::string(type), {username, body.substr(pos, 400)}));
+ pos += 400;
+ }
+ // We always try to insert and we don't care if the username was already
+ // in the set.
+ this->nicks_to_treat_as_private.insert(username);
+}
+
+void IrcClient::send_part_command(const std::string& chan_name, const std::string& status_message)
+{
+ IrcChannel* channel = this->get_channel(chan_name);
+ if (channel->joined == true)
+ {
+ if (chan_name.empty())
+ this->leave_dummy_channel(status_message);
+ else
+ this->send_message(IrcMessage("PART", {chan_name, status_message}));
+ }
+}
+
+void IrcClient::send_mode_command(const std::string& chan_name, const std::vector<std::string>& arguments)
+{
+ std::vector<std::string> args(arguments);
+ args.insert(args.begin(), chan_name);
+ IrcMessage m("MODE", std::move(args));
+ this->send_message(std::move(m));
+}
+
+void IrcClient::send_pong_command(const IrcMessage& message)
+{
+ const std::string id = message.arguments[0];
+ this->send_message(IrcMessage("PONG", {id}));
+}
+
+void IrcClient::on_pong(const IrcMessage&)
+{
+}
+
+void IrcClient::send_ping_command()
+{
+ this->send_message(IrcMessage("PING", {"biboumi"}));
+}
+
+void IrcClient::forward_server_message(const IrcMessage& message)
+{
+ const std::string from = message.prefix;
+ const std::string body = message.arguments[1];
+
+ this->bridge.send_xmpp_message(this->hostname, from, body);
+}
+
+void IrcClient::on_notice(const IrcMessage& message)
+{
+ std::string from = message.prefix;
+ const std::string to = message.arguments[0];
+ const std::string body = message.arguments[1];
+
+ if (!body.empty() && body[0] == '\01' && body[body.size() - 1] == '\01')
+ // Do not forward the notice to the user if it's a CTCP command
+ return ;
+
+ if (!to.empty() && this->chantypes.find(to[0]) == this->chantypes.end())
+ {
+ // The notice is for us precisely.
+
+ // Find out if we already sent a private message to this user. If yes
+ // we treat that message as a private message coming from
+ // it. Otherwise we treat it as a notice coming from the server.
+ IrcUser user(from);
+ std::string nick = utils::tolower(user.nick);
+ if (this->nicks_to_treat_as_private.find(nick) !=
+ this->nicks_to_treat_as_private.end())
+ { // We previously sent a message to that nick)
+ this->bridge.send_message({nick + "!" + this->hostname}, nick, body,
+ false);
+ }
+ else
+ this->bridge.send_xmpp_message(this->hostname, from, body);
+ }
+ else
+ {
+ // The notice was directed at a channel we are in. Modify the message
+ // to indicate that it is a notice, and make it a MUC message coming
+ // from the MUC JID
+ IrcMessage modified_message(std::move(from), "PRIVMSG", {to, "\u000303[notice]\u0003 "s + body});
+ this->on_channel_message(modified_message);
+ }
+}
+
+void IrcClient::on_isupport_message(const IrcMessage& message)
+{
+ const size_t len = message.arguments.size();
+ for (size_t i = 1; i < len; ++i)
+ {
+ const std::string token = message.arguments[i];
+ if (token.substr(0, 10) == "CHANMODES=")
+ {
+ this->chanmodes = utils::split(token.substr(11), ',');
+ // make sure we have 4 strings
+ this->chanmodes.resize(4);
+ }
+ else if (token.substr(0, 7) == "PREFIX=")
+ {
+ size_t i = 8; // jump PREFIX=(
+ size_t j = 9;
+ // Find the ) char
+ while (j < token.size() && token[j] != ')')
+ j++;
+ j++;
+ while (j < token.size() && token[i] != ')')
+ {
+ this->sorted_user_modes.push_back(token[i]);
+ this->prefix_to_mode[token[j++]] = token[i++];
+ }
+ }
+ else if (token.substr(0, 10) == "CHANTYPES=")
+ {
+ // Remove the default types, they apply only if no other value is
+ // specified.
+ this->chantypes.clear();
+ size_t i = 10;
+ while (i < token.size())
+ this->chantypes.insert(token[i++]);
+ }
+ }
+}
+
+void IrcClient::on_server_myinfo(const IrcMessage&)
+{
+}
+
+void IrcClient::send_gateway_message(const std::string& message, const std::string& from)
+{
+ this->bridge.send_xmpp_message(this->hostname, from, message);
+}
+
+void IrcClient::set_and_forward_user_list(const IrcMessage& message)
+{
+ const std::string chan_name = utils::tolower(message.arguments[2]);
+ IrcChannel* channel = this->get_channel(chan_name);
+ std::vector<std::string> nicks = utils::split(message.arguments[3], ' ');
+ for (const std::string& nick: nicks)
+ {
+ const IrcUser* user = channel->add_user(nick, this->prefix_to_mode);
+ if (user->nick != channel->get_self()->nick)
+ {
+ this->bridge.send_user_join(this->hostname, chan_name, user, user->get_most_significant_mode(this->sorted_user_modes), false);
+ }
+ else
+ {
+ // we now know the modes of self, so copy the modes into self
+ channel->get_self()->modes = user->modes;
+ }
+ }
+}
+
+void IrcClient::on_channel_join(const IrcMessage& message)
+{
+ const std::string chan_name = utils::tolower(message.arguments[0]);
+ IrcChannel* channel;
+ if (chan_name.empty())
+ channel = &this->dummy_channel;
+ else
+ channel = this->get_channel(chan_name);
+ const std::string nick = message.prefix;
+ if (channel->joined == false)
+ channel->set_self(nick);
+ else
+ {
+ const IrcUser* user = channel->add_user(nick, this->prefix_to_mode);
+ this->bridge.send_user_join(this->hostname, chan_name, user, user->get_most_significant_mode(this->sorted_user_modes), false);
+ }
+}
+
+void IrcClient::on_channel_message(const IrcMessage& message)
+{
+ const IrcUser user(message.prefix);
+ const std::string nick = user.nick;
+ Iid iid;
+ iid.set_local(message.arguments[0]);
+ iid.set_server(this->hostname);
+ const std::string body = message.arguments[1];
+ bool muc = true;
+ if (!this->get_channel(iid.get_local())->joined)
+ {
+ iid.is_user = true;
+ iid.set_local(nick);
+ muc = false;
+ }
+ else
+ iid.is_channel = true;
+ if (!body.empty() && body[0] == '\01')
+ {
+ if (body.substr(1, 6) == "ACTION")
+ this->bridge.send_message(iid, nick,
+ "/me"s + body.substr(7, body.size() - 8), muc);
+ else if (body.substr(1, 8) == "VERSION\01")
+ this->bridge.send_iq_version_request(nick, this->hostname);
+ else if (body.substr(1, 5) == "PING ")
+ this->bridge.send_xmpp_ping_request(utils::tolower(nick), this->hostname,
+ body.substr(6, body.size() - 7));
+ }
+ else
+ this->bridge.send_message(iid, nick, body, muc);
+}
+
+void IrcClient::on_rpl_liststart(const IrcMessage&)
+{
+}
+
+void IrcClient::on_rpl_list(const IrcMessage&)
+{
+}
+
+void IrcClient::on_rpl_listend(const IrcMessage&)
+{
+}
+
+void IrcClient::empty_motd(const IrcMessage&)
+{
+ this->motd.erase();
+}
+
+void IrcClient::on_empty_topic(const IrcMessage& message)
+{
+ const std::string chan_name = utils::tolower(message.arguments[1]);
+ log_debug("empty topic for ", chan_name);
+ IrcChannel* channel = this->get_channel(chan_name);
+ if (channel)
+ channel->topic.clear();
+}
+
+void IrcClient::on_motd_line(const IrcMessage& message)
+{
+ const std::string body = message.arguments[1];
+ // We could send the MOTD without a line break between each IRC-message,
+ // but sometimes it contains some ASCII art, we use line breaks to keep
+ // them intact.
+ this->motd += body+"\n";
+}
+
+void IrcClient::send_motd(const IrcMessage&)
+{
+ this->bridge.send_xmpp_message(this->hostname, "", this->motd);
+}
+
+void IrcClient::on_topic_received(const IrcMessage& message)
+{
+ const std::string chan_name = utils::tolower(message.arguments[message.arguments.size() - 2]);
+ IrcUser author(message.prefix);
+ IrcChannel* channel = this->get_channel(chan_name);
+ channel->topic = message.arguments[message.arguments.size() - 1];
+ channel->topic_author = author.nick;
+ if (channel->joined)
+ this->bridge.send_topic(this->hostname, chan_name, channel->topic, channel->topic_author);
+}
+
+void IrcClient::on_topic_who_time_received(const IrcMessage& message)
+{
+ IrcUser author(message.arguments[2]);
+ const std::string chan_name = utils::tolower(message.arguments[1]);
+ IrcChannel* channel = this->get_channel(chan_name);
+ channel->topic_author = author.nick;
+}
+
+void IrcClient::on_channel_completely_joined(const IrcMessage& message)
+{
+ const std::string chan_name = utils::tolower(message.arguments[1]);
+ IrcChannel* channel = this->get_channel(chan_name);
+ channel->joined = true;
+ this->bridge.send_user_join(this->hostname, chan_name, channel->get_self(), channel->get_self()->get_most_significant_mode(this->sorted_user_modes), true);
+ this->bridge.send_topic(this->hostname, chan_name, channel->topic, channel->topic_author);
+}
+
+void IrcClient::on_own_host_received(const IrcMessage& message)
+{
+ this->own_host = message.arguments[1];
+ const std::string from = message.prefix;
+ if (message.arguments.size() >= 3)
+ this->bridge.send_xmpp_message(this->hostname, from,
+ this->own_host + " " + message.arguments[2]);
+ else
+ this->bridge.send_xmpp_message(this->hostname, from, this->own_host +
+ " is now your displayed host");
+}
+
+void IrcClient::on_erroneous_nickname(const IrcMessage& message)
+{
+ const std::string error_msg = message.arguments.size() >= 3 ?
+ message.arguments[2]: "Erroneous nickname";
+ this->send_gateway_message(error_msg + ": " + message.arguments[1], message.prefix);
+}
+
+void IrcClient::on_nickname_conflict(const IrcMessage& message)
+{
+ const std::string nickname = message.arguments[1];
+ this->on_generic_error(message);
+ for (auto it = this->channels.begin(); it != this->channels.end(); ++it)
+ {
+ Iid iid;
+ iid.set_local(it->first);
+ iid.set_server(this->hostname);
+ iid.is_channel = true;
+ this->bridge.send_nickname_conflict_error(iid, nickname);
+ }
+}
+
+void IrcClient::on_nickname_change_too_fast(const IrcMessage& message)
+{
+ const std::string nickname = message.arguments[1];
+ std::string txt;
+ if (message.arguments.size() >= 3)
+ txt = message.arguments[2];
+ this->on_generic_error(message);
+ for (auto it = this->channels.begin(); it != this->channels.end(); ++it)
+ {
+ Iid iid;
+ iid.set_local(it->first);
+ iid.set_server(this->hostname);
+ iid.is_channel = true;
+ this->bridge.send_presence_error(iid, nickname,
+ "cancel", "not-acceptable",
+ "", txt);
+ }
+}
+
+void IrcClient::on_generic_error(const IrcMessage& message)
+{
+ const std::string error_msg = message.arguments.size() >= 3 ?
+ message.arguments[2]: "Unspecified error";
+ this->send_gateway_message(message.arguments[1] + ": " + error_msg, message.prefix);
+}
+
+void IrcClient::on_welcome_message(const IrcMessage& message)
+{
+ this->current_nick = message.arguments[0];
+ this->welcomed = true;
+#ifdef USE_DATABASE
+ auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
+ this->get_hostname());
+ if (!options.afterConnectionCommand.value().empty())
+ this->send_raw(options.afterConnectionCommand.value());
+#endif
+ // Install a repeated events to regularly send a PING
+ TimedEventsManager::instance().add_event(TimedEvent(240s, std::bind(&IrcClient::send_ping_command, this),
+ "PING"s + this->hostname + this->bridge.get_jid()));
+ for (const auto& tuple: this->channels_to_join)
+ this->send_join_command(std::get<0>(tuple), std::get<1>(tuple));
+ this->channels_to_join.clear();
+ // Indicate that the dummy channel is joined as well, if needed
+ if (this->dummy_channel.joining)
+ {
+ // Simulate a message coming from the IRC server saying that we joined
+ // the channel
+ const IrcMessage join_message(this->get_nick(), "JOIN", {""});
+ this->on_channel_join(join_message);
+ const IrcMessage end_join_message(std::string(this->hostname), "366",
+ {this->get_nick(),
+ "", "End of NAMES list"});
+ this->on_channel_completely_joined(end_join_message);
+ }
+}
+
+void IrcClient::on_part(const IrcMessage& message)
+{
+ const std::string chan_name = message.arguments[0];
+ IrcChannel* channel = this->get_channel(chan_name);
+ if (!channel->joined)
+ return ;
+ std::string txt;
+ if (message.arguments.size() >= 2)
+ txt = message.arguments[1];
+ const IrcUser* user = channel->find_user(message.prefix);
+ if (user)
+ {
+ std::string nick = user->nick;
+ channel->remove_user(user);
+ Iid iid;
+ iid.set_local(chan_name);
+ iid.set_server(this->hostname);
+ iid.is_channel = true;
+ bool self = channel->get_self()->nick == nick;
+ if (self)
+ {
+ channel->joined = false;
+ this->channels.erase(utils::tolower(chan_name));
+ // channel pointer is now invalid
+ channel = nullptr;
+ }
+ this->bridge.send_muc_leave(std::move(iid), std::move(nick), std::move(txt), self);
+ }
+}
+
+void IrcClient::on_error(const IrcMessage& message)
+{
+ const std::string leave_message = message.arguments[0];
+ // The user is out of all the channels
+ for (auto it = this->channels.begin(); it != this->channels.end(); ++it)
+ {
+ Iid iid;
+ iid.set_local(it->first);
+ iid.set_server(this->hostname);
+ iid.is_channel = true;
+ IrcChannel* channel = it->second.get();
+ if (!channel->joined)
+ continue;
+ std::string own_nick = channel->get_self()->nick;
+ this->bridge.send_muc_leave(std::move(iid), std::move(own_nick), leave_message, true);
+ }
+ this->channels.clear();
+ this->send_gateway_message("ERROR: "s + leave_message);
+}
+
+void IrcClient::on_quit(const IrcMessage& message)
+{
+ std::string txt;
+ if (message.arguments.size() >= 1)
+ txt = message.arguments[0];
+ for (auto it = this->channels.begin(); it != this->channels.end(); ++it)
+ {
+ const std::string chan_name = it->first;
+ IrcChannel* channel = it->second.get();
+ const IrcUser* user = channel->find_user(message.prefix);
+ if (user)
+ {
+ std::string nick = user->nick;
+ channel->remove_user(user);
+ Iid iid;
+ iid.set_local(chan_name);
+ iid.set_server(this->hostname);
+ iid.is_channel = true;
+ this->bridge.send_muc_leave(std::move(iid), std::move(nick), txt, false);
+ }
+ }
+}
+
+void IrcClient::on_nick(const IrcMessage& message)
+{
+ const std::string new_nick = message.arguments[0];
+ for (auto it = this->channels.begin(); it != this->channels.end(); ++it)
+ {
+ const std::string chan_name = it->first;
+ IrcChannel* channel = it->second.get();
+ IrcUser* user = channel->find_user(message.prefix);
+ if (user)
+ {
+ std::string old_nick = user->nick;
+ Iid iid;
+ iid.set_local(chan_name);
+ iid.set_server(this->hostname);
+ iid.is_channel = true;
+ const bool self = channel->get_self()->nick == old_nick;
+ const char user_mode = user->get_most_significant_mode(this->sorted_user_modes);
+ this->bridge.send_nick_change(std::move(iid), old_nick, new_nick, user_mode, self);
+ user->nick = new_nick;
+ if (self)
+ {
+ channel->get_self()->nick = new_nick;
+ this->current_nick = new_nick;
+ }
+ }
+ }
+}
+
+void IrcClient::on_kick(const IrcMessage& message)
+{
+ const std::string chan_name = utils::tolower(message.arguments[0]);
+ const std::string target = message.arguments[1];
+ const std::string reason = message.arguments[2];
+ IrcChannel* channel = this->get_channel(chan_name);
+ if (!channel->joined)
+ return ;
+ if (channel->get_self()->nick == target)
+ channel->joined = false;
+ IrcUser author(message.prefix);
+ Iid iid;
+ iid.set_local(chan_name);
+ iid.set_server(this->hostname);
+ iid.is_channel = true;
+ this->bridge.kick_muc_user(std::move(iid), target, reason, author.nick);
+}
+
+void IrcClient::on_mode(const IrcMessage& message)
+{
+ const std::string target = message.arguments[0];
+ if (this->chantypes.find(target[0]) != this->chantypes.end())
+ this->on_channel_mode(message);
+ else
+ this->on_user_mode(message);
+}
+
+void IrcClient::on_channel_mode(const IrcMessage& message)
+{
+ // For now, just transmit the modes so the user can know what happens
+ // TODO, actually interprete the mode.
+ Iid iid;
+ iid.set_local(message.arguments[0]);
+ iid.set_server(this->hostname);
+ iid.is_channel = true;
+ IrcUser user(message.prefix);
+ std::string mode_arguments;
+ for (size_t i = 1; i < message.arguments.size(); ++i)
+ {
+ if (!message.arguments[i].empty())
+ {
+ if (i != 1)
+ mode_arguments += " ";
+ mode_arguments += message.arguments[i];
+ }
+ }
+ this->bridge.send_message(iid, "", "Mode "s + iid.get_local() +
+ " [" + mode_arguments + "] by " + user.nick,
+ true);
+ const IrcChannel* channel = this->get_channel(iid.get_local());
+ if (!channel)
+ return;
+
+ // parse the received modes, we need to handle things like "+m-oo coucou toutou"
+ const std::string modes = message.arguments[1];
+ // a list of modified IrcUsers. When we applied all modes, we check the
+ // modes that now applies to each of them, and send a notification for
+ // each one. This is to disallow sending two notifications or more when a
+ // single MODE command changes two or more modes on the same participant
+ std::set<const IrcUser*> modified_users;
+ // If it is true, the modes are added, if it’s false they are
+ // removed. When we encounter the '+' char, the value is changed to true,
+ // and with '-' it is changed to false.
+ bool add = true;
+ bool use_arg;
+ size_t arg_pos = 2;
+ for (const char c: modes)
+ {
+ if (c == '+')
+ add = true;
+ else if (c == '-')
+ add = false;
+ else
+ { // lookup the mode symbol in the 4 chanmodes lists, depending on
+ // the list where it is found, it takes an argument or not
+ size_t type;
+ for (type = 0; type < 4; ++type)
+ if (this->chanmodes[type].find(c) != std::string::npos)
+ break;
+ if (type == 4) // if mode was not found
+ {
+ // That mode can also be of type B if it is present in the
+ // prefix_to_mode map
+ for (const std::pair<char, char>& pair: this->prefix_to_mode)
+ if (pair.second == c)
+ {
+ type = 1;
+ break;
+ }
+ }
+ // modes of type A, B or C (but only with add == true)
+ if (type == 0 || type == 1 ||
+ (type == 2 && add == true))
+ use_arg = true;
+ else // modes of type C (but only with add == false), D, or unknown
+ use_arg = false;
+ if (use_arg == true && message.arguments.size() > arg_pos)
+ {
+ const std::string target = message.arguments[arg_pos++];
+ IrcUser* user = channel->find_user(target);
+ if (!user)
+ {
+ log_warning("Trying to set mode for non-existing user '", target
+ , "' in channel", iid.get_local());
+ return;
+ }
+ if (add)
+ user->add_mode(c);
+ else
+ user->remove_mode(c);
+ modified_users.insert(user);
+ }
+ }
+ }
+ for (const IrcUser* u: modified_users)
+ {
+ char most_significant_mode = u->get_most_significant_mode(this->sorted_user_modes);
+ this->bridge.send_affiliation_role_change(iid, u->nick, most_significant_mode);
+ }
+}
+
+void IrcClient::on_user_mode(const IrcMessage& message)
+{
+ this->bridge.send_xmpp_message(this->hostname, "",
+ "User mode for "s + message.arguments[0] +
+ " is [" + message.arguments[1] + "]");
+}
+
+void IrcClient::on_unknown_message(const IrcMessage& message)
+{
+ if (message.arguments.size() < 2)
+ return ;
+ std::string from = message.prefix;
+ std::stringstream ss;
+ for (auto it = message.arguments.begin() + 1; it != message.arguments.end(); ++it)
+ {
+ ss << *it;
+ if (it + 1 != message.arguments.end())
+ ss << " ";
+ }
+ this->bridge.send_xmpp_message(this->hostname, from, ss.str());
+}
+
+size_t IrcClient::number_of_joined_channels() const
+{
+ if (this->dummy_channel.joined)
+ return this->channels.size() + 1;
+ else
+ return this->channels.size();
+}
+
+DummyIrcChannel& IrcClient::get_dummy_channel()
+{
+ return this->dummy_channel;
+}
+
+void IrcClient::leave_dummy_channel(const std::string& exit_message)
+{
+ if (!this->dummy_channel.joined)
+ return;
+ this->dummy_channel.joined = false;
+ this->dummy_channel.joining = false;
+ this->dummy_channel.remove_all_users();
+ this->bridge.send_muc_leave(Iid("%"s + this->hostname), std::string(this->current_nick), exit_message, true);
+}
+
+#ifdef BOTAN_FOUND
+bool IrcClient::abort_on_invalid_cert() const
+{
+#ifdef USE_DATABASE
+ auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(), this->hostname);
+ return options.verifyCert.value();
+#endif
+ return true;
+}
+#endif
diff --git a/src/irc/irc_client.hpp b/src/irc/irc_client.hpp
new file mode 100644
index 0000000..fc3918e
--- /dev/null
+++ b/src/irc/irc_client.hpp
@@ -0,0 +1,383 @@
+#pragma once
+
+
+#include <irc/irc_message.hpp>
+#include <irc/irc_channel.hpp>
+#include <irc/iid.hpp>
+
+#include <network/tcp_socket_handler.hpp>
+#include <network/resolver.hpp>
+
+#include <unordered_map>
+#include <utility>
+#include <memory>
+#include <vector>
+#include <string>
+#include <stack>
+#include <map>
+#include <set>
+
+class Bridge;
+
+/**
+ * Represent one IRC client, i.e. an endpoint connected to a single IRC
+ * server, through a TCP socket, receiving and sending commands to it.
+ */
+class IrcClient: public TCPSocketHandler
+{
+public:
+ explicit IrcClient(std::shared_ptr<Poller> poller, const std::string& hostname,
+ const std::string& nickname, const std::string& username,
+ const std::string& realname, const std::string& user_hostname,
+ Bridge& bridge);
+ ~IrcClient();
+
+ IrcClient(const IrcClient&) = delete;
+ IrcClient(IrcClient&&) = delete;
+ IrcClient& operator=(const IrcClient&) = delete;
+ IrcClient& operator=(IrcClient&&) = delete;
+
+ /**
+ * Connect to the IRC server
+ */
+ void start();
+ /**
+ * Called when the connection to the server cannot be established
+ */
+ void on_connection_failed(const std::string& reason) override final;
+ /**
+ * Called when successfully connected to the server
+ */
+ void on_connected() override final;
+ /**
+ * Close the connection, remove us from the poller
+ */
+ void on_connection_close(const std::string& error) override final;
+ /**
+ * Parse the data we have received so far and try to get one or more
+ * complete messages from it.
+ */
+ void parse_in_buffer(const size_t) override final;
+#ifdef BOTAN_FOUND
+ virtual bool abort_on_invalid_cert() const override final;
+#endif
+ /**
+ * Return the channel with this name, create it if it does not yet exist
+ */
+ IrcChannel* get_channel(const std::string& name);
+ /**
+ * Returns true if the channel is joined
+ */
+ bool is_channel_joined(const std::string& name);
+ /**
+ * Return our own nick
+ */
+ std::string get_own_nick() const;
+ /**
+ * Serialize the given message into a line, and send that into the socket
+ * (actually, into our out_buf and signal the poller that we want to wach
+ * for send events to be ready)
+ */
+ void send_message(IrcMessage&& message);
+ void send_raw(const std::string& txt);
+ /**
+ * Send the PONG irc command
+ */
+ void send_pong_command(const IrcMessage& message);
+ /**
+ * Do nothing when we receive a PONG command (but also do not log that no
+ * handler exist)
+ */
+ void on_pong(const IrcMessage& message);
+ void send_ping_command();
+ /**
+ * Send the USER irc command
+ */
+ void send_user_command(const std::string& username, const std::string& realname);
+ /**
+ * Send the NICK irc command
+ */
+ void send_nick_command(const std::string& username);
+ void send_pass_command(const std::string& password);
+ void send_webirc_command(const std::string& password, const std::string& user_ip);
+ /**
+ * Send the JOIN irc command.
+ */
+ void send_join_command(const std::string& chan_name, const std::string& password);
+ /**
+ * Send a PRIVMSG command for a channel
+ * Return true if the message was actually sent
+ */
+ bool send_channel_message(const std::string& chan_name, const std::string& body);
+ /**
+ * Send a PRIVMSG command for an user
+ */
+ void send_private_message(const std::string& username, const std::string& body, const std::string& type);
+ /**
+ * Send the PART irc command
+ */
+ void send_part_command(const std::string& chan_name, const std::string& status_message);
+ /**
+ * Send the MODE irc command
+ */
+ void send_mode_command(const std::string& chan_name, const std::vector<std::string>& arguments);
+ /**
+ * Send the KICK irc command
+ */
+ void send_kick_command(const std::string& chan_name, const std::string& target, const std::string& reason);
+ /**
+ * Send the LIST irc command
+ */
+ void send_list_command();
+ void send_topic_command(const std::string& chan_name, const std::string& topic);
+ /**
+ * Send the QUIT irc command
+ */
+ void send_quit_command(const std::string& reason);
+ /**
+ * 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
+ * might want to notify the user about the connexion state)
+ */
+ void send_gateway_message(const std::string& message, const std::string& from="");
+ /**
+ * Forward the server message received from IRC to the XMPP component
+ */
+ void forward_server_message(const IrcMessage& message);
+ /**
+ * When receiving the isupport informations. See
+ * http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt
+ */
+ void on_isupport_message(const IrcMessage& message);
+ /**
+ * Does nothing yet. Isn’t that duplicating features from 005?
+ */
+ void on_server_myinfo(const IrcMessage& message);
+ /**
+ * Just empty the motd we kept as a string
+ */
+ void empty_motd(const IrcMessage& message);
+ /**
+ * Send the MOTD string as one single "big" message
+ */
+ void send_motd(const IrcMessage& message);
+ /**
+ * Append this line to the MOTD
+ */
+ void on_motd_line(const IrcMessage& message);
+ /**
+ * Forward the join of an other user into an IRC channel, and save the
+ * IrcUsers in the IrcChannel
+ */
+ void set_and_forward_user_list(const IrcMessage& message);
+ /**
+ * Signal the start of the LIST response. The RFC says its obsolete and
+ * “not used”, but I we receive it on some servers, so just ignore it.
+ */
+ void on_rpl_liststart(const IrcMessage& message);
+ /**
+ * A single LIST response line (one channel)
+ *
+ * The command is handled in a wait_irc callback. This general handler is
+ * empty and just used to avoid sending a message stanza for each received
+ * channel.
+ */
+ void on_rpl_list(const IrcMessage& message);
+ /**
+ * Signal the end of the LIST response, ignore.
+ */
+ void on_rpl_listend(const IrcMessage& message);
+ /**
+ * Remember our nick and host, when we are joined to the channel. The list
+ * of user comes after so we do not send the self-presence over XMPP yet.
+ */
+ void on_channel_join(const IrcMessage& message);
+ /**
+ * When a channel message is received
+ */
+ void on_channel_message(const IrcMessage& message);
+ /**
+ * A notice is received
+ */
+ void on_notice(const IrcMessage& message);
+ /**
+ * Save the topic in the IrcChannel
+ */
+ void on_topic_received(const IrcMessage& message);
+ /**
+ * Save the topic author in the IrcChannel
+ */
+ void on_topic_who_time_received(const IrcMessage& message);
+ /**
+ * Empty the topic
+ */
+ void on_empty_topic(const IrcMessage& message);
+ /**
+ * The channel has been completely joined (self presence, topic, all names
+ * received etc), send the self presence and topic to the XMPP user.
+ */
+ void on_channel_completely_joined(const IrcMessage& message);
+ /**
+ * Save our own host, as reported by the server
+ */
+ void on_own_host_received(const IrcMessage& message);
+ /**
+ * We tried to set an invalid nickname
+ */
+ void on_erroneous_nickname(const IrcMessage& message);
+ /**
+ * When the IRC servers denies our nickname because of a conflict. Send a
+ * presence conflict from all channels, because the name is server-wide.
+ */
+ void on_nickname_conflict(const IrcMessage& message);
+ /**
+ * Idem, but for when the user changes their nickname too quickly
+ */
+ void on_nickname_change_too_fast(const IrcMessage& message);
+ /**
+ * Handles most errors from the server by just forwarding the message to the user.
+ */
+ void on_generic_error(const IrcMessage& message);
+ /**
+ * When a message 001 is received, join the rooms we wanted to join, and set our actual nickname
+ */
+ void on_welcome_message(const IrcMessage& message);
+ void on_part(const IrcMessage& message);
+ void on_error(const IrcMessage& message);
+ void on_nick(const IrcMessage& message);
+ void on_kick(const IrcMessage& message);
+ void on_mode(const IrcMessage& message);
+ /**
+ * A mode towards our own user is received (note, that is different from a
+ * channel mode towards or own nick, see
+ * http://tools.ietf.org/html/rfc2812#section-3.1.5 VS #section-3.2.3)
+ */
+ void on_user_mode(const IrcMessage& message);
+ /**
+ * A mode towards a channel. Note that this can change the mode of the
+ * channel itself or an IrcUser in it.
+ */
+ void on_channel_mode(const IrcMessage& message);
+ void on_quit(const IrcMessage& message);
+ void on_unknown_message(const IrcMessage& message);
+ /**
+ * Return the number of joined channels
+ */
+ size_t number_of_joined_channels() const;
+ /**
+ * Get a reference to the unique dummy channel
+ */
+ DummyIrcChannel& get_dummy_channel();
+ /**
+ * Leave the dummy channel: forward a message to the user to indicate that
+ * he left it, and mark it as not joined.
+ */
+ void leave_dummy_channel(const std::string& exit_message);
+
+ const std::string& get_hostname() const { return this->hostname; }
+ std::string get_nick() const { return this->current_nick; }
+ bool is_welcomed() const { return this->welcomed; }
+
+ const Resolver& get_resolver() const { return this->dns_resolver; }
+
+ const std::vector<char>& get_sorted_user_modes() const { return sorted_user_modes; }
+
+private:
+ /**
+ * The hostname of the server we are connected to.
+ */
+ const std::string hostname;
+ /**
+ * Our own host, as reported by the IRC server.
+ * By default (and if it is not overridden by the server), it is a
+ * meaningless string, with the maximum allowed size
+ */
+ std::string own_host{63, '*'};
+ /**
+ * The hostname of the user. This is used in the USER and the WEBIRC
+ * commands, but only the one in WEBIRC will be used by the IRC server.
+ */
+ const std::string user_hostname;
+ /**
+ * The username used in the USER irc command
+ */
+ std::string username;
+ /**
+ * The realname used in the USER irc command
+ */
+ std::string realname;
+ /**
+ * Our current nickname on the server
+ */
+ std::string current_nick;
+ /**
+ * To communicate back with the bridge
+ */
+ Bridge& bridge;
+ /**
+ * The list of joined channels, indexed by name
+ */
+ std::unordered_map<std::string, std::unique_ptr<IrcChannel>> channels;
+ /**
+ * A single channel with a iid of the form "hostname" (normal channel have
+ * an iid of the form "chan%hostname".
+ */
+ DummyIrcChannel dummy_channel;
+ /**
+ * A list of chan we want to join (tuples with the channel name and the
+ * password, if any), but we need a response 001 from the server before
+ * sending the actual JOIN commands. So we just keep the channel names in
+ * a list, and send the JOIN commands for each of them whenever the
+ * WELCOME message is received.
+ */
+ std::vector<std::tuple<std::string, std::string>> channels_to_join;
+ /**
+ * This flag indicates that the server is completely joined (connection
+ * has been established, we are authentified and we have a nick)
+ */
+ bool welcomed;
+ /**
+ * See http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt section 3.3
+ * We store the possible chanmodes in this object.
+ * chanmodes[0] contains modes of type A, [1] of type B etc
+ */
+ std::vector<std::string> chanmodes;
+ /**
+ * See http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt
+ * section 3.5
+ */
+ std::set<char> chantypes;
+ /**
+ * Each motd line received is appended to this string, which we send when
+ * the motd is completely received
+ */
+ std::string motd;
+ /**
+ * See http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt section 3.14
+ * The example given would be transformed into
+ * modes_to_prefix = {{'&', 'a'}, {'*', 'b'}}
+ */
+ std::map<char, char> prefix_to_mode;
+ /**
+ * Available user modes, sorted from most significant to least significant
+ * (for example 'ahov' is a common order).
+ */
+ std::vector<char> sorted_user_modes;
+ /**
+ * A list of ports to which we will try to connect, in reverse. Each port
+ * is associated with a boolean telling if we should use TLS or not if the
+ * connection succeeds on that port.
+ */
+ std::stack<std::pair<std::string, bool>> ports_to_try;
+ /**
+ * A set of (lowercase) nicknames to which we sent a private message.
+ */
+ std::set<std::string> nicks_to_treat_as_private;
+ /**
+ * DNS resolver, used to resolve the hostname of the user if we are using
+ * the WebIRC protocole.
+ */
+ Resolver dns_resolver;
+};
+
+
diff --git a/src/irc/irc_message.cpp b/src/irc/irc_message.cpp
new file mode 100644
index 0000000..966a47c
--- /dev/null
+++ b/src/irc/irc_message.cpp
@@ -0,0 +1,61 @@
+#include <irc/irc_message.hpp>
+#include <iostream>
+
+IrcMessage::IrcMessage(std::string&& line)
+{
+ std::string::size_type pos;
+
+ // optional prefix
+ if (line[0] == ':')
+ {
+ pos = line.find(" ");
+ this->prefix = line.substr(1, pos - 1);
+ line = line.substr(pos + 1, std::string::npos);
+ }
+ // command
+ pos = line.find(" ");
+ this->command = line.substr(0, pos);
+ line = line.substr(pos + 1, std::string::npos);
+ // arguments
+ do
+ {
+ if (line[0] == ':')
+ {
+ this->arguments.emplace_back(line.substr(1, std::string::npos));
+ break ;
+ }
+ pos = line.find(" ");
+ this->arguments.emplace_back(line.substr(0, pos));
+ line = line.substr(pos + 1, std::string::npos);
+ } while (pos != std::string::npos);
+}
+
+IrcMessage::IrcMessage(std::string&& prefix,
+ std::string&& command,
+ std::vector<std::string>&& args):
+ prefix(std::move(prefix)),
+ command(std::move(command)),
+ arguments(std::move(args))
+{
+}
+
+IrcMessage::IrcMessage(std::string&& command,
+ std::vector<std::string>&& args):
+ prefix(),
+ command(std::move(command)),
+ arguments(std::move(args))
+{
+}
+
+std::ostream& operator<<(std::ostream& os, const IrcMessage& message)
+{
+ os << "IrcMessage";
+ os << "[" << message.command << "]";
+ for (const std::string& arg: message.arguments)
+ {
+ os << "{" << arg << "}";
+ }
+ if (!message.prefix.empty())
+ os << "(from: " << message.prefix << ")";
+ return os;
+}
diff --git a/src/irc/irc_message.hpp b/src/irc/irc_message.hpp
new file mode 100644
index 0000000..fe954e4
--- /dev/null
+++ b/src/irc/irc_message.hpp
@@ -0,0 +1,28 @@
+#pragma once
+
+
+#include <vector>
+#include <string>
+#include <ostream>
+
+class IrcMessage
+{
+public:
+ IrcMessage(std::string&& line);
+ IrcMessage(std::string&& prefix, std::string&& command, std::vector<std::string>&& args);
+ IrcMessage(std::string&& command, std::vector<std::string>&& args);
+ ~IrcMessage() = default;
+
+ IrcMessage(const IrcMessage&) = delete;
+ IrcMessage(IrcMessage&&) = delete;
+ IrcMessage& operator=(const IrcMessage&) = delete;
+ IrcMessage& operator=(IrcMessage&&) = delete;
+
+ std::string prefix;
+ std::string command;
+ std::vector<std::string> arguments;
+};
+
+std::ostream& operator<<(std::ostream& os, const IrcMessage& message);
+
+
diff --git a/src/irc/irc_user.cpp b/src/irc/irc_user.cpp
new file mode 100644
index 0000000..9fa3612
--- /dev/null
+++ b/src/irc/irc_user.cpp
@@ -0,0 +1,57 @@
+#include <irc/irc_user.hpp>
+
+#include <iostream>
+
+IrcUser::IrcUser(const std::string& name,
+ const std::map<char, char>& prefix_to_mode)
+{
+ if (name.empty())
+ return ;
+
+ // One or more prefix (with multi-prefix support) may come before the
+ // actual nick
+ std::string::size_type name_begin = 0;
+ while (name_begin != name.size())
+ {
+ const auto prefix = prefix_to_mode.find(name[name_begin]);
+ // This is not a prefix
+ if (prefix == prefix_to_mode.end())
+ break;
+ this->modes.insert(prefix->second);
+ name_begin++;
+ }
+
+ const std::string::size_type sep = name.find("!", name_begin);
+ if (sep == std::string::npos)
+ this->nick = name.substr(name_begin);
+ else
+ {
+ this->nick = name.substr(name_begin, sep-name_begin);
+ this->host = name.substr(sep+1);
+ }
+}
+
+IrcUser::IrcUser(const std::string& name):
+ IrcUser(name, {})
+{
+}
+
+void IrcUser::add_mode(const char mode)
+{
+ this->modes.insert(mode);
+}
+
+void IrcUser::remove_mode(const char mode)
+{
+ this->modes.erase(mode);
+}
+
+char IrcUser::get_most_significant_mode(const std::vector<char>& modes) const
+{
+ for (const char mode: modes)
+ {
+ if (this->modes.find(mode) != this->modes.end())
+ return mode;
+ }
+ return 0;
+}
diff --git a/src/irc/irc_user.hpp b/src/irc/irc_user.hpp
new file mode 100644
index 0000000..c84030e
--- /dev/null
+++ b/src/irc/irc_user.hpp
@@ -0,0 +1,33 @@
+#pragma once
+
+
+#include <vector>
+#include <string>
+#include <map>
+#include <set>
+
+/**
+ * Keeps various information about one IRC channel user
+ */
+class IrcUser
+{
+public:
+ explicit IrcUser(const std::string& name,
+ const std::map<char, char>& prefix_to_mode);
+ explicit IrcUser(const std::string& name);
+
+ IrcUser(const IrcUser&) = delete;
+ IrcUser(IrcUser&&) = delete;
+ IrcUser& operator=(const IrcUser&) = delete;
+ IrcUser& operator=(IrcUser&&) = delete;
+
+ void add_mode(const char mode);
+ void remove_mode(const char mode);
+ char get_most_significant_mode(const std::vector<char>& sorted_user_modes) const;
+
+ std::string nick;
+ std::string host;
+ std::set<char> modes;
+};
+
+
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..53f3193
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,202 @@
+#include <xmpp/biboumi_component.hpp>
+#include <utils/timed_events.hpp>
+#include <network/poller.hpp>
+#include <config/config.hpp>
+#include <logger/logger.hpp>
+#include <utils/xdg.hpp>
+#include <utils/reload.hpp>
+
+#ifdef CARES_FOUND
+# include <network/dns_handler.hpp>
+#endif
+
+#include <atomic>
+#include <signal.h>
+
+// A flag set by the SIGINT signal handler.
+static volatile std::atomic<bool> stop(false);
+// Flag set by the SIGUSR1/2 signal handler.
+static volatile std::atomic<bool> reload(false);
+// A flag indicating that we are wanting to exit the process. i.e: if this
+// flag is set and all connections are closed, we can exit properly.
+static bool exiting = false;
+
+/**
+ * Provide an helpful message to help the user write a minimal working
+ * configuration file.
+ */
+int config_help(const std::string& missing_option)
+{
+ if (!missing_option.empty())
+ log_error("Configuration error: empty value for option ", missing_option, ".");
+ log_error("Please provide a configuration file filled like this:\n\n"
+ "hostname=irc.example.com\npassword=S3CR3T");
+ return 1;
+}
+
+int display_help()
+{
+ std::cout << "Usage: biboumi [configuration_file]" << std::endl;
+ return 0;
+}
+
+static void sigint_handler(int sig, siginfo_t*, void*)
+{
+ // In 2 seconds, repeat the same signal, to force the exit
+ TimedEventsManager::instance().add_event(TimedEvent(std::chrono::steady_clock::now() + 2s,
+ [sig]() { raise(sig); }));
+ stop.store(true);
+}
+
+static void sigusr_handler(int, siginfo_t*, void*)
+{
+ reload.store(true);
+}
+
+int main(int ac, char** av)
+{
+ if (ac > 1)
+ {
+ const std::string arg = av[1];
+ if (arg.size() >= 2 && arg[0] == '-' && arg[1] == '-')
+ {
+ if (arg == "--help")
+ return display_help();
+ else
+ {
+ std::cerr << "Unknow command line option: " << arg << std::endl;
+ return 1;
+ }
+ }
+ }
+ const std::string conf_filename = ac > 1 ? av[1] : xdg_config_path("biboumi.cfg");
+ std::cout << "Using configuration file: " << conf_filename << std::endl;
+
+ if (!Config::read_conf(conf_filename))
+ return config_help("");
+
+ const std::string password = Config::get("password", "");
+ if (password.empty())
+ return config_help("password");
+ const std::string hostname = Config::get("hostname", "");
+ if (hostname.empty())
+ return config_help("hostname");
+
+ try {
+ open_database();
+ } catch (...) {
+ return 1;
+ }
+
+ // Block the signals we want to manage. They will be unblocked only during
+ // the epoll_pwait or ppoll calls. This avoids some race conditions,
+ // explained in man 2 pselect on linux
+ sigset_t mask;
+ sigemptyset(&mask);
+ sigaddset(&mask, SIGINT);
+ sigaddset(&mask, SIGTERM);
+ sigaddset(&mask, SIGUSR1);
+ sigaddset(&mask, SIGUSR2);
+ sigprocmask(SIG_BLOCK, &mask, nullptr);
+
+ // Install the signals used to exit the process cleanly, or reload the
+ // config
+ struct sigaction on_sigint;
+ on_sigint.sa_sigaction = &sigint_handler;
+ // All signals must be blocked while a signal handler is running
+ sigfillset(&on_sigint.sa_mask);
+ // we want to catch that signal only once.
+ // Sending SIGINT again will "force" an exit
+ on_sigint.sa_flags = SA_RESETHAND;
+ sigaction(SIGINT, &on_sigint, nullptr);
+ sigaction(SIGTERM, &on_sigint, nullptr);
+
+ // Install a signal to reload the config on SIGUSR1/2
+ struct sigaction on_sigusr;
+ on_sigusr.sa_sigaction = &sigusr_handler;
+ sigfillset(&on_sigusr.sa_mask);
+ on_sigusr.sa_flags = 0;
+ sigaction(SIGUSR1, &on_sigusr, nullptr);
+ sigaction(SIGUSR2, &on_sigusr, nullptr);
+
+ auto p = std::make_shared<Poller>();
+ auto xmpp_component =
+ std::make_shared<BiboumiComponent>(p, hostname, password);
+ xmpp_component->start();
+
+#ifdef CARES_FOUND
+ DNSHandler::instance.watch_dns_sockets(p);
+#endif
+ auto timeout = TimedEventsManager::instance().get_timeout();
+ while (p->poll(timeout) != -1)
+ {
+ TimedEventsManager::instance().execute_expired_events();
+ // Check for empty irc_clients (not connected, or with no joined
+ // channel) and remove them
+ xmpp_component->clean();
+ if (stop)
+ {
+ log_info("Signal received, exiting...");
+#ifdef SYSTEMD_FOUND
+ sd_notify(0, "STOPPING=1");
+#endif
+ exiting = true;
+ stop.store(false);
+ xmpp_component->shutdown();
+ // Cancel the timer for a potential reconnection
+ TimedEventsManager::instance().cancel("XMPP reconnection");
+ }
+ if (reload)
+ {
+ log_info("Signal received, reloading the config...");
+ ::reload_process();
+ reload.store(false);
+ }
+ // Reconnect to the XMPP server if this was not intended. This may have
+ // happened because we sent something invalid to it and it decided to
+ // close the connection. This is a bug that should be fixed, but we
+ // still reconnect automatically instead of dropping everything
+ if (!exiting && xmpp_component->ever_auth &&
+ !xmpp_component->is_connected() &&
+ !xmpp_component->is_connecting())
+ {
+ if (xmpp_component->first_connection_try == true)
+ { // immediately re-try to connect
+ xmpp_component->reset();
+ xmpp_component->start();
+ }
+ else
+ { // Re-connecting failed, we now try only each few seconds
+ auto reconnect_later = [xmpp_component]()
+ {
+ xmpp_component->reset();
+ xmpp_component->start();
+ };
+ TimedEvent event(std::chrono::steady_clock::now() + 2s,
+ reconnect_later, "XMPP reconnection");
+ TimedEventsManager::instance().add_event(std::move(event));
+ }
+ }
+ // If the only existing connection is the one to the XMPP component:
+ // close the XMPP stream.
+ if (exiting && xmpp_component->is_connecting())
+ xmpp_component->close();
+ if (exiting && p->size() == 1 && xmpp_component->is_document_open())
+ xmpp_component->close_document();
+#ifdef CARES_FOUND
+ if (!exiting)
+ DNSHandler::instance.watch_dns_sockets(p);
+#endif
+ if (exiting) // If we are exiting, do not wait for any timed event
+ timeout = utils::no_timeout;
+ else
+ timeout = TimedEventsManager::instance().get_timeout();
+ }
+#ifdef CARES_FOUND
+ DNSHandler::instance.destroy();
+#endif
+ if (!xmpp_component->ever_auth)
+ return 1; // To signal that the process did not properly start
+ log_info("All connections cleanly closed, have a nice day.");
+ return 0;
+}
diff --git a/src/utils/empty_if_fixed_server.hpp b/src/utils/empty_if_fixed_server.hpp
new file mode 100644
index 0000000..9ccf5fd
--- /dev/null
+++ b/src/utils/empty_if_fixed_server.hpp
@@ -0,0 +1,26 @@
+#pragma once
+
+
+#include <string>
+
+#include <config/config.hpp>
+
+namespace utils
+{
+ inline std::string empty_if_fixed_server(std::string&& str)
+ {
+ if (!Config::get("fixed_irc_server", "").empty())
+ return {};
+ return str;
+ }
+
+ inline std::string empty_if_fixed_server(const std::string& str)
+ {
+ if (!Config::get("fixed_irc_server", "").empty())
+ return {};
+ return str;
+ }
+
+}
+
+
diff --git a/src/utils/reload.cpp b/src/utils/reload.cpp
new file mode 100644
index 0000000..348c5b5
--- /dev/null
+++ b/src/utils/reload.cpp
@@ -0,0 +1,34 @@
+#include <utils/reload.hpp>
+#include <database/database.hpp>
+#include <config/config.hpp>
+#include <utils/xdg.hpp>
+#include <logger/logger.hpp>
+
+#include "biboumi.h"
+
+void open_database()
+{
+#ifdef USE_DATABASE
+ const auto db_filename = Config::get("db_name", xdg_data_path("biboumi.sqlite"));
+ log_info("Opening database: ", db_filename);
+ Database::open(db_filename);
+ log_info("database successfully opened.");
+#endif
+}
+
+void reload_process()
+{
+ Config::read_conf();
+ // Destroy the logger instance, to be recreated the next time a log
+ // line needs to be written
+ Logger::instance().reset();
+ log_info("Configuration and logger reloaded.");
+#ifdef USE_DATABASE
+ try {
+ open_database();
+ } catch (const litesql::DatabaseError&) {
+ log_warning("Re-using the previous database.");
+ }
+#endif
+}
+
diff --git a/src/utils/reload.hpp b/src/utils/reload.hpp
new file mode 100644
index 0000000..408426a
--- /dev/null
+++ b/src/utils/reload.hpp
@@ -0,0 +1,4 @@
+#pragma once
+
+void open_database();
+void reload_process();
diff --git a/src/xmpp/biboumi_adhoc_commands.cpp b/src/xmpp/biboumi_adhoc_commands.cpp
new file mode 100644
index 0000000..eec930d
--- /dev/null
+++ b/src/xmpp/biboumi_adhoc_commands.cpp
@@ -0,0 +1,635 @@
+#include <xmpp/biboumi_adhoc_commands.hpp>
+#include <xmpp/biboumi_component.hpp>
+#include <config/config.hpp>
+#include <utils/string.hpp>
+#include <utils/split.hpp>
+#include <xmpp/jid.hpp>
+
+#include <biboumi.h>
+
+#ifdef USE_DATABASE
+#include <database/database.hpp>
+#endif
+
+#include <louloulibs.h>
+
+#include <algorithm>
+
+using namespace std::string_literals;
+
+void DisconnectUserStep1(XmppComponent& xmpp_component, AdhocSession&, XmlNode& command_node)
+{
+ auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component);
+
+ XmlNode x("jabber:x:data:x");
+ x["type"] = "form";
+ XmlNode title("title");
+ title.set_inner("Disconnect a user from the gateway");
+ x.add_child(std::move(title));
+ XmlNode instructions("instructions");
+ instructions.set_inner("Choose a user JID and a quit message");
+ x.add_child(std::move(instructions));
+ XmlNode jids_field("field");
+ jids_field["var"] = "jids";
+ jids_field["type"] = "list-multi";
+ jids_field["label"] = "The JIDs to disconnect";
+ XmlNode required("required");
+ jids_field.add_child(std::move(required));
+ for (Bridge* bridge: biboumi_component.get_bridges())
+ {
+ XmlNode option("option");
+ option["label"] = bridge->get_jid();
+ XmlNode value("value");
+ value.set_inner(bridge->get_jid());
+ option.add_child(std::move(value));
+ jids_field.add_child(std::move(option));
+ }
+ x.add_child(std::move(jids_field));
+
+ XmlNode message_field("field");
+ message_field["var"] = "quit-message";
+ message_field["type"] = "text-single";
+ message_field["label"] = "Quit message";
+ XmlNode message_value("value");
+ message_value.set_inner("Disconnected by admin");
+ message_field.add_child(std::move(message_value));
+ x.add_child(std::move(message_field));
+ command_node.add_child(std::move(x));
+}
+
+void DisconnectUserStep2(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node)
+{
+ auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component);
+
+ // Find out if the jids, and the quit message are provided in the form.
+ std::string quit_message;
+ const XmlNode* x = command_node.get_child("x", "jabber:x:data");
+ if (x)
+ {
+ const XmlNode* message_field = nullptr;
+ const XmlNode* jids_field = nullptr;
+ for (const XmlNode* field: x->get_children("field", "jabber:x:data"))
+ if (field->get_tag("var") == "jids")
+ jids_field = field;
+ else if (field->get_tag("var") == "quit-message")
+ message_field = field;
+ if (message_field)
+ {
+ const XmlNode* value = message_field->get_child("value", "jabber:x:data");
+ if (value)
+ quit_message = value->get_inner();
+ }
+ if (jids_field)
+ {
+ std::size_t num = 0;
+ for (const XmlNode* value: jids_field->get_children("value", "jabber:x:data"))
+ {
+ Bridge* bridge = biboumi_component.find_user_bridge(value->get_inner());
+ if (bridge)
+ {
+ bridge->shutdown(quit_message);
+ num++;
+ }
+ }
+ command_node.delete_all_children();
+
+ XmlNode note("note");
+ note["type"] = "info";
+ if (num == 0)
+ note.set_inner("No user were disconnected.");
+ else if (num == 1)
+ note.set_inner("1 user has been disconnected.");
+ else
+ note.set_inner(std::to_string(num) + " users have been disconnected.");
+ 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();
+}
+
+#ifdef USE_DATABASE
+void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node)
+{
+ const Jid owner(session.get_owner_jid());
+ const Jid target(session.get_target_jid());
+ std::string server_domain;
+ if ((server_domain = Config::get("fixed_irc_server", "")).empty())
+ server_domain = target.local;
+ auto options = Database::get_irc_server_options(owner.local + "@" + owner.domain,
+ server_domain);
+
+ XmlNode x("jabber:x:data:x");
+ x["type"] = "form";
+ XmlNode title("title");
+ title.set_inner("Configure the IRC server "s + server_domain);
+ x.add_child(std::move(title));
+ XmlNode instructions("instructions");
+ instructions.set_inner("Edit the form, to configure the settings of the IRC server "s + server_domain);
+ x.add_child(std::move(instructions));
+
+ XmlNode required("required");
+
+ XmlNode ports("field");
+ ports["var"] = "ports";
+ ports["type"] = "text-multi";
+ ports["label"] = "Ports";
+ ports["desc"] = "List of ports to try, without TLS. Defaults: 6667.";
+ auto vals = utils::split(options.ports.value(), ';', false);
+ for (const auto& val: vals)
+ {
+ XmlNode ports_value("value");
+ ports_value.set_inner(val);
+ ports.add_child(std::move(ports_value));
+ }
+ ports.add_child(required);
+ x.add_child(std::move(ports));
+
+#ifdef BOTAN_FOUND
+ XmlNode tls_ports("field");
+ tls_ports["var"] = "tls_ports";
+ tls_ports["type"] = "text-multi";
+ tls_ports["label"] = "TLS ports";
+ tls_ports["desc"] = "List of ports to try, with TLS. Defaults: 6697, 6670.";
+ vals = utils::split(options.tlsPorts.value(), ';', false);
+ for (const auto& val: vals)
+ {
+ XmlNode tls_ports_value("value");
+ tls_ports_value.set_inner(val);
+ tls_ports.add_child(std::move(tls_ports_value));
+ }
+ tls_ports.add_child(required);
+ x.add_child(std::move(tls_ports));
+
+ XmlNode verify_cert("field");
+ verify_cert["var"] = "verify_cert";
+ verify_cert["type"] = "boolean";
+ verify_cert["label"] = "Verify certificate";
+ verify_cert["desc"] = "Whether or not to abort the connection if the server’s TLS certificate is invalid";
+ XmlNode verify_cert_value("value");
+ if (options.verifyCert.value())
+ verify_cert_value.set_inner("true");
+ else
+ verify_cert_value.set_inner("false");
+ verify_cert.add_child(std::move(verify_cert_value));
+ x.add_child(std::move(verify_cert));
+
+ XmlNode fingerprint("field");
+ fingerprint["var"] = "fingerprint";
+ fingerprint["type"] = "text-single";
+ fingerprint["label"] = "SHA-1 fingerprint of the TLS certificate to trust.";
+ if (!options.trustedFingerprint.value().empty())
+ {
+ XmlNode fingerprint_value("value");
+ fingerprint_value.set_inner(options.trustedFingerprint.value());
+ fingerprint.add_child(std::move(fingerprint_value));
+ }
+ fingerprint.add_child(required);
+ x.add_child(std::move(fingerprint));
+#endif
+
+ XmlNode pass("field");
+ pass["var"] = "pass";
+ pass["type"] = "text-private";
+ pass["label"] = "Server password (to be used in a PASS command when connecting)";
+ if (!options.pass.value().empty())
+ {
+ XmlNode pass_value("value");
+ pass_value.set_inner(options.pass.value());
+ pass.add_child(std::move(pass_value));
+ }
+ pass.add_child(required);
+ x.add_child(std::move(pass));
+
+ XmlNode after_cnt_cmd("field");
+ after_cnt_cmd["var"] = "after_connect_command";
+ after_cnt_cmd["type"] = "text-single";
+ after_cnt_cmd["desc"] = "Custom IRC command sent after the connection is established with the server.";
+ after_cnt_cmd["label"] = "After-connection IRC command";
+ if (!options.afterConnectionCommand.value().empty())
+ {
+ XmlNode after_cnt_cmd_value("value");
+ after_cnt_cmd_value.set_inner(options.afterConnectionCommand.value());
+ after_cnt_cmd.add_child(std::move(after_cnt_cmd_value));
+ }
+ after_cnt_cmd.add_child(required);
+ x.add_child(std::move(after_cnt_cmd));
+
+ if (Config::get("realname_customization", "true") == "true")
+ {
+ XmlNode username("field");
+ username["var"] = "username";
+ username["type"] = "text-single";
+ username["label"] = "Username";
+ if (!options.username.value().empty())
+ {
+ XmlNode username_value("value");
+ username_value.set_inner(options.username.value());
+ username.add_child(std::move(username_value));
+ }
+ username.add_child(required);
+ x.add_child(std::move(username));
+
+ XmlNode realname("field");
+ realname["var"] = "realname";
+ realname["type"] = "text-single";
+ realname["label"] = "Realname";
+ if (!options.realname.value().empty())
+ {
+ XmlNode realname_value("value");
+ realname_value.set_inner(options.realname.value());
+ realname.add_child(std::move(realname_value));
+ }
+ realname.add_child(required);
+ x.add_child(std::move(realname));
+ }
+
+ XmlNode encoding_out("field");
+ encoding_out["var"] = "encoding_out";
+ encoding_out["type"] = "text-single";
+ encoding_out["desc"] = "The encoding used when sending messages to the IRC server.";
+ encoding_out["label"] = "Out encoding";
+ if (!options.encodingOut.value().empty())
+ {
+ XmlNode encoding_out_value("value");
+ encoding_out_value.set_inner(options.encodingOut.value());
+ encoding_out.add_child(std::move(encoding_out_value));
+ }
+ encoding_out.add_child(required);
+ x.add_child(std::move(encoding_out));
+
+ XmlNode encoding_in("field");
+ encoding_in["var"] = "encoding_in";
+ encoding_in["type"] = "text-single";
+ encoding_in["desc"] = "The encoding used to decode message received from the IRC server.";
+ encoding_in["label"] = "In encoding";
+ if (!options.encodingIn.value().empty())
+ {
+ XmlNode encoding_in_value("value");
+ encoding_in_value.set_inner(options.encodingIn.value());
+ encoding_in.add_child(std::move(encoding_in_value));
+ }
+ encoding_in.add_child(required);
+ x.add_child(std::move(encoding_in));
+
+
+ command_node.add_child(std::move(x));
+}
+
+void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node)
+{
+ const XmlNode* x = command_node.get_child("x", "jabber:x:data");
+ if (x)
+ {
+ const Jid owner(session.get_owner_jid());
+ const Jid target(session.get_target_jid());
+ std::string server_domain;
+ if ((server_domain = Config::get("fixed_irc_server", "")).empty())
+ server_domain = target.local;
+ auto options = Database::get_irc_server_options(owner.local + "@" + owner.domain,
+ server_domain);
+ for (const XmlNode* field: x->get_children("field", "jabber:x:data"))
+ {
+ const XmlNode* value = field->get_child("value", "jabber:x:data");
+ const std::vector<const XmlNode*> values = field->get_children("value", "jabber:x:data");
+ if (field->get_tag("var") == "ports")
+ {
+ std::string ports;
+ for (const auto& val: values)
+ ports += val->get_inner() + ";";
+ options.ports = ports;
+ }
+
+#ifdef BOTAN_FOUND
+ else if (field->get_tag("var") == "tls_ports")
+ {
+ std::string ports;
+ for (const auto& val: values)
+ ports += val->get_inner() + ";";
+ options.tlsPorts = ports;
+ }
+
+ else if (field->get_tag("var") == "verify_cert" && value
+ && !value->get_inner().empty())
+ {
+ auto val = to_bool(value->get_inner());
+ options.verifyCert = val;
+ }
+
+ else if (field->get_tag("var") == "fingerprint" && value &&
+ !value->get_inner().empty())
+ {
+ options.trustedFingerprint = value->get_inner();
+ }
+
+#endif // BOTAN_FOUND
+
+ else if (field->get_tag("var") == "pass" &&
+ value && !value->get_inner().empty())
+ options.pass = value->get_inner();
+
+ else if (field->get_tag("var") == "after_connect_command" &&
+ value && !value->get_inner().empty())
+ options.afterConnectionCommand = value->get_inner();
+
+ else if (field->get_tag("var") == "username" &&
+ value && !value->get_inner().empty())
+ {
+ auto username = value->get_inner();
+ // The username must not contain spaces
+ std::replace(username.begin(), username.end(), ' ', '_');
+ options.username = username;
+ }
+
+ else if (field->get_tag("var") == "realname" &&
+ value && !value->get_inner().empty())
+ options.realname = value->get_inner();
+
+ else if (field->get_tag("var") == "encoding_out" &&
+ value && !value->get_inner().empty())
+ options.encodingOut = value->get_inner();
+
+ else if (field->get_tag("var") == "encoding_in" &&
+ value && !value->get_inner().empty())
+ options.encodingIn = value->get_inner();
+
+ }
+
+ 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 ConfigureIrcChannelStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node)
+{
+ const Jid owner(session.get_owner_jid());
+ const Jid target(session.get_target_jid());
+ 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());
+
+ XmlNode x("jabber:x:data:x");
+ x["type"] = "form";
+ XmlNode title("title");
+ title.set_inner("Configure the IRC channel "s + iid.get_local() + " on server "s + iid.get_server());
+ x.add_child(std::move(title));
+ XmlNode instructions("instructions");
+ instructions.set_inner("Edit the form, to configure the settings of the IRC channel "s + iid.get_local());
+ x.add_child(std::move(instructions));
+
+ XmlNode required("required");
+
+ XmlNode encoding_out("field");
+ encoding_out["var"] = "encoding_out";
+ encoding_out["type"] = "text-single";
+ encoding_out["desc"] = "The encoding used when sending messages to the IRC server. Defaults to the server's “out encoding” if unset for the channel";
+ encoding_out["label"] = "Out encoding";
+ if (!options.encodingOut.value().empty())
+ {
+ XmlNode encoding_out_value("value");
+ encoding_out_value.set_inner(options.encodingOut.value());
+ encoding_out.add_child(std::move(encoding_out_value));
+ }
+ encoding_out.add_child(required);
+ x.add_child(std::move(encoding_out));
+
+ XmlNode encoding_in("field");
+ encoding_in["var"] = "encoding_in";
+ encoding_in["type"] = "text-single";
+ encoding_in["desc"] = "The encoding used to decode message received from the IRC server. Defaults to the server's “in encoding” if unset for the channel";
+ encoding_in["label"] = "In encoding";
+ if (!options.encodingIn.value().empty())
+ {
+ XmlNode encoding_in_value("value");
+ encoding_in_value.set_inner(options.encodingIn.value());
+ encoding_in.add_child(std::move(encoding_in_value));
+ }
+ encoding_in.add_child(required);
+ x.add_child(std::move(encoding_in));
+
+ command_node.add_child(std::move(x));
+}
+
+void ConfigureIrcChannelStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node)
+{
+ const XmlNode* x = command_node.get_child("x", "jabber:x:data");
+ if (x)
+ {
+ const Jid owner(session.get_owner_jid());
+ const Jid target(session.get_target_jid());
+ 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"))
+ {
+ const XmlNode* value = field->get_child("value", "jabber:x:data");
+
+ if (field->get_tag("var") == "encoding_out" &&
+ value && !value->get_inner().empty())
+ options.encodingOut = value->get_inner();
+
+ else if (field->get_tag("var") == "encoding_in" &&
+ value && !value->get_inner().empty())
+ options.encodingIn = value->get_inner();
+ }
+
+ 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();
+}
+#endif // USE_DATABASE
+
+void DisconnectUserFromServerStep1(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node)
+{
+ const Jid owner(session.get_owner_jid());
+ if (owner.bare() != Config::get("admin", ""))
+ { // A non-admin is not allowed to disconnect other users, only
+ // him/herself, so we just skip this step
+ auto next_step = session.get_next_step();
+ next_step(xmpp_component, session, command_node);
+ }
+ else
+ { // Send a form to select the user to disconnect
+ auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component);
+
+ XmlNode x("jabber:x:data:x");
+ x["type"] = "form";
+ XmlNode title("title");
+ title.set_inner("Disconnect a user from selected IRC servers");
+ x.add_child(std::move(title));
+ XmlNode instructions("instructions");
+ instructions.set_inner("Choose a user JID");
+ x.add_child(std::move(instructions));
+ XmlNode jids_field("field");
+ jids_field["var"] = "jid";
+ jids_field["type"] = "list-single";
+ jids_field["label"] = "The JID to disconnect";
+ XmlNode required("required");
+ jids_field.add_child(std::move(required));
+ for (Bridge* bridge: biboumi_component.get_bridges())
+ {
+ XmlNode option("option");
+ option["label"] = bridge->get_jid();
+ XmlNode value("value");
+ value.set_inner(bridge->get_jid());
+ option.add_child(std::move(value));
+ jids_field.add_child(std::move(option));
+ }
+ x.add_child(std::move(jids_field));
+ command_node.add_child(std::move(x));
+ }
+}
+
+void DisconnectUserFromServerStep2(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node)
+{
+ // If no JID is contained in the command node, it means we skipped the
+ // previous stage, and the jid to disconnect is the executor's jid
+ std::string jid_to_disconnect = session.get_owner_jid();
+
+ if (const XmlNode* x = command_node.get_child("x", "jabber:x:data"))
+ {
+ for (const XmlNode* field: x->get_children("field", "jabber:x:data"))
+ if (field->get_tag("var") == "jid")
+ {
+ if (const XmlNode* value = field->get_child("value", "jabber:x:data"))
+ jid_to_disconnect = value->get_inner();
+ }
+ }
+
+ // Save that JID for the last step
+ session.vars["jid"] = jid_to_disconnect;
+
+ // Send a data form to let the user choose which server to disconnect the
+ // user from
+ command_node.delete_all_children();
+ auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component);
+
+ XmlNode x("jabber:x:data:x");
+ x["type"] = "form";
+ XmlNode title("title");
+ title.set_inner("Disconnect a user from selected IRC servers");
+ x.add_child(std::move(title));
+ XmlNode instructions("instructions");
+ instructions.set_inner("Choose one or more servers to disconnect this JID from");
+ x.add_child(std::move(instructions));
+ XmlNode jids_field("field");
+ jids_field["var"] = "irc-servers";
+ jids_field["type"] = "list-multi";
+ jids_field["label"] = "The servers to disconnect from";
+ XmlNode required("required");
+ jids_field.add_child(std::move(required));
+ Bridge* bridge = biboumi_component.find_user_bridge(jid_to_disconnect);
+
+ if (!bridge || bridge->get_irc_clients().empty())
+ {
+ XmlNode note("note");
+ note["type"] = "info";
+ note.set_inner("User "s + jid_to_disconnect + " is not connected to any IRC server.");
+ command_node.add_child(std::move(note));
+ session.terminate();
+ return ;
+ }
+
+ for (const auto& pair: bridge->get_irc_clients())
+ {
+ XmlNode option("option");
+ option["label"] = pair.first;
+ XmlNode value("value");
+ value.set_inner(pair.first);
+ option.add_child(std::move(value));
+ jids_field.add_child(std::move(option));
+ }
+ x.add_child(std::move(jids_field));
+
+ XmlNode message_field("field");
+ message_field["var"] = "quit-message";
+ message_field["type"] = "text-single";
+ message_field["label"] = "Quit message";
+ XmlNode message_value("value");
+ message_value.set_inner("Killed by admin");
+ message_field.add_child(std::move(message_value));
+ x.add_child(std::move(message_field));
+
+ command_node.add_child(std::move(x));
+}
+
+void DisconnectUserFromServerStep3(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node)
+{
+ const auto it = session.vars.find("jid");
+ if (it == session.vars.end())
+ return ;
+ const auto jid_to_disconnect = it->second;
+
+ std::vector<std::string> servers;
+ std::string quit_message;
+
+ if (const XmlNode* x = command_node.get_child("x", "jabber:x:data"))
+ {
+ for (const XmlNode* field: x->get_children("field", "jabber:x:data"))
+ {
+ if (field->get_tag("var") == "irc-servers")
+ {
+ for (const XmlNode* value: field->get_children("value", "jabber:x:data"))
+ servers.push_back(value->get_inner());
+ }
+ else if (field->get_tag("var") == "quit-message")
+ if (const XmlNode* value = field->get_child("value", "jabber:x:data"))
+ quit_message = value->get_inner();
+ }
+ }
+
+ auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component);
+ Bridge* bridge = biboumi_component.find_user_bridge(jid_to_disconnect);
+ auto& clients = bridge->get_irc_clients();
+
+ std::size_t number = 0;
+
+ for (const auto& hostname: servers)
+ {
+ auto it = clients.find(hostname);
+ if (it != clients.end())
+ {
+ it->second->on_error({"ERROR", {quit_message}});
+ clients.erase(it);
+ number++;
+ }
+ }
+ command_node.delete_all_children();
+ XmlNode note("note");
+ note["type"] = "info";
+ std::string msg = jid_to_disconnect + " was disconnected from " + std::to_string(number) + " IRC server";
+ if (number > 1)
+ msg += "s";
+ msg += ".";
+ note.set_inner(msg);
+ command_node.add_child(std::move(note));
+}
diff --git a/src/xmpp/biboumi_adhoc_commands.hpp b/src/xmpp/biboumi_adhoc_commands.hpp
new file mode 100644
index 0000000..2763a9f
--- /dev/null
+++ b/src/xmpp/biboumi_adhoc_commands.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+
+#include <xmpp/adhoc_command.hpp>
+#include <xmpp/adhoc_session.hpp>
+#include <xmpp/xmpp_stanza.hpp>
+
+class XmppComponent;
+
+void DisconnectUserStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+void DisconnectUserStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+
+void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+
+void ConfigureIrcChannelStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+void ConfigureIrcChannelStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+
+void DisconnectUserFromServerStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+void DisconnectUserFromServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+void DisconnectUserFromServerStep3(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+
+
diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp
new file mode 100644
index 0000000..a6aac21
--- /dev/null
+++ b/src/xmpp/biboumi_component.cpp
@@ -0,0 +1,632 @@
+#include <xmpp/biboumi_component.hpp>
+
+#include <utils/timed_events.hpp>
+#include <utils/scopeguard.hpp>
+#include <utils/tolower.hpp>
+#include <logger/logger.hpp>
+#include <xmpp/adhoc_command.hpp>
+#include <xmpp/biboumi_adhoc_commands.hpp>
+#include <bridge/list_element.hpp>
+#include <config/config.hpp>
+#include <xmpp/jid.hpp>
+#include <utils/sha1.hpp>
+
+#include <stdexcept>
+#include <iostream>
+
+#include <stdio.h>
+
+#include <louloulibs.h>
+#include <biboumi.h>
+
+#include <uuid.h>
+
+#ifdef SYSTEMD_FOUND
+# include <systemd/sd-daemon.h>
+#endif
+
+using namespace std::string_literals;
+
+static std::set<std::string> kickable_errors{
+ "gone",
+ "internal-server-error",
+ "item-not-found",
+ "jid-malformed",
+ "recipient-unavailable",
+ "redirect",
+ "remote-server-not-found",
+ "remote-server-timeout",
+ "service-unavailable",
+ "malformed-error"
+ };
+
+
+BiboumiComponent::BiboumiComponent(std::shared_ptr<Poller> poller, const std::string& hostname, const std::string& secret):
+ XmppComponent(poller, hostname, secret),
+ irc_server_adhoc_commands_handler(*this),
+ irc_channel_adhoc_commands_handler(*this)
+{
+ this->stanza_handlers.emplace("presence",
+ std::bind(&BiboumiComponent::handle_presence, this,std::placeholders::_1));
+ this->stanza_handlers.emplace("message",
+ std::bind(&BiboumiComponent::handle_message, this,std::placeholders::_1));
+ 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)}
+ };
+
+#ifdef USE_DATABASE
+ AdhocCommand configure_server_command({&ConfigureIrcServerStep1, &ConfigureIrcServerStep2}, "Configure a few settings for that IRC server", false);
+ if (!Config::get("fixed_irc_server", "").empty())
+ {
+ this->adhoc_commands_handler.get_commands().emplace(std::make_pair("configure",
+ configure_server_command));
+ }
+#endif
+
+ this->irc_server_adhoc_commands_handler.get_commands() = {
+#ifdef USE_DATABASE
+ {"configure", configure_server_command},
+#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()
+{
+ for (auto it = this->bridges.begin(); it != this->bridges.end(); ++it)
+ {
+ it->second->shutdown("Gateway shutdown");
+ }
+}
+
+void BiboumiComponent::clean()
+{
+ auto it = this->bridges.begin();
+ while (it != this->bridges.end())
+ {
+ it->second->clean();
+ if (it->second->active_clients() == 0)
+ it = this->bridges.erase(it);
+ else
+ ++it;
+ }
+}
+
+void BiboumiComponent::handle_presence(const Stanza& stanza)
+{
+ 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");
+
+ // Check for mandatory tags
+ if (from_str.empty())
+ {
+ log_warning("Received an invalid presence stanza: tag 'from' is missing.");
+ return;
+ }
+ if (to_str.empty())
+ {
+ this->send_stanza_error("presence", from_str, this->served_hostname, id,
+ "modify", "bad-request", "Missing 'to' tag");
+ return;
+ }
+
+ Bridge* bridge = this->get_user_bridge(from_str);
+ Jid to(to_str);
+ Jid from(from_str);
+ Iid iid(to.local);
+
+ // An error stanza is sent whenever we exit this function without
+ // disabling this scopeguard. If error_type and error_name are not
+ // changed, the error signaled is internal-server-error. Change their
+ // value to signal an other kind of error. For example
+ // feature-not-implemented, etc. Any non-error process should reach the
+ // stanza_error.disable() call at the end of the function.
+ std::string error_type("cancel");
+ std::string error_name("internal-server-error");
+ utils::ScopeGuard stanza_error([&](){
+ this->send_stanza_error("presence", from_str, to_str, id,
+ error_type, error_name, "");
+ });
+
+ try {
+ if (iid.is_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())
+ {
+ const std::string own_nick = bridge->get_own_nick(iid);
+ if (!own_nick.empty() && own_nick != to.resource)
+ bridge->send_irc_nick_change(iid, to.resource);
+ const XmlNode* x = stanza.get_child("x", MUC_NS);
+ const XmlNode* password = x ? x->get_child("password", MUC_NS): nullptr;
+ bridge->join_irc_channel(iid, to.resource, password ? password->get_inner(): "",
+ from.resource);
+ }
+ else if (type == "unavailable")
+ {
+ const XmlNode* status = stanza.get_child("status", COMPONENT_NS);
+ bridge->leave_irc_channel(std::move(iid), status ? status->get_inner() : "", from.resource);
+ }
+ }
+ else
+ {
+ // An user wants to join an invalid IRC channel, return a presence error to him
+ if (type.empty())
+ this->send_invalid_room_error(to.local, to.resource, from_str);
+ }
+ }
+ catch (const IRCNotConnected& ex)
+ {
+ this->send_stanza_error("presence", from_str, to_str, id,
+ "cancel", "remote-server-not-found",
+ "Not connected to IRC server "s + ex.hostname,
+ true);
+ }
+ stanza_error.disable();
+}
+
+void BiboumiComponent::handle_message(const Stanza& stanza)
+{
+ std::string from = 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())
+ return;
+ if (type.empty())
+ type = "normal";
+ Bridge* bridge = this->get_user_bridge(from);
+ Jid to(to_str);
+ Iid iid(to.local);
+
+ 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,
+ error_type, error_name, "");
+ });
+ const XmlNode* body = stanza.get_child("body", COMPONENT_NS);
+
+ try { // catch IRCNotConnected exceptions
+ if (type == "groupchat" && iid.is_channel)
+ {
+ if (body && !body->get_inner().empty())
+ {
+ bridge->send_channel_message(iid, body->get_inner());
+ }
+ const XmlNode* subject = stanza.get_child("subject", COMPONENT_NS);
+ if (subject)
+ bridge->set_channel_topic(iid, subject->get_inner());
+ }
+ else if (type == "error")
+ {
+ 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).
+ // We consider this to be true, unless the error condition is
+ // specified and is not in the kickable_errors set
+ bool kickable_error = true;
+ if (error && error->has_children())
+ {
+ const XmlNode* condition = error->get_last_child();
+ if (kickable_errors.find(condition->get_name()) == kickable_errors.end())
+ kickable_error = false;
+ }
+ if (kickable_error)
+ bridge->shutdown("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())
+ {
+ bridge->send_private_message(iid, body->get_inner());
+ bridge->remove_preferred_from_jid(iid.get_local());
+ }
+ else if (!iid.is_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());
+ 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)
+ { // 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);
+ } catch (const IRCNotConnected& ex)
+ {
+ this->send_stanza_error("message", from, to_str, id,
+ "cancel", "remote-server-not-found",
+ "Not connected to IRC server "s + ex.hostname,
+ true);
+ }
+ stanza_error.disable();
+}
+
+// We MUST return an iq, whatever happens, except if the type is
+// "result".
+// To do this, we use a scopeguard. If an exception is raised somewhere, an
+// iq of type error "internal-server-error" is sent. If we handle the
+// request properly (by calling a function that registers an iq to be sent
+// later, or that directly sends an iq), we disable the ScopeGuard. If we
+// reach the end of the function without having disabled the scopeguard, we
+// send a "feature-not-implemented" iq as a result. If an other kind of
+// error is found (for example the feature is implemented in biboumi, but
+// the request is missing some attribute) we can just change the values of
+// error_type and error_name and return from the function (without disabling
+// the scopeguard); an iq error will be sent
+void BiboumiComponent::handle_iq(const Stanza& stanza)
+{
+ std::string id = stanza.get_tag("id");
+ std::string from = stanza.get_tag("from");
+ std::string to_str = stanza.get_tag("to");
+ std::string type = stanza.get_tag("type");
+
+ if (from.empty()) {
+ log_warning("Received an iq without a 'from'. Ignoring.");
+ return;
+ }
+ if (id.empty() || to_str.empty() || type.empty())
+ {
+ this->send_stanza_error("iq", from, this->served_hostname, id,
+ "modify", "bad-request", "");
+ return;
+ }
+
+ Bridge* bridge = this->get_user_bridge(from);
+ Jid to(to_str);
+
+ // These two values will be used in the error iq sent if we don't disable
+ // the scopeguard.
+ std::string error_type("cancel");
+ std::string error_name("internal-server-error");
+ utils::ScopeGuard stanza_error([&](){
+ this->send_stanza_error("iq", from, to_str, id,
+ error_type, error_name, "");
+ });
+ try {
+ if (type == "set")
+ {
+ const XmlNode* query;
+ if ((query = stanza.get_child("query", MUC_ADMIN_NS)))
+ {
+ const XmlNode* child = query->get_child("item", MUC_ADMIN_NS);
+ if (child)
+ {
+ std::string nick = child->get_tag("nick");
+ std::string role = child->get_tag("role");
+ std::string affiliation = child->get_tag("affiliation");
+ if (!nick.empty())
+ {
+ Iid iid(to.local);
+ if (role == "none")
+ { // This is a kick
+ std::string reason;
+ const XmlNode* reason_el = child->get_child("reason", MUC_ADMIN_NS);
+ if (reason_el)
+ reason = reason_el->get_inner();
+ bridge->send_irc_kick(iid, nick, reason, id, from);
+ }
+ else
+ bridge->forward_affiliation_role_change(iid, nick, affiliation, role);
+ stanza_error.disable();
+ }
+ }
+ }
+ else if ((query = stanza.get_child("command", ADHOC_NS)))
+ {
+ Stanza response("iq");
+ response["to"] = from;
+ response["from"] = to_str;
+ response["id"] = id;
+
+ // Depending on the 'to' jid in the request, we use one adhoc
+ // command handler or an other
+ 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
+ adhoc_handler = &this->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);
+ if (inner_node.get_child("error", ADHOC_NS))
+ response["type"] = "error";
+ else
+ response["type"] = "result";
+ response.add_child(std::move(inner_node));
+ this->send_stanza(response);
+ stanza_error.disable();
+ }
+ }
+ else if (type == "get")
+ {
+ const XmlNode* query;
+ if ((query = stanza.get_child("query", DISCO_INFO_NS)))
+ { // Disco info
+ if (to_str == this->served_hostname)
+ {
+ const std::string node = query->get_tag("node");
+ if (node.empty())
+ {
+ // On the gateway itself
+ this->send_self_disco_info(id, from);
+ 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()))
+ {
+ // Get the IRC user version
+ std::string target;
+ if (iid.is_user)
+ target = iid.get_local();
+ else
+ target = to.resource;
+ bridge->send_irc_version_request(iid.get_server(), target, id,
+ from, to_str);
+ }
+ else
+ {
+ // On the gateway itself or on a channel
+ this->send_version(id, from, to_str);
+ }
+ stanza_error.disable();
+ }
+ else if ((query = stanza.get_child("query", DISCO_ITEMS_NS)))
+ {
+ Iid iid(to.local);
+ const std::string node = query->get_tag("node");
+ if (node == ADHOC_NS)
+ {
+ Jid from_jid(from);
+ if (to.local.empty())
+ { // Get biboumi's adhoc commands
+ this->send_adhoc_commands_list(id, from, this->served_hostname,
+ (Config::get("admin", "") ==
+ from_jid.bare()),
+ this->adhoc_commands_handler);
+ stanza_error.disable();
+ }
+ else if (!iid.is_user && !iid.is_channel)
+ { // Get the server's adhoc commands
+ this->send_adhoc_commands_list(id, from, to_str,
+ (Config::get("admin", "") ==
+ from_jid.bare()),
+ this->irc_server_adhoc_commands_handler);
+ stanza_error.disable();
+ }
+ else if (!iid.is_user && iid.is_channel)
+ { // Get the channel's adhoc commands
+ this->send_adhoc_commands_list(id, from, to_str,
+ (Config::get("admin", "") ==
+ from_jid.bare()),
+ this->irc_channel_adhoc_commands_handler);
+ stanza_error.disable();
+ }
+ }
+ else if (node.empty() && !iid.is_user && !iid.is_channel)
+ { // Disco on an IRC server: get the list of channels
+ bridge->send_irc_channel_list_request(iid, id, from);
+ stanza_error.disable();
+ }
+ }
+ else if ((query = stanza.get_child("ping", PING_NS)))
+ {
+ Iid iid(to.local);
+ if (iid.is_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())
+ { // 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);
+ }
+ else
+ { // Ping a channel, a server or the gateway itself
+ bridge->on_gateway_ping(iid.get_server(),
+ id, from, to_str);
+ }
+ stanza_error.disable();
+ }
+ }
+ else if (type == "result")
+ {
+ stanza_error.disable();
+ const XmlNode* query;
+ if ((query = stanza.get_child("query", VERSION_NS)))
+ {
+ const XmlNode* name_node = query->get_child("name", VERSION_NS);
+ const XmlNode* version_node = query->get_child("version", VERSION_NS);
+ const XmlNode* os_node = query->get_child("os", VERSION_NS);
+ std::string name;
+ std::string version;
+ std::string os;
+ if (name_node)
+ name = name_node->get_inner() + " (through the biboumi gateway)";
+ if (version_node)
+ version = version_node->get_inner();
+ if (os_node)
+ os = os_node->get_inner();
+ const Iid iid(to.local);
+ bridge->send_xmpp_version_to_irc(iid, name, version, os);
+ }
+ else
+ {
+ const auto it = this->waiting_iq.find(id);
+ if (it != this->waiting_iq.end())
+ {
+ it->second(bridge, stanza);
+ this->waiting_iq.erase(it);
+ }
+ }
+ }
+ }
+ catch (const IRCNotConnected& ex)
+ {
+ this->send_stanza_error("iq", from, to_str, id,
+ "cancel", "remote-server-not-found",
+ "Not connected to IRC server "s + ex.hostname,
+ true);
+ stanza_error.disable();
+ return;
+ }
+ error_type = "cancel";
+ error_name = "feature-not-implemented";
+}
+
+Bridge* BiboumiComponent::get_user_bridge(const std::string& user_jid)
+{
+ auto bare_jid = Jid{user_jid}.bare();
+ try
+ {
+ return this->bridges.at(bare_jid).get();
+ }
+ 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();
+ }
+}
+
+Bridge* BiboumiComponent::find_user_bridge(const std::string& full_jid)
+{
+ auto bare_jid = Jid{full_jid}.bare();
+ try
+ {
+ return this->bridges.at(bare_jid).get();
+ }
+ catch (const std::out_of_range& exception)
+ {
+ return nullptr;
+ }
+}
+
+std::vector<Bridge*> BiboumiComponent::get_bridges() const
+{
+ std::vector<Bridge*> res;
+ for (auto it = this->bridges.begin(); it != this->bridges.end(); ++it)
+ res.push_back(it->second.get());
+ return res;
+}
+
+void BiboumiComponent::send_self_disco_info(const std::string& id, const std::string& jid_to)
+{
+ Stanza iq("iq");
+ iq["type"] = "result";
+ iq["id"] = id;
+ iq["to"] = jid_to;
+ iq["from"] = this->served_hostname;
+ XmlNode query("query");
+ query["xmlns"] = DISCO_INFO_NS;
+ XmlNode identity("identity");
+ identity["category"] = "conference";
+ 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})
+ {
+ 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_iq_version_request(const std::string& from,
+ const std::string& jid_to)
+{
+ Stanza iq("iq");
+ iq["type"] = "get";
+ iq["id"] = "version_"s + this->next_id();
+ iq["from"] = from + "@" + this->served_hostname;
+ iq["to"] = jid_to;
+ XmlNode query("query");
+ query["xmlns"] = VERSION_NS;
+ iq.add_child(std::move(query));
+ this->send_stanza(iq);
+}
+
+void BiboumiComponent::send_ping_request(const std::string& from,
+ const std::string& jid_to,
+ const std::string& id)
+{
+ Stanza iq("iq");
+ iq["type"] = "get";
+ iq["id"] = id;
+ iq["from"] = from + "@" + this->served_hostname;
+ iq["to"] = jid_to;
+ XmlNode ping("ping");
+ ping["xmlns"] = PING_NS;
+ iq.add_child(std::move(ping));
+ this->send_stanza(iq);
+
+ auto result_cb = [from, id](Bridge* bridge, const Stanza& stanza)
+ {
+ Jid to(stanza.get_tag("to"));
+ if (to.local != from)
+ {
+ log_error("Received a corresponding ping result, but the 'to' from "
+ "the response mismatches the 'from' of the request");
+ }
+ else
+ bridge->send_irc_ping_result(from, 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)
+{
+ Stanza iq("iq");
+ iq["from"] = from + "@" + this->served_hostname;
+ iq["to"] = to_jid;
+ iq["id"] = id;
+ iq["type"] = "result";
+ XmlNode query("query");
+ query["xmlns"] = DISCO_ITEMS_NS;
+ for (const auto& room: rooms_list)
+ {
+ XmlNode item("item");
+ item["jid"] = room.channel + "%" + from + "@" + this->served_hostname;
+ query.add_child(std::move(item));
+ }
+ iq.add_child(std::move(query));
+ this->send_stanza(iq);
+}
diff --git a/src/xmpp/biboumi_component.hpp b/src/xmpp/biboumi_component.hpp
new file mode 100644
index 0000000..24d768a
--- /dev/null
+++ b/src/xmpp/biboumi_component.hpp
@@ -0,0 +1,109 @@
+#pragma once
+
+
+#include <xmpp/xmpp_component.hpp>
+
+#include <bridge/bridge.hpp>
+
+#include <memory>
+#include <string>
+#include <map>
+
+struct ListElement;
+
+/**
+ * A callback called when the waited iq result is received (it is matched
+ * against the iq id)
+ */
+using iq_responder_callback_t = std::function<void(Bridge* bridge, const Stanza& stanza)>;
+
+/**
+ * Interact with the Biboumi Bridge
+ */
+class BiboumiComponent: public XmppComponent
+{
+public:
+ explicit BiboumiComponent(std::shared_ptr<Poller> poller, const std::string& hostname, const std::string& secret);
+ ~BiboumiComponent() = default;
+
+ BiboumiComponent(const BiboumiComponent&) = delete;
+ BiboumiComponent(BiboumiComponent&&) = delete;
+ BiboumiComponent& operator=(const BiboumiComponent&) = delete;
+ BiboumiComponent& operator=(BiboumiComponent&&) = delete;
+
+ /**
+ * Returns the bridge for the given user. If it does not exist, return
+ * nullptr.
+ */
+ Bridge* find_user_bridge(const std::string& full_jid);
+ /**
+ * Return a list of all the managed bridges.
+ */
+ std::vector<Bridge*> get_bridges() const;
+
+ /**
+ * Send a "close" message to all our connected peers. That message
+ * depends on the protocol used (this may be a QUIT irc message, or a
+ * <stream/>, etc). We may also directly close the connection, or we may
+ * wait for the remote peer to acknowledge it before closing.
+ */
+ void shutdown();
+ /**
+ * Run a check on all bridges, to remove all disconnected (socket is
+ * closed, or no channel is joined) IrcClients. Some kind of garbage collector.
+ */
+ void clean();
+ /**
+ * Send a result IQ with the gateway disco informations.
+ */
+ void send_self_disco_info(const std::string& id, const std::string& jid_to);
+ /**
+ * Send an iq version request
+ */
+ void send_iq_version_request(const std::string& from,
+ const std::string& jid_to);
+ /**
+ * Send a ping request
+ */
+ void send_ping_request(const std::string& from,
+ const std::string& jid_to,
+ const std::string& id);
+ /**
+ * 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);
+ /**
+ * Handle the various stanza types
+ */
+ void handle_presence(const Stanza& stanza);
+ void handle_message(const Stanza& stanza);
+ void handle_iq(const Stanza& stanza);
+
+private:
+ /**
+ * 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);
+
+ /**
+ * 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
+ * is received, we look for a corresponding callback in this map. If
+ * found, we call it and remove it.
+ */
+ std::map<std::string, iq_responder_callback_t> waiting_iq;
+
+ /**
+ * One bridge for each user of the component. Indexed by the user's bare
+ * jid
+ */
+ std::unordered_map<std::string, std::unique_ptr<Bridge>> bridges;
+
+ AdhocCommandsHandler irc_server_adhoc_commands_handler;
+ AdhocCommandsHandler irc_channel_adhoc_commands_handler;
+};
+
+