biboumi_component.cpp 34.7 KB
Newer Older
1 2 3 4 5 6 7
#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>
8
#include <xmpp/biboumi_adhoc_commands.hpp>
9 10
#include <bridge/list_element.hpp>
#include <config/config.hpp>
11 12
#include <utils/time.hpp>
#include <xmpp/jid.hpp>
13 14 15 16

#include <stdexcept>
#include <iostream>

17
#include <cstdlib>
18

19
#include <biboumi.h>
20

21
#include <uuid/uuid.h>
22 23 24 25 26

#ifdef SYSTEMD_FOUND
# include <systemd/sd-daemon.h>
#endif

27
#include <database/database.hpp>
28
#include <bridge/result_set_management.hpp>
29

30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
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"
    };


46
BiboumiComponent::BiboumiComponent(std::shared_ptr<Poller>& poller, const std::string& hostname, const std::string& secret):
47
  XmppComponent(poller, hostname, secret),
48 49
  irc_server_adhoc_commands_handler(*this),
  irc_channel_adhoc_commands_handler(*this)
50 51 52 53 54 55 56 57
{
  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));

58 59 60
  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});
61
  this->adhoc_commands_handler.add_command("disconnect-from-irc-server", {{&DisconnectUserFromServerStep1, &DisconnectUserFromServerStep2, &DisconnectUserFromServerStep3}, "Disconnect from the selected IRC servers", false});
62
  this->adhoc_commands_handler.add_command("reload", {{&Reload}, "Reload biboumi’s configuration", true});
63

64 65 66 67 68 69
  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);

70 71
#ifdef USE_DATABASE
  AdhocCommand configure_server_command({&ConfigureIrcServerStep1, &ConfigureIrcServerStep2}, "Configure a few settings for that IRC server", false);
72
  AdhocCommand configure_global_command({&ConfigureGlobalStep1, &ConfigureGlobalStep2}, "Configure a few settings", false);
73

74
  if (!Config::get("fixed_irc_server", "").empty())
75
    this->adhoc_commands_handler.add_command("configure", configure_server_command);
76
  else
77
    this->adhoc_commands_handler.add_command("configure", configure_global_command);
78

79 80
  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});
81
#endif
82 83 84 85
}

void BiboumiComponent::shutdown()
{
86 87
  for (auto& pair: this->bridges)
    pair.second->shutdown("Gateway shutdown");
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
}

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)
{
105
  std::string from_str = stanza.get_tag("from");
106 107 108 109 110
  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
111
  if (from_str.empty())
112 113 114 115 116 117
    {
      log_warning("Received an invalid presence stanza: tag 'from' is missing.");
      return;
    }
  if (to_str.empty())
    {
118
      this->send_stanza_error("presence", from_str, this->served_hostname, id,
119 120 121 122
                              "modify", "bad-request", "Missing 'to' tag");
      return;
    }

123
  Bridge* bridge = this->get_user_bridge(from_str);
124
  Jid to(to_str);
125
  Jid from(from_str);
126
  Iid iid(to.local, bridge);
127 128 129 130 131 132 133 134 135

  // 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");
louiz’'s avatar
louiz’ committed
136
  utils::ScopeGuard stanza_error([this, &from_str, &to_str, &id, &error_type, &error_name](){
137
      this->send_stanza_error("presence", from_str, to_str, id,
138 139 140
                              error_type, error_name, "");
        });

141
  try {
142
  if (iid.type == Iid::Type::Channel && !iid.get_server().empty())
143 144 145 146 147 148
    { // 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)
149
            bridge->send_irc_nick_change(iid, to.resource, from.resource);
150 151
          const XmlNode* x = stanza.get_child("x", MUC_NS);
          const XmlNode* password = x ? x->get_child("password", MUC_NS): nullptr;
152 153
          bridge->join_irc_channel(iid, to.resource, password ? password->get_inner(): "",
                                   from.resource);
154 155 156
        }
      else if (type == "unavailable")
        {
157
          const XmlNode* status = stanza.get_child("status", COMPONENT_NS);
158
          bridge->leave_irc_channel(std::move(iid), status ? status->get_inner() : "", from.resource);
159 160
        }
    }
louiz’'s avatar
louiz’ committed
161 162 163 164 165 166 167 168 169
  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->accept_subscription(to_str, from.bare());
          this->ask_subscription(to_str, from.bare());
        }

    }
170 171
  else
    {
louiz’'s avatar
louiz’ committed
172
      // A user wants to join an invalid IRC channel, return a presence error to him/her
173
      if (type.empty())
174
        this->send_invalid_room_error(to.local, to.resource, from_str);
175
    }
176 177 178
  }
  catch (const IRCNotConnected& ex)
    {
louiz’'s avatar
louiz’ committed
179
      if (type != "unavailable")
180 181 182 183
        this->send_stanza_error("presence", from_str, to_str, id,
                                "cancel", "remote-server-not-found",
                                "Not connected to IRC server "s + ex.hostname,
                                true);
184
    }
185 186 187 188 189
  stanza_error.disable();
}

void BiboumiComponent::handle_message(const Stanza& stanza)
{
190
  std::string from_str = stanza.get_tag("from");
191 192 193 194
  std::string id = stanza.get_tag("id");
  std::string to_str = stanza.get_tag("to");
  std::string type = stanza.get_tag("type");

195
  if (from_str.empty())
196 197 198
    return;
  if (type.empty())
    type = "normal";
199 200
  Bridge* bridge = this->get_user_bridge(from_str);
  Jid from(from_str);
201
  Jid to(to_str);
202
  Iid iid(to.local, bridge);
203 204 205

  std::string error_type("cancel");
  std::string error_name("internal-server-error");
louiz’'s avatar
louiz’ committed
206
  utils::ScopeGuard stanza_error([this, &from_str, &to_str, &id, &error_type, &error_name](){
207
      this->send_stanza_error("message", from_str, to_str, id,
208 209
                              error_type, error_name, "");
    });
210
  const XmlNode* body = stanza.get_child("body", COMPONENT_NS);
211 212

  try {                         // catch IRCNotConnected exceptions
213
  if (type == "groupchat" && iid.type == Iid::Type::Channel)
214 215 216 217 218
    {
      if (body && !body->get_inner().empty())
        {
          bridge->send_channel_message(iid, body->get_inner());
        }
219
      const XmlNode* subject = stanza.get_child("subject", COMPONENT_NS);
220 221 222 223 224 225 226
      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
227
      // them, we purge (we disconnect that resource from all the IRC servers)
228 229 230 231 232 233 234 235 236 237
      // 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)
238
        bridge->remove_resource(from.resource, "Error from remote client");
239 240 241 242 243 244
    }
  else if (type == "chat")
    {
      if (body && !body->get_inner().empty())
        {
          // a message for nick!server
245
          if (iid.type == Iid::Type::User && !iid.get_local().empty())
246 247 248 249
            {
              bridge->send_private_message(iid, body->get_inner());
              bridge->remove_preferred_from_jid(iid.get_local());
            }
250
          else if (iid.type != Iid::Type::User && !to.resource.empty())
251 252 253
            { // a message for chan%server@biboumi/Nick or
              // server@biboumi/Nick
              // Convert that into a message to nick!server
254
              Iid user_iid(utils::tolower(to.resource), iid.get_server(), Iid::Type::User);
255 256 257
              bridge->send_private_message(user_iid, body->get_inner());
              bridge->set_preferred_from_jid(user_iid.get_local(), to_str);
            }
258
          else if (iid.type == Iid::Type::Server)
louiz’'s avatar
louiz’ committed
259 260 261 262
            { // 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());
            }
263 264
        }
    }
265 266 267 268 269 270 271 272 273 274 275 276 277
  else if (type == "normal" && iid.type == Iid::Type::Channel)
    {
      if (const XmlNode* x = stanza.get_child("x", MUC_USER_NS))
        if (const XmlNode* invite = x->get_child("invite", MUC_USER_NS))
          {
            const auto invite_to = invite->get_tag("to");
            if (!invite_to.empty())
              {
                bridge->send_irc_invitation(iid, invite_to);
              }
          }

    }
278 279
  } catch (const IRCNotConnected& ex)
    {
280
      this->send_stanza_error("message", from_str, to_str, id,
281 282 283 284
                              "cancel", "remote-server-not-found",
                              "Not connected to IRC server "s + ex.hostname,
                              true);
    }
285 286 287 288
  stanza_error.disable();
}

// We MUST return an iq, whatever happens, except if the type is
289
// "result" or "error".
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
// 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");

307 308
  if (from.empty()) {
    log_warning("Received an iq without a 'from'. Ignoring.");
309
    return;
310
  }
311 312 313 314 315 316 317 318 319 320 321 322 323 324
  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");
louiz’'s avatar
louiz’ committed
325
  utils::ScopeGuard stanza_error([this, &from, &to_str, &id, &error_type, &error_name](){
326 327 328
      this->send_stanza_error("iq", from, to_str, id,
                              error_type, error_name, "");
    });
329
  try {
330 331
  if (type == "set")
    {
332
      const XmlNode* query;
333 334 335 336 337 338 339 340 341 342
      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())
                {
343
                  Iid iid(to.local, {});
344 345 346
                  if (role == "none")
                    {               // This is a kick
                      std::string reason;
347
                      const XmlNode* reason_el = child->get_child("reason", MUC_ADMIN_NS);
348 349 350 351 352
                      if (reason_el)
                        reason = reason_el->get_inner();
                      bridge->send_irc_kick(iid, nick, reason, id, from);
                    }
                  else
353
                    bridge->forward_affiliation_role_change(iid, from, nick, affiliation, role, id);
354 355 356 357 358 359 360 361
                  stanza_error.disable();
                }
            }
        }
      else if ((query = stanza.get_child("command", ADHOC_NS)))
        {
          Stanza response("iq");
          response["to"] = from;
362
          response["from"] = to_str;
363
          response["id"] = id;
364 365 366

          // Depending on the 'to' jid in the request, we use one adhoc
          // command handler or an other
367
          Iid iid(to.local, {});
368
          AdhocCommandsHandler* adhoc_handler;
369
          if (to.local.empty())
370
            adhoc_handler = &this->adhoc_commands_handler;
371 372 373 374 375 376 377
          else
          {
            if (iid.type == Iid::Type::Server)
              adhoc_handler = &this->irc_server_adhoc_commands_handler;
            else
              adhoc_handler = &this->irc_channel_adhoc_commands_handler;
          }
378 379 380
          // 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);
381 382 383 384 385 386 387 388
          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();
        }
389 390 391 392 393 394
#ifdef USE_DATABASE
      else if ((query = stanza.get_child("query", MAM_NS)))
        {
          if (this->handle_mam_request(stanza))
            stanza_error.disable();
        }
395 396 397 398 399
      else if ((query = stanza.get_child("query", MUC_OWNER_NS)))
        {
          if (this->handle_room_configuration_form(*query, from, to, id))
            stanza_error.disable();
        }
400
#endif
401 402 403
    }
  else if (type == "get")
    {
404
      const XmlNode* query;
405 406
      if ((query = stanza.get_child("query", DISCO_INFO_NS)))
        { // Disco info
louiz’'s avatar
louiz’ committed
407
          Iid iid(to.local, {'#', '&'});
408
          const std::string node = query->get_tag("node");
409 410 411 412 413 414 415 416 417
          if (to_str == this->served_hostname)
            {
              if (node.empty())
                {
                  // On the gateway itself
                  this->send_self_disco_info(id, from);
                  stanza_error.disable();
                }
            }
418 419 420 421 422 423 424 425
          else if (iid.type == Iid::Type::Server)
            {
                if (node.empty())
                {
                    this->send_irc_server_disco_info(id, from, to_str);
                    stanza_error.disable();
                }
            }
louiz’'s avatar
louiz’ committed
426 427
          else if (iid.type == Iid::Type::Channel)
            {
louiz’'s avatar
louiz’ committed
428 429 430 431 432 433
              if (node.empty())
                {
                  this->send_irc_channel_disco_info(id, from, to_str);
                  stanza_error.disable();
                }
              else if (node == MUC_TRAFFIC_NS)
louiz’'s avatar
louiz’ committed
434 435
                {
                  this->send_irc_channel_muc_traffic_info(id, from, to_str);
louiz’'s avatar
louiz’ committed
436
                  stanza_error.disable();
louiz’'s avatar
louiz’ committed
437 438
                }
            }
439 440 441
        }
      else if ((query = stanza.get_child("query", VERSION_NS)))
        {
442
          Iid iid(to.local, bridge);
443 444
          if ((iid.type == Iid::Type::Channel && !to.resource.empty()) ||
              (iid.type == Iid::Type::User))
445 446 447
            {
              // Get the IRC user version
              std::string target;
448
              if (iid.type == Iid::Type::User)
449 450 451 452 453 454 455 456 457 458 459 460 461 462 463
                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)))
        {
464
          Iid iid(to.local, bridge);
465 466 467
          const std::string node = query->get_tag("node");
          if (node == ADHOC_NS)
            {
468
              Jid from_jid(from);
469 470 471 472
              if (to.local.empty())
                {               // Get biboumi's adhoc commands
                  this->send_adhoc_commands_list(id, from, this->served_hostname,
                                                 (Config::get("admin", "") ==
473
                                                  from_jid.bare()),
474 475 476
                                                 this->adhoc_commands_handler);
                  stanza_error.disable();
                }
477
              else if (iid.type == Iid::Type::Server)
478 479 480
                {               // Get the server's adhoc commands
                  this->send_adhoc_commands_list(id, from, to_str,
                                                 (Config::get("admin", "") ==
481
                                                  from_jid.bare()),
482 483 484
                                                 this->irc_server_adhoc_commands_handler);
                  stanza_error.disable();
                }
485
              else if (iid.type == Iid::Type::Channel)
486 487 488 489 490 491 492
                {               // 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();
                }
493
            }
494
          else if (node.empty() && iid.type == Iid::Type::Server)
495
            { // Disco on an IRC server: get the list of channels
496 497 498 499 500 501 502 503 504 505 506 507 508 509 510
              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());

                }
511 512
              if (rs_info.max == -1)
                rs_info.max = 100;
513
              bridge->send_irc_channel_list_request(iid, id, from, std::move(rs_info));
514 515 516 517 518
              stanza_error.disable();
            }
        }
      else if ((query = stanza.get_child("ping", PING_NS)))
        {
519 520
          Iid iid(to.local, bridge);
          if (iid.type == Iid::Type::User)
521 522 523 524
            { // 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);
            }
525
          else if (iid.type == Iid::Type::Channel && !to.resource.empty())
526 527 528 529 530 531 532 533 534 535 536
            { // 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();
        }
537 538 539 540 541 542 543
#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
544 545 546 547
    }
  else if (type == "result")
    {
      stanza_error.disable();
548
      const XmlNode* query;
549 550
      if ((query = stanza.get_child("query", VERSION_NS)))
        {
551 552 553
          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);
554 555 556 557 558 559 560 561 562
          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();
563
          const Iid iid(to.local, bridge);
564 565 566 567 568 569 570 571 572 573 574 575
          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);
            }
        }
    }
576 577 578
  else if (type == "error")
    {
      stanza_error.disable();
579 580 581 582 583 584
      const auto it = this->waiting_iq.find(id);
      if (it != this->waiting_iq.end())
        {
          it->second(bridge, stanza);
          this->waiting_iq.erase(it);
        }
585
    }
586 587 588 589 590 591 592 593 594 595
  }
  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;
    }
596 597 598 599
  error_type = "cancel";
  error_name = "feature-not-implemented";
}

600 601 602 603 604 605 606 607 608 609
#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, {'#', '&'});
610
    if (query && iid.type == Iid::Type::Channel && to.resource.empty())
611
      {
612
        const std::string query_id = query->get_tag("queryid");
613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635
        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();
                  }
              }
          }
636 637 638 639 640 641 642 643
        const XmlNode* set = query->get_child("set", RSM_NS);
        int limit = -1;
        if (set)
          {
            const XmlNode* max = set->get_child("max", RSM_NS);
            if (max)
              limit = std::atoi(max->get_inner().data());
          }
644 645 646 647 648 649
        // If the archive is really big, and the client didn’t specify any
        // limit, we avoid flooding it: we set an arbitrary max limit.
        if (limit == -1 && start.empty() && end.empty())
          {
            limit = 100;
          }
650
        const auto lines = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(), limit, start, end);
651 652 653
        for (const db::MucLogLine& line: lines)
        {
          if (!line.nick.value().empty())
654
            this->send_archived_message(line, to.full(), from.full(), query_id);
655
        }
656 657 658 659 660 661 662 663 664
        this->send_iq_result_full_jid(id, from.full(), to.full());
        return true;
      }
  return false;
}

void BiboumiComponent::send_archived_message(const db::MucLogLine& log_line, const std::string& from, const std::string& to,
                                             const std::string& queryid)
{
665 666
  Stanza message("message");
  {
667 668 669
    message["from"] = from;
    message["to"] = to;

670
    XmlSubNode result(message, "result");
671
    result["xmlns"] = MAM_NS;
672 673
    if (!queryid.empty())
      result["queryid"] = queryid;
674 675
    result["id"] = log_line.uuid.value();

676
    XmlSubNode forwarded(result, "forwarded");
677 678
    forwarded["xmlns"] = FORWARD_NS;

679
    XmlSubNode delay(forwarded, "delay");
680 681 682
    delay["xmlns"] = DELAY_NS;
    delay["stamp"] = utils::to_string(log_line.date.value().timeStamp());

683
    XmlSubNode submessage(forwarded, "message");
684 685 686 687
    submessage["xmlns"] = CLIENT_NS;
    submessage["from"] = from + "/" + log_line.nick.value();
    submessage["type"] = "groupchat";

688
    XmlSubNode body(submessage, "body");
689
    body.set_inner(log_line.body.value());
690 691
  }
  this->send_stanza(message);
692 693
}

694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737
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)
    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)
    return false;

  Jid requester(from);
  if (!handle_irc_channel_configuration_form(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;
}

738 739
#endif

740 741
Bridge* BiboumiComponent::get_user_bridge(const std::string& user_jid)
{
742
  auto bare_jid = Jid{user_jid}.bare();
743 744
  try
    {
745
      return this->bridges.at(bare_jid).get();
746 747 748
    }
  catch (const std::out_of_range& exception)
    {
749
      return this->bridges.emplace(bare_jid, std::make_unique<Bridge>(bare_jid, *this, this->poller)).first->second.get();
750 751 752
    }
}

753
Bridge* BiboumiComponent::find_user_bridge(const std::string& full_jid)
754
{
755
  auto bare_jid = Jid{full_jid}.bare();
756 757
  try
    {
758
      return this->bridges.at(bare_jid).get();
759 760 761 762 763 764 765
    }
  catch (const std::out_of_range& exception)
    {
      return nullptr;
    }
}

louiz’'s avatar
louiz’ committed
766
std::vector<Bridge*> BiboumiComponent::get_bridges() const
767
{
louiz’'s avatar
louiz’ committed
768
  std::vector<Bridge*> res;
769 770
  for (const auto& bridge: this->bridges)
    res.push_back(bridge.second.get());
771 772 773 774 775 776
  return res;
}

void BiboumiComponent::send_self_disco_info(const std::string& id, const std::string& jid_to)
{
  Stanza iq("iq");
777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793
  {
    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})
      {
        XmlSubNode feature(query, "feature");
        feature["var"] = ns;
      }
  }
794 795 796
  this->send_stanza(iq);
}

797 798 799 800
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");
801 802 803 804 805 806 807 808 809 810 811
  {
    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 "s + from.local + " over Biboumi";
812
    for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS})
813 814 815 816 817
      {
        XmlSubNode feature(query, "feature");
        feature["var"] = ns;
      }
  }
818 819 820
  this->send_stanza(iq);
}

821
void BiboumiComponent::send_irc_channel_muc_traffic_info(const std::string& id, const std::string& jid_to, const std::string& jid_from)
louiz’'s avatar
louiz’ committed
822 823
{
  Stanza iq("iq");
824 825 826 827 828 829 830 831 832 833 834 835
  {
    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/>
  }
louiz’'s avatar
louiz’ committed
836 837 838
  this->send_stanza(iq);
}

louiz’'s avatar
louiz’ committed
839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863
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"] = "IRC channel "s + iid.get_local() + " from server " + iid.get_server() + " over biboumi";
    for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS})
      {
        XmlSubNode feature(query, "feature");
        feature["var"] = ns;
      }
  }
  this->send_stanza(iq);
}

864
void BiboumiComponent::send_ping_request(const std::string& from,
865 866
                                         const std::string& jid_to,
                                         const std::string& id)
867 868
{
  Stanza iq("iq");
869 870 871 872 873 874 875 876
  {
    iq["type"] = "get";
    iq["id"] = id;
    iq["from"] = from + "@" + this->served_hostname;
    iq["to"] = jid_to;
    XmlSubNode ping(iq, "ping");
    ping["xmlns"] = PING_NS;
  }
877 878 879 880 881 882 883 884 885
  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");
886
          return;
887
        }
888 889 890 891 892 893
      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))))
894
        bridge->send_irc_ping_result({from, bridge}, id);
895 896 897 898
    };
  this->waiting_iq[id] = result_cb;
}

899 900 901 902 903
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)
904 905
{
  Stanza iq("iq");
906 907 908 909 910 911 912
  {
    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;
913 914

    for (auto it = begin; it != end; ++it)
915 916
      {
        XmlSubNode item(query, "item");
917
        item["jid"] = it->channel + "@" + this->served_hostname;
918
      }
919

920 921 922 923
    if ((rs_info.max >= 0 || !rs_info.after.empty() || !rs_info.before.empty()))
      {
        XmlSubNode set_node(query, "set");
        set_node["xmlns"] = RSM_NS;
924

925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942
        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()));
          }
      }
  }
943 944
  this->send_stanza(iq);
}
louiz’'s avatar
louiz’ committed
945 946 947 948 949 950

void BiboumiComponent::send_invitation(const std::string& room_target,
                                       const std::string& jid_to,
                                       const std::string& author_nick)
{
  Stanza message("message");
951 952 953 954 955 956 957 958 959 960 961
  {
    message["from"] = room_target + "@" + this->served_hostname;
    message["to"] = jid_to;
    XmlSubNode x(message, "x");
    x["xmlns"] = MUC_USER_NS;
    XmlSubNode invite(x, "invite");
    if (author_nick.empty())
      invite["from"] = room_target + "@" + this->served_hostname;
    else
      invite["from"] = room_target + "@" + this->served_hostname + "/" + author_nick;
  }
louiz’'s avatar
louiz’ committed
962 963
  this->send_stanza(message);
}
louiz’'s avatar
louiz’ committed
964 965 966 967 968 969 970 971 972 973 974 975 976

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)
{
977
  Stanza presence("presence");
louiz’'s avatar
louiz’ committed
978 979 980 981 982 983
  presence["from"] = from;
  presence["to"] = to;
  presence["id"] = this->next_id();
  presence["type"] = "subscribe";
  this->send_stanza(presence);
}