Serenity Operating System
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 <pwd.h>
43#include <time.h>
44#include <unistd.h>
45
46#define IRC_DEBUG
47
48enum IRCNumeric {
49 RPL_WHOISUSER = 311,
50 RPL_WHOISSERVER = 312,
51 RPL_WHOISOPERATOR = 313,
52 RPL_ENDOFWHO = 315,
53 RPL_WHOISIDLE = 317,
54 RPL_ENDOFWHOIS = 318,
55 RPL_WHOISCHANNELS = 319,
56 RPL_TOPIC = 332,
57 RPL_TOPICWHOTIME = 333,
58 RPL_NAMREPLY = 353,
59 RPL_ENDOFNAMES = 366,
60 RPL_BANLIST = 367,
61 RPL_ENDOFBANLIST = 368,
62 RPL_ENDOFWHOWAS = 369,
63 RPL_ENDOFMOTD = 376,
64 ERR_NOSUCHNICK = 401,
65 ERR_UNKNOWNCOMMAND = 421,
66 ERR_NICKNAMEINUSE = 433,
67};
68
69IRCClient::IRCClient()
70 : m_nickname("seren1ty")
71 , m_client_window_list_model(IRCWindowListModel::create(*this))
72 , m_log(IRCLogBuffer::create())
73 , m_config(Core::ConfigFile::get_for_app("IRCClient"))
74{
75 struct passwd* user_pw = getpwuid(getuid());
76 m_socket = Core::TCPSocket::construct(this);
77 m_nickname = m_config->read_entry("User", "Nickname", String::format("%s_seren1ty", user_pw->pw_name));
78 m_hostname = m_config->read_entry("Connection", "Server", "");
79 m_port = m_config->read_num_entry("Connection", "Port", 6667);
80 m_ctcp_version_reply = m_config->read_entry("CTCP", "VersionReply", "IRC Client [x86] / Serenity OS");
81 m_ctcp_userinfo_reply = m_config->read_entry("CTCP", "UserInfoReply", user_pw->pw_name);
82 m_ctcp_finger_reply = m_config->read_entry("CTCP", "FingerReply", user_pw->pw_name);
83}
84
85IRCClient::~IRCClient()
86{
87}
88
89void IRCClient::set_server(const String& hostname, int port)
90{
91 m_hostname = hostname;
92 m_port = port;
93 m_config->write_entry("Connection", "Server", hostname);
94 m_config->write_num_entry("Connection", "Port", port);
95 m_config->sync();
96}
97
98void IRCClient::on_socket_connected()
99{
100 m_notifier = Core::Notifier::construct(m_socket->fd(), Core::Notifier::Read);
101 m_notifier->on_ready_to_read = [this] { receive_from_server(); };
102
103 send_user();
104 send_nick();
105
106 auto channel_str = m_config->read_entry("Connection", "AutoJoinChannels", "#test");
107 dbgprintf("IRCClient: Channels to autojoin: %s\n", channel_str.characters());
108 auto channels = channel_str.split(',');
109 for (auto& channel : channels) {
110 join_channel(channel);
111 dbgprintf("IRCClient: Auto joining channel: %s\n", channel.characters());
112 }
113}
114
115bool IRCClient::connect()
116{
117 if (m_socket->is_connected())
118 ASSERT_NOT_REACHED();
119
120 m_socket->on_connected = [this] { on_socket_connected(); };
121 bool success = m_socket->connect(m_hostname, m_port);
122 if (!success)
123 return false;
124 return true;
125}
126
127void IRCClient::receive_from_server()
128{
129 while (m_socket->can_read_line()) {
130 auto line = m_socket->read_line(PAGE_SIZE);
131 if (line.is_null()) {
132 if (!m_socket->is_connected()) {
133 printf("IRCClient: Connection closed!\n");
134 exit(1);
135 }
136 ASSERT_NOT_REACHED();
137 }
138 process_line(move(line));
139 }
140}
141
142void IRCClient::process_line(ByteBuffer&& line)
143{
144 Message msg;
145 Vector<char, 32> prefix;
146 Vector<char, 32> command;
147 Vector<char, 256> current_parameter;
148 enum {
149 Start,
150 InPrefix,
151 InCommand,
152 InStartOfParameter,
153 InParameter,
154 InTrailingParameter,
155 } state
156 = Start;
157
158 for (size_t i = 0; i < line.size(); ++i) {
159 char ch = line[i];
160 if (ch == '\r')
161 continue;
162 if (ch == '\n')
163 break;
164 switch (state) {
165 case Start:
166 if (ch == ':') {
167 state = InPrefix;
168 continue;
169 }
170 state = InCommand;
171 [[fallthrough]];
172 case InCommand:
173 if (ch == ' ') {
174 state = InStartOfParameter;
175 continue;
176 }
177 command.append(ch);
178 continue;
179 case InPrefix:
180 if (ch == ' ') {
181 state = InCommand;
182 continue;
183 }
184 prefix.append(ch);
185 continue;
186 case InStartOfParameter:
187 if (ch == ':') {
188 state = InTrailingParameter;
189 continue;
190 }
191 state = InParameter;
192 [[fallthrough]];
193 case InParameter:
194 if (ch == ' ') {
195 if (!current_parameter.is_empty())
196 msg.arguments.append(String(current_parameter.data(), current_parameter.size()));
197 current_parameter.clear_with_capacity();
198 state = InStartOfParameter;
199 continue;
200 }
201 current_parameter.append(ch);
202 continue;
203 case InTrailingParameter:
204 current_parameter.append(ch);
205 continue;
206 }
207 }
208 if (!current_parameter.is_empty())
209 msg.arguments.append(String::copy(current_parameter));
210 msg.prefix = String::copy(prefix);
211 msg.command = String::copy(command);
212 handle(msg);
213}
214
215void IRCClient::send(const String& text)
216{
217 if (!m_socket->send(ByteBuffer::wrap(text.characters(), text.length()))) {
218 perror("send");
219 exit(1);
220 }
221}
222
223void IRCClient::send_user()
224{
225 send(String::format("USER %s 0 * :%s\r\n", m_nickname.characters(), m_nickname.characters()));
226}
227
228void IRCClient::send_nick()
229{
230 send(String::format("NICK %s\r\n", m_nickname.characters()));
231}
232
233void IRCClient::send_pong(const String& server)
234{
235 send(String::format("PONG %s\r\n", server.characters()));
236 sleep(1);
237}
238
239void IRCClient::join_channel(const String& channel_name)
240{
241 send(String::format("JOIN %s\r\n", channel_name.characters()));
242}
243
244void IRCClient::part_channel(const String& channel_name)
245{
246 send(String::format("PART %s\r\n", channel_name.characters()));
247}
248
249void IRCClient::send_whois(const String& nick)
250{
251 send(String::format("WHOIS %s\r\n", nick.characters()));
252}
253
254void IRCClient::handle(const Message& msg)
255{
256#ifdef IRC_DEBUG
257 printf("IRCClient::execute: prefix='%s', command='%s', arguments=%zu\n",
258 msg.prefix.characters(),
259 msg.command.characters(),
260 msg.arguments.size());
261
262 int i = 0;
263 for (auto& arg : msg.arguments) {
264 printf(" [%d]: %s\n", i, arg.characters());
265 ++i;
266 }
267#endif
268
269 bool is_numeric;
270 int numeric = msg.command.to_uint(is_numeric);
271
272 if (is_numeric) {
273 switch (numeric) {
274 case RPL_WHOISCHANNELS:
275 return handle_rpl_whoischannels(msg);
276 case RPL_ENDOFWHO:
277 return handle_rpl_endofwho(msg);
278 case RPL_ENDOFWHOIS:
279 return handle_rpl_endofwhois(msg);
280 case RPL_ENDOFWHOWAS:
281 return handle_rpl_endofwhowas(msg);
282 case RPL_ENDOFMOTD:
283 return handle_rpl_endofmotd(msg);
284 case RPL_WHOISOPERATOR:
285 return handle_rpl_whoisoperator(msg);
286 case RPL_WHOISSERVER:
287 return handle_rpl_whoisserver(msg);
288 case RPL_WHOISUSER:
289 return handle_rpl_whoisuser(msg);
290 case RPL_WHOISIDLE:
291 return handle_rpl_whoisidle(msg);
292 case RPL_TOPICWHOTIME:
293 return handle_rpl_topicwhotime(msg);
294 case RPL_TOPIC:
295 return handle_rpl_topic(msg);
296 case RPL_NAMREPLY:
297 return handle_rpl_namreply(msg);
298 case RPL_ENDOFNAMES:
299 return handle_rpl_endofnames(msg);
300 case RPL_BANLIST:
301 return handle_rpl_banlist(msg);
302 case RPL_ENDOFBANLIST:
303 return handle_rpl_endofbanlist(msg);
304 case ERR_NOSUCHNICK:
305 return handle_err_nosuchnick(msg);
306 case ERR_UNKNOWNCOMMAND:
307 return handle_err_unknowncommand(msg);
308 case ERR_NICKNAMEINUSE:
309 return handle_err_nicknameinuse(msg);
310 }
311 }
312
313 if (msg.command == "PING")
314 return handle_ping(msg);
315
316 if (msg.command == "JOIN")
317 return handle_join(msg);
318
319 if (msg.command == "PART")
320 return handle_part(msg);
321
322 if (msg.command == "QUIT")
323 return handle_quit(msg);
324
325 if (msg.command == "TOPIC")
326 return handle_topic(msg);
327
328 if (msg.command == "PRIVMSG")
329 return handle_privmsg_or_notice(msg, PrivmsgOrNotice::Privmsg);
330
331 if (msg.command == "NOTICE")
332 return handle_privmsg_or_notice(msg, PrivmsgOrNotice::Notice);
333
334 if (msg.command == "NICK")
335 return handle_nick(msg);
336
337 if (msg.arguments.size() >= 2)
338 add_server_message(String::format("[%s] %s", msg.command.characters(), msg.arguments[1].characters()));
339}
340
341void IRCClient::add_server_message(const String& text, Color color)
342{
343 m_log->add_message(0, "", text, color);
344 m_server_subwindow->did_add_message();
345}
346
347void IRCClient::send_topic(const String& channel_name, const String& text)
348{
349 send(String::format("TOPIC %s :%s\r\n", channel_name.characters(), text.characters()));
350}
351
352void IRCClient::send_invite(const String& channel_name, const String& nick)
353{
354 send(String::format("INVITE %s %s\r\n", nick.characters(), channel_name.characters()));
355}
356
357void IRCClient::send_banlist(const String& channel_name)
358{
359 send(String::format("MODE %s +b\r\n", channel_name.characters()));
360}
361
362void IRCClient::send_voice_user(const String& channel_name, const String& nick)
363{
364 send(String::format("MODE %s +v %s\r\n", channel_name.characters(), nick.characters()));
365}
366
367void IRCClient::send_devoice_user(const String& channel_name, const String& nick)
368{
369 send(String::format("MODE %s -v %s\r\n", channel_name.characters(), nick.characters()));
370}
371
372void IRCClient::send_hop_user(const String& channel_name, const String& nick)
373{
374 send(String::format("MODE %s +h %s\r\n", channel_name.characters(), nick.characters()));
375}
376
377void IRCClient::send_dehop_user(const String& channel_name, const String& nick)
378{
379 send(String::format("MODE %s -h %s\r\n", channel_name.characters(), nick.characters()));
380}
381
382void IRCClient::send_op_user(const String& channel_name, const String& nick)
383{
384 send(String::format("MODE %s +o %s\r\n", channel_name.characters(), nick.characters()));
385}
386
387void IRCClient::send_deop_user(const String& channel_name, const String& nick)
388{
389 send(String::format("MODE %s -o %s\r\n", channel_name.characters(), nick.characters()));
390}
391
392void IRCClient::send_kick(const String& channel_name, const String& nick, const String& comment)
393{
394 send(String::format("KICK %s %s :%s\r\n", channel_name.characters(), nick.characters(), comment.characters()));
395}
396
397void IRCClient::send_list()
398{
399 send("LIST\r\n");
400}
401
402void IRCClient::send_privmsg(const String& target, const String& text)
403{
404 send(String::format("PRIVMSG %s :%s\r\n", target.characters(), text.characters()));
405}
406
407void IRCClient::send_notice(const String& target, const String& text)
408{
409 send(String::format("NOTICE %s :%s\r\n", target.characters(), text.characters()));
410}
411
412void IRCClient::handle_user_input_in_channel(const String& channel_name, const String& input)
413{
414 if (input.is_empty())
415 return;
416 if (input[0] == '/')
417 return handle_user_command(input);
418 ensure_channel(channel_name).say(input);
419}
420
421void IRCClient::handle_user_input_in_query(const String& query_name, const String& input)
422{
423 if (input.is_empty())
424 return;
425 if (input[0] == '/')
426 return handle_user_command(input);
427 ensure_query(query_name).say(input);
428}
429
430void IRCClient::handle_user_input_in_server(const String& input)
431{
432 if (input.is_empty())
433 return;
434 if (input[0] == '/')
435 return handle_user_command(input);
436}
437
438String IRCClient::nick_without_prefix(const String& nick)
439{
440 assert(!nick.is_empty());
441 if (IRCClient::is_nick_prefix(nick[0]))
442 return nick.substring(1, nick.length() - 1);
443 return nick;
444}
445
446bool IRCClient::is_nick_prefix(char ch)
447{
448 switch (ch) {
449 case '@':
450 case '+':
451 case '~':
452 case '&':
453 case '%':
454 return true;
455 }
456 return false;
457}
458
459bool IRCClient::is_channel_prefix(char ch)
460{
461 switch (ch) {
462 case '&':
463 case '#':
464 case '+':
465 case '!':
466 return true;
467 }
468 return false;
469}
470
471static bool has_ctcp_payload(const StringView& string)
472{
473 return string.length() >= 2 && string[0] == 0x01 && string[string.length() - 1] == 0x01;
474}
475
476void IRCClient::handle_privmsg_or_notice(const Message& msg, PrivmsgOrNotice type)
477{
478 if (msg.arguments.size() < 2)
479 return;
480 if (msg.prefix.is_empty())
481 return;
482 auto parts = msg.prefix.split('!');
483 auto sender_nick = parts[0];
484 auto target = msg.arguments[0];
485
486 bool is_ctcp = has_ctcp_payload(msg.arguments[1]);
487
488#ifdef IRC_DEBUG
489 printf("handle_privmsg_or_notice: type='%s'%s, sender_nick='%s', target='%s'\n",
490 type == PrivmsgOrNotice::Privmsg ? "privmsg" : "notice",
491 is_ctcp ? " (ctcp)" : "",
492 sender_nick.characters(),
493 target.characters());
494#endif
495
496 if (sender_nick.is_empty())
497 return;
498
499 char sender_prefix = 0;
500 if (is_nick_prefix(sender_nick[0])) {
501 sender_prefix = sender_nick[0];
502 sender_nick = sender_nick.substring(1, sender_nick.length() - 1);
503 }
504
505 String message_text = msg.arguments[1];
506 auto message_color = Color::Black;
507
508 if (is_ctcp) {
509 auto ctcp_payload = msg.arguments[1].substring_view(1, msg.arguments[1].length() - 2);
510 if (type == PrivmsgOrNotice::Privmsg)
511 handle_ctcp_request(sender_nick, ctcp_payload);
512 else
513 handle_ctcp_response(sender_nick, ctcp_payload);
514 StringBuilder builder;
515 builder.append("(CTCP) ");
516 builder.append(ctcp_payload);
517 message_text = builder.to_string();
518 message_color = Color::Blue;
519 }
520
521 {
522 auto it = m_channels.find(target);
523 if (it != m_channels.end()) {
524 (*it).value->add_message(sender_prefix, sender_nick, message_text, message_color);
525 return;
526 }
527 }
528
529 // For NOTICE or CTCP messages, only put them in query if one already exists.
530 // Otherwise, put them in the server window. This seems to match other clients.
531 IRCQuery* query = nullptr;
532 if (is_ctcp || type == PrivmsgOrNotice::Notice) {
533 query = query_with_name(sender_nick);
534 } else {
535 query = &ensure_query(sender_nick);
536 }
537 if (query)
538 query->add_message(sender_prefix, sender_nick, message_text, message_color);
539 else {
540 add_server_message(String::format("<%s> %s", sender_nick.characters(), message_text.characters()), message_color);
541 }
542}
543
544IRCQuery* IRCClient::query_with_name(const String& name)
545{
546 return const_cast<IRCQuery*>(m_queries.get(name).value_or(nullptr));
547}
548
549IRCQuery& IRCClient::ensure_query(const String& name)
550{
551 auto it = m_queries.find(name);
552 if (it != m_queries.end())
553 return *(*it).value;
554 auto query = IRCQuery::create(*this, name);
555 auto& query_reference = *query;
556 m_queries.set(name, query);
557 return query_reference;
558}
559
560IRCChannel& IRCClient::ensure_channel(const String& name)
561{
562 auto it = m_channels.find(name);
563 if (it != m_channels.end())
564 return *(*it).value;
565 auto channel = IRCChannel::create(*this, name);
566 auto& channel_reference = *channel;
567 m_channels.set(name, channel);
568 return channel_reference;
569}
570
571void IRCClient::handle_ping(const Message& msg)
572{
573 if (msg.arguments.size() < 1)
574 return;
575 m_log->add_message(0, "", "Ping? Pong!");
576 send_pong(msg.arguments[0]);
577}
578
579void IRCClient::handle_join(const Message& msg)
580{
581 if (msg.arguments.size() != 1)
582 return;
583 auto prefix_parts = msg.prefix.split('!');
584 if (prefix_parts.size() < 1)
585 return;
586 auto nick = prefix_parts[0];
587 auto& channel_name = msg.arguments[0];
588 ensure_channel(channel_name).handle_join(nick, msg.prefix);
589}
590
591void IRCClient::handle_part(const Message& msg)
592{
593 if (msg.arguments.size() < 1)
594 return;
595 auto prefix_parts = msg.prefix.split('!');
596 if (prefix_parts.size() < 1)
597 return;
598 auto nick = prefix_parts[0];
599 auto& channel_name = msg.arguments[0];
600 ensure_channel(channel_name).handle_part(nick, msg.prefix);
601}
602
603void IRCClient::handle_quit(const Message& msg)
604{
605 if (msg.arguments.size() < 1)
606 return;
607 auto prefix_parts = msg.prefix.split('!');
608 if (prefix_parts.size() < 1)
609 return;
610 auto nick = prefix_parts[0];
611 auto& message = msg.arguments[0];
612 for (auto& it : m_channels) {
613 it.value->handle_quit(nick, msg.prefix, message);
614 }
615}
616
617void IRCClient::handle_nick(const Message& msg)
618{
619 auto prefix_parts = msg.prefix.split('!');
620 if (prefix_parts.size() < 1)
621 return;
622 auto old_nick = prefix_parts[0];
623 if (msg.arguments.size() != 1)
624 return;
625 auto& new_nick = msg.arguments[0];
626 if (old_nick == m_nickname)
627 m_nickname = new_nick;
628 add_server_message(String::format("~ %s changed nickname to %s", old_nick.characters(), new_nick.characters()));
629 if (on_nickname_changed)
630 on_nickname_changed(new_nick);
631 for (auto& it : m_channels) {
632 it.value->notify_nick_changed(old_nick, new_nick);
633 }
634}
635
636void IRCClient::handle_topic(const Message& msg)
637{
638 if (msg.arguments.size() != 2)
639 return;
640 auto prefix_parts = msg.prefix.split('!');
641 if (prefix_parts.size() < 1)
642 return;
643 auto nick = prefix_parts[0];
644 auto& channel_name = msg.arguments[0];
645 ensure_channel(channel_name).handle_topic(nick, msg.arguments[1]);
646}
647
648void IRCClient::handle_rpl_topic(const Message& msg)
649{
650 if (msg.arguments.size() < 3)
651 return;
652 auto& channel_name = msg.arguments[1];
653 auto& topic = msg.arguments[2];
654 ensure_channel(channel_name).handle_topic({}, topic);
655}
656
657void IRCClient::handle_rpl_namreply(const Message& msg)
658{
659 if (msg.arguments.size() < 4)
660 return;
661 auto& channel_name = msg.arguments[2];
662 auto& channel = ensure_channel(channel_name);
663 auto members = msg.arguments[3].split(' ');
664
665 quick_sort(members, [](auto& a, auto& b) {
666 return strcasecmp(a.characters(), b.characters()) < 0;
667 });
668
669 for (auto& member : members) {
670 if (member.is_empty())
671 continue;
672 char prefix = 0;
673 if (is_nick_prefix(member[0]))
674 prefix = member[0];
675 channel.add_member(member, prefix);
676 }
677}
678
679void IRCClient::handle_rpl_endofnames(const Message&)
680{
681 add_server_message("// End of NAMES");
682}
683
684void IRCClient::handle_rpl_banlist(const Message& msg)
685{
686 if (msg.arguments.size() < 5)
687 return;
688 auto& channel = msg.arguments[1];
689 auto& mask = msg.arguments[2];
690 auto& user = msg.arguments[3];
691 auto& datestamp = msg.arguments[4];
692 add_server_message(String::format("* %s: %s on %s by %s", channel.characters(), mask.characters(), datestamp.characters(), user.characters()));
693}
694
695void IRCClient::handle_rpl_endofbanlist(const Message&)
696{
697 add_server_message("// End of BANLIST");
698}
699
700void IRCClient::handle_rpl_endofwho(const Message&)
701{
702 add_server_message("// End of WHO");
703}
704
705void IRCClient::handle_rpl_endofwhois(const Message&)
706{
707 add_server_message("// End of WHOIS");
708}
709
710void IRCClient::handle_rpl_endofwhowas(const Message&)
711{
712 add_server_message("// End of WHOWAS");
713}
714
715void IRCClient::handle_rpl_endofmotd(const Message&)
716{
717 add_server_message("// End of MOTD");
718}
719
720void IRCClient::handle_rpl_whoisoperator(const Message& msg)
721{
722 if (msg.arguments.size() < 2)
723 return;
724 auto& nick = msg.arguments[1];
725 add_server_message(String::format("* %s is an IRC operator", nick.characters()));
726}
727
728void IRCClient::handle_rpl_whoisserver(const Message& msg)
729{
730 if (msg.arguments.size() < 3)
731 return;
732 auto& nick = msg.arguments[1];
733 auto& server = msg.arguments[2];
734 add_server_message(String::format("* %s is using server %s", nick.characters(), server.characters()));
735}
736
737void IRCClient::handle_rpl_whoisuser(const Message& msg)
738{
739 if (msg.arguments.size() < 6)
740 return;
741 auto& nick = msg.arguments[1];
742 auto& username = msg.arguments[2];
743 auto& host = msg.arguments[3];
744 auto& asterisk = msg.arguments[4];
745 auto& realname = msg.arguments[5];
746 (void)asterisk;
747 add_server_message(String::format("* %s is %s@%s, real name: %s",
748 nick.characters(),
749 username.characters(),
750 host.characters(),
751 realname.characters()));
752}
753
754void IRCClient::handle_rpl_whoisidle(const Message& msg)
755{
756 if (msg.arguments.size() < 3)
757 return;
758 auto& nick = msg.arguments[1];
759 auto& secs = msg.arguments[2];
760 add_server_message(String::format("* %s is %s seconds idle", nick.characters(), secs.characters()));
761}
762
763void IRCClient::handle_rpl_whoischannels(const Message& msg)
764{
765 if (msg.arguments.size() < 3)
766 return;
767 auto& nick = msg.arguments[1];
768 auto& channel_list = msg.arguments[2];
769 add_server_message(String::format("* %s is in channels %s", nick.characters(), channel_list.characters()));
770}
771
772void IRCClient::handle_rpl_topicwhotime(const Message& msg)
773{
774 if (msg.arguments.size() < 4)
775 return;
776 auto& channel_name = msg.arguments[1];
777 auto& nick = msg.arguments[2];
778 auto setat = msg.arguments[3];
779 bool ok;
780 time_t setat_time = setat.to_uint(ok);
781 if (ok)
782 setat = Core::DateTime::from_timestamp(setat_time).to_string();
783 ensure_channel(channel_name).add_message(String::format("*** (set by %s at %s)", nick.characters(), setat.characters()), Color::Blue);
784}
785
786void IRCClient::handle_err_nosuchnick(const Message& msg)
787{
788 if (msg.arguments.size() < 3)
789 return;
790 auto& nick = msg.arguments[1];
791 auto& message = msg.arguments[2];
792 add_server_message(String::format("* %s :%s", nick.characters(), message.characters()));
793}
794
795void IRCClient::handle_err_unknowncommand(const Message& msg)
796{
797 if (msg.arguments.size() < 2)
798 return;
799 auto& cmd = msg.arguments[1];
800 add_server_message(String::format("* Unknown command: %s", cmd.characters()));
801}
802
803void IRCClient::handle_err_nicknameinuse(const Message& msg)
804{
805 if (msg.arguments.size() < 2)
806 return;
807 auto& nick = msg.arguments[1];
808 add_server_message(String::format("* %s :Nickname in use", nick.characters()));
809}
810
811void IRCClient::register_subwindow(IRCWindow& subwindow)
812{
813 if (subwindow.type() == IRCWindow::Server) {
814 m_server_subwindow = &subwindow;
815 subwindow.set_log_buffer(*m_log);
816 }
817 m_windows.append(&subwindow);
818 m_client_window_list_model->update();
819}
820
821void IRCClient::unregister_subwindow(IRCWindow& subwindow)
822{
823 if (subwindow.type() == IRCWindow::Server) {
824 m_server_subwindow = &subwindow;
825 }
826 for (size_t i = 0; i < m_windows.size(); ++i) {
827 if (m_windows.at(i) == &subwindow) {
828 m_windows.remove(i);
829 break;
830 }
831 }
832 m_client_window_list_model->update();
833}
834
835void IRCClient::handle_user_command(const String& input)
836{
837 auto parts = input.split_view(' ');
838 if (parts.is_empty())
839 return;
840 auto command = String(parts[0]).to_uppercase();
841 if (command == "/RAW") {
842 if (parts.size() <= 1)
843 return;
844 int command_length = command.length() + 1;
845 StringView raw_message = input.view().substring_view(command_length, input.view().length() - command_length);
846 send(String::format("%s\r\n", String(raw_message).characters()));
847 return;
848 }
849 if (command == "/NICK") {
850 if (parts.size() >= 2)
851 change_nick(parts[1]);
852 return;
853 }
854 if (command == "/JOIN") {
855 if (parts.size() >= 2)
856 join_channel(parts[1]);
857 return;
858 }
859 if (command == "/PART") {
860 if (parts.size() >= 2) {
861 auto channel = parts[1];
862 part_channel(channel);
863 } else {
864 auto* window = current_window();
865 if (!window || window->type() != IRCWindow::Type::Channel)
866 return;
867 auto channel = window->channel().name();
868 join_channel(channel);
869 }
870 return;
871 }
872 if (command == "/CYCLE") {
873 if (parts.size() >= 2) {
874 auto channel = parts[1];
875 part_channel(channel);
876 join_channel(channel);
877 } else {
878 auto* window = current_window();
879 if (!window || window->type() != IRCWindow::Type::Channel)
880 return;
881 auto channel = window->channel().name();
882 part_channel(channel);
883 join_channel(channel);
884 }
885 return;
886 }
887 if (command == "/BANLIST") {
888 if (parts.size() >= 2) {
889 auto channel = parts[1];
890 send_banlist(channel);
891 } else {
892 auto* window = current_window();
893 if (!window || window->type() != IRCWindow::Type::Channel)
894 return;
895 auto channel = window->channel().name();
896 send_banlist(channel);
897 }
898 return;
899 }
900 if (command == "/TOPIC") {
901 if (parts.size() < 2)
902 return;
903 if (parts[1].is_empty())
904 return;
905
906 if (is_channel_prefix(parts[1][0])) {
907 if (parts.size() < 3)
908 return;
909 auto channel = parts[1];
910 auto topic = input.view().substring_view_starting_after_substring(channel);
911 send_topic(channel, topic);
912 } else {
913 auto* window = current_window();
914 if (!window || window->type() != IRCWindow::Type::Channel)
915 return;
916 auto channel = window->channel().name();
917 auto topic = input.view().substring_view_starting_after_substring(parts[0]);
918 send_topic(channel, topic);
919 }
920 return;
921 }
922 if (command == "/KICK") {
923 if (parts.size() < 2)
924 return;
925 if (parts[1].is_empty())
926 return;
927
928 if (is_channel_prefix(parts[1][0])) {
929 if (parts.size() < 3)
930 return;
931 auto channel = parts[1];
932 auto nick = parts[2];
933 auto reason = input.view().substring_view_starting_after_substring(nick);
934 send_kick(channel, nick, reason);
935 } else {
936 auto* window = current_window();
937 if (!window || window->type() != IRCWindow::Type::Channel)
938 return;
939 auto channel = window->channel().name();
940 auto nick = parts[1];
941 auto reason = input.view().substring_view_starting_after_substring(nick);
942 send_kick(channel, nick, reason);
943 }
944 return;
945 }
946 if (command == "/LIST") {
947 send_list();
948 return;
949 }
950 if (command == "/QUERY") {
951 if (parts.size() >= 2) {
952 auto& query = ensure_query(parts[1]);
953 IRCAppWindow::the().set_active_window(query.window());
954 }
955 return;
956 }
957 if (command == "/MSG") {
958 if (parts.size() < 3)
959 return;
960 auto nick = parts[1];
961 auto& query = ensure_query(nick);
962 IRCAppWindow::the().set_active_window(query.window());
963 query.say(input.view().substring_view_starting_after_substring(nick));
964 return;
965 }
966 if (command == "/WHOIS") {
967 if (parts.size() >= 2)
968 send_whois(parts[1]);
969 return;
970 }
971}
972
973void IRCClient::change_nick(const String& nick)
974{
975 send(String::format("NICK %s\r\n", nick.characters()));
976}
977
978void IRCClient::handle_list_channels_action()
979{
980 send_list();
981}
982
983void IRCClient::handle_whois_action(const String& nick)
984{
985 send_whois(nick);
986}
987
988void IRCClient::handle_open_query_action(const String& nick)
989{
990 ensure_query(nick);
991}
992
993void IRCClient::handle_change_nick_action(const String& nick)
994{
995 change_nick(nick);
996}
997
998void IRCClient::handle_change_topic_action(const String& channel, const String& topic)
999{
1000 send_topic(channel, topic);
1001}
1002
1003void IRCClient::handle_invite_user_action(const String& channel, const String& nick)
1004{
1005 send_invite(channel, nick);
1006}
1007
1008void IRCClient::handle_banlist_action(const String& channel)
1009{
1010 send_banlist(channel);
1011}
1012
1013void IRCClient::handle_voice_user_action(const String& channel, const String& nick)
1014{
1015 send_voice_user(channel, nick);
1016}
1017
1018void IRCClient::handle_devoice_user_action(const String& channel, const String& nick)
1019{
1020 send_devoice_user(channel, nick);
1021}
1022
1023void IRCClient::handle_hop_user_action(const String& channel, const String& nick)
1024{
1025 send_hop_user(channel, nick);
1026}
1027
1028void IRCClient::handle_dehop_user_action(const String& channel, const String& nick)
1029{
1030 send_dehop_user(channel, nick);
1031}
1032
1033void IRCClient::handle_op_user_action(const String& channel, const String& nick)
1034{
1035 send_op_user(channel, nick);
1036}
1037
1038void IRCClient::handle_deop_user_action(const String& channel, const String& nick)
1039{
1040 send_deop_user(channel, nick);
1041}
1042
1043void IRCClient::handle_kick_user_action(const String& channel, const String& nick, const String& message)
1044{
1045 send_kick(channel, nick, message);
1046}
1047
1048void IRCClient::handle_close_query_action(const String& nick)
1049{
1050 m_queries.remove(nick);
1051 m_client_window_list_model->update();
1052}
1053
1054void IRCClient::handle_join_action(const String& channel)
1055{
1056 join_channel(channel);
1057}
1058
1059void IRCClient::handle_part_action(const String& channel)
1060{
1061 part_channel(channel);
1062}
1063
1064void IRCClient::handle_cycle_channel_action(const String& channel)
1065{
1066 part_channel(channel);
1067 join_channel(channel);
1068}
1069
1070void IRCClient::did_part_from_channel(Badge<IRCChannel>, IRCChannel& channel)
1071{
1072 if (on_part_from_channel)
1073 on_part_from_channel(channel);
1074}
1075
1076void IRCClient::send_ctcp_response(const StringView& peer, const StringView& payload)
1077{
1078 StringBuilder builder;
1079 builder.append(0x01);
1080 builder.append(payload);
1081 builder.append(0x01);
1082 auto message = builder.to_string();
1083 send_notice(peer, message);
1084}
1085
1086void IRCClient::handle_ctcp_request(const StringView& peer, const StringView& payload)
1087{
1088 dbg() << "handle_ctcp_request: " << payload;
1089
1090 if (payload == "VERSION") {
1091 auto version = ctcp_version_reply();
1092 if (version.is_empty())
1093 return;
1094 send_ctcp_response(peer, String::format("VERSION %s", version.characters()));
1095 return;
1096 }
1097
1098 if (payload == "USERINFO") {
1099 auto userinfo = ctcp_userinfo_reply();
1100 if (userinfo.is_empty())
1101 return;
1102 send_ctcp_response(peer, String::format("USERINFO %s", userinfo.characters()));
1103 return;
1104 }
1105
1106 if (payload == "FINGER") {
1107 auto finger = ctcp_finger_reply();
1108 if (finger.is_empty())
1109 return;
1110 send_ctcp_response(peer, String::format("FINGER %s", finger.characters()));
1111 return;
1112 }
1113
1114 if (payload.starts_with("PING")) {
1115 send_ctcp_response(peer, payload);
1116 return;
1117 }
1118}
1119
1120void IRCClient::handle_ctcp_response(const StringView& peer, const StringView& payload)
1121{
1122 dbg() << "handle_ctcp_response(" << peer << "): " << payload;
1123}