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