1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2013-2017, The OpenClonk Team and contributors
6 * Copyright (c) 2017-2020, The LegacyClonk Team and contributors
7 *
8 * Distributed under the terms of the ISC license; see accompanying file
9 * "COPYING" for details.
10 *
11 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
12 * See accompanying file "TRADEMARK" for details.
13 *
14 * To redistribute this file separately, substitute the full license texts
15 * for the above references.
16 */
17
18#include "C4Include.h"
19#include "C4Network2IRC.h"
20#include "C4Config.h"
21#include "C4Version.h"
22#include "C4InteractiveThread.h"
23
24#include "C4Gui.h" // for clearly visible error message
25
26#include <cctype> // for isdigit
27#include <format>
28
29// Helper for IRC command parameter parsing
30StdStrBuf ircExtractPar(const char **ppPar)
31{
32 // No parameter left?
33 if (!ppPar || !*ppPar || !**ppPar)
34 return StdStrBuf("");
35 // Last parameter?
36 StdStrBuf Result;
37 if (**ppPar == ':')
38 {
39 // Reference everything after the double-colon
40 Result.Ref(pnData: *ppPar + 1);
41 *ppPar = nullptr;
42 }
43 else
44 {
45 // Copy until next space (or end of string)
46 Result.CopyUntil(szString: *ppPar, cUntil: ' ');
47 // Go over parameters
48 *ppPar += Result.getLength();
49 if (**ppPar == ' ')
50 (*ppPar)++;
51 else
52 *ppPar = nullptr;
53 }
54 // Done
55 return Result;
56}
57
58// *** C4Network2IRCUser
59
60C4Network2IRCUser::C4Network2IRCUser(const char *szName)
61 : Name(szName) {}
62
63// *** C4Network2IRCChannel
64
65C4Network2IRCChannel::C4Network2IRCChannel(const char *szName)
66 : Name(szName), pUsers(nullptr), fReceivingUsers(false) {}
67
68C4Network2IRCChannel::~C4Network2IRCChannel()
69{
70 ClearUsers();
71}
72
73C4Network2IRCUser *C4Network2IRCChannel::getUser(const char *szName) const
74{
75 for (C4Network2IRCUser *pUser = pUsers; pUser; pUser = pUser->Next)
76 if (SEqual(szStr1: pUser->getName(), szStr2: szName))
77 return pUser;
78 return nullptr;
79}
80
81void C4Network2IRCChannel::OnUsers(const char *szUsers, const char *szPrefixes)
82{
83 // Find actual prefixes
84 szPrefixes = SSearch(szString: szPrefixes, szIndex: ")");
85 // Reconstructs the list
86 if (!fReceivingUsers)
87 ClearUsers();
88 while (szUsers && *szUsers)
89 {
90 // Get user name
91 StdStrBuf PrefixedName = ircExtractPar(ppPar: &szUsers);
92 // Remove prefix(es)
93 const char *szName = PrefixedName.getData();
94 if (szPrefixes)
95 while (strchr(s: szPrefixes, c: *szName))
96 szName++;
97 // Copy prefix
98 StdStrBuf Prefix;
99 Prefix.Copy(pnData: PrefixedName.getData(), iChars: szName - PrefixedName.getData());
100 // Add user
101 AddUser(szName)->SetPrefix(Prefix.getData());
102 }
103 // Set flag the user list won't get cleared again until OnUsersEnd is called
104 fReceivingUsers = true;
105}
106
107void C4Network2IRCChannel::OnUsersEnd()
108{
109 // Reset flag
110 fReceivingUsers = false;
111}
112
113void C4Network2IRCChannel::OnTopic(const char *szTopic)
114{
115 Topic = szTopic;
116}
117
118void C4Network2IRCChannel::OnKick(const char *szUser, const char *szComment)
119{
120 // Remove named user from channel list
121 C4Network2IRCUser *pUser = getUser(szName: szUser);
122 if (pUser) DeleteUser(pUser);
123}
124
125void C4Network2IRCChannel::OnPart(const char *szUser, const char *szComment)
126{
127 // Remove named user from channel list
128 C4Network2IRCUser *pUser = getUser(szName: szUser);
129 if (pUser) DeleteUser(pUser);
130}
131
132void C4Network2IRCChannel::OnJoin(const char *szUser)
133{
134 // Add user (do not set prefix)
135 if (!getUser(szName: szUser))
136 AddUser(szName: szUser);
137}
138
139C4Network2IRCUser *C4Network2IRCChannel::AddUser(const char *szName)
140{
141 // Check if the user already exists
142 C4Network2IRCUser *pUser = getUser(szName);
143 if (pUser) return pUser;
144 // Add to list
145 pUser = new C4Network2IRCUser(szName);
146 pUser->Next = pUsers;
147 pUsers = pUser;
148 return pUser;
149}
150
151void C4Network2IRCChannel::DeleteUser(C4Network2IRCUser *pUser)
152{
153 // Unlink
154 if (pUser == pUsers)
155 pUsers = pUser->Next;
156 else
157 {
158 C4Network2IRCUser *pPrev = pUsers;
159 while (pPrev && pPrev->Next != pUser)
160 pPrev = pPrev->Next;
161 if (pPrev)
162 pPrev->Next = pUser->Next;
163 }
164 // Delete
165 delete pUser;
166}
167
168void C4Network2IRCChannel::ClearUsers()
169{
170 while (pUsers)
171 DeleteUser(pUser: pUsers);
172}
173
174// *** C4Network2IRCClient
175
176C4Network2IRCClient::C4Network2IRCClient()
177 : fConnecting(false), fConnected(false),
178 pChannels(nullptr),
179 pLog(nullptr), pLogEnd(nullptr), iLogLength(0), iUnreadLogLength(0),
180 pNotify(nullptr) {}
181
182C4Network2IRCClient::~C4Network2IRCClient()
183{
184 Close();
185}
186
187void C4Network2IRCClient::PackPacket(const C4NetIOPacket &rPacket, StdBuf &rOutBuf)
188{
189 // Enlarge buffer
190 int iSize = rPacket.getSize(),
191 iPos = rOutBuf.getSize();
192 rOutBuf.Grow(iGrow: iSize + 2);
193 // Write packet
194 rOutBuf.Write(Buf2: rPacket, iAt: iPos);
195 // Terminate
196 uint8_t *pPos = rOutBuf.getMPtr<uint8_t>(pos: iPos + iSize);
197 *pPos = '\r'; *(pPos + 1) = '\n';
198}
199
200size_t C4Network2IRCClient::UnpackPacket(const StdBuf &rInBuf, const C4NetIO::addr_t &addr)
201{
202 // Find line separation
203 const char *pSep = reinterpret_cast<const char *>(memchr(s: rInBuf.getData(), c: '\n', n: rInBuf.getSize()));
204 if (!pSep)
205 return 0;
206 // Check if it's actually correct separation (rarely the case)
207 int iSize = pSep - rInBuf.getPtr<char>() + 1,
208 iLength = iSize - 1;
209 if (iLength && *(pSep - 1) == '\r')
210 iLength--;
211 // Copy the line
212 StdStrBuf Buf; Buf.Copy(pnData: rInBuf.getPtr<char>(), iChars: iLength);
213 // Ignore prefix
214 const char *pMsg = Buf.getData();
215 StdStrBuf Prefix;
216 if (*pMsg == ':')
217 {
218 Prefix.CopyUntil(szString: pMsg + 1, cUntil: ' ');
219 pMsg += Prefix.getLength() + 1;
220 }
221 // Strip whitespace
222 while (*pMsg == ' ')
223 pMsg++;
224 // Ignore empty message
225 if (!*pMsg)
226 return iSize;
227 // Get command
228 StdStrBuf Cmd; Cmd.CopyUntil(szString: pMsg, cUntil: ' ');
229 // Precess command
230 const char *szParameters = SSearch(szString: pMsg, szIndex: " ");
231 OnCommand(szSender: Prefix.getData(), szCommand: Cmd.getData(), szParameters: szParameters ? szParameters : "");
232 // Consume the line
233 return iSize;
234}
235
236bool C4Network2IRCClient::OnConn(const C4NetIO::addr_t &AddrPeer, const C4NetIO::addr_t &AddrConnect, const addr_t *pOwnAddr, C4NetIO *pNetIO)
237{
238 // Security checks
239 if (!fConnecting || fConnected || AddrConnect != ServerAddr) return false;
240 CStdLock Lock(&CSec);
241 // Save connection data
242 fConnected = true;
243 fConnecting = false;
244 C4Network2IRCClient::PeerAddr = AddrPeer;
245 // Send welcome message
246 if (!Password.isNull())
247 Send(szCommand: "PASS", szParameters: Password.getData());
248 Send(szCommand: "NICK", szParameters: Nick.getData());
249 Send(szCommand: "USER", szParameters: std::format(fmt: "clonk x x :{}", args: RealName.getData()).c_str());
250 // Okay
251 return true;
252}
253
254void C4Network2IRCClient::OnDisconn(const C4NetIO::addr_t &AddrPeer, C4NetIO *pNetIO, const char *szReason)
255{
256 fConnected = false;
257 // Show a message with the reason
258 PushMessage(eType: MSG_Status, szSource: "", szTarget: Nick.getData(), szText: LoadResStr(id: C4ResStrTableKey::IDS_MSG_DISCONNECTEDFROMSERVER, args&: szReason).c_str());
259}
260
261void C4Network2IRCClient::OnPacket(const class C4NetIOPacket &rPacket, C4NetIO *pNetIO)
262{
263 // Won't get called
264}
265
266C4Network2IRCChannel *C4Network2IRCClient::getFirstChannel() const
267{
268 return pChannels;
269}
270
271C4Network2IRCChannel *C4Network2IRCClient::getNextChannel(C4Network2IRCChannel *pPrevChan) const
272{
273 return pPrevChan ? pPrevChan->Next : pChannels;
274}
275
276C4Network2IRCChannel *C4Network2IRCClient::getChannel(const char *szName) const
277{
278 for (C4Network2IRCChannel *pChan = pChannels; pChan; pChan = pChan->Next)
279 if (SEqualNoCase(szStr1: pChan->getName(), szStr2: szName))
280 return pChan;
281 return nullptr;
282}
283
284void C4Network2IRCClient::ClearMessageLog()
285{
286 // Clear log
287 while (iLogLength)
288 PopMessage();
289}
290
291void C4Network2IRCClient::MarkMessageLogRead()
292{
293 // set read marker to last message
294 pLogLastRead = pLogEnd;
295 iUnreadLogLength = 0;
296 // message buffer is smaller for messages already read: Remove old ones
297 while (iLogLength > C4NetIRCMaxReadLogLength)
298 PopMessage();
299}
300
301bool C4Network2IRCClient::Connect(const char *szServer, const char *szNick, const char *szRealName, const char *szPassword, const char *szAutoJoin)
302{
303 // Already connected? Close connection
304 if (fConnecting || fConnected)
305 Close();
306 // Initialize
307 C4NetIOTCP::SetCallback(this);
308 if (!Init())
309 return false;
310
311 // Resolve address
312 ServerAddr.SetAddress(addr: StdStrBuf(szServer));
313 if (ServerAddr.IsNull())
314 {
315 SetError(strnError: "Could no resolve server address!"); return false;
316 }
317 ServerAddr.SetDefaultPort(6666);
318
319 // Set connection data
320 Nick = szNick; RealName = szRealName;
321 Password = szPassword; AutoJoin = szAutoJoin;
322 // Truncate password
323 if (Password.getLength() > 31)
324 Password.SetLength(31);
325 // Start connecting
326 if (!C4NetIOTCP::Connect(addr: ServerAddr))
327 return false;
328 // Reset status data
329 Prefixes = "(ov)@+";
330 // Okay, let's wait for the connection.
331 fConnecting = true;
332 return true;
333}
334
335bool C4Network2IRCClient::Close()
336{
337 // Close network
338 C4NetIOTCP::Close();
339 // Clear channels
340 while (pChannels)
341 DeleteChannel(pChannel: pChannels);
342 // Clear log
343 ClearMessageLog();
344 // Reset flags
345 fConnected = fConnecting = false;
346 return true;
347}
348
349bool C4Network2IRCClient::Send(const char *szCommand, const char *szParameters)
350{
351 if (!fConnected)
352 {
353 SetError(strnError: "not connected"); return false;
354 }
355 // Create message
356 std::string msg;
357 if (szParameters)
358 msg = std::format(fmt: "{} {}", args&: szCommand, args&: szParameters);
359 else
360 msg = szCommand;
361 // Send
362 return C4NetIOTCP::Send(rPacket: C4NetIOPacket(msg.c_str(), msg.size(), false, PeerAddr));
363}
364
365bool C4Network2IRCClient::Quit(const char *szReason)
366{
367 if (!Send(szCommand: "QUIT", szParameters: std::format(fmt: ":{}", args&: szReason).c_str()))
368 return false;
369 // Must be last message
370 return Close();
371}
372
373bool C4Network2IRCClient::Join(const char *szChannel)
374{
375 return Send(szCommand: "JOIN", szParameters: szChannel);
376}
377
378bool C4Network2IRCClient::Part(const char *szChannel)
379{
380 return Send(szCommand: "PART", szParameters: szChannel);
381}
382
383bool C4Network2IRCClient::Message(const char *szTarget, const char *szText)
384{
385 if (!Send(szCommand: "PRIVMSG", szParameters: std::format(fmt: "{} :{}", args&: szTarget, args&: szText).c_str()))
386 return false;
387 PushMessage(eType: MSG_Message, szSource: Nick.getData(), szTarget, szText);
388 return true;
389}
390
391bool C4Network2IRCClient::Notice(const char *szTarget, const char *szText)
392{
393 if (!Send(szCommand: "NOTICE", szParameters: std::format(fmt: "{} :{}", args&: szTarget, args&: szText).c_str()))
394 return false;
395 PushMessage(eType: MSG_Notice, szSource: Nick.getData(), szTarget, szText);
396 return true;
397}
398
399bool C4Network2IRCClient::Action(const char *szTarget, const char *szText)
400{
401 if (!Send(szCommand: "PRIVMSG", szParameters: std::format(fmt: "{} :\1ACTION {}\1", args&: szTarget, args&: szText).c_str()))
402 return false;
403 PushMessage(eType: MSG_Action, szSource: Nick.getData(), szTarget, szText);
404 return true;
405}
406
407bool C4Network2IRCClient::ChangeNick(const char *szNewNick)
408{
409 return Send(szCommand: "NICK", szParameters: szNewNick);
410}
411
412void C4Network2IRCClient::OnCommand(const char *szSender, const char *szCommand, const char *szParameters)
413{
414 CStdLock Lock(&CSec);
415 // Numeric command?
416 if (isdigit(static_cast<unsigned char>(*szCommand)) && SLen(sptr: szCommand) == 3)
417 {
418 OnNumericCommand(szSender, iCommand: atoi(nptr: szCommand), szParameters);
419 return;
420 }
421 // Sender's nick
422 StdStrBuf SenderNick;
423 if (szSender) SenderNick.CopyUntil(szString: szSender, cUntil: '!');
424 // Ping?
425 if (SEqualNoCase(szStr1: szCommand, szStr2: "PING"))
426 Send(szCommand: "PONG", szParameters);
427 // Message?
428 if (SEqualNoCase(szStr1: szCommand, szStr2: "NOTICE") || SEqualNoCase(szStr1: szCommand, szStr2: "PRIVMSG"))
429 {
430 // Get target
431 StdStrBuf Target = ircExtractPar(ppPar: &szParameters);
432 // Get text
433 StdStrBuf Text = ircExtractPar(ppPar: &szParameters);
434 // Process message
435 OnMessage(fNotice: SEqualNoCase(szStr1: szCommand, szStr2: "NOTICE"), szSource: szSender, szTarget: Target.getData(), szText: Text.getData());
436 }
437 // Channel join?
438 if (SEqualNoCase(szStr1: szCommand, szStr2: "JOIN"))
439 {
440 // Get channel
441 StdStrBuf Channel = ircExtractPar(ppPar: &szParameters);
442 C4Network2IRCChannel *pChan = AddChannel(szName: Channel.getData());
443 // Add user
444 pChan->OnJoin(szUser: SenderNick.getData());
445 // Myself?
446 if (SenderNick == Nick)
447 PushMessage(eType: MSG_Status, szSource: szSender, szTarget: Channel.getData(), szText: LoadResStr(id: C4ResStrTableKey::IDS_MSG_YOUHAVEJOINEDCHANNEL, args: Channel.getData()).c_str());
448 else
449 PushMessage(eType: MSG_Status, szSource: szSender, szTarget: Channel.getData(), szText: LoadResStr(id: C4ResStrTableKey::IDS_MSG_HASJOINEDTHECHANNEL, args: SenderNick.getData()).c_str());
450 }
451 // Channel part?
452 if (SEqualNoCase(szStr1: szCommand, szStr2: "PART"))
453 {
454 // Get channel
455 StdStrBuf Channel = ircExtractPar(ppPar: &szParameters);
456 C4Network2IRCChannel *pChan = AddChannel(szName: Channel.getData());
457 // Get message
458 StdStrBuf Comment = ircExtractPar(ppPar: &szParameters);
459 // Remove user
460 pChan->OnPart(szUser: SenderNick.getData(), szComment: Comment.getData());
461 // Myself?
462 if (SenderNick == Nick)
463 {
464 DeleteChannel(pChannel: pChan);
465 PushMessage(eType: MSG_Status, szSource: szSender, szTarget: Nick.getData(), szText: LoadResStr(id: C4ResStrTableKey::IDS_MSG_YOUHAVELEFTCHANNEL, args: Channel.getData(), args: Comment.getData()).c_str());
466 }
467 else
468 PushMessage(eType: MSG_Status, szSource: szSender, szTarget: Channel.getData(), szText: LoadResStr(id: C4ResStrTableKey::IDS_MSG_HASLEFTTHECHANNEL, args: SenderNick.getData(), args: Comment.getData()).c_str());
469 }
470 // Kick?
471 if (SEqualNoCase(szStr1: szCommand, szStr2: "KICK"))
472 {
473 // Get channel
474 StdStrBuf Channel = ircExtractPar(ppPar: &szParameters);
475 C4Network2IRCChannel *pChan = AddChannel(szName: Channel.getData());
476 // Get kicked user
477 StdStrBuf Kicked = ircExtractPar(ppPar: &szParameters);
478 // Get message
479 StdStrBuf Comment = ircExtractPar(ppPar: &szParameters);
480 // Remove user
481 pChan->OnKick(szUser: Kicked.getData(), szComment: Comment.getData());
482 // Myself?
483 if (Kicked == Nick)
484 {
485 DeleteChannel(pChannel: pChan);
486 PushMessage(eType: MSG_Status, szSource: szSender, szTarget: Nick.getData(), szText: LoadResStr(id: C4ResStrTableKey::IDS_MSG_YOUWEREKICKEDFROMCHANNEL, args: Channel.getData(), args: Comment.getData()).c_str());
487 }
488 else
489 PushMessage(eType: MSG_Status, szSource: szSender, szTarget: Channel.getData(), szText: LoadResStr(id: C4ResStrTableKey::IDS_MSG_WASKICKEDFROMTHECHANNEL, args: Kicked.getData(), args: Comment.getData()).c_str());
490 }
491 // Quit?
492 if (SEqualNoCase(szStr1: szCommand, szStr2: "QUIT"))
493 {
494 // Get comment
495 StdStrBuf Comment = ircExtractPar(ppPar: &szParameters);
496 // Format status message
497 const std::string message{LoadResStr(id: C4ResStrTableKey::IDS_MSG_HASDISCONNECTED, args: SenderNick.getData(), args: Comment.getData())};
498 // Remove him from all channels
499 for (C4Network2IRCChannel *pChan = pChannels; pChan; pChan = pChan->Next)
500 if (pChan->getUser(szName: SenderNick.getData()))
501 {
502 pChan->OnPart(szUser: SenderNick.getData(), szComment: "Quit");
503 PushMessage(eType: MSG_Status, szSource: szSender, szTarget: pChan->getName(), szText: message.c_str());
504 }
505 }
506 // Topic change?
507 if (SEqualNoCase(szStr1: szCommand, szStr2: "TOPIC"))
508 {
509 // Get channel and topic
510 StdStrBuf Channel = ircExtractPar(ppPar: &szParameters);
511 StdStrBuf Topic = ircExtractPar(ppPar: &szParameters);
512 // Set topic
513 AddChannel(szName: Channel.getData())->OnTopic(szTopic: Topic.getData());
514 // Message
515 PushMessage(eType: MSG_Status, szSource: szSender, szTarget: Channel.getData(), szText: LoadResStr(id: C4ResStrTableKey::IDS_MSG_CHANGESTHETOPICTO, args: SenderNick.getData(), args: Topic.getData()).c_str());
516 }
517 // Mode?
518 if (SEqualNoCase(szStr1: szCommand, szStr2: "MODE"))
519 {
520 // Get all data
521 StdStrBuf Channel = ircExtractPar(ppPar: &szParameters);
522 StdStrBuf Flags = ircExtractPar(ppPar: &szParameters);
523 StdStrBuf What = ircExtractPar(ppPar: &szParameters);
524 // Make sure it's a channel
525 C4Network2IRCChannel *pChan = getChannel(szName: Channel.getData());
526 if (pChan)
527 // Ask for names, because user prefixes might be out of sync
528 Send(szCommand: "NAMES", szParameters: Channel.getData());
529 // Show Message
530 PushMessage(eType: MSG_Status, szSource: szSender, szTarget: Channel.getData(), szText: LoadResStr(id: C4ResStrTableKey::IDS_MSG_SETSMODE, args: SenderNick.getData(), args: Flags.getData(), args: What.getData()).c_str());
531 }
532 // Error?
533 if (SEqualNoCase(szStr1: szCommand, szStr2: "ERROR"))
534 {
535 // Get message
536 StdStrBuf Message = ircExtractPar(ppPar: &szParameters);
537 // Push it
538 PushMessage(eType: MSG_Server, szSource: szSender, szTarget: Nick.getData(), szText: Message.getData());
539 }
540 // Nickchange?
541 if (SEqualNoCase(szStr1: szCommand, szStr2: "NICK"))
542 {
543 // Get new nick
544 StdStrBuf NewNick = ircExtractPar(ppPar: &szParameters);
545 // Format status message
546 const std::string message{LoadResStr(id: C4ResStrTableKey::IDS_MSG_ISNOWKNOWNAS, args: SenderNick.getData(), args: NewNick.getData())};
547 // Rename on all channels
548 for (C4Network2IRCChannel *pChan = pChannels; pChan; pChan = pChan->Next)
549 if (pChan->getUser(szName: SenderNick.getData()))
550 {
551 pChan->OnPart(szUser: SenderNick.getData(), szComment: "Nickchange");
552 pChan->OnJoin(szUser: NewNick.getData());
553 PushMessage(eType: MSG_Status, szSource: szSender, szTarget: pChan->getName(), szText: message.c_str());
554 }
555 // Self?
556 if (SenderNick == Nick)
557 Nick = NewNick;
558 }
559}
560
561void C4Network2IRCClient::OnNumericCommand(const char *szSender, int iCommand, const char *szParameters)
562{
563 bool fShowMessage = true;
564 // Get target
565 StdStrBuf Target = ircExtractPar(ppPar: &szParameters);
566 // Handle command
567 switch (iCommand)
568 {
569 case 433: // Nickname already in use
570 {
571 StdStrBuf DesiredNick = ircExtractPar(ppPar: &szParameters);
572 // Automatically try to choose a new one
573 DesiredNick.AppendChar(cChar: '_');
574 Send(szCommand: "NICK", szParameters: DesiredNick.getData());
575 break;
576 }
577
578 case 376: // End of MOTD
579 case 422: // MOTD missing
580 // Let's take this a sign that the connection is established.
581 OnConnected();
582 break;
583
584 case 331: // No topic set
585 case 332: // Topic notify / change
586 {
587 // Get Channel name and topic
588 StdStrBuf Channel = ircExtractPar(ppPar: &szParameters);
589 StdStrBuf Topic = (iCommand == 332 ? ircExtractPar(ppPar: &szParameters) : StdStrBuf(""));
590 // Set it
591 AddChannel(szName: Channel.getData())->OnTopic(szTopic: Topic.getData());
592 // Log
593 if (Topic.getLength())
594 PushMessage(eType: MSG_Status, szSource: szSender, szTarget: Channel.getData(), szText: LoadResStr(id: C4ResStrTableKey::IDS_MSG_TOPICIN, args: Channel.getData(), args: Topic.getData()).c_str());
595 }
596 break;
597
598 case 333: // Last topic change
599 fShowMessage = false; // ignore
600 break;
601
602 case 353: // Names in channel
603 {
604 // Get Channel name and name list
605 StdStrBuf Junk = ircExtractPar(ppPar: &szParameters); // ??!
606 StdStrBuf Channel = ircExtractPar(ppPar: &szParameters);
607 StdStrBuf Names = ircExtractPar(ppPar: &szParameters);
608 // Set it
609 AddChannel(szName: Channel.getData())->OnUsers(szUsers: Names.getData(), szPrefixes: Prefixes.getData());
610 fShowMessage = false;
611 }
612 break;
613
614 case 366: // End of names list
615 {
616 // Get Channel name
617 StdStrBuf Channel = ircExtractPar(ppPar: &szParameters);
618 // Finish
619 AddChannel(szName: Channel.getData())->OnUsersEnd();
620 fShowMessage = false;
621 // Notify
622 if (pNotify) pNotify->PushEvent(eEventType: Ev_IRC_Message, data: this);
623 }
624 break;
625
626 case 4: // Server version
627 fShowMessage = false; // ignore
628 break;
629
630 case 5: // Server support string
631 {
632 while (szParameters && *szParameters)
633 {
634 // Get support-token.
635 StdStrBuf Token = ircExtractPar(ppPar: &szParameters);
636 StdStrBuf Parameter; Parameter.CopyUntil(szString: Token.getData(), cUntil: '=');
637 // Check if it's interesting and safe data if so.
638 if (SEqualNoCase(szStr1: Parameter.getData(), szStr2: "PREFIX"))
639 Prefixes.Copy(pnData: SSearch(szString: Token.getData(), szIndex: "="));
640 }
641 fShowMessage = false;
642 }
643 break;
644 }
645 // Show embedded message, if any?
646 if (fShowMessage)
647 {
648 // Check if first parameter is some sort of channel name
649 C4Network2IRCChannel *pChannel = nullptr;
650 if (szParameters && *szParameters && *szParameters != ':')
651 pChannel = getChannel(szName: ircExtractPar(ppPar: &szParameters).getData());
652 // Go over other parameters
653 const char *pMsg = szParameters;
654 while (pMsg && *pMsg && *pMsg != ':')
655 pMsg = SSearch(szString: pMsg, szIndex: " ");
656 // Show it
657 if (pMsg && *pMsg)
658 if (!pChannel)
659 PushMessage(eType: MSG_Server, szSource: szSender, szTarget: Nick.getData(), szText: pMsg + 1);
660 else
661 PushMessage(eType: MSG_Status, szSource: szSender, szTarget: pChannel->getName(), szText: pMsg + 1);
662 }
663}
664
665void C4Network2IRCClient::OnConnected()
666{
667 // Set flag
668 fConnected = true;
669
670 // Try to join channel(s)
671 if (AutoJoin.getLength())
672 Join(szChannel: AutoJoin.getData());
673}
674
675void C4Network2IRCClient::OnMessage(bool fNotice, const char *szSender, const char *szTarget, const char *szText)
676{
677 // Find channel, if not private.
678 C4Network2IRCChannel *pChan = nullptr;
679 if (!SEqualNoCase(szStr1: szTarget, szStr2: Nick.getData()))
680 pChan = getChannel(szName: szTarget);
681
682 // CTCP tagged data?
683 const char X_DELIM = '\001';
684 if (szText[0] == X_DELIM)
685 {
686 // Process messages (it's very rarely more than one, but the spec allows it)
687 const char *pMsg = szText + 1;
688 while (*pMsg)
689 {
690 // Find end
691 const char *pEnd = strchr(s: pMsg, c: X_DELIM);
692 if (!pEnd) pEnd = pMsg + SLen(sptr: pMsg);
693 // Copy CTCP query/reply, get tag
694 StdStrBuf CTCP; CTCP.Copy(pnData: pMsg, iChars: pEnd - pMsg);
695 StdStrBuf Tag; Tag.CopyUntil(szString: CTCP.getData(), cUntil: ' ');
696 const char *szData = SSearch(szString: CTCP.getData(), szIndex: " ");
697 if (!szData) szData = "";
698 StdStrBuf Sender; Sender.CopyUntil(szString: szSender, cUntil: '!');
699 // Process
700 if (SEqualNoCase(szStr1: Tag.getData(), szStr2: "ACTION"))
701 PushMessage(eType: MSG_Action, szSource: szSender, szTarget, szText: szData);
702 if (SEqualNoCase(szStr1: Tag.getData(), szStr2: "VERSION") && !fNotice)
703 Send(szCommand: "NOTICE", szParameters: std::format(fmt: "{} :{}VERSION " C4ENGINECAPTION ":" C4VERSION ":" C4_OS "{}",
704 args: Sender.getData(), args: X_DELIM, args: X_DELIM).c_str());
705 if (SEqualNoCase(szStr1: Tag.getData(), szStr2: "PING") && !fNotice)
706 Send(szCommand: "NOTICE", szParameters: std::format(fmt: "{} :{}PING {}{}",
707 args: Sender.getData(), args: X_DELIM, args&: szData, args: X_DELIM).c_str());
708 // Get next message
709 pMsg = pEnd;
710 if (*pMsg == X_DELIM) pMsg++;
711 }
712 }
713
714 // Standard message (not CTCP tagged): Push
715 else
716 PushMessage(eType: fNotice ? MSG_Notice : MSG_Message, szSource: szSender, szTarget, szText);
717}
718
719void C4Network2IRCClient::PopMessage()
720{
721 if (!iLogLength) return;
722 // Unlink message
723 C4Network2IRCMessage *pMsg = pLog;
724 pLog = pMsg->Next;
725 if (!pLog) pLogEnd = nullptr;
726 if (pLogLastRead == pMsg) pLogLastRead = nullptr;
727 // Delete it
728 delete pMsg;
729 iLogLength--;
730 if (iUnreadLogLength > iLogLength) iUnreadLogLength = iLogLength;
731}
732
733void C4Network2IRCClient::PushMessage(C4Network2IRCMessageType eType, const char *szSource, const char *szTarget, const char *szText)
734{
735 // Create message
736 C4Network2IRCMessage *pMsg = new C4Network2IRCMessage(eType, szSource, szTarget, szText);
737 // Add to list
738 if (pLogEnd)
739 {
740 pLogEnd->Next = pMsg;
741 }
742 else
743 {
744 pLog = pMsg;
745 }
746 pLogEnd = pMsg;
747 // Count
748 iLogLength++;
749 iUnreadLogLength++;
750 while (iLogLength > C4NetIRCMaxLogLength)
751 PopMessage();
752 // Notify
753 if (pNotify)
754 pNotify->PushEvent(eEventType: Ev_IRC_Message, data: this);
755}
756
757C4Network2IRCChannel *C4Network2IRCClient::AddChannel(const char *szName)
758{
759 // Already exists?
760 C4Network2IRCChannel *pChan = getChannel(szName);
761 if (pChan) return pChan;
762 // Create new, insert
763 pChan = new C4Network2IRCChannel(szName);
764 pChan->Next = pChannels;
765 pChannels = pChan;
766 return pChan;
767}
768
769void C4Network2IRCClient::DeleteChannel(C4Network2IRCChannel *pChannel)
770{
771 // Unlink
772 if (pChannel == pChannels)
773 pChannels = pChannel->Next;
774 else
775 {
776 C4Network2IRCChannel *pPrev = pChannels;
777 while (pPrev && pPrev->Next != pChannel)
778 pPrev = pPrev->Next;
779 if (pPrev)
780 pPrev->Next = pChannel->Next;
781 }
782 // Delete
783 delete pChannel;
784}
785