PLEASE backup your database before testing this commit, and report any migration issue. In postgresql, we use timestamp with timezone. In sqlite3 we use REAL (the date is expressed as julianday) This requires a migration of the muclogline_ table: In postgresql it’s pretty simple, we convert all the integer into timestamps With sqlite3, we actually rename the table, create the new one with the correct type, then copy everything to the new table, with a conversion function for the Date_ column, and then we delete the old table. fix #3343
1181 lines
43 KiB
C++
1181 lines
43 KiB
C++
#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 <utils/encoding.hpp>
|
||
#include <config/config.hpp>
|
||
#include <utils/time.hpp>
|
||
#include <xmpp/jid.hpp>
|
||
|
||
#include <stdexcept>
|
||
#include <iostream>
|
||
|
||
#include <cstdlib>
|
||
|
||
#include <biboumi.h>
|
||
|
||
#ifdef SYSTEMD_FOUND
|
||
# include <systemd/sd-daemon.h>
|
||
#endif
|
||
|
||
#include <database/database.hpp>
|
||
#include <bridge/result_set_management.hpp>
|
||
#include <bridge/history_limit.hpp>
|
||
|
||
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.add_command("ping", {{&PingStep1}, "Do a ping", false});
|
||
this->adhoc_commands_handler.add_command("hello", {{&HelloStep1, &HelloStep2}, "Receive a custom greeting", false});
|
||
this->adhoc_commands_handler.add_command("disconnect-user", {{&DisconnectUserStep1, &DisconnectUserStep2}, "Disconnect selected users from the gateway", true});
|
||
this->adhoc_commands_handler.add_command("disconnect-from-irc-server", {{&DisconnectUserFromServerStep1, &DisconnectUserFromServerStep2, &DisconnectUserFromServerStep3}, "Disconnect from the selected IRC servers", false});
|
||
this->adhoc_commands_handler.add_command("reload", {{&Reload}, "Reload biboumi’s configuration", true});
|
||
|
||
AdhocCommand get_irc_connection_info{{&GetIrcConnectionInfoStep1}, "Returns various information about your connection to this IRC server.", false};
|
||
if (!Config::get("fixed_irc_server", "").empty())
|
||
this->adhoc_commands_handler.add_command("get-irc-connection-info", get_irc_connection_info);
|
||
else
|
||
this->irc_server_adhoc_commands_handler.add_command("get-irc-connection-info", get_irc_connection_info);
|
||
|
||
#ifdef USE_DATABASE
|
||
AdhocCommand configure_server_command({&ConfigureIrcServerStep1, &ConfigureIrcServerStep2}, "Configure a few settings for that IRC server", false);
|
||
AdhocCommand configure_global_command({&ConfigureGlobalStep1, &ConfigureGlobalStep2}, "Configure a few settings", false);
|
||
|
||
if (!Config::get("fixed_irc_server", "").empty())
|
||
this->adhoc_commands_handler.add_command("configure", configure_server_command);
|
||
else
|
||
this->adhoc_commands_handler.add_command("configure", configure_global_command);
|
||
|
||
this->irc_server_adhoc_commands_handler.add_command("configure", configure_server_command);
|
||
this->irc_channel_adhoc_commands_handler.add_command("configure", {{&ConfigureIrcChannelStep1, &ConfigureIrcChannelStep2}, "Configure a few settings for that IRC channel", false});
|
||
#endif
|
||
}
|
||
|
||
void BiboumiComponent::shutdown()
|
||
{
|
||
for (auto& pair: this->bridges)
|
||
pair.second->shutdown("Gateway shutdown");
|
||
#ifdef USE_DATABASE
|
||
for (const Database::RosterItem& roster_item: Database::get_full_roster())
|
||
{
|
||
this->send_presence_to_contact(roster_item.col<Database::LocalJid>(),
|
||
roster_item.col<Database::RemoteJid>(),
|
||
"unavailable");
|
||
}
|
||
#endif
|
||
}
|
||
|
||
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, bridge);
|
||
|
||
// 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, &from_str, &to_str, &id, &error_type, &error_name](){
|
||
this->send_stanza_error("presence", from_str, to_str, id,
|
||
error_type, error_name, "");
|
||
});
|
||
|
||
try {
|
||
if (iid.type == Iid::Type::Channel && !iid.get_server().empty())
|
||
{ // presence toward a MUC that corresponds to an irc channel
|
||
if (type.empty())
|
||
{
|
||
const std::string own_nick = bridge->get_own_nick(iid);
|
||
const XmlNode* x = stanza.get_child("x", MUC_NS);
|
||
const XmlNode* password = x ? x->get_child("password", MUC_NS): nullptr;
|
||
const XmlNode* history = x ? x->get_child("history", MUC_NS): nullptr;
|
||
HistoryLimit history_limit;
|
||
if (history)
|
||
{
|
||
const auto seconds = history->get_tag("seconds");
|
||
if (!seconds.empty())
|
||
{
|
||
const auto now = std::chrono::system_clock::now();
|
||
std::time_t timestamp = std::chrono::system_clock::to_time_t(now);
|
||
int int_seconds = std::atoi(seconds.data());
|
||
timestamp -= int_seconds;
|
||
history_limit.since = utils::to_string(timestamp);
|
||
}
|
||
const auto since = history->get_tag("since");
|
||
if (!since.empty())
|
||
history_limit.since = since;
|
||
const auto maxstanzas = history->get_tag("maxstanzas");
|
||
if (!maxstanzas.empty())
|
||
history_limit.stanzas = std::atoi(maxstanzas.data());
|
||
// Ignore any other value, because this is too complex to implement,
|
||
// so I won’t do it.
|
||
if (history->get_tag("maxchars") == "0")
|
||
history_limit.stanzas = 0;
|
||
}
|
||
bridge->join_irc_channel(iid, to.resource, password ? password->get_inner(): "",
|
||
from.resource, history_limit, x != nullptr);
|
||
if (!own_nick.empty() && own_nick != to.resource)
|
||
bridge->send_irc_nick_change(iid, to.resource, 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 if (iid.type == Iid::Type::Server || iid.type == Iid::Type::None)
|
||
{
|
||
if (type == "subscribe")
|
||
{ // Auto-accept any subscription request for an IRC server
|
||
this->send_presence_to_contact(to_str, from.bare(), "subscribed", id);
|
||
if (iid.type == Iid::Type::None || bridge->find_irc_client(iid.get_server()))
|
||
this->send_presence_to_contact(to_str, from.bare(), "");
|
||
this->send_presence_to_contact(to_str, from.bare(), "subscribe");
|
||
#ifdef USE_DATABASE
|
||
if (!Database::has_roster_item(to_str, from.bare()))
|
||
Database::add_roster_item(to_str, from.bare());
|
||
#endif
|
||
}
|
||
else if (type == "unsubscribe")
|
||
{
|
||
this->send_presence_to_contact(to_str, from.bare(), "unavailable", id);
|
||
this->send_presence_to_contact(to_str, from.bare(), "unsubscribed");
|
||
this->send_presence_to_contact(to_str, from.bare(), "unsubscribe");
|
||
#ifdef USE_DATABASE
|
||
const bool res = Database::has_roster_item(to_str, from.bare());
|
||
if (res)
|
||
Database::delete_roster_item(to_str, from.bare());
|
||
#endif
|
||
}
|
||
else if (type == "probe")
|
||
{
|
||
if ((iid.type == Iid::Type::Server && bridge->find_irc_client(iid.get_server()))
|
||
|| iid.type == Iid::Type::None)
|
||
{
|
||
#ifdef USE_DATABASE
|
||
if (Database::has_roster_item(to_str, from.bare()))
|
||
#endif
|
||
this->send_presence_to_contact(to_str, from.bare(), "");
|
||
#ifdef USE_DATABASE
|
||
else // rfc 6121 4.3.2.1
|
||
this->send_presence_to_contact(to_str, from.bare(), "unsubscribed");
|
||
#endif
|
||
}
|
||
}
|
||
else if (type.empty())
|
||
{ // We just receive a presence from someone (as the result of a probe,
|
||
// or a directed presence, or a normal presence change)
|
||
if (iid.type == Iid::Type::None)
|
||
this->send_presence_to_contact(to_str, from.bare(), "");
|
||
}
|
||
}
|
||
else if (iid.type == Iid::Type::User)
|
||
{ // Do nothing yet
|
||
}
|
||
}
|
||
catch (const IRCNotConnected& ex)
|
||
{
|
||
if (type != "unavailable")
|
||
this->send_stanza_error("presence", from_str, to_str, id,
|
||
"cancel", "remote-server-not-found",
|
||
"Not connected to IRC server " + ex.hostname,
|
||
true);
|
||
}
|
||
stanza_error.disable();
|
||
}
|
||
|
||
void BiboumiComponent::handle_message(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");
|
||
|
||
if (from_str.empty())
|
||
return;
|
||
if (type.empty())
|
||
type = "normal";
|
||
Bridge* bridge = this->get_user_bridge(from_str);
|
||
Jid from(from_str);
|
||
Jid to(to_str);
|
||
Iid iid(to.local, bridge);
|
||
|
||
std::string error_type("cancel");
|
||
std::string error_name("internal-server-error");
|
||
utils::ScopeGuard stanza_error([this, &from_str, &to_str, &id, &error_type, &error_name](){
|
||
this->send_stanza_error("message", from_str, to_str, id,
|
||
error_type, error_name, "");
|
||
});
|
||
const XmlNode* body = stanza.get_child("body", COMPONENT_NS);
|
||
|
||
try { // catch IRCNotConnected exceptions
|
||
if (type == "groupchat" && iid.type == Iid::Type::Channel)
|
||
{
|
||
if (body && !body->get_inner().empty())
|
||
{
|
||
bridge->send_channel_message(iid, body->get_inner(), id);
|
||
}
|
||
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 that resource from all the IRC servers)
|
||
// We consider this to be true, unless the error condition is
|
||
// specified and is not in the kickable_errors set
|
||
bool kickable_error = true;
|
||
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->remove_resource(from.resource, "Error from remote client");
|
||
}
|
||
else if (type == "chat")
|
||
{
|
||
if (body && !body->get_inner().empty())
|
||
{
|
||
const auto fixed_irc_server = Config::get("fixed_irc_server", "");
|
||
// a message for nick!server
|
||
if (iid.type == Iid::Type::User && !iid.get_local().empty())
|
||
{
|
||
bridge->send_private_message(iid, body->get_inner());
|
||
bridge->remove_preferred_from_jid(iid.get_local());
|
||
}
|
||
else if (iid.type != Iid::Type::User && !to.resource.empty())
|
||
{ // a message for chan%server@biboumi/Nick or
|
||
// server@biboumi/Nick
|
||
// Convert that into a message to nick!server
|
||
Iid user_iid(utils::tolower(to.resource), iid.get_server(), Iid::Type::User);
|
||
bridge->send_private_message(user_iid, body->get_inner());
|
||
bridge->set_preferred_from_jid(user_iid.get_local(), to_str);
|
||
}
|
||
else if (iid.type == Iid::Type::Server)
|
||
bridge->send_raw_message(iid.get_server(), body->get_inner());
|
||
else if (iid.type == Iid::Type::None && !fixed_irc_server.empty())
|
||
{ // Message sent to the server JID
|
||
// Convert the message body into a raw IRC message
|
||
bridge->send_raw_message(fixed_irc_server, body->get_inner());
|
||
}
|
||
}
|
||
}
|
||
else if (type == "normal" && iid.type == Iid::Type::Channel)
|
||
{
|
||
if (const XmlNode* x = stanza.get_child("x", MUC_USER_NS))
|
||
if (const XmlNode* invite = x->get_child("invite", MUC_USER_NS))
|
||
{
|
||
const auto invite_to = invite->get_tag("to");
|
||
if (!invite_to.empty())
|
||
{
|
||
Jid invited_jid{invite_to};
|
||
if (invited_jid.domain == this->get_served_hostname() || invited_jid.local.empty())
|
||
bridge->send_irc_invitation(iid, invite_to);
|
||
else
|
||
this->send_invitation_from_fulljid(std::to_string(iid), invite_to, from_str);
|
||
}
|
||
}
|
||
|
||
}
|
||
} catch (const IRCNotConnected& ex)
|
||
{
|
||
this->send_stanza_error("message", from_str, to_str, id,
|
||
"cancel", "remote-server-not-found",
|
||
"Not connected to IRC server " + ex.hostname,
|
||
true);
|
||
}
|
||
stanza_error.disable();
|
||
}
|
||
|
||
// We MUST return an iq, whatever happens, except if the type is
|
||
// "result" or "error".
|
||
// 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, &from, &to_str, &id, &error_type, &error_name](){
|
||
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, from, nick, affiliation, role, id);
|
||
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())
|
||
adhoc_handler = &this->adhoc_commands_handler;
|
||
else
|
||
{
|
||
if (iid.type == Iid::Type::Server)
|
||
adhoc_handler = &this->irc_server_adhoc_commands_handler;
|
||
else if (iid.type == Iid::Type::Channel && to.resource.empty())
|
||
adhoc_handler = &this->irc_channel_adhoc_commands_handler;
|
||
else
|
||
{
|
||
error_name = "feature-not-implemented";
|
||
return;
|
||
}
|
||
}
|
||
// 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();
|
||
}
|
||
#ifdef USE_DATABASE
|
||
else if ((query = stanza.get_child("query", MAM_NS)))
|
||
{
|
||
try {
|
||
if (this->handle_mam_request(stanza))
|
||
stanza_error.disable();
|
||
} catch (const Database::RecordNotFound& exc) {
|
||
error_name = "item-not-found";
|
||
return;
|
||
}
|
||
}
|
||
else if ((query = stanza.get_child("query", MUC_OWNER_NS)))
|
||
{
|
||
if (this->handle_room_configuration_form(*query, from, to, id))
|
||
stanza_error.disable();
|
||
}
|
||
#endif
|
||
}
|
||
else if (type == "get")
|
||
{
|
||
const XmlNode* query;
|
||
if ((query = stanza.get_child("query", DISCO_INFO_NS)))
|
||
{ // Disco info
|
||
Iid iid(to.local, {'#', '&'});
|
||
const std::string node = query->get_tag("node");
|
||
if (to_str == this->served_hostname)
|
||
{
|
||
if (node.empty())
|
||
{
|
||
// On the gateway itself
|
||
this->send_self_disco_info(id, from);
|
||
stanza_error.disable();
|
||
}
|
||
}
|
||
else if (iid.type == Iid::Type::Server)
|
||
{
|
||
if (node.empty())
|
||
{
|
||
this->send_irc_server_disco_info(id, from, to_str);
|
||
stanza_error.disable();
|
||
}
|
||
}
|
||
else if (iid.type == Iid::Type::Channel && to.resource.empty())
|
||
{
|
||
if (node.empty())
|
||
{
|
||
this->send_irc_channel_disco_info(id, from, to_str);
|
||
stanza_error.disable();
|
||
}
|
||
else if (node == MUC_TRAFFIC_NS)
|
||
{
|
||
this->send_irc_channel_muc_traffic_info(id, from, to_str);
|
||
stanza_error.disable();
|
||
}
|
||
}
|
||
}
|
||
else if ((query = stanza.get_child("query", VERSION_NS)))
|
||
{
|
||
Iid iid(to.local, bridge);
|
||
if ((iid.type == Iid::Type::Channel && !to.resource.empty()) ||
|
||
(iid.type == Iid::Type::User))
|
||
{
|
||
// Get the IRC user version
|
||
std::string target;
|
||
if (iid.type == Iid::Type::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, bridge);
|
||
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.type == Iid::Type::Server)
|
||
{ // 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.type == Iid::Type::Channel && to.resource.empty())
|
||
{ // 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 // “to” is a MUC user, not the room itself
|
||
error_name = "feature-not-implemented";
|
||
}
|
||
else if (node.empty() && iid.type == Iid::Type::Server)
|
||
{ // Disco on an IRC server: get the list of channels
|
||
ResultSetInfo rs_info;
|
||
const XmlNode* set_node = query->get_child("set", RSM_NS);
|
||
if (set_node)
|
||
{
|
||
const XmlNode* after = set_node->get_child("after", RSM_NS);
|
||
if (after)
|
||
rs_info.after = after->get_inner();
|
||
const XmlNode* before = set_node->get_child("before", RSM_NS);
|
||
if (before)
|
||
rs_info.before = before->get_inner();
|
||
const XmlNode* max = set_node->get_child("max", RSM_NS);
|
||
if (max)
|
||
rs_info.max = std::atoi(max->get_inner().data());
|
||
|
||
}
|
||
if (rs_info.max == -1)
|
||
rs_info.max = 100;
|
||
bridge->send_irc_channel_list_request(iid, id, from, std::move(rs_info));
|
||
stanza_error.disable();
|
||
}
|
||
}
|
||
else if ((query = stanza.get_child("ping", PING_NS)))
|
||
{
|
||
Iid iid(to.local, bridge);
|
||
if (iid.type == Iid::Type::User)
|
||
{ // Ping any user (no check on the nick done ourself)
|
||
bridge->send_irc_user_ping_request(iid.get_server(),
|
||
iid.get_local(), id, from, to_str);
|
||
}
|
||
else if (iid.type == Iid::Type::Channel && !to.resource.empty())
|
||
{ // Ping a room participant (we check if the nick is in the room)
|
||
bridge->send_irc_participant_ping_request(iid,
|
||
to.resource, id, from, to_str);
|
||
}
|
||
else
|
||
{ // Ping a channel, a server or the gateway itself
|
||
bridge->on_gateway_ping(iid.get_server(),
|
||
id, from, to_str);
|
||
}
|
||
stanza_error.disable();
|
||
}
|
||
#ifdef USE_DATABASE
|
||
else if ((query = stanza.get_child("query", MUC_OWNER_NS)))
|
||
{
|
||
if (this->handle_room_configuration_form_request(from, to, id))
|
||
stanza_error.disable();
|
||
}
|
||
#endif
|
||
}
|
||
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);
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
else if (type == "error")
|
||
{
|
||
stanza_error.disable();
|
||
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 " + ex.hostname,
|
||
true);
|
||
stanza_error.disable();
|
||
return;
|
||
}
|
||
error_type = "cancel";
|
||
error_name = "feature-not-implemented";
|
||
}
|
||
|
||
#ifdef USE_DATABASE
|
||
bool BiboumiComponent::handle_mam_request(const Stanza& stanza)
|
||
{
|
||
std::string id = stanza.get_tag("id");
|
||
Jid from(stanza.get_tag("from"));
|
||
Jid to(stanza.get_tag("to"));
|
||
|
||
const XmlNode* query = stanza.get_child("query", MAM_NS);
|
||
|
||
Iid iid(to.local, {'#', '&'});
|
||
if (query && iid.type == Iid::Type::Channel && to.resource.empty())
|
||
{
|
||
const std::string query_id = query->get_tag("queryid");
|
||
std::string start;
|
||
std::string end;
|
||
const XmlNode* x = query->get_child("x", DATAFORM_NS);
|
||
if (x)
|
||
{
|
||
const XmlNode* value;
|
||
const auto fields = x->get_children("field", DATAFORM_NS);
|
||
for (const auto& field: fields)
|
||
{
|
||
if (field->get_tag("var") == "start")
|
||
{
|
||
value = field->get_child("value", DATAFORM_NS);
|
||
if (value)
|
||
start = value->get_inner();
|
||
}
|
||
else if (field->get_tag("var") == "end")
|
||
{
|
||
value = field->get_child("value", DATAFORM_NS);
|
||
if (value)
|
||
end = value->get_inner();
|
||
}
|
||
}
|
||
}
|
||
const XmlNode* set = query->get_child("set", RSM_NS);
|
||
int limit = -1;
|
||
Id::real_type reference_record_id{Id::unset_value};
|
||
Database::Paging paging_order{Database::Paging::first};
|
||
if (set)
|
||
{
|
||
const XmlNode* max = set->get_child("max", RSM_NS);
|
||
if (max)
|
||
limit = std::atoi(max->get_inner().data());
|
||
const XmlNode* after = set->get_child("after", RSM_NS);
|
||
if (after)
|
||
{
|
||
auto after_record = Database::get_muc_log(from.bare(), iid.get_local(), iid.get_server(),
|
||
after->get_inner(), start, end);
|
||
reference_record_id = after_record.col<Id>();
|
||
}
|
||
const XmlNode* before = set->get_child("before", RSM_NS);
|
||
if (before)
|
||
{
|
||
paging_order = Database::Paging::last;
|
||
if (!before->get_inner().empty())
|
||
{
|
||
auto before_record = Database::get_muc_log(from.bare(), iid.get_local(), iid.get_server(), before->get_inner(), start, end);
|
||
reference_record_id = before_record.col<Id>();
|
||
}
|
||
}
|
||
}
|
||
// Do not send more than 100 messages, even if the client asked for more,
|
||
// or if it didn’t specify any limit.
|
||
// 101 is just a trick to know if there are more available messages.
|
||
// If our query returns 101 message, we know it’s incomplete, but we
|
||
// still send only 100
|
||
if ((limit == -1 && start.empty() && end.empty())
|
||
|| limit > 100)
|
||
limit = 101;
|
||
auto lines = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(), limit, start, end, reference_record_id, paging_order);
|
||
bool complete = true;
|
||
if (lines.size() > 100)
|
||
{
|
||
complete = false;
|
||
lines.erase(lines.begin(), std::prev(lines.end(), 100));
|
||
}
|
||
for (const Database::MucLogLine& line: lines)
|
||
{
|
||
if (!line.col<Database::Nick>().empty())
|
||
this->send_archived_message(line, to.full(), from.full(), query_id);
|
||
}
|
||
{
|
||
auto fin_ptr = std::make_unique<XmlNode>("fin");
|
||
{
|
||
XmlNode& fin = *(fin_ptr.get());
|
||
fin["xmlns"] = MAM_NS;
|
||
if (complete)
|
||
fin["complete"] = "true";
|
||
XmlSubNode set(fin, "set");
|
||
set["xmlns"] = RSM_NS;
|
||
if (!lines.empty())
|
||
{
|
||
XmlSubNode first(set, "first");
|
||
first["index"] = "0";
|
||
first.set_inner(lines[0].col<Database::Uuid>());
|
||
XmlSubNode last(set, "last");
|
||
last.set_inner(lines[lines.size() - 1].col<Database::Uuid>());
|
||
}
|
||
}
|
||
this->send_iq_result_full_jid(id, from.full(), to.full(), std::move(fin_ptr));
|
||
}
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void BiboumiComponent::send_archived_message(const Database::MucLogLine& log_line, const std::string& from, const std::string& to,
|
||
const std::string& queryid)
|
||
{
|
||
Stanza message("message");
|
||
{
|
||
message["from"] = from;
|
||
message["to"] = to;
|
||
|
||
XmlSubNode result(message, "result");
|
||
result["xmlns"] = MAM_NS;
|
||
if (!queryid.empty())
|
||
result["queryid"] = queryid;
|
||
result["id"] = log_line.col<Database::Uuid>();
|
||
|
||
XmlSubNode forwarded(result, "forwarded");
|
||
forwarded["xmlns"] = FORWARD_NS;
|
||
|
||
XmlSubNode delay(forwarded, "delay");
|
||
delay["xmlns"] = DELAY_NS;
|
||
delay["stamp"] = log_line.col<Database::Date>().to_string();
|
||
|
||
XmlSubNode submessage(forwarded, "message");
|
||
submessage["xmlns"] = CLIENT_NS;
|
||
submessage["from"] = from + "/" + log_line.col<Database::Nick>();
|
||
submessage["type"] = "groupchat";
|
||
|
||
XmlSubNode body(submessage, "body");
|
||
body.set_inner(log_line.col<Database::Body>());
|
||
}
|
||
this->send_stanza(message);
|
||
}
|
||
|
||
bool BiboumiComponent::handle_room_configuration_form_request(const std::string& from, const Jid& to, const std::string& id)
|
||
{
|
||
Iid iid(to.local, {'#', '&'});
|
||
|
||
if (iid.type != Iid::Type::Channel || !to.resource.empty())
|
||
return false;
|
||
|
||
Stanza iq("iq");
|
||
{
|
||
iq["from"] = to.full();
|
||
iq["to"] = from;
|
||
iq["id"] = id;
|
||
iq["type"] = "result";
|
||
XmlSubNode query(iq, "query");
|
||
query["xmlns"] = MUC_OWNER_NS;
|
||
Jid requester(from);
|
||
insert_irc_channel_configuration_form(query, requester, to);
|
||
}
|
||
this->send_stanza(iq);
|
||
return true;
|
||
}
|
||
|
||
bool BiboumiComponent::handle_room_configuration_form(const XmlNode& query, const std::string &from, const Jid &to, const std::string &id)
|
||
{
|
||
Iid iid(to.local, {'#', '&'});
|
||
|
||
if (iid.type != Iid::Type::Channel || !to.resource.empty())
|
||
return false;
|
||
|
||
Jid requester(from);
|
||
if (!handle_irc_channel_configuration_form(*this, query, requester, to))
|
||
return false;
|
||
|
||
Stanza iq("iq");
|
||
iq["type"] = "result";
|
||
iq["from"] = to.full();
|
||
iq["to"] = from;
|
||
iq["id"] = id;
|
||
|
||
this->send_stanza(iq);
|
||
|
||
return true;
|
||
}
|
||
|
||
#endif
|
||
|
||
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)
|
||
{
|
||
return this->bridges.emplace(bare_jid, std::make_unique<Bridge>(bare_jid, *this, this->poller)).first->second.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 (const auto& bridge: this->bridges)
|
||
res.push_back(bridge.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;
|
||
XmlSubNode query(iq, "query");
|
||
query["xmlns"] = DISCO_INFO_NS;
|
||
XmlSubNode identity(query, "identity");
|
||
identity["category"] = "conference";
|
||
identity["type"] = "irc";
|
||
identity["name"] = "Biboumi XMPP-IRC gateway";
|
||
for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS, STABLE_MUC_ID_NS})
|
||
{
|
||
XmlSubNode feature(query, "feature");
|
||
feature["var"] = ns;
|
||
}
|
||
}
|
||
this->send_stanza(iq);
|
||
}
|
||
|
||
void BiboumiComponent::send_irc_server_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from)
|
||
{
|
||
Jid from(jid_from);
|
||
Stanza iq("iq");
|
||
{
|
||
iq["type"] = "result";
|
||
iq["id"] = id;
|
||
iq["to"] = jid_to;
|
||
iq["from"] = jid_from;
|
||
XmlSubNode query(iq, "query");
|
||
query["xmlns"] = DISCO_INFO_NS;
|
||
XmlSubNode identity(query, "identity");
|
||
identity["category"] = "conference";
|
||
identity["type"] = "irc";
|
||
identity["name"] = "IRC server " + from.local + " over Biboumi";
|
||
for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS, STABLE_MUC_ID_NS})
|
||
{
|
||
XmlSubNode feature(query, "feature");
|
||
feature["var"] = ns;
|
||
}
|
||
}
|
||
this->send_stanza(iq);
|
||
}
|
||
|
||
void BiboumiComponent::send_irc_channel_muc_traffic_info(const std::string& id, const std::string& jid_to, const std::string& jid_from)
|
||
{
|
||
Stanza iq("iq");
|
||
{
|
||
iq["type"] = "result";
|
||
iq["id"] = id;
|
||
iq["from"] = jid_from;
|
||
iq["to"] = jid_to;
|
||
|
||
XmlSubNode query(iq, "query");
|
||
query["xmlns"] = DISCO_INFO_NS;
|
||
query["node"] = MUC_TRAFFIC_NS;
|
||
// We drop all “special” traffic (like xhtml-im, chatstates, etc), so
|
||
// don’t include any <feature/>
|
||
}
|
||
this->send_stanza(iq);
|
||
}
|
||
|
||
void BiboumiComponent::send_irc_channel_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from)
|
||
{
|
||
Jid from(jid_from);
|
||
Iid iid(from.local, {});
|
||
Stanza iq("iq");
|
||
{
|
||
iq["type"] = "result";
|
||
iq["id"] = id;
|
||
iq["to"] = jid_to;
|
||
iq["from"] = jid_from;
|
||
XmlSubNode query(iq, "query");
|
||
query["xmlns"] = DISCO_INFO_NS;
|
||
XmlSubNode identity(query, "identity");
|
||
identity["category"] = "conference";
|
||
identity["type"] = "irc";
|
||
identity["name"] = ""s + iid.get_local() + " on " + iid.get_server();
|
||
for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS, STABLE_MUC_ID_NS})
|
||
{
|
||
XmlSubNode feature(query, "feature");
|
||
feature["var"] = ns;
|
||
}
|
||
}
|
||
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;
|
||
XmlSubNode ping(iq, "ping");
|
||
ping["xmlns"] = PING_NS;
|
||
}
|
||
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");
|
||
return;
|
||
}
|
||
const std::string type = stanza.get_tag("type");
|
||
const XmlNode* error = stanza.get_child("error", COMPONENT_NS);
|
||
// Check if what we receive is considered a valid response. And yes, those errors are valid responses
|
||
if (type == "result" ||
|
||
(type == "error" && error && (error->get_child("feature-not-implemented", STANZA_NS) ||
|
||
error->get_child("service-unavailable", STANZA_NS))))
|
||
bridge->send_irc_ping_result({from, bridge}, id);
|
||
};
|
||
this->waiting_iq[id] = result_cb;
|
||
}
|
||
|
||
void BiboumiComponent::send_iq_room_list_result(const std::string& id, const std::string& to_jid,
|
||
const std::string& from, const ChannelList& channel_list,
|
||
std::vector<ListElement>::const_iterator begin,
|
||
std::vector<ListElement>::const_iterator end,
|
||
const ResultSetInfo& rs_info)
|
||
{
|
||
Stanza iq("iq");
|
||
{
|
||
iq["from"] = from + "@" + this->served_hostname;
|
||
iq["to"] = to_jid;
|
||
iq["id"] = id;
|
||
iq["type"] = "result";
|
||
XmlSubNode query(iq, "query");
|
||
query["xmlns"] = DISCO_ITEMS_NS;
|
||
|
||
for (auto it = begin; it != end; ++it)
|
||
{
|
||
XmlSubNode item(query, "item");
|
||
std::string channel_name = it->channel;
|
||
xep0106::encode(channel_name);
|
||
item["jid"] = channel_name + "@" + this->served_hostname;
|
||
}
|
||
|
||
if ((rs_info.max >= 0 || !rs_info.after.empty() || !rs_info.before.empty()))
|
||
{
|
||
XmlSubNode set_node(query, "set");
|
||
set_node["xmlns"] = RSM_NS;
|
||
|
||
if (begin != channel_list.channels.cend())
|
||
{
|
||
XmlSubNode first_node(set_node, "first");
|
||
first_node["index"] = std::to_string(std::distance(channel_list.channels.cbegin(), begin));
|
||
first_node.set_inner(begin->channel + "@" + this->served_hostname);
|
||
}
|
||
if (end != channel_list.channels.cbegin())
|
||
{
|
||
XmlSubNode last_node(set_node, "last");
|
||
last_node.set_inner(std::prev(end)->channel + "@" + this->served_hostname);
|
||
}
|
||
if (channel_list.complete)
|
||
{
|
||
XmlSubNode count_node(set_node, "count");
|
||
count_node.set_inner(std::to_string(channel_list.channels.size()));
|
||
}
|
||
}
|
||
}
|
||
this->send_stanza(iq);
|
||
}
|
||
|
||
void BiboumiComponent::send_invitation(const std::string& room_target,
|
||
const std::string& jid_to,
|
||
const std::string& author_nick)
|
||
{
|
||
if (author_nick.empty())
|
||
this->send_invitation_from_fulljid(room_target, jid_to, room_target + "@" + this->served_hostname);
|
||
else
|
||
this->send_invitation_from_fulljid(room_target, jid_to, room_target + "@" + this->served_hostname + "/" + author_nick);
|
||
}
|
||
|
||
void BiboumiComponent::send_invitation_from_fulljid(const std::string& room_target,
|
||
const std::string& jid_to,
|
||
const std::string& from)
|
||
{
|
||
Stanza message("message");
|
||
{
|
||
message["from"] = room_target + "@" + this->served_hostname;
|
||
message["to"] = jid_to;
|
||
XmlSubNode x(message, "x");
|
||
x["xmlns"] = MUC_USER_NS;
|
||
XmlSubNode invite(x, "invite");
|
||
invite["from"] = from;
|
||
}
|
||
this->send_stanza(message);
|
||
}
|
||
|
||
void BiboumiComponent::accept_subscription(const std::string& from, const std::string& to)
|
||
{
|
||
Stanza presence("presence");
|
||
presence["from"] = from;
|
||
presence["to"] = to;
|
||
presence["id"] = this->next_id();
|
||
presence["type"] = "subscribed";
|
||
this->send_stanza(presence);
|
||
}
|
||
|
||
void BiboumiComponent::ask_subscription(const std::string& from, const std::string& to)
|
||
{
|
||
Stanza presence("presence");
|
||
presence["from"] = from;
|
||
presence["to"] = to;
|
||
presence["id"] = this->next_id();
|
||
presence["type"] = "subscribe";
|
||
this->send_stanza(presence);
|
||
}
|
||
|
||
void BiboumiComponent::send_presence_to_contact(const std::string& from, const std::string& to,
|
||
const std::string& type, const std::string& id)
|
||
{
|
||
Stanza presence("presence");
|
||
presence["from"] = from;
|
||
presence["to"] = to;
|
||
if (!type.empty())
|
||
presence["type"] = type;
|
||
if (!id.empty())
|
||
presence["id"] = id;
|
||
this->send_stanza(presence);
|
||
}
|
||
|
||
void BiboumiComponent::on_irc_client_connected(const std::string& irc_hostname, const std::string& jid)
|
||
{
|
||
#ifdef USE_DATABASE
|
||
const auto local_jid = irc_hostname + "@" + this->served_hostname;
|
||
if (Database::has_roster_item(local_jid, jid))
|
||
this->send_presence_to_contact(local_jid, jid, "");
|
||
#else
|
||
(void)irc_hostname;
|
||
(void)jid;
|
||
#endif
|
||
}
|
||
|
||
void BiboumiComponent::on_irc_client_disconnected(const std::string& irc_hostname, const std::string& jid)
|
||
{
|
||
#ifdef USE_DATABASE
|
||
const auto local_jid = irc_hostname + "@" + this->served_hostname;
|
||
if (Database::has_roster_item(local_jid, jid))
|
||
this->send_presence_to_contact(irc_hostname + "@" + this->served_hostname, jid, "unavailable");
|
||
#else
|
||
(void)irc_hostname;
|
||
(void)jid;
|
||
#endif
|
||
}
|
||
|
||
void BiboumiComponent::after_handshake()
|
||
{
|
||
XmppComponent::after_handshake();
|
||
|
||
#ifdef USE_DATABASE
|
||
const auto contacts = Database::get_contact_list(this->get_served_hostname());
|
||
|
||
for (const Database::RosterItem& roster_item: contacts)
|
||
{
|
||
const auto remote_jid = roster_item.col<Database::RemoteJid>();
|
||
// In response, we will receive a presence indicating the
|
||
// contact is online, to which we will respond with our own
|
||
// presence.
|
||
// If the contact removed us from their roster while we were
|
||
// offline, we will receive an unsubscribed presence, letting us
|
||
// stay in sync.
|
||
this->send_presence_to_contact(this->get_served_hostname(), remote_jid, "probe");
|
||
}
|
||
#endif
|
||
}
|