bridge.cpp 47.8 KB
Newer Older
1
#include <bridge/bridge.hpp>
louiz’'s avatar
louiz’ committed
2
#include <utility>
3
#include <xmpp/biboumi_component.hpp>
4
#include <network/poller.hpp>
5
#include <utils/empty_if_fixed_server.hpp>
6
#include <utils/encoding.hpp>
7
#include <utils/tolower.hpp>
louiz’'s avatar
louiz’ committed
8
#include <utils/uuid.hpp>
louiz’'s avatar
louiz’ committed
9
#include <logger/logger.hpp>
10
#include <utils/revstr.hpp>
louiz’'s avatar
louiz’ committed
11
#include <utils/split.hpp>
12
#include <xmpp/jid.hpp>
13
#include <database/database.hpp>
14
#include "result_set_management.hpp"
louiz’'s avatar
louiz’ committed
15
#include <algorithm>
louiz’'s avatar
louiz’ committed
16

louiz’'s avatar
louiz’ committed
17 18
using namespace std::string_literals;

19 20
static const char* action_prefix = "\01ACTION ";

21 22 23 24 25

static std::string in_encoding_for(const Bridge& bridge, const Iid& iid)
{
#ifdef USE_DATABASE
  const auto jid = bridge.get_bare_jid();
26
  return Database::get_encoding_in(jid, iid.get_server(), iid.get_local());
27
#else
28 29
  (void)bridge;
  (void)iid;
30
  return {"ISO-8859-1"};
31
#endif
32 33
}

louiz’'s avatar
louiz’ committed
34
Bridge::Bridge(std::string user_jid, BiboumiComponent& xmpp, std::shared_ptr<Poller>& poller):
35
 user_jid(std::move(user_jid)),
36 37 38
  xmpp(xmpp),
  poller(poller)
{
39 40
#ifdef USE_DATABASE
  const auto options = Database::get_global_options(this->user_jid);
41
  this->set_record_history(options.col<Database::RecordHistory>());
42
#endif
43 44
}

45
/**
46 47
 * Return the role and affiliation, corresponding to the given irc mode
 */
48 49
static std::tuple<std::string, std::string> get_role_affiliation_from_irc_mode(const char mode)
{
50 51
  if (mode == 'a' || mode == 'q'){
    return std::make_tuple("moderator", "owner");}
52 53 54 55 56 57 58 59 60 61
  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");
}

62
void Bridge::shutdown(const std::string& exit_message)
louiz’'s avatar
louiz’ committed
63
{
louiz’'s avatar
louiz’ committed
64
  for (auto& pair: this->irc_clients)
louiz’'s avatar
louiz’ committed
65
  {
louiz’'s avatar
louiz’ committed
66
    pair.second->send_quit_command(exit_message);
louiz’'s avatar
louiz’ committed
67 68 69
  }
}

70 71 72 73 74 75 76 77 78 79 80 81 82 83
void Bridge::remove_resource(const std::string& resource,
                             const std::string& part_message)
{
  const auto resources_in_chan_copy = this->resources_in_chan;
  for (const auto& chan_pair: resources_in_chan_copy)
  {
    const ChannelKey& channel_key = chan_pair.first;
    const std::set<Resource>& resources = chan_pair.second;
    if (resources.count(resource))
      this->leave_irc_channel({std::get<0>(channel_key), std::get<1>(channel_key), {}},
                              part_message, resource);
  }
}

louiz’'s avatar
louiz’ committed
84 85 86 87 88 89
void Bridge::clean()
{
  auto it = this->irc_clients.begin();
  while (it != this->irc_clients.end())
  {
    IrcClient* client = it->second.get();
louiz’'s avatar
louiz’ committed
90 91
    if (!client->is_connected() && !client->is_connecting() &&
        !client->get_resolver().is_resolving())
louiz’'s avatar
louiz’ committed
92 93 94 95 96 97
      it = this->irc_clients.erase(it);
    else
      ++it;
  }
}

98 99 100 101 102
const std::string& Bridge::get_jid() const
{
  return this->user_jid;
}

louiz’'s avatar
louiz’ committed
103 104 105 106 107 108
std::string Bridge::get_bare_jid() const
{
  Jid jid(this->user_jid);
  return jid.local + "@" + jid.domain;
}

109
Xmpp::body Bridge::make_xmpp_body(const std::string& str, const std::string& encoding)
110 111 112 113 114
{
  std::string res;
  if (utils::is_valid_utf8(str.c_str()))
    res = str;
  else
115
    res = utils::convert_to_utf8(str, encoding.data());
116
  return irc_format_to_xhtmlim(res);
117 118
}

119
IrcClient* Bridge::make_irc_client(const std::string& hostname, const std::string& nickname)
120 121 122 123 124 125 126
{
  try
    {
      return this->irc_clients.at(hostname).get();
    }
  catch (const std::out_of_range& exception)
    {
127 128
      auto username = nickname;
      auto realname = nickname;
louiz’'s avatar
louiz’ committed
129
      Jid jid(this->user_jid);
130 131 132 133 134 135 136 137
      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,
louiz’'s avatar
louiz’ committed
138
                                                            realname, jid.domain,
139
                                                            *this));
140 141 142 143 144
      std::shared_ptr<IrcClient> irc = this->irc_clients.at(hostname);
      return irc.get();
    }
}

louiz’'s avatar
louiz’ committed
145
IrcClient* Bridge::get_irc_client(const std::string& hostname)
146 147 148 149 150 151 152 153 154 155 156
{
  try
    {
      return this->irc_clients.at(hostname).get();
    }
  catch (const std::out_of_range& exception)
    {
      throw IRCNotConnected(hostname);
    }
}

157
IrcClient* Bridge::find_irc_client(const std::string& hostname) const
louiz’'s avatar
louiz’ committed
158 159 160 161 162 163 164 165 166 167 168
{
  try
    {
      return this->irc_clients.at(hostname).get();
    }
  catch (const std::out_of_range& exception)
    {
      return nullptr;
    }
}

169
bool Bridge::join_irc_channel(const Iid& iid, std::string nickname, const std::string& password,
170
                              const std::string& resource, HistoryLimit history_limit)
171
{
louiz’'s avatar
louiz’ committed
172
  const auto& hostname = iid.get_server();
173 174 175 176 177
#ifdef USE_DATABASE
  auto soptions = Database::get_irc_server_options(this->get_bare_jid(), hostname);
  if (!soptions.col<Database::Nick>().empty())
    nickname = soptions.col<Database::Nick>();
#endif
178
  IrcClient* irc = this->make_irc_client(hostname, nickname);
179
  irc->history_limit = history_limit;
180 181 182 183
  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);
louiz’'s avatar
louiz’ committed
184
  if (irc->is_channel_joined(iid.get_local()) == false)
louiz’'s avatar
louiz’ committed
185
    {
186
      irc->send_join_command(iid.get_local(), password);
louiz’'s avatar
louiz’ committed
187
      return true;
188 189
    } else if (!res_in_chan) {
      this->generate_channel_join_for_resource(iid, resource);
louiz’'s avatar
louiz’ committed
190 191
    }
  return false;
192 193
}

louiz’'s avatar
louiz’ committed
194
void Bridge::send_channel_message(const Iid& iid, const std::string& body, std::string id)
louiz’'s avatar
louiz’ committed
195
{
196
  if (iid.get_server().empty())
louiz’'s avatar
louiz’ committed
197
    {
198
      for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
199 200 201 202 203
        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);
louiz’'s avatar
louiz’ committed
204 205
      return;
    }
louiz’'s avatar
louiz’ committed
206
  IrcClient* irc = this->get_irc_client(iid.get_server());
207

208 209 210 211 212 213 214 215 216 217 218
  // 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 ;
louiz’'s avatar
louiz’ committed
219
  bool first = true;
220
  for (const std::string& line: lines)
louiz’'s avatar
louiz’ committed
221
    {
louiz’'s avatar
louiz’ committed
222
      if (line.substr(0, 5) == "/mode")
223
        {
louiz’'s avatar
louiz’ committed
224
          std::vector<std::string> args = utils::split(line.substr(5), ' ', false);
louiz’'s avatar
louiz’ committed
225
          irc->send_mode_command(iid.get_local(), args);
226 227 228 229
          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 ")
louiz’'s avatar
louiz’ committed
230
        irc->send_channel_message(iid.get_local(), action_prefix + line.substr(4) + "\01");
231
      else
louiz’'s avatar
louiz’ committed
232
        irc->send_channel_message(iid.get_local(), line);
233

234
      std::string uuid;
235
#ifdef USE_DATABASE
236
      const auto xmpp_body = this->make_xmpp_body(line);
237
      if (this->record_history)
238
        uuid = Database::store_muc_message(this->get_bare_jid(), iid.get_local(), iid.get_server(), std::chrono::system_clock::now(),
239
                                    std::get<0>(xmpp_body), irc->get_own_nick());
240
#endif
241
      if (!first || id.empty())
louiz’'s avatar
louiz’ committed
242
        id = utils::gen_uuid();
243
      for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
244
        this->xmpp.send_muc_message(std::to_string(iid), irc->get_own_nick(), this->make_xmpp_body(line),
louiz’'s avatar
louiz’ committed
245 246
                                    this->user_jid + "/" + resource, uuid, id);
      first = false;
louiz’'s avatar
louiz’ committed
247
    }
louiz’'s avatar
louiz’ committed
248 249
}

250 251
void Bridge::forward_affiliation_role_change(const Iid& iid, const std::string& from,
                                             const std::string& nick,
252
                                             const std::string& affiliation,
253 254
                                             const std::string& role,
                                             const std::string& id)
255 256 257 258 259 260 261
{
  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)
262 263 264 265 266
    {
      this->xmpp.send_stanza_error("iq", from, std::to_string(iid), id, "cancel",
                                   "item-not-found", "no such nick", false);
      return;
    }
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
  // 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);
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358

  irc_responder_callback_t cb = [this, iid, irc, id, from, nick](const std::string& irc_hostname, const IrcMessage& message) -> bool
  {
    if (irc_hostname != iid.get_server())
      return false;

    if (message.command == "MODE" && message.arguments.size() >= 2)
      {
        const std::string& chan_name = message.arguments[0];
        if (chan_name != iid.get_local())
          return false;
        const std::string actor_nick = IrcUser{message.prefix}.nick;
        if (!irc || irc->get_own_nick() != actor_nick)
          return false;

        this->xmpp.send_iq_result(id, from, std::to_string(iid));
      }
    else if (message.command == "401" && message.arguments.size() >= 2)
        {
          const std::string target_later = message.arguments[1];
          if (target_later != nick)
            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", from, std::to_string(iid), 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", from, std::to_string(iid), id, "cancel", "not-allowed",
                                     error_message, false);
      }
    else if (message.command == "472" && message.arguments.size() >= 2)
      {
359
          std::string error_message = "Unknown mode: " + message.arguments[1];
360 361 362 363 364 365 366 367
          if (message.arguments.size() >= 3)
            error_message = message.arguments[2];
          this->xmpp.send_stanza_error("iq", from, std::to_string(iid), id, "cancel", "not-allowed",
                                        error_message, false);
      }
    return true;
  };
  this->add_waiting_irc(std::move(cb));
368 369
}

louiz’'s avatar
louiz’ committed
370
void Bridge::send_private_message(const Iid& iid, const std::string& body, const std::string& type)
louiz’'s avatar
louiz’ committed
371
{
louiz’'s avatar
louiz’ committed
372
  if (iid.get_local().empty() || iid.get_server().empty())
373
    {
374
      this->xmpp.send_stanza_error("message", this->user_jid, std::to_string(iid), "",
375 376 377 378
                                    "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);
379 380
      return;
    }
381
  IrcClient* irc = this->get_irc_client(iid.get_server());
382 383 384 385 386 387
  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 ")
louiz’'s avatar
louiz’ committed
388
        irc->send_private_message(iid.get_local(), action_prefix + line.substr(4) + "\01", type);
389
      else
louiz’'s avatar
louiz’ committed
390
        irc->send_private_message(iid.get_local(), line, type);
391
    }
louiz’'s avatar
louiz’ committed
392 393
}

louiz’'s avatar
louiz’ committed
394 395 396 397 398 399
void Bridge::send_raw_message(const std::string& hostname, const std::string& body)
{
  IrcClient* irc = this->get_irc_client(hostname);
  irc->send_raw(body);
}

400
void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, const std::string& resource)
louiz’'s avatar
louiz’ committed
401
{
louiz’'s avatar
louiz’ committed
402
  IrcClient* irc = this->get_irc_client(iid.get_server());
403 404 405 406
  const auto key = iid.to_tuple();
  if (!this->is_resource_in_chan(key, resource))
    return ;

louiz’'s avatar
louiz’ committed
407 408
  IrcChannel* channel = irc->get_channel(iid.get_local());

409 410
  const auto resources = this->number_of_resources_in_chan(key);
  if (resources == 1)
411
    {
412 413 414
      // Do not send a PART message if we actually are not in that channel
      // or if we already sent a PART but we are just waiting for the
      // acknowledgment from the server
louiz’'s avatar
louiz’ committed
415 416
      bool persistent = false;
#ifdef USE_DATABASE
417
      const auto goptions = Database::get_global_options(this->user_jid);
418
      if (goptions.col<Database::GlobalPersistent>())
419 420 421 422 423 424
        persistent = true;
      else
        {
          const auto coptions = Database::get_irc_channel_options_with_server_default(this->user_jid, iid.get_server(), iid.get_local());
          persistent = coptions.col<Database::Persistent>();
        }
louiz’'s avatar
louiz’ committed
425 426 427
#endif
      if (channel->joined && !channel->parting && !persistent)
        {
428
          irc->send_part_command(iid.get_local(), status_message);
louiz’'s avatar
louiz’ committed
429
        }
430
      else if (channel->joined)
louiz’'s avatar
louiz’ committed
431
        {
432
          this->send_muc_leave(iid, *channel->get_self(), "", true, true, resource, irc);
louiz’'s avatar
louiz’ committed
433
        }
434 435
      if (persistent)
        this->remove_resource_from_chan(key, resource);
436 437 438 439
      // 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());
    }
440 441
  else
    {
442
      if (channel && channel->joined)
443
        this->send_muc_leave(iid, *channel->get_self(),
444
                             "Biboumi note: " + std::to_string(resources - 1) + " resources are still in this channel.",
445
                             true, true, resource, irc);
louiz’'s avatar
louiz’ committed
446
      this->remove_resource_from_chan(key, resource);
louiz’'s avatar
louiz’ committed
447
    }
louiz’'s avatar
louiz’ committed
448 449 450
      if (this->number_of_channels_the_resource_is_in(iid.get_server(), resource) == 0)
        this->remove_resource_from_server(iid.get_server(), resource);

451
}
louiz’'s avatar
louiz’ committed
452

453
void Bridge::send_irc_nick_change(const Iid& iid, const std::string& new_nick, const std::string& requesting_resource)
louiz’'s avatar
louiz’ committed
454
{
455 456 457 458
  // We don’t change the nick if the presence was sent to a channel the resource is not in.
  auto res_in_chan = this->is_resource_in_chan(ChannelKey{iid.get_local(), iid.get_server()}, requesting_resource);
  if (!res_in_chan)
    return;
louiz’'s avatar
louiz’ committed
459
  IrcClient* irc = this->get_irc_client(iid.get_server());
460
  irc->send_nick_command(new_nick);
louiz’'s avatar
louiz’ committed
461 462
}

463 464
void Bridge::send_irc_channel_list_request(const Iid& iid, const std::string& iq_id, const std::string& to_jid,
                                           ResultSetInfo rs_info)
465
{
466
  auto& list = this->channel_list_cache[iid.get_server()];
467

468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487
  // We fetch the list from the IRC server only if we have a complete
  // cached list that needs to be invalidated (that is, when the request
  // doesn’t have a after or before, or when the list is empty).
  // If the list is not complete, this means that a request is already
  // ongoing, so we just need to add the callback.
  // By default the list is complete and empty.
  if (list.complete &&
      (list.channels.empty() || (rs_info.after.empty() && rs_info.before.empty())))
    {
      IrcClient* irc = this->get_irc_client(iid.get_server());
      irc->send_list_command();

      // Add a callback that will populate our list
      list.channels.clear();
      list.complete = false;
      irc_responder_callback_t cb = [this, iid](const std::string& irc_hostname,
                                                const IrcMessage& message) -> bool
      {
        if (irc_hostname != iid.get_server())
          return false;
488

489
        auto& list = this->channel_list_cache[iid.get_server()];
490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512

        if (message.command == "263" || message.command == "RPL_TRYAGAIN" || message.command == "ERR_TOOMANYMATCHES"
            || message.command == "ERR_NOSUCHSERVER")
          {
            list.complete = true;
            return true;
          }
        else if (message.command == "322" || message.command == "RPL_LIST")
          { // Add element to list
            if (message.arguments.size() == 4)
              {
                list.channels.emplace_back(message.arguments[1] + utils::empty_if_fixed_server("%" + iid.get_server()),
                                           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
            list.complete = true;
            return true;
          }
        return false;
      };
513

514 515 516 517 518 519 520
      this->add_waiting_irc(std::move(cb));
    }

  // If the list is complete, we immediately send the answer.
  // Otherwise, we install a callback, that will populate our list and send
  // the answer when we can.
  if (list.complete)
521
    {
522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554
      this->send_matching_channel_list(list, rs_info, iq_id, to_jid, std::to_string(iid));
    }
  else
    {
      // Add a callback to answer the request as soon as we can
      irc_responder_callback_t cb = [this, iid, iq_id, to_jid,
                                     rs_info=std::move(rs_info)](const std::string& irc_hostname,
                                                                 const IrcMessage& message) -> 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")
          {
            auto& list = channel_list_cache[iid.get_server()];
            const auto res = this->send_matching_channel_list(list, rs_info, iq_id, to_jid, std::to_string(iid));
            return res;
          }
        else if (message.command == "323" || message.command == "RPL_LISTEND")
          { // Send the iq response with the content of the list
            auto& list = channel_list_cache[iid.get_server()];
            this->send_matching_channel_list(list, rs_info, iq_id, to_jid, std::to_string(iid));
            return true;
          }
555
        return false;
556 557 558 559 560 561 562 563 564 565
      };

      this->add_waiting_irc(std::move(cb));
    }
}

bool Bridge::send_matching_channel_list(const ChannelList& channel_list, const ResultSetInfo& rs_info,
                                        const std::string& id, const std::string& to_jid, const std::string& from)
{
  auto begin = channel_list.channels.begin();
566
  auto end = channel_list.channels.end();
567 568 569 570 571 572 573 574 575 576 577 578 579 580 581
  if (channel_list.complete)
    {
      begin = std::find_if(channel_list.channels.begin(), channel_list.channels.end(), [this, &rs_info](const ListElement& element)
      {
        return rs_info.after == element.channel + "@" + this->xmpp.get_served_hostname();
      });
      if (begin == channel_list.channels.end())
        begin = channel_list.channels.begin();
      else
        begin = std::next(begin);
      end = std::find_if(channel_list.channels.begin(), channel_list.channels.end(), [this, &rs_info](const ListElement& element)
      {
        return rs_info.before == element.channel + "@" + this->xmpp.get_served_hostname();
      });
      if (rs_info.max >= 0)
582
        {
583 584
          if (std::distance(begin, end) >= rs_info.max)
            end = begin + rs_info.max;
585
        }
586 587 588 589 590 591 592 593 594 595 596 597 598 599
    }
  else
    {
      if (rs_info.after.empty() && rs_info.before.empty() && rs_info.max < 0)
        return false;
      if (!rs_info.after.empty())
        {
          begin = std::find_if(channel_list.channels.begin(), channel_list.channels.end(), [this, &rs_info](const ListElement& element)
          {
            return rs_info.after == element.channel + "@" + this->xmpp.get_served_hostname();
          });
          if (begin == channel_list.channels.end())
            return false;
          begin = std::next(begin);
600
        }
601 602 603 604 605 606 607 608
        if (!rs_info.before.empty())
        {
          end = std::find_if(channel_list.channels.begin(), channel_list.channels.end(), [this, &rs_info](const ListElement& element)
          {
            return rs_info.before == element.channel + "@" + this->xmpp.get_served_hostname();
          });
          if (end == channel_list.channels.end())
            return false;
609
        }
610 611 612 613 614 615 616 617 618 619
      if (rs_info.max >= 0)
        {
          if (std::distance(begin, end) < rs_info.max)
            return false;
          else
            end = begin + rs_info.max;
        }
    }
  this->xmpp.send_iq_room_list_result(id, to_jid, from, channel_list, begin, end, rs_info);
  return true;
620 621
}

622 623
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)
624
{
louiz’'s avatar
louiz’ committed
625
  IrcClient* irc = this->get_irc_client(iid.get_server());
626 627 628 629

  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
630
    {
631 632 633 634 635 636 637
      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())
638
            return false;
639
          this->xmpp.send_iq_result(iq_id, to_jid, std::to_string(iid));
640 641 642 643 644 645 646 647 648
        }
      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];
649
          this->xmpp.send_stanza_error("iq", to_jid, std::to_string(iid), iq_id, "cancel", "item-not-found",
650 651 652 653 654 655 656 657 658 659
                                        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];
660
          this->xmpp.send_stanza_error("iq", to_jid, std::to_string(iid), iq_id, "cancel", "not-allowed",
661 662 663 664 665
                                        error_message, false);
        }
      return true;
    };
  this->add_waiting_irc(std::move(cb));
666 667
}

louiz’'s avatar
louiz’ committed
668
void Bridge::set_channel_topic(const Iid& iid, std::string subject)
669
{
louiz’'s avatar
louiz’ committed
670
  IrcClient* irc = this->get_irc_client(iid.get_server());
louiz’'s avatar
louiz’ committed
671 672 673
  std::string::size_type pos{0};
  while ((pos = subject.find('\n', pos)) != std::string::npos)
    subject[pos] = ' ';
674
  irc->send_topic_command(iid.get_local(), subject);
675 676
}

louiz’'s avatar
louiz’ committed
677 678 679 680
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);

681
  this->send_private_message(iid, "\01VERSION " + result + "\01", "NOTICE");
louiz’'s avatar
louiz’ committed
682 683
}

684 685
void Bridge::send_irc_ping_result(const Iid& iid, const std::string& id)
{
686
  this->send_private_message(iid, "\01PING " + utils::revstr(id) + "\01", "NOTICE");
687 688 689 690 691 692
}

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)
{
693
  Iid iid(nick, irc_hostname, Iid::Type::User);
694 695
  this->send_private_message(iid, "\01PING " + iq_id + "\01");

louiz’'s avatar
louiz’ committed
696 697
  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
698
    {
louiz’'s avatar
louiz’ committed
699
      if (irc_hostname != hostname || message.arguments.size() < 2)
700 701 702
        return false;
      IrcUser user(message.prefix);
      const std::string body = message.arguments[1];
louiz’'s avatar
louiz’ committed
703 704
      if (message.command == "NOTICE" && utils::tolower(user.nick) == nick
          && body.substr(0, 6) == "\01PING ")
705
        {
706
          const std::string id = body.substr(6, body.size() - 7);
707 708
          if (id != iq_id)
            return false;
709
          this->xmpp.send_iq_result_full_jid(iq_id, to_jid, from_jid);
710 711
          return true;
        }
louiz’'s avatar
louiz’ committed
712
      if (message.command == "401" && message.arguments[1] == nick)
713 714 715 716
        {
          std::string error_message = "No such nick";
          if (message.arguments.size() >= 3)
            error_message = message.arguments[2];
717
          this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "service-unavailable",
718 719 720 721 722 723 724 725 726
                                        error_message, true);
          return true;
        }

      return false;
    };
  this->add_waiting_irc(std::move(cb));
}

727 728 729 730
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)
{
731
  Jid from(to_jid);
732 733
  IrcClient* irc = this->get_irc_client(iid.get_server());
  IrcChannel* chan = irc->get_channel(iid.get_local());
734
  if (!chan->joined || !this->is_resource_in_chan(iid.to_tuple(), from.resource))
735
    {
736
      this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "not-allowed",
737 738 739 740 741
                                    "", true);
      return;
    }
  if (chan->get_self()->nick != nick && !chan->find_user(nick))
    {
742
      this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "item-not-found",
743 744 745 746 747 748 749 750
                                    "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);
}

751 752 753 754
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);
755
  if (irc_hostname.empty() || this->find_irc_client(irc_hostname))
756
    this->xmpp.send_iq_result(iq_id, to_jid, jid.local);
757
  else
758
    this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "service-unavailable",
759 760 761
                                  "", true);
}

762
void Bridge::send_irc_invitation(const Iid& iid, const std::string& to)
763 764 765 766 767 768 769 770 771 772 773 774 775 776 777
{
  IrcClient* irc = this->get_irc_client(iid.get_server());
  Jid to_jid(to);
  std::string target_nick;
  // Many ways to address a nick:
  // A jid (ANY jid…) with a resource
  if (!to_jid.resource.empty())
    target_nick = to_jid.resource;
  else if (!to_jid.local.empty()) // A jid with a iid with a local part
    target_nick = Iid(to_jid.local, {}).get_local();
  else
    target_nick = to; // Not a jid, just the nick
  irc->send_invitation(iid.get_local(), target_nick);
}

778 779 780 781
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)
{
782
  Iid iid(target, irc_hostname, Iid::Type::User);
783 784 785
  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
louiz’'s avatar
louiz’ committed
786 787
  irc_responder_callback_t cb = [this, target, iq_id, to_jid, irc_hostname, from_jid]
          (const std::string& hostname, const IrcMessage& message) -> bool
788
    {
789 790 791
      if (irc_hostname != hostname)
        return false;
      IrcUser user(message.prefix);
792
      if (message.command == "NOTICE" && utils::tolower(user.nick) == utils::tolower(target) &&
793 794 795 796
          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);
797
          this->xmpp.send_version(iq_id, to_jid, from_jid, version);
798 799 800 801 802 803 804 805
          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];
806
          this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "item-not-found",
807 808 809 810
                                        error_message, true);
          return true;
        }
      return false;
811 812
    };
  this->add_waiting_irc(std::move(cb));
813 814
}

louiz’'s avatar
louiz’ committed
815
void Bridge::send_message(const Iid& iid, const std::string& nick, const std::string& body, const bool muc)
louiz’'s avatar
louiz’ committed
816
{
817
  const auto encoding = in_encoding_for(*this, iid);
louiz’'s avatar
louiz’ committed
818
  if (muc)
819
    {
820
#ifdef USE_DATABASE
821
      const auto xmpp_body = this->make_xmpp_body(body, encoding);
822
      if (!nick.empty() && this->record_history)
823
        Database::store_muc_message(this->get_bare_jid(), iid.get_local(), iid.get_server(), std::chrono::system_clock::now(),
824
                                    std::get<0>(xmpp_body), nick);
825
#endif
826
      for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
827
        {
828
          this->xmpp.send_muc_message(std::to_string(iid), nick, this->make_xmpp_body(body, encoding),
louiz’'s avatar
louiz’ committed
829
                                      this->user_jid + "/" + resource, {}, utils::gen_uuid());
830

831 832
        }
    }
louiz’'s avatar
louiz’ committed
833
  else
834
    {
835
      const auto it = this->preferred_user_from.find(iid.get_local());
836 837
      if (it != this->preferred_user_from.end())
        {
838
          const auto chan_name = Iid(Jid(it->second).local, {}).get_local();
839 840
          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),
841 842
                                    this->user_jid + "/"
                                    + resource, "chat", true, true, true);
843
        }
844
      else
845
        {
846 847
          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),
848
                                    this->user_jid + "/" + resource, "chat", false, true);
849
        }
850
    }
louiz’'s avatar
louiz’ committed
851 852
}

853 854 855
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)
856
{
857
  this->xmpp.send_presence_error(std::to_string(iid), nick, this->user_jid, type, condition, error_code, text);
858 859
}

860
void Bridge::send_muc_leave(const Iid& iid, const IrcUser& user,
861
                            const std::string& message, const bool self,
862
                            const bool user_requested,
863 864
                            const std::string& resource,
                            const IrcClient* client)
louiz’'s avatar
louiz’ committed
865
{
866 867 868 869
  std::string affiliation;
  std::string role;
  std::tie(role, affiliation) = get_role_affiliation_from_irc_mode(user.get_most_significant_mode(client->get_sorted_user_modes()));

870
  if (!resource.empty())
871 872
    this->xmpp.send_muc_leave(std::to_string(iid), user.nick, this->make_xmpp_body(message),
                              this->user_jid + "/" + resource, self, user_requested, affiliation, role);
873
  else
louiz’'s avatar
louiz’ committed
874 875
    {
      for (const auto &res: this->resources_in_chan[iid.to_tuple()])
876 877
        this->xmpp.send_muc_leave(std::to_string(iid), user.nick, this->make_xmpp_body(message),
                                  this->user_jid + "/" + res, self, user_requested, affiliation, role);
878
      if (self)
879 880 881 882 883 884 885 886 887 888 889 890 891
        {
          // Copy the resources currently in that channel
          const auto resources_in_chan = this->resources_in_chan[iid.to_tuple()];

          this->remove_all_resources_from_chan(iid.to_tuple());

          // Now, for each resource that was in that channel, remove it from the server if it’s
          // not in any other channel
          for (const auto& r: resources_in_chan)
          if (this->number_of_channels_the_resource_is_in(iid.get_server(), r) == 0)
            this->remove_resource_from_server(iid.get_server(), r);

        }
louiz’'s avatar
louiz’ committed
892 893

    }
894
  IrcClient* irc = this->find_irc_client(iid.get_server());
895
  if (self && irc && irc->number_of_joined_channels() == 0)
896
    irc->send_quit_command("");
louiz’'s avatar
louiz’ committed
897 898
}

899 900 901 902 903
void Bridge::send_nick_change(Iid&& iid,
                              const std::string& old_nick,
                              const std::string& new_nick,
                              const char user_mode,
                              const bool self)
louiz’'s avatar
louiz’ committed
904
{
905 906 907 908
  std::string affiliation;
  std::string role;
  std::tie(role, affiliation) = get_role_affiliation_from_irc_mode(user_mode);

909
  for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
910 911
    this->xmpp.send_nick_change(std::to_string(iid),
                                old_nick, new_nick, affiliation, role, this->user_jid + "/" + resource, self);
louiz’'s avatar
louiz’ committed
912 913
}

914 915
void Bridge::send_xmpp_message(const std::string& from, const std::string& author, const std::string& msg)
{
louiz’'s avatar
louiz’ committed
916 917
  std::string body;
  if (!author.empty())
918 919
    {
      IrcUser user(author);
920
      body = "\u000303" + user.nick + (user.host.empty()?
louiz’'s avatar
louiz’ committed
921 922
                                        "\u0003: ":
                                        (" (\u000310" + user.host + "\u000303)\u0003: ")) + msg;
923
    }
louiz’'s avatar
louiz’ committed
924 925
  else
    body = msg;
926

927
  const auto encoding = in_encoding_for(*this, {from, this});
928 929
  for (const auto& resource: this->resources_in_server[from])
    {
930
      if (Config::get("fixed_irc_server", "").empty())
931
        this->xmpp.send_message(from, this->make_xmpp_body(body, encoding), this->user_jid + "/" + resource, "chat", false, true);
932
      else
933
        this->xmpp.send_message("", this->make_xmpp_body(body, encoding), this->user_jid + "/" + resource, "chat", false, true);
934
    }
935 936
}

937 938 939
void Bridge::send_user_join(const std::string& hostname, const std::string& chan_name,
                            const IrcUser* user, const char user_mode, const bool self)
{
940 941 942 943 944 945 946 947
  const auto resources = this->resources_in_chan[ChannelKey{chan_name, hostname}];
  if (self && resources.empty())
    { // This was a forced join: no client ever asked to join this room,
      // but the server tells us we are in that room anyway.  XMPP can’t
      // do that, so we invite all the resources to join that channel.
      const Iid iid(chan_name, hostname, Iid::Type::Channel);
      this->send_xmpp_invitation(iid, "");
    }
948
  else
949 950 951 952
    {
      for (const auto& resource: resources)
        this->send_user_join(hostname, chan_name, user, user_mode, self, resource);
    }
953 954 955 956 957
}

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)
958
{
959 960 961 962
  std::string affiliation;
  std::string role;
  std::tie(role, affiliation) = get_role_affiliation_from_irc_mode(user_mode);

963 964 965 966
  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,
967
                            affiliation, role, this->user_jid + "/" + resource, self);
968 969
}

louiz’'s avatar
louiz’ committed
970 971
void Bridge::send_topic(const std::string& hostname, const std::string& chan_name, const std::string& topic,
                        const std::string& who)
972
{
973 974
  for (const auto& resource: this->resources_in_chan[ChannelKey{chan_name, hostname}])
    {
975
      this->send_topic(hostname, chan_name, topic, who, resource);
976
    }
977
}
louiz’'s avatar
louiz’ committed
978