| 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 |
| 30 | StdStrBuf (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 | |
| 60 | C4Network2IRCUser::C4Network2IRCUser(const char *szName) |
| 61 | : Name(szName) {} |
| 62 | |
| 63 | // *** C4Network2IRCChannel |
| 64 | |
| 65 | C4Network2IRCChannel::C4Network2IRCChannel(const char *szName) |
| 66 | : Name(szName), pUsers(nullptr), fReceivingUsers(false) {} |
| 67 | |
| 68 | C4Network2IRCChannel::~C4Network2IRCChannel() |
| 69 | { |
| 70 | ClearUsers(); |
| 71 | } |
| 72 | |
| 73 | C4Network2IRCUser *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 | |
| 81 | void 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 | |
| 107 | void C4Network2IRCChannel::OnUsersEnd() |
| 108 | { |
| 109 | // Reset flag |
| 110 | fReceivingUsers = false; |
| 111 | } |
| 112 | |
| 113 | void C4Network2IRCChannel::OnTopic(const char *szTopic) |
| 114 | { |
| 115 | Topic = szTopic; |
| 116 | } |
| 117 | |
| 118 | void C4Network2IRCChannel::OnKick(const char *szUser, const char *) |
| 119 | { |
| 120 | // Remove named user from channel list |
| 121 | C4Network2IRCUser *pUser = getUser(szName: szUser); |
| 122 | if (pUser) DeleteUser(pUser); |
| 123 | } |
| 124 | |
| 125 | void C4Network2IRCChannel::OnPart(const char *szUser, const char *) |
| 126 | { |
| 127 | // Remove named user from channel list |
| 128 | C4Network2IRCUser *pUser = getUser(szName: szUser); |
| 129 | if (pUser) DeleteUser(pUser); |
| 130 | } |
| 131 | |
| 132 | void C4Network2IRCChannel::OnJoin(const char *szUser) |
| 133 | { |
| 134 | // Add user (do not set prefix) |
| 135 | if (!getUser(szName: szUser)) |
| 136 | AddUser(szName: szUser); |
| 137 | } |
| 138 | |
| 139 | C4Network2IRCUser *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 | |
| 151 | void 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 | |
| 168 | void C4Network2IRCChannel::ClearUsers() |
| 169 | { |
| 170 | while (pUsers) |
| 171 | DeleteUser(pUser: pUsers); |
| 172 | } |
| 173 | |
| 174 | // *** C4Network2IRCClient |
| 175 | |
| 176 | C4Network2IRCClient::C4Network2IRCClient() |
| 177 | : fConnecting(false), fConnected(false), |
| 178 | pChannels(nullptr), |
| 179 | pLog(nullptr), pLogEnd(nullptr), iLogLength(0), iUnreadLogLength(0), |
| 180 | pNotify(nullptr) {} |
| 181 | |
| 182 | C4Network2IRCClient::~C4Network2IRCClient() |
| 183 | { |
| 184 | Close(); |
| 185 | } |
| 186 | |
| 187 | void 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 | |
| 200 | size_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 | |
| 236 | bool 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 | |
| 254 | void 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 | |
| 261 | void C4Network2IRCClient::OnPacket(const class C4NetIOPacket &rPacket, C4NetIO *pNetIO) |
| 262 | { |
| 263 | // Won't get called |
| 264 | } |
| 265 | |
| 266 | C4Network2IRCChannel *C4Network2IRCClient::getFirstChannel() const |
| 267 | { |
| 268 | return pChannels; |
| 269 | } |
| 270 | |
| 271 | C4Network2IRCChannel *C4Network2IRCClient::getNextChannel(C4Network2IRCChannel *pPrevChan) const |
| 272 | { |
| 273 | return pPrevChan ? pPrevChan->Next : pChannels; |
| 274 | } |
| 275 | |
| 276 | C4Network2IRCChannel *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 | |
| 284 | void C4Network2IRCClient::ClearMessageLog() |
| 285 | { |
| 286 | // Clear log |
| 287 | while (iLogLength) |
| 288 | PopMessage(); |
| 289 | } |
| 290 | |
| 291 | void 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 | |
| 301 | bool 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 | |
| 335 | bool 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 | |
| 349 | bool 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 | |
| 365 | bool 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 | |
| 373 | bool C4Network2IRCClient::Join(const char *szChannel) |
| 374 | { |
| 375 | return Send(szCommand: "JOIN" , szParameters: szChannel); |
| 376 | } |
| 377 | |
| 378 | bool C4Network2IRCClient::Part(const char *szChannel) |
| 379 | { |
| 380 | return Send(szCommand: "PART" , szParameters: szChannel); |
| 381 | } |
| 382 | |
| 383 | bool 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 | |
| 391 | bool 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 | |
| 399 | bool 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 | |
| 407 | bool C4Network2IRCClient::ChangeNick(const char *szNewNick) |
| 408 | { |
| 409 | return Send(szCommand: "NICK" , szParameters: szNewNick); |
| 410 | } |
| 411 | |
| 412 | void 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 = 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 = 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 = 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 | |
| 561 | void 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 | |
| 665 | void 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 | |
| 675 | void 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 | |
| 719 | void 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 | |
| 733 | void 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 | |
| 757 | C4Network2IRCChannel *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 | |
| 769 | void 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 | |