diff --git a/.gitmodules b/.gitmodules
index 6f327ab4b..5b6b09606 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -22,6 +22,9 @@
[submodule "lib/rapidjson"]
path = lib/rapidjson
url = https://github.com/Tencent/rapidjson
+[submodule "lib/qtkeychain"]
+ path = lib/qtkeychain
+ url = https://github.com/Chatterino/qtkeychain
[submodule "lib/websocketpp"]
path = lib/websocketpp
url = https://github.com/ziocleto/websocketpp
diff --git a/chatterino.pro b/chatterino.pro
index a6c29eebd..29319e894 100644
--- a/chatterino.pro
+++ b/chatterino.pro
@@ -40,8 +40,6 @@ macx {
}
# Submodules
-DEFINES += IRC_NAMESPACE=Communi
-
include(lib/warnings.pri)
include(lib/appbase.pri)
include(lib/fmt.pri)
@@ -54,6 +52,7 @@ include(lib/settings.pri)
include(lib/serialize.pri)
include(lib/winsdk.pri)
include(lib/rapidjson.pri)
+include(lib/qtkeychain.pri)
exists( $$OUT_PWD/conanbuildinfo.pri ) {
message("Using conan packages")
@@ -78,7 +77,9 @@ SOURCES += \
src/autogenerated/ResourcesAutogen.cpp \
src/BrowserExtension.cpp \
src/common/Channel.cpp \
+ src/common/ChannelChatters.cpp \
src/common/CompletionModel.cpp \
+ src/common/Credentials.cpp \
src/common/DownloadManager.cpp \
src/common/Env.cpp \
src/common/LinkParser.cpp \
@@ -132,8 +133,10 @@ SOURCES += \
src/providers/emoji/Emojis.cpp \
src/providers/ffz/FfzEmotes.cpp \
src/providers/irc/AbstractIrcServer.cpp \
+ src/providers/irc/Irc2.cpp \
src/providers/irc/IrcAccount.cpp \
src/providers/irc/IrcChannel2.cpp \
+ src/providers/irc/IrcCommands.cpp \
src/providers/irc/IrcConnection2.cpp \
src/providers/irc/IrcServer.cpp \
src/providers/LinkResolver.cpp \
@@ -150,9 +153,9 @@ SOURCES += \
src/providers/twitch/TwitchChannel.cpp \
src/providers/twitch/TwitchEmotes.cpp \
src/providers/twitch/TwitchHelpers.cpp \
+ src/providers/twitch/TwitchIrcServer.cpp \
src/providers/twitch/TwitchMessageBuilder.cpp \
src/providers/twitch/TwitchParseCheerEmotes.cpp \
- src/providers/twitch/TwitchServer.cpp \
src/providers/twitch/TwitchUser.cpp \
src/RunGui.cpp \
src/singletons/Badges.cpp \
@@ -180,6 +183,7 @@ SOURCES += \
src/widgets/AccountSwitchWidget.cpp \
src/widgets/AttachedWindow.cpp \
src/widgets/dialogs/EmotePopup.cpp \
+ src/widgets/dialogs/IrcConnectionEditor.cpp \
src/widgets/dialogs/LastRunCrashDialog.cpp \
src/widgets/dialogs/LoginDialog.cpp \
src/widgets/dialogs/LogsPopup.cpp \
@@ -230,9 +234,11 @@ HEADERS += \
src/common/Aliases.hpp \
src/common/Atomic.hpp \
src/common/Channel.hpp \
+ src/common/ChannelChatters.hpp \
src/common/Common.hpp \
src/common/CompletionModel.hpp \
src/common/ConcurrentMap.hpp \
+ src/common/Credentials.hpp \
src/common/DownloadManager.hpp \
src/common/Env.hpp \
src/common/LinkParser.hpp \
@@ -302,8 +308,10 @@ HEADERS += \
src/providers/emoji/Emojis.hpp \
src/providers/ffz/FfzEmotes.hpp \
src/providers/irc/AbstractIrcServer.hpp \
+ src/providers/irc/Irc2.hpp \
src/providers/irc/IrcAccount.hpp \
src/providers/irc/IrcChannel2.hpp \
+ src/providers/irc/IrcCommands.hpp \
src/providers/irc/IrcConnection2.hpp \
src/providers/irc/IrcServer.hpp \
src/providers/LinkResolver.hpp \
@@ -322,9 +330,9 @@ HEADERS += \
src/providers/twitch/TwitchCommon.hpp \
src/providers/twitch/TwitchEmotes.hpp \
src/providers/twitch/TwitchHelpers.hpp \
+ src/providers/twitch/TwitchIrcServer.hpp \
src/providers/twitch/TwitchMessageBuilder.hpp \
src/providers/twitch/TwitchParseCheerEmotes.hpp \
- src/providers/twitch/TwitchServer.hpp \
src/providers/twitch/TwitchUser.hpp \
src/RunGui.hpp \
src/singletons/Badges.hpp \
@@ -350,6 +358,7 @@ HEADERS += \
src/util/IsBigEndian.hpp \
src/util/JsonQuery.hpp \
src/util/LayoutCreator.hpp \
+ src/util/Overloaded.hpp \
src/util/QObjectRef.hpp \
src/util/QStringHash.hpp \
src/util/rangealgorithm.hpp \
@@ -363,6 +372,7 @@ HEADERS += \
src/widgets/AccountSwitchWidget.hpp \
src/widgets/AttachedWindow.hpp \
src/widgets/dialogs/EmotePopup.hpp \
+ src/widgets/dialogs/IrcConnectionEditor.hpp \
src/widgets/dialogs/LastRunCrashDialog.hpp \
src/widgets/dialogs/LoginDialog.hpp \
src/widgets/dialogs/LogsPopup.hpp \
@@ -414,7 +424,8 @@ RESOURCES += \
DISTFILES +=
-FORMS +=
+FORMS += \
+ src/widgets/dialogs/IrcConnectionEditor.ui
# do not use windows min/max macros
#win32 {
diff --git a/.clang-format b/lib/appbase/.clang-format
similarity index 100%
rename from .clang-format
rename to lib/appbase/.clang-format
diff --git a/lib/libcommuni.pri b/lib/libcommuni.pri
index 43b902fbc..8402eb747 100644
--- a/lib/libcommuni.pri
+++ b/lib/libcommuni.pri
@@ -1,3 +1,5 @@
+DEFINES += IRC_NAMESPACE=Communi
+
include(../lib/libcommuni/src/core/core.pri)
include(../lib/libcommuni/src/model/model.pri)
include(../lib/libcommuni/src/util/util.pri)
diff --git a/lib/qtkeychain b/lib/qtkeychain
new file mode 160000
index 000000000..832f550da
--- /dev/null
+++ b/lib/qtkeychain
@@ -0,0 +1 @@
+Subproject commit 832f550da3f6655168a737d2e1b7df37272e936d
diff --git a/lib/qtkeychain.pri b/lib/qtkeychain.pri
new file mode 100644
index 000000000..2ed6914fd
--- /dev/null
+++ b/lib/qtkeychain.pri
@@ -0,0 +1 @@
+include(qtkeychain/qt5keychain.pri)
diff --git a/resources/licenses/qtkeychain.txt b/resources/licenses/qtkeychain.txt
new file mode 100644
index 000000000..cca2a5c9a
--- /dev/null
+++ b/resources/licenses/qtkeychain.txt
@@ -0,0 +1,20 @@
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/resources/resources_autogenerated.qrc b/resources/resources_autogenerated.qrc
index 9f0569e3d..04deaf238 100644
--- a/resources/resources_autogenerated.qrc
+++ b/resources/resources_autogenerated.qrc
@@ -46,6 +46,7 @@
licenses/pajlada_settings.txt
licenses/pajlada_signals.txt
licenses/qt_lgpl-3.0.txt
+ licenses/qtkeychain.txt
licenses/rapidjson.txt
licenses/websocketpp.txt
pajaDank.png
diff --git a/src/.clang-format b/src/.clang-format
new file mode 100644
index 000000000..fd28e7c25
--- /dev/null
+++ b/src/.clang-format
@@ -0,0 +1,34 @@
+Language: Cpp
+
+AccessModifierOffset: -1
+AccessModifierOffset: -4
+AlignEscapedNewlinesLeft: true
+AllowShortFunctionsOnASingleLine: false
+AllowShortIfStatementsOnASingleLine: false
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakAfterDefinitionReturnType: false
+AlwaysBreakBeforeMultilineStrings: false
+BasedOnStyle: Google
+BraceWrapping: {
+ AfterNamespace: 'false'
+ AfterClass: 'true'
+ BeforeElse: 'true'
+ AfterControlStatement: 'true'
+ AfterFunction: 'true'
+ BeforeCatch: 'true'
+}
+BreakBeforeBraces: Custom
+BreakConstructorInitializersBeforeComma: true
+ColumnLimit: 80
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+DerivePointerBinding: false
+FixNamespaceComments: true
+IndentCaseLabels: true
+IndentWidth: 4
+IndentWrappedFunctionNames: true
+IndentPPDirectives: AfterHash
+NamespaceIndentation: Inner
+PointerBindsToType: false
+SpacesBeforeTrailingComments: 2
+Standard: Auto
+ReflowComments: false
diff --git a/src/Application.cpp b/src/Application.cpp
index 42b9bd0bf..c0749037e 100644
--- a/src/Application.cpp
+++ b/src/Application.cpp
@@ -13,8 +13,9 @@
#include "providers/bttv/BttvEmotes.hpp"
#include "providers/chatterino/ChatterinoBadges.hpp"
#include "providers/ffz/FfzEmotes.hpp"
+#include "providers/irc/Irc2.hpp"
#include "providers/twitch/PubsubClient.hpp"
-#include "providers/twitch/TwitchServer.hpp"
+#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Emotes.hpp"
#include "singletons/Fonts.hpp"
#include "singletons/Logging.hpp"
@@ -59,7 +60,7 @@ Application::Application(Settings &_settings, Paths &_paths)
, ignores(&this->emplace())
, taggedUsers(&this->emplace())
, moderationActions(&this->emplace())
- , twitch2(&this->emplace())
+ , twitch2(&this->emplace())
, chatterinoBadges(&this->emplace())
, logging(&this->emplace())
@@ -78,6 +79,8 @@ void Application::initialize(Settings &settings, Paths &paths)
assert(isAppInitialized == false);
isAppInitialized = true;
+ Irc::getInstance().load();
+
for (auto &singleton : this->singletons_)
{
singleton->initialize(settings, paths);
@@ -116,6 +119,8 @@ void Application::save()
void Application::initNm(Paths &paths)
{
+ (void)paths;
+
#ifdef Q_OS_WIN
# if defined QT_NO_DEBUG || defined C_DEBUG_NM
registerNmHost(paths);
diff --git a/src/Application.hpp b/src/Application.hpp
index 49c8fe96c..88cea1aba 100644
--- a/src/Application.hpp
+++ b/src/Application.hpp
@@ -8,7 +8,7 @@
namespace chatterino {
-class TwitchServer;
+class TwitchIrcServer;
class PubSub;
class CommandController;
@@ -67,14 +67,14 @@ public:
IgnoreController *const ignores{};
TaggedUsersController *const taggedUsers{};
ModerationActions *const moderationActions{};
- TwitchServer *const twitch2{};
+ TwitchIrcServer *const twitch2{};
ChatterinoBadges *const chatterinoBadges{};
/*[[deprecated]]*/ Logging *const logging{};
/// Provider-specific
struct {
- /*[[deprecated("use twitch2 instead")]]*/ TwitchServer *server{};
+ /*[[deprecated("use twitch2 instead")]]*/ TwitchIrcServer *server{};
/*[[deprecated("use twitch2->pubsub instead")]]*/ PubSub *pubsub{};
} twitch;
diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp
index 4fe403758..1c13b6db4 100644
--- a/src/common/Channel.cpp
+++ b/src/common/Channel.cpp
@@ -70,13 +70,6 @@ void Channel::addMessage(MessagePtr message,
auto app = getApp();
MessagePtr deleted;
- const QString &username = message->loginName;
- if (!username.isEmpty())
- {
- // TODO: Add recent chatters display name
- this->addRecentChatter(message);
- }
-
// FOURTF: change this when adding more providers
if (this->isTwitchChannel() &&
(!overridingFlags || !overridingFlags->has(MessageFlag::DoNotLog)))
@@ -246,10 +239,6 @@ void Channel::deleteMessage(QString messageID)
}
}
-void Channel::addRecentChatter(const MessagePtr &message)
-{
-}
-
bool Channel::canSendMessage() const
{
return false;
@@ -291,6 +280,15 @@ bool Channel::shouldIgnoreHighlights() const
this->type_ == Type::TwitchWhispers;
}
+bool Channel::canReconnect() const
+{
+ return false;
+}
+
+void Channel::reconnect()
+{
+}
+
std::shared_ptr Channel::getEmpty()
{
static std::shared_ptr channel(new Channel("", Type::None));
diff --git a/src/common/Channel.hpp b/src/common/Channel.hpp
index 596560bd8..f484235ac 100644
--- a/src/common/Channel.hpp
+++ b/src/common/Channel.hpp
@@ -37,15 +37,16 @@ public:
TwitchWatching,
TwitchMentions,
TwitchEnd,
+ Irc,
Misc
};
explicit Channel(const QString &name, Type type);
virtual ~Channel();
+ // SIGNALS
pajlada::Signals::Signal
sendMessageSignal;
-
pajlada::Signals::Signal messageRemovedFromStart;
pajlada::Signals::Signal>
messageAppended;
@@ -60,6 +61,7 @@ public:
virtual bool isEmpty() const;
LimitedQueueSnapshot getMessageSnapshot();
+ // MESSAGES
// overridingFlags can be filled in with flags that should be used instead
// of the message's flags. This is useful in case a flag is specific to a
// type of split
@@ -71,9 +73,11 @@ public:
void disableAllMessages();
void replaceMessage(MessagePtr message, MessagePtr replacement);
void deleteMessage(QString messageID);
+ void clearMessages();
QStringList modList;
+ // CHANNEL INFO
virtual bool canSendMessage() const;
virtual void sendMessage(const QString &message);
virtual bool isMod() const;
@@ -82,6 +86,8 @@ public:
virtual bool hasHighRateLimit() const;
virtual bool isLive() const;
virtual bool shouldIgnoreHighlights() const;
+ virtual bool canReconnect() const;
+ virtual void reconnect();
static std::shared_ptr getEmpty();
@@ -89,7 +95,6 @@ public:
protected:
virtual void onConnected();
- virtual void addRecentChatter(const MessagePtr &message);
private:
const QString name_;
diff --git a/src/common/ChannelChatters.cpp b/src/common/ChannelChatters.cpp
new file mode 100644
index 000000000..396c74aa8
--- /dev/null
+++ b/src/common/ChannelChatters.cpp
@@ -0,0 +1,72 @@
+#include "ChannelChatters.hpp"
+
+#include "messages/Message.hpp"
+#include "messages/MessageBuilder.hpp"
+
+namespace chatterino {
+
+ChannelChatters::ChannelChatters(Channel &channel)
+ : channel_(channel)
+{
+}
+
+AccessGuard ChannelChatters::accessChatters() const
+{
+ return this->chatters_.accessConst();
+}
+
+void ChannelChatters::addRecentChatter(const QString &user)
+{
+ this->chatters_.access()->insert(user);
+}
+
+void ChannelChatters::addJoinedUser(const QString &user)
+{
+ auto joinedUsers = this->joinedUsers_.access();
+ joinedUsers->append(user);
+
+ if (!this->joinedUsersMergeQueued_)
+ {
+ this->joinedUsersMergeQueued_ = true;
+
+ QTimer::singleShot(500, &this->lifetimeGuard_, [this] {
+ auto joinedUsers = this->joinedUsers_.access();
+
+ MessageBuilder builder(systemMessage,
+ "Users joined: " + joinedUsers->join(", "));
+ builder->flags.set(MessageFlag::Collapsed);
+ joinedUsers->clear();
+ this->channel_.addMessage(builder.release());
+ this->joinedUsersMergeQueued_ = false;
+ });
+ }
+}
+
+void ChannelChatters::addPartedUser(const QString &user)
+{
+ auto partedUsers = this->partedUsers_.access();
+ partedUsers->append(user);
+
+ if (!this->partedUsersMergeQueued_)
+ {
+ this->partedUsersMergeQueued_ = true;
+
+ QTimer::singleShot(500, &this->lifetimeGuard_, [this] {
+ auto partedUsers = this->partedUsers_.access();
+
+ MessageBuilder builder(systemMessage,
+ "Users parted: " + partedUsers->join(", "));
+ builder->flags.set(MessageFlag::Collapsed);
+ this->channel_.addMessage(builder.release());
+ partedUsers->clear();
+
+ this->partedUsersMergeQueued_ = false;
+ });
+ }
+}
+void ChannelChatters::setChatters(UsernameSet &&set)
+{
+ *this->chatters_.access() = set;
+}
+
+} // namespace chatterino
diff --git a/src/common/ChannelChatters.hpp b/src/common/ChannelChatters.hpp
new file mode 100644
index 000000000..a0c5896e1
--- /dev/null
+++ b/src/common/ChannelChatters.hpp
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "common/Channel.hpp"
+#include "common/UniqueAccess.hpp"
+#include "common/UsernameSet.hpp"
+
+namespace chatterino {
+
+class ChannelChatters
+{
+public:
+ ChannelChatters(Channel &channel);
+ virtual ~ChannelChatters() = default; // add vtable
+
+ AccessGuard accessChatters() const;
+
+ void addRecentChatter(const QString &user);
+ void addJoinedUser(const QString &user);
+ void addPartedUser(const QString &user);
+ void setChatters(UsernameSet &&set);
+
+private:
+ Channel &channel_;
+
+ // maps 2 char prefix to set of names
+ UniqueAccess chatters_;
+
+ // combines multiple joins/parts into one message
+ UniqueAccess joinedUsers_;
+ bool joinedUsersMergeQueued_ = false;
+ UniqueAccess partedUsers_;
+ bool partedUsersMergeQueued_ = false;
+
+ QObject lifetimeGuard_;
+};
+
+} // namespace chatterino
diff --git a/src/common/CompletionModel.cpp b/src/common/CompletionModel.cpp
index 5895b6a40..cc988122b 100644
--- a/src/common/CompletionModel.cpp
+++ b/src/common/CompletionModel.cpp
@@ -8,7 +8,7 @@
#include "debug/Benchmark.hpp"
#include "debug/Log.hpp"
#include "providers/twitch/TwitchChannel.hpp"
-#include "providers/twitch/TwitchServer.hpp"
+#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Emotes.hpp"
#include "singletons/Settings.hpp"
diff --git a/src/common/Credentials.cpp b/src/common/Credentials.cpp
new file mode 100644
index 000000000..16efc4484
--- /dev/null
+++ b/src/common/Credentials.cpp
@@ -0,0 +1,232 @@
+#include "Credentials.hpp"
+
+#include "debug/AssertInGuiThread.hpp"
+#include "keychain.h"
+#include "singletons/Paths.hpp"
+#include "singletons/Settings.hpp"
+#include "util/CombinePath.hpp"
+#include "util/Overloaded.hpp"
+
+#include
+#include
+
+#define FORMAT_NAME \
+ ([&] { \
+ assert(!provider.contains(":")); \
+ return QString("chatterino:%1:%2").arg(provider).arg(name_); \
+ })()
+
+namespace chatterino {
+
+namespace {
+ bool useKeyring()
+ {
+ if (getPaths()->isPortable())
+ {
+ return false;
+ }
+ else
+ {
+#ifdef Q_OS_LINUX
+ return getSettings()->useKeyring;
+#else
+ return true;
+#endif
+ }
+ }
+
+ // Insecure storage:
+ QString insecurePath()
+ {
+ return combinePath(getPaths()->settingsDirectory, "credentials.json");
+ }
+
+ QJsonDocument loadInsecure()
+ {
+ QFile file(insecurePath());
+ file.open(QIODevice::ReadOnly);
+ return QJsonDocument::fromJson(file.readAll());
+ }
+
+ void storeInsecure(const QJsonDocument &doc)
+ {
+ QSaveFile file(insecurePath());
+ file.open(QIODevice::WriteOnly);
+ file.write(doc.toJson());
+ file.commit();
+ }
+
+ QJsonDocument &insecureInstance()
+ {
+ static auto store = loadInsecure();
+ return store;
+ }
+
+ void queueInsecureSave()
+ {
+ static bool isQueued = false;
+
+ if (!isQueued)
+ {
+ isQueued = true;
+ QTimer::singleShot(200, qApp, [] {
+ storeInsecure(insecureInstance());
+ isQueued = false;
+ });
+ }
+ }
+
+ // QKeychain runs jobs asyncronously, so we have to assure that set/erase
+ // jobs gets executed in order.
+ struct SetJob {
+ QString name;
+ QString credential;
+ };
+
+ struct EraseJob {
+ QString name;
+ };
+
+ using Job = boost::variant;
+
+ static std::queue &jobQueue()
+ {
+ static std::queue jobs;
+ return jobs;
+ }
+
+ static void runNextJob()
+ {
+ auto &&queue = jobQueue();
+
+ if (!queue.empty())
+ {
+ // we were gonna use std::visit here but macos is shit
+
+ auto &&item = queue.front();
+
+ if (item.which() == 0) // set job
+ {
+ auto set = boost::get(item);
+ auto job = new QKeychain::WritePasswordJob("chatterino");
+ job->setAutoDelete(true);
+ job->setKey(set.name);
+ job->setTextData(set.credential);
+ QObject::connect(job, &QKeychain::Job::finished, qApp,
+ [](auto) { runNextJob(); });
+ job->start();
+ }
+ else // erase job
+ {
+ auto erase = boost::get(item);
+ auto job = new QKeychain::DeletePasswordJob("chatterino");
+ job->setAutoDelete(true);
+ job->setKey(erase.name);
+ QObject::connect(job, &QKeychain::Job::finished, qApp,
+ [](auto) { runNextJob(); });
+ job->start();
+ }
+
+ queue.pop();
+ }
+ }
+
+ static void queueJob(Job &&job)
+ {
+ auto &&queue = jobQueue();
+
+ queue.push(std::move(job));
+ if (queue.size() == 1)
+ {
+ runNextJob();
+ }
+ }
+} // namespace
+
+Credentials &Credentials::getInstance()
+{
+ static Credentials creds;
+ return creds;
+}
+
+Credentials::Credentials()
+{
+}
+
+void Credentials::get(const QString &provider, const QString &name_,
+ QObject *receiver,
+ std::function &&onLoaded)
+{
+ assertInGuiThread();
+
+ auto name = FORMAT_NAME;
+
+ if (useKeyring())
+ {
+ auto job = new QKeychain::ReadPasswordJob("chatterino");
+ job->setAutoDelete(true);
+ job->setKey(name);
+ QObject::connect(job, &QKeychain::Job::finished, receiver,
+ [job, onLoaded = std::move(onLoaded)](auto) mutable {
+ onLoaded(job->textData());
+ },
+ Qt::DirectConnection);
+ job->start();
+ }
+ else
+ {
+ auto &instance = insecureInstance();
+
+ onLoaded(instance.object().find(name).value().toString());
+ }
+}
+
+void Credentials::set(const QString &provider, const QString &name_,
+ const QString &credential)
+{
+ assertInGuiThread();
+
+ /// On linux, we try to use a keychain but show a message to disable it when it fails.
+ /// XXX: add said message
+
+ auto name = FORMAT_NAME;
+
+ if (useKeyring())
+ {
+ queueJob(SetJob{name, credential});
+ }
+ else
+ {
+ auto &instance = insecureInstance();
+
+ instance.object()[name] = credential;
+
+ queueInsecureSave();
+ }
+}
+
+void Credentials::erase(const QString &provider, const QString &name_)
+{
+ assertInGuiThread();
+
+ auto name = FORMAT_NAME;
+
+ if (useKeyring())
+ {
+ queueJob(EraseJob{name});
+ }
+ else
+ {
+ auto &instance = insecureInstance();
+
+ if (auto it = instance.object().find(name);
+ it != instance.object().end())
+ {
+ instance.object().erase(it);
+ }
+
+ queueInsecureSave();
+ }
+}
+
+} // namespace chatterino
diff --git a/src/common/Credentials.hpp b/src/common/Credentials.hpp
new file mode 100644
index 000000000..b71ed6cd2
--- /dev/null
+++ b/src/common/Credentials.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#include
+#include
+
+namespace chatterino {
+
+class Credentials
+{
+public:
+ static Credentials &getInstance();
+
+ void get(const QString &provider, const QString &name, QObject *receiver,
+ std::function &&onLoaded);
+ void set(const QString &provider, const QString &name,
+ const QString &credential);
+ void erase(const QString &provider, const QString &name);
+
+private:
+ Credentials();
+};
+
+} // namespace chatterino
diff --git a/src/common/SignalVectorModel.hpp b/src/common/SignalVectorModel.hpp
index d23694eb4..adea78843 100644
--- a/src/common/SignalVectorModel.hpp
+++ b/src/common/SignalVectorModel.hpp
@@ -102,19 +102,26 @@ public:
int rowCount(const QModelIndex &parent) const override
{
+ (void)parent;
+
return this->rows_.size();
}
int columnCount(const QModelIndex &parent) const override
{
+ (void)parent;
+
return this->columnCount_;
}
QVariant data(const QModelIndex &index, int role) const override
{
int row = index.row(), column = index.column();
- assert(row >= 0 && row < this->rows_.size() && column >= 0 &&
- column < this->columnCount_);
+ if (row < 0 || column < 0 || row >= this->rows_.size() ||
+ column >= this->columnCount_)
+ {
+ return QVariant();
+ }
return rows_[row].items[column]->data(role);
}
@@ -123,8 +130,11 @@ public:
int role) override
{
int row = index.row(), column = index.column();
- assert(row >= 0 && row < this->rows_.size() && column >= 0 &&
- column < this->columnCount_);
+ if (row < 0 || column < 0 || row >= this->rows_.size() ||
+ column >= this->columnCount_)
+ {
+ return false;
+ }
Row &rowItem = this->rows_[row];
@@ -185,6 +195,13 @@ public:
Qt::ItemFlags flags(const QModelIndex &index) const override
{
int row = index.row(), column = index.column();
+
+ if (row < 0 || column < 0 || row >= this->rows_.size() ||
+ column >= this->columnCount_)
+ {
+ return Qt::NoItemFlags;
+ }
+
assert(row >= 0 && row < this->rows_.size() && column >= 0 &&
column < this->columnCount_);
@@ -207,6 +224,8 @@ public:
bool removeRows(int row, int count, const QModelIndex &parent) override
{
+ (void)parent;
+
if (count != 1)
{
return false;
@@ -237,18 +256,22 @@ protected:
std::vector &row,
int proposedIndex)
{
+ (void)item, (void)row;
+
return proposedIndex;
}
virtual void afterRemoved(const TVectorItem &item,
std::vector &row, int index)
{
+ (void)item, (void)row, (void)index;
}
virtual void customRowSetData(const std::vector &row,
int column, const QVariant &value, int role,
int rowIndex)
{
+ (void)row, (void)column, (void)value, (void)role, (void)rowIndex;
}
void insertCustomRow(std::vector row, int index)
diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp
index 76e3e31e1..cccdf63a1 100644
--- a/src/controllers/commands/CommandController.cpp
+++ b/src/controllers/commands/CommandController.cpp
@@ -11,7 +11,7 @@
#include "messages/MessageElement.hpp"
#include "providers/twitch/TwitchApi.hpp"
#include "providers/twitch/TwitchChannel.hpp"
-#include "providers/twitch/TwitchServer.hpp"
+#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Emotes.hpp"
#include "singletons/Paths.hpp"
#include "singletons/Settings.hpp"
diff --git a/src/controllers/notifications/NotificationController.cpp b/src/controllers/notifications/NotificationController.cpp
index 40643aa85..1c0b7ecc7 100644
--- a/src/controllers/notifications/NotificationController.cpp
+++ b/src/controllers/notifications/NotificationController.cpp
@@ -6,7 +6,7 @@
#include "controllers/notifications/NotificationModel.hpp"
#include "debug/Log.hpp"
#include "providers/twitch/TwitchApi.hpp"
-#include "providers/twitch/TwitchServer.hpp"
+#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Toasts.hpp"
#include "singletons/WindowManager.hpp"
#include "widgets/Window.hpp"
diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp
index 31d8bdd32..47af81abf 100644
--- a/src/messages/Message.hpp
+++ b/src/messages/Message.hpp
@@ -32,6 +32,7 @@ enum class MessageFlag : uint32_t {
RecentMessage = (1 << 15),
Whisper = (1 << 16),
HighlightedWhisper = (1 << 17),
+ Debug = (1 << 18),
};
using MessageFlags = FlagsEnum;
diff --git a/src/messages/layouts/MessageLayout.cpp b/src/messages/layouts/MessageLayout.cpp
index a517bd7ce..07b7c79d4 100644
--- a/src/messages/layouts/MessageLayout.cpp
+++ b/src/messages/layouts/MessageLayout.cpp
@@ -269,6 +269,10 @@ void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/,
{
backgroundColor = QColor("#404040");
}
+ else if (this->message_->flags.has(MessageFlag::Debug))
+ {
+ backgroundColor = QColor("#4A273D");
+ }
painter.fillRect(buffer->rect(), backgroundColor);
diff --git a/src/providers/irc/AbstractIrcServer.cpp b/src/providers/irc/AbstractIrcServer.cpp
index 05172d56e..4ca3fb04a 100644
--- a/src/providers/irc/AbstractIrcServer.cpp
+++ b/src/providers/irc/AbstractIrcServer.cpp
@@ -18,13 +18,17 @@ const int MAX_FALLOFF_COUNTER = 60;
AbstractIrcServer::AbstractIrcServer()
{
// Initialize the connections
+ // XXX: don't create write connection if there is not separate write connection.
this->writeConnection_.reset(new IrcConnection);
this->writeConnection_->moveToThread(
QCoreApplication::instance()->thread());
QObject::connect(
this->writeConnection_.get(), &Communi::IrcConnection::messageReceived,
- [this](auto msg) { this->writeConnectionMessageReceived(msg); });
+ this, [this](auto msg) { this->writeConnectionMessageReceived(msg); });
+ QObject::connect(
+ this->writeConnection_.get(), &Communi::IrcConnection::connected, this,
+ [this] { this->onWriteConnected(this->writeConnection_.get()); });
// Listen to read connection message signals
this->readConnection_.reset(new IrcConnection);
@@ -32,21 +36,18 @@ AbstractIrcServer::AbstractIrcServer()
QObject::connect(
this->readConnection_.get(), &Communi::IrcConnection::messageReceived,
- [this](auto msg) { this->readConnectionMessageReceived(msg); });
+ this, [this](auto msg) { this->readConnectionMessageReceived(msg); });
QObject::connect(this->readConnection_.get(),
- &Communi::IrcConnection::privateMessageReceived,
+ &Communi::IrcConnection::privateMessageReceived, this,
[this](auto msg) { this->privateMessageReceived(msg); });
QObject::connect(
- this->readConnection_.get(), &Communi::IrcConnection::connected,
+ this->readConnection_.get(), &Communi::IrcConnection::connected, this,
[this] { this->onReadConnected(this->readConnection_.get()); });
- QObject::connect(
- this->writeConnection_.get(), &Communi::IrcConnection::connected,
- [this] { this->onWriteConnected(this->writeConnection_.get()); });
QObject::connect(this->readConnection_.get(),
- &Communi::IrcConnection::disconnected,
+ &Communi::IrcConnection::disconnected, this,
[this] { this->onDisconnected(); });
QObject::connect(this->readConnection_.get(),
- &Communi::IrcConnection::socketError,
+ &Communi::IrcConnection::socketError, this,
[this] { this->onSocketError(); });
// listen to reconnect request
@@ -75,37 +76,29 @@ void AbstractIrcServer::connect()
{
this->disconnect();
- bool separateWriteConnection = this->hasSeparateWriteConnection();
-
- if (separateWriteConnection)
+ if (this->hasSeparateWriteConnection())
{
- this->initializeConnection(this->writeConnection_.get(), false, true);
- this->initializeConnection(this->readConnection_.get(), true, false);
+ this->initializeConnection(this->writeConnection_.get(), Write);
+ this->initializeConnection(this->readConnection_.get(), Read);
}
else
{
- this->initializeConnection(this->readConnection_.get(), true, true);
+ this->initializeConnection(this->readConnection_.get(), Both);
}
+}
- // fourtf: this should be asynchronous
+void AbstractIrcServer::open(ConnectionType type)
+{
+ std::lock_guard lock(this->connectionMutex_);
+
+ if (type == Write)
{
- std::lock_guard lock1(this->connectionMutex_);
- std::lock_guard lock2(this->channelMutex);
-
- for (std::weak_ptr &weak : this->channels.values())
- {
- if (auto channel = std::shared_ptr(weak.lock()))
- {
- this->readConnection_->sendRaw("JOIN #" + channel->getName());
- }
- }
-
this->writeConnection_->open();
+ }
+ if (type & Read)
+ {
this->readConnection_->open();
}
-
- // this->onConnected();
- // possbile event: started to connect
}
void AbstractIrcServer::disconnect()
@@ -113,7 +106,10 @@ void AbstractIrcServer::disconnect()
std::lock_guard locker(this->connectionMutex_);
this->readConnection_->close();
- this->writeConnection_->close();
+ if (this->hasSeparateWriteConnection())
+ {
+ this->writeConnection_->close();
+ }
}
void AbstractIrcServer::sendMessage(const QString &channelName,
@@ -139,10 +135,10 @@ void AbstractIrcServer::sendRawMessage(const QString &rawMessage)
void AbstractIrcServer::writeConnectionMessageReceived(
Communi::IrcMessage *message)
{
+ (void)message;
}
-std::shared_ptr AbstractIrcServer::getOrAddChannel(
- const QString &dirtyChannelName)
+ChannelPtr AbstractIrcServer::getOrAddChannel(const QString &dirtyChannelName)
{
auto channelName = this->cleanChannelName(dirtyChannelName);
@@ -162,26 +158,24 @@ std::shared_ptr AbstractIrcServer::getOrAddChannel(
return Channel::getEmpty();
}
- QString clojuresInCppAreShit = channelName;
-
this->channels.insert(channelName, chan);
- chan->destroyed.connect([this, clojuresInCppAreShit] {
+ this->connections_.emplace_back(chan->destroyed.connect([this,
+ channelName] {
// fourtf: issues when the server itself is destroyed
- log("[AbstractIrcServer::addChannel] {} was destroyed",
- clojuresInCppAreShit);
- this->channels.remove(clojuresInCppAreShit);
+ log("[AbstractIrcServer::addChannel] {} was destroyed", channelName);
+ this->channels.remove(channelName);
if (this->readConnection_)
{
- this->readConnection_->sendRaw("PART #" + clojuresInCppAreShit);
+ this->readConnection_->sendRaw("PART #" + channelName);
}
- if (this->writeConnection_)
+ if (this->writeConnection_ && this->hasSeparateWriteConnection())
{
- this->writeConnection_->sendRaw("PART #" + clojuresInCppAreShit);
+ this->writeConnection_->sendRaw("PART #" + channelName);
}
- });
+ }));
// join irc channel
{
@@ -189,20 +183,25 @@ std::shared_ptr AbstractIrcServer::getOrAddChannel(
if (this->readConnection_)
{
- this->readConnection_->sendRaw("JOIN #" + channelName);
+ if (this->readConnection_->isConnected())
+ {
+ this->readConnection_->sendRaw("JOIN #" + channelName);
+ }
}
- if (this->writeConnection_)
+ if (this->writeConnection_ && this->hasSeparateWriteConnection())
{
- this->writeConnection_->sendRaw("JOIN #" + channelName);
+ if (this->readConnection_->isConnected())
+ {
+ this->writeConnection_->sendRaw("JOIN #" + channelName);
+ }
}
}
return chan;
}
-std::shared_ptr AbstractIrcServer::getChannelOrEmpty(
- const QString &dirtyChannelName)
+ChannelPtr AbstractIrcServer::getChannelOrEmpty(const QString &dirtyChannelName)
{
auto channelName = this->cleanChannelName(dirtyChannelName);
@@ -230,10 +229,35 @@ std::shared_ptr AbstractIrcServer::getChannelOrEmpty(
return Channel::getEmpty();
}
+std::vector> AbstractIrcServer::getChannels()
+{
+ std::lock_guard lock(this->channelMutex);
+ std::vector> channels;
+
+ for (auto &&weak : this->channels.values())
+ {
+ channels.push_back(weak);
+ }
+
+ return channels;
+}
+
void AbstractIrcServer::onReadConnected(IrcConnection *connection)
{
- std::lock_guard lock(this->channelMutex);
+ (void)connection;
+ std::lock_guard lock(this->channelMutex);
+
+ // join channels
+ for (auto &&weak : this->channels)
+ {
+ if (auto channel = weak.lock())
+ {
+ connection->sendRaw("JOIN #" + channel->getName());
+ }
+ }
+
+ // connected/disconnected message
auto connectedMsg = makeSystemMessage("connected");
connectedMsg->flags.set(MessageFlag::ConnectedMessage);
auto reconnected = makeSystemMessage("reconnected");
@@ -267,6 +291,7 @@ void AbstractIrcServer::onReadConnected(IrcConnection *connection)
void AbstractIrcServer::onWriteConnected(IrcConnection *connection)
{
+ (void)connection;
}
void AbstractIrcServer::onDisconnected()
@@ -297,12 +322,16 @@ void AbstractIrcServer::onSocketError()
std::shared_ptr AbstractIrcServer::getCustomChannel(
const QString &channelName)
{
+ (void)channelName;
return nullptr;
}
QString AbstractIrcServer::cleanChannelName(const QString &dirtyChannelName)
{
- return dirtyChannelName;
+ if (dirtyChannelName.startsWith('#'))
+ return dirtyChannelName.mid(1);
+ else
+ return dirtyChannelName;
}
void AbstractIrcServer::addFakeMessage(const QString &data)
@@ -324,6 +353,7 @@ void AbstractIrcServer::addFakeMessage(const QString &data)
void AbstractIrcServer::privateMessageReceived(
Communi::IrcPrivateMessage *message)
{
+ (void)message;
}
void AbstractIrcServer::readConnectionMessageReceived(
@@ -337,7 +367,7 @@ void AbstractIrcServer::forEachChannel(std::function func)
for (std::weak_ptr &weak : this->channels.values())
{
- std::shared_ptr chan = weak.lock();
+ ChannelPtr chan = weak.lock();
if (!chan)
{
continue;
diff --git a/src/providers/irc/AbstractIrcServer.hpp b/src/providers/irc/AbstractIrcServer.hpp
index 3718ec23d..9b17d3012 100644
--- a/src/providers/irc/AbstractIrcServer.hpp
+++ b/src/providers/irc/AbstractIrcServer.hpp
@@ -4,6 +4,7 @@
#include
#include
+#include
#include
#include
@@ -13,9 +14,11 @@ namespace chatterino {
class Channel;
using ChannelPtr = std::shared_ptr;
-class AbstractIrcServer
+class AbstractIrcServer : public QObject
{
public:
+ enum ConnectionType { Read = 1, Write = 2, Both = 3 };
+
virtual ~AbstractIrcServer() = default;
// connection
@@ -26,14 +29,13 @@ public:
void sendRawMessage(const QString &rawMessage);
// channels
- std::shared_ptr getOrAddChannel(const QString &dirtyChannelName);
- std::shared_ptr getChannelOrEmpty(const QString &dirtyChannelName);
+ ChannelPtr getOrAddChannel(const QString &dirtyChannelName);
+ ChannelPtr getChannelOrEmpty(const QString &dirtyChannelName);
+ std::vector> getChannels();
// signals
pajlada::Signals::NoArgSignal connected;
pajlada::Signals::NoArgSignal disconnected;
- // pajlada::Signals::Signal
- // onPrivateMessage;
void addFakeMessage(const QString &data);
@@ -43,8 +45,8 @@ public:
protected:
AbstractIrcServer();
- virtual void initializeConnection(IrcConnection *connection, bool isRead,
- bool isWrite) = 0;
+ virtual void initializeConnection(IrcConnection *connection,
+ ConnectionType type) = 0;
virtual std::shared_ptr createChannel(
const QString &channelName) = 0;
@@ -63,14 +65,23 @@ protected:
virtual bool hasSeparateWriteConnection() const = 0;
virtual QString cleanChannelName(const QString &dirtyChannelName);
+ void open(ConnectionType type);
+
QMap> channels;
std::mutex channelMutex;
private:
void initConnection();
- std::unique_ptr writeConnection_ = nullptr;
- std::unique_ptr readConnection_ = nullptr;
+ struct Deleter {
+ void operator()(IrcConnection *conn)
+ {
+ conn->deleteLater();
+ }
+ };
+
+ std::unique_ptr writeConnection_ = nullptr;
+ std::unique_ptr readConnection_ = nullptr;
QTimer reconnectTimer_;
int falloffCounter_ = 1;
@@ -78,6 +89,7 @@ private:
std::mutex connectionMutex_;
// bool autoReconnect_ = false;
+ pajlada::Signals::SignalHolder connections_;
};
} // namespace chatterino
diff --git a/src/providers/irc/Irc2.cpp b/src/providers/irc/Irc2.cpp
new file mode 100644
index 000000000..7409ac986
--- /dev/null
+++ b/src/providers/irc/Irc2.cpp
@@ -0,0 +1,261 @@
+#include "Irc2.hpp"
+
+#include
+#include "common/Credentials.hpp"
+#include "common/SignalVectorModel.hpp"
+#include "singletons/Paths.hpp"
+#include "util/CombinePath.hpp"
+#include "util/RapidjsonHelpers.hpp"
+#include "util/StandardItemHelper.hpp"
+
+#include
+#include
+
+namespace chatterino {
+
+namespace {
+ QString configPath()
+ {
+ return combinePath(getPaths()->settingsDirectory, "irc.json");
+ }
+
+ class Model : public SignalVectorModel
+ {
+ public:
+ Model(QObject *parent)
+ : SignalVectorModel(6, parent)
+ {
+ }
+
+ // turn a vector item into a model row
+ IrcServerData getItemFromRow(std::vector &row,
+ const IrcServerData &original)
+ {
+ return IrcServerData{
+ row[0]->data(Qt::EditRole).toString(), // host
+ row[1]->data(Qt::EditRole).toInt(), // port
+ row[2]->data(Qt::CheckStateRole).toBool(), // ssl
+ row[3]->data(Qt::EditRole).toString(), // user
+ row[4]->data(Qt::EditRole).toString(), // nick
+ row[5]->data(Qt::EditRole).toString(), // real
+ original.authType, // authType
+ original.connectCommands, // connectCommands
+ original.id, // id
+ };
+ }
+
+ // turns a row in the model into a vector item
+ void getRowFromItem(const IrcServerData &item,
+ std::vector &row)
+ {
+ setStringItem(row[0], item.host, false);
+ setStringItem(row[1], QString::number(item.port));
+ setBoolItem(row[2], item.ssl);
+ setStringItem(row[3], item.user);
+ setStringItem(row[4], item.nick);
+ setStringItem(row[5], item.real);
+ }
+ };
+} // namespace
+
+inline QString escape(QString str)
+{
+ return str.replace(":", "::");
+}
+
+// This returns a unique id for every server which is understandeable in the systems credential manager.
+inline QString getCredentialName(const IrcServerData &data)
+{
+ return escape(QString::number(data.id)) + ":" + escape(data.user) + "@" +
+ escape(data.host);
+}
+
+void IrcServerData::getPassword(
+ QObject *receiver, std::function &&onLoaded) const
+{
+ Credentials::getInstance().get("irc", getCredentialName(*this), receiver,
+ std::move(onLoaded));
+}
+
+void IrcServerData::setPassword(const QString &password)
+{
+ Credentials::getInstance().set("irc", getCredentialName(*this), password);
+}
+
+Irc::Irc()
+{
+ this->connections.itemInserted.connect([this](auto &&args) {
+ // make sure only one id can only exist for one server
+ assert(this->servers_.find(args.item.id) == this->servers_.end());
+
+ // add new server
+ if (auto ab = this->abandonedChannels_.find(args.item.id);
+ ab != this->abandonedChannels_.end())
+ {
+ auto server = std::make_unique(args.item, ab->second);
+
+ // set server of abandoned channels
+ for (auto weak : ab->second)
+ if (auto shared = weak.lock())
+ if (auto ircChannel =
+ dynamic_cast(shared.get()))
+ ircChannel->setServer(server.get());
+
+ // add new server with abandoned channels
+ this->servers_.emplace(args.item.id, std::move(server));
+ this->abandonedChannels_.erase(ab);
+ }
+ else
+ {
+ // add new server
+ this->servers_.emplace(args.item.id,
+ std::make_unique(args.item));
+ }
+ });
+
+ this->connections.itemRemoved.connect([this](auto &&args) {
+ // restore
+ if (auto server = this->servers_.find(args.item.id);
+ server != this->servers_.end())
+ {
+ auto abandoned = server->second->getChannels();
+
+ // set server of abandoned servers to nullptr
+ for (auto weak : abandoned)
+ if (auto shared = weak.lock())
+ if (auto ircChannel =
+ dynamic_cast(shared.get()))
+ ircChannel->setServer(nullptr);
+
+ this->abandonedChannels_[args.item.id] = abandoned;
+ this->servers_.erase(server);
+ }
+
+ if (args.caller != Irc::noEraseCredentialCaller)
+ {
+ Credentials::getInstance().erase("irc",
+ getCredentialName(args.item));
+ }
+ });
+
+ this->connections.delayedItemsChanged.connect([this] { this->save(); });
+}
+
+QAbstractTableModel *Irc::newConnectionModel(QObject *parent)
+{
+ auto model = new Model(parent);
+ model->init(&this->connections);
+ return model;
+}
+
+ChannelPtr Irc::getOrAddChannel(int id, QString name)
+{
+ if (auto server = this->servers_.find(id); server != this->servers_.end())
+ {
+ return server->second->getOrAddChannel(name);
+ }
+ else
+ {
+ auto channel = std::make_shared(name, nullptr);
+
+ this->abandonedChannels_[id].push_back(channel);
+
+ return std::move(channel);
+ }
+}
+
+Irc &Irc::getInstance()
+{
+ static Irc irc;
+ return irc;
+}
+
+int Irc::uniqueId()
+{
+ int i = this->currentId_ + 1;
+ auto it = this->servers_.find(i);
+ auto it2 = this->abandonedChannels_.find(i);
+
+ while (it != this->servers_.end() || it2 != this->abandonedChannels_.end())
+ {
+ i++;
+ it = this->servers_.find(i);
+ it2 = this->abandonedChannels_.find(i);
+ }
+
+ return (this->currentId_ = i);
+}
+
+void Irc::save()
+{
+ QJsonDocument doc;
+ QJsonObject root;
+ QJsonArray servers;
+
+ for (auto &&conn : this->connections)
+ {
+ QJsonObject obj;
+ obj.insert("host", conn.host);
+ obj.insert("port", conn.port);
+ obj.insert("ssl", conn.ssl);
+ obj.insert("username", conn.user);
+ obj.insert("nickname", conn.nick);
+ obj.insert("realname", conn.real);
+ obj.insert("connectCommands",
+ QJsonArray::fromStringList(conn.connectCommands));
+ obj.insert("id", conn.id);
+ obj.insert("authType", int(conn.authType));
+
+ servers.append(obj);
+ }
+
+ root.insert("servers", servers);
+ doc.setObject(root);
+
+ QSaveFile file(configPath());
+ file.open(QIODevice::WriteOnly);
+ file.write(doc.toJson());
+ file.commit();
+}
+
+void Irc::load()
+{
+ if (this->loaded_)
+ return;
+ this->loaded_ = true;
+
+ QString config = configPath();
+ QFile file(configPath());
+ file.open(QIODevice::ReadOnly);
+ auto object = QJsonDocument::fromJson(file.readAll()).object();
+
+ std::unordered_set ids;
+
+ // load servers
+ for (auto server : object.value("servers").toArray())
+ {
+ auto obj = server.toObject();
+ IrcServerData data;
+ data.host = obj.value("host").toString(data.host);
+ data.port = obj.value("port").toInt(data.port);
+ data.ssl = obj.value("ssl").toBool(data.ssl);
+ data.user = obj.value("username").toString(data.user);
+ data.nick = obj.value("nickname").toString(data.nick);
+ data.real = obj.value("realname").toString(data.real);
+ data.connectCommands =
+ obj.value("connectCommands").toVariant().toStringList();
+ data.id = obj.value("id").toInt(data.id);
+ data.authType =
+ IrcAuthType(obj.value("authType").toInt(int(data.authType)));
+
+ // duplicate id's are not allowed :(
+ if (ids.find(data.id) == ids.end())
+ {
+ ids.insert(data.id);
+
+ this->connections.appendItem(data);
+ }
+ }
+}
+
+} // namespace chatterino
diff --git a/src/providers/irc/Irc2.hpp b/src/providers/irc/Irc2.hpp
new file mode 100644
index 000000000..0d40f6fe1
--- /dev/null
+++ b/src/providers/irc/Irc2.hpp
@@ -0,0 +1,67 @@
+#pragma once
+
+#include
+#include
+
+#include "providers/irc/IrcChannel2.hpp"
+#include "providers/irc/IrcServer.hpp"
+
+class QAbstractTableModel;
+
+namespace chatterino {
+
+enum class IrcAuthType { Anonymous, Custom, Pass, Sasl };
+
+struct IrcServerData {
+ QString host;
+ int port = 6697;
+ bool ssl = true;
+
+ QString user;
+ QString nick;
+ QString real;
+
+ IrcAuthType authType = IrcAuthType::Anonymous;
+ void getPassword(QObject *receiver,
+ std::function &&onLoaded) const;
+ void setPassword(const QString &password);
+
+ QStringList connectCommands;
+
+ int id;
+};
+
+class Irc
+{
+public:
+ Irc();
+
+ static Irc &getInstance();
+
+ static inline void *const noEraseCredentialCaller =
+ reinterpret_cast(1);
+
+ UnsortedSignalVector connections;
+ QAbstractTableModel *newConnectionModel(QObject *parent);
+
+ ChannelPtr getOrAddChannel(int serverId, QString name);
+
+ void save();
+ void load();
+
+ int uniqueId();
+
+private:
+ int currentId_{};
+ bool loaded_{};
+
+ // Servers have a unique id.
+ // When a server gets changed it gets removed and then added again.
+ // So we store the channels of that server in abandonedChannels_ temporarily.
+ // Or if the server got removed permanently then it's still stored there.
+ std::unordered_map> servers_;
+ std::unordered_map>>
+ abandonedChannels_;
+};
+
+} // namespace chatterino
diff --git a/src/providers/irc/IrcChannel2.cpp b/src/providers/irc/IrcChannel2.cpp
index ce8de21ba..b7b17fc72 100644
--- a/src/providers/irc/IrcChannel2.cpp
+++ b/src/providers/irc/IrcChannel2.cpp
@@ -1,9 +1,68 @@
#include "IrcChannel2.hpp"
+#include "debug/AssertInGuiThread.hpp"
+#include "messages/MessageBuilder.hpp"
+#include "providers/irc/IrcCommands.hpp"
+#include "providers/irc/IrcServer.hpp"
+
namespace chatterino {
-// IrcChannel::IrcChannel()
-//{
-//}
-//
+IrcChannel::IrcChannel(const QString &name, IrcServer *server)
+ : Channel(name, Channel::Type::Irc)
+ , ChannelChatters(*static_cast(this))
+ , server_(server)
+{
+}
+
+void IrcChannel::sendMessage(const QString &message)
+{
+ assertInGuiThread();
+
+ if (message.startsWith("/"))
+ {
+ int index = message.indexOf(' ', 1);
+ QString command = message.mid(1, index - 1);
+ QString params = index == -1 ? "" : message.mid(index + 1);
+
+ invokeIrcCommand(command, params, *this);
+ }
+ else
+ {
+ if (this->server())
+ this->server()->sendMessage(this->getName(), message);
+
+ MessageBuilder builder;
+ builder.emplace();
+ builder.emplace(this->server()->nick() + ":",
+ MessageElementFlag::Username);
+ builder.emplace(message, MessageElementFlag::Text);
+ this->addMessage(builder.release());
+ }
+}
+
+IrcServer *IrcChannel::server()
+{
+ assertInGuiThread();
+
+ return this->server_;
+}
+
+void IrcChannel::setServer(IrcServer *server)
+{
+ assertInGuiThread();
+
+ this->server_ = server;
+}
+
+bool IrcChannel::canReconnect() const
+{
+ return true;
+}
+
+void IrcChannel::reconnect()
+{
+ if (this->server())
+ this->server()->connect();
+}
+
} // namespace chatterino
diff --git a/src/providers/irc/IrcChannel2.hpp b/src/providers/irc/IrcChannel2.hpp
index 0646fbec9..81b85ce18 100644
--- a/src/providers/irc/IrcChannel2.hpp
+++ b/src/providers/irc/IrcChannel2.hpp
@@ -1,11 +1,33 @@
#pragma once
+#include "common/Channel.hpp"
+#include "common/ChannelChatters.hpp"
+
namespace chatterino {
-// class IrcChannel
-//{
-// public:
-// IrcChannel();
-//};
-//
+class Irc;
+class IrcServer;
+
+class IrcChannel : public Channel, public ChannelChatters
+{
+public:
+ explicit IrcChannel(const QString &name, IrcServer *server);
+
+ void sendMessage(const QString &message) override;
+
+ // server may be nullptr
+ IrcServer *server();
+
+ // Channel methods
+ virtual bool canReconnect() const override;
+ virtual void reconnect() override;
+
+private:
+ void setServer(IrcServer *server);
+
+ IrcServer *server_;
+
+ friend class Irc;
+};
+
} // namespace chatterino
diff --git a/src/providers/irc/IrcCommands.cpp b/src/providers/irc/IrcCommands.cpp
new file mode 100644
index 000000000..87d07c8d9
--- /dev/null
+++ b/src/providers/irc/IrcCommands.cpp
@@ -0,0 +1,76 @@
+#include "IrcCommands.hpp"
+
+#include "messages/MessageBuilder.hpp"
+#include "providers/irc/IrcChannel2.hpp"
+#include "providers/irc/IrcServer.hpp"
+#include "util/Overloaded.hpp"
+#include "util/QStringHash.hpp"
+
+namespace chatterino {
+
+Outcome invokeIrcCommand(const QString &commandName, const QString &allParams,
+ IrcChannel &channel)
+{
+ if (!channel.server())
+ {
+ return Failure;
+ }
+
+ // STATIC MESSAGES
+ static auto staticMessages = std::unordered_map{
+ {"join", "/join is not supported. Press ctrl+r to change the "
+ "channel. If required use /raw JOIN #channel."},
+ {"part", "/part is not supported. Press ctrl+r to change the "
+ "channel. If required use /raw PART #channel."},
+ };
+ auto cmd = commandName.toLower();
+
+ if (auto it = staticMessages.find(cmd); it != staticMessages.end())
+ {
+ channel.addMessage(makeSystemMessage(it->second));
+ return Success;
+ }
+
+ // CUSTOM COMMANDS
+ auto params = allParams.split(' ');
+ auto paramsAfter = [&](int i) { return params.mid(i + 1).join(' '); };
+
+ auto sendRaw = [&](QString str) { channel.server()->sendRawMessage(str); };
+
+ if (cmd == "msg")
+ {
+ sendRaw("PRIVMSG " + params[0] + " :" + paramsAfter(0));
+ }
+ else if (cmd == "away")
+ {
+ sendRaw("AWAY" + params[0] + " :" + paramsAfter(0));
+ }
+ else if (cmd == "knock")
+ {
+ sendRaw("KNOCK #" + params[0] + " " + paramsAfter(0));
+ }
+ else if (cmd == "kick")
+ {
+ if (paramsAfter(1).isEmpty())
+ sendRaw("KICK " + params[0] + " " + params[1]);
+ else
+ sendRaw("KICK " + params[0] + " " + params[1] + " :" +
+ paramsAfter(1));
+ }
+ else if (cmd == "wallops")
+ {
+ sendRaw("WALLOPS :" + allParams);
+ }
+ else if (cmd == "raw")
+ {
+ sendRaw(allParams);
+ }
+ else
+ {
+ sendRaw(cmd.toUpper() + " " + allParams);
+ }
+
+ return Success;
+}
+
+} // namespace chatterino
diff --git a/src/providers/irc/IrcCommands.hpp b/src/providers/irc/IrcCommands.hpp
new file mode 100644
index 000000000..ffdbde023
--- /dev/null
+++ b/src/providers/irc/IrcCommands.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+#include "common/Outcome.hpp"
+
+namespace chatterino {
+
+class IrcChannel;
+
+Outcome invokeIrcCommand(const QString &command, const QString ¶ms,
+ IrcChannel &channel);
+
+} // namespace chatterino
diff --git a/src/providers/irc/IrcConnection2.cpp b/src/providers/irc/IrcConnection2.cpp
index 0f164d8b0..0ba639247 100644
--- a/src/providers/irc/IrcConnection2.cpp
+++ b/src/providers/irc/IrcConnection2.cpp
@@ -13,7 +13,7 @@ IrcConnection::IrcConnection(QObject *parent)
{
if (!this->recentlyReceivedMessage_.load())
{
- this->sendRaw("PING");
+ this->sendRaw("PING chatterino/ping");
this->reconnectTimer_.start();
}
this->recentlyReceivedMessage_ = false;
diff --git a/src/providers/irc/IrcServer.cpp b/src/providers/irc/IrcServer.cpp
index ba96379d3..2266996c6 100644
--- a/src/providers/irc/IrcServer.cpp
+++ b/src/providers/irc/IrcServer.cpp
@@ -1,12 +1,260 @@
#include "IrcServer.hpp"
#include
+#include
+
+#include "messages/Message.hpp"
+#include "messages/MessageBuilder.hpp"
+#include "providers/irc/Irc2.hpp"
+#include "providers/irc/IrcChannel2.hpp"
+#include "singletons/Settings.hpp"
+#include "util/QObjectRef.hpp"
namespace chatterino {
-// IrcServer::IrcServer(const QString &hostname, int port)
-//{
-// this->initConnection();
-//}
-//
+IrcServer::IrcServer(const IrcServerData &data)
+ : data_(new IrcServerData(data))
+{
+ this->connect();
+}
+
+IrcServer::IrcServer(const IrcServerData &data,
+ const std::vector> &restoreChannels)
+ : IrcServer(data)
+{
+ for (auto &&weak : restoreChannels)
+ {
+ if (auto shared = weak.lock())
+ {
+ this->channels[shared->getName()] = weak;
+ }
+ }
+}
+
+IrcServer::~IrcServer()
+{
+ delete this->data_;
+}
+
+int IrcServer::id()
+{
+ return this->data_->id;
+}
+
+const QString &IrcServer::user()
+{
+ return this->data_->user;
+}
+
+const QString &IrcServer::nick()
+{
+ return this->data_->nick.isEmpty() ? this->data_->user : this->data_->nick;
+}
+
+void IrcServer::initializeConnection(IrcConnection *connection,
+ ConnectionType type)
+{
+ assert(type == Both);
+
+ connection->setSecure(this->data_->ssl);
+ connection->setHost(this->data_->host);
+ connection->setPort(this->data_->port);
+
+ connection->setUserName(this->data_->user);
+ connection->setNickName(this->data_->nick.isEmpty() ? this->data_->user
+ : this->data_->nick);
+ connection->setRealName(this->data_->real.isEmpty() ? this->data_->user
+ : this->data_->nick);
+
+ switch (this->data_->authType)
+ {
+ case IrcAuthType::Sasl:
+ connection->setSaslMechanism("PLAIN");
+ [[fallthrough]];
+ case IrcAuthType::Pass:
+ this->data_->getPassword(
+ this, [conn = new QObjectRef(connection) /* can't copy */,
+ this](const QString &password) mutable {
+ if (*conn)
+ {
+ (*conn)->setPassword(password);
+ this->open(Both);
+ }
+
+ delete conn;
+ });
+ break;
+ default:
+ this->open(Both);
+ }
+
+ QObject::connect(
+ connection, &Communi::IrcConnection::socketError, this,
+ [this](QAbstractSocket::SocketError error) {
+ static int index =
+ QAbstractSocket::staticMetaObject.indexOfEnumerator(
+ "SocketError");
+
+ std::lock_guard lock(this->channelMutex);
+
+ for (auto &&weak : this->channels)
+ if (auto shared = weak.lock())
+ shared->addMessage(makeSystemMessage(
+ QStringLiteral("Socket error: ") +
+ QAbstractSocket::staticMetaObject.enumerator(index)
+ .valueToKey(error)));
+ });
+
+ QObject::connect(connection, &Communi::IrcConnection::nickNameRequired,
+ this, [](const QString &reserved, QString *result) {
+ *result = reserved + (std::rand() % 100);
+ });
+
+ QObject::connect(connection, &Communi::IrcConnection::noticeMessageReceived,
+ this, [this](Communi::IrcNoticeMessage *message) {
+ MessageBuilder builder;
+
+ builder.emplace();
+ builder.emplace(
+ message->nick(), MessageElementFlag::Username);
+ builder.emplace(
+ "-> you:", MessageElementFlag::Username);
+ builder.emplace(message->content(),
+ MessageElementFlag::Text);
+
+ auto msg = builder.release();
+
+ for (auto &&weak : this->channels)
+ if (auto shared = weak.lock())
+ shared->addMessage(msg);
+ });
+}
+
+std::shared_ptr IrcServer::createChannel(const QString &channelName)
+{
+ return std::make_shared(channelName, this);
+}
+
+bool IrcServer::hasSeparateWriteConnection() const
+{
+ return false;
+}
+
+void IrcServer::onReadConnected(IrcConnection *connection)
+{
+ {
+ std::lock_guard lock(this->channelMutex);
+
+ for (auto &&command : this->data_->connectCommands)
+ {
+ connection->sendRaw(command + "\r\n");
+ }
+ }
+
+ AbstractIrcServer::onReadConnected(connection);
+}
+
+void IrcServer::privateMessageReceived(Communi::IrcPrivateMessage *message)
+{
+ auto target = message->target();
+ target = target.startsWith('#') ? target.mid(1) : target;
+
+ if (auto channel = this->getChannelOrEmpty(target); !channel->isEmpty())
+ {
+ MessageBuilder builder;
+
+ builder.emplace();
+ builder.emplace(message->nick() + ":",
+ MessageElementFlag::Username);
+ builder.emplace(message->content(),
+ MessageElementFlag::Text);
+
+ channel->addMessage(builder.release());
+ }
+}
+
+void IrcServer::readConnectionMessageReceived(Communi::IrcMessage *message)
+{
+ AbstractIrcServer::readConnectionMessageReceived(message);
+
+ switch (message->type())
+ {
+ case Communi::IrcMessage::Join:
+ {
+ auto x = static_cast(message);
+
+ if (auto it =
+ this->channels.find(this->cleanChannelName(x->channel()));
+ it != this->channels.end())
+ {
+ if (auto shared = it->lock())
+ {
+ if (message->nick() == this->data_->nick)
+ {
+ shared->addMessage(
+ MessageBuilder(systemMessage, "joined").release());
+ }
+ else
+ {
+ if (auto c =
+ dynamic_cast(shared.get()))
+ c->addJoinedUser(x->nick());
+ }
+ }
+ }
+ return;
+ }
+
+ case Communi::IrcMessage::Part:
+ {
+ auto x = static_cast(message);
+
+ if (auto it =
+ this->channels.find(this->cleanChannelName(x->channel()));
+ it != this->channels.end())
+ {
+ if (auto shared = it->lock())
+ {
+ if (message->nick() == this->data_->nick)
+ {
+ shared->addMessage(
+ MessageBuilder(systemMessage, "parted").release());
+ }
+ else
+ {
+ if (auto c =
+ dynamic_cast(shared.get()))
+ c->addPartedUser(x->nick());
+ }
+ }
+ }
+ return;
+ }
+
+ case Communi::IrcMessage::Pong:
+ case Communi::IrcMessage::Notice:
+ case Communi::IrcMessage::Private:
+ return;
+
+ default:
+ if (getSettings()->showUnhandledIrcMessages)
+ {
+ MessageBuilder builder;
+
+ builder.emplace();
+ builder.emplace(message->toData(),
+ MessageElementFlag::Text);
+ builder->flags.set(MessageFlag::Debug);
+
+ auto msg = builder.release();
+
+ for (auto &&weak : this->channels)
+ {
+ if (auto shared = weak.lock())
+ shared->addMessage(msg);
+ }
+ };
+ }
+}
+
} // namespace chatterino
diff --git a/src/providers/irc/IrcServer.hpp b/src/providers/irc/IrcServer.hpp
index 1998188c4..0b5a0aad5 100644
--- a/src/providers/irc/IrcServer.hpp
+++ b/src/providers/irc/IrcServer.hpp
@@ -5,20 +5,34 @@
namespace chatterino {
-// class IrcServer
-//{
-// public:
-// IrcServer(const QString &hostname, int port);
+struct IrcServerData;
-// void setAccount(std::shared_ptr newAccount);
-// std::shared_ptr getAccount() const;
+class IrcServer : public AbstractIrcServer
+{
+public:
+ explicit IrcServer(const IrcServerData &data);
+ IrcServer(const IrcServerData &data,
+ const std::vector> &restoreChannels);
+ ~IrcServer() override;
-// protected:
-// virtual void initializeConnection(Communi::IrcConnection *connection, bool
-// isReadConnection);
+ int id();
+ const QString &user();
+ const QString &nick();
+
+ // AbstractIrcServer interface
+protected:
+ void initializeConnection(IrcConnection *connection,
+ ConnectionType type) override;
+ std::shared_ptr createChannel(const QString &channelName) override;
+ bool hasSeparateWriteConnection() const override;
+
+ void onReadConnected(IrcConnection *connection) override;
+ void privateMessageReceived(Communi::IrcPrivateMessage *message) override;
+ void readConnectionMessageReceived(Communi::IrcMessage *message) override;
+
+private:
+ // pointer so we don't have to circle include Irc2.hpp
+ IrcServerData *data_;
+};
-// virtual void privateMessageReceived(Communi::IrcPrivateMessage *message);
-// virtual void messageReceived(Communi::IrcMessage *message);
-//};
-//
} // namespace chatterino
diff --git a/src/providers/twitch/ChatroomChannel.hpp b/src/providers/twitch/ChatroomChannel.hpp
index 7131dd9d1..2ed249141 100644
--- a/src/providers/twitch/ChatroomChannel.hpp
+++ b/src/providers/twitch/ChatroomChannel.hpp
@@ -20,7 +20,7 @@ protected:
QString chatroomOwnerId;
QString chatroomOwnerName;
- friend class TwitchServer;
+ friend class TwitchIrcServer;
friend class TwitchMessageBuilder;
friend class IrcMessageHandler;
};
diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp
index 8000350a1..3586c2a35 100644
--- a/src/providers/twitch/IrcMessageHandler.cpp
+++ b/src/providers/twitch/IrcMessageHandler.cpp
@@ -1,14 +1,16 @@
#include "IrcMessageHandler.hpp"
#include "Application.hpp"
+#include "controllers/accounts/AccountController.hpp"
#include "controllers/highlights/HighlightController.hpp"
#include "debug/Log.hpp"
#include "messages/LimitedQueue.hpp"
#include "messages/Message.hpp"
+#include "providers/twitch/TwitchAccountManager.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchHelpers.hpp"
+#include "providers/twitch/TwitchIrcServer.hpp"
#include "providers/twitch/TwitchMessageBuilder.hpp"
-#include "providers/twitch/TwitchServer.hpp"
#include "singletons/Resources.hpp"
#include "singletons/Settings.hpp"
#include "singletons/WindowManager.hpp"
@@ -85,7 +87,7 @@ std::vector IrcMessageHandler::parsePrivMessage(
}
void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message,
- TwitchServer &server)
+ TwitchIrcServer &server)
{
this->addMessage(message, message->target(), message->content(), server,
false, message->isAction());
@@ -93,8 +95,9 @@ void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message,
void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
const QString &target,
- const QString &content, TwitchServer &server,
- bool isSub, bool isAction)
+ const QString &content,
+ TwitchIrcServer &server, bool isSub,
+ bool isAction)
{
QString channelName;
if (!trimChannelName(target, channelName))
@@ -144,6 +147,10 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
}
chan->addMessage(msg);
+ if (auto chatters = dynamic_cast(chan.get()))
+ {
+ chatters->addRecentChatter(msg->displayName);
+ }
}
}
@@ -439,7 +446,7 @@ std::vector IrcMessageHandler::parseUserNoticeMessage(
}
void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message,
- TwitchServer &server)
+ TwitchIrcServer &server)
{
auto data = message->toData();
@@ -591,7 +598,12 @@ void IrcMessageHandler::handleJoinMessage(Communi::IrcMessage *message)
if (TwitchChannel *twitchChannel =
dynamic_cast(channel.get()))
{
- twitchChannel->addJoinedUser(message->nick());
+ if (message->nick() !=
+ getApp()->accounts->twitch.getCurrent()->getUserName() &&
+ getSettings()->showJoins.getValue())
+ {
+ twitchChannel->addJoinedUser(message->nick());
+ }
}
}
@@ -604,7 +616,12 @@ void IrcMessageHandler::handlePartMessage(Communi::IrcMessage *message)
if (TwitchChannel *twitchChannel =
dynamic_cast(channel.get()))
{
- twitchChannel->addPartedUser(message->nick());
+ if (message->nick() !=
+ getApp()->accounts->twitch.getCurrent()->getUserName() &&
+ getSettings()->showJoins.getValue())
+ {
+ twitchChannel->addPartedUser(message->nick());
+ }
}
}
diff --git a/src/providers/twitch/IrcMessageHandler.hpp b/src/providers/twitch/IrcMessageHandler.hpp
index 631c0d3d5..c0bc67d58 100644
--- a/src/providers/twitch/IrcMessageHandler.hpp
+++ b/src/providers/twitch/IrcMessageHandler.hpp
@@ -5,7 +5,7 @@
namespace chatterino {
-class TwitchServer;
+class TwitchIrcServer;
class Channel;
class IrcMessageHandler
@@ -23,7 +23,7 @@ public:
std::vector parsePrivMessage(
Channel *channel, Communi::IrcPrivateMessage *message);
void handlePrivMessage(Communi::IrcPrivateMessage *message,
- TwitchServer &server);
+ TwitchIrcServer &server);
void handleRoomStateMessage(Communi::IrcMessage *message);
void handleClearChatMessage(Communi::IrcMessage *message);
@@ -36,7 +36,7 @@ public:
std::vector parseUserNoticeMessage(
Channel *channel, Communi::IrcMessage *message);
void handleUserNoticeMessage(Communi::IrcMessage *message,
- TwitchServer &server);
+ TwitchIrcServer &server);
void handleModeMessage(Communi::IrcMessage *message);
@@ -51,8 +51,8 @@ public:
private:
void addMessage(Communi::IrcMessage *message, const QString &target,
- const QString &content, TwitchServer &server, bool isResub,
- bool isAction);
+ const QString &content, TwitchIrcServer &server,
+ bool isResub, bool isAction);
};
} // namespace chatterino
diff --git a/src/providers/twitch/PubsubClient.hpp b/src/providers/twitch/PubsubClient.hpp
index 9e2def087..132cd35a5 100644
--- a/src/providers/twitch/PubsubClient.hpp
+++ b/src/providers/twitch/PubsubClient.hpp
@@ -2,7 +2,7 @@
#include "providers/twitch/PubsubActions.hpp"
#include "providers/twitch/TwitchAccount.hpp"
-#include "providers/twitch/TwitchServer.hpp"
+#include "providers/twitch/TwitchIrcServer.hpp"
#include
#include
diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp
index 1b84d11be..907bdaf0a 100644
--- a/src/providers/twitch/TwitchChannel.cpp
+++ b/src/providers/twitch/TwitchChannel.cpp
@@ -79,6 +79,7 @@ TwitchChannel::TwitchChannel(const QString &name,
TwitchBadges &globalTwitchBadges, BttvEmotes &bttv,
FfzEmotes &ffz)
: Channel(name, Channel::Type::Twitch)
+ , ChannelChatters(*static_cast(this))
, subscriptionUrl_("https://www.twitch.tv/subs/" + name)
, channelUrl_("https://twitch.tv/" + name)
, popoutPlayerUrl_("https://player.twitch.tv/?channel=" + name)
@@ -280,69 +281,14 @@ bool TwitchChannel::hasHighRateLimit() const
return this->isMod() || this->isBroadcaster() || this->isVIP();
}
-void TwitchChannel::addRecentChatter(const MessagePtr &message)
+bool TwitchChannel::canReconnect() const
{
- this->chatters_.access()->insert(message->displayName);
+ return true;
}
-void TwitchChannel::addJoinedUser(const QString &user)
+void TwitchChannel::reconnect()
{
- auto app = getApp();
- if (user == app->accounts->twitch.getCurrent()->getUserName() ||
- !getSettings()->showJoins.getValue())
- {
- return;
- }
-
- auto joinedUsers = this->joinedUsers_.access();
- joinedUsers->append(user);
-
- if (!this->joinedUsersMergeQueued_)
- {
- this->joinedUsersMergeQueued_ = true;
-
- QTimer::singleShot(500, &this->lifetimeGuard_, [this] {
- auto joinedUsers = this->joinedUsers_.access();
-
- MessageBuilder builder(systemMessage,
- "Users joined: " + joinedUsers->join(", "));
- builder->flags.set(MessageFlag::Collapsed);
- joinedUsers->clear();
- this->addMessage(builder.release());
- this->joinedUsersMergeQueued_ = false;
- });
- }
-}
-
-void TwitchChannel::addPartedUser(const QString &user)
-{
- auto app = getApp();
-
- if (user == app->accounts->twitch.getCurrent()->getUserName() ||
- !getSettings()->showJoins.getValue())
- {
- return;
- }
-
- auto partedUsers = this->partedUsers_.access();
- partedUsers->append(user);
-
- if (!this->partedUsersMergeQueued_)
- {
- this->partedUsersMergeQueued_ = true;
-
- QTimer::singleShot(500, &this->lifetimeGuard_, [this] {
- auto partedUsers = this->partedUsers_.access();
-
- MessageBuilder builder(systemMessage,
- "Users parted: " + partedUsers->join(", "));
- builder->flags.set(MessageFlag::Collapsed);
- this->addMessage(builder.release());
- partedUsers->clear();
-
- this->partedUsersMergeQueued_ = false;
- });
- }
+ getApp()->twitch.server->connect();
}
QString TwitchChannel::roomId() const
@@ -384,11 +330,6 @@ AccessGuard
return this->streamStatus_.accessConst();
}
-AccessGuard TwitchChannel::accessChatters() const
-{
- return this->chatters_.accessConst();
-}
-
const TwitchBadges &TwitchChannel::globalTwitchBadges() const
{
return this->globalTwitchBadges_;
@@ -691,7 +632,7 @@ void TwitchChannel::refreshChatters()
auto pair = parseChatters(result.parseJson());
if (pair.first)
{
- *this->chatters_.access() = std::move(pair.second);
+ this->setChatters(std::move(pair.second));
}
return pair.first;
diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp
index c4da1309a..0d754ec1b 100644
--- a/src/providers/twitch/TwitchChannel.hpp
+++ b/src/providers/twitch/TwitchChannel.hpp
@@ -3,6 +3,7 @@
#include "common/Aliases.hpp"
#include "common/Atomic.hpp"
#include "common/Channel.hpp"
+#include "common/ChannelChatters.hpp"
#include "common/Outcome.hpp"
#include "common/UniqueAccess.hpp"
#include "common/UsernameSet.hpp"
@@ -29,9 +30,11 @@ class TwitchBadges;
class FfzEmotes;
class BttvEmotes;
-class TwitchServer;
+class TwitchIrcServer;
-class TwitchChannel : public Channel, pajlada::Signals::SignalHolder
+class TwitchChannel : public Channel,
+ public ChannelChatters,
+ pajlada::Signals::SignalHolder
{
public:
struct StreamStatus {
@@ -64,6 +67,8 @@ public:
bool isStaff() const;
virtual bool isBroadcaster() const override;
virtual bool hasHighRateLimit() const override;
+ virtual bool canReconnect() const override;
+ virtual void reconnect() override;
// Data
const QString &subscriptionUrl();
@@ -73,7 +78,6 @@ public:
QString roomId() const;
AccessGuard accessRoomModes() const;
AccessGuard accessStreamStatus() const;
- AccessGuard accessChatters() const;
// Emotes
const TwitchBadges &globalTwitchBadges() const;
@@ -101,9 +105,6 @@ public:
pajlada::Signals::NoArgSignal liveStatusChanged;
pajlada::Signals::NoArgSignal roomModesChanged;
-protected:
- void addRecentChatter(const MessagePtr &message) override;
-
private:
struct NameOptions {
QString displayName;
@@ -125,8 +126,6 @@ private:
void refreshCheerEmotes();
void loadRecentMessages();
- void addJoinedUser(const QString &user);
- void addPartedUser(const QString &user);
void setLive(bool newLiveStatus);
void setMod(bool value);
void setVIP(bool value);
@@ -140,7 +139,6 @@ private:
const QString popoutPlayerUrl_;
UniqueAccess streamStatus_;
UniqueAccess roomModes_;
- UniqueAccess chatters_; // maps 2 char prefix to set of names
// Emotes
TwitchBadges &globalTwitchBadges_;
@@ -163,18 +161,13 @@ private:
bool staff_ = false;
UniqueAccess roomID_;
- UniqueAccess joinedUsers_;
- bool joinedUsersMergeQueued_ = false;
- UniqueAccess partedUsers_;
- bool partedUsersMergeQueued_ = false;
-
// --
QString lastSentMessage_;
QObject lifetimeGuard_;
QTimer liveStatusTimer_;
QTimer chattersListTimer_;
- friend class TwitchServer;
+ friend class TwitchIrcServer;
friend class TwitchMessageBuilder;
friend class IrcMessageHandler;
};
diff --git a/src/providers/twitch/TwitchServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp
similarity index 85%
rename from src/providers/twitch/TwitchServer.cpp
rename to src/providers/twitch/TwitchIrcServer.cpp
index 1f6181e4b..5161f928e 100644
--- a/src/providers/twitch/TwitchServer.cpp
+++ b/src/providers/twitch/TwitchIrcServer.cpp
@@ -1,4 +1,4 @@
-#include "TwitchServer.hpp"
+#include "TwitchIrcServer.hpp"
#include "Application.hpp"
#include "common/Common.hpp"
@@ -38,13 +38,11 @@ namespace {
}
} // namespace
-TwitchServer::TwitchServer()
+TwitchIrcServer::TwitchIrcServer()
: whispersChannel(new Channel("/whispers", Channel::Type::TwitchWhispers))
, mentionsChannel(new Channel("/mentions", Channel::Type::TwitchMentions))
, watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching)
{
- qDebug() << "init TwitchServer";
-
this->pubsub = new PubSub;
// getSettings()->twitchSeperateWriteConnection.connect([this](auto, auto) {
@@ -53,7 +51,7 @@ TwitchServer::TwitchServer()
// false);
}
-void TwitchServer::initialize(Settings &settings, Paths &paths)
+void TwitchIrcServer::initialize(Settings &settings, Paths &paths)
{
getApp()->accounts->twitch.currentUserChanged.connect(
[this]() { postToThread([this] { this->connect(); }); });
@@ -63,11 +61,9 @@ void TwitchServer::initialize(Settings &settings, Paths &paths)
this->ffz.loadEmotes();
}
-void TwitchServer::initializeConnection(IrcConnection *connection, bool isRead,
- bool isWrite)
+void TwitchIrcServer::initializeConnection(IrcConnection *connection,
+ ConnectionType type)
{
- this->singleConnection_ = isRead == isWrite;
-
std::shared_ptr account =
getApp()->accounts->twitch.getCurrent();
@@ -97,9 +93,12 @@ void TwitchServer::initializeConnection(IrcConnection *connection, bool isRead,
// SSL enabled: irc://irc.chat.twitch.tv:6697
connection->setHost("irc.chat.twitch.tv");
connection->setPort(6697);
+
+ this->open(type);
}
-std::shared_ptr TwitchServer::createChannel(const QString &channelName)
+std::shared_ptr TwitchIrcServer::createChannel(
+ const QString &channelName)
{
std::shared_ptr channel;
if (isChatroom(channelName))
@@ -123,13 +122,17 @@ std::shared_ptr TwitchServer::createChannel(const QString &channelName)
return std::shared_ptr(channel);
}
-void TwitchServer::privateMessageReceived(Communi::IrcPrivateMessage *message)
+void TwitchIrcServer::privateMessageReceived(
+ Communi::IrcPrivateMessage *message)
{
IrcMessageHandler::getInstance().handlePrivMessage(message, *this);
}
-void TwitchServer::readConnectionMessageReceived(Communi::IrcMessage *message)
+void TwitchIrcServer::readConnectionMessageReceived(
+ Communi::IrcMessage *message)
{
+ AbstractIrcServer::readConnectionMessageReceived(message);
+
if (message->type() == Communi::IrcMessage::Type::Private)
{
// We already have a handler for private messages
@@ -155,7 +158,8 @@ void TwitchServer::readConnectionMessageReceived(Communi::IrcMessage *message)
}
}
-void TwitchServer::writeConnectionMessageReceived(Communi::IrcMessage *message)
+void TwitchIrcServer::writeConnectionMessageReceived(
+ Communi::IrcMessage *message)
{
const QString &command = message->command();
@@ -193,7 +197,7 @@ void TwitchServer::writeConnectionMessageReceived(Communi::IrcMessage *message)
}
}
-void TwitchServer::onReadConnected(IrcConnection *connection)
+void TwitchIrcServer::onReadConnected(IrcConnection *connection)
{
AbstractIrcServer::onReadConnected(connection);
@@ -202,7 +206,7 @@ void TwitchServer::onReadConnected(IrcConnection *connection)
connection->sendRaw("CAP REQ :twitch.tv/tags twitch.tv/membership");
}
-void TwitchServer::onWriteConnected(IrcConnection *connection)
+void TwitchIrcServer::onWriteConnected(IrcConnection *connection)
{
AbstractIrcServer::onWriteConnected(connection);
@@ -211,7 +215,7 @@ void TwitchServer::onWriteConnected(IrcConnection *connection)
connection->sendRaw("CAP REQ :twitch.tv/tags twitch.tv/commands");
}
-std::shared_ptr TwitchServer::getCustomChannel(
+std::shared_ptr TwitchIrcServer::getCustomChannel(
const QString &channelName)
{
if (channelName == "/whispers")
@@ -249,7 +253,7 @@ std::shared_ptr TwitchServer::getCustomChannel(
return nullptr;
}
-void TwitchServer::forEachChannelAndSpecialChannels(
+void TwitchIrcServer::forEachChannelAndSpecialChannels(
std::function func)
{
this->forEachChannel(func);
@@ -258,7 +262,7 @@ void TwitchServer::forEachChannelAndSpecialChannels(
func(this->mentionsChannel);
}
-std::shared_ptr TwitchServer::getChannelOrEmptyByID(
+std::shared_ptr TwitchIrcServer::getChannelOrEmptyByID(
const QString &channelId)
{
std::lock_guard lock(this->channelMutex);
@@ -283,19 +287,22 @@ std::shared_ptr TwitchServer::getChannelOrEmptyByID(
return Channel::getEmpty();
}
-QString TwitchServer::cleanChannelName(const QString &dirtyChannelName)
+QString TwitchIrcServer::cleanChannelName(const QString &dirtyChannelName)
{
- return dirtyChannelName.toLower();
+ if (dirtyChannelName.startsWith('#'))
+ return dirtyChannelName.mid(1).toLower();
+ else
+ return dirtyChannelName.toLower();
}
-bool TwitchServer::hasSeparateWriteConnection() const
+bool TwitchIrcServer::hasSeparateWriteConnection() const
{
return true;
// return getSettings()->twitchSeperateWriteConnection;
}
-void TwitchServer::onMessageSendRequested(TwitchChannel *channel,
- const QString &message, bool &sent)
+void TwitchIrcServer::onMessageSendRequested(TwitchChannel *channel,
+ const QString &message, bool &sent)
{
sent = false;
@@ -354,11 +361,11 @@ void TwitchServer::onMessageSendRequested(TwitchChannel *channel,
sent = true;
}
-const BttvEmotes &TwitchServer::getBttvEmotes() const
+const BttvEmotes &TwitchIrcServer::getBttvEmotes() const
{
return this->bttv;
}
-const FfzEmotes &TwitchServer::getFfzEmotes() const
+const FfzEmotes &TwitchIrcServer::getFfzEmotes() const
{
return this->ffz;
}
diff --git a/src/providers/twitch/TwitchServer.hpp b/src/providers/twitch/TwitchIrcServer.hpp
similarity index 90%
rename from src/providers/twitch/TwitchServer.hpp
rename to src/providers/twitch/TwitchIrcServer.hpp
index f037a72cc..b33f9a330 100644
--- a/src/providers/twitch/TwitchServer.hpp
+++ b/src/providers/twitch/TwitchIrcServer.hpp
@@ -20,11 +20,11 @@ class Paths;
class PubSub;
class TwitchChannel;
-class TwitchServer final : public AbstractIrcServer, public Singleton
+class TwitchIrcServer final : public AbstractIrcServer, public Singleton
{
public:
- TwitchServer();
- virtual ~TwitchServer() override = default;
+ TwitchIrcServer();
+ virtual ~TwitchIrcServer() override = default;
virtual void initialize(Settings &settings, Paths &paths) override;
@@ -44,8 +44,8 @@ public:
const FfzEmotes &getFfzEmotes() const;
protected:
- virtual void initializeConnection(IrcConnection *connection, bool isRead,
- bool isWrite) override;
+ virtual void initializeConnection(IrcConnection *connection,
+ ConnectionType type) override;
virtual std::shared_ptr createChannel(
const QString &channelName) override;
@@ -75,7 +75,6 @@ private:
std::chrono::steady_clock::time_point lastErrorTimeSpeed_;
std::chrono::steady_clock::time_point lastErrorTimeAmount_;
- bool singleConnection_ = false;
TwitchBadges twitchBadges;
BttvEmotes bttv;
FfzEmotes ffz;
diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp
index a79cdbd63..3f94877a1 100644
--- a/src/providers/twitch/TwitchMessageBuilder.cpp
+++ b/src/providers/twitch/TwitchMessageBuilder.cpp
@@ -10,7 +10,7 @@
#include "providers/chatterino/ChatterinoBadges.hpp"
#include "providers/twitch/TwitchBadges.hpp"
#include "providers/twitch/TwitchChannel.hpp"
-#include "providers/twitch/TwitchServer.hpp"
+#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Emotes.hpp"
#include "singletons/Resources.hpp"
#include "singletons/Settings.hpp"
diff --git a/src/singletons/NativeMessaging.cpp b/src/singletons/NativeMessaging.cpp
index 2a2853d2b..4d22f6fa4 100644
--- a/src/singletons/NativeMessaging.cpp
+++ b/src/singletons/NativeMessaging.cpp
@@ -1,7 +1,7 @@
#include "singletons/NativeMessaging.hpp"
#include "Application.hpp"
-#include "providers/twitch/TwitchServer.hpp"
+#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Paths.hpp"
#include "util/PostToThread.hpp"
diff --git a/src/singletons/Paths.cpp b/src/singletons/Paths.cpp
index 9164cb6ee..ea8e2526b 100644
--- a/src/singletons/Paths.cpp
+++ b/src/singletons/Paths.cpp
@@ -136,7 +136,7 @@ void Paths::initSubDirectories()
this->messageLogDirectory = makePath("Logs");
this->miscDirectory = makePath("Misc");
this->twitchProfileAvatars = makePath("ProfileAvatars");
- QDir().mkdir(this->twitchProfileAvatars + "/twitch");
+ //QDir().mkdir(this->twitchProfileAvatars + "/twitch");
}
Paths *getPaths()
diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp
index d5b0ad881..a516cdd2a 100644
--- a/src/singletons/Settings.hpp
+++ b/src/singletons/Settings.hpp
@@ -201,6 +201,10 @@ public:
/// Misc
BoolSetting betaUpdates = {"/misc/beta", false};
+#ifdef Q_OS_LINUX
+ BoolSetting useKeyring = {"/misc/useKeyring", true};
+#endif
+
IntSetting startUpNotification = {"/misc/startUpNotification", 0};
QStringSetting currentVersion = {"/misc/currentVersion", ""};
BoolSetting loadTwitchMessageHistoryOnConnect = {
@@ -210,6 +214,15 @@ public:
QStringSetting cachePath = {"/cache/path", ""};
+ /// Debug
+ BoolSetting showUnhandledIrcMessages = {"/debug/showUnhandledIrcMessages",
+ false};
+
+ /// UI
+ // Purely QOL settings are here (like last item in a list).
+ IntSetting lastSelectChannelTab = {"/ui/lastSelectChannelTab", 0};
+ IntSetting lastSelectIrcConn = {"/ui/lastSelectIrcConn", 0};
+
private:
void updateModerationActions();
};
diff --git a/src/singletons/Toasts.cpp b/src/singletons/Toasts.cpp
index c72b43e58..e550b633c 100644
--- a/src/singletons/Toasts.cpp
+++ b/src/singletons/Toasts.cpp
@@ -6,7 +6,7 @@
#include "controllers/notifications/NotificationController.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchCommon.hpp"
-#include "providers/twitch/TwitchServer.hpp"
+#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Paths.hpp"
#include "util/StreamLink.hpp"
#include "widgets/helper/CommonTexts.hpp"
diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp
index 2afd7c62d..abafc90dc 100644
--- a/src/singletons/WindowManager.cpp
+++ b/src/singletons/WindowManager.cpp
@@ -4,7 +4,10 @@
#include "debug/AssertInGuiThread.hpp"
#include "debug/Log.hpp"
#include "messages/MessageElement.hpp"
-#include "providers/twitch/TwitchServer.hpp"
+#include "providers/irc/Irc2.hpp"
+#include "providers/irc/IrcChannel2.hpp"
+#include "providers/irc/IrcServer.hpp"
+#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Fonts.hpp"
#include "singletons/Paths.hpp"
#include "singletons/Settings.hpp"
@@ -611,6 +614,20 @@ void WindowManager::encodeChannel(IndirectChannel channel, QJsonObject &obj)
obj.insert("type", "whispers");
}
break;
+ case Channel::Type::Irc:
+ {
+ if (auto ircChannel =
+ dynamic_cast(channel.get().get()))
+ {
+ obj.insert("type", "irc");
+ if (ircChannel->server())
+ {
+ obj.insert("server", ircChannel->server()->id());
+ }
+ obj.insert("channel", ircChannel->getName());
+ }
+ }
+ break;
}
}
@@ -638,6 +655,11 @@ IndirectChannel WindowManager::decodeChannel(const QJsonObject &obj)
{
return app->twitch.server->whispersChannel;
}
+ else if (type == "irc")
+ {
+ return Irc::getInstance().getOrAddChannel(
+ obj.value("server").toInt(-1), obj.value("channel").toString());
+ }
return Channel::getEmpty();
}
diff --git a/src/util/LayoutCreator.hpp b/src/util/LayoutCreator.hpp
index 43ed7bc95..4ca95db57 100644
--- a/src/util/LayoutCreator.hpp
+++ b/src/util/LayoutCreator.hpp
@@ -126,6 +126,20 @@ public:
return LayoutCreator(item);
}
+ template
+ LayoutCreator connect(Slot slot, QObject *receiver, Func func)
+ {
+ QObject::connect(this->getElement(), slot, receiver, func);
+ return *this;
+ }
+
+ template
+ LayoutCreator onClick(QObject *receiver, Func func)
+ {
+ QObject::connect(this->getElement(), &T::clicked, receiver, func);
+ return *this;
+ }
+
private:
T *item_;
@@ -169,4 +183,12 @@ private:
}
};
+template
+LayoutCreator makeDialog(Args &&... args)
+{
+ T *t = new T(std::forward(args)...);
+ t->setAttribute(Qt::WA_DeleteOnClose);
+ return LayoutCreator(t);
+}
+
} // namespace chatterino
diff --git a/src/util/Overloaded.hpp b/src/util/Overloaded.hpp
new file mode 100644
index 000000000..7deb38a67
--- /dev/null
+++ b/src/util/Overloaded.hpp
@@ -0,0 +1,13 @@
+#pragma once
+
+namespace chatterino {
+
+template
+struct Overloaded : Ts... {
+ using Ts::operator()...;
+};
+
+template
+Overloaded(Ts...)->Overloaded;
+
+} // namespace chatterino
diff --git a/src/util/QObjectRef.hpp b/src/util/QObjectRef.hpp
index cf8ad69ee..4a15545d1 100644
--- a/src/util/QObjectRef.hpp
+++ b/src/util/QObjectRef.hpp
@@ -62,8 +62,9 @@ private:
if (other)
{
this->conn_ =
- QObject::connect(other, &QObject::destroyed,
- [this](QObject *) { this->set(nullptr); });
+ QObject::connect(other, &QObject::destroyed, qApp,
+ [this](QObject *) { this->set(nullptr); },
+ Qt::DirectConnection);
}
this->t_ = other;
diff --git a/src/util/StandardItemHelper.hpp b/src/util/StandardItemHelper.hpp
index 1c54a3f21..7b120e4d7 100644
--- a/src/util/StandardItemHelper.hpp
+++ b/src/util/StandardItemHelper.hpp
@@ -7,7 +7,7 @@ namespace chatterino {
static void setBoolItem(QStandardItem *item, bool value,
bool userCheckable = true, bool selectable = true)
{
- item->setFlags((Qt::ItemFlags)(
+ item->setFlags(Qt::ItemFlags(
Qt::ItemIsEnabled | (selectable ? Qt::ItemIsSelectable : 0) |
(userCheckable ? Qt::ItemIsUserCheckable : 0)));
item->setCheckState(value ? Qt::Checked : Qt::Unchecked);
@@ -17,15 +17,15 @@ static void setStringItem(QStandardItem *item, const QString &value,
bool editable = true, bool selectable = true)
{
item->setData(value, Qt::EditRole);
- item->setFlags((Qt::ItemFlags)(Qt::ItemIsEnabled |
- (selectable ? Qt::ItemIsSelectable : 0) |
- (editable ? (Qt::ItemIsEditable) : 0)));
+ item->setFlags(Qt::ItemFlags(Qt::ItemIsEnabled |
+ (selectable ? Qt::ItemIsSelectable : 0) |
+ (editable ? (Qt::ItemIsEditable) : 0)));
}
static QStandardItem *emptyItem()
{
auto *item = new QStandardItem();
- item->setFlags((Qt::ItemFlags)0);
+ item->setFlags(Qt::ItemFlags(0));
return item;
}
diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp
index 1a597dc40..0c4838abf 100644
--- a/src/widgets/Window.cpp
+++ b/src/widgets/Window.cpp
@@ -1,10 +1,11 @@
#include "widgets/Window.hpp"
#include "Application.hpp"
+#include "common/Credentials.hpp"
#include "common/Modes.hpp"
#include "common/Version.hpp"
#include "controllers/accounts/AccountController.hpp"
-#include "providers/twitch/TwitchServer.hpp"
+#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
#include "singletons/Updates.hpp"
@@ -108,7 +109,7 @@ bool Window::event(QEvent *event)
break;
default:;
- };
+ }
return BaseWindow::event(event);
}
diff --git a/src/widgets/dialogs/IrcConnectionEditor.cpp b/src/widgets/dialogs/IrcConnectionEditor.cpp
new file mode 100644
index 000000000..055b229fe
--- /dev/null
+++ b/src/widgets/dialogs/IrcConnectionEditor.cpp
@@ -0,0 +1,97 @@
+#include "IrcConnectionEditor.hpp"
+#include "ui_IrcConnectionEditor.h"
+
+namespace chatterino {
+
+IrcConnectionEditor::IrcConnectionEditor(const IrcServerData &data, bool isAdd,
+ QWidget *parent)
+
+ : QDialog(parent, Qt::WindowStaysOnTopHint)
+ , ui_(new Ui::IrcConnectionEditor)
+ , data_(data)
+{
+ this->ui_->setupUi(this);
+
+ this->setWindowTitle(QString(isAdd ? "Add " : "Edit ") + "Irc Connection");
+
+ QObject::connect(this->ui_->userNameLineEdit, &QLineEdit::textChanged, this,
+ [this](const QString &text) {
+ this->ui_->nickNameLineEdit->setPlaceholderText(text);
+ this->ui_->realNameLineEdit->setPlaceholderText(text);
+ });
+
+ this->ui_->serverLineEdit->setText(data.host);
+ this->ui_->portSpinBox->setValue(data.port);
+ this->ui_->securityCheckBox->setChecked(data.ssl);
+ this->ui_->userNameLineEdit->setText(data.user);
+ this->ui_->nickNameLineEdit->setText(data.nick);
+ this->ui_->realNameLineEdit->setText(data.real);
+ this->ui_->connectCommandsEditor->setPlainText(
+ data.connectCommands.join('\n'));
+
+ data.getPassword(this, [this](const QString &password) {
+ this->ui_->passwordLineEdit->setText(password);
+ });
+
+ this->ui_->loginMethodComboBox->setCurrentIndex([&] {
+ switch (data.authType)
+ {
+ case IrcAuthType::Custom:
+ return 1;
+ case IrcAuthType::Pass:
+ return 2;
+ case IrcAuthType::Sasl:
+ return 3;
+ default:
+ return 0;
+ }
+ }());
+
+ QObject::connect(this->ui_->loginMethodComboBox,
+ qOverload(&QComboBox::currentIndexChanged), this,
+ [this](int index) {
+ if (index == 1) // Custom
+ {
+ this->ui_->connectCommandsEditor->setFocus();
+ }
+ });
+
+ QFont font("Monospace");
+ font.setStyleHint(QFont::TypeWriter);
+ this->ui_->connectCommandsEditor->setFont(font);
+}
+
+IrcConnectionEditor::~IrcConnectionEditor()
+{
+ delete ui_;
+}
+
+IrcServerData IrcConnectionEditor::data()
+{
+ auto data = this->data_;
+ data.host = this->ui_->serverLineEdit->text();
+ data.port = this->ui_->portSpinBox->value();
+ data.ssl = this->ui_->securityCheckBox->isChecked();
+ data.user = this->ui_->userNameLineEdit->text();
+ data.nick = this->ui_->nickNameLineEdit->text();
+ data.real = this->ui_->realNameLineEdit->text();
+ data.connectCommands =
+ this->ui_->connectCommandsEditor->toPlainText().split('\n');
+ data.setPassword(this->ui_->passwordLineEdit->text());
+ data.authType = [this] {
+ switch (this->ui_->loginMethodComboBox->currentIndex())
+ {
+ case 1:
+ return IrcAuthType::Custom;
+ case 2:
+ return IrcAuthType::Pass;
+ case 3:
+ return IrcAuthType::Sasl;
+ default:
+ return IrcAuthType::Anonymous;
+ }
+ }();
+ return data;
+}
+
+} // namespace chatterino
diff --git a/src/widgets/dialogs/IrcConnectionEditor.hpp b/src/widgets/dialogs/IrcConnectionEditor.hpp
new file mode 100644
index 000000000..958b2a2f5
--- /dev/null
+++ b/src/widgets/dialogs/IrcConnectionEditor.hpp
@@ -0,0 +1,32 @@
+#pragma once
+
+#include
+
+#include "providers/irc/Irc2.hpp"
+#include "widgets/BaseWindow.hpp"
+
+namespace Ui {
+class IrcConnectionEditor;
+}
+
+namespace chatterino {
+
+struct IrcServerData;
+
+class IrcConnectionEditor : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit IrcConnectionEditor(const IrcServerData &data, bool isAdd = false,
+ QWidget *parent = nullptr);
+ ~IrcConnectionEditor();
+
+ IrcServerData data();
+
+private:
+ Ui::IrcConnectionEditor *ui_;
+ IrcServerData data_;
+};
+
+} // namespace chatterino
diff --git a/src/widgets/dialogs/IrcConnectionEditor.ui b/src/widgets/dialogs/IrcConnectionEditor.ui
new file mode 100644
index 000000000..516fe2b55
--- /dev/null
+++ b/src/widgets/dialogs/IrcConnectionEditor.ui
@@ -0,0 +1,261 @@
+
+
+ IrcConnectionEditor
+
+
+
+ 0
+ 0
+ 329
+ 414
+
+
+
+ Dialog
+
+
+ -
+
+
-
+
+
+ Host:
+
+
+
+ -
+
+
+ irc.example.com
+
+
+
+ -
+
+
+ Port:
+
+
+
+ -
+
+
+ 65636
+
+
+ 6697
+
+
+
+ -
+
+
+ SSL:
+
+
+
+ -
+
+
+ true
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 20
+
+
+
+
+ -
+
+
+ User Name:
+
+
+
+ -
+
+
+ -
+
+
+ Nick Name:
+
+
+
+ -
+
+
+ -
+
+
+ Real Name:
+
+
+
+ -
+
+
+ -
+
+
+ Login method:
+
+
+
+ -
+
+
+ Password:
+
+
+
+ -
+
+
+ QLineEdit::Password
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 20
+
+
+
+
+ -
+
+
-
+
+ Anonymous
+
+
+ -
+
+ Custom
+
+
+ -
+
+ Server Password (/PASS $password)
+
+
+ -
+
+ SASL
+
+
+
+
+ -
+
+
+ Send IRC commands
+on connect:
+
+
+ Qt::PlainText
+
+
+
+ -
+
+
+
+ 0
+ 1
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 20
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+ serverLineEdit
+ portSpinBox
+ securityCheckBox
+ userNameLineEdit
+ nickNameLineEdit
+ realNameLineEdit
+ loginMethodComboBox
+ passwordLineEdit
+
+
+
+
+ buttonBox
+ accepted()
+ IrcConnectionEditor
+ accept()
+
+
+ 254
+ 248
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ IrcConnectionEditor
+ reject()
+
+
+ 254
+ 248
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/src/widgets/dialogs/SelectChannelDialog.cpp b/src/widgets/dialogs/SelectChannelDialog.cpp
index b0aedfa81..e95c90340 100644
--- a/src/widgets/dialogs/SelectChannelDialog.cpp
+++ b/src/widgets/dialogs/SelectChannelDialog.cpp
@@ -1,10 +1,11 @@
#include "SelectChannelDialog.hpp"
#include "Application.hpp"
-#include "providers/twitch/TwitchServer.hpp"
+#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Theme.hpp"
#include "util/LayoutCreator.hpp"
#include "widgets/Notebook.hpp"
+#include "widgets/dialogs/IrcConnectionEditor.hpp"
#include "widgets/helper/NotebookTab.hpp"
#include
@@ -14,7 +15,12 @@
#include
#include
+#include
+#include "providers/irc/Irc2.hpp"
+#include "widgets/helper/EditableModelView.hpp"
+
#define TAB_TWITCH 0
+#define TAB_IRC 1
namespace chatterino {
@@ -122,21 +128,69 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent)
}
// irc
- /*
+ {
+ LayoutCreator obj(new QWidget());
+ auto outerBox = obj.setLayoutType();
+
{
- LayoutCreator obj(new QWidget());
- auto vbox = obj.setLayoutType();
- auto form = vbox.emplace();
+ auto view = this->ui_.irc.servers = new EditableModelView(
+ Irc::getInstance().newConnectionModel(this));
- form->addRow(new QLabel("User name:"), new QLineEdit());
- form->addRow(new QLabel("First nick choice:"), new QLineEdit());
- form->addRow(new QLabel("Second nick choice:"), new QLineEdit());
- form->addRow(new QLabel("Third nick choice:"), new QLineEdit());
+ view->setTitles({"host", "port", "ssl", "user", "nick", "real",
+ "password", "login command"});
+ view->getTableView()->horizontalHeader()->resizeSection(0, 140);
- auto tab = notebook->addPage(obj.getElement());
- tab->setCustomTitle("Irc");
+ view->getTableView()->horizontalHeader()->setSectionHidden(1, true);
+ view->getTableView()->horizontalHeader()->setSectionHidden(2, true);
+ view->getTableView()->horizontalHeader()->setSectionHidden(4, true);
+ view->getTableView()->horizontalHeader()->setSectionHidden(5, true);
+
+ view->addButtonPressed.connect([] {
+ auto unique = IrcServerData{};
+ unique.id = Irc::getInstance().uniqueId();
+
+ auto editor = new IrcConnectionEditor(unique);
+ if (editor->exec() == QDialog::Accepted)
+ {
+ Irc::getInstance().connections.appendItem(editor->data());
+ }
+ });
+
+ QObject::connect(
+ view->getTableView(), &QTableView::doubleClicked,
+ [](const QModelIndex &index) {
+ auto editor = new IrcConnectionEditor(
+ Irc::getInstance()
+ .connections.getVector()[size_t(index.row())]);
+
+ if (editor->exec() == QDialog::Accepted)
+ {
+ auto data = editor->data();
+ auto &&conns =
+ Irc::getInstance().connections.getVector();
+ int i = 0;
+ for (auto &&conn : conns)
+ {
+ if (conn.id == data.id)
+ {
+ Irc::getInstance().connections.removeItem(
+ i, Irc::noEraseCredentialCaller);
+ Irc::getInstance().connections.insertItem(data,
+ i);
+ }
+ i++;
+ }
+ }
+ });
+
+ outerBox->addRow("Server:", view);
}
- */
+
+ outerBox->addRow("Channel:", this->ui_.irc.channel = new QLineEdit);
+
+ auto tab = notebook->addPage(obj.getElement());
+ tab->setCustomTitle("Irc (Beta)");
+ }
layout->setStretchFactor(notebook.getElement(), 1);
@@ -151,7 +205,7 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent)
[=](bool) { this->close(); });
}
- this->setScaleIndependantSize(300, 310);
+ this->setMinimumSize(300, 310);
this->ui_.notebook->selectIndex(TAB_TWITCH);
this->ui_.twitch.channel->setFocus();
@@ -161,10 +215,24 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent)
auto *shortcut_cancel = new QShortcut(QKeySequence("Esc"), this);
QObject::connect(shortcut_cancel, &QShortcut::activated,
[=] { this->close(); });
+
+ // restore ui state
+ this->ui_.notebook->selectIndex(getSettings()->lastSelectChannelTab);
+ this->ui_.irc.servers->getTableView()->selectRow(
+ getSettings()->lastSelectIrcConn);
}
void SelectChannelDialog::ok()
{
+ // save ui state
+ getSettings()->lastSelectChannelTab =
+ this->ui_.notebook->getSelectedIndex();
+ getSettings()->lastSelectIrcConn = this->ui_.irc.servers->getTableView()
+ ->selectionModel()
+ ->currentIndex()
+ .row();
+
+ // accept and close
this->hasSelectedChannel_ = true;
this->close();
}
@@ -204,6 +272,32 @@ void SelectChannelDialog::setSelectedChannel(IndirectChannel _channel)
this->ui_.twitch.whispers->setFocus();
}
break;
+ case Channel::Type::Irc:
+ {
+ this->ui_.notebook->selectIndex(TAB_IRC);
+ this->ui_.irc.channel->setText(_channel.get()->getName());
+
+ if (auto ircChannel =
+ dynamic_cast(_channel.get().get()))
+ {
+ if (auto server = ircChannel->server())
+ {
+ int i = 0;
+ for (auto &&conn : Irc::getInstance().connections)
+ {
+ if (conn.id == server->id())
+ {
+ this->ui_.irc.servers->getTableView()->selectRow(i);
+ break;
+ }
+ i++;
+ }
+ }
+ }
+
+ this->ui_.irc.channel->setFocus();
+ }
+ break;
default:
{
this->ui_.notebook->selectIndex(TAB_TWITCH);
@@ -245,6 +339,27 @@ IndirectChannel SelectChannelDialog::getSelectedChannel() const
return app->twitch.server->whispersChannel;
}
}
+ break;
+ case TAB_IRC:
+ {
+ int row = this->ui_.irc.servers->getTableView()
+ ->selectionModel()
+ ->currentIndex()
+ .row();
+
+ auto &&vector = Irc::getInstance().connections.getVector();
+
+ if (row >= 0 && row < int(vector.size()))
+ {
+ return Irc::getInstance().getOrAddChannel(
+ vector[size_t(row)].id, this->ui_.irc.channel->text());
+ }
+ else
+ {
+ return Channel::getEmpty();
+ }
+ }
+ //break;
}
return this->selectedChannel_;
@@ -258,7 +373,7 @@ bool SelectChannelDialog::hasSeletedChannel() const
bool SelectChannelDialog::EventFilter::eventFilter(QObject *watched,
QEvent *event)
{
- auto *widget = (QWidget *)watched;
+ auto *widget = static_cast(watched);
if (event->type() == QEvent::FocusIn)
{
diff --git a/src/widgets/dialogs/SelectChannelDialog.hpp b/src/widgets/dialogs/SelectChannelDialog.hpp
index 8fb42c01d..229cb4c51 100644
--- a/src/widgets/dialogs/SelectChannelDialog.hpp
+++ b/src/widgets/dialogs/SelectChannelDialog.hpp
@@ -12,6 +12,7 @@
namespace chatterino {
class Notebook;
+class EditableModelView;
class SelectChannelDialog final : public BaseWindow
{
@@ -47,6 +48,10 @@ private:
QRadioButton *mentions;
QRadioButton *watching;
} twitch;
+ struct {
+ QLineEdit *channel;
+ EditableModelView *servers;
+ } irc;
} ui_;
EventFilter tabFilter_;
diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp
index 362cc39e8..d9dc36a64 100644
--- a/src/widgets/helper/ChannelView.cpp
+++ b/src/widgets/helper/ChannelView.cpp
@@ -12,7 +12,7 @@
#include "messages/layouts/MessageLayout.hpp"
#include "messages/layouts/MessageLayoutElement.hpp"
#include "providers/twitch/TwitchChannel.hpp"
-#include "providers/twitch/TwitchServer.hpp"
+#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
#include "singletons/TooltipPreviewImage.hpp"
diff --git a/src/widgets/settingspages/AboutPage.cpp b/src/widgets/settingspages/AboutPage.cpp
index c6b50fb60..71d0a01dc 100644
--- a/src/widgets/settingspages/AboutPage.cpp
+++ b/src/widgets/settingspages/AboutPage.cpp
@@ -98,6 +98,9 @@ AboutPage::AboutPage()
addLicense(form.getElement(), "Websocketpp",
"https://www.zaphoyd.com/websocketpp/",
":/licenses/websocketpp.txt");
+ addLicense(form.getElement(), "QtKeychain",
+ "https://github.com/frankosterfeld/qtkeychain",
+ ":/licenses/qtkeychain.txt");
}
auto attributions = layout.emplace("Attributions...");
diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp
index 93579a046..d49ac2da0 100644
--- a/src/widgets/settingspages/GeneralPage.cpp
+++ b/src/widgets/settingspages/GeneralPage.cpp
@@ -451,6 +451,16 @@ void GeneralPage::initLayout(SettingsLayout &layout)
layout.addCheckbox("Open links in incognito/private mode",
s.openLinksIncognito);
}
+
+#ifdef Q_OS_LINUX
+ if (!getPaths()->isPortable())
+ {
+ layout.addCheckbox(
+ "Use libsecret/KWallet/Gnome keychain to secure passwords",
+ s.useKeyring);
+ }
+#endif
+
layout.addCheckbox("Show moderation messages", s.hideModerationActions,
true);
layout.addCheckbox("Random username color for users who never set a color",
@@ -486,6 +496,9 @@ void GeneralPage::initLayout(SettingsLayout &layout)
layout.addCheckbox("Load message history on connect",
s.loadTwitchMessageHistoryOnConnect);
+ layout.addCheckbox("Show unhandled irc messages",
+ s.showUnhandledIrcMessages);
+
layout.addTitle("Cache");
layout.addDescription(
"Files that are used often (such as emotes) are saved to disk to "
diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp
index 744b51921..209baa028 100644
--- a/src/widgets/splits/Split.cpp
+++ b/src/widgets/splits/Split.cpp
@@ -7,7 +7,7 @@
#include "providers/twitch/EmoteValue.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchMessageBuilder.hpp"
-#include "providers/twitch/TwitchServer.hpp"
+#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
#include "singletons/WindowManager.hpp"
diff --git a/src/widgets/splits/SplitHeader.cpp b/src/widgets/splits/SplitHeader.cpp
index 55802be5d..5a7d018b6 100644
--- a/src/widgets/splits/SplitHeader.cpp
+++ b/src/widgets/splits/SplitHeader.cpp
@@ -6,7 +6,7 @@
#include "controllers/notifications/NotificationController.hpp"
#include "controllers/pings/PingController.hpp"
#include "providers/twitch/TwitchChannel.hpp"
-#include "providers/twitch/TwitchServer.hpp"
+#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Resources.hpp"
#include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
@@ -211,9 +211,12 @@ void SplitHeader::initializeLayout()
}),
// dropdown
this->dropdownButton_ = makeWidget