#2 DCC with reverse initiation functionality and NAT integration.

master
Tomatix 3 years ago
parent 29ea535243
commit 433fdf747c
  1. 19
      ICommand/Internal/dcc.cpp
  2. 9
      IRCClient/CMakeLists.txt
  3. 103
      IRCClient/DCC.cpp
  4. 17
      IRCClient/DCC.h
  5. 4
      IRCClient/IRCBasePriv.h
  6. 52
      IRCClient/Utilities.cpp
  7. 4
      IRCClient/Utilities.h
  8. 36
      IWin/IWinDCCChat.cpp
  9. 11
      IWin/IWinDCCChat.h
  10. 35
      IdealIRC/IdealIRC.cpp
  11. 2
      IdealIRC/IdealIRC.h
  12. 15
      IdealIRC/MdiManager.cpp
  13. 2
      IdealIRC/MdiManager.h

@ -19,20 +19,29 @@ void ICommandPriv::cmd_dcc(const std::string& command, const std::string& target
const auto code = static_cast<int>(result.second); const auto code = static_cast<int>(result.second);
const auto errorStr = QString::fromStdString(IRCErrorToString(result.second)); const auto errorStr = QString::fromStdString(IRCErrorToString(result.second));
mdi.currentStatus()->printToActive(PrintType::ProgramInfo, QObject::tr("/DCC: Unable to initiate DCC Chat to %1 [%2 (%3)]") mdi.currentStatus()->printToActive(PrintType::ProgramInfo, QObject::tr("/DCC: Unable to initiate DCC Chat to %1 [%2 (%3)]")
.arg( QString::fromStdString(target), errorStr, QString::number(code)) ); .arg( QString::fromStdString(target), errorStr, QString::number(code)) );
} }
else { else {
const auto portno = result.first->port(); auto dcc = result.first;
const auto portno = dcc->isReversed() ? 0
: dcc->port();
auto* window = mdi.createDCCSubwindow(mdi.currentStatus(), IWin::Type::DCCChat, result.first, IRCPrefix(target)); auto* window = mdi.createDCCSubwindow(mdi.currentStatus(), IWin::Type::DCCChat, result.first, IRCPrefix(target));
window->print(PrintType::ProgramInfo, QObject::tr("Initiated CHAT with %1 on port %2")
.arg( QString::fromStdString(target) ) if (dcc->isReversed())
.arg(portno)); window->print(PrintType::ProgramInfo, QObject::tr("Initiating CHAT with %1, please wait...")
.arg( QString::fromStdString(target) ));
else
window->print(PrintType::ProgramInfo, QObject::tr("Initiating CHAT with %1 on port %2, please wait...")
.arg( QString::fromStdString(target) )
.arg(portno));
const auto myIp = findOwnIpAddress(); const auto myIp = findOwnIpAddress();
const auto msg = fmt::format( "CHAT chat {} {}", const auto msg = fmt::format( "CHAT chat {} {}",
normalIpToLong(myIp), normalIpToLong(myIp),
portno portno
); );
cmd_ctcp(target, "DCC", msg); cmd_ctcp(target, "DCC", msg);
} }
} }

@ -70,5 +70,12 @@ list(APPEND ${component}_PRIVATE
) )
add_library(${component} STATIC ${${component}_SOURCES} ${${component}_COMMANDS} ${${component}_PRIVATE}) add_library(${component} STATIC ${${component}_SOURCES} ${${component}_COMMANDS} ${${component}_PRIVATE})
target_link_libraries(${component} fmt crypto ssl)
target_link_libraries(${component}
NATUtils
fmt
crypto
ssl
)
target_include_directories(${component} PRIVATE ${CMAKE_SOURCE_DIR}) target_include_directories(${component} PRIVATE ${CMAKE_SOURCE_DIR})

@ -7,6 +7,9 @@
#include "DCC.h" #include "DCC.h"
#include "IRCBase.h" #include "IRCBase.h"
#include "NATUtils/PortMapping.h"
#include "NATUtils/PublicAddress.h"
#include "Utilities.h"
#include <fmt/format.h> #include <fmt/format.h>
#include <iostream> #include <iostream>
@ -89,24 +92,46 @@ struct DCCPriv
if (!ec) return; // No errors. if (!ec) return; // No errors.
std::cerr << fmt::format("'- DCC write error: {} ({})", SystemErrorToIRCError(ec), ec.message()) << std::endl; std::cerr << fmt::format("'- DCC write error: {} ({})", SystemErrorToIRCError(ec), ec.message()) << std::endl;
} }
void startListener()
{
acceptor.open(endpoint.protocol());
acceptor.set_option(tcp::acceptor::reuse_address(true));
acceptor.bind(endpoint);
acceptor.listen(1);
acceptor.async_accept(socket, [this](const asio::error_code& ec){
if (ec == asio::error::operation_aborted)
return;
if (ec)
disconnected(ec);
else
connected(ec);
});
}
}; };
DCC::DCC(IRCBase& ircctx, asio::io_context& ioctx) DCC::DCC(IRCBase& ircctx, asio::io_context& ioctx)
: mp(std::make_unique<DCCPriv>(*this, ircctx, Direction::Initiator, ioctx, "", "0")) : mp(std::make_unique<DCCPriv>(*this, ircctx, Direction::Initiator, ioctx, "", "0"))
{ {
mp->acceptor.open(mp->endpoint.protocol()); auto ipAddress = findOwnIpAddress();
mp->acceptor.set_option(tcp::acceptor::reuse_address(true));
mp->acceptor.bind(mp->endpoint); mp->startListener(); // also acquires a port for us
mp->acceptor.listen(1);
mp->acceptor.async_accept(mp->socket, [this](const asio::error_code& ec){ if (isPrivateIp(ipAddress) && NATPortMapping::add(ipAddress, port(), port())) {
if (ec == asio::error::operation_aborted) auto publicIp = NATPublicAddress::get();
return; if (publicIp.has_value()) {
ipAddress = longIpToNormal(*publicIp);
}
else {
NATPortMapping::remove(ipAddress, port(), port());
// Reset internal state, effectively stopping the listening.
// This DCC instance will be destroyed and replaced when we get a response with the port to use in reverse.
mp = std::make_unique<DCCPriv>(*this, ircctx, Direction::Initiator, ioctx, "", "0");
}
}
if (ec) mp->ip = ipAddress;
mp->disconnected(ec);
else
mp->connected(ec);
});
} }
DCC::DCC(IRCBase& ircctx, asio::io_context& ioctx, const std::string& ip, const std::string& port) DCC::DCC(IRCBase& ircctx, asio::io_context& ioctx, const std::string& ip, const std::string& port)
@ -127,23 +152,50 @@ bool DCC::isPending() const
return mp->pending; return mp->pending;
} }
IRCError DCC::accept() IRCError DCC::accept(const std::string& targetNickname)
{ {
if (mp->direction == Direction::Initiator) if (mp->direction == Direction::Initiator)
return IRCError::DCC_NotATarget; // Only usable if we are a Target... return IRCError::DCC_NotATarget; // Only usable if we are a Target...
mp->resolverResults = mp->resolver.resolve(mp->ip, mp->port); auto acceptNormally = [this] {
mp->resolverResults = mp->resolver.resolve(mp->ip, mp->port);
asio::async_connect(mp->socket, mp->resolverResults,
[this](const asio::error_code& ec, const tcp::endpoint&) { asio::async_connect(mp->socket, mp->resolverResults,
if (ec) [this](const asio::error_code& ec, const tcp::endpoint&) {
mp->disconnected(ec); if (ec)
mp->disconnected(ec);
else
mp->connected(ec);
});
mp->pending = false;
return IRCError::NoError;
};
auto acceptReversed = [this, &targetNickname] {
auto ipAddress = findOwnIpAddress();
mp->startListener();
if (isPrivateIp(ipAddress) && NATPortMapping::add(ipAddress, port(), port())) {
auto publicIp = NATPublicAddress::get();
if (publicIp.has_value())
ipAddress = longIpToNormal(*publicIp);
else else
mp->connected(ec); NATPortMapping::remove(ipAddress, port(), port());
}); }
mp->ircctx.ctcpRequest(targetNickname, "DCC", fmt::format("CHAT chat {} {}",
normalIpToLong(ipAddress),
port()));
mp->pending = false; mp->pending = false;
return IRCError::NoError; return IRCError::NoError;
};
if (mp->port == "0")
return acceptReversed();
else
return acceptNormally();
} }
void DCC::disconnect() void DCC::disconnect()
@ -203,6 +255,11 @@ const std::string& DCC::ip() const
return mp->ip; return mp->ip;
} }
bool DCC::isReversed() const
{
return mp->port == "0";
}
void DCC::setCallbackRead(DCC::CallbackRead&& cb) void DCC::setCallbackRead(DCC::CallbackRead&& cb)
{ {
mp->cbRead = std::move(cb); mp->cbRead = std::move(cb);

@ -47,7 +47,7 @@ public:
DCC(IRCBase& ircctx, asio::io_context& ioctx, const std::string& ip, const std::string& port); DCC(IRCBase& ircctx, asio::io_context& ioctx, const std::string& ip, const std::string& port);
[[nodiscard]] bool isPending() const; [[nodiscard]] bool isPending() const;
IRCError accept(); IRCError accept(const std::string& targetNickname);
void disconnect(); void disconnect();
@ -59,9 +59,22 @@ public:
const IRCBase& context() const; const IRCBase& context() const;
bool isConnected() const; bool isConnected() const;
/*
* As initiator: Port to listen on.
* Reversed initiator: Always zero.
* As target: Port to connect to.
*/
uint16_t port() const; uint16_t port() const;
/*
* As initiator: IP to listen on (published in CTCP DCC request)
* As target or reversed initiator: IP to connect to.
*/
const std::string& ip() const; const std::string& ip() const;
bool isReversed() const;
/* /*
* Callbacks used by 'users' of this class. * Callbacks used by 'users' of this class.
*/ */
@ -84,7 +97,7 @@ protected:
virtual void onDisconnected(IRCError e); virtual void onDisconnected(IRCError e);
private: private:
std::unique_ptr<DCCPriv> mp; std::unique_ptr<DCCPriv> mp;
}; };
#endif // DCC_H #endif // DCC_H

@ -24,8 +24,8 @@
namespace { namespace {
/* Debug/development options */ /* Debug/development options */
constexpr bool DumpReadData = true; constexpr bool DumpReadData = false;
constexpr bool DumpWriteData = true; constexpr bool DumpWriteData = false;
const std::vector<std::string> V3Support { const std::vector<std::string> V3Support {
"account-notify", "account-notify",

@ -15,23 +15,6 @@
namespace { namespace {
constexpr auto DefaultIpAddress{ "127.0.0.1" }; constexpr auto DefaultIpAddress{ "127.0.0.1" };
uint32_t ipStrToInt(const std::string& ip)
{
std::stringstream ss(ip);
std::string p;
std::vector<unsigned> parts;
while (getline(ss, p, '.'))
parts.emplace_back(std::stoul(p));
uint32_t ret = parts[0];
for (int i = 1; i < 4; ++i) {
ret <<= 8u;
ret |= parts[i];
}
return ret;
}
std::string getIpFromSecondToken(const std::string& input) std::string getIpFromSecondToken(const std::string& input)
{ {
if (input.empty()) if (input.empty())
@ -127,24 +110,41 @@ bool singleWildcardMatch(const std::string& str, const std::string& wc)
return !(hasLeft && hasRight && leftEnd == rightBegin); return !(hasLeft && hasRight && leftEnd == rightBegin);
} }
std::string longIpToNormal(std::uint32_t longip)
{
unsigned ip = longip;
unsigned a = (ip & 0xFF000000u) >> 24u;
unsigned b = (ip & 0x00FF0000u) >> 16u;
unsigned c = (ip & 0x0000FF00u) >> 8u;
unsigned d = ip & 0x000000FFu;
return fmt::format("{}.{}.{}.{}", a, b, c, d);
}
std::string longIpToNormal(const std::string& longip) std::string longIpToNormal(const std::string& longip)
{ {
try { try {
unsigned ip = std::stoul(longip); return longIpToNormal( std::stoul(longip) );
unsigned a = (ip & 0xFF000000u) >> 24u;
unsigned b = (ip & 0x00FF0000u) >> 16u;
unsigned c = (ip & 0x0000FF00u) >> 8u;
unsigned d = ip & 0x000000FFu;
return fmt::format("{}.{}.{}.{}", a, b, c, d);
} }
catch (...) { catch (...) {
return "0.0.0.0"; return "0.0.0.0";
} }
} }
std::string normalIpToLong(const std::string& normalip) std::uint32_t normalIpToLong(const std::string& normalip)
{ {
return std::to_string(ipStrToInt(normalip)); std::stringstream ss(normalip);
std::string p;
std::vector<unsigned> parts;
while (getline(ss, p, '.'))
parts.emplace_back(std::stoul(p));
uint32_t ret = parts[0];
for (int i = 1; i < 4; ++i) {
ret <<= 8u;
ret |= parts[i];
}
return ret;
} }
std::string concatenateModes(const std::unordered_map<char, std::string>& modes) std::string concatenateModes(const std::unordered_map<char, std::string>& modes)
@ -292,7 +292,7 @@ bool isPrivateIp(const std::string& ip)
* Class C: 192.168.0.0 - 192.168.255.255 * Class C: 192.168.0.0 - 192.168.255.255
*/ */
const auto intip{ ipStrToInt(ip) }; const auto intip{ normalIpToLong(ip) };
const auto classIp{ (intip & 0xFF000000) >> 24 }; const auto classIp{ (intip & 0xFF000000) >> 24 };

@ -8,6 +8,7 @@
#ifndef IRCUTILITIES_H #ifndef IRCUTILITIES_H
#define IRCUTILITIES_H #define IRCUTILITIES_H
#include <cstdint>
#include <unordered_map> #include <unordered_map>
#include <string> #include <string>
#include <algorithm> #include <algorithm>
@ -16,8 +17,9 @@ constexpr char CTCPflag { 0x01 };
std::pair<std::string,std::string> FormatCTCPLine(std::string line); std::pair<std::string,std::string> FormatCTCPLine(std::string line);
bool singleWildcardMatch(const std::string& str, const std::string& wc); bool singleWildcardMatch(const std::string& str, const std::string& wc);
std::string longIpToNormal(std::uint32_t longip);
std::string longIpToNormal(const std::string& longip); std::string longIpToNormal(const std::string& longip);
std::string normalIpToLong(const std::string& normalip); std::uint32_t normalIpToLong(const std::string& normalip);
std::string concatenateModes(const std::unordered_map<char, std::string>& modes); std::string concatenateModes(const std::unordered_map<char, std::string>& modes);
bool strEquals(const std::string& l, const std::string& r); bool strEquals(const std::string& l, const std::string& r);
std::string toBase64(const std::string& input); std::string toBase64(const std::string& input);

@ -6,12 +6,13 @@
*/ */
#include "IWinDCCChat.h" #include "IWinDCCChat.h"
#include "IWinStatus.h"
#include <IRCClient/IRCBase.h> #include <IRCClient/IRCBase.h>
#include <QMessageBox> #include <QMessageBox>
#include <iostream> #include <iostream>
IWinDCCChat::IWinDCCChat(std::shared_ptr<DCC> dcc_, const QString& targetNickname) IWinDCCChat::IWinDCCChat(IWinStatus* statusParent, std::shared_ptr<DCC> dcc_, const QString& targetNickname)
: IWin(IWin::Type::DCCChat, nullptr) : IWin(IWin::Type::DCCChat, statusParent)
, dcc(std::move(dcc_)) , dcc(std::move(dcc_))
{ {
setButtonText(targetNickname); setButtonText(targetNickname);
@ -32,9 +33,7 @@ IWinDCCChat::IWinDCCChat(std::shared_ptr<DCC> dcc_, const QString& targetNicknam
connect(input, &ILineEdit::newLine, connect(input, &ILineEdit::newLine,
this, &IWinDCCChat::newLine); this, &IWinDCCChat::newLine);
dcc->setCallbackConnected([this]{ onConnected(); }); setupCallbacks();
dcc->setCallbackDisconnected([this](IRCError e){ onDisconnected(e); });
dcc->setCallbackRead([this](const DCC::ByteString& data, std::size_t length){ onRead(data, length); });
} }
bool IWinDCCChat::printWithCustomTime(const QDateTime& timestamp, PrintType ptype, const QString& text) bool IWinDCCChat::printWithCustomTime(const QDateTime& timestamp, PrintType ptype, const QString& text)
@ -51,10 +50,23 @@ void IWinDCCChat::refreshWindowTitle()
void IWinDCCChat::acceptAsTarget() void IWinDCCChat::acceptAsTarget()
{ {
const auto ip = QString::fromStdString(dcc->ip()); const auto ip = QString::fromStdString(dcc->ip());
const auto port = QString::number(dcc->port());
print(PrintType::ProgramInfo, tr("Connecting to %1:%2...").arg(ip, port));
dcc->accept(); if (dcc->isReversed()) {
print(PrintType::ProgramInfo, tr("Waiting for participant..."));
}
else {
const auto port = QString::number(dcc->port());
print(PrintType::ProgramInfo, tr("Connecting to %1:%2...").arg(ip, port));
}
dcc->accept(getButtonText().toStdString());
}
void IWinDCCChat::setupCallbacks()
{
dcc->setCallbackConnected([this]{ onConnected(); });
dcc->setCallbackDisconnected([this](IRCError e){ onDisconnected(e); });
dcc->setCallbackRead([this](const DCC::ByteString& data, std::size_t length){ onRead(data, length); });
} }
void IWinDCCChat::closeEvent(QCloseEvent* evt) void IWinDCCChat::closeEvent(QCloseEvent* evt)
@ -139,3 +151,11 @@ void IWinDCCChat::disconnectFromDCC()
{ {
dcc->disconnect(); dcc->disconnect();
} }
void IWinDCCChat::initiateReversed(std::shared_ptr<DCC> dcc_)
{
reverseInitiatePending = false;
dcc = std::move(dcc_);
setupCallbacks();
dcc->accept(getButtonText().toStdString());
}

@ -15,22 +15,28 @@
#include <QCloseEvent> #include <QCloseEvent>
#include <memory> #include <memory>
class IWinStatus;
class IWinDCCChat : public IWin class IWinDCCChat : public IWin
{ {
Q_OBJECT Q_OBJECT
public: public:
IWinDCCChat(std::shared_ptr<DCC> dcc_, const QString& targetNickname); IWinDCCChat(IWinStatus* statusParent, std::shared_ptr<DCC> dcc_, const QString& targetNickname);
bool printWithCustomTime(const QDateTime& timestamp, PrintType ptype, const QString& text) override; bool printWithCustomTime(const QDateTime& timestamp, PrintType ptype, const QString& text) override;
void refreshWindowTitle() override; void refreshWindowTitle() override;
void clear() override { view->resetView(); } void clear() override { view->resetView(); }
void acceptAsTarget(); void acceptAsTarget();
void disconnectFromDCC(); void disconnectFromDCC();
void markReverseInitiatePending() { reverseInitiatePending = true; }
bool isReverseInitiatePending() const { return reverseInitiatePending; }
void initiateReversed(std::shared_ptr<DCC> dcc_);
private: private:
void setupCallbacks();
void closeEvent(QCloseEvent* evt) override; void closeEvent(QCloseEvent* evt) override;
void onConnected(); void onConnected();
void onDisconnected(IRCError e); void onDisconnected(IRCError e);
@ -43,6 +49,7 @@ private:
ILineEdit* input; ILineEdit* input;
QString lineBuffer; QString lineBuffer;
bool reverseInitiatePending{ false };
std::shared_ptr<DCC> dcc; std::shared_ptr<DCC> dcc;
}; };

@ -8,7 +8,10 @@
#include "IdealIRC.h" #include "IdealIRC.h"
#include "ui_IdealIRC.h" #include "ui_IdealIRC.h"
#include "config.h" #include "config.h"
#include "IWin/IWinStatus.h" #include "IWin/IWinStatus.h"
#include "IWin/IWinDCCChat.h"
#include <ConfigMgr.h> #include <ConfigMgr.h>
#include <QDebug> #include <QDebug>
#include <QDir> #include <QDir>
@ -367,14 +370,36 @@ void IdealIRC::onStatusWindowCreated(IWinStatus* window)
} }
}); });
connect(&(window->getConnection()), &IRC::DCCRequested, this, &IdealIRC::onDCCRequested); connect(&(window->getConnection()), &IRC::DCCRequested, [this,window](const std::shared_ptr<DCC>& dcc, const IRCPrefix& sender, const QString& type, const QString& message) {
onDCCRequested(*window, dcc, sender, type, message);
});
} }
void IdealIRC::onDCCRequested(const std::shared_ptr<DCC>& dcc, const IRCPrefix& sender, const QString& type, const QString& message) void IdealIRC::onDCCRequested(const IWinStatus& statusParent, const std::shared_ptr<DCC>& dcc, const IRCPrefix& sender, const QString& type, const QString& message)
{ {
// TODO check type and message together... DCC SEND would use 'message' to hold filename and size /*
dccQueryDlg.addQuery(sender, type, dcc); * Need to check any DCC windows already open in the given status parent of the inbound DCC for pending reverses.
dccQueryDlg.show(); * Then compare for any matching pending reverses with this inbound DCC.
*/
auto dccChatList = MdiManager::instance().childrenOf(&statusParent, IWin::Type::DCCChat);
bool requestHandled{ false };
for (auto* subwin : dccChatList) {
auto* dccwin = dynamic_cast<IWinDCCChat*>(subwin);
if (!dccwin) {
qWarning() << "Unable to cast a window marked as type DCCChat!?";
continue;
}
if (QString::fromStdString(sender.toString()) == dccwin->getButtonText() && dccwin->isReverseInitiatePending()) {
requestHandled = true;
dccwin->initiateReversed(dcc);
}
}
if (!requestHandled) {
dccQueryDlg.addQuery(sender, type, dcc);
dccQueryDlg.show();
}
} }
void IdealIRC::on_actionConnect_triggered() void IdealIRC::on_actionConnect_triggered()

@ -35,7 +35,7 @@ public:
private slots: private slots:
void onStatusWindowCreated(IWinStatus* window); void onStatusWindowCreated(IWinStatus* window);
void onDCCRequested(const std::shared_ptr<DCC>& dcc, const IRCPrefix& sender, const QString& type, const QString& message); void onDCCRequested(const IWinStatus& statusParent, const std::shared_ptr<DCC>& dcc, const IRCPrefix& sender, const QString& type, const QString& message);
void on_actionConnect_triggered(); void on_actionConnect_triggered();
void on_actionOptions_triggered(); void on_actionOptions_triggered();
void on_actionAbout_IdealIRC_triggered(); void on_actionAbout_IdealIRC_triggered();

@ -109,17 +109,26 @@ IWin* MdiManager::createSubwindow(IWin* parent, const QString& buttonText, IWin:
return createSubwindowCommons(parent, basePtr, buttonText, iconPath, activate, buttonHighlight); return createSubwindowCommons(parent, basePtr, buttonText, iconPath, activate, buttonHighlight);
} }
IWin* MdiManager::createDCCSubwindow(IWin* parent, IWin::Type windowType, std::shared_ptr<DCC> dcc, const IRCPrefix& target) IWin* MdiManager::createDCCSubwindow(IWin* parent, IWin::Type windowType, const std::shared_ptr<DCC>& dcc, const IRCPrefix& target)
{ {
IWin* basePtr{ nullptr }; IWin* basePtr{ nullptr };
IWinStatus* statusParent{ nullptr };
QString iconPath; QString iconPath;
auto targetNickname{ QString::fromStdString(target.toString()) }; auto targetNickname{ QString::fromStdString(target.toString()) };
if (parent && parent->getType() == IWin::Type::Status)
statusParent = dynamic_cast<IWinStatus*>(parent);
switch (windowType) { switch (windowType) {
case IWin::Type::DCCChat: case IWin::Type::DCCChat: {
basePtr = new IWinDCCChat(std::move(dcc), targetNickname); auto* winPtr = new IWinDCCChat(statusParent, dcc, targetNickname);
if (dcc->isReversed())
winPtr->markReverseInitiatePending();
basePtr = winPtr;
iconPath = ":/Icons/private.png"; iconPath = ":/Icons/private.png";
break; break;
}
default: default:
break; break;

@ -34,7 +34,7 @@ public:
static MdiManager& instance(); static MdiManager& instance();
IWin* createSubwindow(IWin* parent, const QString& buttonText, IWin::Type windowType, bool activate = true, Highlight buttonHighlight = HL_None); IWin* createSubwindow(IWin* parent, const QString& buttonText, IWin::Type windowType, bool activate = true, Highlight buttonHighlight = HL_None);
IWin* createDCCSubwindow(IWin* parent, IWin::Type windowType, std::shared_ptr<DCC> dcc, const IRCPrefix& target); IWin* createDCCSubwindow(IWin* parent, IWin::Type windowType, const std::shared_ptr<DCC>& dcc, const IRCPrefix& target);
IWin* currentWindow() const; IWin* currentWindow() const;
IWinStatus* currentStatus() const; IWinStatus* currentStatus() const;

Loading…
Cancel
Save