The complete source code of IdealIRC
http://www.idealirc.org/
1421 lines
36 KiB
1421 lines
36 KiB
#include "IRCBase.h"
|
|
#include "Utilities.h"
|
|
#include "Commands.h"
|
|
#include "Numeric.h"
|
|
#include "IRCMember.h"
|
|
#include "IRCChannel.h"
|
|
#include "DCC.h"
|
|
|
|
#include <asio.hpp>
|
|
#include <asio/ssl.hpp>
|
|
#include <fmt/format.h>
|
|
|
|
#include <ctime>
|
|
#include <iostream>
|
|
|
|
using asio::ip::tcp;
|
|
|
|
namespace {
|
|
|
|
/* Debug/development options */
|
|
constexpr bool DumpReadData = true;
|
|
constexpr bool DumpWriteData = false;
|
|
|
|
const std::vector<std::string> V3Support {
|
|
"account-notify",
|
|
"extended-join",
|
|
"away-notify",
|
|
"invite-notify",
|
|
"multi-prefix",
|
|
"userhost-in-names"
|
|
|
|
/*
|
|
* TODO https://ircv3.net/irc/
|
|
* batch
|
|
* chghost
|
|
* server-time
|
|
* sasl
|
|
*/
|
|
};
|
|
|
|
} // end anonymous namespace
|
|
|
|
struct IRCBasePriv
|
|
{
|
|
explicit IRCBasePriv(IRCBase& super_)
|
|
: super(super_)
|
|
, sslctx(asio::ssl::context::tls)
|
|
, resolver(ioctx)
|
|
, keepaliveTimer(ioctx)
|
|
{
|
|
readbuf.reserve(1024);
|
|
setDefaults();
|
|
}
|
|
|
|
IRCBase& super;
|
|
|
|
std::string hostname;
|
|
std::string port;
|
|
std::string realname;
|
|
std::string ident;
|
|
std::string password;
|
|
std::string nickname;
|
|
|
|
struct {
|
|
bool selfSigned{ false };
|
|
bool CNMismactch{ false };
|
|
bool expired{ false };
|
|
} sslExcept;
|
|
|
|
bool useSSL{ false };
|
|
asio::io_context ioctx;
|
|
asio::ssl::context sslctx;
|
|
tcp::resolver resolver;
|
|
tcp::resolver::results_type endpoints;
|
|
std::string readbuf;
|
|
|
|
std::optional<tcp::socket> sock;
|
|
std::optional< asio::ssl::stream<asio::ip::tcp::socket> > sslsock;
|
|
|
|
std::vector<IRCError> sslExceptions; // List of SSL verification errors that were ignored.
|
|
|
|
bool isConnected{ false }; // When the socket itself is successfully connected.
|
|
bool isOnline{ false }; // When the client itself is successfully registered.
|
|
IRCError lastError{ IRCError::NoError };
|
|
|
|
std::chrono::seconds keepaliveFreq{ 0 };
|
|
asio::steady_timer keepaliveTimer;
|
|
|
|
std::vector<std::shared_ptr<DCC>> dcclist;
|
|
|
|
std::unordered_map<std::string,std::string> isupport;
|
|
std::vector<std::string> registeredV3support;
|
|
std::vector<std::string> serverV3support;
|
|
|
|
std::vector<std::shared_ptr<IRCMember>> allMembers;
|
|
std::vector<std::shared_ptr<IRCChannel>> channels;
|
|
|
|
std::string validPrivilegeModes; // ohv etc. Ordered by most significant first.
|
|
std::string validPrivilegeSymbols; // @%+ etc. Ordered by most significant first.
|
|
|
|
char channelModeGroup(char m) const
|
|
{
|
|
char group[] = {'A', 'B', 'C', 'D', 'M', 0};
|
|
int gi = 0; // group index
|
|
|
|
const std::string& cm = isupport.at("CHANMODES");
|
|
for (char c : cm) {
|
|
if (c == ',') {
|
|
++gi;
|
|
continue;
|
|
}
|
|
if (c == m)
|
|
return group[gi];
|
|
}
|
|
|
|
++gi; // No match yet... Test for channel-member modes
|
|
if (validPrivilegeModes.find(m) == std::string::npos)
|
|
++gi; // Not found, advance to the last "group" (numeric zero, failure).
|
|
|
|
return group[gi];
|
|
}
|
|
|
|
bool isChannelSymbol(char c)
|
|
{
|
|
const std::string& ct = isupport.at("CHANTYPES");
|
|
return ct.find(c) != std::string::npos;
|
|
}
|
|
|
|
bool isMemberPrivilegeSymbol(char m) const
|
|
{
|
|
bool ret = validPrivilegeSymbols.find(m) != std::string::npos;
|
|
return ret;
|
|
}
|
|
|
|
char memberPrivilegeSymbolToMode(char s) const
|
|
{
|
|
auto pos = validPrivilegeSymbols.find(s);
|
|
if (pos == std::string::npos)
|
|
return '\0';
|
|
else
|
|
return validPrivilegeModes[pos];
|
|
}
|
|
|
|
void setDefaults()
|
|
{
|
|
readbuf.clear();
|
|
isConnected = false;
|
|
isOnline = false;
|
|
isupport.clear();
|
|
|
|
/*
|
|
* Setting defaults as found in the IRC standard.
|
|
* Do not remove any of these entries.
|
|
*/
|
|
isupport.emplace("CHANMODES", "b,k,l,imnpstr"); // Channel modes
|
|
isupport.emplace("CHANTYPES", "#"); // Channel prefix characters
|
|
isupport.emplace("PREFIX", "(ov)@+"); // Usermode in channel, modes and prefix characters
|
|
isupport.emplace("MODES", "3"); // Max modes to set in a channel per MODE message
|
|
|
|
validPrivilegeModes = "ov";
|
|
validPrivilegeSymbols = "@+";
|
|
}
|
|
|
|
void startKeepaliveTimer()
|
|
{
|
|
keepaliveTimer.expires_after(keepaliveFreq);
|
|
keepaliveTimer.async_wait([this](const asio::error_code& ec){ keepaliveTimeout(ec); });
|
|
}
|
|
|
|
void stopKeepaliveTimer()
|
|
{
|
|
keepaliveTimer.cancel();
|
|
}
|
|
|
|
void keepaliveTimeout(const asio::error_code& ec)
|
|
{
|
|
if (ec != asio::error::operation_aborted && isOnline) {
|
|
auto ts = std::chrono::steady_clock::now().time_since_epoch().count();
|
|
write(Command::IRC::PING, fmt::format("KeepAlive {}", ts));
|
|
startKeepaliveTimer();
|
|
}
|
|
}
|
|
|
|
void disconnectHandler()
|
|
{
|
|
// Just to ensure we're actually closed... might throw something and we really do not care.
|
|
if (useSSL)
|
|
try { sslsock->lowest_layer().close(); } catch(...) {}
|
|
else
|
|
try { sock->close(); } catch(...) {}
|
|
|
|
readbuf.clear();
|
|
channels.clear();
|
|
allMembers.clear();
|
|
setDefaults();
|
|
super.onDisconnected();
|
|
}
|
|
|
|
void read(const asio::error_code& ec, std::size_t /*size*/)
|
|
{
|
|
lastError = SystemErrorToIRCError(ec);
|
|
if (lastError != IRCError::NoError) {
|
|
disconnectHandler();
|
|
return;
|
|
}
|
|
|
|
auto crIt = std::find(readbuf.begin(), readbuf.end(), '\r');
|
|
std::string line(readbuf.begin(), crIt);
|
|
readbuf.erase(readbuf.begin(), crIt + 2);
|
|
parseIncoming(line);
|
|
|
|
if (useSSL) {
|
|
asio::async_read_until(sslsock.value(), asio::dynamic_buffer(readbuf), "\r\n",
|
|
[this](const asio::error_code& e, std::size_t size){ read(e, size); });
|
|
}
|
|
else {
|
|
asio::async_read_until(sock.value(), asio::dynamic_buffer(readbuf), "\r\n",
|
|
[this](const asio::error_code& e, std::size_t size){ read(e, size); });
|
|
}
|
|
}
|
|
|
|
void connected(const asio::error_code& /*ec*/)
|
|
{
|
|
if (useSSL) {
|
|
asio::async_read_until(sslsock.value(), asio::dynamic_buffer(readbuf), "\r\n",
|
|
[this](const asio::error_code& e, std::size_t size){ read(e, size); });
|
|
}
|
|
else {
|
|
asio::async_read_until(sock.value(), asio::dynamic_buffer(readbuf), "\r\n",
|
|
[this](const asio::error_code& e, std::size_t size){ read(e, size); });
|
|
}
|
|
|
|
isConnected = true;
|
|
|
|
writeNoMsg(Command::IRCv3::CAP, { Command::IRCv3::LS });
|
|
|
|
if (!password.empty())
|
|
write(Command::IRC::PASS, password);
|
|
|
|
write(Command::IRC::NICK, nickname);
|
|
write(Command::IRC::USER, {ident, "0", "*"}, realname);
|
|
|
|
if (useSSL) {
|
|
/* TODO test if these two below must be set _before_ ::connect happens, on Windows platform. */
|
|
sslsock->next_layer().set_option(asio::socket_base::reuse_address(true));
|
|
sslsock->next_layer().set_option(asio::socket_base::keep_alive(true));
|
|
}
|
|
else {
|
|
/* TODO test if these two below must be set _before_ ::connect happens, on Windows platform. */
|
|
sock->set_option(asio::socket_base::reuse_address(true));
|
|
sock->set_option(asio::socket_base::keep_alive(true));
|
|
}
|
|
|
|
if (sslExceptions.empty())
|
|
super.onConnected();
|
|
else
|
|
super.onConnectedWithSSLExceptions(sslExceptions);
|
|
}
|
|
|
|
void write(const std::string& command, const std::vector<std::string>& args, const std::string& msg)
|
|
{
|
|
std::string out = command;
|
|
|
|
if constexpr (DumpWriteData)
|
|
std::cout << fmt::format("[SND] cmd=\"{}\" msg=\"{}\" argc={}", command, msg, args.size());
|
|
|
|
int c = 0;
|
|
for (const auto& arg : args) {
|
|
out.append(" " + arg);
|
|
if constexpr (DumpWriteData) {
|
|
std::cout << fmt::format(" arg({})=\"{}\"", c, arg);
|
|
++c;
|
|
}
|
|
}
|
|
|
|
if constexpr (DumpWriteData)
|
|
std::cout << std::endl;
|
|
|
|
out.append(" :" + msg);
|
|
out.append("\r\n");
|
|
|
|
if (useSSL)
|
|
sslsock->write_some(asio::buffer(out));
|
|
else
|
|
sock->write_some(asio::buffer(out));
|
|
}
|
|
|
|
void writeNoMsg(const std::string& command, const std::vector<std::string>& args)
|
|
{
|
|
std::string out = command;
|
|
|
|
if constexpr (DumpWriteData)
|
|
std::cout << fmt::format("[SND] cmd=\"{}\" argc={}", command, args.size());
|
|
|
|
int c = 0;
|
|
for (const auto& arg : args) {
|
|
out.append(" " + arg);
|
|
if constexpr (DumpWriteData) {
|
|
std::cout << fmt::format(" arg({})=\"{}\"", c, arg);
|
|
++c;
|
|
}
|
|
}
|
|
|
|
if constexpr (DumpWriteData)
|
|
std::cout << std::endl;
|
|
|
|
out.append("\r\n");
|
|
|
|
if (useSSL)
|
|
sslsock->write_some(asio::buffer(out));
|
|
else
|
|
sock->write_some(asio::buffer(out));
|
|
}
|
|
|
|
void write(const std::string& command, const std::string& msg)
|
|
{
|
|
std::string out = fmt::format("{} :{}\r\n", command, msg);
|
|
if constexpr (DumpWriteData) {
|
|
std::cout << fmt::format("[SND] cmd=\"{}\" msg=\"{}\"", command, msg) << std::endl;
|
|
}
|
|
if (useSSL)
|
|
sslsock->write_some(asio::buffer(out));
|
|
else
|
|
sock->write_some(asio::buffer(out));
|
|
}
|
|
|
|
void ctcp(const std::string& messageType, const std::string& target, const std::string& command, const std::string& message = "")
|
|
{
|
|
if (!isConnected)
|
|
return;
|
|
|
|
std::string ctcpdata;
|
|
ctcpdata.push_back(CTCPflag);
|
|
ctcpdata.append(command);
|
|
if (!message.empty())
|
|
ctcpdata.append(" " + message);
|
|
ctcpdata.push_back(CTCPflag);
|
|
write(messageType, { target }, ctcpdata);
|
|
}
|
|
|
|
void addMemberToChannel(const IRCPrefix& prefix, const std::string& channel)
|
|
{
|
|
auto member = super.getMember(prefix.nickname());
|
|
if (!member) {
|
|
member = std::make_shared<IRCMember>(prefix);
|
|
allMembers.emplace_back(member);
|
|
}
|
|
|
|
auto chanentry = super.getChannel(channel);
|
|
if (!chanentry) {
|
|
chanentry = std::make_shared<IRCChannel>(channel, super);
|
|
channels.emplace_back(chanentry);
|
|
}
|
|
|
|
member->addChannel(chanentry);
|
|
chanentry->addMember(member);
|
|
}
|
|
|
|
void delMemberFromChannel(const IRCPrefix& prefix, const std::string& channel)
|
|
{
|
|
auto member = super.getMember(prefix.nickname());
|
|
auto chanentry = super.getChannel(channel);
|
|
|
|
if (member && chanentry) {
|
|
member->delChannel(chanentry);
|
|
|
|
/* Delete only if not us. */
|
|
if (member->channels().empty() && prefix.nickname() != nickname) {
|
|
chanentry->delMember(member);
|
|
auto it = std::find_if(allMembers.begin(), allMembers.end(),
|
|
[&prefix](auto mem) {
|
|
return prefix == mem->prefix();
|
|
});
|
|
|
|
if (it != allMembers.end())
|
|
allMembers.erase(it);
|
|
}
|
|
|
|
/* If us, delete everything we know about this channel. */
|
|
if (prefix.nickname() == nickname) {
|
|
/* We also need to remove this channel from every member in that channel. */
|
|
for (auto m : chanentry->members()) {
|
|
IRCPrefix mp = m.member()->prefix();
|
|
if (mp.nickname() == nickname)
|
|
continue;
|
|
delMemberFromChannel(mp, channel);
|
|
}
|
|
|
|
chanentry->delMember(member);
|
|
|
|
/* Erase this channel */
|
|
auto it = std::find_if(channels.begin(), channels.end(),
|
|
[&channel](auto cptr) {
|
|
return channel == cptr->name();
|
|
});
|
|
|
|
if (it != channels.end())
|
|
channels.erase(it);
|
|
}
|
|
}
|
|
}
|
|
|
|
IRCError verify_X509(X509* cert)
|
|
{
|
|
char sn_char[256];
|
|
X509_NAME_oneline(X509_get_subject_name(cert), sn_char, 256);
|
|
std::string subject(sn_char);
|
|
|
|
std::string CN;
|
|
{
|
|
auto pos = subject.find("/CN=");
|
|
if (pos != std::string::npos) {
|
|
auto begin = subject.begin() + pos;
|
|
auto end = std::find(begin + 1, subject.end(), '/');
|
|
CN = std::string(begin, end);
|
|
}
|
|
}
|
|
|
|
if (X509_check_issued(cert, cert) == X509_V_OK) {
|
|
if (sslExcept.selfSigned)
|
|
sslExceptions.emplace_back(IRCError::SSL_SelfSigned);
|
|
else
|
|
return IRCError::SSL_SelfSigned;
|
|
}
|
|
|
|
const auto notbefore = X509_getm_notBefore(cert);
|
|
if (X509_cmp_current_time(notbefore) > -1) {
|
|
if (sslExcept.expired)
|
|
sslExceptions.emplace_back(IRCError::SSL_NotYetValid);
|
|
else
|
|
return IRCError::SSL_NotYetValid;
|
|
}
|
|
|
|
const auto notafter = X509_getm_notAfter(cert);
|
|
if (X509_cmp_current_time(notafter) < 1) {
|
|
if (sslExcept.expired)
|
|
sslExceptions.emplace_back(IRCError::SSL_Expired);
|
|
else
|
|
return IRCError::SSL_Expired;
|
|
}
|
|
|
|
if (CN.empty()) {
|
|
if (sslExcept.CNMismactch)
|
|
sslExceptions.emplace_back(IRCError::SSL_CN_Mismatch);
|
|
else
|
|
return IRCError::SSL_CN_Missing;
|
|
}
|
|
|
|
/*
|
|
* CN wildcard matching.
|
|
* Rules picked from: https://en.wikipedia.org/wiki/Wildcard_certificate#Examples
|
|
* TODO There are probably more rules! Remember this is IRC and not HTTP so the rules may be more relaxed.
|
|
*/
|
|
if (std::find(CN.begin(), CN.end(), '*') != CN.end()) {
|
|
|
|
// Lambda: Count occurrences of character in a string
|
|
auto strCount = [](const std::string& str, char c) -> std::size_t {
|
|
auto count = 0;
|
|
std::for_each(str.begin(), str.end(), [&count,c](char ch) { if (c == ch) ++count; });
|
|
return count;
|
|
};
|
|
|
|
// Reject "*" and wildcard on the end of CN
|
|
if (CN.back() == '*')
|
|
return IRCError::SSL_CN_WildcardIllegal;
|
|
|
|
// Reject multiple wildcards
|
|
if (strCount(CN, '*') > 1)
|
|
return IRCError::SSL_CN_WildcardIllegal;
|
|
|
|
/*
|
|
* Reject "*.tld"
|
|
* Actually, reject if there is only a single dot
|
|
*/
|
|
if (strCount(CN, '.') == 1)
|
|
return IRCError::SSL_CN_WildcardIllegal;
|
|
|
|
// Reject sub.*.domain.com
|
|
auto wcit = std::find(CN.begin(), CN.end(), '*');
|
|
if (wcit != CN.begin()) {
|
|
if (*(wcit-1) == '.' && *(wcit+1) == '.')
|
|
return IRCError::SSL_CN_WildcardIllegal;
|
|
}
|
|
|
|
if (!singleWildcardMatch(hostname, CN)) {
|
|
if (sslExcept.CNMismactch)
|
|
sslExceptions.emplace_back(IRCError::SSL_CN_Mismatch);
|
|
else
|
|
return IRCError::SSL_CN_Mismatch;
|
|
}
|
|
}
|
|
|
|
/* CN exact hostname match */
|
|
else if (CN != hostname) {
|
|
if (sslExcept.CNMismactch)
|
|
sslExceptions.emplace_back(IRCError::SSL_CN_Mismatch);
|
|
else
|
|
return IRCError::SSL_CN_Mismatch;
|
|
}
|
|
|
|
return IRCError::NoError;
|
|
}
|
|
|
|
void parseChannelModeMessage(std::shared_ptr<IRCChannel> chan, const std::string& modes, const std::vector<std::string>& args) const
|
|
{
|
|
char sign; // + or -
|
|
int argidx = 0;
|
|
for (char m : modes) {
|
|
if (m == '+' || m == '-') {
|
|
sign = m;
|
|
continue;
|
|
}
|
|
char group = channelModeGroup(m);
|
|
if (group == 'A')
|
|
++argidx;
|
|
|
|
else if (group == 'B') {
|
|
if (sign == '+')
|
|
chan->setMode(m, args[argidx]);
|
|
else
|
|
chan->delMode(m);
|
|
++argidx;
|
|
}
|
|
|
|
else if (group == 'C') {
|
|
if (sign == '+') {
|
|
chan->setMode(m, args[argidx]);
|
|
++argidx;
|
|
}
|
|
else
|
|
chan->delMode(m);
|
|
}
|
|
|
|
else if (group == 'D') {
|
|
if (sign == '+')
|
|
chan->setMode(m, "");
|
|
else
|
|
chan->delMode(m);
|
|
}
|
|
|
|
else if (group == 'M') {
|
|
auto member = chan->getMember(args[argidx]);
|
|
++argidx;
|
|
if (sign == '+')
|
|
member->get().addMode(m);
|
|
else
|
|
member->get().delMode(m);
|
|
}
|
|
}
|
|
}
|
|
|
|
void parseIncoming(const std::string& line);
|
|
|
|
}; // IRCBasePriv
|
|
|
|
|
|
/* ********************************************************************************************** *
|
|
* *
|
|
* IRCBase implementation *
|
|
* *
|
|
* ********************************************************************************************** */
|
|
|
|
|
|
IRCBase::IRCBase()
|
|
: mp(new IRCBasePriv(*this))
|
|
{}
|
|
|
|
IRCBase::IRCBase(IRCBase&& other)
|
|
: mp(std::move(other.mp))
|
|
{}
|
|
|
|
IRCBase::~IRCBase() = default;
|
|
|
|
bool IRCBase::poll()
|
|
{
|
|
asio::error_code ioctxerr;
|
|
bool ioDidSomething = false;
|
|
try {
|
|
ioDidSomething = mp->ioctx.poll_one(ioctxerr) > 0;
|
|
}
|
|
catch (const std::system_error& e) {
|
|
mp->lastError = SystemErrorToIRCError(e);
|
|
mp->disconnectHandler();
|
|
return false;
|
|
}
|
|
|
|
if (ioctxerr.value() != 0)
|
|
std::cout << fmt::format("IOCTX err {}: {}", ioctxerr.value(), ioctxerr.message()) << std::endl;
|
|
return ioDidSomething;
|
|
}
|
|
|
|
const std::string& IRCBase::getHostname() const
|
|
{
|
|
return mp->hostname;
|
|
}
|
|
|
|
IRCError IRCBase::setHostname(const std::string& hostname, bool SSL)
|
|
{
|
|
if (isOnline())
|
|
return IRCError::CannotChangeWhenConnected;
|
|
mp->useSSL = SSL;
|
|
mp->hostname = hostname;
|
|
return IRCError::NoError;
|
|
}
|
|
|
|
const std::string& IRCBase::getPort() const
|
|
{
|
|
return mp->port;
|
|
}
|
|
|
|
IRCError IRCBase::setPort(const std::string& port)
|
|
{
|
|
if (isOnline())
|
|
return IRCError::CannotChangeWhenConnected;
|
|
mp->port = port;
|
|
return IRCError::NoError;
|
|
}
|
|
|
|
const std::string& IRCBase::getRealname() const
|
|
{
|
|
return mp->realname;
|
|
}
|
|
|
|
IRCError IRCBase::setRealname(const std::string& realname)
|
|
{
|
|
if (isOnline())
|
|
return IRCError::CannotChangeWhenConnected;
|
|
mp->realname = realname;
|
|
return IRCError::NoError;
|
|
}
|
|
|
|
const std::string& IRCBase::getIdent() const
|
|
{
|
|
return mp->ident;
|
|
}
|
|
|
|
IRCError IRCBase::setIdent(const std::string& ident)
|
|
{
|
|
if (isOnline())
|
|
return IRCError::CannotChangeWhenConnected;
|
|
mp->ident = ident;
|
|
return IRCError::NoError;
|
|
}
|
|
|
|
const std::string& IRCBase::getNickname() const
|
|
{
|
|
return mp->nickname;
|
|
}
|
|
|
|
void IRCBase::setNickname(const std::string& nickname)
|
|
{
|
|
if (mp->isConnected) {
|
|
if (!isOnline())
|
|
mp->nickname = nickname;
|
|
mp->write(Command::IRC::NICK, nickname);
|
|
}
|
|
else
|
|
mp->nickname = nickname;
|
|
}
|
|
|
|
const std::string& IRCBase::getPassword() const
|
|
{
|
|
return mp->password;
|
|
}
|
|
|
|
void IRCBase::setPassword(const std::string& password)
|
|
{
|
|
mp->password = password;
|
|
}
|
|
|
|
void IRCBase::exceptSSL_SelfSigned(bool except)
|
|
{
|
|
mp->sslExcept.selfSigned = except;
|
|
}
|
|
|
|
void IRCBase::exceptSSL_CNMismatch(bool except)
|
|
{
|
|
mp->sslExcept.CNMismactch = except;
|
|
}
|
|
|
|
void IRCBase::exceptSSL_Expired(bool except)
|
|
{
|
|
mp->sslExcept.expired = except;
|
|
}
|
|
|
|
void IRCBase::command(const std::string& command, const std::vector<std::string>& args, const std::string& msg)
|
|
{
|
|
if (!mp->isConnected)
|
|
return;
|
|
|
|
if (msg.empty())
|
|
mp->writeNoMsg(command, args);
|
|
else
|
|
mp->write(command, args, msg);
|
|
}
|
|
|
|
void IRCBase::command(const std::string& command, const std::string& msg)
|
|
{
|
|
if (!mp->isConnected)
|
|
return;
|
|
|
|
mp->write(command, msg);
|
|
}
|
|
|
|
void IRCBase::raw(const std::string& data)
|
|
{
|
|
if (!mp->isConnected)
|
|
return;
|
|
|
|
mp->writeNoMsg(data, {});
|
|
}
|
|
|
|
void IRCBase::ctcpRequest(const std::string& target, const std::string& command, const std::string& message)
|
|
{
|
|
mp->ctcp(Command::IRC::PRIVMSG, target, command, message);
|
|
}
|
|
|
|
void IRCBase::ctcpResponse(const std::string& target, const std::string& command, const std::string& message)
|
|
{
|
|
mp->ctcp(Command::IRC::NOTICE, target, command, message);
|
|
}
|
|
|
|
void IRCBase::setManualKeepalive(std::chrono::seconds freq)
|
|
{
|
|
mp->keepaliveFreq = freq;
|
|
if (mp->isOnline) {
|
|
if (freq > std::chrono::seconds(0))
|
|
mp->startKeepaliveTimer();
|
|
else
|
|
mp->stopKeepaliveTimer();
|
|
}
|
|
}
|
|
|
|
std::chrono::milliseconds IRCBase::getManualKeepaliveFreq() const
|
|
{
|
|
return mp->keepaliveFreq;
|
|
}
|
|
|
|
asio::io_context& IRCBase::getIOCTX()
|
|
{
|
|
return mp->ioctx;
|
|
}
|
|
|
|
std::string IRCBase::toMemberPrefix(const std::string& modes) const
|
|
{ // PREFIX=(qaohv)~&@%+
|
|
const auto& prefixdef = mp->isupport.at("PREFIX"); // PREFIX is always present.
|
|
|
|
const auto prefixmodes = prefixdef.substr(1, prefixdef.find_first_of(')') - 1);
|
|
const auto prefix = prefixdef.substr(prefixdef.find_first_of(')') + 1);
|
|
|
|
std::string ret;
|
|
|
|
for (const char m : modes) {
|
|
// Only accept a-zA-Z
|
|
if (!(m > 'a' && m < 'z' || m > 'A' && m < 'Z')) { // De Morgan pls halp
|
|
ret += ':';
|
|
continue;
|
|
}
|
|
|
|
const auto ctpos = prefixmodes.find_first_of(m);
|
|
if (ctpos == std::string::npos)
|
|
ret += ':';
|
|
else
|
|
ret += prefix[ctpos];
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool IRCBase::isChannelSymbol(char c)
|
|
{
|
|
return mp->isChannelSymbol(c);
|
|
}
|
|
|
|
char IRCBase::channelModeGroup(char m) const
|
|
{
|
|
return mp->channelModeGroup(m);
|
|
}
|
|
|
|
IRCError IRCBase::tryConnect()
|
|
{
|
|
if (isOnline())
|
|
return IRCError::AlreadyConnected;
|
|
|
|
if (mp->hostname.empty())
|
|
return IRCError::HostNotSet;
|
|
|
|
if (mp->port.empty())
|
|
return IRCError::PortNotSet;
|
|
|
|
if (mp->ident.empty())
|
|
return IRCError::IdentNotSet;
|
|
|
|
if (mp->nickname.empty())
|
|
return IRCError::NicknameNotSet;
|
|
|
|
if (mp->realname.empty())
|
|
return IRCError::RealnameNotSet;
|
|
|
|
try {
|
|
mp->sslExceptions.clear();
|
|
mp->ioctx.restart();
|
|
mp->endpoints = mp->resolver.resolve(mp->hostname, mp->port);
|
|
if (mp->endpoints.empty())
|
|
return IRCError::CannotResolveAddress;
|
|
|
|
if (mp->useSSL) {
|
|
mp->sslsock.emplace(mp->ioctx, mp->sslctx);
|
|
|
|
mp->sslsock->set_verify_mode(asio::ssl::verify_peer);
|
|
mp->sslsock->set_verify_callback([this](bool preverified, asio::ssl::verify_context& vc){
|
|
if (preverified)
|
|
return true;
|
|
|
|
X509* cert = X509_STORE_CTX_get_current_cert(vc.native_handle());
|
|
mp->lastError = mp->verify_X509(cert);
|
|
if (mp->lastError != IRCError::NoError) {
|
|
onConnectionError(mp->lastError);
|
|
return false;
|
|
}
|
|
else
|
|
return true;
|
|
});
|
|
|
|
asio::async_connect(mp->sslsock->lowest_layer(), mp->endpoints,
|
|
[this](const asio::error_code& ec, const tcp::endpoint&) {
|
|
std::cout << "ssl connect error: " << ec.value() << " " << ec.message() << std::endl;
|
|
mp->lastError = SystemErrorToIRCError(ec);
|
|
if (ec) {
|
|
onConnectionError(mp->lastError);
|
|
return;
|
|
}
|
|
mp->sslsock->async_handshake(asio::ssl::stream_base::client,
|
|
[this](asio::error_code ec) {
|
|
mp->lastError = SystemErrorToIRCError(ec);
|
|
if (ec) {
|
|
onConnectionError(mp->lastError);
|
|
return;
|
|
}
|
|
// TODO error codes!
|
|
std::cout << "ssl handshake error: " << ec.value() << " " << ec.message() << std::endl;
|
|
mp->connected(ec);
|
|
});
|
|
});
|
|
}
|
|
else {
|
|
mp->sock.emplace(mp->ioctx);
|
|
asio::async_connect(mp->sock.value(), mp->endpoints,
|
|
[this](const asio::error_code& ec, const tcp::endpoint&) {
|
|
mp->lastError = SystemErrorToIRCError(ec);
|
|
if (ec) {
|
|
onConnectionError(mp->lastError);
|
|
return;
|
|
}
|
|
mp->connected(ec);
|
|
}
|
|
);
|
|
}
|
|
}
|
|
catch (const std::system_error& e) {
|
|
return SystemErrorToIRCError(e);
|
|
}
|
|
|
|
return IRCError::NoError;
|
|
}
|
|
|
|
IRCError IRCBase::disconnectFromServer(const std::string& quitMessage)
|
|
{
|
|
if (!mp->isConnected)
|
|
return IRCError::NotConnected;
|
|
|
|
if (mp->isOnline) {
|
|
mp->write(Command::IRC::QUIT, quitMessage);
|
|
// TODO Quit timeout; if we never get disconnected by server, we forcefully must do so.
|
|
}
|
|
else {
|
|
if (mp->useSSL)
|
|
mp->sslsock->lowest_layer().close();
|
|
else
|
|
mp->sock->close();
|
|
}
|
|
|
|
if (mp->keepaliveFreq > std::chrono::seconds(0))
|
|
mp->stopKeepaliveTimer();
|
|
|
|
return IRCError::NoError;
|
|
}
|
|
|
|
IRCError IRCBase::lastErrorCode() const
|
|
{
|
|
return mp->lastError;
|
|
}
|
|
|
|
bool IRCBase::isOnline() const
|
|
{
|
|
return mp->isOnline;
|
|
}
|
|
|
|
bool IRCBase::isConnected() const
|
|
{
|
|
return mp->isConnected;
|
|
}
|
|
|
|
bool IRCBase::isSSL() const
|
|
{
|
|
return mp->useSSL;
|
|
}
|
|
|
|
const std::vector<std::string>& IRCBase::clientV3Support()
|
|
{
|
|
return V3Support;
|
|
}
|
|
|
|
const std::vector<std::string>& IRCBase::registeredV3Support() const
|
|
{
|
|
return mp->registeredV3support;
|
|
}
|
|
|
|
const std::vector<std::string>& IRCBase::serverV3Support() const
|
|
{
|
|
return mp->serverV3support;
|
|
}
|
|
|
|
const std::unordered_map<std::string, std::string>& IRCBase::isupport() const
|
|
{
|
|
return mp->isupport;
|
|
}
|
|
|
|
const std::vector<std::shared_ptr<IRCChannel>>& IRCBase::channels() const
|
|
{
|
|
return mp->channels;
|
|
}
|
|
|
|
std::shared_ptr<IRCChannel> IRCBase::getChannel(const std::string& name) const
|
|
{
|
|
for (auto chanp : mp->channels)
|
|
if (chanp->name() == name)
|
|
return chanp;
|
|
return nullptr;
|
|
}
|
|
|
|
std::shared_ptr<IRCMember> IRCBase::getMember(const std::string& nickname) const
|
|
{
|
|
for (auto memp : mp->allMembers)
|
|
if (memp->prefix().nickname() == nickname)
|
|
return memp;
|
|
return nullptr;
|
|
}
|
|
|
|
std::pair<std::shared_ptr<DCC>, IRCError> IRCBase::initiateDCC(const std::string& /*port*/)
|
|
{
|
|
return std::pair<std::shared_ptr<DCC>, IRCError>();
|
|
}
|
|
|
|
IRCError IRCBase::declineDCC(std::shared_ptr<DCC> /*dcc*/)
|
|
{
|
|
return IRCError::NetworkUnreachable;
|
|
}
|
|
|
|
void IRCBasePriv::parseIncoming(const std::string& line)
|
|
{
|
|
std::string command;
|
|
std::vector<std::string> args;
|
|
std::string msg;
|
|
IRCPrefix sender(hostname);
|
|
|
|
if (line[0] == ':') {
|
|
// :server.addr cmd arg :msg
|
|
enum class S {
|
|
Prefix,
|
|
Command,
|
|
Argument,
|
|
Message
|
|
} parseState = S::Prefix;
|
|
|
|
auto it = line.begin() + 1;
|
|
|
|
auto next = [&it, &line] {
|
|
return std::find(it, line.end(), ' ');
|
|
};
|
|
|
|
while (it != line.end()) {
|
|
auto end = next();
|
|
|
|
switch (parseState) {
|
|
case S::Prefix:
|
|
sender = IRCPrefix(std::string(it, end));
|
|
parseState = S::Command;
|
|
break;
|
|
|
|
case S::Command:
|
|
command = std::string(it, end);
|
|
parseState = S::Argument;
|
|
break;
|
|
|
|
case S::Argument:
|
|
if (*it == ':') {
|
|
++it;
|
|
parseState = S::Message;
|
|
[[fallthrough]];
|
|
}
|
|
else {
|
|
args.emplace_back(it, end);
|
|
break;
|
|
}
|
|
|
|
case S::Message:
|
|
end = line.end();
|
|
msg = std::string(it, end);
|
|
break;
|
|
}
|
|
|
|
it = (end == line.end()) ? end : end + 1;
|
|
}
|
|
}
|
|
else {
|
|
// cmd :msg
|
|
auto wsIt = std::find(line.begin(), line.end(), ' ');
|
|
auto colonIt = std::find(wsIt, line.end(), ':');
|
|
command = std::string(line.begin(), wsIt);
|
|
if (colonIt != line.end()) {
|
|
msg = std::string(colonIt + 1, line.end());
|
|
}
|
|
}
|
|
|
|
if constexpr (DumpReadData) {
|
|
std::cout << fmt::format("[RCV] cmd=\"{}\" msg=\"{}\" argc={} ", command, msg, args.size());
|
|
int c = 0;
|
|
for (const auto& arg : args) {
|
|
std::cout << fmt::format("arg({})=\"{}\" ", c, arg);
|
|
++c;
|
|
}
|
|
std::cout << std::endl;
|
|
}
|
|
|
|
|
|
using namespace Command::IRC;
|
|
using namespace Numeric;
|
|
|
|
/*
|
|
* The 'args' vector and 'msg' string may be modified inside any of the following conditionals.
|
|
* Do not rely on them after the last else-if, but use the below copies.
|
|
*/
|
|
const auto arguments = args;
|
|
const auto message = msg;
|
|
|
|
/*
|
|
* Numeric reply
|
|
*/
|
|
if (command[0] >= '0' && command[0] <= '9') {
|
|
if (command == RPL_ISUPPORT && !isOnline) {
|
|
args.erase(args.begin()); // First arg is always our nickname.
|
|
for (const auto& a : args) {
|
|
std::string key, val;
|
|
auto delim = std::find(a.begin(), a.end(), '=');
|
|
if (delim != a.end()) {
|
|
key = std::string(a.begin(), delim);
|
|
val = std::string(delim + 1, a.end());
|
|
}
|
|
else
|
|
key = a;
|
|
isupport.insert_or_assign(key, val);
|
|
}
|
|
|
|
const std::string& prefix = isupport.find("PREFIX")->second;
|
|
|
|
validPrivilegeModes = std::string(std::find(prefix.begin(), prefix.end(), '(') + 1,
|
|
std::find(prefix.begin(), prefix.end(), ')'));
|
|
|
|
validPrivilegeSymbols = std::string(std::find(prefix.begin(), prefix.end(), ')') + 1,
|
|
prefix.end());
|
|
}
|
|
|
|
else if (command == RPL_ENDOFMOTD && !isOnline) {
|
|
nickname = args[0];
|
|
isOnline = true;
|
|
if (keepaliveFreq > std::chrono::seconds(0))
|
|
startKeepaliveTimer();
|
|
|
|
// Emplace ourself in the all-members list.
|
|
allMembers.emplace_back(std::make_shared<IRCMember>( IRCPrefix::fromNickname(nickname) ));
|
|
|
|
super.onRegistered();
|
|
}
|
|
|
|
else if (command == RPL_NAMREPLY) {
|
|
auto chan = super.getChannel(args[2]);
|
|
if (chan && chan->isPopulating()) {
|
|
std::istringstream ss(msg);
|
|
/*
|
|
* Note these things:
|
|
* A member may contain a usermode.
|
|
* IRCv3 may enable multiple usermodes (multi-prefix).
|
|
* IRCv3 may enable each member entry to be a full hostmask (userhost-in-names) and not only a nickname.
|
|
*/
|
|
auto members = std::vector<std::string>(std::istream_iterator<std::string>{ss},
|
|
std::istream_iterator<std::string>());
|
|
|
|
for (std::string& namem : members) {
|
|
/* Extract mode symbols */
|
|
auto memBegin = std::find_if_not(namem.begin(), namem.end(),
|
|
[this](char ms){
|
|
return isMemberPrivilegeSymbol(ms);
|
|
});
|
|
std::string modesymbols = std::string(namem.begin(), memBegin);
|
|
namem.erase(namem.begin(), memBegin);
|
|
|
|
std::optional<IRCPrefix> memprefix;
|
|
/* IRC standard reply */
|
|
if (namem.find('!') == std::string::npos)
|
|
memprefix.emplace(IRCPrefix::fromNickname(namem));
|
|
/* IRCv3 reply */
|
|
else
|
|
memprefix.emplace(namem);
|
|
|
|
auto mem = super.getMember(memprefix->nickname());
|
|
if (!mem) {
|
|
mem = std::make_shared<IRCMember>(*memprefix);
|
|
allMembers.push_back(mem);
|
|
}
|
|
if (mem->prefix().host().empty() && !memprefix->host().empty())
|
|
mem->setPrefix(*memprefix);
|
|
|
|
mem->addChannel(chan);
|
|
auto& entry = chan->addMember(mem);
|
|
for (char s : modesymbols) {
|
|
char letter = memberPrivilegeSymbolToMode(s);
|
|
if (letter != '\0')
|
|
entry.addMode(letter);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
else if (command == RPL_ENDOFNAMES) {
|
|
auto chan = super.getChannel(args[1]);
|
|
if (chan && chan->isPopulating())
|
|
chan->donePopulating();
|
|
}
|
|
|
|
else if (command == RPL_TOPIC) {
|
|
auto chan = super.getChannel(args[1]);
|
|
if (chan)
|
|
chan->setTopic(msg);
|
|
}
|
|
|
|
else if (command == RPL_CHANNELMODEIS) {
|
|
auto chan = super.getChannel(args[1]);
|
|
const std::string& modes = args[2];
|
|
args.erase(args.begin(), args.begin() + 3);
|
|
|
|
if (chan)
|
|
parseChannelModeMessage(chan, modes, args);
|
|
}
|
|
|
|
super.onMsgNumeric(sender, command, arguments, message);
|
|
} // End numeric message
|
|
|
|
else if (command == NICK) {
|
|
std::vector<std::string> channelsAffected;
|
|
channelsAffected.reserve(16);
|
|
auto member = super.getMember(sender.toString());
|
|
|
|
if (member) {
|
|
const auto& chans = member->channels();
|
|
for (const auto& c : chans)
|
|
channelsAffected.push_back(c.lock()->name());
|
|
member->setNickname(msg);
|
|
}
|
|
|
|
/* We changed our nickname */
|
|
if (sender.toString() == nickname)
|
|
nickname = msg;
|
|
|
|
super.onMsgNick(sender, msg, channelsAffected);
|
|
}
|
|
|
|
else if (command == MODE) {
|
|
const std::string target = args[0];
|
|
std::string modes;
|
|
|
|
if (isChannelSymbol(target[0])) {
|
|
modes = args[1];
|
|
args.erase(args.begin(), args.begin() + 2);
|
|
|
|
auto chan = super.getChannel(target);
|
|
if (chan)
|
|
parseChannelModeMessage(chan, modes, args);
|
|
}
|
|
else {
|
|
modes = msg;
|
|
args.erase(args.begin(), args.begin() + 1);
|
|
}
|
|
|
|
super.onMsgMode(sender, target, modes, args);
|
|
}
|
|
|
|
else if (command == QUIT) {
|
|
std::vector<std::shared_ptr<IRCChannel>> channelsAffected;
|
|
std::vector<std::string> channelsAffectedStr;
|
|
|
|
auto member = super.getMember(sender.nickname());
|
|
if (member) {
|
|
const auto& chans = member->channels();
|
|
for (const auto& c : chans) {
|
|
channelsAffected.push_back(c.lock());
|
|
channelsAffectedStr.push_back(c.lock()->name());
|
|
}
|
|
|
|
auto it = std::find(allMembers.begin(), allMembers.end(), member);
|
|
allMembers.erase(it);
|
|
}
|
|
|
|
super.onMsgQuit(sender, msg, channelsAffectedStr);
|
|
|
|
for (const auto& c : channelsAffected)
|
|
c->delMember(member);
|
|
}
|
|
|
|
else if (command == JOIN) {
|
|
if (args.size() == 2) {
|
|
std::string accountname = args[1];
|
|
if (accountname[0] == '*')
|
|
accountname.clear();
|
|
|
|
/* We are joining a channel */
|
|
if (sender.nickname() == nickname) {
|
|
auto chanentry = super.getChannel(args[0]);
|
|
if (!chanentry)
|
|
channels.emplace_back(std::make_shared<IRCChannel>(args[0], super));
|
|
}
|
|
else
|
|
addMemberToChannel(sender, args[0]);
|
|
super.v3onMsgJoin(sender, args[0], accountname, msg);
|
|
}
|
|
else {
|
|
/* We are joining a channel */
|
|
if (sender.nickname() == nickname) {
|
|
auto chanentry = super.getChannel(msg);
|
|
if (!chanentry)
|
|
channels.emplace_back(std::make_shared<IRCChannel>(msg, super));
|
|
}
|
|
else
|
|
addMemberToChannel(sender, msg);
|
|
super.onMsgJoin(sender, msg);
|
|
}
|
|
}
|
|
|
|
else if (command == PART) {
|
|
super.onMsgPart(sender, args[0], msg);
|
|
delMemberFromChannel(sender, args[0]);
|
|
}
|
|
|
|
else if (command == TOPIC) {
|
|
super.onMsgTopic(sender, args[0], msg);
|
|
}
|
|
|
|
else if (command == INVITE) {
|
|
super.onMsgInvite(sender, msg);
|
|
}
|
|
|
|
else if (command == KICK) {
|
|
super.onMsgKick(sender, args[0], args[1], msg);
|
|
auto prefix = IRCPrefix::fromNickname(args[1]);
|
|
delMemberFromChannel(prefix, args[0]);
|
|
}
|
|
|
|
else if (command == PRIVMSG) {
|
|
if (msg[0] == CTCPflag) {
|
|
auto[cmd,ctcpMsg] = FormatCTCPLine(msg);
|
|
|
|
if (cmd == "DCC") {
|
|
auto[dccCmd,dccMsg] = FormatCTCPLine(ctcpMsg);
|
|
std::vector<std::string> dccMsgTks;
|
|
{
|
|
std::stringstream ss(dccMsg);
|
|
dccMsgTks = std::vector<std::string>(std::istream_iterator<std::string>{ss},
|
|
std::istream_iterator<std::string>());
|
|
}
|
|
|
|
if (dccCmd == "CHAT" && dccMsgTks.size() >= 3 && dccMsgTks[0] == "chat") {
|
|
const auto& dccLongIp = dccMsgTks[1];
|
|
const auto& dccPort = dccMsgTks[2];
|
|
if (port == "0") {
|
|
// nat stuff
|
|
std::cout << "DCC CHAT TODO" << std::endl;
|
|
}
|
|
else {
|
|
const auto ip = longIpToNormal(dccLongIp);
|
|
dcclist.emplace_back(std::make_shared<DCC>(super, ioctx, ip, dccPort));
|
|
|
|
super.onMsgDCCRequest(dcclist.back(), sender, args[0], dccCmd, dccMsg);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
super.onMsgCTCPRequest(sender, args[0], cmd, ctcpMsg);
|
|
}
|
|
else
|
|
super.onMsgPrivmsg(sender, args[0], msg);
|
|
}
|
|
|
|
else if (command == NOTICE) {
|
|
if (msg[0] == CTCPflag) {
|
|
auto[cmd,ctcpMsg] = FormatCTCPLine(msg);
|
|
super.onMsgCTCPResponse(sender, args[0], cmd, ctcpMsg);
|
|
}
|
|
else
|
|
super.onMsgNotice(sender, args[0], msg);
|
|
}
|
|
|
|
else if (command == KILL) {
|
|
super.onMsgKill(sender, msg);
|
|
}
|
|
|
|
else if (command == PING) {
|
|
write(PONG, msg);
|
|
super.onMsgPing(msg);
|
|
}
|
|
|
|
else if (command == PONG) {
|
|
super.onMsgPong(msg);
|
|
}
|
|
|
|
// In Windows, wingdi.h, this is defined and we don't use it in this here source file.
|
|
#if defined(ERROR)
|
|
#undef ERROR
|
|
#endif
|
|
|
|
else if (command == ERROR) {
|
|
super.onMsgError(msg);
|
|
}
|
|
|
|
else if (command == WALLOPS) {
|
|
super.onMsgWallops(sender, msg);
|
|
}
|
|
|
|
|
|
/* *******************************************************************
|
|
* IRCv3 section
|
|
*
|
|
* All commands below here is not part of the IRC standard itself but
|
|
* is a part of the IRCv3 effort from https://ircv3.net/
|
|
* *******************************************************************/
|
|
|
|
else if (command == Command::IRCv3::CAP && !isOnline) {
|
|
|
|
/*
|
|
* List of IRCv3 capabilities from the server.
|
|
* Match it with what we support and request those.
|
|
*/
|
|
if (args[1] == Command::IRCv3::LS) {
|
|
std::istringstream ss(msg);
|
|
serverV3support = std::vector<std::string>(std::istream_iterator<std::string>{ss},
|
|
std::istream_iterator<std::string>());
|
|
|
|
for (const auto& cap : serverV3support) {
|
|
auto myIt = std::find(V3Support.begin(), V3Support.end(), cap);
|
|
if (myIt != V3Support.end())
|
|
registeredV3support.push_back(cap);
|
|
}
|
|
|
|
std::string requestStr;
|
|
for (const auto& cap : registeredV3support) {
|
|
if (!requestStr.empty())
|
|
requestStr += ' ';
|
|
requestStr += cap;
|
|
}
|
|
|
|
write(Command::IRCv3::CAP, { Command::IRCv3::REQ }, requestStr);
|
|
}
|
|
|
|
/*
|
|
* Server accepted our desires.
|
|
*/
|
|
else if (args[1] == Command::IRCv3::ACK) {
|
|
writeNoMsg(Command::IRCv3::CAP, { Command::IRCv3::END });
|
|
}
|
|
|
|
/*
|
|
* Server replied NAK on our IRCv3 capabilities, even though it seems it supported them...
|
|
* Resetting and continuing with our lives.
|
|
*/
|
|
else if (args[1] == Command::IRCv3::NAK) {
|
|
registeredV3support.clear();
|
|
writeNoMsg(Command::IRCv3::CAP, { Command::IRCv3::END }); // TODO erase the offending modules instead.
|
|
}
|
|
}
|
|
|
|
/* IRCv3 capability "away-notify" */
|
|
else if (command == Command::IRC::AWAY) {
|
|
std::vector<std::string> channelsAffected;
|
|
auto member = super.getMember(sender.nickname());
|
|
if (member) {
|
|
const auto& chans = member->channels();
|
|
for (const auto& c : chans) {
|
|
const auto cptr = c.lock();
|
|
channelsAffected.push_back(cptr->name());
|
|
cptr->delMember(member);
|
|
}
|
|
}
|
|
super.v3onMsgAway(sender, msg, channelsAffected);
|
|
}
|
|
|
|
/* IRCv3 capability "account-notify" */
|
|
else if (command == Command::Extension::ACCOUNT) {
|
|
if (args[0] == "*")
|
|
super.v3onMsgAccountLogout(sender);
|
|
else
|
|
super.v3onMsgAccountLogin(sender, args[0]);
|
|
}
|
|
|
|
/* ****************************************
|
|
*
|
|
* Catch-all, message was not handled!
|
|
*
|
|
* ***************************************/
|
|
else {
|
|
super.onMsgUnhandled(sender, command, arguments, message);
|
|
}
|
|
}
|
|
|