Serenity Operating System
at portability 772 lines 22 kB view raw
1/* 2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are met: 7 * 8 * 1. Redistributions of source code must retain the above copyright notice, this 9 * list of conditions and the following disclaimer. 10 * 11 * 2. Redistributions in binary form must reproduce the above copyright notice, 12 * this list of conditions and the following disclaimer in the documentation 13 * and/or other materials provided with the distribution. 14 * 15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 */ 26 27#include "IRCClient.h" 28#include "IRCAppWindow.h" 29#include "IRCChannel.h" 30#include "IRCLogBuffer.h" 31#include "IRCQuery.h" 32#include "IRCWindow.h" 33#include "IRCWindowListModel.h" 34#include <AK/QuickSort.h> 35#include <AK/StringBuilder.h> 36#include <LibCore/DateTime.h> 37#include <LibCore/Notifier.h> 38#include <arpa/inet.h> 39#include <netinet/in.h> 40#include <stdio.h> 41#include <sys/socket.h> 42#include <time.h> 43#include <unistd.h> 44 45#define IRC_DEBUG 46 47enum IRCNumeric { 48 RPL_WHOISUSER = 311, 49 RPL_WHOISSERVER = 312, 50 RPL_WHOISOPERATOR = 313, 51 RPL_WHOISIDLE = 317, 52 RPL_ENDOFWHOIS = 318, 53 RPL_WHOISCHANNELS = 319, 54 RPL_TOPIC = 332, 55 RPL_TOPICWHOTIME = 333, 56 RPL_NAMREPLY = 353, 57 RPL_ENDOFNAMES = 366, 58}; 59 60IRCClient::IRCClient() 61 : m_nickname("seren1ty") 62 , m_client_window_list_model(IRCWindowListModel::create(*this)) 63 , m_log(IRCLogBuffer::create()) 64 , m_config(Core::ConfigFile::get_for_app("IRCClient")) 65{ 66 m_socket = Core::TCPSocket::construct(this); 67 m_nickname = m_config->read_entry("User", "Nickname", "seren1ty"); 68 m_hostname = m_config->read_entry("Connection", "Server", ""); 69 m_port = m_config->read_num_entry("Connection", "Port", 6667); 70} 71 72IRCClient::~IRCClient() 73{ 74} 75 76void IRCClient::set_server(const String& hostname, int port) 77{ 78 m_hostname = hostname; 79 m_port = port; 80 m_config->write_entry("Connection", "Server", hostname); 81 m_config->write_num_entry("Connection", "Port", port); 82 m_config->sync(); 83} 84 85void IRCClient::on_socket_connected() 86{ 87 m_notifier = Core::Notifier::construct(m_socket->fd(), Core::Notifier::Read); 88 m_notifier->on_ready_to_read = [this] { receive_from_server(); }; 89 90 send_user(); 91 send_nick(); 92 93 auto channel_str = m_config->read_entry("Connection", "AutoJoinChannels", "#test"); 94 dbgprintf("IRCClient: Channels to autojoin: %s\n", channel_str.characters()); 95 auto channels = channel_str.split(','); 96 for (auto& channel : channels) { 97 join_channel(channel); 98 dbgprintf("IRCClient: Auto joining channel: %s\n", channel.characters()); 99 } 100} 101 102bool IRCClient::connect() 103{ 104 if (m_socket->is_connected()) 105 ASSERT_NOT_REACHED(); 106 107 m_socket->on_connected = [this] { on_socket_connected(); }; 108 bool success = m_socket->connect(m_hostname, m_port); 109 if (!success) 110 return false; 111 return true; 112} 113 114void IRCClient::receive_from_server() 115{ 116 while (m_socket->can_read_line()) { 117 auto line = m_socket->read_line(PAGE_SIZE); 118 if (line.is_null()) { 119 if (!m_socket->is_connected()) { 120 printf("IRCClient: Connection closed!\n"); 121 exit(1); 122 } 123 ASSERT_NOT_REACHED(); 124 } 125 process_line(move(line)); 126 } 127} 128 129void IRCClient::process_line(ByteBuffer&& line) 130{ 131 Message msg; 132 Vector<char, 32> prefix; 133 Vector<char, 32> command; 134 Vector<char, 256> current_parameter; 135 enum { 136 Start, 137 InPrefix, 138 InCommand, 139 InStartOfParameter, 140 InParameter, 141 InTrailingParameter, 142 } state 143 = Start; 144 145 for (size_t i = 0; i < line.size(); ++i) { 146 char ch = line[i]; 147 if (ch == '\r') 148 continue; 149 if (ch == '\n') 150 break; 151 switch (state) { 152 case Start: 153 if (ch == ':') { 154 state = InPrefix; 155 continue; 156 } 157 state = InCommand; 158 [[fallthrough]]; 159 case InCommand: 160 if (ch == ' ') { 161 state = InStartOfParameter; 162 continue; 163 } 164 command.append(ch); 165 continue; 166 case InPrefix: 167 if (ch == ' ') { 168 state = InCommand; 169 continue; 170 } 171 prefix.append(ch); 172 continue; 173 case InStartOfParameter: 174 if (ch == ':') { 175 state = InTrailingParameter; 176 continue; 177 } 178 state = InParameter; 179 [[fallthrough]]; 180 case InParameter: 181 if (ch == ' ') { 182 if (!current_parameter.is_empty()) 183 msg.arguments.append(String(current_parameter.data(), current_parameter.size())); 184 current_parameter.clear_with_capacity(); 185 state = InStartOfParameter; 186 continue; 187 } 188 current_parameter.append(ch); 189 continue; 190 case InTrailingParameter: 191 current_parameter.append(ch); 192 continue; 193 } 194 } 195 if (!current_parameter.is_empty()) 196 msg.arguments.append(String::copy(current_parameter)); 197 msg.prefix = String::copy(prefix); 198 msg.command = String::copy(command); 199 handle(msg); 200} 201 202void IRCClient::send(const String& text) 203{ 204 if (!m_socket->send(ByteBuffer::wrap(text.characters(), text.length()))) { 205 perror("send"); 206 exit(1); 207 } 208} 209 210void IRCClient::send_user() 211{ 212 send(String::format("USER %s 0 * :%s\r\n", m_nickname.characters(), m_nickname.characters())); 213} 214 215void IRCClient::send_nick() 216{ 217 send(String::format("NICK %s\r\n", m_nickname.characters())); 218} 219 220void IRCClient::send_pong(const String& server) 221{ 222 send(String::format("PONG %s\r\n", server.characters())); 223 sleep(1); 224} 225 226void IRCClient::join_channel(const String& channel_name) 227{ 228 send(String::format("JOIN %s\r\n", channel_name.characters())); 229} 230 231void IRCClient::part_channel(const String& channel_name) 232{ 233 send(String::format("PART %s\r\n", channel_name.characters())); 234} 235 236void IRCClient::send_whois(const String& nick) 237{ 238 send(String::format("WHOIS %s\r\n", nick.characters())); 239} 240 241void IRCClient::handle(const Message& msg) 242{ 243#ifdef IRC_DEBUG 244 printf("IRCClient::execute: prefix='%s', command='%s', arguments=%zu\n", 245 msg.prefix.characters(), 246 msg.command.characters(), 247 msg.arguments.size()); 248 249 int i = 0; 250 for (auto& arg : msg.arguments) { 251 printf(" [%d]: %s\n", i, arg.characters()); 252 ++i; 253 } 254#endif 255 256 bool is_numeric; 257 int numeric = msg.command.to_uint(is_numeric); 258 259 if (is_numeric) { 260 switch (numeric) { 261 case RPL_WHOISCHANNELS: 262 return handle_rpl_whoischannels(msg); 263 case RPL_ENDOFWHOIS: 264 return handle_rpl_endofwhois(msg); 265 case RPL_WHOISOPERATOR: 266 return handle_rpl_whoisoperator(msg); 267 case RPL_WHOISSERVER: 268 return handle_rpl_whoisserver(msg); 269 case RPL_WHOISUSER: 270 return handle_rpl_whoisuser(msg); 271 case RPL_WHOISIDLE: 272 return handle_rpl_whoisidle(msg); 273 case RPL_TOPICWHOTIME: 274 return handle_rpl_topicwhotime(msg); 275 case RPL_TOPIC: 276 return handle_rpl_topic(msg); 277 case RPL_NAMREPLY: 278 return handle_rpl_namreply(msg); 279 case RPL_ENDOFNAMES: 280 return handle_rpl_endofnames(msg); 281 } 282 } 283 284 if (msg.command == "PING") 285 return handle_ping(msg); 286 287 if (msg.command == "JOIN") 288 return handle_join(msg); 289 290 if (msg.command == "PART") 291 return handle_part(msg); 292 293 if (msg.command == "TOPIC") 294 return handle_topic(msg); 295 296 if (msg.command == "PRIVMSG") 297 return handle_privmsg_or_notice(msg, PrivmsgOrNotice::Privmsg); 298 299 if (msg.command == "NOTICE") 300 return handle_privmsg_or_notice(msg, PrivmsgOrNotice::Notice); 301 302 if (msg.command == "NICK") 303 return handle_nick(msg); 304 305 if (msg.arguments.size() >= 2) 306 add_server_message(String::format("[%s] %s", msg.command.characters(), msg.arguments[1].characters())); 307} 308 309void IRCClient::add_server_message(const String& text, Color color) 310{ 311 m_log->add_message(0, "", text, color); 312 m_server_subwindow->did_add_message(); 313} 314 315void IRCClient::send_privmsg(const String& target, const String& text) 316{ 317 send(String::format("PRIVMSG %s :%s\r\n", target.characters(), text.characters())); 318} 319 320void IRCClient::send_notice(const String& target, const String& text) 321{ 322 send(String::format("NOTICE %s :%s\r\n", target.characters(), text.characters())); 323} 324 325void IRCClient::handle_user_input_in_channel(const String& channel_name, const String& input) 326{ 327 if (input.is_empty()) 328 return; 329 if (input[0] == '/') 330 return handle_user_command(input); 331 ensure_channel(channel_name).say(input); 332} 333 334void IRCClient::handle_user_input_in_query(const String& query_name, const String& input) 335{ 336 if (input.is_empty()) 337 return; 338 if (input[0] == '/') 339 return handle_user_command(input); 340 ensure_query(query_name).say(input); 341} 342 343void IRCClient::handle_user_input_in_server(const String& input) 344{ 345 if (input.is_empty()) 346 return; 347 if (input[0] == '/') 348 return handle_user_command(input); 349} 350 351bool IRCClient::is_nick_prefix(char ch) const 352{ 353 switch (ch) { 354 case '@': 355 case '+': 356 case '~': 357 case '&': 358 case '%': 359 return true; 360 } 361 return false; 362} 363 364static bool has_ctcp_payload(const StringView& string) 365{ 366 return string.length() >= 2 && string[0] == 0x01 && string[string.length() - 1] == 0x01; 367} 368 369void IRCClient::handle_privmsg_or_notice(const Message& msg, PrivmsgOrNotice type) 370{ 371 if (msg.arguments.size() < 2) 372 return; 373 if (msg.prefix.is_empty()) 374 return; 375 auto parts = msg.prefix.split('!'); 376 auto sender_nick = parts[0]; 377 auto target = msg.arguments[0]; 378 379 bool is_ctcp = has_ctcp_payload(msg.arguments[1]); 380 381#ifdef IRC_DEBUG 382 printf("handle_privmsg_or_notice: type='%s'%s, sender_nick='%s', target='%s'\n", 383 type == PrivmsgOrNotice::Privmsg ? "privmsg" : "notice", 384 is_ctcp ? " (ctcp)" : "", 385 sender_nick.characters(), 386 target.characters()); 387#endif 388 389 if (sender_nick.is_empty()) 390 return; 391 392 char sender_prefix = 0; 393 if (is_nick_prefix(sender_nick[0])) { 394 sender_prefix = sender_nick[0]; 395 sender_nick = sender_nick.substring(1, sender_nick.length() - 1); 396 } 397 398 String message_text = msg.arguments[1]; 399 auto message_color = Color::Black; 400 401 if (is_ctcp) { 402 auto ctcp_payload = msg.arguments[1].substring_view(1, msg.arguments[1].length() - 2); 403 if (type == PrivmsgOrNotice::Privmsg) 404 handle_ctcp_request(sender_nick, ctcp_payload); 405 else 406 handle_ctcp_response(sender_nick, ctcp_payload); 407 StringBuilder builder; 408 builder.append("(CTCP) "); 409 builder.append(ctcp_payload); 410 message_text = builder.to_string(); 411 message_color = Color::Blue; 412 } 413 414 { 415 auto it = m_channels.find(target); 416 if (it != m_channels.end()) { 417 (*it).value->add_message(sender_prefix, sender_nick, message_text, message_color); 418 return; 419 } 420 } 421 422 // For NOTICE or CTCP messages, only put them in query if one already exists. 423 // Otherwise, put them in the server window. This seems to match other clients. 424 IRCQuery* query = nullptr; 425 if (is_ctcp || type == PrivmsgOrNotice::Notice) { 426 query = query_with_name(sender_nick); 427 } else { 428 query = &ensure_query(sender_nick); 429 } 430 if (query) 431 query->add_message(sender_prefix, sender_nick, message_text, message_color); 432 else { 433 add_server_message(String::format("<%s> %s", sender_nick.characters(), message_text.characters()), message_color); 434 } 435} 436 437IRCQuery* IRCClient::query_with_name(const String& name) 438{ 439 return const_cast<IRCQuery*>(m_queries.get(name).value_or(nullptr)); 440} 441 442IRCQuery& IRCClient::ensure_query(const String& name) 443{ 444 auto it = m_queries.find(name); 445 if (it != m_queries.end()) 446 return *(*it).value; 447 auto query = IRCQuery::create(*this, name); 448 auto& query_reference = *query; 449 m_queries.set(name, query); 450 return query_reference; 451} 452 453IRCChannel& IRCClient::ensure_channel(const String& name) 454{ 455 auto it = m_channels.find(name); 456 if (it != m_channels.end()) 457 return *(*it).value; 458 auto channel = IRCChannel::create(*this, name); 459 auto& channel_reference = *channel; 460 m_channels.set(name, channel); 461 return channel_reference; 462} 463 464void IRCClient::handle_ping(const Message& msg) 465{ 466 if (msg.arguments.size() < 1) 467 return; 468 m_log->add_message(0, "", "Ping? Pong!"); 469 send_pong(msg.arguments[0]); 470} 471 472void IRCClient::handle_join(const Message& msg) 473{ 474 if (msg.arguments.size() != 1) 475 return; 476 auto prefix_parts = msg.prefix.split('!'); 477 if (prefix_parts.size() < 1) 478 return; 479 auto nick = prefix_parts[0]; 480 auto& channel_name = msg.arguments[0]; 481 ensure_channel(channel_name).handle_join(nick, msg.prefix); 482} 483 484void IRCClient::handle_part(const Message& msg) 485{ 486 if (msg.arguments.size() < 1) 487 return; 488 auto prefix_parts = msg.prefix.split('!'); 489 if (prefix_parts.size() < 1) 490 return; 491 auto nick = prefix_parts[0]; 492 auto& channel_name = msg.arguments[0]; 493 ensure_channel(channel_name).handle_part(nick, msg.prefix); 494} 495 496void IRCClient::handle_nick(const Message& msg) 497{ 498 auto prefix_parts = msg.prefix.split('!'); 499 if (prefix_parts.size() < 1) 500 return; 501 auto old_nick = prefix_parts[0]; 502 if (msg.arguments.size() != 1) 503 return; 504 auto& new_nick = msg.arguments[0]; 505 if (old_nick == m_nickname) 506 m_nickname = new_nick; 507 add_server_message(String::format("~ %s changed nickname to %s", old_nick.characters(), new_nick.characters())); 508 if (on_nickname_changed) 509 on_nickname_changed(new_nick); 510 for (auto& it : m_channels) { 511 it.value->notify_nick_changed(old_nick, new_nick); 512 } 513} 514 515void IRCClient::handle_topic(const Message& msg) 516{ 517 if (msg.arguments.size() != 2) 518 return; 519 auto prefix_parts = msg.prefix.split('!'); 520 if (prefix_parts.size() < 1) 521 return; 522 auto nick = prefix_parts[0]; 523 auto& channel_name = msg.arguments[0]; 524 ensure_channel(channel_name).handle_topic(nick, msg.arguments[1]); 525} 526 527void IRCClient::handle_rpl_topic(const Message& msg) 528{ 529 if (msg.arguments.size() < 3) 530 return; 531 auto& channel_name = msg.arguments[1]; 532 auto& topic = msg.arguments[2]; 533 ensure_channel(channel_name).handle_topic({}, topic); 534 // FIXME: Handle RPL_TOPICWHOTIME so we can know who set it and when. 535} 536 537void IRCClient::handle_rpl_namreply(const Message& msg) 538{ 539 if (msg.arguments.size() < 4) 540 return; 541 auto& channel_name = msg.arguments[2]; 542 auto& channel = ensure_channel(channel_name); 543 auto members = msg.arguments[3].split(' '); 544 545 quick_sort(members.begin(), members.end(), [](auto& a, auto& b) { 546 return strcasecmp(a.characters(), b.characters()) < 0; 547 }); 548 549 for (auto& member : members) { 550 if (member.is_empty()) 551 continue; 552 char prefix = 0; 553 if (is_nick_prefix(member[0])) 554 prefix = member[0]; 555 channel.add_member(member, prefix); 556 } 557} 558 559void IRCClient::handle_rpl_endofnames(const Message&) 560{ 561} 562 563void IRCClient::handle_rpl_endofwhois(const Message&) 564{ 565 add_server_message("// End of WHOIS"); 566} 567 568void IRCClient::handle_rpl_whoisoperator(const Message& msg) 569{ 570 if (msg.arguments.size() < 2) 571 return; 572 auto& nick = msg.arguments[1]; 573 add_server_message(String::format("* %s is an IRC operator", nick.characters())); 574} 575 576void IRCClient::handle_rpl_whoisserver(const Message& msg) 577{ 578 if (msg.arguments.size() < 3) 579 return; 580 auto& nick = msg.arguments[1]; 581 auto& server = msg.arguments[2]; 582 add_server_message(String::format("* %s is using server %s", nick.characters(), server.characters())); 583} 584 585void IRCClient::handle_rpl_whoisuser(const Message& msg) 586{ 587 if (msg.arguments.size() < 6) 588 return; 589 auto& nick = msg.arguments[1]; 590 auto& username = msg.arguments[2]; 591 auto& host = msg.arguments[3]; 592 auto& asterisk = msg.arguments[4]; 593 auto& realname = msg.arguments[5]; 594 (void)asterisk; 595 add_server_message(String::format("* %s is %s@%s, real name: %s", 596 nick.characters(), 597 username.characters(), 598 host.characters(), 599 realname.characters())); 600} 601 602void IRCClient::handle_rpl_whoisidle(const Message& msg) 603{ 604 if (msg.arguments.size() < 3) 605 return; 606 auto& nick = msg.arguments[1]; 607 auto& secs = msg.arguments[2]; 608 add_server_message(String::format("* %s is %s seconds idle", nick.characters(), secs.characters())); 609} 610 611void IRCClient::handle_rpl_whoischannels(const Message& msg) 612{ 613 if (msg.arguments.size() < 3) 614 return; 615 auto& nick = msg.arguments[1]; 616 auto& channel_list = msg.arguments[2]; 617 add_server_message(String::format("* %s is in channels %s", nick.characters(), channel_list.characters())); 618} 619 620void IRCClient::handle_rpl_topicwhotime(const Message& msg) 621{ 622 if (msg.arguments.size() < 4) 623 return; 624 auto& channel_name = msg.arguments[1]; 625 auto& nick = msg.arguments[2]; 626 auto setat = msg.arguments[3]; 627 bool ok; 628 time_t setat_time = setat.to_uint(ok); 629 if (ok) 630 setat = Core::DateTime::from_timestamp(setat_time).to_string(); 631 ensure_channel(channel_name).add_message(String::format("*** (set by %s at %s)", nick.characters(), setat.characters()), Color::Blue); 632} 633 634void IRCClient::register_subwindow(IRCWindow& subwindow) 635{ 636 if (subwindow.type() == IRCWindow::Server) { 637 m_server_subwindow = &subwindow; 638 subwindow.set_log_buffer(*m_log); 639 } 640 m_windows.append(&subwindow); 641 m_client_window_list_model->update(); 642} 643 644void IRCClient::unregister_subwindow(IRCWindow& subwindow) 645{ 646 if (subwindow.type() == IRCWindow::Server) { 647 m_server_subwindow = &subwindow; 648 } 649 for (size_t i = 0; i < m_windows.size(); ++i) { 650 if (m_windows.at(i) == &subwindow) { 651 m_windows.remove(i); 652 break; 653 } 654 } 655 m_client_window_list_model->update(); 656} 657 658void IRCClient::handle_user_command(const String& input) 659{ 660 auto parts = input.split_view(' '); 661 if (parts.is_empty()) 662 return; 663 auto command = String(parts[0]).to_uppercase(); 664 if (command == "/NICK") { 665 if (parts.size() >= 2) 666 change_nick(parts[1]); 667 return; 668 } 669 if (command == "/JOIN") { 670 if (parts.size() >= 2) 671 join_channel(parts[1]); 672 return; 673 } 674 if (command == "/PART") { 675 if (parts.size() >= 2) 676 part_channel(parts[1]); 677 return; 678 } 679 if (command == "/QUERY") { 680 if (parts.size() >= 2) { 681 auto& query = ensure_query(parts[1]); 682 IRCAppWindow::the().set_active_window(query.window()); 683 } 684 return; 685 } 686 if (command == "/MSG") { 687 if (parts.size() < 3) 688 return; 689 auto nick = parts[1]; 690 auto& query = ensure_query(nick); 691 IRCAppWindow::the().set_active_window(query.window()); 692 query.say(input.view().substring_view_starting_after_substring(nick)); 693 return; 694 } 695 if (command == "/WHOIS") { 696 if (parts.size() >= 2) 697 send_whois(parts[1]); 698 return; 699 } 700} 701 702void IRCClient::change_nick(const String& nick) 703{ 704 send(String::format("NICK %s\r\n", nick.characters())); 705} 706 707void IRCClient::handle_whois_action(const String& nick) 708{ 709 send_whois(nick); 710} 711 712void IRCClient::handle_open_query_action(const String& nick) 713{ 714 ensure_query(nick); 715} 716 717void IRCClient::handle_change_nick_action(const String& nick) 718{ 719 change_nick(nick); 720} 721 722void IRCClient::handle_close_query_action(const String& nick) 723{ 724 m_queries.remove(nick); 725 m_client_window_list_model->update(); 726} 727 728void IRCClient::handle_join_action(const String& channel) 729{ 730 join_channel(channel); 731} 732 733void IRCClient::handle_part_action(const String& channel) 734{ 735 part_channel(channel); 736} 737 738void IRCClient::did_part_from_channel(Badge<IRCChannel>, IRCChannel& channel) 739{ 740 if (on_part_from_channel) 741 on_part_from_channel(channel); 742} 743 744void IRCClient::send_ctcp_response(const StringView& peer, const StringView& payload) 745{ 746 StringBuilder builder; 747 builder.append(0x01); 748 builder.append(payload); 749 builder.append(0x01); 750 auto message = builder.to_string(); 751 send_notice(peer, message); 752} 753 754void IRCClient::handle_ctcp_request(const StringView& peer, const StringView& payload) 755{ 756 dbg() << "handle_ctcp_request: " << payload; 757 758 if (payload == "VERSION") { 759 send_ctcp_response(peer, "VERSION IRC Client [x86] / Serenity OS"); 760 return; 761 } 762 763 if (payload.starts_with("PING")) { 764 send_ctcp_response(peer, payload); 765 return; 766 } 767} 768 769void IRCClient::handle_ctcp_response(const StringView& peer, const StringView& payload) 770{ 771 dbg() << "handle_ctcp_response(" << peer << "): " << payload; 772}