irc_client.cpp 32.3 KB
Newer Older
1
#include <utils/timed_events.hpp>
2
#include <database/database.hpp>
3
#include <irc/irc_message.hpp>
4 5 6 7
#include <irc/irc_client.hpp>
#include <bridge/bridge.hpp>
#include <irc/irc_user.hpp>

louiz’'s avatar
louiz’ committed
8
#include <logger/logger.hpp>
9
#include <config/config.hpp>
louiz’'s avatar
louiz’ committed
10
#include <utils/tolower.hpp>
11
#include <utils/split.hpp>
12

13
#include <sstream>
14 15 16
#include <iostream>
#include <stdexcept>

17
#include <chrono>
louiz’'s avatar
louiz’ committed
18
#include <string>
19

20
#include "biboumi.h"
21
#include "louloulibs.h"
louiz’'s avatar
louiz’ committed
22

louiz’'s avatar
louiz’ committed
23
using namespace std::string_literals;
24
using namespace std::chrono_literals;
louiz’'s avatar
louiz’ committed
25

26

27 28
IrcClient::IrcClient(std::shared_ptr<Poller> poller, const std::string& hostname,
                     const std::string& nickname, const std::string& username,
louiz’'s avatar
louiz’ committed
29 30
                     const std::string& realname, const std::string& user_hostname,
                     Bridge* bridge):
31
  TCPSocketHandler(poller),
32
  hostname(hostname),
louiz’'s avatar
louiz’ committed
33
  user_hostname(user_hostname),
34
  username(username),
35 36
  realname(realname),
  current_nick(nickname),
louiz’'s avatar
louiz’ committed
37
  bridge(bridge),
louiz’'s avatar
louiz’ committed
38
  welcomed(false),
39 40
  chanmodes({"", "", "", ""}),
  chantypes({'#', '&'})
41
{
42 43 44 45 46 47 48 49
  this->dummy_channel.topic = "This is a virtual channel provided for "
                              "convenience by biboumi, it is not connected "
                              "to any actual IRC channel of the server '" + this->hostname +
                              "', and sending messages in it has no effect. "
                              "Its main goal is to keep the connection to the IRC server "
                              "alive without having to join a real channel of that server. "
                              "To disconnect from the IRC server, leave this room and all "
                              "other IRC channels of that server.";
50 51 52 53 54 55 56 57 58 59 60 61 62
#ifdef USE_DATABASE
  auto options = Database::get_irc_server_options(this->bridge->get_bare_jid(),
                                                  this->get_hostname());
  std::vector<std::string> ports = utils::split(options.ports, ';', false);
  for (auto it = ports.rbegin(); it != ports.rend(); ++it)
    this->ports_to_try.emplace(*it, false);
# ifdef BOTAN_FOUND
  ports = utils::split(options.tlsPorts, ';', false);
  for (auto it = ports.rbegin(); it != ports.rend(); ++it)
    this->ports_to_try.emplace(*it, true);
# endif // BOTAN_FOUND

#else  // not USE_DATABASE
louiz’'s avatar
louiz’ committed
63
  this->ports_to_try.emplace("6667", false); // standard non-encrypted port
64
# ifdef BOTAN_FOUND
louiz’'s avatar
louiz’ committed
65 66
  this->ports_to_try.emplace("6670", true);  // non-standard but I want it for some servers
  this->ports_to_try.emplace("6697", true);  // standard encrypted port
67 68
# endif // BOTAN_FOUND
#endif // USE_DATABASE
69 70 71 72
}

IrcClient::~IrcClient()
{
73 74 75
  // This event may or may not exist (if we never got connected, it
  // doesn't), but it's ok
  TimedEventsManager::instance().cancel("PING"s + this->hostname + this->bridge->get_jid());
76 77
}

78 79
void IrcClient::start()
{
80 81
  if (this->is_connecting() || this->is_connected())
    return;
louiz’'s avatar
louiz’ committed
82 83 84 85
  std::string port;
  bool tls;
  std::tie(port, tls) = this->ports_to_try.top();
  this->ports_to_try.pop();
louiz’'s avatar
louiz’ committed
86
  this->bridge->send_xmpp_message(this->hostname, "", "Connecting to "s +
louiz’'s avatar
louiz’ committed
87 88
                                  this->hostname + ":" + port + " (" +
                                  (tls ? "encrypted" : "not encrypted") + ")");
louiz’'s avatar
louiz’ committed
89 90 91

  this->bind_addr = Config::get("outgoing_bind", "");

louiz’'s avatar
louiz’ committed
92
  this->connect(this->hostname, port, tls);
93 94 95 96 97
}

void IrcClient::on_connection_failed(const std::string& reason)
{
  this->bridge->send_xmpp_message(this->hostname, "",
louiz’'s avatar
louiz’ committed
98
                                  "Connection failed: "s + reason);
99 100 101 102 103

  if (this->hostname_resolution_failed)
    while (!this->ports_to_try.empty())
      this->ports_to_try.pop();

louiz’'s avatar
louiz’ committed
104
  if (this->ports_to_try.empty())
105
    {
louiz’'s avatar
louiz’ committed
106
      // Send an error message for all room that the user wanted to join
107
      for (const auto& tuple: this->channels_to_join)
louiz’'s avatar
louiz’ committed
108
        {
109
          Iid iid(std::get<0>(tuple) + "%" + this->hostname);
110 111 112
          this->bridge->send_presence_error(iid, this->current_nick,
                                            "cancel", "item-not-found",
                                            "", reason);
louiz’'s avatar
louiz’ committed
113
        }
114
    }
louiz’'s avatar
louiz’ committed
115 116
  else                          // try the next port
    this->start();
117 118
}

louiz’'s avatar
louiz’ committed
119 120
void IrcClient::on_connected()
{
louiz’'s avatar
louiz’ committed
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
  const auto webirc_password = Config::get("webirc_password", "");
  static std::string resolved_ip;

  if (!webirc_password.empty())
    {
      if (!resolved_ip.empty())
        this->send_webirc_command(webirc_password, resolved_ip);
      else
        {  // Start resolving the hostname of the user, and call
           // on_connected again when it’s done
          this->dns_resolver.resolve(this->user_hostname, "5222",
                                     [this](const struct addrinfo* addr)
                                     {
                                       resolved_ip = addr_to_string(addr);
                                       // Only continue the process if we
                                       // didn’t get connected while we were
                                       // resolving
                                       if (this->is_connected())
                                         this->on_connected();
                                     },
                                     [this](const char* error_msg)
                                     {
                                       if (this->is_connected())
                                         {
                                           this->on_connection_close("Could not resolve hostname "s + this->user_hostname +
                                                                     ": " + error_msg);
                                           this->send_quit_command("");
                                         }
                                     });
          return;
        }
    }

154 155 156 157 158 159
#ifdef USE_DATABASE
  auto options = Database::get_irc_server_options(this->bridge->get_bare_jid(),
                                                  this->get_hostname());
  if (!options.pass.value().empty())
    this->send_pass_command(options.pass.value());
#endif
160

161
  this->send_nick_command(this->current_nick);
162

163
#ifdef USE_DATABASE
164 165 166 167 168 169 170 171
  if (Config::get("realname_customization", "true") == "true")
    {
      if (!options.username.value().empty())
        this->username = options.username.value();
      if (!options.realname.value().empty())
        this->realname = options.realname.value();
      this->send_user_command(username, realname);
    }
172 173 174
  else
    this->send_user_command(this->username, this->realname);
#else
175
  this->send_user_command(this->username, this->realname);
176
#endif
louiz’'s avatar
louiz’ committed
177
  this->send_gateway_message("Connected to IRC server"s + (this->use_tls ? " (encrypted)": "") + ".");
178
  this->send_pending_data();
louiz’'s avatar
louiz’ committed
179 180
}

181
void IrcClient::on_connection_close(const std::string& error_msg)
182
{
183
  std::string message = "Connection closed";
184 185
  if (!error_msg.empty())
    message += ": " + error_msg;
186 187
  else
    message += ".";
188 189
  const IrcMessage error{"ERROR", {message}};
  this->on_error(error);
190
  log_warning(message);
191 192
}

louiz’'s avatar
louiz’ committed
193
IrcChannel* IrcClient::get_channel(const std::string& n)
194
{
louiz’'s avatar
louiz’ committed
195
  if (n.empty())
196
    return &this->dummy_channel;
louiz’'s avatar
louiz’ committed
197
  const std::string name = utils::tolower(n);
198 199 200 201 202 203 204 205 206 207 208
  try
    {
      return this->channels.at(name).get();
    }
  catch (const std::out_of_range& exception)
    {
      this->channels.emplace(name, std::make_unique<IrcChannel>());
      return this->channels.at(name).get();
    }
}

louiz’'s avatar
louiz’ committed
209 210
bool IrcClient::is_channel_joined(const std::string& name)
{
louiz’'s avatar
louiz’ committed
211 212
  IrcChannel* channel = this->get_channel(name);
  return channel->joined;
louiz’'s avatar
louiz’ committed
213 214
}

louiz’'s avatar
louiz’ committed
215 216 217 218 219
std::string IrcClient::get_own_nick() const
{
  return this->current_nick;
}

220
void IrcClient::parse_in_buffer(const size_t)
221
{
louiz’'s avatar
louiz’ committed
222 223 224 225 226 227 228
  while (true)
    {
      auto pos = this->in_buf.find("\r\n");
      if (pos == std::string::npos)
        break ;
      IrcMessage message(this->in_buf.substr(0, pos));
      this->in_buf = this->in_buf.substr(pos + 2, std::string::npos);
229
      log_debug("IRC RECEIVING: (" << this->get_hostname() << ") " << message);
230 231 232

      // Call the standard callback (if any), associated with the command
      // name that we just received.
233 234
      auto it = irc_callbacks.find(message.command);
      if (it != irc_callbacks.end())
235
        {
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
          const auto& limits = it->second.second;
          // Check that the Message is well formed before actually calling
          // the callback. limits.first is the min number of arguments,
          // second is the max
          if (message.arguments.size() < limits.first ||
              (limits.second > 0 && message.arguments.size() > limits.second))
            log_warning("Invalid number of arguments for IRC command “" << message.command <<
                        "”: " << message.arguments.size());
          else
            {
              const auto& cb = it->second.first;
              try {
                (this->*(cb))(message);
              } catch (const std::exception& e) {
                log_error("Unhandled exception: " << e.what());
              }
            }
253
        }
254
      else
255 256 257 258 259
        {
          log_info("No handler for command " << message.command <<
                   ", forwarding the arguments to the user");
          this->on_unknown_message(message);
        }
260
      // Try to find a waiting_iq, which response will be triggered by this IrcMessage
261
      this->bridge->trigger_on_irc_message(this->hostname, message);
louiz’'s avatar
louiz’ committed
262 263 264 265 266
    }
}

void IrcClient::send_message(IrcMessage&& message)
{
267
  log_debug("IRC SENDING: (" << this->get_hostname() << ") " << message);
louiz’'s avatar
louiz’ committed
268 269 270 271 272 273
  std::string res;
  if (!message.prefix.empty())
    res += ":" + std::move(message.prefix) + " ";
  res += std::move(message.command);
  for (const std::string& arg: message.arguments)
    {
louiz’'s avatar
:3  
louiz’ committed
274
      if (arg.find(" ") != std::string::npos ||
louiz’'s avatar
louiz’ committed
275
          (!arg.empty() && arg[0] == ':'))
louiz’'s avatar
louiz’ committed
276 277 278 279 280 281 282
        {
          res += " :" + arg;
          break;
        }
      res += " " + arg;
    }
  res += "\r\n";
283
  this->send_data(std::move(res));
louiz’'s avatar
louiz’ committed
284 285
}

louiz’'s avatar
louiz’ committed
286 287
void IrcClient::send_raw(const std::string& txt)
{
288
  log_debug("IRC SENDING (raw): (" << this->get_hostname() << ") " << txt);
louiz’'s avatar
louiz’ committed
289 290 291
  this->send_data(txt + "\r\n");
}

louiz’'s avatar
louiz’ committed
292 293
void IrcClient::send_user_command(const std::string& username, const std::string& realname)
{
louiz’'s avatar
louiz’ committed
294
  this->send_message(IrcMessage("USER", {username, this->user_hostname, "ignored", realname}));
louiz’'s avatar
louiz’ committed
295 296 297 298 299 300 301
}

void IrcClient::send_nick_command(const std::string& nick)
{
  this->send_message(IrcMessage("NICK", {nick}));
}

302 303 304 305 306
void IrcClient::send_pass_command(const std::string& password)
{
  this->send_message(IrcMessage("PASS", {password}));
}

louiz’'s avatar
louiz’ committed
307 308 309 310 311
void IrcClient::send_webirc_command(const std::string& password, const std::string& user_ip)
{
  this->send_message(IrcMessage("WEBIRC", {password, "biboumi", this->user_hostname, user_ip}));
}

312 313 314 315 316
void IrcClient::send_kick_command(const std::string& chan_name, const std::string& target, const std::string& reason)
{
  this->send_message(IrcMessage("KICK", {chan_name, target, reason}));
}

317 318 319 320 321
void IrcClient::send_list_command()
{
  this->send_message(IrcMessage("LIST", {}));
}

322 323 324 325 326
void IrcClient::send_topic_command(const std::string& chan_name, const std::string& topic)
{
  this->send_message(IrcMessage("TOPIC", {chan_name, topic}));
}

327
void IrcClient::send_quit_command(const std::string& reason)
louiz’'s avatar
louiz’ committed
328
{
329
  this->send_message(IrcMessage("QUIT", {reason}));
louiz’'s avatar
louiz’ committed
330 331
}

332
void IrcClient::send_join_command(const std::string& chan_name, const std::string& password)
louiz’'s avatar
louiz’ committed
333
{
louiz’'s avatar
louiz’ committed
334
  if (this->welcomed == false)
335
    this->channels_to_join.emplace_back(chan_name, password);
336 337
  else if (password.empty())
    this->send_message(IrcMessage("JOIN", {chan_name}));
louiz’'s avatar
louiz’ committed
338
  else
339
    this->send_message(IrcMessage("JOIN", {chan_name, password}));
340
  this->start();
341 342
}

louiz’'s avatar
louiz’ committed
343
bool IrcClient::send_channel_message(const std::string& chan_name, const std::string& body)
344
{
louiz’'s avatar
louiz’ committed
345 346 347
  IrcChannel* channel = this->get_channel(chan_name);
  if (channel->joined == false)
    {
louiz’'s avatar
louiz’ committed
348
      log_warning("Cannot send message to channel " << chan_name << ", it is not joined");
louiz’'s avatar
louiz’ committed
349 350
      return false;
    }
351 352 353 354 355 356 357 358 359
  // Cut the message body into 400-bytes parts (because the whole command
  // must fit into 512 bytes, that's an easy way to make sure the chan name
  // + body fits. I’m lazy.)
  std::string::size_type pos = 0;
  while (pos < body.size())
    {
      this->send_message(IrcMessage("PRIVMSG", {chan_name, body.substr(pos, 400)}));
      pos += 400;
    }
louiz’'s avatar
louiz’ committed
360 361 362
  return true;
}

louiz’'s avatar
louiz’ committed
363
void IrcClient::send_private_message(const std::string& username, const std::string& body, const std::string& type)
louiz’'s avatar
louiz’ committed
364
{
365 366 367
  std::string::size_type pos = 0;
  while (pos < body.size())
    {
louiz’'s avatar
louiz’ committed
368
      this->send_message(IrcMessage(std::string(type), {username, body.substr(pos, 400)}));
369 370
      pos += 400;
    }
371 372 373
  // We always try to insert and we don't care if the username was already
  // in the set.
  this->nicks_to_treat_as_private.insert(username);
louiz’'s avatar
louiz’ committed
374 375
}

louiz’'s avatar
louiz’ committed
376 377 378 379
void IrcClient::send_part_command(const std::string& chan_name, const std::string& status_message)
{
  IrcChannel* channel = this->get_channel(chan_name);
  if (channel->joined == true)
louiz’'s avatar
louiz’ committed
380 381
    {
      if (chan_name.empty())
louiz’'s avatar
louiz’ committed
382
        this->leave_dummy_channel(status_message);
louiz’'s avatar
louiz’ committed
383 384 385
      else
        this->send_message(IrcMessage("PART", {chan_name, status_message}));
    }
louiz’'s avatar
louiz’ committed
386 387 388 389 390 391 392 393
}

void IrcClient::send_mode_command(const std::string& chan_name, const std::vector<std::string>& arguments)
{
  std::vector<std::string> args(arguments);
  args.insert(args.begin(), chan_name);
  IrcMessage m("MODE", std::move(args));
  this->send_message(std::move(m));
louiz’'s avatar
louiz’ committed
394 395
}

louiz’'s avatar
louiz’ committed
396 397 398 399
void IrcClient::send_pong_command(const IrcMessage& message)
{
  const std::string id = message.arguments[0];
  this->send_message(IrcMessage("PONG", {id}));
400 401
}

louiz’'s avatar
louiz’ committed
402
void IrcClient::on_pong(const IrcMessage&)
403 404 405
{
}

406 407 408 409 410
void IrcClient::send_ping_command()
{
  this->send_message(IrcMessage("PING", {"biboumi"}));
}

411 412 413 414 415 416 417
void IrcClient::forward_server_message(const IrcMessage& message)
{
  const std::string from = message.prefix;
  const std::string body = message.arguments[1];

  this->bridge->send_xmpp_message(this->hostname, from, body);
}
louiz’'s avatar
louiz’ committed
418

419 420 421 422 423 424
void IrcClient::on_notice(const IrcMessage& message)
{
  std::string from = message.prefix;
  const std::string to = message.arguments[0];
  const std::string body = message.arguments[1];

425 426 427 428
  if (!body.empty() && body[0] == '\01' && body[body.size() - 1] == '\01')
    // Do not forward the notice to the user if it's a CTCP command
    return ;

429
  if (!to.empty() && this->chantypes.find(to[0]) == this->chantypes.end())
430
    {
louiz’'s avatar
louiz’ committed
431
      // The notice is for us precisely.
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446

      // Find out if we already sent a private message to this user. If yes
      // we treat that message as a private message coming from
      // it. Otherwise we treat it as a notice coming from the server.
      IrcUser user(from);
      std::string nick = utils::tolower(user.nick);
      if (this->nicks_to_treat_as_private.find(nick) !=
          this->nicks_to_treat_as_private.end())
        { // We previously sent a message to that nick)
          this->bridge->send_message({nick + "!" + this->hostname}, nick, body,
                                     false);
        }
      else
        this->bridge->send_xmpp_message(this->hostname, from, body);
    }
447 448
  else
    {
449 450 451
      // The notice was directed at a channel we are in. Modify the message
      // to indicate that it is a notice, and make it a MUC message coming
      // from the MUC JID
louiz’'s avatar
louiz’ committed
452
      IrcMessage modified_message(std::move(from), "PRIVMSG", {to, "\u000303[notice]\u0003 "s + body});
453 454 455 456
      this->on_channel_message(modified_message);
    }
}

louiz’'s avatar
louiz’ committed
457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477
void IrcClient::on_isupport_message(const IrcMessage& message)
{
  const size_t len = message.arguments.size();
  for (size_t i = 1; i < len; ++i)
  {
    const std::string token = message.arguments[i];
    if (token.substr(0, 10) == "CHANMODES=")
    {
      this->chanmodes = utils::split(token.substr(11), ',');
      // make sure we have 4 strings
      this->chanmodes.resize(4);
    }
    else if (token.substr(0, 7) == "PREFIX=")
      {
        size_t i = 8;           // jump PREFIX=(
        size_t j = 9;
        // Find the ) char
        while (j < token.size() && token[j] != ')')
          j++;
        j++;
        while (j < token.size() && token[i] != ')')
478 479 480 481
          {
            this->sorted_user_modes.push_back(token[i]);
            this->prefix_to_mode[token[j++]] = token[i++];
          }
louiz’'s avatar
louiz’ committed
482
      }
483 484 485 486 487
    else if (token.substr(0, 10) == "CHANTYPES=")
      {
        // Remove the default types, they apply only if no other value is
        // specified.
        this->chantypes.clear();
488
        size_t i = 10;
489 490 491
        while (i < token.size())
          this->chantypes.insert(token[i++]);
      }
louiz’'s avatar
louiz’ committed
492 493
  }
}
494

495 496 497 498 499
void IrcClient::send_gateway_message(const std::string& message, const std::string& from)
{
  this->bridge->send_xmpp_message(this->hostname, from, message);
}

500 501
void IrcClient::set_and_forward_user_list(const IrcMessage& message)
{
louiz’'s avatar
louiz’ committed
502
  const std::string chan_name = utils::tolower(message.arguments[2]);
503 504 505 506
  IrcChannel* channel = this->get_channel(chan_name);
  std::vector<std::string> nicks = utils::split(message.arguments[3], ' ');
  for (const std::string& nick: nicks)
    {
507
      const IrcUser* user = channel->add_user(nick, this->prefix_to_mode);
508 509
      if (user->nick != channel->get_self()->nick)
        {
510 511 512
          this->bridge->send_user_join(this->hostname, chan_name, user,
                                       user->get_most_significant_mode(this->sorted_user_modes),
                                       false);
513 514 515 516 517
        }
      else
        {
          // we now know the modes of self, so copy the modes into self
          channel->get_self()->modes = user->modes;
518 519 520 521
        }
    }
}

louiz’'s avatar
louiz’ committed
522
void IrcClient::on_channel_join(const IrcMessage& message)
523
{
louiz’'s avatar
louiz’ committed
524
  const std::string chan_name = utils::tolower(message.arguments[0]);
louiz’'s avatar
louiz’ committed
525 526 527 528 529
  IrcChannel* channel;
  if (chan_name.empty())
    channel = &this->dummy_channel;
  else
    channel = this->get_channel(chan_name);
louiz’'s avatar
louiz’ committed
530 531
  const std::string nick = message.prefix;
  if (channel->joined == false)
532
    channel->set_self(nick);
louiz’'s avatar
louiz’ committed
533 534
  else
    {
535
      const IrcUser* user = channel->add_user(nick, this->prefix_to_mode);
536 537 538
      this->bridge->send_user_join(this->hostname, chan_name, user,
                                   user->get_most_significant_mode(this->sorted_user_modes),
                                   false);
louiz’'s avatar
louiz’ committed
539
    }
540 541
}

louiz’'s avatar
louiz’ committed
542 543 544 545 546
void IrcClient::on_channel_message(const IrcMessage& message)
{
  const IrcUser user(message.prefix);
  const std::string nick = user.nick;
  Iid iid;
louiz’'s avatar
louiz’ committed
547 548
  iid.set_local(message.arguments[0]);
  iid.set_server(this->hostname);
louiz’'s avatar
louiz’ committed
549
  const std::string body = message.arguments[1];
louiz’'s avatar
louiz’ committed
550
  bool muc = true;
louiz’'s avatar
louiz’ committed
551
  if (!this->get_channel(iid.get_local())->joined)
louiz’'s avatar
louiz’ committed
552
    {
louiz’'s avatar
louiz’ committed
553 554
      iid.is_user = true;
      iid.set_local(nick);
louiz’'s avatar
louiz’ committed
555 556
      muc = false;
    }
louiz’'s avatar
louiz’ committed
557 558
  else
    iid.is_channel = true;
559 560 561 562
  if (!body.empty() && body[0] == '\01')
    {
      if (body.substr(1, 6) == "ACTION")
        this->bridge->send_message(iid, nick,
louiz’'s avatar
louiz’ committed
563
                  "/me"s + body.substr(7, body.size() - 8), muc);
564 565
      else if (body.substr(1, 8) == "VERSION\01")
        this->bridge->send_iq_version_request(nick, this->hostname);
566
      else if (body.substr(1, 5) == "PING ")
567
        this->bridge->send_xmpp_ping_request(utils::tolower(nick), this->hostname,
568
                                             body.substr(6, body.size() - 7));
569 570 571
    }
  else
    this->bridge->send_message(iid, nick, body, muc);
louiz’'s avatar
louiz’ committed
572 573
}

574 575 576 577 578 579 580 581 582 583 584 585
void IrcClient::on_rpl_liststart(const IrcMessage&)
{
}

void IrcClient::on_rpl_list(const IrcMessage&)
{
}

void IrcClient::on_rpl_listend(const IrcMessage&)
{
}

louiz’'s avatar
louiz’ committed
586
void IrcClient::empty_motd(const IrcMessage&)
587 588 589 590 591 592 593 594 595 596 597 598 599
{
  this->motd.erase();
}

void IrcClient::on_motd_line(const IrcMessage& message)
{
  const std::string body = message.arguments[1];
  // We could send the MOTD without a line break between each IRC-message,
  // but sometimes it contains some ASCII art, we use line breaks to keep
  // them intact.
  this->motd += body+"\n";
}

louiz’'s avatar
louiz’ committed
600
void IrcClient::send_motd(const IrcMessage&)
601 602 603 604
{
  this->bridge->send_xmpp_message(this->hostname, "", this->motd);
}

605 606
void IrcClient::on_topic_received(const IrcMessage& message)
{
607
  const std::string chan_name = utils::tolower(message.arguments[message.arguments.size() - 2]);
608
  IrcChannel* channel = this->get_channel(chan_name);
609
  channel->topic = message.arguments[message.arguments.size() - 1];
louiz’'s avatar
louiz’ committed
610 611
  if (channel->joined)
    this->bridge->send_topic(this->hostname, chan_name, channel->topic);
612 613 614 615
}

void IrcClient::on_channel_completely_joined(const IrcMessage& message)
{
louiz’'s avatar
louiz’ committed
616
  const std::string chan_name = utils::tolower(message.arguments[1]);
617
  IrcChannel* channel = this->get_channel(chan_name);
618
  channel->joined = true;
619 620 621
  this->bridge->send_user_join(this->hostname, chan_name, channel->get_self(),
                               channel->get_self()->get_most_significant_mode(this->sorted_user_modes),
                               true);
622
  this->bridge->send_topic(this->hostname, chan_name, channel->topic);
623
}
louiz’'s avatar
louiz’ committed
624

625 626 627 628 629 630 631
void IrcClient::on_erroneous_nickname(const IrcMessage& message)
{
  const std::string error_msg = message.arguments.size() >= 3 ?
    message.arguments[2]: "Erroneous nickname";
  this->send_gateway_message(error_msg + ": " + message.arguments[1], message.prefix);
}

632 633 634 635 636 637 638
void IrcClient::on_nickname_conflict(const IrcMessage& message)
{
  const std::string nickname = message.arguments[1];
  this->on_generic_error(message);
  for (auto it = this->channels.begin(); it != this->channels.end(); ++it)
  {
    Iid iid;
louiz’'s avatar
louiz’ committed
639 640 641
    iid.set_local(it->first);
    iid.set_server(this->hostname);
    iid.is_channel = true;
642 643 644 645
    this->bridge->send_nickname_conflict_error(iid, nickname);
  }
}

646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664
void IrcClient::on_nickname_change_too_fast(const IrcMessage& message)
{
  const std::string nickname = message.arguments[1];
  std::string txt;
  if (message.arguments.size() >= 3)
    txt = message.arguments[2];
  this->on_generic_error(message);
  for (auto it = this->channels.begin(); it != this->channels.end(); ++it)
  {
    Iid iid;
    iid.set_local(it->first);
    iid.set_server(this->hostname);
    iid.is_channel = true;
    this->bridge->send_presence_error(iid, nickname,
                                      "cancel", "not-acceptable",
                                      "", txt);
  }
}

665 666 667 668 669 670 671
void IrcClient::on_generic_error(const IrcMessage& message)
{
  const std::string error_msg = message.arguments.size() >= 3 ?
    message.arguments[2]: "Unspecified error";
  this->send_gateway_message(message.arguments[1] + ": " + error_msg, message.prefix);
}

louiz’'s avatar
louiz’ committed
672 673 674 675
void IrcClient::on_welcome_message(const IrcMessage& message)
{
  this->current_nick = message.arguments[0];
  this->welcomed = true;
676 677 678 679 680 681
#ifdef USE_DATABASE
  auto options = Database::get_irc_server_options(this->bridge->get_bare_jid(),
                                                  this->get_hostname());
  if (!options.afterConnectionCommand.value().empty())
    this->send_raw(options.afterConnectionCommand.value());
#endif
682 683 684
  // Install a repeated events to regularly send a PING
  TimedEventsManager::instance().add_event(TimedEvent(240s, std::bind(&IrcClient::send_ping_command, this),
                                                      "PING"s + this->hostname + this->bridge->get_jid()));
685 686
  for (const auto& tuple: this->channels_to_join)
    this->send_join_command(std::get<0>(tuple), std::get<1>(tuple));
louiz’'s avatar
louiz’ committed
687
  this->channels_to_join.clear();
louiz’'s avatar
louiz’ committed
688 689 690 691 692 693 694 695 696 697 698 699
  // Indicate that the dummy channel is joined as well, if needed
  if (this->dummy_channel.joining)
    {
      // Simulate a message coming from the IRC server saying that we joined
      // the channel
      const IrcMessage join_message(this->get_nick(), "JOIN", {""});
      this->on_channel_join(join_message);
      const IrcMessage end_join_message(std::string(this->hostname), "366",
                                        {this->get_nick(),
                                            "", "End of NAMES list"});
      this->on_channel_completely_joined(end_join_message);
    }
louiz’'s avatar
louiz’ committed
700
}
louiz’'s avatar
louiz’ committed
701 702 703

void IrcClient::on_part(const IrcMessage& message)
{
louiz’'s avatar
louiz’ committed
704
  const std::string chan_name = message.arguments[0];
louiz’'s avatar
louiz’ committed
705
  IrcChannel* channel = this->get_channel(chan_name);
706 707
  if (!channel->joined)
    return ;
louiz’'s avatar
louiz’ committed
708 709 710 711 712 713 714 715 716
  std::string txt;
  if (message.arguments.size() >= 2)
    txt = message.arguments[1];
  const IrcUser* user = channel->find_user(message.prefix);
  if (user)
    {
      std::string nick = user->nick;
      channel->remove_user(user);
      Iid iid;
louiz’'s avatar
louiz’ committed
717 718 719
      iid.set_local(chan_name);
      iid.set_server(this->hostname);
      iid.is_channel = true;
louiz’'s avatar
louiz’ committed
720 721
      bool self = channel->get_self()->nick == nick;
      if (self)
louiz’'s avatar
louiz’ committed
722
      {
louiz’'s avatar
louiz’ committed
723
        channel->joined = false;
louiz’'s avatar
louiz’ committed
724
        this->channels.erase(utils::tolower(chan_name));
louiz’'s avatar
louiz’ committed
725 726 727
        // channel pointer is now invalid
        channel = nullptr;
      }
728
      this->bridge->send_muc_leave(std::move(iid), std::move(nick), std::move(txt), self);
louiz’'s avatar
louiz’ committed
729 730
    }
}
louiz’'s avatar
louiz’ committed
731

louiz’'s avatar
louiz’ committed
732 733 734 735 736 737 738
void IrcClient::on_error(const IrcMessage& message)
{
  const std::string leave_message = message.arguments[0];
  // The user is out of all the channels
  for (auto it = this->channels.begin(); it != this->channels.end(); ++it)
  {
    Iid iid;
louiz’'s avatar
louiz’ committed
739 740 741
    iid.set_local(it->first);
    iid.set_server(this->hostname);
    iid.is_channel = true;
louiz’'s avatar
louiz’ committed
742
    IrcChannel* channel = it->second.get();
743 744
    if (!channel->joined)
      continue;
louiz’'s avatar
louiz’ committed
745 746 747
    std::string own_nick = channel->get_self()->nick;
    this->bridge->send_muc_leave(std::move(iid), std::move(own_nick), leave_message, true);
  }
louiz’'s avatar
louiz’ committed
748
  this->channels.clear();
louiz’'s avatar
louiz’ committed
749
  this->send_gateway_message("ERROR: "s + leave_message);
louiz’'s avatar
louiz’ committed
750 751
}

louiz’'s avatar
louiz’ committed
752 753 754 755 756 757 758 759 760 761 762 763 764 765 766
void IrcClient::on_quit(const IrcMessage& message)
{
  std::string txt;
  if (message.arguments.size() >= 1)
    txt = message.arguments[0];
  for (auto it = this->channels.begin(); it != this->channels.end(); ++it)
    {
      const std::string chan_name = it->first;
      IrcChannel* channel = it->second.get();
      const IrcUser* user = channel->find_user(message.prefix);
      if (user)
        {
          std::string nick = user->nick;
          channel->remove_user(user);
          Iid iid;
louiz’'s avatar
louiz’ committed
767 768 769
          iid.set_local(chan_name);
          iid.set_server(this->hostname);
          iid.is_channel = true;
louiz’'s avatar
louiz’ committed
770
          this->bridge->send_muc_leave(std::move(iid), std::move(nick), txt, false);
louiz’'s avatar
louiz’ committed
771 772 773
        }
    }
}
louiz’'s avatar
louiz’ committed
774 775 776 777 778 779 780 781 782 783 784 785 786

void IrcClient::on_nick(const IrcMessage& message)
{
  const std::string new_nick = message.arguments[0];
  for (auto it = this->channels.begin(); it != this->channels.end(); ++it)
    {
      const std::string chan_name = it->first;
      IrcChannel* channel = it->second.get();
      IrcUser* user = channel->find_user(message.prefix);
      if (user)
        {
          std::string old_nick = user->nick;
          Iid iid;
louiz’'s avatar
louiz’ committed
787 788 789
          iid.set_local(chan_name);
          iid.set_server(this->hostname);
          iid.is_channel = true;
790 791 792
          const bool self = channel->get_self()->nick == old_nick;
          const char user_mode = user->get_most_significant_mode(this->sorted_user_modes);
          this->bridge->send_nick_change(std::move(iid), old_nick, new_nick, user_mode, self);
louiz’'s avatar
louiz’ committed
793 794 795 796 797 798 799 800 801 802
          user->nick = new_nick;
          if (self)
            {
              channel->get_self()->nick = new_nick;
              this->current_nick = new_nick;
            }
        }
    }
}

803 804
void IrcClient::on_kick(const IrcMessage& message)
{
louiz’'s avatar
louiz’ committed
805
  const std::string chan_name = utils::tolower(message.arguments[0]);
806 807 808
  const std::string target = message.arguments[1];
  const std::string reason = message.arguments[2];
  IrcChannel* channel = this->get_channel(chan_name);
809 810
  if (!channel->joined)
    return ;
811 812 813 814
  if (channel->get_self()->nick == target)
    channel->joined = false;
  IrcUser author(message.prefix);
  Iid iid;
louiz’'s avatar
louiz’ committed
815 816 817
  iid.set_local(chan_name);
  iid.set_server(this->hostname);
  iid.is_channel = true;
818 819 820
  this->bridge->kick_muc_user(std::move(iid), target, reason, author.nick);
}

louiz’'s avatar
louiz’ committed
821 822 823
void IrcClient::on_mode(const IrcMessage& message)
{
  const std::string target = message.arguments[0];
824
  if (this->chantypes.find(target[0]) != this->chantypes.end())
louiz’'s avatar
louiz’ committed
825 826 827 828 829 830 831 832 833 834
    this->on_channel_mode(message);
  else
    this->on_user_mode(message);
}

void IrcClient::on_channel_mode(const IrcMessage& message)
{
  // For now, just transmit the modes so the user can know what happens
  // TODO, actually interprete the mode.
  Iid iid;
louiz’'s avatar
louiz’ committed
835 836 837
  iid.set_local(message.arguments[0]);
  iid.set_server(this->hostname);
  iid.is_channel = true;
louiz’'s avatar
louiz’ committed
838
  IrcUser user(message.prefix);
839 840 841 842 843 844 845 846 847 848
  std::string mode_arguments;
  for (size_t i = 1; i < message.arguments.size(); ++i)
    {
      if (!message.arguments[i].empty())
        {
          if (i != 1)
            mode_arguments += " ";
          mode_arguments += message.arguments[i];
        }
    }
louiz’'s avatar
louiz’ committed
849
  this->bridge->send_message(iid, "", "Mode "s + iid.get_local() +
850
                                      " [" + mode_arguments + "] by " + user.nick,
louiz’'s avatar
louiz’ committed
851
                             true);
louiz’'s avatar
louiz’ committed
852
  const IrcChannel* channel = this->get_channel(iid.get_local());
853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905
  if (!channel)
    return;

  // parse the received modes, we need to handle things like "+m-oo coucou toutou"
  const std::string modes = message.arguments[1];
  // a list of modified IrcUsers. When we applied all modes, we check the
  // modes that now applies to each of them, and send a notification for
  // each one. This is to disallow sending two notifications or more when a
  // single MODE command changes two or more modes on the same participant
  std::set<const IrcUser*> modified_users;
  // If it is true, the modes are added, if it’s false they are
  // removed. When we encounter the '+' char, the value is changed to true,
  // and with '-' it is changed to false.
  bool add = true;
  bool use_arg;
  size_t arg_pos = 2;
  for (const char c: modes)
    {
      if (c == '+')
        add = true;
      else if (c == '-')
        add = false;
      else
        { // lookup the mode symbol in the 4 chanmodes lists, depending on
          // the list where it is found, it takes an argument or not
          size_t type;
          for (type = 0; type < 4; ++type)
            if (this->chanmodes[type].find(c) != std::string::npos)
              break;
          if (type == 4)        // if mode was not found
            {
              // That mode can also be of type B if it is present in the
              // prefix_to_mode map
              for (const std::pair<char, char>& pair: this->prefix_to_mode)
                if (pair.second == c)
                  {
                    type = 1;
                    break;
                  }
            }
          // modes of type A, B or C (but only with add == true)
          if (type == 0 || type == 1 ||
              (type == 2 && add == true))
            use_arg = true;
          else // modes of type C (but only with add == false), D, or unknown
            use_arg = false;
          if (use_arg == true && message.arguments.size() > arg_pos)
            {
              const std::string target = message.arguments[arg_pos++];
              IrcUser* user = channel->find_user(target);
              if (!user)
                {
                  log_warning("Trying to set mode for non-existing user '" << target
louiz’'s avatar
louiz’ committed
906
                              << "' in channel" << iid.get_local());
907 908 909 910 911 912 913 914 915 916 917 918 919 920 921
                  return;
                }
              if (add)
                user->add_mode(c);
              else
                user->remove_mode(c);
              modified_users.insert(user);
            }
        }
    }
  for (const IrcUser* u: modified_users)
    {
      char most_significant_mode = u->get_most_significant_mode(this->sorted_user_modes);
      this->bridge->send_affiliation_role_change(iid, u->nick, most_significant_mode);
    }
louiz’'s avatar
louiz’ committed
922 923 924 925 926
}

void IrcClient::on_user_mode(const IrcMessage& message)
{
  this->bridge->send_xmpp_message(this->hostname, "",
louiz’'s avatar
louiz’ committed
927
                                  "User mode for "s + message.arguments[0] +
louiz’'s avatar
louiz’ committed
928 929
                                  " is [" + message.arguments[1] + "]");
}
930

931 932 933 934 935 936 937 938 939 940 941 942 943 944 945
void IrcClient::on_unknown_message(const IrcMessage& message)
{
  if (message.arguments.size() < 2)
    return ;
  std::string from = message.prefix;
  std::stringstream ss;
  for (auto it = message.arguments.begin() + 1; it != message.arguments.end(); ++it)
    {
      ss << *it;
      if (it + 1 != message.arguments.end())
        ss << " ";
    }
  this->bridge->send_xmpp_message(this->hostname, from, ss.str());
}

946 947
size_t IrcClient::number_of_joined_channels() const
{
948 949 950 951
  if (this->dummy_channel.joined)
    return this->channels.size() + 1;
  else
    return this->channels.size();
952
}
louiz’'s avatar
louiz’ committed
953 954 955 956 957

DummyIrcChannel& IrcClient::get_dummy_channel()
{
  return this->dummy_channel;
}
louiz’'s avatar
louiz’ committed
958 959 960 961 962 963 964 965

void IrcClient::leave_dummy_channel(const std::string& exit_message)
{
  if (!this->dummy_channel.joined)
    return;
  this->dummy_channel.joined = false;
  this->dummy_channel.joining = false;
  this->dummy_channel.remove_all_users();
louiz’'s avatar
louiz’ committed
966
  this->bridge->send_muc_leave(Iid("%"s + this->hostname), std::string(this->current_nick), exit_message, true);
louiz’'s avatar
louiz’ committed
967
}
968 969 970 971 972 973 974 975 976 977 978

#ifdef BOTAN_FOUND
bool IrcClient::abort_on_invalid_cert() const
{
#ifdef USE_DATABASE
  auto options = Database::get_irc_server_options(this->bridge->get_bare_jid(), this->hostname);
  return options.verifyCert.value();
#endif
  return true;
}
#endif