Compare commits

...

81 Commits

Author SHA1 Message Date
Tomatix 48189c58a1 Version 1.1.0 2 years ago
Tomatix 4da5512456 #137 Added some popular servers to servers.json. Updated InnoSetup script to use servers.json. 2 years ago
Tomatix b8c2fd7d1b #143 Script engine: Renamed TokenType to STokenType yielding for some Windows API specific stuff. 3 years ago
Tomatix bb09217d7d #2 DCC with Windows OS specific implementation 3 years ago
Tomatix 433fdf747c #2 DCC with reverse initiation functionality and NAT integration. 3 years ago
Tomatix 29ea535243 #2 Inbound and outbound DCC chat, still need better NAT handling. 3 years ago
Tomatix 4bfc9b2380 #2 Added function to check if an IP is private (f.ex. 192.168.1.1) or not. Added functions to read the computer's own IP address and gateway IP address. Missing Windows implementation for that. 3 years ago
Tomatix 60768be2ce #2 Show a "not connected" message in the DCC Chat window if there is no connection while typing a message. 3 years ago
Tomatix 523e21d3db #2 Various improvements on existing, albeit incomplete DCC implementation in Core. DCC Chat implementation. DCC request dialog. 3 years ago
Tomatix c0c6a6eee1 Test release 1.1.0 dev8 3 years ago
Tomatix 3fae7faed0 #124 Use SASL credentials from config to Core client. 3 years ago
Tomatix 4c27c89967 #124 Correctly use PLAIN mechanism 3 years ago
Tomatix 9904699c43 #124 Added missing file from previous commit 3 years ago
Tomatix 5e159e2b9c #124 SASL PLAIN implemented in Core 3 years ago
Tomatix 7d955b7a8f #124 Base64 encoding implementation 3 years ago
Tomatix 8bae52c9e4 #124 SASL configuration. Client implementation not finished. 3 years ago
Tomatix 1bb74bf12c #134 Added checking of a modified ServerModel, raising a prompt if modified. If "No" is selected, the model is reset. If "Yes", model is saved to file as if by pressing the "Save" button. 3 years ago
Tomatix 8312170b71 #134 Saving servers complete, plus some small input checks with fallbacks. 3 years ago
Tomatix d7c744baa5 #134 Added a hack to the ServerModel that mitigates an issue with the server tree view where expanded networks won't show newly added servers, as well as expand/collapse decoration for empty networks doesn't show when adding a server to them. 3 years ago
Tomatix 50abd64932 #134 Delete server/network implemented 3 years ago
Tomatix ff81cabbb3 #134 When adding server without port in address, use defaults 6697 if SSL is checked, 6667 if not. 3 years ago
Tomatix 9185893037 #134 More work done on the new server config editing. 3 years ago
Tomatix c26715cf65 #134 Added an 'Add server' dialog, wip 3 years ago
Tomatix 2a600143d8 #133 Removed obsolete files for Server model 3 years ago
Tomatix f4a95a8eb1 #133 Better model for handling servers; using json (servers.json) for storing servers 3 years ago
Tomatix d032da0ce6 #127 Wrong logic, dangling member is when its channels _is_ empty. 4 years ago
Tomatix e5a8498e7c #132 Clear nickname list in channel when parting 4 years ago
Tomatix 8c8e85a263 #70 Check if subwindow is not nullptr 4 years ago
Tomatix b5dfe048a6 #70 Show QUIT message for user if their query window is open 4 years ago
Tomatix e81498fdaa Test release dev5 4 years ago
Tomatix 7c75677cec #127 Fixed removing and adding of members of channels 4 years ago
Tomatix cde36f9fea #125 ConfigMgr refactor. 4 years ago
Tomatix 1e3cd9a1a7 #121 Use specified IRCv3 features given from the configuration. 4 years ago
Tomatix 353b088dea #121 Instead of explicit enable/disable IRCv3 features, just specify which ones we need. 4 years ago
Tomatix 505231f9b2 #121 Added capability to enable/disable IRCv3 extensions for IRCClient. 4 years ago
Tomatix 34efc87086 #121 Connected ConfigMgr and the GUI frontend for IRCv3 configuration. 4 years ago
Tomatix 11c0d64c27 #121 Added configuration management for IRCv3 config 4 years ago
Tomatix 320cf68fff #121 Added GUI frontend for IRCv3 options. 4 years ago
Tomatix 9d19dc75ba #123 Hide "Background image" tab - it's useless for now. 4 years ago
Tomatix 3b3cdb860c Merge remote-tracking branch 'origin/1_1_0_dev' into 1_1_0_dev 4 years ago
Tomatix 9012607434 #91 Timestamp for chat history. 4 years ago
Tomatix 36c8fc20e7 #91 Timestamp for chat history / batch. 4 years ago
Tomatix 72de07fb30 #90 Added capability to print messages with custom timestamp 4 years ago
Tomatix d2974336de #90 Give read-only access to the current IRCMessage being parsed. 4 years ago
Tomatix 12158d9f10 #119 IRCv3 message tags. 4 years ago
Tomatix 02e09bccb6 #118 Fixed a bug in script engine with logical operators. 4 years ago
Tomatix 147d8b5905 #117 Fixed using /kick nickname while in a channel window. 4 years ago
Tomatix c417b137d6 #116 Reverting issue #106 - Mark connection as registered only upon receiving RPL_ENDOFMOTD or ERR_NOMOTD. 4 years ago
Tomatix 93e4840ec4 #17 Tabs to spaces 4 years ago
Tomatix fac1766fc5 #108 Removed unneccessary "undef" for Windows builds. 4 years ago
Tomatix 5dd821e475 #114 Added showing of tray messages 4 years ago
Tomatix b13547646a #114 Added tray messages 4 years ago
Tomatix 101e18429e #114 Highlight messages with our nickname in it and the corresponding window 4 years ago
Tomatix 94423c479b #114 Highlight incoming private message windows 4 years ago
Tomatix 8efebe04f5 #114 Added functionality to highlight window buttons upon creation. 4 years ago
Tomatix d41008283b #113 Always parse ISUPPORT message. 4 years ago
Tomatix 7cee1eb1cc #112 Added a tray icon menu. Double click on tray icon toggles visibility. 4 years ago
Tomatix 8a7579ac43 #106 Mark connection as registered upon receiving 001 4 years ago
Tomatix 5090761258 #95 Show error code and message in Unhandled exception error. Emit disconnected signal on connection error (affects connection button in toolbar.) 4 years ago
Tomatix d6ca35f084 #13 Reload the selected server item upon save 4 years ago
Tomatix c1f0055e5e #13 Added SSL option to Server editor. 4 years ago
Tomatix 040e63b625 #111 Use server password from configuration 4 years ago
Tomatix 7255df9077 #109 Use "raw" data to handle clearing of topic (/topic -c #channel). 4 years ago
Tomatix 909b6ff5f1 #110 Added script functions: strupper() strlower() 4 years ago
Tomatix cca07d10ed #108 ERROR is a macro in msvc :( 4 years ago
Tomatix de1ca627db #6 Option to hide the toolbar 4 years ago
Tomatix 80fe93184f #27 Disable compiler warnings about deprecated declarations. 4 years ago
Tomatix be0ca028fa #27 Compiler warning fixes 4 years ago
Tomatix 9d29452eb4 #27 Compiler warning fix 4 years ago
Tomatix 204513405a #27 Compiler warning fix 4 years ago
Tomatix 54bd4c268f #27 Compiler warning fixes 4 years ago
Tomatix 6c82c4bced #103 Refresh window title even if TOPIC is marked with "ignore verbosity". 4 years ago
Tomatix d3898e7b43 #103 Update internal topic storage upon change. 4 years ago
Tomatix e088cbb40a #107 Nickname and Channel lookup is now case-insensitive. 4 years ago
Tomatix 56a4f0f029 #102 Various cleanup from the previous refactor 4 years ago
Tomatix 05a9d50e04 #102 KILL command was treated as KICK... 4 years ago
Tomatix bba7227716 #102 Removed unneeded includes 4 years ago
Tomatix 8587e11c3c #102 Moved more logic for IRCClient to own source files 4 years ago
Tomatix 31be24eb7f #102 Added a simple sandbox-implementation of "IRCClient" - For testing stuff. 4 years ago
Tomatix b74969cef9 #102 Encapsulate inbound messages, handle tokenization in there as well. 4 years ago
Tomatix 54b6e79ce0 Version 1.1.0 development 4 years ago
  1. 85
      CHANGELOG
  2. 29
      CMakeLists.txt
  3. 3
      ICommand/CMakeLists.txt
  4. 60
      ICommand/CommandData.cpp
  5. 16
      ICommand/CommandData.h
  6. 21
      ICommand/Commands.h
  7. 20
      ICommand/External/kick.cpp
  8. 28
      ICommand/External/notice.cpp
  9. 30
      ICommand/External/privmsg.cpp
  10. 1127
      ICommand/ICommand.cpp
  11. 44
      ICommand/ICommand.h
  12. 6
      ICommand/ICommandPriv.cpp
  13. 16
      ICommand/ICommandPriv.h
  14. 52
      ICommand/Internal/dcc.cpp
  15. 26
      ICommand/Internal/me.cpp
  16. 98
      IConfig/AddServer.cpp
  17. 46
      IConfig/AddServer.h
  18. 183
      IConfig/AddServer.ui
  19. 12
      IConfig/CMakeLists.txt
  20. 552
      IConfig/ColorConfig.cpp
  21. 74
      IConfig/ColorConfig.h
  22. 178
      IConfig/IConfig.cpp
  23. 52
      IConfig/IConfig.h
  24. 100
      IConfig/IConfigLogging.cpp
  25. 30
      IConfig/IConfigLogging.h
  26. 200
      IConfig/IConfigOptions.cpp
  27. 73
      IConfig/IConfigOptions.h
  28. 897
      IConfig/IConfigOptions.ui
  29. 223
      IConfig/IConfigServers.cpp
  30. 51
      IConfig/IConfigServers.h
  31. 63
      IConfig/IConfigServers.ui
  32. 313
      IConfig/ServerEditor.cpp
  33. 59
      IConfig/ServerEditor.h
  34. 251
      IConfig/ServerEditor.ui
  35. 51
      IConfig/ServerItem.cpp
  36. 76
      IConfig/ServerItem.h
  37. 133
      IConfig/ServerMgr.cpp
  38. 66
      IConfig/ServerMgr.h
  39. 612
      IConfig/ServerModel.cpp
  40. 81
      IConfig/ServerModel.h
  41. 105
      IConfig/ServerOptionsDelegate.cpp
  42. 26
      IConfig/ServerOptionsDelegate.h
  43. 125
      IConfig/todo/ServerItem.cpp
  44. 67
      IConfig/todo/ServerItem.h
  45. 172
      IConfig/todo/ServerModel.cpp
  46. 54
      IConfig/todo/ServerModel.h
  47. 59
      IRCClient/CMakeLists.txt
  48. 109
      IRCClient/Commands.h
  49. 14
      IRCClient/Commands/Unhandled.cpp
  50. 14
      IRCClient/Commands/cmdError.cpp
  51. 14
      IRCClient/Commands/cmdInvite.cpp
  52. 43
      IRCClient/Commands/cmdJoin.cpp
  53. 16
      IRCClient/Commands/cmdKick.cpp
  54. 14
      IRCClient/Commands/cmdKill.cpp
  55. 31
      IRCClient/Commands/cmdMode.cpp
  56. 29
      IRCClient/Commands/cmdNick.cpp
  57. 23
      IRCClient/Commands/cmdNotice.cpp
  58. 16
      IRCClient/Commands/cmdPart.cpp
  59. 19
      IRCClient/Commands/cmdPing.cpp
  60. 14
      IRCClient/Commands/cmdPong.cpp
  61. 51
      IRCClient/Commands/cmdPrivmsg.cpp
  62. 32
      IRCClient/Commands/cmdQuit.cpp
  63. 17
      IRCClient/Commands/cmdTopic.cpp
  64. 18
      IRCClient/Commands/cmdV3Account.cpp
  65. 20
      IRCClient/Commands/cmdV3Authenticate.cpp
  66. 24
      IRCClient/Commands/cmdV3Away.cpp
  67. 64
      IRCClient/Commands/cmdV3Cap.cpp
  68. 14
      IRCClient/Commands/cmdWallops.cpp
  69. 135
      IRCClient/Commands/handleNumeric.cpp
  70. 373
      IRCClient/DCC.cpp
  71. 107
      IRCClient/DCC.h
  72. 1438
      IRCClient/IRCBase.cpp
  73. 222
      IRCClient/IRCBase.h
  74. 162
      IRCClient/IRCBasePriv.h
  75. 50
      IRCClient/IRCChannel.cpp
  76. 40
      IRCClient/IRCChannel.h
  77. 205
      IRCClient/IRCError.cpp
  78. 85
      IRCClient/IRCError.h
  79. 35
      IRCClient/IRCMember.cpp
  80. 22
      IRCClient/IRCMember.h
  81. 60
      IRCClient/IRCMemberEntry.cpp
  82. 18
      IRCClient/IRCMemberEntry.h
  83. 114
      IRCClient/IRCMessage.cpp
  84. 50
      IRCClient/IRCMessage.h
  85. 96
      IRCClient/IRCPrefix.cpp
  86. 58
      IRCClient/IRCPrefix.h
  87. 13
      IRCClient/Numeric.h
  88. 27
      IRCClient/Private/addMemberToChannel.cpp
  89. 30
      IRCClient/Private/channelModeGroup.cpp
  90. 51
      IRCClient/Private/connected.cpp
  91. 24
      IRCClient/Private/ctcp.cpp
  92. 63
      IRCClient/Private/delMemberFromChannel.cpp
  93. 24
      IRCClient/Private/disconnectHandler.cpp
  94. 14
      IRCClient/Private/isChannelSymbol.cpp
  95. 14
      IRCClient/Private/isMemberPrivilegeSymbol.cpp
  96. 21
      IRCClient/Private/keepaliveTimeout.cpp
  97. 17
      IRCClient/Private/memberPrivilegeSymbolToMode.cpp
  98. 56
      IRCClient/Private/parseChannelModeMessage.cpp
  99. 143
      IRCClient/Private/parseIncoming.cpp
  100. 34
      IRCClient/Private/read.cpp
  101. Some files were not shown because too many files have changed in this diff Show More

@ -2,6 +2,91 @@ IdealIRC changelog. http://www.idealirc.org/
Issues at https://git.idealirc.org/idealirc/idealirc/issues
Version 1.1.0 - January 5th 2023
#2 Implement DCC
* Added: DCC Chat.
#6 Consider removing or hiding the toolbar
* Added: GUI option to hide/show the toolbar.
#13 Improve the server editor
* Added: SSL option for individual servers.
* Added: SASL option for individual servers.
* Added: Checkable buttons for managing individual servers.
#70 Show QUIT message for user if their query window is open
* Added: Message in Query window if the user quits IRC.
#90 IRCv3 server-time
* Added: Support for timestamping chat history upon joining channels.
#95 Better error handling / display
* Improved: Show error code and message when SSL connections fail with "Unhandled exception".
#102 Improve the message parser
* Improved: Parser for inbound data from IRC servers is now more flexible at handling new features as well as IRCv3 extensions.
#103 Topic in channel's titlebar does not change
* Fixed: Refresh titlebar with new channel topic when it is changed.
#107 Looking up channels and nicknames isn't case sensitive
* Fixed: Case-insensitive lookup of the internal storage of channels and users.
#109 Topic doesn't show in the Channel Settings dialog
* Fixed: Corrected how topic clear command works (/topic -c #channel)
* Fixed: ChannelSettings.iis now handles channel names case-insensitively
#110 New script functions: strupper, strlower
* Added: Script finction strupper(string) that makes the input string all upper-case.
* Added: Script finction strlower(string) that makes the input string all lower-case.
#111 Server password isn't sent using the Options dialog
* Fixed: Use server password from configuration.
#112 Make use of the tray icon
* Added: Context menu when right-clicking the tray icon.
* Added: Double-clicking the icon toggles the program's visibility.
#114 Message highlighter doesn't work
* Fixed: Added missing functionality that prevented this to work.
#117 /KICK nickname in channel doesn't work
* Fixed: Missed check of channel parameter existence.
#118 ValueHolder seems to have breaks in some if..else if...else chains
* Fixed: Corrected an issue in the script engine related to logical operators.
#119 Support IRCv3 tags
* Added: Support for the message-tags IRCv3 extension.
#121 Add config options to enable/disable IRCv3 features
* Added: Own tab page in the Options dialog to enable/disable IRCv3 features.
#123 Remove the "Backround image" tab
* Removed: Background image option in Options dialog. This was only half-done and not working as intended anyway. Might see a come-back.
#124 SASL
* Added: SASL PLAIN authentication
* Added: Configuration option in the Options dialog to set SASL PLAIN credentials
#127 Re-joining seems to only populate a few of the actual users in channel
* Fixed: Corrected the clean-up procedures for the internal storage of members.
#132 Clear nickname list in channel when parting
* Improved: Clear the nickname list when leaving a channel (ie. by /part and the window is therefore still open)
#133 Replace servers.ini with servers.json
* Improved: Changed to use JSON format for servers due to how much information we need about each.
#134 Improve editing and adding servers
* Added: Double-clicking the columns in the editor allows editing on-the-fly (and auto saves).
* Added: More intuitive "add server" dialog box.
#137 Create a pre-populated servers.json with a few popular networks
* Improved: More servers and networks in servers.json.
Various source code cleanup (Issues #17, #27, #125 and #143)
Version 1.0.1 - July 8th 2021
#68 QUIT message isn't always sent
* Bugfix: Ensured that the configured QUIT message is used when closing status window, using tool buttons and when

@ -7,8 +7,8 @@ project(IdealIRC)
#
set(BUILD_TYPE "packaged")
set(VERSION_MAJOR 1)
set(VERSION_MINOR 0)
set(VERSION_PATCH 1)
set(VERSION_MINOR 1)
set(VERSION_PATCH 0)
set(VERSION_APPEND "")
#
@ -25,26 +25,36 @@ include_directories(${IIRC_SOURCE_DIR})
include_directories(${IIRC_SOURCE_DIR}/IdealIRC)
include_directories(${CMAKE_BINARY_DIR})
option(BUILD_DEV_STUFF "Build additional development stuff" OFF)
#
# Additional paths
#
set(IIRC_DEP_INCLUDE "" CACHE PATH "Additional include path (single path only)")
set(IIRC_DEP_LIBRARY "" CACHE PATH "Additional library path (single path only)")
if (NOT IIRC_DEP_INCLUDE STREQUAL "")
include_directories(${IIRC_DEP_INCLUDE})
message("Using additional include path: ${IIRC_DEP_INCLUDE}")
include_directories(${IIRC_DEP_INCLUDE})
message("Using additional include path: ${IIRC_DEP_INCLUDE}")
endif()
if (NOT IIRC_DEP_LIBRARY STREQUAL "")
link_directories(${IIRC_DEP_LIBRARY})
message("Using additional library path: ${IIRC_DEP_LIBRARY}")
link_directories(${IIRC_DEP_LIBRARY})
message("Using additional library path: ${IIRC_DEP_LIBRARY}")
endif()
# For GCC and Clang, basically.
if (UNIX)
# set(CMAKE_CXX_FLAGS "-Wall -Werror")
set(CMAKE_CXX_FLAGS "-pthread")
set(CMAKE_CXX_FLAGS "-pthread")
#
# Remove warnings about deprecated stuff.
# In order to change a statement using a deprecated API, it has to be investigated what it would affect.
# It is safe to temporarily disable this during development but should be enabled for release and test builds.
#
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-deprecated-declarations")
endif()
#
# Qt setup
#
@ -73,4 +83,9 @@ add_subdirectory(Script)
add_subdirectory(ScriptDialog)
add_subdirectory(ScriptFunctions)
add_subdirectory(Widgets)
add_subdirectory(NATUtils)
add_subdirectory(IdealIRC) # This one builds the binary.
if (BUILD_DEV_STUFF)
add_subdirectory(IRCClientSandbox)
endif()

@ -12,20 +12,19 @@ list(APPEND ${component}_SOURCES
list(APPEND ${component}_SOURCES_Internal
${CMAKE_CURRENT_SOURCE_DIR}/Internal/ctcp.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Internal/dcc.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Internal/ctcpreply.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Internal/me.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Internal/echo.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Internal/server.cpp
)
list(APPEND ${component}_SOURCES_External
${CMAKE_CURRENT_SOURCE_DIR}/External/kick.cpp
${CMAKE_CURRENT_SOURCE_DIR}/External/notice.cpp
${CMAKE_CURRENT_SOURCE_DIR}/External/privmsg.cpp
)
add_library(${component} STATIC ${${component}_SOURCES} ${${component}_SOURCES_Internal} ${${component}_SOURCES_External})
qt5_use_modules(${component} Widgets)
target_include_directories(${component} PRIVATE ${CMAKE_SOURCE_DIR})

@ -10,48 +10,48 @@
CommandData::CommandData(const QString& line, PredicateList argp, bool repeatLastPredicate)
{
for (int i = 0, ppos = 0; i < line.length(); ++i) {
QChar c = line[i];
if (c == ' ')
continue;
else {
if (ppos >= static_cast<int>(argp.size()) && m_l.at(ppos-1)) {
if (repeatLastPredicate)
ppos = argp.size()-1;
else
break;
}
for (int i = 0, ppos = 0; i < line.length(); ++i) {
QChar c = line[i];
if (c == ' ')
continue;
else {
if (ppos >= static_cast<int>(argp.size()) && m_l.at(ppos-1)) {
if (repeatLastPredicate)
ppos = argp.size()-1;
else
break;
}
const auto& result = argp[ppos](i, line);
m_l.push_back(result);
++ppos;
if (!result)
--i;
}
}
const auto& result = argp[ppos](i, line);
m_l.push_back(result);
++ppos;
if (!result)
--i;
}
}
while (m_l.size() < argp.size())
m_l.push_back(std::nullopt);
while (m_l.size() < argp.size())
m_l.push_back(std::nullopt);
}
int CommandData::size() const
{
return m_l.size();
return m_l.size();
}
const std::optional<std::string>& CommandData::operator[](int idx)
{
return m_l.at(idx);
return m_l.at(idx);
}
std::string CommandData::joinString(char sep) const
{
std::string ret;
for (const auto& item : m_l) {
if (!ret.empty())
ret += sep;
if (item.has_value())
ret += *item;
}
return ret;
std::string ret;
for (const auto& item : m_l) {
if (!ret.empty())
ret += sep;
if (item.has_value())
ret += *item;
}
return ret;
}

@ -15,22 +15,22 @@
#include <optional>
using PredicateList = std::vector< std::function<
std::optional<std::string>
(int& idx, const QString& line)
>>;
std::optional<std::string>
(int& idx, const QString& line)
>>;
class ICommand;
class CommandData
{
public:
CommandData(const QString& line, PredicateList argp, bool repeatLastPredicate = false);
int size() const;
const std::optional<std::string>& operator[](int idx);
std::string joinString(char sep) const;
CommandData(const QString& line, PredicateList argp, bool repeatLastPredicate = false);
int size() const;
const std::optional<std::string>& operator[](int idx);
std::string joinString(char sep) const;
private:
std::vector<std::optional<std::string>> m_l;
std::vector<std::optional<std::string>> m_l;
};
#endif // COMMANDDATA_H

@ -9,16 +9,17 @@
#define ICOMMAND_COMMANDS_H
namespace Command::Internal {
constexpr auto* ACTION = "ACTION";
constexpr auto* CTCP = "CTCP";
constexpr auto* CTCPREPLY = "CTCPREPLY";
constexpr auto* ME = "ME";
constexpr auto* ECHO = "ECHO";
constexpr auto* QUERY = "QUERY";
constexpr auto* MUTERESP = "MUTERESP";
constexpr auto* UNMUTERESP = "UNMUTERESP";
constexpr auto* CLEAR = "CLEAR";
constexpr auto* MSG = "MSG";
constexpr auto* ACTION = "ACTION";
constexpr auto* CTCP = "CTCP";
constexpr auto* DCC = "DCC";
constexpr auto* CTCPREPLY = "CTCPREPLY";
constexpr auto* ME = "ME";
constexpr auto* ECHO = "ECHO";
constexpr auto* QUERY = "QUERY";
constexpr auto* MUTERESP = "MUTERESP";
constexpr auto* UNMUTERESP = "UNMUTERESP";
constexpr auto* CLEAR = "CLEAR";
constexpr auto* MSG = "MSG";
constexpr auto* QUOTE = "QUOTE";
constexpr auto* RAW = "RAW";
constexpr auto* SERVER = "SERVER";

@ -11,16 +11,16 @@ using namespace Command::IRC;
void ICommandPriv::cmd_kick(const std::string& channel, const std::string& nickname, const std::string& reason)
{
auto& mdi = MdiManager::instance();
std::string chan = channel;
auto& mdi = MdiManager::instance();
std::string chan = channel;
if (chan.empty()) {
if (mdi.currentWindow()->getType() != IWin::Type::Channel) {
mdi.currentWindow()->print(PrintType::ProgramInfo, "Not in a channel window");
return;
}
chan = mdi.currentWindow()->getButtonText().toStdString();
}
if (chan.empty()) {
if (mdi.currentWindow()->getType() != IWin::Type::Channel) {
mdi.currentWindow()->print(PrintType::ProgramInfo, "Not in a channel window");
return;
}
chan = mdi.currentWindow()->getButtonText().toStdString();
}
connection.command(KICK, { channel, nickname }, reason);
connection.command(KICK, { chan, nickname }, reason);
}

@ -11,21 +11,21 @@ using namespace Command::IRC;
void ICommandPriv::cmd_notice(const std::string& target, const std::string& message)
{
auto& mdi = MdiManager::instance();
auto& mdi = MdiManager::instance();
connection.command(NOTICE, { target }, message);
connection.command(NOTICE, { target }, message);
IWin* winTarget = mdi.findWindow(&status, target.c_str());
IWin* active = status.getActiveWindow();
IWin* winTarget = mdi.findWindow(&status, target.c_str());
IWin* active = status.getActiveWindow();
if (!target.empty() && !message.empty()) {
if (winTarget && winTarget != active)
winTarget->print(PrintType::OwnText, QStringLiteral("->%1<- %2")
.arg(target.c_str())
.arg(message.c_str()));
if (active && active != winTarget)
active->print(PrintType::OwnText, QStringLiteral("->%1<- %2")
.arg(target.c_str())
.arg(message.c_str()));
}
if (!target.empty() && !message.empty()) {
if (winTarget && winTarget != active)
winTarget->print(PrintType::OwnText, QStringLiteral("->%1<- %2")
.arg(target.c_str())
.arg(message.c_str()));
if (active && active != winTarget)
active->print(PrintType::OwnText, QStringLiteral("->%1<- %2")
.arg(target.c_str())
.arg(message.c_str()));
}
}

@ -11,22 +11,22 @@ using namespace Command::IRC;
void ICommandPriv::cmd_privmsg(const std::string& target, const std::string& message)
{
auto& mdi = MdiManager::instance();
auto& mdi = MdiManager::instance();
connection.command(PRIVMSG, { target }, message);
connection.command(PRIVMSG, { target }, message);
if (!target.empty() && !message.empty()) {
IWin* win = mdi.findWindow(&status, target.c_str());
if (win) {
const auto* myNick = connection.getNickname().c_str();
win->print(PrintType::OwnText, QStringLiteral("<%1> %2")
.arg(myNick)
.arg(message.c_str()));
}
else
status.print(PrintType::OwnText, QStringLiteral(">%1< %2")
.arg(target.c_str())
.arg(message.c_str()));
}
if (!target.empty() && !message.empty()) {
IWin* win = mdi.findWindow(&status, target.c_str());
if (win) {
const auto* myNick = connection.getNickname().c_str();
win->print(PrintType::OwnText, QStringLiteral("<%1> %2")
.arg(myNick)
.arg(message.c_str()));
}
else
status.print(PrintType::OwnText, QStringLiteral(">%1< %2")
.arg(target.c_str())
.arg(message.c_str()));
}
}

File diff suppressed because it is too large Load Diff

@ -20,34 +20,34 @@ struct ICommandPriv;
class ICommand : public QObject
{
Q_OBJECT
Q_OBJECT
friend struct ICommandPriv;
friend struct ICommandPriv;
public:
explicit ICommand(IRC& connection_);
~ICommand();
explicit ICommand(IRC& connection_);
~ICommand();
void parse(QString text);
IRC& getConnection();
void parse(QString text);
IRC& getConnection();
private:
bool tryParseIRC(QString cmdLine);
bool tryParseInternal(QString cmdLine);
void print_notEnoughParameters(const QString& command);
void print_invalidCommandSwitch(const QString& command);
void print_invalidWindowTypeForCommand(const QString& command);
void print_notConnectedToServer(const QString& command);
/* Predicates for parsing commands */
static std::optional<std::string> prd_AnyWord(int& idx, const QString& line);
static std::optional<std::string> prd_AnyWordToUpper(int& idx, const QString& line);
static std::optional<std::string> prd_OptionalChannel(int& idx, const QString& line);
static std::optional<std::string> prd_NotSwitch(int& idx, const QString& line);
static std::optional<std::string> prd_Switch(int& idx, const QString& line);
static std::optional<std::string> prd_Message(int& idx, const QString& line);
std::unique_ptr<ICommandPriv> mp;
bool tryParseIRC(QString cmdLine);
bool tryParseInternal(QString cmdLine);
void print_notEnoughParameters(const QString& command);
void print_invalidCommandSwitch(const QString& command);
void print_invalidWindowTypeForCommand(const QString& command);
void print_notConnectedToServer(const QString& command);
/* Predicates for parsing commands */
static std::optional<std::string> prd_AnyWord(int& idx, const QString& line);
static std::optional<std::string> prd_AnyWordToUpper(int& idx, const QString& line);
static std::optional<std::string> prd_OptionalChannel(int& idx, const QString& line);
static std::optional<std::string> prd_NotSwitch(int& idx, const QString& line);
static std::optional<std::string> prd_Switch(int& idx, const QString& line);
static std::optional<std::string> prd_Message(int& idx, const QString& line);
std::unique_ptr<ICommandPriv> mp;
};
#endif // ICOMMAND_H

@ -8,8 +8,8 @@
#include "ICommandPriv.h"
ICommandPriv::ICommandPriv(ICommand& super_, IRC& connection_, IWinStatus& status_)
: super(super_)
, connection(connection_)
, status(status_)
: super(super_)
, connection(connection_)
, status(status_)
{}

@ -12,26 +12,26 @@
#include "IWin/IWinStatus.h"
#include "MdiManager.h"
#include "IRCClient/Commands.h"
#include "IRCClient/Utilities.h"
#include "Widgets/IIRCView.h"
#include <string>
constexpr char CTCPflag { 0x01 };
struct ICommandPriv
{
ICommandPriv(ICommand& super_, IRC& connection_, IWinStatus& status_);
ICommandPriv(ICommand& super_, IRC& connection_, IWinStatus& status_);
ICommand& super;
IRC& connection;
IWinStatus& status;
void cmd_kick(const std::string& channel, const std::string& nickname, const std::string& reason);
void cmd_privmsg(const std::string& target, const std::string& message);
void cmd_notice(const std::string& target, const std::string& message);
void cmd_kick(const std::string& channel, const std::string& nickname, const std::string& reason);
void cmd_privmsg(const std::string& target, const std::string& message);
void cmd_notice(const std::string& target, const std::string& message);
void cmd_ctcp(const std::string& target, const std::string& command, std::string message);
void cmd_ctcp(const std::string& target, const std::string& command, std::string message);
void cmd_dcc(const std::string& command, const std::string& target, const std::string& message);
void cmd_ctcpreply(const std::string& target, const std::string& command, const std::string& message);
void cmd_me(const std::string& target, const std::string& message);
void cmd_me(const std::string& target, const std::string& message);
void cmd_echo(const std::string& arg1, const std::string& arg2, const std::string& text);
void cmd_server(bool ssl, bool newstatus, bool activate, const std::string& host, const std::string& port);
};

@ -0,0 +1,52 @@
/*
* IdealIRC - Internet Relay Chat client
* Copyright (c) 2022 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "ICommand/ICommandPriv.h"
#include "IRCClient/DCC.h"
#include <fmt/format.h>
void ICommandPriv::cmd_dcc(const std::string& command, const std::string& target, const std::string& message)
{
auto& mdi = MdiManager::instance();
if (command == "CHAT") {
auto result = connection.initiateDCC();
if (result.second != IRCError::NoError) {
const auto code = static_cast<int>(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)]")
.arg( QString::fromStdString(target), errorStr, QString::number(code)) );
}
else {
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));
if (dcc->isReversed())
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 msg = fmt::format( "CHAT chat {} {}",
normalIpToLong(myIp),
portno
);
cmd_ctcp(target, "DCC", msg);
}
}
else {
mdi.currentStatus()->printToActive(PrintType::ProgramInfo, QObject::tr("/DCC: Unknown DCC method: %1")
.arg( QString::fromStdString(command) ));
}
}

@ -19,19 +19,19 @@ void ICommandPriv::cmd_me(const std::string& target, const std::string& message)
IWin* cw = status.getActiveWindow();
if (cw->getType() == IWin::Type::Channel) {
auto channel = connection.getChannel( cw->getButtonText().toStdString() );
auto& member = channel->getMember(mynick)->get();
const std::string& mymodes = member.modes();
auto channel = connection.getChannel( cw->getButtonText().toStdString() );
auto& member = channel->getMember(mynick)->get();
const std::string& mymodes = member.modes();
if (!mymodes.empty()) {
std::string pf = connection.toMemberPrefix(mymodes);
if (!pf.empty())
mynick.insert(mynick.begin(), pf[0]);
}
}
if (!mymodes.empty()) {
std::string pf = connection.toMemberPrefix(mymodes);
if (!pf.empty())
mynick.insert(mynick.begin(), pf[0]);
}
}
if (!target.empty() && !message.empty())
cw->print(PrintType::Action, QStringLiteral("%1 %2")
.arg(mynick.c_str())
.arg(message.c_str()));
if (!target.empty() && !message.empty())
cw->print(PrintType::Action, QStringLiteral("%1 %2")
.arg(mynick.c_str())
.arg(message.c_str()));
}

@ -0,0 +1,98 @@
/*
* IdealIRC - Internet Relay Chat client
* Copyright (C) 2022 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "AddServer.h"
#include "ui_AddServer.h"
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
AddServer::AddServer(const QStringList& networkList, QWidget *parent) :
QDialog(parent),
ui(new Ui::AddServer)
{
ui->setupUi(this);
setAttribute(Qt::WA_DeleteOnClose);
auto EnableOrDisableSave = [this](const QString&) {
auto isDisabled = ui->edName->text().isEmpty() || ui->edAddress->text().isEmpty();
ui->btnSave->setEnabled(!isDisabled);
};
connect(ui->edName, &QLineEdit::textChanged, EnableOrDisableSave);
connect(ui->edAddress, &QLineEdit::textChanged, EnableOrDisableSave);
for (const auto& networkName : networkList)
ui->edNetwork->addItem(networkName);
}
AddServer::~AddServer()
{
delete ui;
}
QString AddServer::name() const
{
return ui->edName->text();
}
QString AddServer::address() const
{
return ui->edAddress->text();
}
QString AddServer::password() const
{
return ui->edPassword->text();
}
QString AddServer::sasl() const
{
return ui->edSASL->text();
}
bool AddServer::ssl() const
{
return ui->chkSSL->isChecked();
}
bool AddServer::isServer() const
{
return ui->rdServer->isChecked();
}
int AddServer::networkIndex() const
{
// 0th index is the "No network" selection, make that into a "-1" index instead... and the rest must also follow.
return ui->edNetwork->currentIndex() - 1;
}
void AddServer::on_btnCancel_clicked()
{
close();
}
void AddServer::on_btnSave_clicked()
{
emit saved();
close();
}
void AddServer::on_rdNetwork_toggled(bool checked)
{
ui->edNetwork->setEnabled(!checked);
// Network servers (child servers) uses their parent server's SASL.
// The individual child servers will not have own SASL credentials.
ui->edSASL->setEnabled(!checked && ui->edNetwork->currentIndex() == 0 || checked);
}
void AddServer::on_edNetwork_currentIndexChanged(int index)
{
ui->edSASL->setEnabled(index == 0);
}

@ -0,0 +1,46 @@
/*
* IdealIRC - Internet Relay Chat client
* Copyright (C) 2022 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#ifndef ADDSERVER_H
#define ADDSERVER_H
#include <QDialog>
namespace Ui {
class AddServer;
}
class AddServer : public QDialog
{
Q_OBJECT
public:
explicit AddServer(const QStringList& networkList, QWidget *parent = nullptr);
~AddServer();
QString name() const;
QString address() const;
QString password() const;
QString sasl() const;
bool ssl() const;
bool isServer() const;
int networkIndex() const;
signals:
void saved();
private slots:
void on_btnCancel_clicked();
void on_btnSave_clicked();
void on_rdNetwork_toggled(bool checked);
void on_edNetwork_currentIndexChanged(int index);
private:
Ui::AddServer *ui;
};
#endif // ADDSERVER_H

@ -0,0 +1,183 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddServer</class>
<widget class="QDialog" name="AddServer">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>200</height>
</rect>
</property>
<property name="windowTitle">
<string>Add server</string>
</property>
<property name="windowIcon">
<iconset resource="../Resources/resources.qrc">
<normaloff>:/Icons/options.png</normaloff>:/Icons/options.png</iconset>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QRadioButton" name="rdNetwork">
<property name="text">
<string>Network</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="rdServer">
<property name="text">
<string>Server</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="edNetwork">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="placeholderText">
<string/>
</property>
<item>
<property name="text">
<string>&lt;No network&gt;</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Name</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="edName"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Address</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="edAddress"/>
</item>
<item>
<widget class="QCheckBox" name="chkSSL">
<property name="text">
<string>SSL</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Password</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="edPassword">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>SASL credential</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="edSASL">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="btnCancel">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnSave">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Save</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<tabstops>
<tabstop>rdNetwork</tabstop>
<tabstop>rdServer</tabstop>
<tabstop>edNetwork</tabstop>
<tabstop>edName</tabstop>
<tabstop>edAddress</tabstop>
<tabstop>chkSSL</tabstop>
<tabstop>edPassword</tabstop>
<tabstop>edSASL</tabstop>
<tabstop>btnCancel</tabstop>
<tabstop>btnSave</tabstop>
</tabstops>
<resources>
<include location="../Resources/resources.qrc"/>
</resources>
<connections/>
</ui>

@ -15,13 +15,15 @@ list(APPEND ${component}_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/IConfigServers.cpp
${CMAKE_CURRENT_SOURCE_DIR}/IConfigServers.h
${CMAKE_CURRENT_SOURCE_DIR}/IConfigServers.ui
${CMAKE_CURRENT_SOURCE_DIR}/ServerEditor.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ServerEditor.h
${CMAKE_CURRENT_SOURCE_DIR}/ServerEditor.ui
${CMAKE_CURRENT_SOURCE_DIR}/ServerMgr.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ServerMgr.h
${CMAKE_CURRENT_SOURCE_DIR}/AddServer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/AddServer.h
${CMAKE_CURRENT_SOURCE_DIR}/AddServer.ui
${CMAKE_CURRENT_SOURCE_DIR}/ServerItem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ServerItem.h
${CMAKE_CURRENT_SOURCE_DIR}/ServerModel.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ServerModel.h
${CMAKE_CURRENT_SOURCE_DIR}/ServerOptionsDelegate.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ServerOptionsDelegate.h
)
add_library(${component} STATIC ${${component}_SOURCES})

@ -21,360 +21,360 @@ constexpr auto* Text_AddPrefix_Short = "Add";
}
ColorConfig::ColorConfig(QWidget* parent)
: QWidget(parent)
, colorDlg(this)
: QWidget(parent)
, colorDlg(this)
{
addPrefixContainer = new QWidget(this);
addPrefixLayout = new QHBoxLayout;
btnAddPrefix = new QToolButton;
btnAddPrefix->setText(Text_AddPrefix_Full);
edAddPrefix = new QLineEdit;
edAddPrefix->setMaxLength(1);
edAddPrefix->setAlignment(Qt::AlignmentFlag::AlignCenter);
edAddPrefix->hide();
addPrefixLayout->addWidget(edAddPrefix);
addPrefixLayout->addWidget(btnAddPrefix);
addPrefixContainer->setLayout(addPrefixLayout);
connect(btnAddPrefix, &QToolButton::clicked, this, &ColorConfig::addPrefixClicked);
connect(edAddPrefix, &QLineEdit::returnPressed, this, &ColorConfig::addPrefixClicked);
ConfigMgr& conf = ConfigMgr::instance();
palette = conf.color();
prefixColor = conf.prefixColor();
textTypeMap = {
// Config key, Descriptive text
{ "Action", "Action/role-play message" },
{ "CTCP", "CTCP message" },
{ "Highlight", "Highlighted message" },
{ "Invite", "Invite message" },
{ "Join", "Join message" },
{ "Kick", "Kick message" },
{ "Mode", "Mode message" },
{ "Nick", "Nick message" },
{ "Normal", "Normal message" },
{ "Notice", "Notice message" },
{ "OwnText", "Own message" },
{ "Part", "Part message" },
{ "ProgramInfo", "Program info" },
{ "Quit", "Quit message" },
{ "ServerInfo", "General server info" },
{ "Topic", "Topic message" },
{ "Wallops", "Wallops message" },
{ "Links", "Links / anchors" }
};
colorTypeMap = {
{ "TextviewBackground", "Text view background" },
{ "InputBackground", "Text input background" },
{ "InputForeground", "Text input" },
{ "ListboxBackground", "Member list background" },
{ "ListboxForeground", "Member list default" }
};
connect(&colorDlg, &QColorDialog::currentColorChanged,
this, &ColorConfig::colorSelected);
connect(&colorDlg, &QColorDialog::rejected, [this](){
if (colorDlgItem.left(8) == "lbprefix")
setPrefixColor(colorDlgItem[9], originalColor);
else
palette.insert(colorDlgItem, originalColor);
repaint();
});
addPrefixContainer = new QWidget(this);
addPrefixLayout = new QHBoxLayout;
btnAddPrefix = new QToolButton;
btnAddPrefix->setText(Text_AddPrefix_Full);
edAddPrefix = new QLineEdit;
edAddPrefix->setMaxLength(1);
edAddPrefix->setAlignment(Qt::AlignmentFlag::AlignCenter);
edAddPrefix->hide();
addPrefixLayout->addWidget(edAddPrefix);
addPrefixLayout->addWidget(btnAddPrefix);
addPrefixContainer->setLayout(addPrefixLayout);
connect(btnAddPrefix, &QToolButton::clicked, this, &ColorConfig::addPrefixClicked);
connect(edAddPrefix, &QLineEdit::returnPressed, this, &ColorConfig::addPrefixClicked);
ConfigMgr& conf = ConfigMgr::instance();
palette = conf.color();
prefixColor = conf.prefixColor();
textTypeMap = {
// Config key, Descriptive text
{ "Action", "Action/role-play message" },
{ "CTCP", "CTCP message" },
{ "Highlight", "Highlighted message" },
{ "Invite", "Invite message" },
{ "Join", "Join message" },
{ "Kick", "Kick message" },
{ "Mode", "Mode message" },
{ "Nick", "Nick message" },
{ "Normal", "Normal message" },
{ "Notice", "Notice message" },
{ "OwnText", "Own message" },
{ "Part", "Part message" },
{ "ProgramInfo", "Program info" },
{ "Quit", "Quit message" },
{ "ServerInfo", "General server info" },
{ "Topic", "Topic message" },
{ "Wallops", "Wallops message" },
{ "Links", "Links / anchors" }
};
colorTypeMap = {
{ "TextviewBackground", "Text view background" },
{ "InputBackground", "Text input background" },
{ "InputForeground", "Text input" },
{ "ListboxBackground", "Member list background" },
{ "ListboxForeground", "Member list default" }
};
connect(&colorDlg, &QColorDialog::currentColorChanged,
this, &ColorConfig::colorSelected);
connect(&colorDlg, &QColorDialog::rejected, [this](){
if (colorDlgItem.left(8) == "lbprefix")
setPrefixColor(colorDlgItem[9], originalColor);
else
palette.insert(colorDlgItem, originalColor);
repaint();
});
}
void ColorConfig::save()
{
ConfigMgr& conf = ConfigMgr::instance();
conf.setColorPalette(palette);
conf.setPrefixColorPalette(prefixColor);
m_isChanged = false;
ConfigMgr& conf = ConfigMgr::instance();
conf.setColorPalette(palette);
conf.setPrefixColorPalette(prefixColor);
m_isChanged = false;
}
bool ColorConfig::isChanged() const
{
return m_isChanged;
return m_isChanged;
}
void ColorConfig::reset()
{
ConfigMgr& conf = ConfigMgr::instance();
palette = conf.color();
prefixColor = conf.prefixColor();
m_isChanged = false;
repaint();
ConfigMgr& conf = ConfigMgr::instance();
palette = conf.color();
prefixColor = conf.prefixColor();
m_isChanged = false;
repaint();
}
void ColorConfig::paintEvent(QPaintEvent*)
{
QPainter paint(this);
int w = width();
int h = height();
QPainter paint(this);
int w = width();
int h = height();
QColor frame(palette.value("TextviewBackground"));
frame.setRed(frame.red() ^ 255);
frame.setGreen(frame.green() ^ 255);
frame.setBlue(frame.blue() ^ 255);
QColor frame(palette.value("TextviewBackground"));
frame.setRed(frame.red() ^ 255);
frame.setGreen(frame.green() ^ 255);
frame.setBlue(frame.blue() ^ 255);
paint.fillRect(0, 0, w, h, frame);
w -= 2;
h -= 2;
paint.fillRect(0, 0, w, h, frame);
w -= 2;
h -= 2;
const int listboxW = 150;
const int inputH = 24;
const int listboxW = 150;
const int inputH = 24;
/* Text-view background */
textViewBB = { 1, 1, w - listboxW, h - inputH };
paint.fillRect(textViewBB, QColor(palette.value("TextviewBackground")));
/* Text-view background */
textViewBB = { 1, 1, w - listboxW, h - inputH };
paint.fillRect(textViewBB, QColor(palette.value("TextviewBackground")));
/* Member list background */
listboxBB = { w - listboxW + 2, 1, listboxW - 1, h - inputH };
paint.fillRect(listboxBB, QColor(palette.value("ListboxBackground"))); // Listbox
/* Member list background */
listboxBB = { w - listboxW + 2, 1, listboxW - 1, h - inputH };
paint.fillRect(listboxBB, QColor(palette.value("ListboxBackground"))); // Listbox
/* Input background */
inputBB = { 1, h - inputH + 2, w, inputH - 1 };
paint.fillRect(inputBB, QColor(palette.value("InputBackground"))); // Input
/* Input background */
inputBB = { 1, h - inputH + 2, w, inputH - 1 };
paint.fillRect(inputBB, QColor(palette.value("InputBackground"))); // Input
ConfigMgr& conf = ConfigMgr::instance();
QFont font(conf.common("Font"));
paint.setFont(font);
ConfigMgr& conf = ConfigMgr::instance();
QFont font(conf.common("Font"));
paint.setFont(font);
createMessageBoxTexts(paint);
createMemberListTexts(paint, listboxW);
createInputBoxTexts(paint, inputH);
createMessageBoxTexts(paint);
createMemberListTexts(paint, listboxW);
createInputBoxTexts(paint, inputH);
}
void ColorConfig::mouseReleaseEvent(QMouseEvent* evt)
{
const int X = evt->x();
const int Y = evt->y();
/* Check click for text item types */
{
QHashIterator<QString,QRect> it(textTypeBB);
while (it.hasNext()) {
it.next();
const QString& key = it.key();
const QRect& val = it.value();
if (val.contains(X, Y)) {
chooseColorFor(key);
return;
}
}
}
/* Check click for listbox items */
{
QHashIterator<QString,QRect> it(listboxItemBB);
while (it.hasNext()) {
it.next();
const QString& key = it.key();
const QRect& val = it.value();
if (val.contains(X, Y)) {
chooseColorFor(key);
return;
}
}
}
/* Check click for delete listbox item */
{
QHashIterator<QChar,QRect> it(prefixDeleteBB);
while (it.hasNext()) {
it.next();
const QChar& key = it.key();
const QRect& val = it.value();
if (val.contains(X, Y)) {
askDeletePrefix(key);
return;
}
}
}
if (inputTextBB.contains(X, Y))
chooseColorFor("InputForeground");
else if (textViewBB.contains(X, Y))
chooseColorFor("TextviewBackground");
else if (listboxBB.contains(X, Y))
chooseColorFor("ListboxBackground");
else if (inputBB.contains(X, Y))
chooseColorFor("InputBackground");
const int X = evt->x();
const int Y = evt->y();
/* Check click for text item types */
{
QHashIterator<QString,QRect> it(textTypeBB);
while (it.hasNext()) {
it.next();
const QString& key = it.key();
const QRect& val = it.value();
if (val.contains(X, Y)) {
chooseColorFor(key);
return;
}
}
}
/* Check click for listbox items */
{
QHashIterator<QString,QRect> it(listboxItemBB);
while (it.hasNext()) {
it.next();
const QString& key = it.key();
const QRect& val = it.value();
if (val.contains(X, Y)) {
chooseColorFor(key);
return;
}
}
}
/* Check click for delete listbox item */
{
QHashIterator<QChar,QRect> it(prefixDeleteBB);
while (it.hasNext()) {
it.next();
const QChar& key = it.key();
const QRect& val = it.value();
if (val.contains(X, Y)) {
askDeletePrefix(key);
return;
}
}
}
if (inputTextBB.contains(X, Y))
chooseColorFor("InputForeground");
else if (textViewBB.contains(X, Y))
chooseColorFor("TextviewBackground");
else if (listboxBB.contains(X, Y))
chooseColorFor("ListboxBackground");
else if (inputBB.contains(X, Y))
chooseColorFor("InputBackground");
}
void ColorConfig::createMessageBoxTexts(QPainter& paint)
{
const int itemIncr = 19;
int itemTop = itemIncr;
QPen originalPen = paint.pen();
QFontMetrics fm(paint.fontMetrics());
const int itemIncr = 19;
int itemTop = itemIncr;
QPen originalPen = paint.pen();
QFontMetrics fm(paint.fontMetrics());
textTypeBB.clear();
QHashIterator<QString,QString> it(textTypeMap);
QStringList sorted;
textTypeBB.clear();
QHashIterator<QString,QString> it(textTypeMap);
QStringList sorted;
while (it.hasNext())
sorted << it.next().key();
while (it.hasNext())
sorted << it.next().key();
std::sort(sorted.begin(), sorted.end(),
[this](const QString& left, const QString& right) {
QString leftText = descriptiveColorText(left);
QString rightText = descriptiveColorText(right);
return leftText < rightText;
});
std::sort(sorted.begin(), sorted.end(),
[this](const QString& left, const QString& right) {
QString leftText = descriptiveColorText(left);
QString rightText = descriptiveColorText(right);
return leftText < rightText;
});
for (const QString& key : sorted) {
const QString val = descriptiveColorText(key);
for (const QString& key : sorted) {
const QString val = descriptiveColorText(key);
paint.setPen(QColor(palette.value(key)));
paint.drawText(4, itemTop, val);
paint.setPen(QColor(palette.value(key)));
paint.drawText(4, itemTop, val);
QRect itemRect(4, itemTop - fm.ascent(), fm.width(val), fm.height());
textTypeBB.insert(key, itemRect);
QRect itemRect(4, itemTop - fm.ascent(), fm.width(val), fm.height());
textTypeBB.insert(key, itemRect);
itemTop += itemIncr;
}
itemTop += itemIncr;
}
paint.setPen(originalPen);
paint.setPen(originalPen);
}
void ColorConfig::createMemberListTexts(QPainter& paint, int listboxW)
{
const int x = width() - listboxW + 4;
const int itemIncr = 20;
int itemTop = itemIncr;
QPen originalPen = paint.pen();
QFontMetrics fm(paint.fontMetrics());
listboxItemBB.clear();
prefixDeleteBB.clear();
paint.setPen(QColor(palette.value("ListboxForeground")));
paint.drawText(x, itemTop, "Default text");
listboxItemBB.insert("ListboxForeground", QRect(x, itemTop - fm.ascent(), fm.width("Default text"), fm.height()));
itemTop += itemIncr;
constexpr auto* DeleteLabel = "[X]";
for (const auto& item : prefixColor) {
QString itemText = QStringLiteral("%1member").arg(item.first);
int itemTextWidth = fm.width(itemText);
paint.setPen(item.second);
paint.drawText(x, itemTop, itemText);
paint.drawText(x + itemTextWidth + 10, itemTop, DeleteLabel);
listboxItemBB.insert(QStringLiteral("lbprefix %1").arg(item.first), QRect(x, itemTop - fm.ascent(), itemTextWidth, fm.height()));
prefixDeleteBB.insert(item.first, QRect(x + itemTextWidth + 10, itemTop - fm.ascent(), fm.width(DeleteLabel), fm.height()));
itemTop += itemIncr;
}
paint.setPen(originalPen);
addPrefixContainer->move(x, itemTop);
const int x = width() - listboxW + 4;
const int itemIncr = 20;
int itemTop = itemIncr;
QPen originalPen = paint.pen();
QFontMetrics fm(paint.fontMetrics());
listboxItemBB.clear();
prefixDeleteBB.clear();
paint.setPen(QColor(palette.value("ListboxForeground")));
paint.drawText(x, itemTop, "Default text");
listboxItemBB.insert("ListboxForeground", QRect(x, itemTop - fm.ascent(), fm.width("Default text"), fm.height()));
itemTop += itemIncr;
constexpr auto* DeleteLabel = "[X]";
for (const auto& item : prefixColor) {
QString itemText = QStringLiteral("%1member").arg(item.first);
int itemTextWidth = fm.width(itemText);
paint.setPen(item.second);
paint.drawText(x, itemTop, itemText);
paint.drawText(x + itemTextWidth + 10, itemTop, DeleteLabel);
listboxItemBB.insert(QStringLiteral("lbprefix %1").arg(item.first), QRect(x, itemTop - fm.ascent(), itemTextWidth, fm.height()));
prefixDeleteBB.insert(item.first, QRect(x + itemTextWidth + 10, itemTop - fm.ascent(), fm.width(DeleteLabel), fm.height()));
itemTop += itemIncr;
}
paint.setPen(originalPen);
addPrefixContainer->move(x, itemTop);
}
void ColorConfig::createInputBoxTexts(QPainter& paint, int inputH)
{
QFontMetrics fm(paint.fontMetrics());
const int fh = fm.height();
const int y = height() - inputH + fh;
QPen originalPen = paint.pen();
QFontMetrics fm(paint.fontMetrics());
const int fh = fm.height();
const int y = height() - inputH + fh;
QPen originalPen = paint.pen();
paint.setPen(QColor(palette.value("InputForeground")));
paint.drawText(4, y, "Input box text");
inputTextBB = {4, y - fm.ascent(), fm.width("Input box text"), fm.height()};
paint.setPen(QColor(palette.value("InputForeground")));
paint.drawText(4, y, "Input box text");
inputTextBB = {4, y - fm.ascent(), fm.width("Input box text"), fm.height()};
paint.setPen(originalPen);
paint.setPen(originalPen);
}
QString ColorConfig::descriptiveColorText(const QString& key) const
{
if (key.left(8) == "lbprefix")
return QStringLiteral("Prefix '%1'").arg(key[9]);
if (key.left(8) == "lbprefix")
return QStringLiteral("Prefix '%1'").arg(key[9]);
QString text = textTypeMap.value(key);
if (text.isEmpty())
text = colorTypeMap.value(key);
return text;
QString text = textTypeMap.value(key);
if (text.isEmpty())
text = colorTypeMap.value(key);
return text;
}
void ColorConfig::chooseColorFor(const QString& item)
{
colorDlgItem = item;
if (colorDlgItem.left(8) == "lbprefix")
colorDlg.setCurrentColor(*getPrefixColor(colorDlgItem[9]));
else
colorDlg.setCurrentColor(QColor(palette.value(item)));
originalColor = colorDlg.currentColor().name();
colorDlg.setWindowTitle(QStringLiteral("Choose color for: %1").arg(descriptiveColorText(item)));
if (!colorDlg.isVisible())
colorDlg.show();
colorDlgItem = item;
if (colorDlgItem.left(8) == "lbprefix")
colorDlg.setCurrentColor(*getPrefixColor(colorDlgItem[9]));
else
colorDlg.setCurrentColor(QColor(palette.value(item)));
originalColor = colorDlg.currentColor().name();
colorDlg.setWindowTitle(QStringLiteral("Choose color for: %1").arg(descriptiveColorText(item)));
if (!colorDlg.isVisible())
colorDlg.show();
}
void ColorConfig::colorSelected(const QColor& color)
{
if (colorDlgItem.left(8) == "lbprefix")
setPrefixColor(colorDlgItem[9], color);
else
palette.insert(colorDlgItem, color.name());
m_isChanged = true;
repaint();
if (colorDlgItem.left(8) == "lbprefix")
setPrefixColor(colorDlgItem[9], color);
else
palette.insert(colorDlgItem, color.name());
m_isChanged = true;
repaint();
}
std::optional<QColor> ColorConfig::getPrefixColor(QChar prefix)
{
for (const auto& item : prefixColor)
if (item.first == prefix)
return item.second;
return std::nullopt;
for (const auto& item : prefixColor)
if (item.first == prefix)
return item.second;
return std::nullopt;
}
void ColorConfig::setPrefixColor(QChar prefix, const QColor& color)
{
for (auto& item : prefixColor)
if (item.first == prefix)
item.second = color;
for (auto& item : prefixColor)
if (item.first == prefix)
item.second = color;
}
void ColorConfig::addPrefixClicked()
{
if (edAddPrefix->isVisible()) {
if (!edAddPrefix->text().isEmpty()) {
const QChar prefix = edAddPrefix->text()[0];
if (getPrefixColor(prefix)) {
QMessageBox::information(this, tr("Prefix exists"), tr("The given prefix '%1' is already defined.").arg(prefix));
return;
}
if (!prefix.isSpace() && !prefix.isLetter() && !prefix.isNumber()) {
prefixColor.push_back(std::make_pair(prefix, palette.value("ListboxForeground")));
m_isChanged = true;
repaint();
}
}
edAddPrefix->clear();
edAddPrefix->hide();
btnAddPrefix->setText(Text_AddPrefix_Full);
}
else {
edAddPrefix->show();
btnAddPrefix->setText(Text_AddPrefix_Short);
}
if (edAddPrefix->isVisible()) {
if (!edAddPrefix->text().isEmpty()) {
const QChar prefix = edAddPrefix->text()[0];
if (getPrefixColor(prefix)) {
QMessageBox::information(this, tr("Prefix exists"), tr("The given prefix '%1' is already defined.").arg(prefix));
return;
}
if (!prefix.isSpace() && !prefix.isLetter() && !prefix.isNumber()) {
prefixColor.push_back(std::make_pair(prefix, palette.value("ListboxForeground")));
m_isChanged = true;
repaint();
}
}
edAddPrefix->clear();
edAddPrefix->hide();
btnAddPrefix->setText(Text_AddPrefix_Full);
}
else {
edAddPrefix->show();
btnAddPrefix->setText(Text_AddPrefix_Short);
}
}
void ColorConfig::askDeletePrefix(QChar prefix)
{
const QString title = tr("Delete prefix");
const QString question = tr("Are you sure you want to delete the prefix '%1'?").arg(prefix);
if (QMessageBox::question(this, title, question) == QMessageBox::No)
return;
for (int i = 0; i < prefixColor.count(); ++i) {
QChar p = prefixColor[i].first;
if (p == prefix) {
prefixColor.removeAt(i);
break;
}
}
repaint();
const QString title = tr("Delete prefix");
const QString question = tr("Are you sure you want to delete the prefix '%1'?").arg(prefix);
if (QMessageBox::question(this, title, question) == QMessageBox::No)
return;
for (int i = 0; i < prefixColor.count(); ++i) {
QChar p = prefixColor[i].first;
if (p == prefix) {
prefixColor.removeAt(i);
break;
}
}
repaint();
}

@ -21,51 +21,51 @@
class ColorConfig : public QWidget
{
Q_OBJECT
Q_OBJECT
public:
explicit ColorConfig(QWidget* parent = nullptr);
void save();
bool isChanged() const;
void reset();
explicit ColorConfig(QWidget* parent = nullptr);
void save();
bool isChanged() const;
void reset();
private:
void paintEvent(QPaintEvent*);
void mouseReleaseEvent(QMouseEvent *evt);
void createMessageBoxTexts(QPainter& paint);
void createMemberListTexts(QPainter& paint, int listboxW);
void createInputBoxTexts(QPainter& paint, int inputH);
void recalculateBB();
QString descriptiveColorText(const QString& key) const;
void chooseColorFor(const QString& item);
void colorSelected(const QColor& color);
std::optional<QColor> getPrefixColor(QChar prefix);
void setPrefixColor(QChar prefix, const QColor& color);
void addPrefixClicked();
void askDeletePrefix(QChar prefix);
void paintEvent(QPaintEvent*);
void mouseReleaseEvent(QMouseEvent *evt);
void createMessageBoxTexts(QPainter& paint);
void createMemberListTexts(QPainter& paint, int listboxW);
void createInputBoxTexts(QPainter& paint, int inputH);
void recalculateBB();
QString descriptiveColorText(const QString& key) const;
void chooseColorFor(const QString& item);
void colorSelected(const QColor& color);
std::optional<QColor> getPrefixColor(QChar prefix);
void setPrefixColor(QChar prefix, const QColor& color);
void addPrefixClicked();
void askDeletePrefix(QChar prefix);
QWidget* addPrefixContainer;
QHBoxLayout* addPrefixLayout;
QToolButton* btnAddPrefix;
QLineEdit* edAddPrefix;
QHash<QString,QString> palette;
QWidget* addPrefixContainer;
QHBoxLayout* addPrefixLayout;
QToolButton* btnAddPrefix;
QLineEdit* edAddPrefix;
QHash<QString,QString> palette;
QHash<QString,QString> textTypeMap;
QHash<QString,QString> colorTypeMap;
QVector<std::pair<QChar,QColor>> prefixColor; // Use vector for displaying prefixes in insertion order.
QHash<QString,QString> textTypeMap;
QHash<QString,QString> colorTypeMap;
QVector<std::pair<QChar,QColor>> prefixColor; // Use vector for displaying prefixes in insertion order.
QHash<QString,QRect> textTypeBB;
QHash<QString,QRect> listboxItemBB;
QHash<QChar,QRect> prefixDeleteBB;
QRect textViewBB;
QRect inputBB;
QRect inputTextBB;
QRect listboxBB;
QHash<QString,QRect> textTypeBB;
QHash<QString,QRect> listboxItemBB;
QHash<QChar,QRect> prefixDeleteBB;
QRect textViewBB;
QRect inputBB;
QRect inputTextBB;
QRect listboxBB;
QColorDialog colorDlg;
QString colorDlgItem;
QString originalColor;
QColorDialog colorDlg;
QString colorDlgItem;
QString originalColor;
bool m_isChanged{ false };
bool m_isChanged{ false };
};
#endif // COLORCONFIG_H

@ -11,75 +11,75 @@
#include <QMessageBox>
IConfig::IConfig(QWidget *parent) :
QDialog(parent),
ui(new Ui::IConfig)
QDialog(parent),
ui(new Ui::IConfig)
{
ui->setupUi(this);
ui->setupUi(this);
layout = new QHBoxLayout(this);
servers = new IConfigServers;
options = new IConfigOptions;
logging = new IConfigLogging;
layout = new QHBoxLayout(this);
servers = new IConfigServers;
options = new IConfigOptions;
logging = new IConfigLogging;
layout->addWidget(servers);
layout->addWidget(options);
layout->addWidget(logging);
ui->frame->setLayout(layout);
layout->addWidget(servers);
layout->addWidget(options);
layout->addWidget(logging);
ui->frame->setLayout(layout);
connect(ui->toolServers, &QAbstractButton::released, [this](){ showSubDialog(SubDialogType::Servers); });
connect(ui->toolOptions, &QAbstractButton::released, [this](){ showSubDialog(SubDialogType::Options); });
connect(ui->toolLogging, &QAbstractButton::released, [this](){ showSubDialog(SubDialogType::Logging); });
connect(ui->toolServers, &QAbstractButton::released, [this](){ showSubDialog(SubDialogType::Servers); });
connect(ui->toolOptions, &QAbstractButton::released, [this](){ showSubDialog(SubDialogType::Options); });
connect(ui->toolLogging, &QAbstractButton::released, [this](){ showSubDialog(SubDialogType::Logging); });
showSubDialog(SubDialogType::Servers);
showSubDialog(SubDialogType::Servers);
}
IConfig::~IConfig()
{
delete ui;
delete ui;
}
void IConfig::showDisconnectButton()
{
ui->btnDisconnect->show();
ui->btnDisconnect->show();
}
void IConfig::hideDisconnectButton()
{
ui->btnDisconnect->hide();
ui->btnDisconnect->hide();
}
void IConfig::showSubDialog(IConfig::SubDialogType dlg)
{
servers->hide();
options->hide();
logging->hide();
ui->toolServers->setChecked(false);
ui->toolOptions->setChecked(false);
ui->toolLogging->setChecked(false);
switch (dlg) {
case SubDialogType::Servers:
servers->show();
ui->toolServers->setChecked(true);
break;
case SubDialogType::Options:
options->show();
ui->toolOptions->setChecked(true);
break;
case SubDialogType::Logging:
logging->show();
ui->toolLogging->setChecked(true);
break;
}
servers->hide();
options->hide();
logging->hide();
ui->toolServers->setChecked(false);
ui->toolOptions->setChecked(false);
ui->toolLogging->setChecked(false);
switch (dlg) {
case SubDialogType::Servers:
servers->show();
ui->toolServers->setChecked(true);
break;
case SubDialogType::Options:
options->show();
ui->toolOptions->setChecked(true);
break;
case SubDialogType::Logging:
logging->show();
ui->toolLogging->setChecked(true);
break;
}
}
void IConfig::saveAll()
{
servers->save();
options->save();
logging->save();
servers->save();
options->save();
logging->save();
ConfigMgr::instance().save();
ConfigMgr::instance().save();
}
bool IConfig::askForSave()
@ -87,70 +87,70 @@ bool IConfig::askForSave()
bool serversChanged = servers->isChanged();
bool optionsChanged = options->isChanged();
bool loggingChanged = logging->isChanged();
if (serversChanged || optionsChanged || loggingChanged) {
auto btn = QMessageBox::question(this, tr("Changes made"), tr("Do you want to save the changes?"), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
if (btn == QMessageBox::Yes) {
saveAll();
}
else if (btn == QMessageBox::No) {
servers->reset();
options->reset();
logging->reset();
}
else if (btn == QMessageBox::Cancel) {
return false;
}
}
return true;
if (serversChanged || optionsChanged || loggingChanged) {
auto btn = QMessageBox::question(this, tr("Changes made"), tr("Do you want to save the changes?"), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
if (btn == QMessageBox::Yes) {
saveAll();
}
else if (btn == QMessageBox::No) {
servers->reset();
options->reset();
logging->reset();
}
else if (btn == QMessageBox::Cancel) {
return false;
}
}
return true;
}
void IConfig::on_btnSave_clicked()
{
saveAll();
saveAll();
}
void IConfig::on_btnSaveConnect_clicked()
{
saveAll();
ConfigMgr& conf = ConfigMgr::instance();
if (conf.connection("Realname").isEmpty()) {
QMessageBox::information(this, tr("Missing field"), tr("Real name must be filled."));
return;
}
if (conf.connection("Username").isEmpty()) {
QMessageBox::information(this, tr("Missing field"), tr("Username/email must be filled."));
return;
}
if (conf.connection("Nickname").isEmpty()) {
QMessageBox::information(this, tr("Missing field"), tr("Nickname must be filled"));
return;
}
if (!servers->connectToNewStatus())
emit disconnectFromServer();
emit connectToServer(servers->connectToNewStatus());
servers->unsetConnectToNewStatus();
close();
saveAll();
ConfigMgr& conf = ConfigMgr::instance();
if (conf.connection("Realname").isEmpty()) {
QMessageBox::information(this, tr("Missing field"), tr("Real name must be filled."));
return;
}
if (conf.connection("Username").isEmpty()) {
QMessageBox::information(this, tr("Missing field"), tr("Username/email must be filled."));
return;
}
if (conf.connection("Nickname").isEmpty()) {
QMessageBox::information(this, tr("Missing field"), tr("Nickname must be filled"));
return;
}
if (!servers->connectToNewStatus())
emit disconnectFromServer();
emit connectToServer(servers->connectToNewStatus());
servers->unsetConnectToNewStatus();
close();
}
void IConfig::on_btnDisconnect_clicked()
{
ui->btnDisconnect->hide();
emit disconnectFromServer();
ui->btnDisconnect->hide();
emit disconnectFromServer();
}
void IConfig::on_btnClose_clicked()
{
servers->unsetConnectToNewStatus();
close();
servers->unsetConnectToNewStatus();
close();
}
void IConfig::closeEvent(QCloseEvent* evt)
{
if (askForSave())
evt->accept();
else
evt->ignore();
if (askForSave())
evt->accept();
else
evt->ignore();
}

@ -22,40 +22,40 @@ class IConfig;
class IConfig : public QDialog
{
Q_OBJECT
Q_OBJECT
public:
explicit IConfig(QWidget *parent = nullptr);
~IConfig();
void showDisconnectButton();
void hideDisconnectButton();
explicit IConfig(QWidget *parent = nullptr);
~IConfig();
void showDisconnectButton();
void hideDisconnectButton();
private slots:
void on_btnSave_clicked();
void on_btnSaveConnect_clicked();
void on_btnDisconnect_clicked();
void on_btnClose_clicked();
void on_btnSave_clicked();
void on_btnSaveConnect_clicked();
void on_btnDisconnect_clicked();
void on_btnClose_clicked();
private:
enum class SubDialogType {
Servers,
Options,
Logging
};
void closeEvent(QCloseEvent* evt);
void showSubDialog(SubDialogType dlg);
void saveAll();
bool askForSave();
Ui::IConfig* ui;
QHBoxLayout *layout;
IConfigServers* servers;
IConfigOptions* options;
IConfigLogging* logging;
enum class SubDialogType {
Servers,
Options,
Logging
};
void closeEvent(QCloseEvent* evt);
void showSubDialog(SubDialogType dlg);
void saveAll();
bool askForSave();
Ui::IConfig* ui;
QHBoxLayout *layout;
IConfigServers* servers;
IConfigOptions* options;
IConfigLogging* logging;
signals:
void connectToServer(bool newServer);
void disconnectFromServer();
void connectToServer(bool newServer);
void disconnectFromServer();
};
#endif // ICONFIG_H

@ -13,95 +13,95 @@
#include <QDebug>
IConfigLogging::IConfigLogging(QWidget *parent) :
QWidget(parent),
ui(new Ui::IConfigLogging)
QWidget(parent),
ui(new Ui::IConfigLogging)
{
ui->setupUi(this);
ui->splitter->setStretchFactor(0, 1);
ui->splitter->setStretchFactor(1, 4);
ui->setupUi(this);
ui->splitter->setStretchFactor(0, 1);
ui->splitter->setStretchFactor(1, 4);
ConfigMgr& conf = ConfigMgr::instance();
cf_Chnanels = conf.logging("Channels").toInt();
cf_Privates = conf.logging("Privates").toInt();
cf_Path = conf.logging("Path");
ConfigMgr& conf = ConfigMgr::instance();
cf_Chnanels = conf.logging("Channels").toInt();
cf_Privates = conf.logging("Privates").toInt();
cf_Path = conf.logging("Path");
ui->chkChannels->setChecked(cf_Chnanels);
ui->chkPrivates->setChecked(cf_Privates);
ui->edPath->setText(cf_Path);
ui->chkChannels->setChecked(cf_Chnanels);
ui->chkPrivates->setChecked(cf_Privates);
ui->edPath->setText(cf_Path);
}
IConfigLogging::~IConfigLogging()
{
delete ui;
delete ui;
}
bool IConfigLogging::isChanged() const
{
return cf_Chnanels != ui->chkChannels->isChecked()
|| cf_Privates != ui->chkPrivates->isChecked()
|| cf_Path != ui->edPath->text();
return cf_Chnanels != ui->chkChannels->isChecked()
|| cf_Privates != ui->chkPrivates->isChecked()
|| cf_Path != ui->edPath->text();
}
void IConfigLogging::save()
{
ConfigMgr& conf = ConfigMgr::instance();
conf.setLogging("Channels", QString::number(ui->chkChannels->isChecked()));
conf.setLogging("Privates", QString::number(ui->chkPrivates->isChecked()));
conf.setLogging("Path", ui->edPath->text());
ConfigMgr& conf = ConfigMgr::instance();
conf.setLogging("Channels", QString::number(ui->chkChannels->isChecked()));
conf.setLogging("Privates", QString::number(ui->chkPrivates->isChecked()));
conf.setLogging("Path", ui->edPath->text());
cf_Chnanels = conf.logging("Channels").toInt();
cf_Privates = conf.logging("Privates").toInt();
cf_Path = conf.logging("Path");
cf_Chnanels = conf.logging("Channels").toInt();
cf_Privates = conf.logging("Privates").toInt();
cf_Path = conf.logging("Path");
}
void IConfigLogging::reset()
{
ui->chkChannels->setChecked(cf_Chnanels);
ui->chkPrivates->setChecked(cf_Privates);
ui->edPath->setText(cf_Path);
ui->chkChannels->setChecked(cf_Chnanels);
ui->chkPrivates->setChecked(cf_Privates);
ui->edPath->setText(cf_Path);
}
void IConfigLogging::showEvent(QShowEvent* evt)
{
QWidget::showEvent(evt);
if (!ui->edPath->text().isEmpty())
loadFiles(ui->edPath->text());
QWidget::showEvent(evt);
if (!ui->edPath->text().isEmpty())
loadFiles(ui->edPath->text());
}
void IConfigLogging::on_btnBrowse_clicked()
{
QString dirstr = QFileDialog::getExistingDirectory(this, tr("Log directory"), LOCAL_PATH);
loadFiles(dirstr);
QString dirstr = QFileDialog::getExistingDirectory(this, tr("Log directory"), LOCAL_PATH);
loadFiles(dirstr);
}
void IConfigLogging::loadFiles(const QString& path)
{
ui->fileList->clear();
ui->edPath->setText(path);
if (path.isEmpty())
return;
QDir dir(path);
QStringList files = dir.entryList(QDir::Files, QDir::Name);
ui->fileList->addItems(files);
ui->fileList->clear();
ui->edPath->setText(path);
if (path.isEmpty())
return;
QDir dir(path);
QStringList files = dir.entryList(QDir::Files, QDir::Name);
ui->fileList->addItems(files);
}
void IConfigLogging::on_edPath_textChanged(const QString &arg1)
{
loadFiles(ui->edPath->text());
loadFiles(ui->edPath->text());
}
void IConfigLogging::on_fileList_itemClicked(QListWidgetItem *item)
{
if (!item)
return;
if (!item)
return;
QString filePath = ui->edPath->text() + "/" + item->text();
QFile f(filePath);
if (!f.open(QIODevice::ReadOnly)) {
qWarning() << "Unable to open log file" << filePath;
return;
}
QByteArray data = f.readAll();
f.close();
ui->logView->setPlainText(data);
QString filePath = ui->edPath->text() + "/" + item->text();
QFile f(filePath);
if (!f.open(QIODevice::ReadOnly)) {
qWarning() << "Unable to open log file" << filePath;
return;
}
QByteArray data = f.readAll();
f.close();
ui->logView->setPlainText(data);
}

@ -18,27 +18,27 @@ class IConfigLogging;
class IConfigLogging : public QWidget
{
Q_OBJECT
Q_OBJECT
public:
explicit IConfigLogging(QWidget *parent = nullptr);
~IConfigLogging();
bool isChanged() const;
void save();
void reset();
explicit IConfigLogging(QWidget *parent = nullptr);
~IConfigLogging();
bool isChanged() const;
void save();
void reset();
private slots:
void showEvent(QShowEvent* evt);
void on_btnBrowse_clicked();
void on_edPath_textChanged(const QString &arg1);
void on_fileList_itemClicked(QListWidgetItem *item);
void showEvent(QShowEvent* evt);
void on_btnBrowse_clicked();
void on_edPath_textChanged(const QString &arg1);
void on_fileList_itemClicked(QListWidgetItem *item);
private:
void loadFiles(const QString& path);
Ui::IConfigLogging *ui;
bool cf_Chnanels;
bool cf_Privates;
QString cf_Path;
void loadFiles(const QString& path);
Ui::IConfigLogging *ui;
bool cf_Chnanels;
bool cf_Privates;
QString cf_Path;
};
#endif // ICONFIGLOGGING_H

@ -13,46 +13,50 @@
#include <QFileDialog>
IConfigOptions::IConfigOptions(QWidget *parent) :
QWidget(parent),
ui(new Ui::IConfigOptions)
QWidget(parent),
ui(new Ui::IConfigOptions)
{
ui->setupUi(this);
layout = new QVBoxLayout(ui->tabColors);
colorCfg = new ColorConfig;
{
QLabel* bgcolorhintlabel = new QLabel("Click on a text type to change its color.\nClick anywhere on the backgrounds to change background color.");
QFont labelFont = font();
bgcolorhintlabel->setAlignment(Qt::AlignCenter);
labelFont.setPointSize(8);
bgcolorhintlabel->setFont(labelFont);
layout->addWidget(bgcolorhintlabel);
}
layout->addWidget(colorCfg);
colorCfg->setSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding);
ui->tabColors->setLayout(layout);
/* Background opacity is not yet determined */
ui->label_4->hide();
ui->hsImageOpacity->hide();
/* Background scaling is not yet determined */
ui->label_5->hide();
ui->edImageScaling->hide();
reload();
reset();
cf_Font = ui->edFont->currentFont();
ui->setupUi(this);
// TODO remove this line whenever background images can be dealt with...
ui->tabWidget->setTabVisible(3, false);
layout = new QVBoxLayout(ui->tabColors);
colorCfg = new ColorConfig;
{
QLabel* bgcolorhintlabel = new QLabel("Click on a text type to change its color.\nClick anywhere on the backgrounds to change background color.");
QFont labelFont = font();
bgcolorhintlabel->setAlignment(Qt::AlignCenter);
labelFont.setPointSize(8);
bgcolorhintlabel->setFont(labelFont);
layout->addWidget(bgcolorhintlabel);
}
layout->addWidget(colorCfg);
colorCfg->setSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding);
ui->tabColors->setLayout(layout);
/* Background opacity is not yet determined */
ui->label_4->hide();
ui->hsImageOpacity->hide();
/* Background scaling is not yet determined */
ui->label_5->hide();
ui->edImageScaling->hide();
reload();
reset();
cf_Font = ui->edFont->currentFont();
}
IConfigOptions::~IConfigOptions()
{
delete ui;
delete ui;
}
bool IConfigOptions::isChanged() const
{
bool changed = cf_ShowOptions != ui->chkShowOptions->isChecked()
bool changed = cf_ShowOptions != ui->chkShowOptions->isChecked()
|| cf_Reconnect != ui->chkReconnect->isChecked()
|| cf_ReconnectDelay != ui->edReconnectDelay->value()
|| cf_RejoinChannelsOnConnect != ui->chkRejoinConnect->isChecked()
@ -73,12 +77,19 @@ bool IConfigOptions::isChanged() const
|| cf_BgImageOpacity != ui->hsImageOpacity->value()
|| cf_SSLSelfSigned != ui->chkSSLSelfsigned->isChecked()
|| cf_SSLExpired != ui->chkSSLExpired->isChecked()
|| cf_SSLCNMismatch != ui->chkSSLCNMismatch->isChecked();
|| cf_SSLCNMismatch != ui->chkSSLCNMismatch->isChecked()
|| cf_AccountNotify != ui->chkAccountNotify->isChecked()
|| cf_ExtendedJoin != ui->chkExtendedJoin->isChecked()
|| cf_AwayNotify != ui->chkAwayNotify->isChecked()
|| cf_InviteNotify != ui->chkInviteNotify->isChecked()
|| cf_MultiPrefix != ui->chkMutltiPrefix->isChecked()
|| cf_UserHostInNames != ui->chkUserhostInNames->isChecked()
|| cf_HistoryPlayback != ui->chkHistoryPlayback->isChecked();
if (changed)
return true;
else
return colorCfg->isChanged();
if (changed)
return true;
else
return colorCfg->isChanged();
}
void IConfigOptions::save()
@ -105,73 +116,96 @@ void IConfigOptions::save()
conf.setCommon("SSLSelfsigned", QString::number(ui->chkSSLSelfsigned->isChecked()));
conf.setCommon("SSLExpired", QString::number(ui->chkSSLExpired->isChecked()));
conf.setCommon("SSLCNMismatch", QString::number(ui->chkSSLCNMismatch->isChecked()));
conf.setIRCv3("account-notify", QString::number(ui->chkAccountNotify->isChecked()));
conf.setIRCv3("extended-join", QString::number(ui->chkExtendedJoin->isChecked()));
conf.setIRCv3("away-notify", QString::number(ui->chkAwayNotify->isChecked()));
conf.setIRCv3("invite-notify", QString::number(ui->chkInviteNotify->isChecked()));
conf.setIRCv3("multi-prefix", QString::number(ui->chkMutltiPrefix->isChecked()));
conf.setIRCv3("userhost-in-names", QString::number(ui->chkUserhostInNames->isChecked()));
conf.setIRCv3("message-tags", QString::number(ui->chkHistoryPlayback->isChecked()));
conf.setIRCv3("server-time", QString::number(ui->chkHistoryPlayback->isChecked()));
reload();
colorCfg->save();
}
void IConfigOptions::reset()
{
ui->chkShowOptions->setChecked(cf_ShowOptions);
ui->chkReconnect->setChecked(cf_Reconnect);
ui->edReconnectDelay->setValue(cf_ReconnectDelay);
ui->chkRejoinConnect->setChecked(cf_RejoinChannelsOnConnect);
ui->chkWhoisActive->setChecked(cf_ShowWhoisActiveWindow);
ui->chkShowModeMsg->setChecked(cf_ShowModeInMessage);
ui->chkTrayNotify->setChecked(cf_TrayNotify);
ui->edTrayDelay->setValue(cf_TrayNotifyDelay);
ui->chkTimestamp->setChecked(cf_ShowTimestamp);
ui->edTimestamp->setText(cf_TimestampFormat);
ui->chkManualKeepalive->setChecked(cf_ManualKeepaliveEnabled);
ui->edManualKeepalive->setValue(cf_ManualKeepalive);
ui->edQuit->setText(cf_QuitMessage);
ui->edFont->setCurrentFont(cf_Font);
ui->edFontSize->setValue(cf_FontSize);
ui->chkEnableBgImage->setChecked(cf_BgImageEnabled);
ui->edImage->setText(cf_BgImagePath);
ui->hsImageOpacity->setValue(cf_BgImageOpacity);
ui->chkSSLSelfsigned->setChecked(cf_SSLSelfSigned);
ui->chkSSLExpired->setChecked(cf_SSLExpired);
ui->chkSSLCNMismatch->setChecked(cf_SSLCNMismatch);
colorCfg->reset();
ui->chkShowOptions->setChecked(cf_ShowOptions);
ui->chkReconnect->setChecked(cf_Reconnect);
ui->edReconnectDelay->setValue(cf_ReconnectDelay);
ui->chkRejoinConnect->setChecked(cf_RejoinChannelsOnConnect);
ui->chkWhoisActive->setChecked(cf_ShowWhoisActiveWindow);
ui->chkShowModeMsg->setChecked(cf_ShowModeInMessage);
ui->chkTrayNotify->setChecked(cf_TrayNotify);
ui->edTrayDelay->setValue(cf_TrayNotifyDelay);
ui->chkTimestamp->setChecked(cf_ShowTimestamp);
ui->edTimestamp->setText(cf_TimestampFormat);
ui->chkManualKeepalive->setChecked(cf_ManualKeepaliveEnabled);
ui->edManualKeepalive->setValue(cf_ManualKeepalive);
ui->edQuit->setText(cf_QuitMessage);
ui->edFont->setCurrentFont(cf_Font);
ui->edFontSize->setValue(cf_FontSize);
ui->chkEnableBgImage->setChecked(cf_BgImageEnabled);
ui->edImage->setText(cf_BgImagePath);
ui->hsImageOpacity->setValue(cf_BgImageOpacity);
ui->chkSSLSelfsigned->setChecked(cf_SSLSelfSigned);
ui->chkSSLExpired->setChecked(cf_SSLExpired);
ui->chkSSLCNMismatch->setChecked(cf_SSLCNMismatch);
ui->chkAccountNotify->setChecked(cf_AccountNotify);
ui->chkExtendedJoin->setChecked(cf_ExtendedJoin);
ui->chkAwayNotify->setChecked(cf_AwayNotify);
ui->chkInviteNotify->setChecked(cf_InviteNotify);
ui->chkMutltiPrefix->setChecked(cf_MultiPrefix);
ui->chkUserhostInNames->setChecked(cf_UserHostInNames);
ui->chkHistoryPlayback->setChecked(cf_HistoryPlayback);
colorCfg->reset();
}
void IConfigOptions::on_chkSSLExpired_toggled(bool checked)
{
if (checked)
QMessageBox::warning(this, tr("Expired SSL certificates"),
tr("Allowing expired certificates is dangerous!\nThis option will revert after next connection."));
if (checked)
QMessageBox::warning(this, tr("Expired SSL certificates"),
tr("Allowing expired certificates is dangerous!\nThis option will revert after next connection."));
}
void IConfigOptions::reload()
{
ConfigMgr& conf = ConfigMgr::instance();
cf_ShowOptions = conf.common("ShowOptions").toInt();
cf_Reconnect = conf.common("Reconnect").toInt();
cf_ReconnectDelay = conf.common("ReconnectDelay").toInt();
cf_RejoinChannelsOnConnect = conf.common("RejoinChannelsOnConnect").toInt();
cf_ShowWhoisActiveWindow = conf.common("ShowWhoisActiveWindow").toInt();
cf_ShowModeInMessage = conf.common("ShowModeInMessage").toInt();
cf_TrayNotify = conf.common("TrayNotify").toInt();
cf_TrayNotifyDelay = conf.common("TrayNotifyDelay").toInt();
cf_ShowTimestamp = conf.common("ShowTimestamp").toInt();
ConfigMgr& conf = ConfigMgr::instance();
cf_ShowOptions = conf.common("ShowOptions").toInt();
cf_Reconnect = conf.common("Reconnect").toInt();
cf_ReconnectDelay = conf.common("ReconnectDelay").toInt();
cf_RejoinChannelsOnConnect = conf.common("RejoinChannelsOnConnect").toInt();
cf_ShowWhoisActiveWindow = conf.common("ShowWhoisActiveWindow").toInt();
cf_ShowModeInMessage = conf.common("ShowModeInMessage").toInt();
cf_TrayNotify = conf.common("TrayNotify").toInt();
cf_TrayNotifyDelay = conf.common("TrayNotifyDelay").toInt();
cf_ShowTimestamp = conf.common("ShowTimestamp").toInt();
cf_TimestampFormat = conf.common("TimestampFormat");
cf_ManualKeepaliveEnabled = conf.common("ManualKeepaliveEnabled").toInt();
cf_ManualKeepalive = conf.common("ManualKeepalive").toInt();
cf_QuitMessage = conf.common("QuitMessage");
cf_Font = conf.common("Font");
cf_FontSize = conf.common("FontSize").toInt();
cf_BgImageEnabled = conf.common("BgImageEnabled").toInt();
cf_BgImagePath = conf.common("BgImagePath");
cf_BgImageOpacity = conf.common("BgImageOpacity").toInt();
// TODO background image scaling combo box from config
cf_SSLSelfSigned = conf.common("SSLSelfsigned").toInt();
cf_SSLExpired = conf.common("SSLExpired").toInt();
cf_SSLCNMismatch = conf.common("SSLCNMismatch").toInt();
cf_Font = conf.common("Font");
cf_FontSize = conf.common("FontSize").toInt();
cf_BgImageEnabled = conf.common("BgImageEnabled").toInt();
cf_BgImagePath = conf.common("BgImagePath");
cf_BgImageOpacity = conf.common("BgImageOpacity").toInt();
// TODO background image scaling combo box from config
cf_SSLSelfSigned = conf.common("SSLSelfsigned").toInt();
cf_SSLExpired = conf.common("SSLExpired").toInt();
cf_SSLCNMismatch = conf.common("SSLCNMismatch").toInt();
cf_AccountNotify = conf.IRCv3("account-notify").toInt();
cf_ExtendedJoin = conf.IRCv3("extended-join").toInt();
cf_AwayNotify = conf.IRCv3("away-notify").toInt();
cf_InviteNotify = conf.IRCv3("invite-notify").toInt();
cf_MultiPrefix = conf.IRCv3("multi-prefix").toInt();
cf_UserHostInNames = conf.IRCv3("userhost-in-names").toInt();
cf_HistoryPlayback = conf.IRCv3("message-tags").toInt() && conf.IRCv3("server-time").toInt();
}
void IConfigOptions::on_btnImageBrowse_clicked()
{
QString path = QFileDialog::getOpenFileName(this, tr("Choose background image"), LOCAL_PATH, tr("Image Files (*.png *.jpg *.bmp)"));
ui->edImage->setText(path);
QString path = QFileDialog::getOpenFileName(this, tr("Choose background image"), LOCAL_PATH, tr("Image Files (*.png *.jpg *.bmp)"));
ui->edImage->setText(path);
}

@ -19,46 +19,53 @@ class IConfigOptions;
class IConfigOptions : public QWidget
{
Q_OBJECT
Q_OBJECT
public:
explicit IConfigOptions(QWidget *parent = nullptr);
~IConfigOptions();
bool isChanged() const;
void save();
void reset();
explicit IConfigOptions(QWidget *parent = nullptr);
~IConfigOptions();
bool isChanged() const;
void save();
void reset();
private slots:
void on_chkSSLExpired_toggled(bool checked);
void on_btnImageBrowse_clicked();
void on_chkSSLExpired_toggled(bool checked);
void on_btnImageBrowse_clicked();
private:
void reload();
Ui::IConfigOptions *ui;
QVBoxLayout *layout;
ColorConfig *colorCfg;
bool cf_ShowOptions;
bool cf_Reconnect;
int cf_ReconnectDelay;
bool cf_RejoinChannelsOnConnect;
bool cf_ShowWhoisActiveWindow;
bool cf_ShowModeInMessage;
bool cf_TrayNotify;
int cf_TrayNotifyDelay;
bool cf_ShowTimestamp;
QString cf_TimestampFormat;
void reload();
Ui::IConfigOptions *ui;
QVBoxLayout *layout;
ColorConfig *colorCfg;
bool cf_ShowOptions;
bool cf_Reconnect;
int cf_ReconnectDelay;
bool cf_RejoinChannelsOnConnect;
bool cf_ShowWhoisActiveWindow;
bool cf_ShowModeInMessage;
bool cf_TrayNotify;
int cf_TrayNotifyDelay;
bool cf_ShowTimestamp;
QString cf_TimestampFormat;
bool cf_ManualKeepaliveEnabled;
int cf_ManualKeepalive;
QString cf_QuitMessage;
QFont cf_Font;
int cf_FontSize;
bool cf_BgImageEnabled;
QString cf_BgImagePath;
int cf_BgImageOpacity;
// TODO background image scaling combo box
bool cf_SSLSelfSigned;
bool cf_SSLExpired;
bool cf_SSLCNMismatch;
int cf_ManualKeepalive;
QString cf_QuitMessage;
QFont cf_Font;
int cf_FontSize;
bool cf_BgImageEnabled;
QString cf_BgImagePath;
int cf_BgImageOpacity;
// TODO background image scaling combo box
bool cf_SSLSelfSigned;
bool cf_SSLExpired;
bool cf_SSLCNMismatch;
bool cf_AccountNotify;
bool cf_ExtendedJoin;
bool cf_AwayNotify;
bool cf_InviteNotify;
bool cf_MultiPrefix;
bool cf_UserHostInNames;
bool cf_HistoryPlayback;
};
#endif // ICONFIGOPTIONS_H

@ -13,417 +13,647 @@
<property name="windowTitle">
<string>Form</string>
</property>
<widget class="QTabWidget" name="tabWidget">
<property name="geometry">
<rect>
<x>9</x>
<y>9</y>
<width>591</width>
<height>561</height>
</rect>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tabCommon">
<attribute name="title">
<string>Common</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="chkShowOptions">
<property name="text">
<string>Show options upon start</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_10">
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tabCommon">
<attribute name="title">
<string>Common</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="chkReconnect">
<widget class="QCheckBox" name="chkShowOptions">
<property name="text">
<string>Automatic re-connect after lost connection, delay:</string>
<string>Show options upon start</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="edReconnectDelay">
<property name="suffix">
<string> sec</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>9999999</number>
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<widget class="QCheckBox" name="chkReconnect">
<property name="text">
<string>Automatic re-connect after lost connection, delay:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="edReconnectDelay">
<property name="suffix">
<string> sec</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>9999999</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="chkRejoinConnect">
<property name="text">
<string>Auto re-join channels after re-connect</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<widget class="QCheckBox" name="chkWhoisActive">
<property name="text">
<string>Show /whois in active window</string>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</widget>
</item>
<item>
<widget class="QCheckBox" name="chkShowModeMsg">
<property name="text">
<string>Show modes in messages</string>
</property>
</spacer>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QCheckBox" name="chkTrayNotify">
<property name="text">
<string>Tray notify, delay:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="edTrayDelay">
<property name="suffix">
<string> sec</string>
</property>
<property name="minimum">
<number>1</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="chkRejoinConnect">
<property name="text">
<string>Auto re-join channels after re-connect</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="chkWhoisActive">
<property name="text">
<string>Show /whois in active window</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="chkShowModeMsg">
<property name="text">
<string>Show modes in messages</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QCheckBox" name="chkTrayNotify">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QCheckBox" name="chkTimestamp">
<property name="text">
<string>Display timestamp, format:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="edTimestamp"/>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QCheckBox" name="chkManualKeepalive">
<property name="text">
<string>Manual keep-alive</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="edManualKeepalive">
<property name="suffix">
<string> sec</string>
</property>
<property name="minimum">
<number>10</number>
</property>
<property name="maximum">
<number>120</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Tray notify, delay:</string>
<string>Quit message</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="edTrayDelay">
<property name="suffix">
<string> sec</string>
<widget class="QLineEdit" name="edQuit"/>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Font</string>
</property>
<property name="minimum">
<number>1</number>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QFontComboBox" name="edFont"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Size</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="edFontSize">
<property name="suffix">
<string> px</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>200</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>323</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>SSL exceptions</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QCheckBox" name="chkSSLSelfsigned">
<property name="text">
<string>Allow self-signed certificates</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="chkSSLExpired">
<property name="text">
<string>Allow expired certificates</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="chkSSLCNMismatch">
<property name="text">
<string>Allow mismatching Common Name (host name)</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
<width>20</width>
<height>44</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QCheckBox" name="chkTimestamp">
<property name="text">
<string>Display timestamp, format:</string>
</widget>
<widget class="QWidget" name="tabColors">
<attribute name="title">
<string>Colors</string>
</attribute>
</widget>
<widget class="QWidget" name="tabIRCv3">
<attribute name="title">
<string>IRCv3 extensions</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_3">
<property name="verticalSpacing">
<number>12</number>
</property>
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>6</number>
</property>
</widget>
<item>
<widget class="QCheckBox" name="chkAccountNotify">
<property name="text">
<string>Account notification (account-notify)</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>Receive notification when other members login to accounts.</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_17">
<property name="text">
<string>Handling of this notification is not dealt with in IdealIRC yet, but we do receive it from the server.</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLineEdit" name="edTimestamp"/>
</item>
<item>
<spacer name="horizontalSpacer">
<item row="0" column="1">
<spacer name="horizontalSpacer_7">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<width>377</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QCheckBox" name="chkManualKeepalive">
<property name="text">
<string>Manual keep-alive</string>
<item row="1" column="0">
<layout class="QVBoxLayout" name="verticalLayout_5">
<property name="spacing">
<number>6</number>
</property>
</widget>
<item>
<widget class="QCheckBox" name="chkExtendedJoin">
<property name="text">
<string>Extended join (extended-join)</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>Receive account name for members joining a channel.</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QSpinBox" name="edManualKeepalive">
<property name="suffix">
<string> sec</string>
<item row="2" column="0">
<layout class="QVBoxLayout" name="verticalLayout_6">
<property name="spacing">
<number>6</number>
</property>
<property name="minimum">
<number>10</number>
<item>
<widget class="QCheckBox" name="chkAwayNotify">
<property name="text">
<string>Away notification (away-notify)</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_9">
<property name="text">
<string>Receive away messages from other members.</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<layout class="QVBoxLayout" name="verticalLayout_7">
<property name="spacing">
<number>6</number>
</property>
<property name="maximum">
<number>120</number>
<item>
<widget class="QCheckBox" name="chkInviteNotify">
<property name="text">
<string>Invite notification (invite-notify)</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_10">
<property name="text">
<string>Allows privileged channel users to see when someone is invited to their channel.</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0">
<layout class="QVBoxLayout" name="verticalLayout_8">
<property name="spacing">
<number>6</number>
</property>
</widget>
<item>
<widget class="QCheckBox" name="chkMutltiPrefix">
<property name="text">
<string>Multiple prefixes (multi-prefix)</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_11">
<property name="text">
<string>Allows IdealIRC to see all the statuses (i.e. voice (+), chanop(@)) that other clients have in a channel</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_14">
<property name="text">
<string>rather than just the highest. This also mitigates an issue if someone has both voice (+) and chanop (@),</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_15">
<property name="text">
<string>and you join that channel, you only receive their chanop (@). When they de-op, you don't know about</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_16">
<property name="text">
<string>their voice (+) mode.</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer_5">
<item row="5" column="0">
<layout class="QVBoxLayout" name="verticalLayout_9">
<property name="spacing">
<number>6</number>
</property>
<item>
<widget class="QCheckBox" name="chkUserhostInNames">
<property name="text">
<string>Hostname in /NAMES (userhost-in-names)</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_12">
<property name="text">
<string>Allows IdealIRC to more easily see the user/hostnames of other clients when joining channels.</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="6" column="0">
<layout class="QVBoxLayout" name="verticalLayout_10">
<property name="spacing">
<number>6</number>
</property>
<item>
<widget class="QCheckBox" name="chkHistoryPlayback">
<property name="text">
<string>Server history playback (message-tags, server-time)</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_13">
<property name="text">
<string>This is a combination of the extensions &quot;message-tags&quot; and &quot;server-time&quot; which triggers this feature</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_18">
<property name="text">
<string>for channels that enable it.</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="7" column="0" colspan="2">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
<width>20</width>
<height>329</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Quit message</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="edQuit"/>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Font</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QFontComboBox" name="edFont"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Size</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="edFontSize">
<property name="suffix">
<string> px</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>200</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>323</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>SSL exceptions</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QCheckBox" name="chkSSLSelfsigned">
<property name="text">
<string>Allow self-signed certificates</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="chkSSLExpired">
<property name="text">
<string>Allow expired certificates</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="chkSSLCNMismatch">
<property name="text">
<string>Allow mismatching Common Name (host name)</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>44</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabColors">
<attribute name="title">
<string>Colors</string>
</attribute>
</widget>
<widget class="QWidget" name="tabBgImg">
<attribute name="title">
<string>Background image</string>
</attribute>
<layout class="QGridLayout" name="gridLayout">
<item row="3" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QLabel" name="label_4">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
<widget class="QWidget" name="tabBgImg">
<attribute name="title">
<string>Background image</string>
</attribute>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="0">
<widget class="QCheckBox" name="chkEnableBgImage">
<property name="text">
<string>Opacity</string>
<string>Enable background image</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="hsImageOpacity">
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>100</number>
</property>
<item row="5" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLabel" name="label_5">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Scaling</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="edImageScaling"/>
</item>
</layout>
</item>
<item row="3" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Image file</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="edImage"/>
</item>
<item>
<widget class="QPushButton" name="btnImageBrowse">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="5" column="1">
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="invertedAppearance">
<bool>false</bool>
</property>
<property name="invertedControls">
<bool>false</bool>
<property name="sizeHint" stdset="0">
<size>
<width>287</width>
<height>20</height>
</size>
</property>
</widget>
</spacer>
</item>
</layout>
</item>
<item row="4" column="1">
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>287</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="chkEnableBgImage">
<property name="text">
<string>Enable background image</string>
</property>
</widget>
</item>
<item row="4" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLabel" name="label_5">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
<item row="4" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QLabel" name="label_4">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Opacity</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="hsImageOpacity">
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="invertedAppearance">
<bool>false</bool>
</property>
<property name="invertedControls">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="7" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="text">
<string>Scaling</string>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>301</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="edImageScaling"/>
</spacer>
</item>
</layout>
</item>
<item row="5" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>301</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Image file</string>
<item row="1" column="0" colspan="2">
<widget class="QLabel" name="label_6">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="edImage"/>
</item>
<item>
<widget class="QPushButton" name="btnImageBrowse">
<property name="text">
<string>Browse...</string>
<string>This is trash, don't bother enabling this tab page.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</widget>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>chkShowOptions</tabstop>
@ -449,7 +679,6 @@
<tabstop>btnImageBrowse</tabstop>
<tabstop>hsImageOpacity</tabstop>
<tabstop>edImageScaling</tabstop>
<tabstop>tabWidget</tabstop>
</tabstops>
<resources/>
<connections/>

@ -7,119 +7,200 @@
#include "IConfigServers.h"
#include "ui_IConfigServers.h"
#include "AddServer.h"
#include "ConfigMgr.h"
#include "ServerItem.h"
#include <QMessageBox>
#include <QDebug>
IConfigServers::IConfigServers(QWidget *parent) :
QWidget(parent),
ui(new Ui::IConfigServers)
QWidget(parent),
ui(new Ui::IConfigServers)
{
ui->setupUi(this);
ConfigMgr& conf = ConfigMgr::instance();
cf_Realname = conf.connection("Realname");
cf_Username = conf.connection("Username");
cf_Nickname = conf.connection("Nickname");
cf_AltNickame = conf.connection("AltNickname");
cf_Server = conf.connection("Server");
cf_Password = conf.connection("Password");
cf_SSL = conf.connection("SSL").toInt();
ui->edRealName->setText(cf_Realname);
ui->edUsername->setText(cf_Username);
ui->edNickname->setText(cf_Nickname);
ui->edAltNickname->setText(cf_AltNickame);
ui->edServer->setText(cf_Server);
ui->edServerPassword->setText(cf_Password);
ui->chkSSL->setChecked(cf_SSL);
editor = new ServerEditor(smodel, this);
ui->servers->setModel(&smodel);
QModelIndex sindex = smodel.indexFromHost(cf_Server);
if (sindex.isValid())
ui->servers->selectionModel()->setCurrentIndex(sindex, QItemSelectionModel::Select | QItemSelectionModel::Rows);
ui->setupUi(this);
ConfigMgr& conf = ConfigMgr::instance();
cf_Realname = conf.connection("Realname");
cf_Username = conf.connection("Username");
cf_Nickname = conf.connection("Nickname");
cf_AltNickame = conf.connection("AltNickname");
cf_Server = conf.connection("Server");
cf_Password = conf.connection("Password");
cf_SASL = conf.connection("SASL");
cf_SSL = conf.connection("SSL").toInt();
ui->edRealName->setText(cf_Realname);
ui->edUsername->setText(cf_Username);
ui->edNickname->setText(cf_Nickname);
ui->edAltNickname->setText(cf_AltNickame);
ui->edServer->setText(cf_Server);
ui->edServerPassword->setText(cf_Password);
ui->edSASL->setText(cf_SASL);
ui->chkSSL->setChecked(cf_SSL);
ui->servers->setModel(&smodel);
ui->servers->setItemDelegateForColumn(2, &m_serverOptionsDelegate);
ui->servers->setItemDelegateForColumn(3, &m_serverOptionsDelegate);
ui->servers->setItemDelegateForColumn(4, &m_serverOptionsDelegate);
for (const auto& index : smodel.getEditorColumns()) {
ui->servers->openPersistentEditor(index);
}
connect(&smodel, &ServerModel::newEntry,
[this](const QModelIndex& sslIdx, const QModelIndex& passwordIdx, const QModelIndex& saslIdx) {
ui->servers->openPersistentEditor(sslIdx);
ui->servers->openPersistentEditor(passwordIdx);
ui->servers->openPersistentEditor(saslIdx);
serversModified = true;
});
ui->servers->setColumnWidth(2, 50); // SSL column
ui->servers->setColumnWidth(3, 100); // Password column
ui->servers->setColumnWidth(4, 50); // SASL column
}
IConfigServers::~IConfigServers()
{
delete ui;
delete ui;
}
bool IConfigServers::isChanged() const
{
return cf_Realname != ui->edRealName->text()
return cf_Realname != ui->edRealName->text()
|| cf_Username != ui->edUsername->text()
|| cf_Nickname != ui->edNickname->text()
|| cf_AltNickame != ui->edAltNickname->text()
|| cf_Server != ui->edServer->text()
|| cf_Password != ui->edServerPassword->text()
|| cf_SSL != ui->chkSSL->isChecked();
|| cf_SASL != ui->edSASL->text()
|| cf_SSL != ui->chkSSL->isChecked()
|| serversModified;
}
bool IConfigServers::connectToNewStatus() const
{
return ui->chkNewStatus->isChecked();
return ui->chkNewStatus->isChecked();
}
void IConfigServers::unsetConnectToNewStatus()
{
ui->chkNewStatus->setChecked(false);
ui->chkNewStatus->setChecked(false);
}
void IConfigServers::save()
{
ConfigMgr& conf = ConfigMgr::instance();
conf.setConnection("Realname", ui->edRealName->text());
conf.setConnection("Username", ui->edUsername->text());
conf.setConnection("Nickname", ui->edNickname->text());
conf.setConnection("AltNickname", ui->edAltNickname->text());
conf.setConnection("Server", ui->edServer->text());
conf.setConnection("Password", ui->edServerPassword->text());
conf.setConnection("SSL", QString::number(ui->chkSSL->isChecked()));
cf_Realname = conf.connection("Realname");
cf_Username = conf.connection("Username");
cf_Nickname = conf.connection("Nickname");
cf_AltNickame = conf.connection("AltNickname");
cf_Server = conf.connection("Server");
cf_Password = conf.connection("Password");
cf_SSL = conf.connection("SSL").toInt();
ConfigMgr& conf = ConfigMgr::instance();
conf.setConnection("Realname", ui->edRealName->text());
conf.setConnection("Username", ui->edUsername->text());
conf.setConnection("Nickname", ui->edNickname->text());
conf.setConnection("AltNickname", ui->edAltNickname->text());
conf.setConnection("Server", ui->edServer->text());
conf.setConnection("Password", ui->edServerPassword->text());
conf.setConnection("SASL", ui->edSASL->text());
conf.setConnection("SSL", QString::number(ui->chkSSL->isChecked()));
cf_Realname = conf.connection("Realname");
cf_Username = conf.connection("Username");
cf_Nickname = conf.connection("Nickname");
cf_AltNickame = conf.connection("AltNickname");
cf_Server = conf.connection("Server");
cf_Password = conf.connection("Password");
cf_SASL = conf.connection("SASL");
cf_SSL = conf.connection("SSL").toInt();
if (!smodel.saveToFile())
QMessageBox::warning(this, tr("Failed to save servers.json"), tr("Unable to open servers.json for writing, changes will be lost upon next restart!"));
else
serversModified = false;
}
void IConfigServers::reset()
{
ui->edRealName->setText(cf_Realname);
ui->edUsername->setText(cf_Username);
ui->edNickname->setText(cf_Nickname);
ui->edAltNickname->setText(cf_AltNickame);
ui->edServer->setText(cf_Server);
ui->edServerPassword->setText(cf_Password);
ui->edRealName->setText(cf_Realname);
ui->edUsername->setText(cf_Username);
ui->edNickname->setText(cf_Nickname);
ui->edAltNickname->setText(cf_AltNickame);
ui->edServer->setText(cf_Server);
ui->edServerPassword->setText(cf_Password);
ui->edSASL->setText(cf_SASL);
if (serversModified) {
smodel.reloadModel();
serversModified = false;
for (const auto& index : smodel.getEditorColumns()) {
ui->servers->openPersistentEditor(index);
}
}
}
void IConfigServers::on_btnShowPassword_toggled(bool checked)
{
ui->edServerPassword->setEchoMode(checked ? QLineEdit::Normal : QLineEdit::Password);
ui->edServerPassword->setEchoMode(checked ? QLineEdit::Normal : QLineEdit::Password);
}
void IConfigServers::on_btnShowSASL_toggled(bool checked)
{
ui->edSASL->setEchoMode(checked ? QLineEdit::Normal : QLineEdit::Password);
}
void IConfigServers::on_servers_clicked(const QModelIndex& index)
{
auto* item = static_cast<const ServerItem*>(index.internalPointer());
ui->edServer->setText( item->address() );
ui->edServerPassword->setText( item->password() );
ui->chkSSL->setChecked( item->ssl() );
if (item->parent())
ui->edSASL->setText(item->parent()->sasl());
else
ui->edSASL->setText(item->sasl());
}
void IConfigServers::on_btnEditServers_clicked()
void IConfigServers::on_btnAddServer_clicked()
{
editor->show();
if (!addServerDlg) {
addServerDlg = new AddServer(smodel.getNetworks(), this);
addServerDlg->show();
connect(addServerDlg, &QDialog::destroyed,
[this] {
addServerDlg = nullptr;
});
connect(addServerDlg, &AddServer::saved,
[this] {
if (addServerDlg->isServer())
smodel.addServer(addServerDlg->name(), addServerDlg->address(), addServerDlg->password(), addServerDlg->sasl(), addServerDlg->ssl(), addServerDlg->networkIndex());
else
smodel.addNetwork(addServerDlg->name(), addServerDlg->address(), addServerDlg->password(), addServerDlg->sasl(), addServerDlg->ssl());
});
}
}
void IConfigServers::on_servers_clicked(const QModelIndex &index)
void IConfigServers::on_btnDeleteSelected_clicked()
{
auto spair = smodel.fromIndex(index);
QString details = smodel.details(spair.second, spair.first);
QString server;
QString password;
if (details.contains('|')) {
password = details.split('|')[1];
details.remove("|"+password);
}
server = details;
ui->edServer->setText(server);
ui->edServerPassword->setText(password);
auto index = ui->servers->currentIndex();
if (!index.isValid())
return;
const auto* item = static_cast<const ServerItem*>(index.internalPointer());
QMessageBox::StandardButton btn;
if (item->isNetwork())
btn = QMessageBox::question(this,
tr("Delete entire network"),
tr("You are about to delete an entire network!\nConfirm deletion of %1?").arg(item->name()));
else
btn = QMessageBox::question(this,
tr("Delete server"),
tr("Confirm deletion of %1?").arg(item->name()));
if (btn == QMessageBox::Yes) {
smodel.deleteEntry(index);
serversModified = true;
}
else {
return;
}
}

@ -8,43 +8,50 @@
#ifndef ICONFIGSERVERS_H
#define ICONFIGSERVERS_H
#include "ServerEditor.h"
#include "ServerModel.h"
#include "ServerOptionsDelegate.h"
#include <QWidget>
namespace Ui {
class IConfigServers;
}
class AddServer;
class IConfigServers : public QWidget
{
Q_OBJECT
Q_OBJECT
public:
explicit IConfigServers(QWidget *parent = nullptr);
~IConfigServers();
bool isChanged() const;
bool connectToNewStatus() const;
void unsetConnectToNewStatus();
void save();
void reset();
explicit IConfigServers(QWidget *parent = nullptr);
~IConfigServers();
bool isChanged() const;
bool connectToNewStatus() const;
void unsetConnectToNewStatus();
void save();
void reset();
private slots:
void on_btnShowPassword_toggled(bool checked);
void on_btnEditServers_clicked();
void on_servers_clicked(const QModelIndex &index);
void on_btnShowPassword_toggled(bool checked);
void on_btnShowSASL_toggled(bool checked);
void on_servers_clicked(const QModelIndex& index);
void on_btnAddServer_clicked();
void on_btnDeleteSelected_clicked();
private:
Ui::IConfigServers *ui;
ServerModel smodel;
ServerEditor *editor;
QString cf_Realname;
QString cf_Username;
QString cf_Nickname;
QString cf_AltNickame;
QString cf_Server;
QString cf_Password;
bool cf_SSL;
Ui::IConfigServers *ui;
AddServer* addServerDlg{};
ServerOptionsDelegate m_serverOptionsDelegate;
ServerModel smodel;
QString cf_Realname;
QString cf_Username;
QString cf_Nickname;
QString cf_AltNickame;
QString cf_Server;
QString cf_Password;
QString cf_SASL;
bool cf_SSL;
bool serversModified{ false };
};
#endif // ICONFIGSERVERS_H

@ -159,6 +159,40 @@
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QLabel" name="label_7">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>SASL credentials</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="edSASL">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="btnShowSASL">
<property name="text">
<string>Show</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="chkSSL">
<property name="text">
@ -169,16 +203,35 @@
<item>
<widget class="QTreeView" name="servers">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
<set>QAbstractItemView::DoubleClicked</set>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<property name="animated">
<bool>true</bool>
</property>
<attribute name="headerDefaultSectionSize">
<number>150</number>
</attribute>
<attribute name="headerStretchLastSection">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QPushButton" name="btnEditServers">
<widget class="QPushButton" name="btnAddServer">
<property name="text">
<string>Add...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnDeleteSelected">
<property name="text">
<string>Edit servers...</string>
<string>Delete selected</string>
</property>
</widget>
</item>
@ -238,8 +291,10 @@
<tabstop>edServer</tabstop>
<tabstop>edServerPassword</tabstop>
<tabstop>btnShowPassword</tabstop>
<tabstop>chkSSL</tabstop>
<tabstop>servers</tabstop>
<tabstop>btnEditServers</tabstop>
<tabstop>btnAddServer</tabstop>
<tabstop>btnDeleteSelected</tabstop>
<tabstop>chkNewStatus</tabstop>
</tabstops>
<resources/>

@ -1,313 +0,0 @@
/*
* IdealIRC - Internet Relay Chat client
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "ServerEditor.h"
#include "ui_ServerEditor.h"
#include <QDebug>
#include <QMessageBox>
#include <QInputDialog>
ServerEditor::ServerEditor(ServerModel& model, QWidget* parent)
: QDialog(parent)
, ui(new Ui::ServerEditor)
, smodel(model)
{
ui->setupUi(this);
addMenu = new QMenu(this);
addServerAction = addMenu->addAction(tr("Server"), this, &ServerEditor::on_addServerAction_triggered);
addNetworkAction = addMenu->addAction(tr("Network"), this, &ServerEditor::on_addNetworkAction_triggered);
ui->btnAdd->setMenu(addMenu);
ui->serverView->setModel(&smodel);
ui->serverView->header()->setSectionResizeMode(QHeaderView::Interactive);
QStringList networks = smodel.networkList();
ui->edNetwork->addItem("");
for (const QString& network : networks) {
if (network == "NONE")
continue;
ui->edNetwork->addItem(network);
}
}
ServerEditor::~ServerEditor()
{
delete ui;
}
void ServerEditor::on_addServerAction_triggered()
{
QString network = "NONE";
if (editMode == EditMode::Network) {
QModelIndex current = ui->serverView->currentIndex();
auto spair = smodel.fromIndex(current);
network = spair.first;
}
else if (editMode == EditMode::Server) {
QModelIndex current = ui->serverView->currentIndex();
if (current.parent().isValid()) {
auto spair = smodel.fromIndex(current.parent());
network = spair.first;
}
}
QString name;
QString host;
for (int i = 1 ;; ++i) {
name = QStringLiteral("New server %1").arg(i);
host = QStringLiteral("host%1.name:6667").arg(i);
QString det = smodel.details(name);
if (det.isEmpty())
break;
}
QModelIndex idx = smodel.addServer(name, host, network);
selectItem(idx);
}
void ServerEditor::on_addNetworkAction_triggered()
{
QString name;
QString host;
for (int i = 1 ;; ++i) {
name = QStringLiteral("New network %1").arg(i);
host = QStringLiteral("irc%1.host.name:6667").arg(i);
QString det = smodel.details("DEFAULT", name);
if (det.isEmpty())
break;
}
QModelIndex idx = smodel.addNetwork(name, host);
ui->edNetwork->addItem(name);
selectItem(idx);
}
void ServerEditor::on_btnDel_clicked()
{
if (editMode == EditMode::Off)
return;
QModelIndex current = ui->serverView->currentIndex();
auto spair = smodel.fromIndex(current);
QString network = spair.first;
QString name = spair.second;
if (editMode == EditMode::Server) {
if (QMessageBox::question(this, tr("Delete server"), tr("Do you want to delete the server '%1'?").arg(name)) == QMessageBox::No)
return;
smodel.delServer(name, network);
}
if (editMode == EditMode::Network) {
name = spair.first;
auto answer = QMessageBox::question(this, tr("Delete network"), tr("Do you want to delete the network '%1'?\nHitting 'Yes' will keep the servers as orphans.").arg(name), QMessageBox::Yes | QMessageBox::YesAll | QMessageBox::No);
if (answer == QMessageBox::No)
return;
bool keepServers = answer != QMessageBox::YesAll;
smodel.delNetwork(name, keepServers);
}
ui->serverView->clearSelection();
disableAll();
}
void ServerEditor::on_btnShowPassword_toggled(bool checked)
{
ui->edPassword->setEchoMode(checked ? QLineEdit::Normal : QLineEdit::Password);
}
void ServerEditor::on_btnSave_clicked()
{
if (editMode == EditMode::Off)
return;
if (ui->edName->text().isEmpty()) {
QMessageBox::information(this, tr("Field missing"), tr("No name is specified."));
return;
}
if (ui->edHostname->text().isEmpty()) {
QMessageBox::information(this, tr("Field missing"), tr("No hostname is specified."));
return;
}
if (editMode == EditMode::Server)
saveServerEdit();
else if (editMode == EditMode::Network)
saveNetworkEdit();
}
void ServerEditor::enableForServer()
{
enableAll();
ui->lbNetwork->show();
ui->edNetwork->show();
editMode = EditMode::Server;
}
void ServerEditor::enableForNetwork()
{
enableAll();
ui->lbNetwork->hide();
ui->edNetwork->hide();
editMode = EditMode::Network;
}
void ServerEditor::enableAll()
{
ui->edName->setEnabled(true);
ui->edHostname->setEnabled(true);
ui->edPort->setEnabled(true);
ui->edPassword->setEnabled(true);
ui->btnShowPassword->setEnabled(true);
ui->btnShowPassword->setChecked(false);
ui->edNetwork->setEnabled(true);
ui->btnSave->setEnabled(true);
}
void ServerEditor::disableAll()
{
ui->edName->clear();
ui->edHostname->clear();
ui->edPort->setValue(6667);
ui->edPassword->clear();
ui->btnShowPassword->setChecked(false);
ui->edNetwork->clearEditText();
ui->edName->setEnabled(false);
ui->edHostname->setEnabled(false);
ui->edPort->setEnabled(false);
ui->edPassword->setEnabled(false);
ui->btnShowPassword->setEnabled(false);
ui->edNetwork->setEnabled(false);
ui->btnSave->setEnabled(false);
ui->lbNetwork->show();
ui->edNetwork->show();
editMode = EditMode::Off;
}
void ServerEditor::saveNetworkEdit()
{
qDebug() << "Save network";
QModelIndex current = ui->serverView->currentIndex();
auto spair = smodel.fromIndex(current);
QString network = spair.first;
/* Network name changed */
if (network != ui->edName->text()) {
QString newNetwork = ui->edName->text();
if (ui->edNetwork->findText(newNetwork) > -1) {
QMessageBox::information(this, tr("Duplicate network name"), tr("The specified network name already exist."));
return;
}
smodel.renameNetwork(network, newNetwork);
int networkIdx = ui->edNetwork->findText(network);
ui->edNetwork->setItemText(networkIdx, newNetwork);
network = newNetwork;
}
QString details = ui->edHostname->text() + ":" + QString::number(ui->edPort->value());
QString password = ui->edPassword->text();
if (!password.isEmpty())
details += "|" + password;
smodel.setNetworkServer(network, details);
}
void ServerEditor::saveServerEdit()
{
qDebug() << "Save server";
QModelIndex current = ui->serverView->currentIndex();
auto spair = smodel.fromIndex(current);
QString name = ui->edName->text();
QString host = ui->edHostname->text();
QString port = QString::number(ui->edPort->value());
QString password = ui->edPassword->text();
QString network = ui->edNetwork->currentText();
if (ui->edNetwork->currentIndex() == 0)
network = "NONE";
if (spair.first != network) {
/* New network */
if (network != "NONE" && ui->edNetwork->findText(network) == -1) {
bool ok = false;
QString hostname = QInputDialog::getText(this, tr("New network"), tr("Set hostname:port for the new network %1").arg(network), QLineEdit::Normal, "", &ok);
if (!ok || hostname.isEmpty())
return;
smodel.addNetwork(network, hostname);
ui->edNetwork->addItem(network);
}
QString details = smodel.details(spair.second, spair.first);
smodel.delServer(spair.second, spair.first);
QModelIndex idx = smodel.addServer(spair.second, details, network);
selectItem(idx);
}
smodel.setServer(spair.second, host + ":" + port, password, network);
if (spair.second != name) {
QString details = smodel.details(spair.second, network);
smodel.delServer(spair.second, network);
smodel.resetModel();
QModelIndex idx = smodel.addServer(name, details, network);
selectItem(idx);
}
}
void ServerEditor::selectItem(const QModelIndex& index)
{
ui->serverView->clearSelection();
ui->serverView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Select | QItemSelectionModel::Rows);
on_serverView_clicked(index);
}
void ServerEditor::on_serverView_clicked(const QModelIndex &index)
{
if (!index.isValid()) {
disableAll();
return;
}
auto spair = smodel.fromIndex(index);
if (spair.first.isEmpty()) {
disableAll();
return;
}
QString name;
if (spair.second == "DEFAULT") {
name = spair.first;
enableForNetwork();
}
else {
name = spair.second;
enableForServer();
}
QString details = smodel.details(spair.second, spair.first);
QString host, port, password;
// Value format: host:port|password
if (details.contains('|')) {
password = details.split('|')[1];
details = details.remove("|"+password);
}
if (details.contains(':')) {
port = details.split(':')[1];
details = details.remove(":"+port);
}
host = details;
ui->edName->setText(name);
ui->edHostname->setText(host);
ui->edPort->setValue(port.toInt());
ui->edPassword->setText(password);
int networkUiIndex = ui->edNetwork->findText(spair.first);
if (networkUiIndex == -1)
networkUiIndex = 0;
ui->edNetwork->setCurrentIndex(networkUiIndex);
}

@ -1,59 +0,0 @@
/*
* IdealIRC - Internet Relay Chat client
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#ifndef SERVEREDITOR_H
#define SERVEREDITOR_H
#include "ServerModel.h"
#include <QDialog>
#include <QMenu>
namespace Ui {
class ServerEditor;
}
class ServerEditor : public QDialog
{
Q_OBJECT
public:
explicit ServerEditor(ServerModel& model, QWidget *parent = nullptr);
~ServerEditor();
private slots:
void on_addServerAction_triggered();
void on_addNetworkAction_triggered();
void on_btnDel_clicked();
void on_btnShowPassword_toggled(bool checked);
void on_btnSave_clicked();
void on_serverView_clicked(const QModelIndex &index);
private:
enum class EditMode {
Off,
Server,
Network
} editMode = EditMode::Off;
void enableForServer();
void enableForNetwork();
void enableAll();
void disableAll();
void saveNetworkEdit();
void saveServerEdit();
void selectItem(const QModelIndex& index);
Ui::ServerEditor* ui;
QMenu* addMenu;
QAction* addServerAction;
QAction* addNetworkAction;
ServerModel& smodel;
};
#endif // SERVEREDITOR_H

@ -1,251 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ServerEditor</class>
<widget class="QDialog" name="ServerEditor">
<property name="windowModality">
<enum>Qt::WindowModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>370</width>
<height>450</height>
</rect>
</property>
<property name="windowTitle">
<string>Server editor</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0" rowspan="3" colspan="2">
<widget class="QTreeView" name="serverView">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QToolButton" name="btnAdd">
<property name="text">
<string>+</string>
</property>
<property name="popupMode">
<enum>QToolButton::InstantPopup</enum>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QToolButton" name="btnDel">
<property name="text">
<string>-</string>
</property>
</widget>
</item>
<item row="2" column="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>178</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="0" colspan="2">
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Name</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Hostname</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Port</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Password</string>
</property>
</widget>
</item>
<item row="3" column="1" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="edPassword">
<property name="enabled">
<bool>false</bool>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="btnShowPassword">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Show</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="QLabel" name="lbNetwork">
<property name="text">
<string>Network</string>
</property>
</widget>
</item>
<item row="5" column="1">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="5" column="2">
<widget class="QPushButton" name="btnSave">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Save</string>
</property>
</widget>
</item>
<item row="2" column="1" colspan="2">
<widget class="QSpinBox" name="edPort">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>6667</number>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QLineEdit" name="edHostname">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="QLineEdit" name="edName">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="4" column="1" colspan="2">
<widget class="QComboBox" name="edNetwork">
<property name="enabled">
<bool>false</bool>
</property>
<property name="editable">
<bool>true</bool>
</property>
<property name="insertPolicy">
<enum>QComboBox::InsertAlphabetically</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="4" column="0">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>230</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="1">
<widget class="QPushButton" name="btnClose">
<property name="text">
<string>Close</string>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>serverView</tabstop>
<tabstop>btnAdd</tabstop>
<tabstop>btnDel</tabstop>
<tabstop>edName</tabstop>
<tabstop>edHostname</tabstop>
<tabstop>edPort</tabstop>
<tabstop>edPassword</tabstop>
<tabstop>edNetwork</tabstop>
<tabstop>btnSave</tabstop>
<tabstop>btnClose</tabstop>
<tabstop>btnShowPassword</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>btnClose</sender>
<signal>clicked()</signal>
<receiver>ServerEditor</receiver>
<slot>close()</slot>
<hints>
<hint type="sourcelabel">
<x>272</x>
<y>327</y>
</hint>
<hint type="destinationlabel">
<x>358</x>
<y>288</y>
</hint>
</hints>
</connection>
</connections>
</ui>

@ -0,0 +1,51 @@
/*
* IdealIRC - Internet Relay Chat client
* Copyright (C) 2022 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "ServerItem.h"
namespace Key = ServerJsonKey;
ServerItem::ServerItem(const QJsonObject& data, int row, ServerItem* parentItem, bool isNetwork)
: m_data(data)
, m_row(row)
, m_parent(parentItem)
, m_isNetwork{ isNetwork }
{}
QString ServerItem::name() const
{
return m_data[Key::Name].toString();
}
QString ServerItem::address() const
{
auto host{ m_data[Key::Host].toString() };
auto port{ m_data[Key::Port].toInt() };
if (host.isEmpty())
host = "no.host";
if (port == 0)
port = ssl() ? 6697 : 6667;
return QStringLiteral("%1:%2").arg(host).arg(port);
}
QString ServerItem::password() const
{
return m_data[Key::Password].toString();
}
QString ServerItem::sasl() const
{
return m_data[Key::SASL].toString();
}
bool ServerItem::ssl() const
{
return m_data[Key::SSL].toBool();
}

@ -0,0 +1,76 @@
/*
* IdealIRC - Internet Relay Chat client
* Copyright (C) 2022 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#ifndef SERVERITEM_H
#define SERVERITEM_H
#include <QList>
#include <QString>
#include <QJsonObject>
namespace ServerJsonKey
{
constexpr auto Servers = "Servers";
constexpr auto Networks = "Networks";
constexpr auto Name = "Name";
constexpr auto Host = "Host";
constexpr auto Port = "Port";
constexpr auto Password = "Password";
constexpr auto SASL = "SASL";
constexpr auto SSL = "SSL";
}
class ServerItem
{
public:
ServerItem(const QJsonObject& data, int row, ServerItem* parentItem, bool isNetwork);
QString name() const;
void setName(const QString& name) { m_data[ServerJsonKey::Name] = name; }
QString address() const;
void setAddress(const QString& address)
{
auto hostport = address.split(':');
if (hostport.size() == 1)
hostport << (ssl() ? "6697" : "6667");
if (hostport[0].isEmpty())
hostport[0] = "server.name";
m_data[ServerJsonKey::Host] = hostport[0];
m_data[ServerJsonKey::Port] = hostport[1].toUShort();
}
QString password() const;
void setPassword(const QString& password) { m_data[ServerJsonKey::Password] = password; }
QString sasl() const;
void setSasl(const QString& credentials) { m_data[ServerJsonKey::SASL] = credentials; }
bool ssl() const;
void setSsl(bool enable) { m_data[ServerJsonKey::SSL] = enable; }
ServerItem* parent() const { return m_parent; }
int row() const { return m_row; }
void incRow() { ++m_row; }
void decRow() { --m_row; }
QList<ServerItem>& children() { return m_children; }
bool isNetwork() const { return m_isNetwork; }
private:
QJsonObject m_data;
QList<ServerItem> m_children;
int m_row;
ServerItem* m_parent;
bool m_isNetwork;
};
#endif // SERVERITEM_H

@ -1,133 +0,0 @@
/*
* IdealIRC - Internet Relay Chat client
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "ServerMgr.h"
#include "config.h"
#include <fmt/format.h>
ServerMgr::ServerMgr(QObject *parent) :
QObject(parent),
ini( QString(LOCAL_PATH+"/servers.ini").toStdString() )
{
}
QStringList ServerMgr::networkList()
{
int count = ini.count();
QStringList r;
for (int i = 0; i < count; i++) {
const auto name = QString::fromStdString( ini.section(i) );
r.push_back(name);
}
return r;
}
QHash<QString,QString> ServerMgr::serverList(const QString& network)
{
const auto networkStr = network.toStdString();
int count = ini.count(networkStr);
QHash<QString,QString> r;
for (int i = 0; i < count; i++) {
const auto servername = QString::fromStdString( ini.item(networkStr, i) );
const auto serverdetails = QString::fromStdString( ini.read(networkStr, i) );
r.insert(servername, serverdetails);
}
return r;
}
QString ServerMgr::defaultServer(const QString& network)
{
return QString::fromStdString( ini.read(network.toStdString(), "DEFAULT") );
}
bool ServerMgr::addNetwork(const QString& name)
{
const auto nameStr = name.toStdString();
if (nameStr == "NONE" || ini.exist(nameStr))
return false;
ini.write(nameStr, "DEFAULT", "server.name");
return true;
}
bool ServerMgr::renameNetwork(const QString& o_name, const QString& n_name)
{
if ((o_name == "NONE") || (n_name == "NONE"))
return false;
auto err = ini.rename(o_name.toStdString(), n_name.toStdString());
return err == IniFile::Error::NoError;
}
void ServerMgr::delNetwork(const QString& name, bool keep_servers)
{
// If servers=true, we will keep the servers by moving them to the NONE section.
const auto nameStr = name.toStdString();
if (! ini.exist(nameStr))
return;
if (keep_servers) {
int max = ini.count(nameStr);
for (int i = 0; i < max; i++) {
auto item = fmt::format("{}_{}", name.toStdString(), ini.item(nameStr, i));
auto value = ini.read(nameStr, i);
ini.write("NONE", item, value);
}
}
ini.remove(nameStr);
}
bool ServerMgr::addServer(const QString& name, const QString& host, const QString& pw, const QString& network)
{
QString details;
if (pw.isEmpty()) {
details = host;
}
else {
details = QString("%1|%2")
.arg(host)
.arg(pw);
}
const auto networkStr = network.toStdString();
if (!ini.exist(networkStr) && networkStr != "NONE")
return false;
ini.write(networkStr, name.toStdString(), details.toStdString());
return true;
}
void ServerMgr::delServer(const QString& name, const QString& network)
{
ini.remove(network.toStdString(), name.toStdString());
}
bool ServerMgr::hasNetwork(const QString& name)
{
return ini.exist(name.toStdString());
}
bool ServerMgr::hasServer(const QString& name, const QString& network)
{
return ini.exist(network.toStdString(), name.toStdString());
}
QString ServerMgr::getServerDetails(const QString& name, const QString& network)
{
return QString::fromStdString( ini.read(network.toStdString(), name.toStdString()) );
}

@ -1,66 +0,0 @@
/*
* IdealIRC - Internet Relay Chat client
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#ifndef SERVERMGR_H
#define SERVERMGR_H
#include <QObject>
#include <QStringList>
#include <QHash>
#include "IniFile.h"
/**
* @brief Manages servers.ini
* @details
* This class is from IdealIRC 0.x series and is going to be removed/replaced at any time.
*/
class ServerMgr : public QObject
{
Q_OBJECT
public:
explicit ServerMgr(QObject *parent = nullptr);
// All networks in a string list (Also counts in the NONE network)
QStringList networkList();
// All servers from a network in a hash map <"name","server:port|passwd">
QHash<QString,QString> serverList(const QString& network = "NONE");
// Return default server of given network (The "NONE" network have no default!) - returns empty if no default server is set.
QString defaultServer(const QString& network);
// Add new network to servers.ini - returns false if network exist
bool addNetwork(const QString& name);
// Rename a network - returns false if new network name already exist
bool renameNetwork(const QString& o_name, const QString& n_name);
// Delete network
void delNetwork(const QString& name, bool keep_servers = false);
// Add (or update) a server to network - returns false if network doesn't exsist
bool addServer(const QString& name, const QString& host /*host:port*/, const QString& pw = "", const QString& network = "NONE");
// Delete a server from network
void delServer(const QString& name, const QString& network = "NONE");
// Check of we have the given network name
bool hasNetwork(const QString& name);
// Check if we have the given server name inside the network
bool hasServer(const QString& name, const QString& network = "NONE");
// Get server details
QString getServerDetails(const QString& name, const QString& network = "NONE");
private:
IniFile ini;
};
#endif // SERVERMGR_H

@ -1,358 +1,430 @@
/*
* IdealIRC - Internet Relay Chat client
* Copyright (C) 2021 Tom-Andre Barstad.
* Copyright (C) 2022 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "ServerModel.h"
#include <QHashIterator>
#include "config.h"
#include <QJsonArray>
#include <QFile>
#include <QDebug>
#include <QTimer>
#include <QIcon>
#include <QFile>
ServerModel::ServerModel(QObject *parent) :
QStandardItemModel(parent)
namespace {
QJsonObject createJsonObject(const QString& name, const QString& address, const QString& password, const QString& sasl, bool isSsl)
{
resetModel();
}
QModelIndex ServerModel::indexFromHost(QString hostname)
{
return hostmap.value(hostname, QModelIndex());
}
QPair<QString, QString> ServerModel::fromIndex(const QModelIndex& index)
{
/* Locate netmap */
{
QHashIterator<QString,QModelIndex> it(netmap);
while (it.hasNext()) {
const QString& network = it.next().key();
const QModelIndex& iidx = it.value();
if (index == iidx || index == iidx.siblingAtColumn(1))
return { network, "DEFAULT" };
for (int i = 0 ;; ++i) {
QModelIndex cidx = iidx.child(i, 0);
if (!cidx.isValid())
break;
QString name = cidx.data().toString();
if (index == cidx || index == cidx.siblingAtColumn(1))
return { network, name };
}
}
}
/* Locate nonemap */
{
QHashIterator<QString,QModelIndex> it(hostmap);
while (it.hasNext()) {
it.next();
const QModelIndex& iidx = it.value();
if (index == iidx || index == iidx.siblingAtColumn(1))
return { "NONE", iidx.data().toString() };
}
}
return {};
}
namespace Key = ServerJsonKey;
QModelIndex ServerModel::addNetwork(QString name, QString server)
{
QStandardItem *root = invisibleRootItem();
auto hostport{ address.split(':') };
if (hostport.size() == 1) {
hostport[0] = "no.host";
hostport << (isSsl ? "6697" : "6667");
}
QStandardItem *pname = new QStandardItem(QIcon(":/options/gfx/network.png"), name);
QStandardItem *phost = new QStandardItem(server);
QList<QStandardItem*> list;
list << pname << phost;
const auto host{ hostport[0] };
const auto port{ hostport.count() > 1 ? hostport[1].toShort()
: (isSsl ? 6697 : 6667) };
QJsonObject obj;
root->appendRow(list);
hostmap.insert(server, pname->index());
netmap.insert(name, pname->index());
obj[Key::Name] = name;
obj[Key::Host] = host;
obj[Key::Port] = port;
obj[Key::SSL] = isSsl;
obj[Key::Password] = password;
if (smgr.addNetwork(name))
smgr.addServer("DEFAULT", server, "", name);
if (!sasl.isEmpty())
obj[Key::SASL] = sasl;
return pname->index();
return obj;
}
}
void ServerModel::setNetworkServer(QString name, QString server)
ServerModel::ServerModel(QObject* parent) :
QAbstractItemModel(parent)
{
QModelIndex current = netmap.value(name);
int row = current.row();
QModelIndex serverIndex = index(row, 1, current.parent());
QStandardItem *item = itemFromIndex(serverIndex);
item->setText(server);
smgr.addServer("DEFAULT", server, "", name);
loadFromFile();
}
void ServerModel::renameNetwork(QString name, QString newname)
bool ServerModel::saveToFile()
{
QModelIndex current = netmap.value(name);
int row = current.row();
QModelIndex nameIndex = index(row, 0, current.parent());
namespace Key = ServerJsonKey;
QJsonArray servers, networks;
for (auto& item : m_items) {
if (item.isNetwork()) {
QJsonArray networkServers;
for (auto& child : item.children()) {
QJsonObject server{ createJsonObject(child.name(), child.address(), child.password(), "", child.ssl()) };
networkServers << server;
}
QJsonObject network{ createJsonObject(item.name(), item.address(), item.password(), item.sasl(), item.ssl()) };
network[Key::Servers] = networkServers;
networks << network;
}
else {
QJsonObject server{ createJsonObject(item.name(), item.address(), item.password(), item.sasl(), item.ssl()) };
servers << server;
}
}
QJsonObject root;
root[Key::Servers] = servers;
root[Key::Networks] = networks;
QJsonDocument doc(root);
QStandardItem *item = itemFromIndex(nameIndex);
item->setText(newname);
const auto path{ QStringLiteral("%1/servers.json").arg(LOCAL_PATH) };
QFile f{ path };
if (!f.open(QIODevice::WriteOnly))
return false;
netmap.remove(name);
netmap.insert(newname, current);
smgr.renameNetwork(name, newname);
f.write(doc.toJson(QJsonDocument::Indented));
f.flush();
f.close();
return true;
}
void ServerModel::delNetwork(QString name, bool keepServers)
void ServerModel::reloadModel()
{
smgr.delNetwork(name, keepServers);
resetModel();
beginResetModel();
m_items.clear();
loadFromFile();
endResetModel();
}
QModelIndex ServerModel::addServer(QString name, QString server, QString network)
Qt::ItemFlags ServerModel::flags(const QModelIndex& index) const
{
QStandardItem *parent;
if (network.length() == 0)
network = "NONE";
/* Don't care about invalid indexes */
if (!index.isValid())
return Qt::NoItemFlags;
if (network == "NONE")
parent = invisibleRootItem();
else
parent = itemFromIndex( netmap.value(network) );
/* Columns containing widgets for editing */
if (index.column() > 1)
return Qt::ItemIsEnabled | Qt::ItemIsEditable;
QStandardItem *sname = new QStandardItem(QIcon(":/options/gfx/server.png"), name);
QStandardItem *shost = new QStandardItem(server);
QList<QStandardItem*> list;
list << sname << shost;
/* Directly editable columns */
Qt::ItemFlags flags{ Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled };
parent->appendRow(list);
hostmap.insert(server, indexFromItem(sname));
if (network == "NONE")
nonemap.insert(name, indexFromItem(sname));
const auto* item = static_cast<const ServerItem*>(index.internalPointer());
if (!item->isNetwork())
flags |= Qt::ItemNeverHasChildren;
smgr.addServer(name, server, "", network);
return indexFromItem(sname);
return flags;
}
void ServerModel::setServer(QString name, QString server, QString password, QString network)
QVariant ServerModel::data(const QModelIndex& index, int role) const
{
QStandardItem *parent;
if (!index.isValid())
return {};
if (network.length() == 0)
network = "NONE";
if (role == Qt::DisplayRole || role == Qt::EditRole) {
const auto* item = static_cast<const ServerItem*>(index.internalPointer());
if (network == "NONE")
parent = invisibleRootItem();
else
parent = itemFromIndex( netmap.value(network) );
if (index.column() == 0)
return item->name();
QModelIndex parentIdx = indexFromItem(parent); // Parent index
QModelIndex current; // Item's index
else if (index.column() == 1)
return item->address();
if (network != "NONE") {
else
return {};
}
for (int r = 0 ;; r++) {
QModelIndex idx = parentIdx.child(r, 0);
if (role == Qt::DecorationRole && index.column() == 0) {
const auto* item = static_cast<const ServerItem*>(index.internalPointer());
if (item->isNetwork())
return QIcon(":/Icons/network.png");
else
return QIcon(":/Icons/serverwindow.png");
}
if (! idx.isValid())
return; // No relevant child found, stop.
return {};
}
if (idx.data().toString() == name) {
current = idx;
break;
}
}
QVariant ServerModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
QStringList labels {
tr("Name"),
tr("Address"),
"",
"",
""
};
return labels[section];
}
else {
current = nonemap.value(name);
return {};
}
}
bool ServerModel::setData(const QModelIndex& index, const QVariant& value, int role)
{
if (!index.isValid() || role != Qt::EditRole)
return false;
auto* item = static_cast<ServerItem*>(index.internalPointer());
bool ret{ true };
switch (index.column()) {
case 0:
if (value.toString().isEmpty())
ret = false;
else
item->setName(value.toString());
break;
case 1:
if (value.toString().isEmpty())
ret = false;
else
item->setAddress(value.toString());
break;
default:
ret = false;
}
int row = current.row();
QModelIndex nameIndex = index(row, 0, current.parent());
QModelIndex serverIndex = index(row, 1, current.parent());
QStandardItem *serverItem = itemFromIndex(serverIndex);
serverItem->setText(server);
return ret;
}
hostmap.insert(server, nameIndex);
smgr.addServer(name, server, password, network);
int ServerModel::rowCount(const QModelIndex& parent) const
{
if (!parent.isValid())
return m_items.count();
else {
auto* parentItem = static_cast<ServerItem*>(parent.internalPointer());
return parentItem->children().count();
}
}
void ServerModel::renameServer(QString name, QString newname, QString network)
QModelIndex ServerModel::index(int row, int column, const QModelIndex& parent) const
{
QStandardItem *parent;
if (!hasIndex(row, column, parent))
return {};
if (network.length() == 0)
network = "NONE";
ServerItem* item{};
if (network == "NONE")
parent = invisibleRootItem();
else
parent = itemFromIndex( netmap.value(network) );
if (!parent.isValid())
item = &m_items[row];
else {
auto* parentItem = static_cast<ServerItem*>(parent.internalPointer());
item = &parentItem->children()[row];
}
QModelIndex parentIdx = indexFromItem(parent); // Parent index
QModelIndex current; // Item's index
return createIndex(row, column, item);
}
if (network != "NONE") {
QModelIndex ServerModel::parent(const QModelIndex& index) const
{
if (!index.isValid())
return {};
for (int r = 0 ;; r++) {
QModelIndex idx = parentIdx.child(r, 0);
auto* parentItem = static_cast<ServerItem*>(index.internalPointer())->parent();
if (!parentItem)
return {};
if (! idx.isValid())
return; // No relevant child found, stop.
return createIndex(parentItem->row(), 0, parentItem);
}
if (idx.data().toString() == name) {
current = idx;
break;
}
}
void ServerModel::addServer(const QString& name, const QString& address, const QString& password, const QString& sasl, bool isSsl, int networkIdx)
{
QList<ServerItem>* items{};
ServerItem* parentNetworkItem{};
int pos{};
QModelIndex modelIdx;
/* Network server */
if (networkIdx > -1) {
modelIdx = networkIndex(networkIdx);
parentNetworkItem = static_cast<ServerItem*>(modelIdx.internalPointer());
items = &(parentNetworkItem->children());
pos = items->count();
}
/* Non-network server */
else {
current = nonemap.value(name);
nonemap.remove(name);
nonemap.insert(newname, current);
}
items = &m_items;
int row = current.row();
QModelIndex serverIndex = index(row, 0, current.parent());
QStandardItem *item = itemFromIndex(serverIndex);
item->setText(newname);
for (auto& item : m_items) {
if (item.isNetwork())
item.incRow(); // Increment row number for each network, since any non-network server is inserted before these.
else
++pos; // Finds the end position of non-network servers. Add server there (bottom of non-networks; before networks are listed.)
}
}
QString details = smgr.getServerDetails(name, network);
smgr.delServer(name, network);
smgr.addServer(name, details, "", network);
const auto obj{ createJsonObject(name, address, password, sasl, isSsl) };
beginInsertRows(modelIdx, pos, pos);
items->insert(pos, ServerItem(obj, pos, parentNetworkItem, false));
endInsertRows();
emit newEntry(
createIndex(pos, 2, &((*items)[pos])), // SSL
createIndex(pos, 3, &((*items)[pos])), // Password
createIndex(pos, 4, &((*items)[pos])) // SASL
);
/*
* A hack to make new child items appear if parent's expanded.
* This also "solves" when a new/empty network gets its first entry, the network item (parent) won't get the "expand/collapse" decoration.
* Doing this should not be necessary, so there is an issue with this ServerModel, somewhere...
*/
QTimer::singleShot(1, [this] {
auto pos = m_items.size();
beginInsertRows(QModelIndex{}, pos, pos);
m_items << ServerItem({}, pos, nullptr, false);
endInsertRows();
QTimer::singleShot(1, [this] {
auto pos = m_items.size() - 1;
beginRemoveRows(QModelIndex{}, pos, pos);
m_items.removeLast();
endRemoveRows();
});
});
}
void ServerModel::delServer(QString name, QString network)
void ServerModel::addNetwork(const QString& name, const QString& address, const QString& password, const QString& sasl, bool isSsl)
{
QStandardItem *parent;
const auto obj{ createJsonObject(name, address, password, sasl, isSsl) };
const auto idx{ m_items.count() };
beginInsertRows(QModelIndex{}, idx, idx);
m_items << ServerItem(obj, idx, nullptr, true);
endInsertRows();
emit newEntry(
createIndex(idx, 2, &(m_items.back())), // SSL
createIndex(idx, 3, &(m_items.back())), // Password
createIndex(idx, 4, &(m_items.back())) // SASL
);
}
if (network.length() == 0)
network = "NONE";
void ServerModel::deleteEntry(const QModelIndex& index)
{
auto* item = static_cast<ServerItem*>(index.internalPointer());
auto* parent = item->parent();
QList<ServerItem>* items{};
if (network == "NONE")
parent = invisibleRootItem();
if (parent == nullptr)
items = &m_items;
else
parent = itemFromIndex( netmap.value(network) );
items = &(parent->children());
QModelIndex parentIdx = indexFromItem(parent); // Parent index
QModelIndex current; // Item's index
beginRemoveRows(index.parent(), index.row(), index.row());
if (network != "NONE") {
items->removeAt(item->row());
for (int i = item->row(); i < items->count(); ++i)
(*items)[i].decRow();
for (int r = 0 ;; r++) {
QModelIndex idx = parentIdx.child(r, 0);
endRemoveRows();
}
if (! idx.isValid())
return; // No relevant child found, stop.
QStringList ServerModel::getNetworks() const
{
QStringList networks;
for (const auto& item : m_items) {
if (item.isNetwork())
networks << item.name();
}
if (idx.data().toString() == name) {
current = idx;
break;
return networks;
}
QList<QModelIndex> ServerModel::getEditorColumns()
{
QList<QModelIndex> indexList;
for (auto& topItem : m_items) {
indexList << createIndex(topItem.row(), 2, &topItem);
indexList << createIndex(topItem.row(), 3, &topItem);
indexList << createIndex(topItem.row(), 4, &topItem);
if (topItem.isNetwork()) {
auto& items = topItem.children();
for (auto& item : items) {
indexList << createIndex(item.row(), 2, &item);
indexList << createIndex(item.row(), 3, &item);
}
}
}
else {
current = nonemap.value(name);
nonemap.remove(name);
}
int row = current.row();
removeRow(row, current.parent());
smgr.delServer(name, network);
return indexList;
}
void ServerModel::resetModel()
QModelIndex ServerModel::networkIndex(int row, int col)
{
clear();
QStandardItem *root = invisibleRootItem();
QStandardItem *i = new QStandardItem();
QStringList l;
l << tr("Name") << tr("Host");
setColumnCount(2);
setHorizontalHeaderItem(0, i);
setHorizontalHeaderLabels(l);
hostmap.clear();
netmap.clear();
nonemap.clear();
QStringList netlist = smgr.networkList();
if (netlist.contains("NONE")) { // "None" network is a section with servers not assigned to a network.
QHash<QString,QString> sl = smgr.serverList("NONE");
QHashIterator<QString,QString> i(sl);
while (i.hasNext()) {
i.next();
// Key: Server name
// Value: host:port|pass
QString name = i.key();
QString detail = i.value();
QString host; // hostname with port, e.g. irc.network.org:6667
host = detail.split('|')[0];
QStandardItem *itemname = new QStandardItem(QIcon(":/options/gfx/server.png"), name);
QStandardItem *itemhost = new QStandardItem(host);
QList<QStandardItem*> list;
list << itemname << itemhost;
root->appendRow(list);
hostmap.insert(host, indexFromItem(itemname));
nonemap.insert(name, indexFromItem(itemname));
}
}
for (int i = 0; i <= netlist.count()-1; ++i) {
if (netlist[i] == "NONE")
continue; // The "None" network already taken care of - ignore.
QString data = smgr.defaultServer(netlist[i]);
QString host = data.split('|')[0];
QStandardItem *pname = new QStandardItem(QIcon(":/options/gfx/network.png"), netlist[i]); // parent name
QStandardItem *phost = new QStandardItem(host); // parent host
QList<QStandardItem*> list;
list << pname << phost;
root->appendRow(list);
hostmap.insert(host, pname->index());
netmap.insert(netlist[i], pname->index());
QHash<QString,QString> sl = smgr.serverList(netlist[i]);
QHashIterator<QString,QString> sli(sl);
while (sli.hasNext()) {
sli.next();
// Key: Server name
// Value: host:port|pass
QString name = sli.key();
if (name == "DEFAULT")
continue; // The default value already taken care of, it's the address of parent item.
QString detail = sli.value();
QString host; // hostname with port, e.g. irc.network.org:6667
host = detail.split('|')[0];
QStandardItem *itemname = new QStandardItem(QIcon(":/options/gfx/server.png"), name); // parent name
QStandardItem *itemhost = new QStandardItem(host); // parent host
QList<QStandardItem*> list;
list << itemname << itemhost;
pname->appendRow(list);
hostmap.insert(host, indexFromItem(itemname));
}
}
int rc{ 0 };
for (auto& item : m_items) {
if (!item.isNetwork())
continue;
if (rc == row)
return createIndex(row, col, &item);
else
++rc;
}
return {};
}
QStringList ServerModel::networkList()
void ServerModel::createItems(const QJsonObject& json)
{
return smgr.networkList();
namespace Key = ServerJsonKey;
const auto servers = json[Key::Servers].toArray();
const auto networks = json[Key::Networks].toArray();
int rootRow{ 0 };
for (const auto& server : servers) {
m_items << ServerItem(server.toObject(), rootRow++, nullptr, false);
}
for (const auto& network : networks) {
const auto networkObj = network.toObject();
const auto childServers = networkObj[Key::Servers].toArray();
m_items << ServerItem(networkObj, rootRow++, nullptr, true);
auto& networkItem = m_items.back();
int childRow{ 0 };
for (const auto& cs : childServers) {
auto& networkServers = networkItem.children();
networkServers << ServerItem(cs.toObject(), childRow++, &networkItem, false);
}
}
}
QString ServerModel::details(QString name, QString network)
void ServerModel::loadFromFile()
{
return smgr.getServerDetails(name, network);
QJsonDocument jsondoc;
const auto path{ QStringLiteral("%1/servers.json").arg(LOCAL_PATH) };
QFile f{ path };
if (f.open(QIODevice::ReadOnly)) {
const auto jsonData{ f.readAll() };
qDebug() << "Read" << jsonData.size() << "bytes from" << path;
jsondoc = QJsonDocument::fromJson(jsonData);
f.close();
if (!jsondoc.isObject())
qWarning() << "Parse error for servers.json";
else
createItems(jsondoc.object());
}
else
qWarning() << "Unable to open servers.json:" << path;
}

@ -1,6 +1,6 @@
/*
* IdealIRC - Internet Relay Chat client
* Copyright (C) 2021 Tom-Andre Barstad.
* Copyright (C) 2022 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
@ -8,43 +8,68 @@
#ifndef SERVERMODEL_H
#define SERVERMODEL_H
#include <QStandardItemModel>
#include "ServerMgr.h"
#include <QHash>
#include <QPair>
#include "ServerItem.h"
#include <QAbstractItemModel>
#include <QList>
#include <QJsonDocument>
#include <QJsonObject>
/**
* @brief Model of servers.ini
* @details
* This class is from IdealIRC 0.x series and is going to be removed/replaced at any time.
* @brief Model of servers.json
*/
class ServerModel : public QStandardItemModel
class ServerModel : public QAbstractItemModel
{
Q_OBJECT
public:
explicit ServerModel(QObject *parent = nullptr);
QModelIndex indexFromHost(QString hostname); // Hostname:Port
QPair<QString,QString> fromIndex(const QModelIndex& index);
QModelIndex addNetwork(QString name, QString server);
void setNetworkServer(QString name, QString server = "");
void renameNetwork(QString name, QString newname);
void delNetwork(QString name, bool keepServers);
QModelIndex addServer(QString name, QString server, QString network = "NONE");
void setServer(QString name, QString server, QString password, QString network = "NONE");
void renameServer(QString name, QString newname, QString network = "NONE");
void delServer(QString name, QString network = "NONE");
void resetModel();
QStringList networkList();
QString details(QString name, QString network = "NONE");
explicit ServerModel(QObject* parent = nullptr);
/// Save model to servers.json
bool saveToFile();
/// Resets and reloads entire model
void reloadModel();
Qt::ItemFlags flags(const QModelIndex &index) const;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant& value, int role = Qt::EditRole) override;
int rowCount(const QModelIndex& parent = {}) const override;
int columnCount(const QModelIndex& parent = {}) const override
{
return 5;
};
QModelIndex index(int row, int column, const QModelIndex& parent = {}) const override;
QModelIndex parent(const QModelIndex& index) const override;
/// Add a server to the model. networkIdx = -1 means server is not part of a network.
void addServer(const QString& name, const QString& address, const QString& password, const QString& sasl, bool isSsl, int networkIdx);
/// Add a network to the model.
void addNetwork(const QString& name, const QString& address, const QString& password, const QString& sasl, bool isSsl);
void deleteEntry(const QModelIndex& index);
QStringList getNetworks() const;
QList<QModelIndex> getEditorColumns();
signals:
void newEntry(const QModelIndex& sslIdx, const QModelIndex& passwordIdx, const QModelIndex& saslIdx);
private:
ServerMgr smgr;
QModelIndex networkIndex(int row, int col = 0);
void createItems(const QJsonObject& json);
void loadFromFile();
QHash<QString,QModelIndex> hostmap; // host:port to index
QHash<QString,QModelIndex> netmap; // network to index
QHash<QString,QModelIndex> nonemap; // All names (servers) in NONE to index
// I don't like this being mutable, but the nature of Qt's item model seems to have forced my hand.
// Or I've probably not done the correct implementation design. In any way, it works, but smells a bit...
// This is the top-level items as shown in the UI, same order.
mutable QList<ServerItem> m_items;
};
#endif // SERVERMODEL_H

@ -0,0 +1,105 @@
/*
* IdealIRC - Internet Relay Chat client
* Copyright (C) 2022 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "ServerOptionsDelegate.h"
#include "ServerItem.h"
#include <QInputDialog>
#include <QPushButton>
#include <QDebug>
ServerOptionsDelegate::ServerOptionsDelegate(QObject* parent)
: QStyledItemDelegate(parent)
{}
QWidget* ServerOptionsDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
if (!index.isValid())
return nullptr;
if (index.column() == 2) {
auto* btn = new QPushButton(tr("SSL"), parent);
btn->setCheckable(true);
return btn;
}
else if (index.column() == 3) {
auto* item = static_cast<ServerItem*>(index.internalPointer());
auto* btn = new QPushButton(tr("Password"), parent);
connect(btn, &QPushButton::pressed,
[item, parent] {
auto password = QInputDialog::getText(
parent,
tr("Enter password"),
tr("Password for %1:").arg(item->name()),
QLineEdit::Password,
item->password()
);
item->setPassword(password);
});
return btn;
}
else if (index.column() == 4) {
auto* item = static_cast<ServerItem*>(index.internalPointer());
if (item->parent()) // Only items with a parent are network servers / child servers. No SASL for them.
return nullptr;
auto* btn = new QPushButton(tr("SASL"), parent);
connect(btn, &QPushButton::pressed,
[item, parent] {
auto password = QInputDialog::getText(
parent,
tr("SASL credentials"),
tr("SASL credentials for %1:").arg(item->name()),
QLineEdit::Password,
item->sasl()
);
item->setSasl(password);
});
return btn;
}
else {
return nullptr;
}
}
void ServerOptionsDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const
{
const auto* item = static_cast<const ServerItem*>(index.internalPointer());
if (index.column() == 2) {
auto* btn = qobject_cast<QPushButton*>(editor);
if (btn)
btn->setChecked(item->ssl());
else
qCritical() << "SSL button is not a QPushButton?!";
}
}
void ServerOptionsDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
{
auto* item = static_cast<ServerItem*>(index.internalPointer());
if (index.column() == 2) {
auto* btn = qobject_cast<QPushButton*>(editor);
if (btn)
item->setSsl(btn->isChecked());
else
qCritical() << "SSL button is not a QPushButton?!";
}
}
void ServerOptionsDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& /*index*/) const
{
editor->setGeometry(option.rect);
}

@ -0,0 +1,26 @@
/*
* IdealIRC - Internet Relay Chat client
* Copyright (C) 2022 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#ifndef SERVEROPTIONSDELEGATE_H
#define SERVEROPTIONSDELEGATE_H
#include <QStyledItemDelegate>
class ServerOptionsDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
explicit ServerOptionsDelegate(QObject* parent = nullptr);
QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
void setEditorData(QWidget* editor, const QModelIndex& index) const override;
void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override;
void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
};
#endif // SERVEROPTIONSDELEGATE_H

@ -1,125 +0,0 @@
/*
* IdealIRC - Internet Relay Chat client
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "ServerItem.h"
#include <QDebug>
ServerItem::ServerItem()
: m_type(Type::Root)
{}
ServerItem::ServerItem(ServerItem* parentItem, const QString& serverName, const QString& host, const quint16 port, const QString& password)
: m_type(Type::Server)
, m_serverName(serverName)
, m_host(host)
, m_port(port)
, m_password(password)
, m_parent(parentItem)
{}
ServerItem::ServerItem(const QString& networkName, const QString& host, const quint16 port, const QString& password)
: m_type(Type::Network)
, m_networkName(networkName)
, m_host(host)
, m_port(port)
, m_password(password)
{}
ServerItem::~ServerItem()
{
qDeleteAll(m_children);
}
ServerItem::Type ServerItem::getType() const
{
return m_type;
}
const QString& ServerItem::getNetworkName() const
{
return m_networkName;
}
const QString& ServerItem::getServerName() const
{
return m_serverName;
}
const QString& ServerItem::getHost() const
{
return m_host;
}
quint16 ServerItem::getPort() const
{
return m_port;
}
const QString& ServerItem::getPassword() const
{
return m_password;
}
ServerItem*ServerItem::getParentItem() const
{
return m_parent;
}
bool ServerItem::isOrphan() const
{
return m_parent == nullptr;
}
bool ServerItem::addChild(ServerItem* child)
{
if (m_type == Type::Server) {
qWarning() << "Tried to add a child item on a server entry!";
return false;
}
m_children.push_back(child);
return true;
}
bool ServerItem::delChild(ServerItem* child)
{
if (m_type == Type::Server) {
qWarning() << "Tried to delete a child item on a server entry!";
return false;
}
m_children.removeOne(child);
return true;
}
ServerItem* ServerItem::getChild(int row) const
{
if (m_type == Type::Server) {
qWarning() << "Tried to get a child item on a server entry!";
return nullptr;
}
if (row >= m_children.count()) {
qWarning() << "Tried to get child from row" << row << "- but it's out of index. Network:" << m_networkName;
return nullptr;
}
return m_children[row];
}
int ServerItem::childCount() const
{
return m_children.count();
}
int ServerItem::childRow(ServerItem* child) const
{
return m_children.indexOf(child);
}
ServerItem*ServerItem::getParent() const
{
return m_parent;
}

@ -1,67 +0,0 @@
/*
* IdealIRC - Internet Relay Chat client
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#ifndef SERVERITEM_H
#define SERVERITEM_H
#include <QString>
#include <QList>
class ServerItem
{
public:
enum class Type {
Root,
Server,
Network
};
/**
* @brief Construct a root item. Should only be constructed once as the "invisible" root item in the tree.
*/
explicit ServerItem();
/**
* @brief Construct a server item. Set parentItem=nullptr for an orphaned server.
*/
ServerItem(ServerItem* parentItem, const QString& serverName, const QString& host, const quint16 port, const QString& password);
/**
* @brief Construct a network item.
*/
ServerItem(const QString& networkName, const QString& host, const quint16 port, const QString& password);
~ServerItem();
Type getType() const;
const QString& getNetworkName() const;
const QString& getServerName() const;
const QString& getHost() const;
quint16 getPort() const;
const QString& getPassword() const;
ServerItem* getParentItem() const;
bool isOrphan() const;
bool addChild(ServerItem* child);
bool delChild(ServerItem* child);
ServerItem* getChild(int row) const;
int childCount() const;
int childRow(ServerItem* child) const;
ServerItem* getParent() const;
private:
Type m_type;
QString m_networkName;
QString m_serverName;
QString m_host;
quint16 m_port;
QString m_password;
ServerItem* m_parent{ nullptr };
QList<ServerItem*> m_children;
};
#endif // SERVERITEM_H

@ -1,172 +0,0 @@
/*
* IdealIRC - Internet Relay Chat client
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "ServerModel.h"
#include "config.h"
#include <QDebug>
ServerModel::ServerModel(QObject *parent)
: QAbstractItemModel(parent)
{
IniFile ini(LOCAL_PATH+"/servers.ini");
buildToplevelItem(ini, "NONE"); // Orphaned servers. Always build first.
const int sectionCount = ini.countSections();
for (int i = 0; i < sectionCount; ++i) {
QString section = ini.section(i);
if (section == "NONE")
continue;
buildToplevelItem(ini, section);
}
qDebug() << "Built server tree model.";
}
ServerModel::~ServerModel()
{
qDeleteAll(toplevelItems);
}
ServerModel::ItemIniValue::ItemIniValue(QString value)
{
if (value.isEmpty())
return;
// Value format: host:port|password
if (value.contains('|')) {
password = value.split('|')[1];
value = value.remove("|"+password);
}
if (value.contains(':')) {
port = value.split(':')[1];
value = value.remove(":"+port);
}
host = value;
}
QModelIndex ServerModel::index(int row, int column, const QModelIndex &parent) const
{
// TEST
//if (!hasIndex(row, column, parent))
// return QModelIndex();
ServerItem* item{ nullptr };
if (!parent.isValid()) {
if (row < toplevelItems.count())
item = toplevelItems[row];
}
else
item = static_cast<ServerItem*>(parent.internalPointer());
if (item)
return createIndex(row, column, item);
else
return QModelIndex();
}
QModelIndex ServerModel::parent(const QModelIndex &index) const
{
// FIXME: Implement me!
if (!index.isValid())
return QModelIndex();
ServerItem* childItem = static_cast<ServerItem*>(index.internalPointer());
if (!childItem->isOrphan()) {
ServerItem* parentItem = childItem->getParentItem();
return createIndex(parentItem->childRow(childItem), 0, parentItem);
}
else {
return QModelIndex();
}
}
int ServerModel::rowCount(const QModelIndex &parent) const
{
if (!parent.isValid())
return toplevelItems.count();
ServerItem* item = static_cast<ServerItem*>(parent.internalPointer());
if (!item)
return 0;
else
return item->childCount();
}
int ServerModel::columnCount(const QModelIndex&) const
{
return 1;
}
QVariant ServerModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
// FIXME: Implement me!
return "neger";
}
bool ServerModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (data(index, role) != value) {
// FIXME: Implement me!
emit dataChanged(index, index, QVector<int>() << role);
return true;
}
return false;
}
Qt::ItemFlags ServerModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return Qt::NoItemFlags;
return Qt::ItemIsEditable; // FIXME: Implement me!
}
bool ServerModel::insertRows(int row, int count, const QModelIndex &parent)
{
beginInsertRows(parent, row, row + count - 1);
// FIXME: Implement me!
endInsertRows();
}
bool ServerModel::removeRows(int row, int count, const QModelIndex &parent)
{
beginRemoveRows(parent, row, row + count - 1);
// FIXME: Implement me!
endRemoveRows();
}
void ServerModel::buildToplevelItem(IniFile& ini, const QString& section)
{
if (!ini.sectionExists(section))
return;
ServerItem* parent{ nullptr };
if (section != "NONE") {
ItemIniValue networkDefault(ini.value(section, "DEFAULT"));
parent = new ServerItem(section, networkDefault.host,
networkDefault.port.toUShort(),
networkDefault.password);
toplevelItems.push_back(parent);
}
const int itemCount = ini.countItems(section);
for (int i = 0; i < itemCount; ++i) {
QString key = ini.key(section, i);
if (key == "DEFAULT")
continue;
ItemIniValue val(ini.value(section, key));
ServerItem* item = new ServerItem(parent, key, val.host, val.port.toUShort(), val.password);
if (parent)
parent->addChild(item);
else
toplevelItems.push_back(item);
}
}

@ -1,54 +0,0 @@
/*
* IdealIRC - Internet Relay Chat client
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#ifndef SERVERMODEL_H
#define SERVERMODEL_H
#include "ServerItem.h"
#include "IniFile.h"
#include <QAbstractItemModel>
#include <QList>
class ServerModel : public QAbstractItemModel
{
Q_OBJECT
public:
explicit ServerModel(QObject *parent = nullptr);
~ServerModel() override;
// Basic functionality:
QModelIndex index(int row, int column,
const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &index) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
// Editable:
bool setData(const QModelIndex &index, const QVariant &value,
int role = Qt::EditRole) override;
Qt::ItemFlags flags(const QModelIndex& index) const override;
bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;
bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;
private:
struct ItemIniValue {
ItemIniValue(QString value);
QString host;
QString port;
QString password;
};
void buildToplevelItem(IniFile& ini, const QString& section);
QList<ServerItem*> toplevelItems;
};
#endif // SERVERMODEL_H

@ -6,6 +6,8 @@ list(APPEND ${component}_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/DCC.h
${CMAKE_CURRENT_SOURCE_DIR}/IRCBase.cpp
${CMAKE_CURRENT_SOURCE_DIR}/IRCBase.h
${CMAKE_CURRENT_SOURCE_DIR}/IRCMessage.cpp
${CMAKE_CURRENT_SOURCE_DIR}/IRCMessage.h
${CMAKE_CURRENT_SOURCE_DIR}/IRCChannel.cpp
${CMAKE_CURRENT_SOURCE_DIR}/IRCChannel.h
${CMAKE_CURRENT_SOURCE_DIR}/IRCError.cpp
@ -21,6 +23,59 @@ list(APPEND ${component}_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/Utilities.h
)
add_library(${component} STATIC ${${component}_SOURCES})
target_link_libraries(${component} fmt crypto ssl)
list(APPEND ${component}_COMMANDS
${CMAKE_CURRENT_SOURCE_DIR}/Commands/cmdError.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Commands/cmdInvite.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Commands/cmdJoin.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Commands/cmdKick.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Commands/cmdKill.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Commands/cmdMode.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Commands/cmdNick.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Commands/cmdNotice.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Commands/cmdPart.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Commands/cmdPing.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Commands/cmdPong.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Commands/cmdPrivmsg.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Commands/cmdQuit.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Commands/cmdTopic.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Commands/cmdV3Account.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Commands/cmdV3Authenticate.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Commands/cmdV3Away.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Commands/cmdV3Cap.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Commands/cmdWallops.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Commands/handleNumeric.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Commands/Unhandled.cpp
)
list(APPEND ${component}_PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/Private/addMemberToChannel.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Private/channelModeGroup.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Private/connected.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Private/ctcp.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Private/delMemberFromChannel.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Private/disconnectHandler.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Private/isChannelSymbol.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Private/isMemberPrivilegeSymbol.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Private/keepaliveTimeout.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Private/memberPrivilegeSymbolToMode.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Private/parseChannelModeMessage.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Private/parseIncoming.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Private/read.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Private/setDefaults.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Private/startKeepaliveTimer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Private/stopKeepaliveTimer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Private/verify_X509.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Private/write.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Private/writeNoMsg.cpp
)
add_library(${component} STATIC ${${component}_SOURCES} ${${component}_COMMANDS} ${${component}_PRIVATE})
target_link_libraries(${component}
NATUtils
fmt
crypto
ssl
)
target_include_directories(${component} PRIVATE ${CMAKE_SOURCE_DIR})

@ -10,66 +10,67 @@
namespace Command {
namespace IRC {
constexpr auto* PASS = "PASS";
constexpr auto* NICK = "NICK";
constexpr auto* USER = "USER";
constexpr auto* OPER = "OPER";
constexpr auto* MODE = "MODE";
constexpr auto* QUIT = "QUIT";
constexpr auto* SQUIT = "SQUIT";
constexpr auto* JOIN = "JOIN";
constexpr auto* PART = "PART";
constexpr auto* TOPIC = "TOPIC";
constexpr auto* NAMES = "NAMES";
constexpr auto* LIST = "LIST";
constexpr auto* INVITE = "INVITE";
constexpr auto* KICK = "KICK";
constexpr auto* PRIVMSG = "PRIVMSG";
constexpr auto* NOTICE = "NOTICE";
constexpr auto* MOTD = "MOTD";
constexpr auto* LUSERS = "LUSERS";
constexpr auto* VERSION = "VERSION";
constexpr auto* STATS = "STATS";
constexpr auto* LINKS = "LINKS";
constexpr auto* TIME = "TIME";
constexpr auto* CONNECT = "CONNECT";
constexpr auto* TRACE = "TRACE";
constexpr auto* ADMIN = "ADMIN";
constexpr auto* INFO = "INFO";
constexpr auto* SERVLIST = "SERVLIST";
constexpr auto* SQUERY = "SQUERY";
constexpr auto* WHO = "WHO";
constexpr auto* WHOIS = "WHOIS";
constexpr auto* WHOWAS = "WHOWAS";
constexpr auto* KILL = "KILL";
constexpr auto* PING = "PING";
constexpr auto* PONG = "PONG";
constexpr auto* ERROR = "ERROR";
constexpr auto* AWAY = "AWAY";
constexpr auto* REHASH = "REHASH";
constexpr auto* DIE = "DIE";
constexpr auto* RESTART = "RESTART";
constexpr auto* SUMMON = "SUMMON";
constexpr auto* USERS = "USERS";
constexpr auto* WALLOPS = "WALLOPS";
constexpr auto* USERHOST = "USERHOST";
constexpr auto* ISON = "ISON";
constexpr auto* PASS = "PASS";
constexpr auto* NICK = "NICK";
constexpr auto* USER = "USER";
constexpr auto* OPER = "OPER";
constexpr auto* MODE = "MODE";
constexpr auto* QUIT = "QUIT";
constexpr auto* SQUIT = "SQUIT";
constexpr auto* JOIN = "JOIN";
constexpr auto* PART = "PART";
constexpr auto* TOPIC = "TOPIC";
constexpr auto* NAMES = "NAMES";
constexpr auto* LIST = "LIST";
constexpr auto* INVITE = "INVITE";
constexpr auto* KICK = "KICK";
constexpr auto* PRIVMSG = "PRIVMSG";
constexpr auto* NOTICE = "NOTICE";
constexpr auto* MOTD = "MOTD";
constexpr auto* LUSERS = "LUSERS";
constexpr auto* VERSION = "VERSION";
constexpr auto* STATS = "STATS";
constexpr auto* LINKS = "LINKS";
constexpr auto* TIME = "TIME";
constexpr auto* CONNECT = "CONNECT";
constexpr auto* TRACE = "TRACE";
constexpr auto* ADMIN = "ADMIN";
constexpr auto* INFO = "INFO";
constexpr auto* SERVLIST = "SERVLIST";
constexpr auto* SQUERY = "SQUERY";
constexpr auto* WHO = "WHO";
constexpr auto* WHOIS = "WHOIS";
constexpr auto* WHOWAS = "WHOWAS";
constexpr auto* KILL = "KILL";
constexpr auto* PING = "PING";
constexpr auto* PONG = "PONG";
constexpr auto* ERROR_ = "ERROR"; // Environment for msvc already defines ERROR as a macro of sorts...
constexpr auto* AWAY = "AWAY";
constexpr auto* REHASH = "REHASH";
constexpr auto* DIE = "DIE";
constexpr auto* RESTART = "RESTART";
constexpr auto* SUMMON = "SUMMON";
constexpr auto* USERS = "USERS";
constexpr auto* WALLOPS = "WALLOPS";
constexpr auto* USERHOST = "USERHOST";
constexpr auto* ISON = "ISON";
}
namespace Extension {
constexpr auto* ACCOUNT = "ACCOUNT";
constexpr auto* ACCOUNT = "ACCOUNT";
constexpr auto* AUTHENTICATE = "AUTHENTICATE";
}
namespace IRCv3 {
constexpr auto* CAP = "CAP";
constexpr auto* LS = "LS";
constexpr auto* LIST = "LIST";
constexpr auto* REQ = "REQ";
constexpr auto* ACK = "ACK";
constexpr auto* NAK = "NAK";
constexpr auto* END = "END";
constexpr auto* NEW = "NEW";
constexpr auto* DEL = "DEL";
constexpr auto* CAP = "CAP";
constexpr auto* LS = "LS";
constexpr auto* LIST = "LIST";
constexpr auto* REQ = "REQ";
constexpr auto* ACK = "ACK";
constexpr auto* NAK = "NAK";
constexpr auto* END = "END";
constexpr auto* NEW = "NEW";
constexpr auto* DEL = "DEL";
} // namespace IRCv3
} // namespace command

@ -0,0 +1,14 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
void IRCBasePriv::handleUnhandled(const IRCMessage& ircmessage)
{
super.onMsgUnhandled(ircmessage.getSender(), ircmessage.getCommand(), ircmessage.getArgs(), ircmessage.getMessage());
}

@ -0,0 +1,14 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
void IRCBasePriv::cmdError(const IRCMessage& ircmessage)
{
super.onMsgError(ircmessage.getMessage());
}

@ -0,0 +1,14 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
void IRCBasePriv::cmdInvite(const IRCMessage& ircmessage)
{
super.onMsgInvite(ircmessage.getSender(), ircmessage.getMessage());
}

@ -0,0 +1,43 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
void IRCBasePriv::cmdJoin(const IRCMessage& ircmessage)
{
const auto& msg = ircmessage.getMessage();
const auto& sender = ircmessage.getSender();
if (ircmessage.getArgs().size() == 2) {
std::string accountname = ircmessage[1];
if (accountname[0] == '*')
accountname.clear();
/* We are joining a channel */
if (sender.nickname() == nickname) {
auto chanentry = super.getChannel(ircmessage[0]);
if (!chanentry)
channels.emplace_back(std::make_shared<IRCChannel>(ircmessage[0], super));
}
else
addMemberToChannel(sender, ircmessage[0]);
super.v3onMsgJoin(sender, ircmessage[0], accountname, msg);
}
else {
/* We are joining a channel */
auto channelName = msg.empty() ? ircmessage[0] : msg;
if (sender.nickname() == nickname) {
auto chanentry = super.getChannel(channelName);
if (!chanentry)
channels.emplace_back(std::make_shared<IRCChannel>(channelName, super));
}
else
addMemberToChannel(sender, channelName);
super.onMsgJoin(sender, channelName);
}
}

@ -0,0 +1,16 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
void IRCBasePriv::cmdKick(const IRCMessage& ircmessage)
{
super.onMsgKick(ircmessage.getSender(), ircmessage[0], ircmessage[1], ircmessage.getMessage());
auto prefix = IRCPrefix::fromNickname(ircmessage[1]);
delMemberFromChannel(prefix, ircmessage[0]);
}

@ -0,0 +1,14 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
void IRCBasePriv::cmdKill(const IRCMessage& ircmessage)
{
super.onMsgKill(ircmessage.getSender(), ircmessage.getMessage());
}

@ -0,0 +1,31 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
void IRCBasePriv::cmdMode(const IRCMessage& ircmessage)
{
auto args = ircmessage.getArgs();
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 = ircmessage.getMessage();
args.erase(args.begin(), args.begin() + 1);
}
super.onMsgMode(ircmessage.getSender(), target, modes, args);
}

@ -0,0 +1,29 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
void IRCBasePriv::cmdNick(const IRCMessage& ircmessage)
{
std::vector<std::string> channelsAffected;
channelsAffected.reserve(16);
auto member = super.getMember(ircmessage.getSender().toString());
if (member) {
const auto& chans = member->channels();
for (const auto& c : chans)
channelsAffected.push_back(c.lock()->name());
member->setNickname(ircmessage.getMessage());
}
/* We changed our nickname */
if (ircmessage.getSender().toString() == nickname)
nickname = ircmessage.getMessage();
super.onMsgNick(ircmessage.getSender(), ircmessage.getMessage(), channelsAffected);
}

@ -0,0 +1,23 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
#include "../Utilities.h"
void IRCBasePriv::cmdNotice(const IRCMessage& ircmessage)
{
const auto& sender = ircmessage.getSender();
const auto& msg = ircmessage.getMessage();
if (msg[0] == CTCPflag) {
auto[cmd,ctcpMsg] = FormatCTCPLine(msg);
super.onMsgCTCPResponse(sender, ircmessage[0], cmd, ctcpMsg);
}
else
super.onMsgNotice(sender, ircmessage[0], msg);
}

@ -0,0 +1,16 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
void IRCBasePriv::cmdPart(const IRCMessage& ircmessage)
{
const auto& sender = ircmessage.getSender();
super.onMsgPart(sender, ircmessage[0], ircmessage.getMessage());
delMemberFromChannel(sender, ircmessage[0]);
}

@ -0,0 +1,19 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
#include "../Commands.h"
void IRCBasePriv::cmdPing(const IRCMessage& ircmessage)
{
using namespace Command::IRC;
const auto& msg = ircmessage.getMessage();
write(PONG, msg);
super.onMsgPing(msg);
}

@ -0,0 +1,14 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
void IRCBasePriv::cmdPong(const IRCMessage& ircmessage)
{
super.onMsgPong(ircmessage.getMessage());
}

@ -0,0 +1,51 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
#include "../Utilities.h"
#include <iostream> // Temporary printouts for making DCC work.
void IRCBasePriv::cmdPrivmsg(const IRCMessage& ircmessage)
{
const auto& sender = ircmessage.getSender();
const auto& msg = ircmessage.getMessage();
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, ircmessage[0], dccCmd, dccMsg);
}
}
}
else
super.onMsgCTCPRequest(sender, ircmessage[0], cmd, ctcpMsg);
}
else
super.onMsgPrivmsg(sender, ircmessage[0], msg);
}

@ -0,0 +1,32 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
void IRCBasePriv::cmdQuit(const IRCMessage& ircmessage)
{
std::vector<std::shared_ptr<IRCChannel>> channelsAffected;
std::vector<std::string> channelsAffectedStr;
auto member = super.getMember(ircmessage.getSender().toString());
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(ircmessage.getSender(), ircmessage.getMessage(), channelsAffectedStr);
for (const auto& c : channelsAffected)
c->delMember(member);
}

@ -0,0 +1,17 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
void IRCBasePriv::cmdTopic(const IRCMessage& ircmessage)
{
auto channel = super.getChannel(ircmessage[0]);
if (channel)
channel->setTopic(ircmessage.getMessage());
super.onMsgTopic(ircmessage.getSender(), ircmessage[0], ircmessage.getMessage());
}

@ -0,0 +1,18 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
void IRCBasePriv::cmdV3Account(const IRCMessage& ircmessage)
{
const auto& sender = ircmessage.getSender();
if (ircmessage[0] == "*")
super.v3onMsgAccountLogout(sender);
else
super.v3onMsgAccountLogin(sender, ircmessage[0]);
}

@ -0,0 +1,20 @@
/*
* IdealIRC - Internet Relay Chat client
* Copyright (c) 2022 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
#include "../Commands.h"
#include "../Utilities.h"
void IRCBasePriv::cmdV3Authenticate(const IRCMessage& ircmessage)
{
using namespace Command::IRC;
if (saslInProgress && ircmessage[0] == "+") {
const auto credentials = ident + char(0) + ident + char(0) + v3SASL;
writeNoMsg(Command::Extension::AUTHENTICATE, { toBase64(credentials) });
}
}

@ -0,0 +1,24 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
void IRCBasePriv::cmdV3Away(const IRCMessage& ircmessage)
{
const auto& sender = ircmessage.getSender();
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());
}
}
super.v3onMsgAway(sender, ircmessage.getMessage(), channelsAffected);
}

@ -0,0 +1,64 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
#include "../Commands.h"
void IRCBasePriv::cmdV3Cap(const IRCMessage& ircmessage)
{
using namespace Command::IRC;
bool saslSupported{ std::find(registeredV3support.begin(), registeredV3support.end(), "sasl") != registeredV3support.end() };
/*
* List of IRCv3 capabilities from the server.
* Match it with what we support and request those.
*/
if (ircmessage[1] == Command::IRCv3::LS) {
std::istringstream ss(ircmessage.getMessage());
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(ircv3enabled.begin(), ircv3enabled.end(), cap);
if (myIt != ircv3enabled.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 (ircmessage[1] == Command::IRCv3::ACK) {
if (saslSupported && !v3SASL.empty()) {
saslInProgress = true;
writeNoMsg(Command::Extension::AUTHENTICATE, { "PLAIN" });
}
else {
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 (ircmessage[1] == Command::IRCv3::NAK) {
registeredV3support.clear();
writeNoMsg(Command::IRCv3::CAP, { Command::IRCv3::END }); // TODO erase the offending modules instead.
}
}

@ -0,0 +1,14 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
void IRCBasePriv::cmdWallops(const IRCMessage& ircmessage)
{
super.onMsgWallops(ircmessage.getSender(), ircmessage.getMessage());
}

@ -0,0 +1,135 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
#include "../Commands.h"
void IRCBasePriv::handleNumeric(const IRCMessage& ircmessage) {
using namespace Numeric;
if (!isOnline && ircmessage == RPL_ISUPPORT) {
// Note: skip the first item (argument), it is always our nickname
for (auto it = ircmessage.getArgs().cbegin() + 1; it != ircmessage.getArgs().cend(); ++it) {
const auto& arg = *it;
std::string key, val;
auto delim = std::find(arg.begin(), arg.end(), '=');
if (delim != arg.end()) {
key = std::string(arg.begin(), delim);
val = std::string(delim + 1, arg.end());
}
else
key = arg;
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 (!isOnline && (ircmessage == RPL_ENDOFMOTD || ircmessage == ERR_NOMOTD)) {
nickname = ircmessage[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 (ircmessage == RPL_NAMREPLY) {
auto chan = super.getChannel(ircmessage[2]);
if (chan && chan->isPopulating()) {
std::istringstream ss(ircmessage.getMessage());
/*
* 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 (ircmessage == RPL_ENDOFNAMES) {
auto chan = super.getChannel(ircmessage[1]);
if (chan && chan->isPopulating())
chan->donePopulating();
}
else if (ircmessage == RPL_TOPIC) {
auto chan = super.getChannel(ircmessage[1]);
if (chan)
chan->setTopic(ircmessage.getMessage());
}
else if (ircmessage == RPL_CHANNELMODEIS) {
auto chan = super.getChannel(ircmessage[1]);
const std::string& modes = ircmessage[2];
auto args = ircmessage.getArgs();
args.erase(args.begin(), args.begin() + 3);
if (chan)
parseChannelModeMessage(chan, modes, args);
}
else if (ircmessage == NumericV3::RPL_SASLSUCCESS && saslInProgress) {
saslInProgress = false;
writeNoMsg(Command::IRCv3::CAP, { Command::IRCv3::END });
}
else if (ircmessage == NumericV3::ERR_SASLFAIL && saslInProgress) {
saslInProgress = false;
super.disconnectFromServer();
}
super.onMsgNumeric(ircmessage.getSender(), ircmessage.getCommand(), ircmessage.getArgs(), ircmessage.getMessage());
}

@ -7,6 +7,9 @@
#include "DCC.h"
#include "IRCBase.h"
#include "NATUtils/PortMapping.h"
#include "NATUtils/PublicAddress.h"
#include "Utilities.h"
#include <fmt/format.h>
#include <iostream>
@ -14,201 +17,295 @@ using asio::ip::tcp;
struct DCCPriv
{
DCCPriv(DCC& super_, IRCBase& ircctx_, DCC::Direction dir_, asio::io_context& ioctx_, const std::string& ip_, const std::string& port_)
: super(super_)
, ircctx(ircctx_)
, direction(dir_)
, ioctx(ioctx_)
, ip(ip_)
, port(port_)
, resolver(ioctx_)
, acceptor(ioctx_)
, endpoint(tcp::v4(), std::stoi(port_))
, socket(ioctx_)
{}
DCC& super;
IRCBase& ircctx;
bool pending{ true };
const DCC::Direction direction;
asio::io_context& ioctx;
std::string ip; // Only used when Direction=Target, and describes the Initiator's IP.
std::string port;
tcp::resolver resolver; // for outbound connection (we act as a Target)
tcp::acceptor acceptor; // for inbound connection (we act as an Initiator)
tcp::resolver::results_type resolverResults;
asio::ip::tcp::endpoint endpoint;
tcp::socket socket;
asio::streambuf readbuf;
DCC::CallbackRead cbRead;
DCC::CallbackConnected cbCon;
DCC::CallbackDisconnected cbDiscon;
void connected(const asio::error_code& ec)
{
super.onConnected();
asio::streambuf::mutable_buffers_type mutableBuf{ readbuf.prepare(16384) };
socket.async_receive(mutableBuf,
[this](const asio::error_code& ec, std::size_t rl){
readbuf.commit(rl);
read(ec);
});
}
void disconnected(const asio::error_code& ec)
{
super.onDisconnected(SystemErrorToIRCError(ec));
}
void read(const asio::error_code& ec)
{
if (ec) {
disconnected(ec);
return;
}
std::istream is(&readbuf);
super.onRead(is);
asio::streambuf::mutable_buffers_type mutableBuf{ readbuf.prepare(16384) };
socket.async_receive(mutableBuf,
[this](const asio::error_code& ec, std::size_t rl){
readbuf.commit(rl);
read(ec);
});
}
void writeFinished(const asio::error_code& ec, std::size_t bytes_transferred)
{
std::cout << "DCC wrote " << bytes_transferred << " bytes." << std::endl;
if (!ec) return; // No errors.
std::cerr << fmt::format("'- DCC write error: {} ({})", SystemErrorToIRCError(ec), ec.message()) << std::endl;
}
DCCPriv(DCC& super_, IRCBase& ircctx_, DCC::Direction dir_, asio::io_context& ioctx_, const std::string& ip_, const std::string& port_)
: super(super_)
, ircctx(ircctx_)
, direction(dir_)
, ioctx(ioctx_)
, ip(ip_)
, port(port_)
, resolver(ioctx_)
, acceptor(ioctx_)
, endpoint(tcp::v4(), std::stoi(port_))
, socket(ioctx_)
, readbuf(16384)
{
}
DCC& super;
IRCBase& ircctx;
bool pending{ true };
const DCC::Direction direction;
asio::io_context& ioctx;
std::string ip; // Only used when Direction=Target, and describes the Initiator's IP.
std::string port;
tcp::resolver resolver; // for outbound connection (we act as a Target)
tcp::acceptor acceptor; // for inbound connection (we act as an Initiator)
tcp::resolver::results_type resolverResults;
asio::ip::tcp::endpoint endpoint;
tcp::socket socket;
DCC::ByteString readbuf;
DCC::CallbackRead cbRead;
DCC::CallbackConnected cbCon;
DCC::CallbackDisconnected cbDiscon;
void connected(const asio::error_code& ec)
{
super.onConnected();
read(ec);
}
void disconnected(const asio::error_code& ec)
{
try {
socket.close();
super.onDisconnected(SystemErrorToIRCError(ec));
}
catch (...) {} // Might already be closed, and so we don't care about any exceptions.
}
void read(const asio::error_code& ec)
{
socket.async_receive(asio::buffer(readbuf.data(), readbuf.size()),
[this](const asio::error_code& ec, std::size_t rl) {
if (!ec) {
std::cout << "DCC read " << rl << " bytes." << std::endl;
super.onRead(readbuf, rl);
read(ec);
}
/* Operation aborted would happen during destruction and socket closing has already taken place. */
else if (ec != asio::error::operation_aborted) {
disconnected(ec);
}
});
}
void writeFinished(const asio::error_code& ec, std::size_t bytes_transferred)
{
std::cout << "DCC wrote " << bytes_transferred << " bytes." << std::endl;
if (!ec) return; // No errors.
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, const std::string& port)
: mp(std::make_unique<DCCPriv>(*this, ircctx, Direction::Initiator, ioctx, "", port))
DCC::DCC(IRCBase& ircctx, asio::io_context& ioctx)
: mp(std::make_unique<DCCPriv>(*this, ircctx, Direction::Initiator, ioctx, "", "0"))
{
mp->acceptor.open(mp->endpoint.protocol());
mp->acceptor.bind(mp->endpoint);
mp->acceptor.listen(1);
mp->acceptor.async_accept(mp->socket, [this](const asio::error_code& ec){
if (ec)
mp->disconnected(ec);
else
mp->connected(ec);
});
auto ipAddress = findOwnIpAddress();
mp->startListener(); // also acquires a port for us
mp->port = std::to_string(port());
if (isPrivateIp(ipAddress) && NATPortMapping::add(port(), port())) {
auto publicIp = NATPublicAddress::get();
if (publicIp.has_value()) {
ipAddress = longIpToNormal(*publicIp);
}
else {
NATPortMapping::remove(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");
}
}
mp->ip = ipAddress;
}
DCC::DCC(IRCBase& ircctx, asio::io_context& ioctx, const std::string& ip, const std::string& port)
: mp(std::make_unique<DCCPriv>(*this, ircctx, Direction::Target, ioctx, ip, port))
: mp(std::make_unique<DCCPriv>(*this, ircctx, Direction::Target, ioctx, ip, port))
{}
DCC::DCC(DCC&& other) noexcept
: mp(std::move(other.mp))
: mp(std::move(other.mp))
{}
DCC::~DCC()
{
try {
if (mp->socket.is_open()) {
mp->socket.shutdown(asio::socket_base::shutdown_both);
mp->socket.close();
}
if (mp->direction == Direction::Initiator)
mp->acceptor.close();
}
catch(const asio::system_error& e) {
// Properly log e.what() or e.code() maybe?
std::cerr << fmt::format("~DCC() caught exception ({}): {}\n IP={}, Port={}, Direction={}",
e.code().value(), e.what(), mp->ip, mp->port, mp->direction) << std::endl;
}
NATPortMapping::remove(port(), port());
disconnect();
}
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)
return IRCError::DCC_NotATarget; // Only usable if we are a Target...
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&) {
if (ec)
mp->disconnected(ec);
else
mp->connected(ec);
});
if (mp->direction == Direction::Initiator)
return IRCError::DCC_NotATarget; // Only usable if we are a Target...
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&) {
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(port(), port())) {
auto publicIp = NATPublicAddress::get();
if (publicIp.has_value())
ipAddress = longIpToNormal(*publicIp);
else
NATPortMapping::remove(port(), port());
}
mp->ircctx.ctcpRequest(targetNickname, "DCC", fmt::format("CHAT chat {} {}",
normalIpToLong(ipAddress),
port()));
mp->pending = false;
return IRCError::NoError;
};
if (mp->port == "0")
return acceptReversed();
else
return acceptNormally();
}
mp->pending = false;
return IRCError::NoError;
void DCC::disconnect()
{
try {
if (mp->socket.is_open()) {
mp->socket.shutdown(asio::socket_base::shutdown_both);
mp->socket.close();
}
if (mp->direction == Direction::Initiator)
mp->acceptor.close();
}
catch(const asio::system_error& e) {
std::cerr << fmt::format("DCC::disconnect() caught exception ({}): {}\n IP={}, Port={}, Direction={}",
e.code().value(), e.what(), mp->ip, mp->port, static_cast<int>(mp->direction)) << std::endl;
}
}
DCC::Direction DCC::direction() const
{
return mp->direction;
return mp->direction;
}
void DCC::write(const ByteString& data)
{
mp->socket.async_send(asio::buffer(data),
[this](const asio::error_code& ec, std::size_t bytes_transferred){
mp->writeFinished(ec, bytes_transferred);
});
mp->socket.async_send(asio::buffer(data),
[this](const asio::error_code& ec, std::size_t bytes_transferred){
mp->writeFinished(ec, bytes_transferred);
});
}
IRCBase& DCC::context()
{
return mp->ircctx;
return mp->ircctx;
}
const IRCBase& DCC::context() const
{
return mp->ircctx;
return mp->ircctx;
}
void DCC::callbackRead(DCC::CallbackRead&& cb)
bool DCC::isConnected() const
{
mp->cbRead = std::move(cb);
return mp->socket.is_open();
}
void DCC::callbackConnected(DCC::CallbackConnected&& cb)
uint16_t DCC::port() const
{
mp->cbCon = std::move(cb);
if (mp->port.empty() || mp->port == "0")
return mp->acceptor.local_endpoint().port();
else
return std::stoi(mp->port);
}
void DCC::callbackDisconnected(DCC::CallbackDisconnected&& cb)
const std::string& DCC::ip() const
{
mp->cbDiscon = std::move(cb);
return mp->ip;
}
bool DCC::isReversed() const
{
return mp->port == "0";
}
void DCC::onRead(std::istream& is)
void DCC::setCallbackRead(DCC::CallbackRead&& cb)
{
if (mp->cbRead)
mp->cbRead(is);
mp->cbRead = std::move(cb);
}
void DCC::setCallbackConnected(DCC::CallbackConnected&& cb)
{
mp->cbCon = std::move(cb);
}
void DCC::setCallbackDisconnected(DCC::CallbackDisconnected&& cb)
{
mp->cbDiscon = std::move(cb);
}
void DCC::onRead(const DCC::ByteString& data, std::size_t length)
{
if (mp->cbRead)
mp->cbRead(data, length);
}
void DCC::onConnected()
{
if (mp->cbCon)
mp->cbCon();
if (mp->cbCon)
mp->cbCon();
}
void DCC::onDisconnected(IRCError e)
{
if (mp->cbDiscon)
mp->cbDiscon(e);
if (mp->cbDiscon)
mp->cbDiscon(e);
}
void DCC::resetCallbacks()
{
mp->cbRead = {};
mp->cbCon = {};
mp->cbDiscon = {};
}
DCC::ByteString DCC::stringToByteString(const std::string& input)
{
std::basic_string<std::byte> temp(reinterpret_cast<const std::byte*>(input.data()), input.size());
DCC::ByteString output(temp.begin(), temp.end());
return output;
}

@ -9,9 +9,9 @@
#define DCC_H
#include "IRCError.h"
#include <string>
#include <memory>
#include <asio.hpp>
#include <vector>
#include <memory>
using asio::ip::tcp;
@ -20,61 +20,84 @@ class IRCBase;
struct DCCPriv;
class DCC
{
friend struct IRCBasePriv;
friend struct DCCPriv;
friend struct IRCBasePriv;
friend struct DCCPriv;
public:
using CallbackRead = std::function<void(std::istream&)>;
using CallbackConnected = std::function<void()>;
using CallbackDisconnected = std::function<void(IRCError e)>;
using ByteString = std::basic_string<std::byte>;
using ByteString = std::vector<std::byte>;
using CallbackRead = std::function<void(const ByteString&, std::size_t)>;
using CallbackConnected = std::function<void()>;
using CallbackDisconnected = std::function<void(IRCError e)>;
/*
* Initiator acts as the server, and the target is the client
*/
enum class Direction {
Initiator,
Target
};
virtual ~DCC();
DCC(DCC&& other) noexcept;
//! Constructs an Initiator (server)
DCC(IRCBase& ircctx, asio::io_context& ioctx);
//! Constructs a Target (client)
DCC(IRCBase& ircctx, asio::io_context& ioctx, const std::string& ip, const std::string& port);
[[nodiscard]] bool isPending() const;
IRCError accept(const std::string& targetNickname);
void disconnect();
[[nodiscard]] Direction direction() const;
/*
* Initiator acts as the server, and the target is the client
*/
enum class Direction {
Initiator,
Target
};
void write(const ByteString& data);
~DCC();
DCC(DCC&& other) noexcept;
IRCBase& context();
const IRCBase& context() const;
//! Constructs an Initiator (server)
DCC(IRCBase& ircctx, asio::io_context& ioctx, const std::string& port);
bool isConnected() const;
//! Constructs a Target (client)
DCC(IRCBase& ircctx, asio::io_context& ioctx, const std::string& ip, const std::string& port);
/*
* As initiator: Port to listen on.
* Reversed initiator: Always zero.
* As target: Port to connect to.
*/
uint16_t port() const;
[[nodiscard]] bool isPending() const;
IRCError accept();
/*
* 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;
[[nodiscard]] Direction direction() const;
bool isReversed() const;
void write(const ByteString& data);
/*
* Callbacks used by 'users' of this class.
*/
void setCallbackRead(CallbackRead&& cb);
void setCallbackConnected(CallbackConnected&& cb);
void setCallbackDisconnected(CallbackDisconnected&& cb);
IRCBase& context();
const IRCBase& context() const;
void resetCallbacks();
/*
* Callbacks used by 'users' of this class.
*/
void callbackRead(CallbackRead&& cb);
void callbackConnected(CallbackConnected&& cb);
void callbackDisconnected(CallbackDisconnected&& cb);
static ByteString stringToByteString(const std::string& input);
protected:
/*
* Use these to read data in a custom derivative of DCC.
* If you re-implement any of these, make sure to forward the
* calls to this base class at the end of your function.
*/
virtual void onRead(std::istream& is);
virtual void onConnected();
virtual void onDisconnected(IRCError e);
/*
* Use these to read data in a custom derivative of DCC.
* If you re-implement any of these, make sure to forward the
* calls to this base class at the end of your function.
*/
virtual void onRead(const ByteString& data, std::size_t length);
virtual void onConnected();
virtual void onDisconnected(IRCError e);
private:
std::unique_ptr<DCCPriv> mp;
std::unique_ptr<DCCPriv> mp;
};
#endif // DCC_H

File diff suppressed because it is too large Load Diff

@ -10,6 +10,7 @@
#include "IRCError.h"
#include "IRCPrefix.h"
#include "IRCMessage.h"
#include <string>
#include <vector>
@ -25,61 +26,64 @@ class DCC;
struct IRCBasePriv;
class IRCBase
{
friend struct IRCBasePriv;
friend struct IRCBasePriv;
public:
IRCBase();
~IRCBase();
IRCBase();
~IRCBase();
IRCBase(IRCBase&& other);
IRCBase(const IRCBase&) = delete;
IRCBase& operator=(const IRCBase&) = delete;
IRCBase(IRCBase&& other) noexcept;
IRCBase(const IRCBase&) = delete;
IRCBase& operator=(const IRCBase&) = delete;
bool poll();
bool poll();
[[nodiscard]] const std::string& getHostname() const;
IRCError setHostname(const std::string& hostname, bool SSL = false);
[[nodiscard]] const std::string& getHostname() const;
IRCError setHostname(const std::string& hostname, bool SSL = false);
[[nodiscard]] const std::string& getPort() const;
IRCError setPort(const std::string& port);
[[nodiscard]] const std::string& getPort() const;
IRCError setPort(const std::string& port);
[[nodiscard]] const std::string& getRealname() const;
IRCError setRealname(const std::string& realname);
[[nodiscard]] const std::string& getRealname() const;
IRCError setRealname(const std::string& realname);
[[nodiscard]] const std::string& getIdent() const;
IRCError setIdent(const std::string& ident);
[[nodiscard]] const std::string& getIdent() const;
IRCError setIdent(const std::string& ident);
[[nodiscard]] const std::string& getNickname() const;
void setNickname(const std::string& nickname);
[[nodiscard]] const std::string& getNickname() const;
void setNickname(const std::string& nickname);
[[nodiscard]] const std::string& getPassword() const;
void setPassword(const std::string& password);
[[nodiscard]] const std::string& getPassword() const;
void setPassword(const std::string& password);
void exceptSSL_SelfSigned(bool except);
void exceptSSL_CNMismatch(bool except);
void exceptSSL_Expired(bool except);
[[nodiscard]] const std::string& getV3SASL() const;
void setV3SASL(const std::string& credentials);
void command(const std::string& command, const std::vector<std::string>& args, const std::string& msg = "");
void command(const std::string& command, const std::string& msg);
void raw(const std::string& data);
void exceptSSL_SelfSigned(bool except);
void exceptSSL_CNMismatch(bool except);
void exceptSSL_Expired(bool except);
void ctcpRequest(const std::string& target, const std::string& command, const std::string& message = "");
void command(const std::string& command, const std::vector<std::string>& args, const std::string& msg = "");
void command(const std::string& command, const std::string& msg);
void raw(const std::string& data);
void ctcpRequest(const std::string& target, const std::string& command, const std::string& message = "");
void ctcpResponse(const std::string& target, const std::string& command, const std::string& message = "");
//! Set to >0 to enable, =0 to disable.
void setManualKeepalive(std::chrono::seconds freq = std::chrono::seconds(30));
//! Set to >0 to enable, =0 to disable.
void setManualKeepalive(std::chrono::seconds freq = std::chrono::seconds(30));
[[nodiscard]] std::chrono::milliseconds getManualKeepaliveFreq() const;
[[nodiscard]] std::chrono::milliseconds getManualKeepaliveFreq() const;
[[nodiscard]] asio::io_context& getIOCTX();
[[nodiscard]] asio::io_context& getIOCTX();
/**
* Takes a string of modes (letters like ohv), in their intended order
* and converts each letter to its corresponding display prefix (ie. @%+).
* For mode (letters) that isn't valid, a colon ':' is placed in-stead (since
* those can't naturally exist with the IRC protocol.)
*/
[[nodiscard]] std::string toMemberPrefix(const std::string& modes) const;
/**
* Takes a string of modes (letters like ohv), in their intended order
* and converts each letter to its corresponding display prefix (ie. @%+).
* For mode (letters) that isn't valid, a colon ':' is placed in-stead (since
* those can't naturally exist with the IRC protocol.)
*/
[[nodiscard]] std::string toMemberPrefix(const std::string& modes) const;
bool isChannelSymbol(char c);
@ -97,81 +101,91 @@ public:
*/
[[nodiscard]] char channelModeGroup(char m) const;
[[nodiscard]] IRCError tryConnect();
[[nodiscard]] IRCError disconnectFromServer(const std::string& quitMessage = "");
[[nodiscard]] IRCError lastErrorCode() const;
[[nodiscard]] IRCError tryConnect();
[[nodiscard]] IRCError disconnectFromServer(const std::string& quitMessage = "");
[[nodiscard]] IRCError lastErrorCode() const;
[[nodiscard]] bool isOnline() const;
[[nodiscard]] bool isConnected() const;
[[nodiscard]] bool isSSL() const;
[[nodiscard]] bool isOnline() const;
[[nodiscard]] bool isConnected() const;
[[nodiscard]] bool isSSL() const;
[[nodiscard]] static const std::vector<std::string>& clientV3Support();
[[nodiscard]] const std::vector<std::string>& enabledV3Support() const;
[[nodiscard]] const std::vector<std::string>& registeredV3Support() const;
[[nodiscard]] const std::vector<std::string>& serverV3Support() const;
[[nodiscard]] const std::unordered_map<std::string,std::string>& isupport() const;
[[nodiscard]] static const std::vector<std::string>& clientV3Support();
[[nodiscard]] const std::vector<std::string>& registeredV3Support() const;
[[nodiscard]] const std::vector<std::string>& serverV3Support() const;
[[nodiscard]] const std::unordered_map<std::string,std::string>& isupport() const;
[[nodiscard]] const std::vector<std::shared_ptr<IRCChannel>>& channels() const;
[[nodiscard]] std::shared_ptr<IRCChannel> getChannel(const std::string& name) const;
[[nodiscard]] std::shared_ptr<IRCMember> getMember(const std::string& nickname) const;
[[nodiscard]] const std::vector<std::shared_ptr<IRCChannel>>& channels() const;
[[nodiscard]] std::shared_ptr<IRCChannel> getChannel(const std::string& name) const;
[[nodiscard]] std::shared_ptr<IRCMember> getMember(const std::string& nickname) const;
std::pair<std::shared_ptr<DCC>, IRCError> initiateDCC(); // Creates a DCC initiator (tcp server)
std::pair<std::shared_ptr<DCC>, IRCError> initiateDCC(const std::string& port); // Creates a DCC initiator (tcp server)
IRCError declineDCC(std::shared_ptr<DCC> dcc);
[[nodiscard]] const IRCMessage* getCurrentMessageRaw() const;
/**
* Uses a set of IRCv3 features. The keys are strings as described by the standard (eg. account-notify, away-notify, etc.)
* If a feature given is not actually supported by the client, it's ignored. See 'V3Support' in IRCBasePriv.h.
* By default, if never called, all supported features are enabled. Persists between server-connections.
* To re-enable all features, pass clientV3Support() as an argument.
*/
void useIRCv3Features(const std::vector<std::string>& features);
protected:
virtual void onConnected() {}
virtual void onConnectedWithSSLExceptions(const std::vector<IRCError>& codes) {}
virtual void onDisconnected() {}
virtual void onRegistered() {}
virtual void onConnectionError(IRCError e) {}
/*
* To correctly implement and get all data from a standard IRC server,
* the following virtuals must be implemented.
*/
virtual void onMsgNick(const IRCPrefix& sender, const std::string& newNickname, const std::vector<std::string>& channelsAffected) = 0;
virtual void onMsgMode(const IRCPrefix& sender, const std::string& target, const std::string& modes, const std::vector<std::string>& args) = 0;
virtual void onMsgQuit(const IRCPrefix& sender, const std::string& message, const std::vector<std::string>& channelsAffected) = 0;
virtual void onMsgJoin(const IRCPrefix& sender, const std::string& target) = 0;
virtual void onMsgPart(const IRCPrefix& sender, const std::string& target, const std::string& message) = 0;
virtual void onMsgTopic(const IRCPrefix& sender, const std::string& target, const std::string& topic) = 0;
virtual void onMsgInvite(const IRCPrefix& sender, const std::string& target) = 0;
virtual void onMsgKick(const IRCPrefix& sender, const std::string& target, const std::string& who, const std::string& reason) = 0;
virtual void onMsgPrivmsg(const IRCPrefix& sender, const std::string& target, const std::string& message) = 0;
virtual void onMsgNotice(const IRCPrefix& sender, const std::string& target, const std::string& message) = 0;
virtual void onMsgKill(const IRCPrefix& sender, const std::string& reason) = 0;
virtual void onMsgPing(const std::string& message) = 0;
virtual void onMsgPong(const std::string& message) = 0;
virtual void onMsgError(const std::string& message) = 0;
virtual void onMsgWallops(const IRCPrefix& sender, const std::string& message) = 0;
virtual void onMsgNumeric(const IRCPrefix& sender, const std::string& num, const std::vector<std::string>& args, const std::string& message) = 0;
/*
* CTCP/DCC isn't really a part of the IRC standard so these are optional.
*/
virtual void onMsgCTCPRequest(const IRCPrefix& sender, const std::string& target, const std::string& command, const std::string& message) {};
virtual void onMsgCTCPResponse(const IRCPrefix& sender, const std::string& target, const std::string& command, const std::string& message) {};
virtual void onMsgDCCRequest(std::shared_ptr<DCC> dcc, const IRCPrefix& sender, const std::string& target, const std::string& type, const std::string& message) {};
/*
* Optional IRCv3 extensions to implement.
*/
virtual void v3onMsgAway(const IRCPrefix& sender, const std::string& message, const std::vector<std::string>& channelsAffected) {};
virtual void v3onMsgAccountLogin(const IRCPrefix& sender, const std::string& useraccount) {}
virtual void v3onMsgAccountLogout(const IRCPrefix& sender) {}
/*
* Required IRCv3 extensions to implement.
*/
virtual void v3onMsgJoin(const IRCPrefix& sender, const std::string& channel, const std::string& useraccount, const std::string& realname) = 0;
/*
* Catch-all handler. If the message parser cannot match for any of the commands, it will turn up here.
* Optional to implement.
*/
virtual void onMsgUnhandled(const IRCPrefix& sender, const std::string& command, const std::vector<std::string>& args, const std::string& message) {}
virtual void onConnected() {}
virtual void onConnectedWithSSLExceptions(const std::vector<IRCError>& codes) {}
virtual void onDisconnected() {}
virtual void onRegistered() {}
virtual void onConnectionError(IRCError e) {}
/*
* To correctly implement and get all data from a standard IRC server,
* the following virtuals must be implemented.
*/
virtual void onMsgNick(const IRCPrefix& sender, const std::string& newNickname, const std::vector<std::string>& channelsAffected) = 0;
virtual void onMsgMode(const IRCPrefix& sender, const std::string& target, const std::string& modes, const std::vector<std::string>& args) = 0;
virtual void onMsgQuit(const IRCPrefix& sender, const std::string& message, const std::vector<std::string>& channelsAffected) = 0;
virtual void onMsgJoin(const IRCPrefix& sender, const std::string& target) = 0;
virtual void onMsgPart(const IRCPrefix& sender, const std::string& target, const std::string& message) = 0;
virtual void onMsgTopic(const IRCPrefix& sender, const std::string& target, const std::string& topic) = 0;
virtual void onMsgInvite(const IRCPrefix& sender, const std::string& target) = 0;
virtual void onMsgKick(const IRCPrefix& sender, const std::string& target, const std::string& who, const std::string& reason) = 0;
virtual void onMsgPrivmsg(const IRCPrefix& sender, const std::string& target, const std::string& message) = 0;
virtual void onMsgNotice(const IRCPrefix& sender, const std::string& target, const std::string& message) = 0;
virtual void onMsgKill(const IRCPrefix& sender, const std::string& reason) = 0;
virtual void onMsgPing(const std::string& message) = 0;
virtual void onMsgPong(const std::string& message) = 0;
virtual void onMsgError(const std::string& message) = 0;
virtual void onMsgWallops(const IRCPrefix& sender, const std::string& message) = 0;
virtual void onMsgNumeric(const IRCPrefix& sender, const std::string& num, const std::vector<std::string>& args, const std::string& message) = 0;
/*
* CTCP/DCC isn't really a part of the IRC standard so these are optional.
*/
virtual void onMsgCTCPRequest(const IRCPrefix& sender, const std::string& target, const std::string& command, const std::string& message) {};
virtual void onMsgCTCPResponse(const IRCPrefix& sender, const std::string& target, const std::string& command, const std::string& message) {};
virtual void onMsgDCCRequest(std::shared_ptr<DCC> dcc, const IRCPrefix& sender, const std::string& target, const std::string& type, const std::string& message) {};
/*
* Optional IRCv3 extensions to implement.
*/
virtual void v3onMsgAway(const IRCPrefix& sender, const std::string& message, const std::vector<std::string>& channelsAffected) {};
virtual void v3onMsgAccountLogin(const IRCPrefix& sender, const std::string& useraccount) {}
virtual void v3onMsgAccountLogout(const IRCPrefix& sender) {}
/*
* Required IRCv3 extensions to implement.
*/
virtual void v3onMsgJoin(const IRCPrefix& sender, const std::string& channel, const std::string& useraccount, const std::string& realname) = 0;
/*
* Catch-all handler. If the message parser cannot match for any of the commands, it will turn up here.
* Optional to implement.
*/
virtual void onMsgUnhandled(const IRCPrefix& sender, const std::string& command, const std::vector<std::string>& args, const std::string& message) {}
private:
std::unique_ptr<IRCBasePriv> mp;
std::unique_ptr<IRCBasePriv> mp;
};
#endif // IRCBASE_H

@ -0,0 +1,162 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#ifndef IRCBASEPRIV_H
#define IRCBASEPRIV_H
#include "Numeric.h"
#include "IRCError.h"
#include "IRCPrefix.h"
#include "IRCMember.h"
#include "IRCChannel.h"
#include "IRCMessage.h"
#include "DCC.h"
#include <asio.hpp>
#include <asio/ssl.hpp>
#include <ctime>
namespace {
/* Debug/development options */
constexpr bool DumpReadData = false;
constexpr bool DumpWriteData = false;
const std::vector<std::string> V3Support {
"account-notify",
"extended-join",
"away-notify",
"invite-notify",
"multi-prefix",
"userhost-in-names",
"message-tags",
"server-time",
"sasl"
};
} // end anonymous namespace
class IRCBase;
using asio::ip::tcp;
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 v3SASL;
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.
bool saslInProgress{ false }; // Used during server registration
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.
IRCMessage* currentMessage{ nullptr };
std::vector<std::string> ircv3enabled;
/*
* Various helper functions for the IRC client implementation.
*/
char channelModeGroup(char m) const;
bool isChannelSymbol(char c);
bool isMemberPrivilegeSymbol(char m) const;
char memberPrivilegeSymbolToMode(char s) const;
void setDefaults();
void startKeepaliveTimer();
void stopKeepaliveTimer();
void keepaliveTimeout(const asio::error_code& ec);
void disconnectHandler();
void read(const asio::error_code& ec, std::size_t /*size*/);
void connected(const asio::error_code& /*ec*/);
void write(const std::string& command, const std::vector<std::string>& args, const std::string& msg);
void writeNoMsg(const std::string& command, const std::vector<std::string>& args);
void write(const std::string& command, const std::string& msg);
void ctcp(const std::string& messageType, const std::string& target, const std::string& command, const std::string& message = "");
void addMemberToChannel(const IRCPrefix& prefix, const std::string& channel);
void delMemberFromChannel(const IRCPrefix& prefix, const std::string& channel);
IRCError verify_X509(X509* cert);
void parseChannelModeMessage(std::shared_ptr<IRCChannel> chan, const std::string& modes, const std::vector<std::string>& args) const;
void parseIncoming(const std::string& line);
/*
* Message handlers for the various messages we receive from the IRC server.
*/
void handleNumeric(const IRCMessage& ircmessage);
void cmdNick(const IRCMessage& ircmessage);
void cmdMode(const IRCMessage& ircmessage);
void cmdQuit(const IRCMessage& ircmessage);
void cmdJoin(const IRCMessage& ircmessage);
void cmdPart(const IRCMessage& ircmessage);
void cmdTopic(const IRCMessage& ircmessage);
void cmdInvite(const IRCMessage& ircmessage);
void cmdKick(const IRCMessage& ircmessage);
void cmdPrivmsg(const IRCMessage& ircmessage);
void cmdNotice(const IRCMessage& ircmessage);
void cmdKill(const IRCMessage& ircmessage);
void cmdPing(const IRCMessage& ircmessage);
void cmdPong(const IRCMessage& ircmessage);
void cmdError(const IRCMessage& ircmessage);
void cmdWallops(const IRCMessage& ircmessage);
void cmdV3Cap(const IRCMessage& ircmessage);
void cmdV3Away(const IRCMessage& ircmessage);
void cmdV3Account(const IRCMessage& ircmessage);
void cmdV3Authenticate(const IRCMessage& ircmessage);
void handleUnhandled(const IRCMessage& ircmessage);
};
#endif // IRCBASEPRIV_H

@ -11,24 +11,24 @@
#include <algorithm>
IRCChannel::IRCChannel(const std::string& name, IRCBase& owner)
: m_name(name)
, m_owner(&owner)
: m_name(name)
, m_owner(&owner)
{}
const std::string& IRCChannel::name() const
{
return m_name;
return m_name;
}
std::optional<IRCMemberEntryRef> IRCChannel::getMember(const std::string& nickname)
{
auto it = std::find_if(m_members.begin(), m_members.end(),
[&nickname](const IRCMemberEntry& entry){
return entry.member()->prefix().nickname() == nickname;
});
auto it = std::find_if(m_members.begin(), m_members.end(),
[&nickname](const IRCMemberEntry& entry){
return entry.member()->prefix().nickname() == nickname;
});
return it == m_members.end() ? std::nullopt
: std::make_optional<IRCMemberEntryRef>(*it);
return it == m_members.end() ? std::nullopt
: std::make_optional<IRCMemberEntryRef>(*it);
}
IRCMemberEntry& IRCChannel::addMember(std::shared_ptr<IRCMember> member)
@ -48,55 +48,55 @@ IRCMemberEntry& IRCChannel::addMember(std::shared_ptr<IRCMember> member)
void IRCChannel::delMember(std::shared_ptr<IRCMember> member)
{
auto newEnd = std::remove_if(m_members.begin(), m_members.end(),
auto newEnd = std::remove_if(m_members.begin(), m_members.end(),
[&member](const IRCMemberEntry& e){
return member->prefix() == e.member()->prefix();
return member->prefix() == e.member()->prefix();
});
m_members.erase(newEnd, m_members.end());
m_members.erase(newEnd, m_members.end());
}
const std::vector<IRCMemberEntry>& IRCChannel::members() const
{
return m_members;
return m_members;
}
void IRCChannel::setTopic(const std::string& topic)
{
m_topic = topic;
m_topic = topic;
}
const std::string& IRCChannel::topic() const
{
return m_topic;
return m_topic;
}
const std::unordered_map<char, std::string>& IRCChannel::modes() const
{
return m_modes;
return m_modes;
}
void IRCChannel::setMode(char m, const std::string& parameter)
{
/*
* Note that if a mode requiring an argument is already set,
* the server will always send an 'unset' before setting a new value.
* But let's use insert_or_assign anyway.
*/
m_modes.insert_or_assign(m, parameter);
/*
* Note that if a mode requiring an argument is already set,
* the server will always send an 'unset' before setting a new value.
* But let's use insert_or_assign anyway.
*/
m_modes.insert_or_assign(m, parameter);
}
void IRCChannel::delMode(char m)
{
m_modes.erase(m);
m_modes.erase(m);
}
bool IRCChannel::isPopulating() const
{
return m_populating;
return m_populating;
}
void IRCChannel::donePopulating()
{
m_populating = false;
m_populating = false;
}

@ -23,33 +23,33 @@ using IRCMemberEntryRef = std::reference_wrapper<IRCMemberEntry>;
class IRCChannel
{
public:
IRCChannel(const std::string& name, IRCBase& owner);
~IRCChannel() = default;
IRCChannel(const std::string& name, IRCBase& owner);
~IRCChannel() = default;
const std::string& name() const;
const std::string& name() const;
std::optional<IRCMemberEntryRef> getMember(const std::string& nickname);
IRCMemberEntry& addMember(std::shared_ptr<IRCMember> member);
void delMember(std::shared_ptr<IRCMember> member);
const std::vector<IRCMemberEntry>& members() const;
std::optional<IRCMemberEntryRef> getMember(const std::string& nickname);
IRCMemberEntry& addMember(std::shared_ptr<IRCMember> member);
void delMember(std::shared_ptr<IRCMember> member);
const std::vector<IRCMemberEntry>& members() const;
void setTopic(const std::string& topic);
const std::string& topic() const;
void setTopic(const std::string& topic);
const std::string& topic() const;
const std::unordered_map<char, std::string>& modes() const;
void setMode(char m, const std::string& parameter = "");
void delMode(char m);
const std::unordered_map<char, std::string>& modes() const;
void setMode(char m, const std::string& parameter = "");
void delMode(char m);
bool isPopulating() const;
void donePopulating();
bool isPopulating() const;
void donePopulating();
private:
std::string m_name;
std::vector<IRCMemberEntry> m_members;
std::string m_topic;
std::unordered_map<char, std::string> m_modes;
bool m_populating{ true };
IRCBase* m_owner; // TODO use reference?
std::string m_name;
std::vector<IRCMemberEntry> m_members;
std::string m_topic;
std::unordered_map<char, std::string> m_modes;
bool m_populating{ true };
IRCBase* m_owner; // TODO use reference?
};

@ -7,106 +7,123 @@
#include "IRCError.h"
#include <iostream>
#include <fmt/format.h>
#include <asio/error.hpp>
int lastAsioErrorCode{ 0 };
std::string lastAsioErrorMessage;
std::string IRCErrorToString(IRCError e)
{
switch (e) {
case IRCError::NoError:
return "No error";
case IRCError::NotConnected:
return "Not connected";
case IRCError::AlreadyConnected:
return "Already connected";
case IRCError::CannotResolveAddress:
return "Cannot resolve address";
case IRCError::BrokenPipe:
return "Broken pipe";
case IRCError::ConnectionAborted:
return "Connection aborted";
case IRCError::ConnectionRefused:
return "Connection refused";
case IRCError::ConnectionReset:
return "Connection reset";
case IRCError::HostUnreachable:
return "Host unreachable";
case IRCError::NetworkDown:
return "Network down";
case IRCError::NetworkReset:
return "Network reset";
case IRCError::NetworkUnreachable:
return "Network unreachable";
case IRCError::NoDescriptors:
return "No file descriptors left";
case IRCError::TimedOut:
return "Timed out";
case IRCError::EndOfFile:
return "EOF";
case IRCError::SSL_SelfSigned:
return "SSL: Self-signed certificate";
case IRCError::SSL_CN_Mismatch:
return "SSL: (CN) Hostname mismatch";
case IRCError::SSL_CN_Missing:
return "SSL: (CN) Hostname missing";
case IRCError::SSL_CN_WildcardIllegal:
return "SSL: (CN) Illegal wildcard usage";
case IRCError::SSL_NotYetValid:
return "SSL: Certificate not yet valid";
case IRCError::SSL_Expired:
return "SSL: Certificate expired";
case IRCError::CannotChangeWhenConnected:
return "Cannot change this setting when connected";
case IRCError::HostNotSet:
return "Hostname is not set";
case IRCError::PortNotSet:
return "Port number is not set";
case IRCError::IdentNotSet:
return "Ident/username is not set";
case IRCError::NicknameNotSet:
return "Nickname is not set";
case IRCError::RealnameNotSet:
return "Real name is not set";
case IRCError::DCC_NotATarget:
return "DCC: We are not a DCC target";
case IRCError::DCC_TimedOut:
return "DCC: Request timed out";
case IRCError::UnhandledException:
return "Unhandled exception";
}
switch (e) {
case IRCError::NoError:
return "No error";
case IRCError::NotConnected:
return "Not connected";
case IRCError::AlreadyConnected:
return "Already connected";
case IRCError::CannotResolveAddress:
return "Cannot resolve address";
case IRCError::BrokenPipe:
return "Broken pipe";
case IRCError::ConnectionAborted:
return "Connection aborted";
case IRCError::ConnectionRefused:
return "Connection refused";
case IRCError::ConnectionReset:
return "Connection reset";
case IRCError::HostUnreachable:
return "Host unreachable";
case IRCError::NetworkDown:
return "Network down";
case IRCError::NetworkReset:
return "Network reset";
case IRCError::NetworkUnreachable:
return "Network unreachable";
case IRCError::NoDescriptors:
return "No file descriptors left";
case IRCError::TimedOut:
return "Timed out";
case IRCError::EndOfFile:
return "EOF";
case IRCError::SSL_SelfSigned:
return "SSL: Self-signed certificate";
case IRCError::SSL_CN_Mismatch:
return "SSL: (CN) Hostname mismatch";
case IRCError::SSL_CN_Missing:
return "SSL: (CN) Hostname missing";
case IRCError::SSL_CN_WildcardIllegal:
return "SSL: (CN) Illegal wildcard usage";
case IRCError::SSL_NotYetValid:
return "SSL: Certificate not yet valid";
case IRCError::SSL_Expired:
return "SSL: Certificate expired";
case IRCError::CannotChangeWhenConnected:
return "Cannot change this setting when connected";
case IRCError::HostNotSet:
return "Hostname is not set";
case IRCError::PortNotSet:
return "Port number is not set";
case IRCError::IdentNotSet:
return "Ident/username is not set";
case IRCError::NicknameNotSet:
return "Nickname is not set";
case IRCError::RealnameNotSet:
return "Real name is not set";
case IRCError::DCC_NotATarget:
return "DCC: We are not a DCC target";
case IRCError::DCC_TimedOut:
return "DCC: Request timed out";
case IRCError::DCC_InitiateError:
return "DCC: Unable to initiate";
case IRCError::UnhandledException:
return "Unhandled exception";
}
/*
* Compiler complains even if all cases are covered...
* I don't wanna use a 'default' so that my IDE and tools can pick up on missing enumerations.
* A return here would suffice.
*/
return fmt::format("Uncaught error code {}", static_cast<int>(e));
}
IRCError SystemErrorToIRCError(const std::system_error& e)
{
switch (e.code().value()) {
case 0:
return IRCError::NoError;
case asio::error::host_not_found:
return IRCError::CannotResolveAddress;
case asio::error::broken_pipe:
return IRCError::BrokenPipe;
case asio::error::connection_aborted:
return IRCError::ConnectionAborted;
case asio::error::connection_refused:
return IRCError::ConnectionRefused;
case asio::error::connection_reset:
return IRCError::ConnectionReset;
case asio::error::host_unreachable:
return IRCError::HostUnreachable;
case asio::error::network_down:
return IRCError::NetworkDown;
case asio::error::network_reset:
return IRCError::NetworkReset;
case asio::error::network_unreachable:
return IRCError::NetworkUnreachable;
case asio::error::no_descriptors:
return IRCError::NoDescriptors;
case asio::error::timed_out:
return IRCError::TimedOut;
case asio::error::eof:
return IRCError::EndOfFile;
default:
std::cerr << fmt::format("Unhandled exception {}: {}", e.code().value(), e.code().message()) << std::endl;
return IRCError::UnhandledException;
}
switch (e.code().value()) {
case 0:
return IRCError::NoError;
case asio::error::host_not_found:
return IRCError::CannotResolveAddress;
case asio::error::broken_pipe:
return IRCError::BrokenPipe;
case asio::error::connection_aborted:
return IRCError::ConnectionAborted;
case asio::error::connection_refused:
return IRCError::ConnectionRefused;
case asio::error::connection_reset:
return IRCError::ConnectionReset;
case asio::error::host_unreachable:
return IRCError::HostUnreachable;
case asio::error::network_down:
return IRCError::NetworkDown;
case asio::error::network_reset:
return IRCError::NetworkReset;
case asio::error::network_unreachable:
return IRCError::NetworkUnreachable;
case asio::error::no_descriptors:
return IRCError::NoDescriptors;
case asio::error::timed_out:
return IRCError::TimedOut;
case asio::error::eof:
return IRCError::EndOfFile;
/*
* A 'default' here is needed since asio::error has a few errors I don't care about, or is able to care for...
*/
default:
lastAsioErrorCode = e.code().value();
lastAsioErrorMessage = e.code().message();
std::cerr << fmt::format("Unhandled exception {}: {}", lastAsioErrorCode, lastAsioErrorMessage) << std::endl;
return IRCError::UnhandledException;
}
}

@ -8,50 +8,63 @@
#ifndef IRCERROR_H
#define IRCERROR_H
#include <fmt/format.h>
#include <system_error>
#include <string>
extern int lastAsioErrorCode;
extern std::string lastAsioErrorMessage;
enum class IRCError
{
NoError,
NotConnected,
AlreadyConnected,
CannotResolveAddress,
BrokenPipe,
ConnectionAborted,
ConnectionRefused,
ConnectionReset,
HostUnreachable,
NetworkDown,
NetworkReset,
NetworkUnreachable,
NoDescriptors,
TimedOut,
EndOfFile,
SSL_SelfSigned,
SSL_CN_Mismatch,
SSL_CN_Missing,
SSL_CN_WildcardIllegal,
SSL_NotYetValid,
SSL_Expired,
CannotChangeWhenConnected,
HostNotSet,
PortNotSet,
IdentNotSet,
NicknameNotSet,
RealnameNotSet,
DCC_NotATarget,
DCC_TimedOut,
UnhandledException
NoError,
NotConnected,
AlreadyConnected,
CannotResolveAddress,
BrokenPipe,
ConnectionAborted,
ConnectionRefused,
ConnectionReset,
HostUnreachable,
NetworkDown,
NetworkReset,
NetworkUnreachable,
NoDescriptors,
TimedOut,
EndOfFile,
SSL_SelfSigned,
SSL_CN_Mismatch,
SSL_CN_Missing,
SSL_CN_WildcardIllegal,
SSL_NotYetValid,
SSL_Expired,
CannotChangeWhenConnected,
HostNotSet,
PortNotSet,
IdentNotSet,
NicknameNotSet,
RealnameNotSet,
DCC_NotATarget,
DCC_TimedOut,
DCC_InitiateError,
UnhandledException
};
std::string IRCErrorToString(IRCError e);
IRCError SystemErrorToIRCError(const std::system_error& e);
template <> struct fmt::formatter<IRCError> : formatter<string_view> {
template <typename FormatContext>
auto format(IRCError e, FormatContext& ctx) {
auto txt = IRCErrorToString(e);
return formatter<string_view>::format(txt, ctx);
}
};
#endif // IRCERROR_H

@ -7,54 +7,61 @@
#include "IRCMember.h"
#include "IRCChannel.h"
#include "Utilities.h"
#include <algorithm>
IRCMember::IRCMember(const IRCPrefix& prefix)
: m_prefix(prefix)
: m_prefix(prefix)
{}
IRCMember::IRCMember(const std::string& nickname)
: m_prefix(IRCPrefix::fromNickname(nickname))
: m_prefix(IRCPrefix::fromNickname(nickname))
{}
const IRCPrefix& IRCMember::prefix() const
{
return m_prefix;
return m_prefix;
}
void IRCMember::setPrefix(const IRCPrefix& prefix)
{
m_prefix = prefix;
m_prefix = prefix;
}
const std::vector<std::weak_ptr<IRCChannel>>& IRCMember::channels()
{
return m_channels;
return m_channels;
}
void IRCMember::addChannel(std::weak_ptr<IRCChannel> channel)
{
auto it = std::find_if(m_channels.begin(), m_channels.end(),
auto it = std::find_if(m_channels.begin(), m_channels.end(),
[channel](const std::weak_ptr<IRCChannel>& p){
return p.lock()->name() == channel.lock()->name();
});
if (p.expired())
return false;
else
return strEquals(p.lock()->name(), channel.lock()->name());
});
if (it == m_channels.end())
m_channels.emplace_back(channel);
if (it == m_channels.end())
m_channels.emplace_back(channel);
}
void IRCMember::delChannel(std::weak_ptr<IRCChannel> channel)
{
auto it = std::find_if(m_channels.begin(), m_channels.end(),
auto it = std::find_if(m_channels.begin(), m_channels.end(),
[channel](const std::weak_ptr<IRCChannel>& p){
return p.lock()->name() == channel.lock()->name();
if (p.expired())
return false;
else
return strEquals(p.lock()->name(), channel.lock()->name());
});
if (it != m_channels.end())
m_channels.erase(it);
m_channels.erase(it);
}
void IRCMember::setNickname(const std::string& nickname)
{
m_prefix.setNickname(nickname);
m_prefix.setNickname(nickname);
}

@ -18,20 +18,22 @@ class IRCChannel;
class IRCMember
{
public:
explicit IRCMember(const IRCPrefix& prefix);
explicit IRCMember(const std::string& nickname);
explicit IRCMember(const IRCPrefix& prefix);
explicit IRCMember(const std::string& nickname);
const IRCPrefix& prefix() const;
void setPrefix(const IRCPrefix& prefix);
const std::vector<std::weak_ptr<IRCChannel>>& channels();
void addChannel(std::weak_ptr<IRCChannel> channel);
void delChannel(std::weak_ptr<IRCChannel> channel);
const IRCPrefix& prefix() const;
void setPrefix(const IRCPrefix& prefix);
const std::vector<std::weak_ptr<IRCChannel>>& channels();
void addChannel(std::weak_ptr<IRCChannel> channel);
void delChannel(std::weak_ptr<IRCChannel> channel);
void setNickname(const std::string& nickname);
void setNickname(const std::string& nickname);
bool isDangling() const { return m_channels.empty(); }
private:
IRCPrefix m_prefix;
std::vector<std::weak_ptr<IRCChannel>> m_channels;
IRCPrefix m_prefix;
std::vector<std::weak_ptr<IRCChannel>> m_channels;
};
#endif // IRCMEMBER_H

@ -10,54 +10,54 @@
#include <algorithm>
IRCMemberEntry::IRCMemberEntry(std::shared_ptr<IRCMember> member, IRCBase& owner)
: m_member(member)
, m_owner(&owner)
: m_member(member)
, m_owner(&owner)
{}
std::shared_ptr<IRCMember> IRCMemberEntry::member() const
{
return m_member;
return m_member;
}
const std::string& IRCMemberEntry::modes() const
{
return m_modes;
return m_modes;
}
void IRCMemberEntry::addMode(char m)
{
// Note: "PREFIX" is always present.
// Value example: (ohv)@%+
// Value is ordered with most significant first.
const std::string& prefix = m_owner->isupport().find("PREFIX")->second;
// Note: "PREFIX" is always present.
// Value example: (ohv)@%+
// Value is ordered with most significant first.
const std::string& prefix = m_owner->isupport().find("PREFIX")->second;
// Just interested in the mode letters (ie. ohv)
const std::string validModes(prefix.begin() + 1, std::find(prefix.begin(), prefix.end(), ')'));
auto order = [validModes](const char m) -> int {
auto ret = validModes.find(m);
return ret != std::string::npos ? static_cast<int>(ret)
: -1;
};
// Just interested in the mode letters (ie. ohv)
const std::string validModes(prefix.begin() + 1, std::find(prefix.begin(), prefix.end(), ')'));
auto order = [validModes](const char m) -> int {
auto ret = validModes.find(m);
return ret != std::string::npos ? static_cast<int>(ret)
: -1;
};
auto mOrd = order(m);
if (mOrd < 0)
return;
auto mOrd = order(m);
if (mOrd < 0)
return;
auto it = m_modes.begin();
for (auto& mode : m_modes) {
if (mode == m)
return;
else if (order(mode) >= mOrd)
break;
else
++it;
}
m_modes.insert(it, m);
auto it = m_modes.begin();
for (auto& mode : m_modes) {
if (mode == m)
return;
else if (order(mode) >= mOrd)
break;
else
++it;
}
m_modes.insert(it, m);
}
void IRCMemberEntry::delMode(char m)
{
auto pos = m_modes.find_first_of(m);
auto pos = m_modes.find_first_of(m);
/*
* Out-of-bounds can happen due to an IRC protocol bug. For example, where you join a channel which has
@ -66,6 +66,6 @@ void IRCMemberEntry::delMode(char m)
* IRCv3 implements a fix for this which this client support, but not all servers do.
* When a member then removes the unknown mode, we get this situation below;
*/
if (pos != std::string::npos)
if (pos != std::string::npos)
m_modes.erase(pos, 1);
}

@ -17,19 +17,19 @@ class IRCBase;
class IRCMemberEntry
{
public:
IRCMemberEntry(std::shared_ptr<IRCMember> member, IRCBase& owner);
~IRCMemberEntry() = default;
IRCMemberEntry(std::shared_ptr<IRCMember> member, IRCBase& owner);
~IRCMemberEntry() = default;
[[nodiscard]] std::shared_ptr<IRCMember> member() const;
[[nodiscard]] std::shared_ptr<IRCMember> member() const;
[[nodiscard]] const std::string& modes() const;
void addMode(char m);
void delMode(char m);
[[nodiscard]] const std::string& modes() const;
void addMode(char m);
void delMode(char m);
private:
std::shared_ptr<IRCMember> m_member;
std::string m_modes;
IRCBase* m_owner; // can't be reference since we store IRCMemberEntry in a vector, requiring assignment operator.
std::shared_ptr<IRCMember> m_member;
std::string m_modes;
IRCBase* m_owner; // can't be reference since we store IRCMemberEntry in a vector, requiring assignment operator.
};
#endif // IRCMEMBERENTRY_H

@ -0,0 +1,114 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "IRCMessage.h"
#include <algorithm>
#include <iterator>
IRCMessage::IRCMessage(const IRCPrefix& defaultSender, const std::string& message)
: m_sender(defaultSender)
{
/*
* :sender command arg :message
*/
if (message.empty())
return;
enum class S {
IRCv3Tags,
Prefix,
Command,
Argument,
Message
} parseState = S::IRCv3Tags;
auto it = message.begin() + 1;
if (message[0] != '@') {
parseState = S::Prefix;
}
if (message[0] != ':' && message[0] != '@') {
// Set up parser for the format: "cmd arg :msg"
it = message.begin();
parseState = S::Command;
}
/*
* Tokenize the message. Three formats may occur:
* :server.addr cmd arg :msg
* @IRCv3Tag;tag2;tag3 :server.addr cmd arg :msg
* cmd arg :msg
*/
while (it != message.end()) {
auto next = std::find(it, message.end(), ' ');
switch (parseState) {
case S::IRCv3Tags:
{
std::string tagstr;
std::copy(it, next, std::back_inserter(tagstr));
parseIRCv3Tags(tagstr);
parseState = S::Prefix;
break;
}
case S::Prefix:
/* This condition happens if parseState started on IRCv3Tags. */
if (*it == ':')
++it;
m_sender = IRCPrefix(std::string(it, next));
parseState = S::Command;
break;
case S::Command:
m_command = std::string(it, next);
parseState = S::Argument;
break;
case S::Argument:
if (*it == ':') {
++it;
parseState = S::Message;
// note: fallthrough to "Message"
}
else {
m_args.emplace_back(it, next);
break;
}
[[fallthrough]];
case S::Message:
next = message.end();
m_message = std::string(it, next);
break;
}
it = (next == message.end()) ? next : next + 1;
}
}
const std::string& IRCMessage::operator[](int idx) const
{
static const std::string empty;
if (idx < 0 || idx >= m_args.size())
return empty;
else
return m_args[idx];
}
void IRCMessage::parseIRCv3Tags(const std::string& tagstr)
{
auto it = tagstr.begin();
while (it != tagstr.end()) {
auto next = std::find(it, tagstr.end(), ';');
m_tags.emplace_back(it, next);
it = (next == tagstr.end()) ? next : next + 1;
}
}

@ -0,0 +1,50 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#ifndef IRCMESSAGE_H
#define IRCMESSAGE_H
#include "IRCPrefix.h"
#include <string>
#include <vector>
#include <chrono>
class IRCMessage
{
public:
IRCMessage(const IRCPrefix& defaultSender, const std::string& message);
const std::vector<std::string>& getTags() const { return m_tags; }
const IRCPrefix& getSender() const { return m_sender; }
const std::string& getCommand() const { return m_command; }
const std::vector<std::string>& getArgs() const { return m_args; }
const std::string& getMessage() const { return m_message; }
const std::string& getBatchId() const { return m_batchId; }
const std::chrono::system_clock::time_point& getTimestamp() const { return m_timestamp; }
bool isNumeric() const { return !m_command.empty() && m_command[0] >= '0' && m_command[0] <= '9'; }
bool operator==(const char* commandName) const { return m_command == commandName; }
const std::string& operator[](int idx) const;
private:
using SystemClock = std::chrono::system_clock;
std::vector<std::string> m_tags; //!< IRCv3 tags
IRCPrefix m_sender; //!< Sender prefix
std::string m_command; //!< Command name
std::vector<std::string> m_args; //!< Arguments to command
std::string m_message; //!< Message to command
std::string m_batchId; //!< Set if this is part of a batch
SystemClock::time_point m_timestamp; //!< When message occurred.
void parseIRCv3Tags(const std::string& tagstr);
};
#endif // IRCMESSAGE_H

@ -11,107 +11,107 @@
IRCPrefix::IRCPrefix(const std::string& prefix)
{
auto atIt = std::find(prefix.begin(), prefix.end(), '@');
if (atIt != prefix.end()) {
// nickname!user@host
m_type = Type::user;
auto exclmIt = std::find(prefix.begin(), prefix.end(), '!');
m_nickname = std::string(prefix.begin(), exclmIt);
m_user = std::string(exclmIt + 1, atIt);
m_host = std::string(atIt + 1, prefix.end());
}
else {
m_type = Type::server;
m_servername = prefix;
}
auto atIt = std::find(prefix.begin(), prefix.end(), '@');
if (atIt != prefix.end()) {
// nickname!user@host
m_type = Type::user;
auto exclmIt = std::find(prefix.begin(), prefix.end(), '!');
m_nickname = std::string(prefix.begin(), exclmIt);
m_user = std::string(exclmIt + 1, atIt);
m_host = std::string(atIt + 1, prefix.end());
}
else {
m_type = Type::server;
m_servername = prefix;
}
}
IRCPrefix::IRCPrefix(const IRCPrefix& other)
: m_servername(other.m_servername)
, m_nickname(other.m_nickname)
, m_user(other.m_user)
, m_host(other.m_host)
, m_type(other.m_type)
: m_servername(other.m_servername)
, m_nickname(other.m_nickname)
, m_user(other.m_user)
, m_host(other.m_host)
, m_type(other.m_type)
{}
IRCPrefix::IRCPrefix(IRCPrefix&& other) noexcept
: m_servername(std::move(other.m_servername))
, m_nickname(std::move(other.m_nickname))
, m_user(std::move(other.m_user))
, m_host(std::move(other.m_host))
, m_type(std::move(other.m_type))
: m_servername(std::move(other.m_servername))
, m_nickname(std::move(other.m_nickname))
, m_user(std::move(other.m_user))
, m_host(std::move(other.m_host))
, m_type(std::move(other.m_type))
{}
IRCPrefix::IRCPrefix(IRCPrefix::Type t)
: m_type(t)
: m_type(t)
{}
IRCPrefix& IRCPrefix::operator=(const IRCPrefix& other)
{
m_servername = other.m_servername;
m_nickname = other.m_nickname;
m_user = other.m_user;
m_host = other.m_host;
m_type = other.m_type;
return *this;
m_servername = other.m_servername;
m_nickname = other.m_nickname;
m_user = other.m_user;
m_host = other.m_host;
m_type = other.m_type;
return *this;
}
const std::string& IRCPrefix::toString() const
{
if (m_type == Type::server)
return m_servername;
else
return m_nickname;
if (m_type == Type::server)
return m_servername;
else
return m_nickname;
}
const std::string& IRCPrefix::servername() const
{
return m_servername;
return m_servername;
}
const std::string& IRCPrefix::nickname() const
{
return m_nickname;
return m_nickname;
}
const std::string& IRCPrefix::user() const
{
return m_user;
return m_user;
}
const std::string& IRCPrefix::host() const
{
return m_host;
return m_host;
}
void IRCPrefix::setNickname(const std::string& nickname)
{
m_nickname = nickname;
m_nickname = nickname;
}
void IRCPrefix::setHost(const std::string& host)
{
m_host = host;
m_host = host;
}
std::string IRCPrefix::composite() const
{
if (m_type == Type::server)
return m_servername;
else
return fmt::format("{}!{}@{}", m_nickname, m_user, m_host);
if (m_type == Type::server)
return m_servername;
else
return fmt::format("{}!{}@{}", m_nickname, m_user, m_host);
}
IRCPrefix::Type IRCPrefix::type() const
{
return m_type;
return m_type;
}
IRCPrefix IRCPrefix::fromNickname(const std::string& nickname)
{
IRCPrefix p(Type::user);
p.m_nickname = nickname;
return p;
IRCPrefix p(Type::user);
p.m_nickname = nickname;
return p;
}
bool IRCPrefix::operator!=(const IRCPrefix& other)

@ -13,51 +13,51 @@
class IRCPrefix
{
public:
explicit IRCPrefix(const std::string& prefix);
IRCPrefix(const IRCPrefix& other);
IRCPrefix(IRCPrefix&& other) noexcept;
IRCPrefix() = delete;
~IRCPrefix() = default;
explicit IRCPrefix(const std::string& prefix);
IRCPrefix(const IRCPrefix& other);
IRCPrefix(IRCPrefix&& other) noexcept;
IRCPrefix() = delete;
~IRCPrefix() = default;
IRCPrefix& operator=(const IRCPrefix& other);
IRCPrefix& operator=(const IRCPrefix& other);
bool operator!=(const IRCPrefix& other);
bool operator==(const IRCPrefix& other);
[[nodiscard]] const std::string& toString() const; //!< Returns either a servername or nickname.
[[nodiscard]] const std::string& toString() const; //!< Returns either a servername or nickname.
[[nodiscard]] const std::string& servername() const;
[[nodiscard]] const std::string& nickname() const;
[[nodiscard]] const std::string& user() const;
[[nodiscard]] const std::string& host() const;
[[nodiscard]] const std::string& servername() const;
[[nodiscard]] const std::string& nickname() const;
[[nodiscard]] const std::string& user() const;
[[nodiscard]] const std::string& host() const;
void setNickname(const std::string& nickname);
void setHost(const std::string& host);
void setNickname(const std::string& nickname);
void setHost(const std::string& host);
[[nodiscard]] std::string composite() const;
[[nodiscard]] std::string composite() const;
enum class Type
{
server,
user
};
enum class Type
{
server,
user
};
[[nodiscard]] Type type() const;
[[nodiscard]] Type type() const;
[[nodiscard]] static IRCPrefix fromNickname(const std::string& nickname);
[[nodiscard]] static IRCPrefix fromNickname(const std::string& nickname);
private:
explicit IRCPrefix(Type t);
explicit IRCPrefix(Type t);
// Used for :irc.server.name prefix
std::string m_servername;
// Used for :irc.server.name prefix
std::string m_servername;
/* Used for :nickname!user@host prefix */
std::string m_nickname;
std::string m_user;
std::string m_host;
/* Used for :nickname!user@host prefix */
std::string m_nickname;
std::string m_user;
std::string m_host;
Type m_type;
Type m_type;
};
bool operator!=(const IRCPrefix& lhs, const IRCPrefix& rhs);

@ -179,4 +179,17 @@ constexpr auto* RPL_DISPLAYEDHOST = "396";
} // namespace Numeric
namespace NumericV3 {
constexpr auto* RPL_LOGGEDIN = "900";
constexpr auto* RPL_LOGGEDOUT = "901";
constexpr auto* ERR_NICKLOCKED = "902";
constexpr auto* RPL_SASLSUCCESS = "903";
constexpr auto* ERR_SASLFAIL = "904";
constexpr auto* ERR_SASLTOOLONG = "905";
constexpr auto* ERR_SASLABORTED = "906";
constexpr auto* ERR_SASLALREADY = "907";
constexpr auto* RPL_SASLMECHS = "908";
}
#endif // NUMERICS_H

@ -0,0 +1,27 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
void IRCBasePriv::addMemberToChannel(const IRCPrefix& prefix, const std::string& channel)
{
auto member = super.getMember(prefix.toString());
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);
}

@ -0,0 +1,30 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBasePriv.h"
char IRCBasePriv::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];
}

@ -0,0 +1,51 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
#include "../Commands.h"
void IRCBasePriv::connected(const asio::error_code& /*ec*/)
{
using namespace Command::IRC;
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;
registeredV3support.clear();
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);
}

@ -0,0 +1,24 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
#include "../Utilities.h"
void IRCBasePriv::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);
}

@ -0,0 +1,63 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
#include "../Utilities.h"
void IRCBasePriv::delMemberFromChannel(const IRCPrefix& prefix, const std::string& channel)
{
auto member = super.getMember(prefix.nickname());
auto chanentry = super.getChannel(channel);
if (!member || !chanentry)
return;
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. */
else if (prefix.nickname() == nickname) {
/* We also need to remove this channel from every member in that channel. */
for (const auto& mptr : chanentry->members()) {
const auto& m = mptr.member();
IRCPrefix p = m->prefix();
if (p.nickname() != nickname) {
m->delChannel(chanentry);
if (m->isDangling()) {
auto amit = std::find_if(allMembers.begin(), allMembers.end(), [&p](const auto& ptr){
return ptr->prefix() == p;
});
if (amit != allMembers.end())
allMembers.erase(amit);
}
}
}
chanentry->delMember(member);
/* Erase this channel */
auto it = std::find_if(channels.begin(), channels.end(),
[&channel](auto cptr) {
return strEquals(channel, cptr->name());
});
if (it != channels.end())
channels.erase(it);
}
}

@ -0,0 +1,24 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
void IRCBasePriv::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();
}

@ -0,0 +1,14 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBasePriv.h"
bool IRCBasePriv::isChannelSymbol(char c)
{
const std::string& ct = isupport.at("CHANTYPES");
return ct.find(c) != std::string::npos;
}

@ -0,0 +1,14 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBasePriv.h"
bool IRCBasePriv::isMemberPrivilegeSymbol(char m) const
{
bool ret = validPrivilegeSymbols.find(m) != std::string::npos;
return ret;
}

@ -0,0 +1,21 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBasePriv.h"
#include "../Commands.h"
#include <fmt/format.h>
void IRCBasePriv::keepaliveTimeout(const asio::error_code& ec)
{
using namespace Command::IRC;
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();
}
}

@ -0,0 +1,17 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBasePriv.h"
char IRCBasePriv::memberPrivilegeSymbolToMode(char s) const
{
auto pos = validPrivilegeSymbols.find(s);
if (pos == std::string::npos)
return '\0';
else
return validPrivilegeModes[pos];
}

@ -0,0 +1,56 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBasePriv.h"
void IRCBasePriv::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);
}
}
}

@ -0,0 +1,143 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBase.h"
#include "../IRCBasePriv.h"
#include "../Commands.h"
#include <fmt/format.h>
#include <iostream> // Needs better logger, spdlog :)
void IRCBasePriv::parseIncoming(const std::string& line)
{
using namespace Command::IRC;
/*
* Do NOT EVER do an "early return" from this function.
* Make sure it always reaches the end!
* No throwing from any of the message handler functions either.
*/
IRCMessage ircmessage(IRCPrefix(hostname), line);
currentMessage = &ircmessage;
if constexpr (DumpReadData) {
std::cout << "[RCV] ";
std::cout << fmt::format("tagc={} ", ircmessage.getTags().size());
int c = 0;
for (const auto& tag : ircmessage.getTags()) {
std::cout << fmt::format("tag({})=\"{}\" ", c, tag);
++c;
}
std::cout << fmt::format(R"(from="{}" cmd="{}" msg="{}" argc={} )", ircmessage.getSender().composite(), ircmessage.getCommand(), ircmessage.getMessage(), ircmessage.getArgs().size());
c = 0;
for (const auto& arg : ircmessage.getArgs()) {
std::cout << fmt::format("arg({})=\"{}\" ", c, arg);
++c;
}
std::cout << std::endl;
}
using namespace Command::IRC;
using namespace Numeric;
/*
* Numeric reply
*/
if (ircmessage.isNumeric()) {
handleNumeric(ircmessage);
}
else if (ircmessage == NICK) {
cmdNick(ircmessage);
}
else if (ircmessage == MODE) {
cmdMode(ircmessage);
}
else if (ircmessage == QUIT) {
cmdQuit(ircmessage);
}
else if (ircmessage == JOIN) {
cmdJoin(ircmessage);
}
else if (ircmessage == PART) {
cmdPart(ircmessage);
}
else if (ircmessage == TOPIC) {
cmdTopic(ircmessage);
}
else if (ircmessage == INVITE) {
cmdInvite(ircmessage);
}
else if (ircmessage == KICK) {
cmdKick(ircmessage);
}
else if (ircmessage == PRIVMSG) {
cmdPrivmsg(ircmessage);
}
else if (ircmessage == NOTICE) {
cmdNotice(ircmessage);
}
else if (ircmessage == KILL) {
cmdKill(ircmessage);
}
else if (ircmessage == PING) {
cmdPing(ircmessage);
}
else if (ircmessage == PONG) {
cmdPong(ircmessage);
}
else if (ircmessage == ERROR_) {
cmdError(ircmessage);
}
else if (ircmessage == WALLOPS) {
cmdWallops(ircmessage);
}
else if (ircmessage == Command::IRCv3::CAP && !isOnline) {
cmdV3Cap(ircmessage);
}
/* IRCv3 capability "away-notify" */
else if (ircmessage == Command::IRC::AWAY) {
cmdV3Away(ircmessage);
}
/* IRCv3 capability "account-notify" */
else if (ircmessage == Command::Extension::ACCOUNT) {
cmdV3Account(ircmessage);
}
/* IRCv3 capability "sasl" */
else if (ircmessage == Command::Extension::AUTHENTICATE) {
cmdV3Authenticate(ircmessage);
}
else {
handleUnhandled(ircmessage);
}
currentMessage = nullptr;
}

@ -0,0 +1,34 @@
/*
* IdealIRC Core - Internet Relay Chat API
* Copyright (C) 2021 Tom-Andre Barstad.
* This software is licensed under the Software Attribution License.
* See LICENSE for more information.
*/
#include "../IRCBasePriv.h"
void IRCBasePriv::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');
if (crIt != readbuf.end()) {
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); });
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save