Merge branch 'master' into moderation

This commit is contained in:
fourtf 2019-09-18 16:14:45 +02:00 committed by GitHub
commit 5ca0fc0c8f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 2151 additions and 305 deletions

3
.gitmodules vendored
View file

@ -22,6 +22,9 @@
[submodule "lib/rapidjson"] [submodule "lib/rapidjson"]
path = lib/rapidjson path = lib/rapidjson
url = https://github.com/Tencent/rapidjson url = https://github.com/Tencent/rapidjson
[submodule "lib/qtkeychain"]
path = lib/qtkeychain
url = https://github.com/Chatterino/qtkeychain
[submodule "lib/websocketpp"] [submodule "lib/websocketpp"]
path = lib/websocketpp path = lib/websocketpp
url = https://github.com/ziocleto/websocketpp url = https://github.com/ziocleto/websocketpp

View file

@ -40,8 +40,6 @@ macx {
} }
# Submodules # Submodules
DEFINES += IRC_NAMESPACE=Communi
include(lib/warnings.pri) include(lib/warnings.pri)
include(lib/appbase.pri) include(lib/appbase.pri)
include(lib/fmt.pri) include(lib/fmt.pri)
@ -54,6 +52,7 @@ include(lib/settings.pri)
include(lib/serialize.pri) include(lib/serialize.pri)
include(lib/winsdk.pri) include(lib/winsdk.pri)
include(lib/rapidjson.pri) include(lib/rapidjson.pri)
include(lib/qtkeychain.pri)
exists( $$OUT_PWD/conanbuildinfo.pri ) { exists( $$OUT_PWD/conanbuildinfo.pri ) {
message("Using conan packages") message("Using conan packages")
@ -78,7 +77,9 @@ SOURCES += \
src/autogenerated/ResourcesAutogen.cpp \ src/autogenerated/ResourcesAutogen.cpp \
src/BrowserExtension.cpp \ src/BrowserExtension.cpp \
src/common/Channel.cpp \ src/common/Channel.cpp \
src/common/ChannelChatters.cpp \
src/common/CompletionModel.cpp \ src/common/CompletionModel.cpp \
src/common/Credentials.cpp \
src/common/DownloadManager.cpp \ src/common/DownloadManager.cpp \
src/common/Env.cpp \ src/common/Env.cpp \
src/common/LinkParser.cpp \ src/common/LinkParser.cpp \
@ -132,8 +133,10 @@ SOURCES += \
src/providers/emoji/Emojis.cpp \ src/providers/emoji/Emojis.cpp \
src/providers/ffz/FfzEmotes.cpp \ src/providers/ffz/FfzEmotes.cpp \
src/providers/irc/AbstractIrcServer.cpp \ src/providers/irc/AbstractIrcServer.cpp \
src/providers/irc/Irc2.cpp \
src/providers/irc/IrcAccount.cpp \ src/providers/irc/IrcAccount.cpp \
src/providers/irc/IrcChannel2.cpp \ src/providers/irc/IrcChannel2.cpp \
src/providers/irc/IrcCommands.cpp \
src/providers/irc/IrcConnection2.cpp \ src/providers/irc/IrcConnection2.cpp \
src/providers/irc/IrcServer.cpp \ src/providers/irc/IrcServer.cpp \
src/providers/LinkResolver.cpp \ src/providers/LinkResolver.cpp \
@ -150,9 +153,9 @@ SOURCES += \
src/providers/twitch/TwitchChannel.cpp \ src/providers/twitch/TwitchChannel.cpp \
src/providers/twitch/TwitchEmotes.cpp \ src/providers/twitch/TwitchEmotes.cpp \
src/providers/twitch/TwitchHelpers.cpp \ src/providers/twitch/TwitchHelpers.cpp \
src/providers/twitch/TwitchIrcServer.cpp \
src/providers/twitch/TwitchMessageBuilder.cpp \ src/providers/twitch/TwitchMessageBuilder.cpp \
src/providers/twitch/TwitchParseCheerEmotes.cpp \ src/providers/twitch/TwitchParseCheerEmotes.cpp \
src/providers/twitch/TwitchServer.cpp \
src/providers/twitch/TwitchUser.cpp \ src/providers/twitch/TwitchUser.cpp \
src/RunGui.cpp \ src/RunGui.cpp \
src/singletons/Badges.cpp \ src/singletons/Badges.cpp \
@ -180,6 +183,7 @@ SOURCES += \
src/widgets/AccountSwitchWidget.cpp \ src/widgets/AccountSwitchWidget.cpp \
src/widgets/AttachedWindow.cpp \ src/widgets/AttachedWindow.cpp \
src/widgets/dialogs/EmotePopup.cpp \ src/widgets/dialogs/EmotePopup.cpp \
src/widgets/dialogs/IrcConnectionEditor.cpp \
src/widgets/dialogs/LastRunCrashDialog.cpp \ src/widgets/dialogs/LastRunCrashDialog.cpp \
src/widgets/dialogs/LoginDialog.cpp \ src/widgets/dialogs/LoginDialog.cpp \
src/widgets/dialogs/LogsPopup.cpp \ src/widgets/dialogs/LogsPopup.cpp \
@ -230,9 +234,11 @@ HEADERS += \
src/common/Aliases.hpp \ src/common/Aliases.hpp \
src/common/Atomic.hpp \ src/common/Atomic.hpp \
src/common/Channel.hpp \ src/common/Channel.hpp \
src/common/ChannelChatters.hpp \
src/common/Common.hpp \ src/common/Common.hpp \
src/common/CompletionModel.hpp \ src/common/CompletionModel.hpp \
src/common/ConcurrentMap.hpp \ src/common/ConcurrentMap.hpp \
src/common/Credentials.hpp \
src/common/DownloadManager.hpp \ src/common/DownloadManager.hpp \
src/common/Env.hpp \ src/common/Env.hpp \
src/common/LinkParser.hpp \ src/common/LinkParser.hpp \
@ -302,8 +308,10 @@ HEADERS += \
src/providers/emoji/Emojis.hpp \ src/providers/emoji/Emojis.hpp \
src/providers/ffz/FfzEmotes.hpp \ src/providers/ffz/FfzEmotes.hpp \
src/providers/irc/AbstractIrcServer.hpp \ src/providers/irc/AbstractIrcServer.hpp \
src/providers/irc/Irc2.hpp \
src/providers/irc/IrcAccount.hpp \ src/providers/irc/IrcAccount.hpp \
src/providers/irc/IrcChannel2.hpp \ src/providers/irc/IrcChannel2.hpp \
src/providers/irc/IrcCommands.hpp \
src/providers/irc/IrcConnection2.hpp \ src/providers/irc/IrcConnection2.hpp \
src/providers/irc/IrcServer.hpp \ src/providers/irc/IrcServer.hpp \
src/providers/LinkResolver.hpp \ src/providers/LinkResolver.hpp \
@ -322,9 +330,9 @@ HEADERS += \
src/providers/twitch/TwitchCommon.hpp \ src/providers/twitch/TwitchCommon.hpp \
src/providers/twitch/TwitchEmotes.hpp \ src/providers/twitch/TwitchEmotes.hpp \
src/providers/twitch/TwitchHelpers.hpp \ src/providers/twitch/TwitchHelpers.hpp \
src/providers/twitch/TwitchIrcServer.hpp \
src/providers/twitch/TwitchMessageBuilder.hpp \ src/providers/twitch/TwitchMessageBuilder.hpp \
src/providers/twitch/TwitchParseCheerEmotes.hpp \ src/providers/twitch/TwitchParseCheerEmotes.hpp \
src/providers/twitch/TwitchServer.hpp \
src/providers/twitch/TwitchUser.hpp \ src/providers/twitch/TwitchUser.hpp \
src/RunGui.hpp \ src/RunGui.hpp \
src/singletons/Badges.hpp \ src/singletons/Badges.hpp \
@ -350,6 +358,7 @@ HEADERS += \
src/util/IsBigEndian.hpp \ src/util/IsBigEndian.hpp \
src/util/JsonQuery.hpp \ src/util/JsonQuery.hpp \
src/util/LayoutCreator.hpp \ src/util/LayoutCreator.hpp \
src/util/Overloaded.hpp \
src/util/QObjectRef.hpp \ src/util/QObjectRef.hpp \
src/util/QStringHash.hpp \ src/util/QStringHash.hpp \
src/util/rangealgorithm.hpp \ src/util/rangealgorithm.hpp \
@ -363,6 +372,7 @@ HEADERS += \
src/widgets/AccountSwitchWidget.hpp \ src/widgets/AccountSwitchWidget.hpp \
src/widgets/AttachedWindow.hpp \ src/widgets/AttachedWindow.hpp \
src/widgets/dialogs/EmotePopup.hpp \ src/widgets/dialogs/EmotePopup.hpp \
src/widgets/dialogs/IrcConnectionEditor.hpp \
src/widgets/dialogs/LastRunCrashDialog.hpp \ src/widgets/dialogs/LastRunCrashDialog.hpp \
src/widgets/dialogs/LoginDialog.hpp \ src/widgets/dialogs/LoginDialog.hpp \
src/widgets/dialogs/LogsPopup.hpp \ src/widgets/dialogs/LogsPopup.hpp \
@ -414,7 +424,8 @@ RESOURCES += \
DISTFILES += DISTFILES +=
FORMS += FORMS += \
src/widgets/dialogs/IrcConnectionEditor.ui
# do not use windows min/max macros # do not use windows min/max macros
#win32 { #win32 {

View file

@ -1,3 +1,5 @@
DEFINES += IRC_NAMESPACE=Communi
include(../lib/libcommuni/src/core/core.pri) include(../lib/libcommuni/src/core/core.pri)
include(../lib/libcommuni/src/model/model.pri) include(../lib/libcommuni/src/model/model.pri)
include(../lib/libcommuni/src/util/util.pri) include(../lib/libcommuni/src/util/util.pri)

1
lib/qtkeychain Submodule

@ -0,0 +1 @@
Subproject commit 832f550da3f6655168a737d2e1b7df37272e936d

1
lib/qtkeychain.pri Normal file
View file

@ -0,0 +1 @@
include(qtkeychain/qt5keychain.pri)

View file

@ -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.

View file

@ -46,6 +46,7 @@
<file>licenses/pajlada_settings.txt</file> <file>licenses/pajlada_settings.txt</file>
<file>licenses/pajlada_signals.txt</file> <file>licenses/pajlada_signals.txt</file>
<file>licenses/qt_lgpl-3.0.txt</file> <file>licenses/qt_lgpl-3.0.txt</file>
<file>licenses/qtkeychain.txt</file>
<file>licenses/rapidjson.txt</file> <file>licenses/rapidjson.txt</file>
<file>licenses/websocketpp.txt</file> <file>licenses/websocketpp.txt</file>
<file>pajaDank.png</file> <file>pajaDank.png</file>

34
src/.clang-format Normal file
View file

@ -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

View file

@ -13,8 +13,9 @@
#include "providers/bttv/BttvEmotes.hpp" #include "providers/bttv/BttvEmotes.hpp"
#include "providers/chatterino/ChatterinoBadges.hpp" #include "providers/chatterino/ChatterinoBadges.hpp"
#include "providers/ffz/FfzEmotes.hpp" #include "providers/ffz/FfzEmotes.hpp"
#include "providers/irc/Irc2.hpp"
#include "providers/twitch/PubsubClient.hpp" #include "providers/twitch/PubsubClient.hpp"
#include "providers/twitch/TwitchServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Emotes.hpp" #include "singletons/Emotes.hpp"
#include "singletons/Fonts.hpp" #include "singletons/Fonts.hpp"
#include "singletons/Logging.hpp" #include "singletons/Logging.hpp"
@ -59,7 +60,7 @@ Application::Application(Settings &_settings, Paths &_paths)
, ignores(&this->emplace<IgnoreController>()) , ignores(&this->emplace<IgnoreController>())
, taggedUsers(&this->emplace<TaggedUsersController>()) , taggedUsers(&this->emplace<TaggedUsersController>())
, moderationActions(&this->emplace<ModerationActions>()) , moderationActions(&this->emplace<ModerationActions>())
, twitch2(&this->emplace<TwitchServer>()) , twitch2(&this->emplace<TwitchIrcServer>())
, chatterinoBadges(&this->emplace<ChatterinoBadges>()) , chatterinoBadges(&this->emplace<ChatterinoBadges>())
, logging(&this->emplace<Logging>()) , logging(&this->emplace<Logging>())
@ -78,6 +79,8 @@ void Application::initialize(Settings &settings, Paths &paths)
assert(isAppInitialized == false); assert(isAppInitialized == false);
isAppInitialized = true; isAppInitialized = true;
Irc::getInstance().load();
for (auto &singleton : this->singletons_) for (auto &singleton : this->singletons_)
{ {
singleton->initialize(settings, paths); singleton->initialize(settings, paths);
@ -116,6 +119,8 @@ void Application::save()
void Application::initNm(Paths &paths) void Application::initNm(Paths &paths)
{ {
(void)paths;
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
# if defined QT_NO_DEBUG || defined C_DEBUG_NM # if defined QT_NO_DEBUG || defined C_DEBUG_NM
registerNmHost(paths); registerNmHost(paths);

View file

@ -8,7 +8,7 @@
namespace chatterino { namespace chatterino {
class TwitchServer; class TwitchIrcServer;
class PubSub; class PubSub;
class CommandController; class CommandController;
@ -67,14 +67,14 @@ public:
IgnoreController *const ignores{}; IgnoreController *const ignores{};
TaggedUsersController *const taggedUsers{}; TaggedUsersController *const taggedUsers{};
ModerationActions *const moderationActions{}; ModerationActions *const moderationActions{};
TwitchServer *const twitch2{}; TwitchIrcServer *const twitch2{};
ChatterinoBadges *const chatterinoBadges{}; ChatterinoBadges *const chatterinoBadges{};
/*[[deprecated]]*/ Logging *const logging{}; /*[[deprecated]]*/ Logging *const logging{};
/// Provider-specific /// Provider-specific
struct { struct {
/*[[deprecated("use twitch2 instead")]]*/ TwitchServer *server{}; /*[[deprecated("use twitch2 instead")]]*/ TwitchIrcServer *server{};
/*[[deprecated("use twitch2->pubsub instead")]]*/ PubSub *pubsub{}; /*[[deprecated("use twitch2->pubsub instead")]]*/ PubSub *pubsub{};
} twitch; } twitch;

View file

@ -70,13 +70,6 @@ void Channel::addMessage(MessagePtr message,
auto app = getApp(); auto app = getApp();
MessagePtr deleted; 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 // FOURTF: change this when adding more providers
if (this->isTwitchChannel() && if (this->isTwitchChannel() &&
(!overridingFlags || !overridingFlags->has(MessageFlag::DoNotLog))) (!overridingFlags || !overridingFlags->has(MessageFlag::DoNotLog)))
@ -246,10 +239,6 @@ void Channel::deleteMessage(QString messageID)
} }
} }
void Channel::addRecentChatter(const MessagePtr &message)
{
}
bool Channel::canSendMessage() const bool Channel::canSendMessage() const
{ {
return false; return false;
@ -291,6 +280,15 @@ bool Channel::shouldIgnoreHighlights() const
this->type_ == Type::TwitchWhispers; this->type_ == Type::TwitchWhispers;
} }
bool Channel::canReconnect() const
{
return false;
}
void Channel::reconnect()
{
}
std::shared_ptr<Channel> Channel::getEmpty() std::shared_ptr<Channel> Channel::getEmpty()
{ {
static std::shared_ptr<Channel> channel(new Channel("", Type::None)); static std::shared_ptr<Channel> channel(new Channel("", Type::None));

View file

@ -37,15 +37,16 @@ public:
TwitchWatching, TwitchWatching,
TwitchMentions, TwitchMentions,
TwitchEnd, TwitchEnd,
Irc,
Misc Misc
}; };
explicit Channel(const QString &name, Type type); explicit Channel(const QString &name, Type type);
virtual ~Channel(); virtual ~Channel();
// SIGNALS
pajlada::Signals::Signal<const QString &, const QString &, bool &> pajlada::Signals::Signal<const QString &, const QString &, bool &>
sendMessageSignal; sendMessageSignal;
pajlada::Signals::Signal<MessagePtr &> messageRemovedFromStart; pajlada::Signals::Signal<MessagePtr &> messageRemovedFromStart;
pajlada::Signals::Signal<MessagePtr &, boost::optional<MessageFlags>> pajlada::Signals::Signal<MessagePtr &, boost::optional<MessageFlags>>
messageAppended; messageAppended;
@ -60,6 +61,7 @@ public:
virtual bool isEmpty() const; virtual bool isEmpty() const;
LimitedQueueSnapshot<MessagePtr> getMessageSnapshot(); LimitedQueueSnapshot<MessagePtr> getMessageSnapshot();
// MESSAGES
// overridingFlags can be filled in with flags that should be used instead // 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 // of the message's flags. This is useful in case a flag is specific to a
// type of split // type of split
@ -71,9 +73,11 @@ public:
void disableAllMessages(); void disableAllMessages();
void replaceMessage(MessagePtr message, MessagePtr replacement); void replaceMessage(MessagePtr message, MessagePtr replacement);
void deleteMessage(QString messageID); void deleteMessage(QString messageID);
void clearMessages();
QStringList modList; QStringList modList;
// CHANNEL INFO
virtual bool canSendMessage() const; virtual bool canSendMessage() const;
virtual void sendMessage(const QString &message); virtual void sendMessage(const QString &message);
virtual bool isMod() const; virtual bool isMod() const;
@ -82,6 +86,8 @@ public:
virtual bool hasHighRateLimit() const; virtual bool hasHighRateLimit() const;
virtual bool isLive() const; virtual bool isLive() const;
virtual bool shouldIgnoreHighlights() const; virtual bool shouldIgnoreHighlights() const;
virtual bool canReconnect() const;
virtual void reconnect();
static std::shared_ptr<Channel> getEmpty(); static std::shared_ptr<Channel> getEmpty();
@ -89,7 +95,6 @@ public:
protected: protected:
virtual void onConnected(); virtual void onConnected();
virtual void addRecentChatter(const MessagePtr &message);
private: private:
const QString name_; const QString name_;

View file

@ -0,0 +1,72 @@
#include "ChannelChatters.hpp"
#include "messages/Message.hpp"
#include "messages/MessageBuilder.hpp"
namespace chatterino {
ChannelChatters::ChannelChatters(Channel &channel)
: channel_(channel)
{
}
AccessGuard<const UsernameSet> 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

View file

@ -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<const UsernameSet> 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<UsernameSet> chatters_;
// combines multiple joins/parts into one message
UniqueAccess<QStringList> joinedUsers_;
bool joinedUsersMergeQueued_ = false;
UniqueAccess<QStringList> partedUsers_;
bool partedUsersMergeQueued_ = false;
QObject lifetimeGuard_;
};
} // namespace chatterino

View file

@ -8,7 +8,7 @@
#include "debug/Benchmark.hpp" #include "debug/Benchmark.hpp"
#include "debug/Log.hpp" #include "debug/Log.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Emotes.hpp" #include "singletons/Emotes.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"

232
src/common/Credentials.cpp Normal file
View file

@ -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 <QSaveFile>
#include <boost/variant.hpp>
#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<SetJob, EraseJob>;
static std::queue<Job> &jobQueue()
{
static std::queue<Job> 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<SetJob>(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<EraseJob>(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<void(const QString &)> &&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

View file

@ -0,0 +1,23 @@
#pragma once
#include <QString>
#include <functional>
namespace chatterino {
class Credentials
{
public:
static Credentials &getInstance();
void get(const QString &provider, const QString &name, QObject *receiver,
std::function<void(const QString &)> &&onLoaded);
void set(const QString &provider, const QString &name,
const QString &credential);
void erase(const QString &provider, const QString &name);
private:
Credentials();
};
} // namespace chatterino

View file

@ -102,19 +102,26 @@ public:
int rowCount(const QModelIndex &parent) const override int rowCount(const QModelIndex &parent) const override
{ {
(void)parent;
return this->rows_.size(); return this->rows_.size();
} }
int columnCount(const QModelIndex &parent) const override int columnCount(const QModelIndex &parent) const override
{ {
(void)parent;
return this->columnCount_; return this->columnCount_;
} }
QVariant data(const QModelIndex &index, int role) const override QVariant data(const QModelIndex &index, int role) const override
{ {
int row = index.row(), column = index.column(); int row = index.row(), column = index.column();
assert(row >= 0 && row < this->rows_.size() && column >= 0 && if (row < 0 || column < 0 || row >= this->rows_.size() ||
column < this->columnCount_); column >= this->columnCount_)
{
return QVariant();
}
return rows_[row].items[column]->data(role); return rows_[row].items[column]->data(role);
} }
@ -123,8 +130,11 @@ public:
int role) override int role) override
{ {
int row = index.row(), column = index.column(); int row = index.row(), column = index.column();
assert(row >= 0 && row < this->rows_.size() && column >= 0 && if (row < 0 || column < 0 || row >= this->rows_.size() ||
column < this->columnCount_); column >= this->columnCount_)
{
return false;
}
Row &rowItem = this->rows_[row]; Row &rowItem = this->rows_[row];
@ -185,6 +195,13 @@ public:
Qt::ItemFlags flags(const QModelIndex &index) const override Qt::ItemFlags flags(const QModelIndex &index) const override
{ {
int row = index.row(), column = index.column(); 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 && assert(row >= 0 && row < this->rows_.size() && column >= 0 &&
column < this->columnCount_); column < this->columnCount_);
@ -207,6 +224,8 @@ public:
bool removeRows(int row, int count, const QModelIndex &parent) override bool removeRows(int row, int count, const QModelIndex &parent) override
{ {
(void)parent;
if (count != 1) if (count != 1)
{ {
return false; return false;
@ -237,18 +256,22 @@ protected:
std::vector<QStandardItem *> &row, std::vector<QStandardItem *> &row,
int proposedIndex) int proposedIndex)
{ {
(void)item, (void)row;
return proposedIndex; return proposedIndex;
} }
virtual void afterRemoved(const TVectorItem &item, virtual void afterRemoved(const TVectorItem &item,
std::vector<QStandardItem *> &row, int index) std::vector<QStandardItem *> &row, int index)
{ {
(void)item, (void)row, (void)index;
} }
virtual void customRowSetData(const std::vector<QStandardItem *> &row, virtual void customRowSetData(const std::vector<QStandardItem *> &row,
int column, const QVariant &value, int role, int column, const QVariant &value, int role,
int rowIndex) int rowIndex)
{ {
(void)row, (void)column, (void)value, (void)role, (void)rowIndex;
} }
void insertCustomRow(std::vector<QStandardItem *> row, int index) void insertCustomRow(std::vector<QStandardItem *> row, int index)

View file

@ -11,7 +11,7 @@
#include "messages/MessageElement.hpp" #include "messages/MessageElement.hpp"
#include "providers/twitch/TwitchApi.hpp" #include "providers/twitch/TwitchApi.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Emotes.hpp" #include "singletons/Emotes.hpp"
#include "singletons/Paths.hpp" #include "singletons/Paths.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"

View file

@ -6,7 +6,7 @@
#include "controllers/notifications/NotificationModel.hpp" #include "controllers/notifications/NotificationModel.hpp"
#include "debug/Log.hpp" #include "debug/Log.hpp"
#include "providers/twitch/TwitchApi.hpp" #include "providers/twitch/TwitchApi.hpp"
#include "providers/twitch/TwitchServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Toasts.hpp" #include "singletons/Toasts.hpp"
#include "singletons/WindowManager.hpp" #include "singletons/WindowManager.hpp"
#include "widgets/Window.hpp" #include "widgets/Window.hpp"

View file

@ -32,6 +32,7 @@ enum class MessageFlag : uint32_t {
RecentMessage = (1 << 15), RecentMessage = (1 << 15),
Whisper = (1 << 16), Whisper = (1 << 16),
HighlightedWhisper = (1 << 17), HighlightedWhisper = (1 << 17),
Debug = (1 << 18),
}; };
using MessageFlags = FlagsEnum<MessageFlag>; using MessageFlags = FlagsEnum<MessageFlag>;

View file

@ -269,6 +269,10 @@ void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/,
{ {
backgroundColor = QColor("#404040"); backgroundColor = QColor("#404040");
} }
else if (this->message_->flags.has(MessageFlag::Debug))
{
backgroundColor = QColor("#4A273D");
}
painter.fillRect(buffer->rect(), backgroundColor); painter.fillRect(buffer->rect(), backgroundColor);

View file

@ -18,13 +18,17 @@ const int MAX_FALLOFF_COUNTER = 60;
AbstractIrcServer::AbstractIrcServer() AbstractIrcServer::AbstractIrcServer()
{ {
// Initialize the connections // Initialize the connections
// XXX: don't create write connection if there is not separate write connection.
this->writeConnection_.reset(new IrcConnection); this->writeConnection_.reset(new IrcConnection);
this->writeConnection_->moveToThread( this->writeConnection_->moveToThread(
QCoreApplication::instance()->thread()); QCoreApplication::instance()->thread());
QObject::connect( QObject::connect(
this->writeConnection_.get(), &Communi::IrcConnection::messageReceived, 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 // Listen to read connection message signals
this->readConnection_.reset(new IrcConnection); this->readConnection_.reset(new IrcConnection);
@ -32,21 +36,18 @@ AbstractIrcServer::AbstractIrcServer()
QObject::connect( QObject::connect(
this->readConnection_.get(), &Communi::IrcConnection::messageReceived, this->readConnection_.get(), &Communi::IrcConnection::messageReceived,
[this](auto msg) { this->readConnectionMessageReceived(msg); }); this, [this](auto msg) { this->readConnectionMessageReceived(msg); });
QObject::connect(this->readConnection_.get(), QObject::connect(this->readConnection_.get(),
&Communi::IrcConnection::privateMessageReceived, &Communi::IrcConnection::privateMessageReceived, this,
[this](auto msg) { this->privateMessageReceived(msg); }); [this](auto msg) { this->privateMessageReceived(msg); });
QObject::connect( QObject::connect(
this->readConnection_.get(), &Communi::IrcConnection::connected, this->readConnection_.get(), &Communi::IrcConnection::connected, this,
[this] { this->onReadConnected(this->readConnection_.get()); }); [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(), QObject::connect(this->readConnection_.get(),
&Communi::IrcConnection::disconnected, &Communi::IrcConnection::disconnected, this,
[this] { this->onDisconnected(); }); [this] { this->onDisconnected(); });
QObject::connect(this->readConnection_.get(), QObject::connect(this->readConnection_.get(),
&Communi::IrcConnection::socketError, &Communi::IrcConnection::socketError, this,
[this] { this->onSocketError(); }); [this] { this->onSocketError(); });
// listen to reconnect request // listen to reconnect request
@ -75,37 +76,29 @@ void AbstractIrcServer::connect()
{ {
this->disconnect(); this->disconnect();
bool separateWriteConnection = this->hasSeparateWriteConnection(); if (this->hasSeparateWriteConnection())
if (separateWriteConnection)
{ {
this->initializeConnection(this->writeConnection_.get(), false, true); this->initializeConnection(this->writeConnection_.get(), Write);
this->initializeConnection(this->readConnection_.get(), true, false); this->initializeConnection(this->readConnection_.get(), Read);
} }
else 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<std::mutex> lock(this->connectionMutex_);
if (type == Write)
{ {
std::lock_guard<std::mutex> lock1(this->connectionMutex_);
std::lock_guard<std::mutex> lock2(this->channelMutex);
for (std::weak_ptr<Channel> &weak : this->channels.values())
{
if (auto channel = std::shared_ptr<Channel>(weak.lock()))
{
this->readConnection_->sendRaw("JOIN #" + channel->getName());
}
}
this->writeConnection_->open(); this->writeConnection_->open();
}
if (type & Read)
{
this->readConnection_->open(); this->readConnection_->open();
} }
// this->onConnected();
// possbile event: started to connect
} }
void AbstractIrcServer::disconnect() void AbstractIrcServer::disconnect()
@ -113,7 +106,10 @@ void AbstractIrcServer::disconnect()
std::lock_guard<std::mutex> locker(this->connectionMutex_); std::lock_guard<std::mutex> locker(this->connectionMutex_);
this->readConnection_->close(); this->readConnection_->close();
this->writeConnection_->close(); if (this->hasSeparateWriteConnection())
{
this->writeConnection_->close();
}
} }
void AbstractIrcServer::sendMessage(const QString &channelName, void AbstractIrcServer::sendMessage(const QString &channelName,
@ -139,10 +135,10 @@ void AbstractIrcServer::sendRawMessage(const QString &rawMessage)
void AbstractIrcServer::writeConnectionMessageReceived( void AbstractIrcServer::writeConnectionMessageReceived(
Communi::IrcMessage *message) Communi::IrcMessage *message)
{ {
(void)message;
} }
std::shared_ptr<Channel> AbstractIrcServer::getOrAddChannel( ChannelPtr AbstractIrcServer::getOrAddChannel(const QString &dirtyChannelName)
const QString &dirtyChannelName)
{ {
auto channelName = this->cleanChannelName(dirtyChannelName); auto channelName = this->cleanChannelName(dirtyChannelName);
@ -162,26 +158,24 @@ std::shared_ptr<Channel> AbstractIrcServer::getOrAddChannel(
return Channel::getEmpty(); return Channel::getEmpty();
} }
QString clojuresInCppAreShit = channelName;
this->channels.insert(channelName, chan); 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 // fourtf: issues when the server itself is destroyed
log("[AbstractIrcServer::addChannel] {} was destroyed", log("[AbstractIrcServer::addChannel] {} was destroyed", channelName);
clojuresInCppAreShit); this->channels.remove(channelName);
this->channels.remove(clojuresInCppAreShit);
if (this->readConnection_) 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 // join irc channel
{ {
@ -189,20 +183,25 @@ std::shared_ptr<Channel> AbstractIrcServer::getOrAddChannel(
if (this->readConnection_) 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; return chan;
} }
std::shared_ptr<Channel> AbstractIrcServer::getChannelOrEmpty( ChannelPtr AbstractIrcServer::getChannelOrEmpty(const QString &dirtyChannelName)
const QString &dirtyChannelName)
{ {
auto channelName = this->cleanChannelName(dirtyChannelName); auto channelName = this->cleanChannelName(dirtyChannelName);
@ -230,10 +229,35 @@ std::shared_ptr<Channel> AbstractIrcServer::getChannelOrEmpty(
return Channel::getEmpty(); return Channel::getEmpty();
} }
std::vector<std::weak_ptr<Channel>> AbstractIrcServer::getChannels()
{
std::lock_guard lock(this->channelMutex);
std::vector<std::weak_ptr<Channel>> channels;
for (auto &&weak : this->channels.values())
{
channels.push_back(weak);
}
return channels;
}
void AbstractIrcServer::onReadConnected(IrcConnection *connection) void AbstractIrcServer::onReadConnected(IrcConnection *connection)
{ {
std::lock_guard<std::mutex> 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"); auto connectedMsg = makeSystemMessage("connected");
connectedMsg->flags.set(MessageFlag::ConnectedMessage); connectedMsg->flags.set(MessageFlag::ConnectedMessage);
auto reconnected = makeSystemMessage("reconnected"); auto reconnected = makeSystemMessage("reconnected");
@ -267,6 +291,7 @@ void AbstractIrcServer::onReadConnected(IrcConnection *connection)
void AbstractIrcServer::onWriteConnected(IrcConnection *connection) void AbstractIrcServer::onWriteConnected(IrcConnection *connection)
{ {
(void)connection;
} }
void AbstractIrcServer::onDisconnected() void AbstractIrcServer::onDisconnected()
@ -297,12 +322,16 @@ void AbstractIrcServer::onSocketError()
std::shared_ptr<Channel> AbstractIrcServer::getCustomChannel( std::shared_ptr<Channel> AbstractIrcServer::getCustomChannel(
const QString &channelName) const QString &channelName)
{ {
(void)channelName;
return nullptr; return nullptr;
} }
QString AbstractIrcServer::cleanChannelName(const QString &dirtyChannelName) QString AbstractIrcServer::cleanChannelName(const QString &dirtyChannelName)
{ {
return dirtyChannelName; if (dirtyChannelName.startsWith('#'))
return dirtyChannelName.mid(1);
else
return dirtyChannelName;
} }
void AbstractIrcServer::addFakeMessage(const QString &data) void AbstractIrcServer::addFakeMessage(const QString &data)
@ -324,6 +353,7 @@ void AbstractIrcServer::addFakeMessage(const QString &data)
void AbstractIrcServer::privateMessageReceived( void AbstractIrcServer::privateMessageReceived(
Communi::IrcPrivateMessage *message) Communi::IrcPrivateMessage *message)
{ {
(void)message;
} }
void AbstractIrcServer::readConnectionMessageReceived( void AbstractIrcServer::readConnectionMessageReceived(
@ -337,7 +367,7 @@ void AbstractIrcServer::forEachChannel(std::function<void(ChannelPtr)> func)
for (std::weak_ptr<Channel> &weak : this->channels.values()) for (std::weak_ptr<Channel> &weak : this->channels.values())
{ {
std::shared_ptr<Channel> chan = weak.lock(); ChannelPtr chan = weak.lock();
if (!chan) if (!chan)
{ {
continue; continue;

View file

@ -4,6 +4,7 @@
#include <IrcMessage> #include <IrcMessage>
#include <pajlada/signals/signal.hpp> #include <pajlada/signals/signal.hpp>
#include <pajlada/signals/signalholder.hpp>
#include <functional> #include <functional>
#include <mutex> #include <mutex>
@ -13,9 +14,11 @@ namespace chatterino {
class Channel; class Channel;
using ChannelPtr = std::shared_ptr<Channel>; using ChannelPtr = std::shared_ptr<Channel>;
class AbstractIrcServer class AbstractIrcServer : public QObject
{ {
public: public:
enum ConnectionType { Read = 1, Write = 2, Both = 3 };
virtual ~AbstractIrcServer() = default; virtual ~AbstractIrcServer() = default;
// connection // connection
@ -26,14 +29,13 @@ public:
void sendRawMessage(const QString &rawMessage); void sendRawMessage(const QString &rawMessage);
// channels // channels
std::shared_ptr<Channel> getOrAddChannel(const QString &dirtyChannelName); ChannelPtr getOrAddChannel(const QString &dirtyChannelName);
std::shared_ptr<Channel> getChannelOrEmpty(const QString &dirtyChannelName); ChannelPtr getChannelOrEmpty(const QString &dirtyChannelName);
std::vector<std::weak_ptr<Channel>> getChannels();
// signals // signals
pajlada::Signals::NoArgSignal connected; pajlada::Signals::NoArgSignal connected;
pajlada::Signals::NoArgSignal disconnected; pajlada::Signals::NoArgSignal disconnected;
// pajlada::Signals::Signal<Communi::IrcPrivateMessage *>
// onPrivateMessage;
void addFakeMessage(const QString &data); void addFakeMessage(const QString &data);
@ -43,8 +45,8 @@ public:
protected: protected:
AbstractIrcServer(); AbstractIrcServer();
virtual void initializeConnection(IrcConnection *connection, bool isRead, virtual void initializeConnection(IrcConnection *connection,
bool isWrite) = 0; ConnectionType type) = 0;
virtual std::shared_ptr<Channel> createChannel( virtual std::shared_ptr<Channel> createChannel(
const QString &channelName) = 0; const QString &channelName) = 0;
@ -63,14 +65,23 @@ protected:
virtual bool hasSeparateWriteConnection() const = 0; virtual bool hasSeparateWriteConnection() const = 0;
virtual QString cleanChannelName(const QString &dirtyChannelName); virtual QString cleanChannelName(const QString &dirtyChannelName);
void open(ConnectionType type);
QMap<QString, std::weak_ptr<Channel>> channels; QMap<QString, std::weak_ptr<Channel>> channels;
std::mutex channelMutex; std::mutex channelMutex;
private: private:
void initConnection(); void initConnection();
std::unique_ptr<IrcConnection> writeConnection_ = nullptr; struct Deleter {
std::unique_ptr<IrcConnection> readConnection_ = nullptr; void operator()(IrcConnection *conn)
{
conn->deleteLater();
}
};
std::unique_ptr<IrcConnection, Deleter> writeConnection_ = nullptr;
std::unique_ptr<IrcConnection, Deleter> readConnection_ = nullptr;
QTimer reconnectTimer_; QTimer reconnectTimer_;
int falloffCounter_ = 1; int falloffCounter_ = 1;
@ -78,6 +89,7 @@ private:
std::mutex connectionMutex_; std::mutex connectionMutex_;
// bool autoReconnect_ = false; // bool autoReconnect_ = false;
pajlada::Signals::SignalHolder connections_;
}; };
} // namespace chatterino } // namespace chatterino

261
src/providers/irc/Irc2.cpp Normal file
View file

@ -0,0 +1,261 @@
#include "Irc2.hpp"
#include <pajlada/serialize.hpp>
#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 <QSaveFile>
#include <QtConcurrent>
namespace chatterino {
namespace {
QString configPath()
{
return combinePath(getPaths()->settingsDirectory, "irc.json");
}
class Model : public SignalVectorModel<IrcServerData>
{
public:
Model(QObject *parent)
: SignalVectorModel<IrcServerData>(6, parent)
{
}
// turn a vector item into a model row
IrcServerData getItemFromRow(std::vector<QStandardItem *> &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<QStandardItem *> &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<void(const QString &)> &&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<IrcServer>(args.item, ab->second);
// set server of abandoned channels
for (auto weak : ab->second)
if (auto shared = weak.lock())
if (auto ircChannel =
dynamic_cast<IrcChannel *>(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<IrcServer>(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<IrcChannel *>(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<IrcChannel>(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<int> 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

View file

@ -0,0 +1,67 @@
#pragma once
#include <rapidjson/rapidjson.h>
#include <common/SignalVector.hpp>
#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<void(const QString &)> &&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<void *>(1);
UnsortedSignalVector<IrcServerData> 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<int, std::unique_ptr<IrcServer>> servers_;
std::unordered_map<int, std::vector<std::weak_ptr<Channel>>>
abandonedChannels_;
};
} // namespace chatterino

View file

@ -1,9 +1,68 @@
#include "IrcChannel2.hpp" #include "IrcChannel2.hpp"
#include "debug/AssertInGuiThread.hpp"
#include "messages/MessageBuilder.hpp"
#include "providers/irc/IrcCommands.hpp"
#include "providers/irc/IrcServer.hpp"
namespace chatterino { namespace chatterino {
// IrcChannel::IrcChannel() IrcChannel::IrcChannel(const QString &name, IrcServer *server)
//{ : Channel(name, Channel::Type::Irc)
//} , ChannelChatters(*static_cast<Channel *>(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<TimestampElement>();
builder.emplace<TextElement>(this->server()->nick() + ":",
MessageElementFlag::Username);
builder.emplace<TextElement>(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 } // namespace chatterino

View file

@ -1,11 +1,33 @@
#pragma once #pragma once
#include "common/Channel.hpp"
#include "common/ChannelChatters.hpp"
namespace chatterino { namespace chatterino {
// class IrcChannel class Irc;
//{ class IrcServer;
// public:
// IrcChannel(); 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 } // namespace chatterino

View file

@ -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<QString, QString>{
{"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

View file

@ -0,0 +1,12 @@
#pragma once
#include "common/Outcome.hpp"
namespace chatterino {
class IrcChannel;
Outcome invokeIrcCommand(const QString &command, const QString &params,
IrcChannel &channel);
} // namespace chatterino

View file

@ -13,7 +13,7 @@ IrcConnection::IrcConnection(QObject *parent)
{ {
if (!this->recentlyReceivedMessage_.load()) if (!this->recentlyReceivedMessage_.load())
{ {
this->sendRaw("PING"); this->sendRaw("PING chatterino/ping");
this->reconnectTimer_.start(); this->reconnectTimer_.start();
} }
this->recentlyReceivedMessage_ = false; this->recentlyReceivedMessage_ = false;

View file

@ -1,12 +1,260 @@
#include "IrcServer.hpp" #include "IrcServer.hpp"
#include <cassert> #include <cassert>
#include <cstdlib>
#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 { namespace chatterino {
// IrcServer::IrcServer(const QString &hostname, int port) IrcServer::IrcServer(const IrcServerData &data)
//{ : data_(new IrcServerData(data))
// this->initConnection(); {
//} this->connect();
// }
IrcServer::IrcServer(const IrcServerData &data,
const std::vector<std::weak_ptr<Channel>> &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<TimestampElement>();
builder.emplace<TextElement>(
message->nick(), MessageElementFlag::Username);
builder.emplace<TextElement>(
"-> you:", MessageElementFlag::Username);
builder.emplace<TextElement>(message->content(),
MessageElementFlag::Text);
auto msg = builder.release();
for (auto &&weak : this->channels)
if (auto shared = weak.lock())
shared->addMessage(msg);
});
}
std::shared_ptr<Channel> IrcServer::createChannel(const QString &channelName)
{
return std::make_shared<IrcChannel>(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<TimestampElement>();
builder.emplace<TextElement>(message->nick() + ":",
MessageElementFlag::Username);
builder.emplace<TextElement>(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<Communi::IrcJoinMessage *>(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<ChannelChatters *>(shared.get()))
c->addJoinedUser(x->nick());
}
}
}
return;
}
case Communi::IrcMessage::Part:
{
auto x = static_cast<Communi::IrcPartMessage *>(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<ChannelChatters *>(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<TimestampElement>();
builder.emplace<TextElement>(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 } // namespace chatterino

View file

@ -5,20 +5,34 @@
namespace chatterino { namespace chatterino {
// class IrcServer struct IrcServerData;
//{
// public:
// IrcServer(const QString &hostname, int port);
// void setAccount(std::shared_ptr<IrcAccount> newAccount); class IrcServer : public AbstractIrcServer
// std::shared_ptr<IrcAccount> getAccount() const; {
public:
explicit IrcServer(const IrcServerData &data);
IrcServer(const IrcServerData &data,
const std::vector<std::weak_ptr<Channel>> &restoreChannels);
~IrcServer() override;
// protected: int id();
// virtual void initializeConnection(Communi::IrcConnection *connection, bool const QString &user();
// isReadConnection); const QString &nick();
// AbstractIrcServer interface
protected:
void initializeConnection(IrcConnection *connection,
ConnectionType type) override;
std::shared_ptr<Channel> 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 } // namespace chatterino

View file

@ -20,7 +20,7 @@ protected:
QString chatroomOwnerId; QString chatroomOwnerId;
QString chatroomOwnerName; QString chatroomOwnerName;
friend class TwitchServer; friend class TwitchIrcServer;
friend class TwitchMessageBuilder; friend class TwitchMessageBuilder;
friend class IrcMessageHandler; friend class IrcMessageHandler;
}; };

View file

@ -1,14 +1,16 @@
#include "IrcMessageHandler.hpp" #include "IrcMessageHandler.hpp"
#include "Application.hpp" #include "Application.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "controllers/highlights/HighlightController.hpp" #include "controllers/highlights/HighlightController.hpp"
#include "debug/Log.hpp" #include "debug/Log.hpp"
#include "messages/LimitedQueue.hpp" #include "messages/LimitedQueue.hpp"
#include "messages/Message.hpp" #include "messages/Message.hpp"
#include "providers/twitch/TwitchAccountManager.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchHelpers.hpp" #include "providers/twitch/TwitchHelpers.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
#include "providers/twitch/TwitchMessageBuilder.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp"
#include "providers/twitch/TwitchServer.hpp"
#include "singletons/Resources.hpp" #include "singletons/Resources.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
#include "singletons/WindowManager.hpp" #include "singletons/WindowManager.hpp"
@ -85,7 +87,7 @@ std::vector<MessagePtr> IrcMessageHandler::parsePrivMessage(
} }
void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message, void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message,
TwitchServer &server) TwitchIrcServer &server)
{ {
this->addMessage(message, message->target(), message->content(), server, this->addMessage(message, message->target(), message->content(), server,
false, message->isAction()); false, message->isAction());
@ -93,8 +95,9 @@ void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message,
void IrcMessageHandler::addMessage(Communi::IrcMessage *_message, void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
const QString &target, const QString &target,
const QString &content, TwitchServer &server, const QString &content,
bool isSub, bool isAction) TwitchIrcServer &server, bool isSub,
bool isAction)
{ {
QString channelName; QString channelName;
if (!trimChannelName(target, channelName)) if (!trimChannelName(target, channelName))
@ -144,6 +147,10 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
} }
chan->addMessage(msg); chan->addMessage(msg);
if (auto chatters = dynamic_cast<ChannelChatters *>(chan.get()))
{
chatters->addRecentChatter(msg->displayName);
}
} }
} }
@ -439,7 +446,7 @@ std::vector<MessagePtr> IrcMessageHandler::parseUserNoticeMessage(
} }
void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message,
TwitchServer &server) TwitchIrcServer &server)
{ {
auto data = message->toData(); auto data = message->toData();
@ -591,7 +598,12 @@ void IrcMessageHandler::handleJoinMessage(Communi::IrcMessage *message)
if (TwitchChannel *twitchChannel = if (TwitchChannel *twitchChannel =
dynamic_cast<TwitchChannel *>(channel.get())) dynamic_cast<TwitchChannel *>(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 = if (TwitchChannel *twitchChannel =
dynamic_cast<TwitchChannel *>(channel.get())) dynamic_cast<TwitchChannel *>(channel.get()))
{ {
twitchChannel->addPartedUser(message->nick()); if (message->nick() !=
getApp()->accounts->twitch.getCurrent()->getUserName() &&
getSettings()->showJoins.getValue())
{
twitchChannel->addPartedUser(message->nick());
}
} }
} }

View file

@ -5,7 +5,7 @@
namespace chatterino { namespace chatterino {
class TwitchServer; class TwitchIrcServer;
class Channel; class Channel;
class IrcMessageHandler class IrcMessageHandler
@ -23,7 +23,7 @@ public:
std::vector<MessagePtr> parsePrivMessage( std::vector<MessagePtr> parsePrivMessage(
Channel *channel, Communi::IrcPrivateMessage *message); Channel *channel, Communi::IrcPrivateMessage *message);
void handlePrivMessage(Communi::IrcPrivateMessage *message, void handlePrivMessage(Communi::IrcPrivateMessage *message,
TwitchServer &server); TwitchIrcServer &server);
void handleRoomStateMessage(Communi::IrcMessage *message); void handleRoomStateMessage(Communi::IrcMessage *message);
void handleClearChatMessage(Communi::IrcMessage *message); void handleClearChatMessage(Communi::IrcMessage *message);
@ -36,7 +36,7 @@ public:
std::vector<MessagePtr> parseUserNoticeMessage( std::vector<MessagePtr> parseUserNoticeMessage(
Channel *channel, Communi::IrcMessage *message); Channel *channel, Communi::IrcMessage *message);
void handleUserNoticeMessage(Communi::IrcMessage *message, void handleUserNoticeMessage(Communi::IrcMessage *message,
TwitchServer &server); TwitchIrcServer &server);
void handleModeMessage(Communi::IrcMessage *message); void handleModeMessage(Communi::IrcMessage *message);
@ -51,8 +51,8 @@ public:
private: private:
void addMessage(Communi::IrcMessage *message, const QString &target, void addMessage(Communi::IrcMessage *message, const QString &target,
const QString &content, TwitchServer &server, bool isResub, const QString &content, TwitchIrcServer &server,
bool isAction); bool isResub, bool isAction);
}; };
} // namespace chatterino } // namespace chatterino

View file

@ -2,7 +2,7 @@
#include "providers/twitch/PubsubActions.hpp" #include "providers/twitch/PubsubActions.hpp"
#include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchAccount.hpp"
#include "providers/twitch/TwitchServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include <rapidjson/document.h> #include <rapidjson/document.h>
#include <QString> #include <QString>

View file

@ -79,6 +79,7 @@ TwitchChannel::TwitchChannel(const QString &name,
TwitchBadges &globalTwitchBadges, BttvEmotes &bttv, TwitchBadges &globalTwitchBadges, BttvEmotes &bttv,
FfzEmotes &ffz) FfzEmotes &ffz)
: Channel(name, Channel::Type::Twitch) : Channel(name, Channel::Type::Twitch)
, ChannelChatters(*static_cast<Channel *>(this))
, subscriptionUrl_("https://www.twitch.tv/subs/" + name) , subscriptionUrl_("https://www.twitch.tv/subs/" + name)
, channelUrl_("https://twitch.tv/" + name) , channelUrl_("https://twitch.tv/" + name)
, popoutPlayerUrl_("https://player.twitch.tv/?channel=" + name) , popoutPlayerUrl_("https://player.twitch.tv/?channel=" + name)
@ -280,69 +281,14 @@ bool TwitchChannel::hasHighRateLimit() const
return this->isMod() || this->isBroadcaster() || this->isVIP(); 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(); getApp()->twitch.server->connect();
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;
});
}
} }
QString TwitchChannel::roomId() const QString TwitchChannel::roomId() const
@ -384,11 +330,6 @@ AccessGuard<const TwitchChannel::StreamStatus>
return this->streamStatus_.accessConst(); return this->streamStatus_.accessConst();
} }
AccessGuard<const UsernameSet> TwitchChannel::accessChatters() const
{
return this->chatters_.accessConst();
}
const TwitchBadges &TwitchChannel::globalTwitchBadges() const const TwitchBadges &TwitchChannel::globalTwitchBadges() const
{ {
return this->globalTwitchBadges_; return this->globalTwitchBadges_;
@ -691,7 +632,7 @@ void TwitchChannel::refreshChatters()
auto pair = parseChatters(result.parseJson()); auto pair = parseChatters(result.parseJson());
if (pair.first) if (pair.first)
{ {
*this->chatters_.access() = std::move(pair.second); this->setChatters(std::move(pair.second));
} }
return pair.first; return pair.first;

View file

@ -3,6 +3,7 @@
#include "common/Aliases.hpp" #include "common/Aliases.hpp"
#include "common/Atomic.hpp" #include "common/Atomic.hpp"
#include "common/Channel.hpp" #include "common/Channel.hpp"
#include "common/ChannelChatters.hpp"
#include "common/Outcome.hpp" #include "common/Outcome.hpp"
#include "common/UniqueAccess.hpp" #include "common/UniqueAccess.hpp"
#include "common/UsernameSet.hpp" #include "common/UsernameSet.hpp"
@ -29,9 +30,11 @@ class TwitchBadges;
class FfzEmotes; class FfzEmotes;
class BttvEmotes; class BttvEmotes;
class TwitchServer; class TwitchIrcServer;
class TwitchChannel : public Channel, pajlada::Signals::SignalHolder class TwitchChannel : public Channel,
public ChannelChatters,
pajlada::Signals::SignalHolder
{ {
public: public:
struct StreamStatus { struct StreamStatus {
@ -64,6 +67,8 @@ public:
bool isStaff() const; bool isStaff() const;
virtual bool isBroadcaster() const override; virtual bool isBroadcaster() const override;
virtual bool hasHighRateLimit() const override; virtual bool hasHighRateLimit() const override;
virtual bool canReconnect() const override;
virtual void reconnect() override;
// Data // Data
const QString &subscriptionUrl(); const QString &subscriptionUrl();
@ -73,7 +78,6 @@ public:
QString roomId() const; QString roomId() const;
AccessGuard<const RoomModes> accessRoomModes() const; AccessGuard<const RoomModes> accessRoomModes() const;
AccessGuard<const StreamStatus> accessStreamStatus() const; AccessGuard<const StreamStatus> accessStreamStatus() const;
AccessGuard<const UsernameSet> accessChatters() const;
// Emotes // Emotes
const TwitchBadges &globalTwitchBadges() const; const TwitchBadges &globalTwitchBadges() const;
@ -101,9 +105,6 @@ public:
pajlada::Signals::NoArgSignal liveStatusChanged; pajlada::Signals::NoArgSignal liveStatusChanged;
pajlada::Signals::NoArgSignal roomModesChanged; pajlada::Signals::NoArgSignal roomModesChanged;
protected:
void addRecentChatter(const MessagePtr &message) override;
private: private:
struct NameOptions { struct NameOptions {
QString displayName; QString displayName;
@ -125,8 +126,6 @@ private:
void refreshCheerEmotes(); void refreshCheerEmotes();
void loadRecentMessages(); void loadRecentMessages();
void addJoinedUser(const QString &user);
void addPartedUser(const QString &user);
void setLive(bool newLiveStatus); void setLive(bool newLiveStatus);
void setMod(bool value); void setMod(bool value);
void setVIP(bool value); void setVIP(bool value);
@ -140,7 +139,6 @@ private:
const QString popoutPlayerUrl_; const QString popoutPlayerUrl_;
UniqueAccess<StreamStatus> streamStatus_; UniqueAccess<StreamStatus> streamStatus_;
UniqueAccess<RoomModes> roomModes_; UniqueAccess<RoomModes> roomModes_;
UniqueAccess<UsernameSet> chatters_; // maps 2 char prefix to set of names
// Emotes // Emotes
TwitchBadges &globalTwitchBadges_; TwitchBadges &globalTwitchBadges_;
@ -163,18 +161,13 @@ private:
bool staff_ = false; bool staff_ = false;
UniqueAccess<QString> roomID_; UniqueAccess<QString> roomID_;
UniqueAccess<QStringList> joinedUsers_;
bool joinedUsersMergeQueued_ = false;
UniqueAccess<QStringList> partedUsers_;
bool partedUsersMergeQueued_ = false;
// -- // --
QString lastSentMessage_; QString lastSentMessage_;
QObject lifetimeGuard_; QObject lifetimeGuard_;
QTimer liveStatusTimer_; QTimer liveStatusTimer_;
QTimer chattersListTimer_; QTimer chattersListTimer_;
friend class TwitchServer; friend class TwitchIrcServer;
friend class TwitchMessageBuilder; friend class TwitchMessageBuilder;
friend class IrcMessageHandler; friend class IrcMessageHandler;
}; };

View file

@ -1,4 +1,4 @@
#include "TwitchServer.hpp" #include "TwitchIrcServer.hpp"
#include "Application.hpp" #include "Application.hpp"
#include "common/Common.hpp" #include "common/Common.hpp"
@ -38,13 +38,11 @@ namespace {
} }
} // namespace } // namespace
TwitchServer::TwitchServer() TwitchIrcServer::TwitchIrcServer()
: whispersChannel(new Channel("/whispers", Channel::Type::TwitchWhispers)) : whispersChannel(new Channel("/whispers", Channel::Type::TwitchWhispers))
, mentionsChannel(new Channel("/mentions", Channel::Type::TwitchMentions)) , mentionsChannel(new Channel("/mentions", Channel::Type::TwitchMentions))
, watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching) , watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching)
{ {
qDebug() << "init TwitchServer";
this->pubsub = new PubSub; this->pubsub = new PubSub;
// getSettings()->twitchSeperateWriteConnection.connect([this](auto, auto) { // getSettings()->twitchSeperateWriteConnection.connect([this](auto, auto) {
@ -53,7 +51,7 @@ TwitchServer::TwitchServer()
// false); // false);
} }
void TwitchServer::initialize(Settings &settings, Paths &paths) void TwitchIrcServer::initialize(Settings &settings, Paths &paths)
{ {
getApp()->accounts->twitch.currentUserChanged.connect( getApp()->accounts->twitch.currentUserChanged.connect(
[this]() { postToThread([this] { this->connect(); }); }); [this]() { postToThread([this] { this->connect(); }); });
@ -63,11 +61,9 @@ void TwitchServer::initialize(Settings &settings, Paths &paths)
this->ffz.loadEmotes(); this->ffz.loadEmotes();
} }
void TwitchServer::initializeConnection(IrcConnection *connection, bool isRead, void TwitchIrcServer::initializeConnection(IrcConnection *connection,
bool isWrite) ConnectionType type)
{ {
this->singleConnection_ = isRead == isWrite;
std::shared_ptr<TwitchAccount> account = std::shared_ptr<TwitchAccount> account =
getApp()->accounts->twitch.getCurrent(); getApp()->accounts->twitch.getCurrent();
@ -97,9 +93,12 @@ void TwitchServer::initializeConnection(IrcConnection *connection, bool isRead,
// SSL enabled: irc://irc.chat.twitch.tv:6697 // SSL enabled: irc://irc.chat.twitch.tv:6697
connection->setHost("irc.chat.twitch.tv"); connection->setHost("irc.chat.twitch.tv");
connection->setPort(6697); connection->setPort(6697);
this->open(type);
} }
std::shared_ptr<Channel> TwitchServer::createChannel(const QString &channelName) std::shared_ptr<Channel> TwitchIrcServer::createChannel(
const QString &channelName)
{ {
std::shared_ptr<TwitchChannel> channel; std::shared_ptr<TwitchChannel> channel;
if (isChatroom(channelName)) if (isChatroom(channelName))
@ -123,13 +122,17 @@ std::shared_ptr<Channel> TwitchServer::createChannel(const QString &channelName)
return std::shared_ptr<Channel>(channel); return std::shared_ptr<Channel>(channel);
} }
void TwitchServer::privateMessageReceived(Communi::IrcPrivateMessage *message) void TwitchIrcServer::privateMessageReceived(
Communi::IrcPrivateMessage *message)
{ {
IrcMessageHandler::getInstance().handlePrivMessage(message, *this); 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) if (message->type() == Communi::IrcMessage::Type::Private)
{ {
// We already have a handler for private messages // 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(); 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); AbstractIrcServer::onReadConnected(connection);
@ -202,7 +206,7 @@ void TwitchServer::onReadConnected(IrcConnection *connection)
connection->sendRaw("CAP REQ :twitch.tv/tags twitch.tv/membership"); connection->sendRaw("CAP REQ :twitch.tv/tags twitch.tv/membership");
} }
void TwitchServer::onWriteConnected(IrcConnection *connection) void TwitchIrcServer::onWriteConnected(IrcConnection *connection)
{ {
AbstractIrcServer::onWriteConnected(connection); AbstractIrcServer::onWriteConnected(connection);
@ -211,7 +215,7 @@ void TwitchServer::onWriteConnected(IrcConnection *connection)
connection->sendRaw("CAP REQ :twitch.tv/tags twitch.tv/commands"); connection->sendRaw("CAP REQ :twitch.tv/tags twitch.tv/commands");
} }
std::shared_ptr<Channel> TwitchServer::getCustomChannel( std::shared_ptr<Channel> TwitchIrcServer::getCustomChannel(
const QString &channelName) const QString &channelName)
{ {
if (channelName == "/whispers") if (channelName == "/whispers")
@ -249,7 +253,7 @@ std::shared_ptr<Channel> TwitchServer::getCustomChannel(
return nullptr; return nullptr;
} }
void TwitchServer::forEachChannelAndSpecialChannels( void TwitchIrcServer::forEachChannelAndSpecialChannels(
std::function<void(ChannelPtr)> func) std::function<void(ChannelPtr)> func)
{ {
this->forEachChannel(func); this->forEachChannel(func);
@ -258,7 +262,7 @@ void TwitchServer::forEachChannelAndSpecialChannels(
func(this->mentionsChannel); func(this->mentionsChannel);
} }
std::shared_ptr<Channel> TwitchServer::getChannelOrEmptyByID( std::shared_ptr<Channel> TwitchIrcServer::getChannelOrEmptyByID(
const QString &channelId) const QString &channelId)
{ {
std::lock_guard<std::mutex> lock(this->channelMutex); std::lock_guard<std::mutex> lock(this->channelMutex);
@ -283,19 +287,22 @@ std::shared_ptr<Channel> TwitchServer::getChannelOrEmptyByID(
return Channel::getEmpty(); 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 true;
// return getSettings()->twitchSeperateWriteConnection; // return getSettings()->twitchSeperateWriteConnection;
} }
void TwitchServer::onMessageSendRequested(TwitchChannel *channel, void TwitchIrcServer::onMessageSendRequested(TwitchChannel *channel,
const QString &message, bool &sent) const QString &message, bool &sent)
{ {
sent = false; sent = false;
@ -354,11 +361,11 @@ void TwitchServer::onMessageSendRequested(TwitchChannel *channel,
sent = true; sent = true;
} }
const BttvEmotes &TwitchServer::getBttvEmotes() const const BttvEmotes &TwitchIrcServer::getBttvEmotes() const
{ {
return this->bttv; return this->bttv;
} }
const FfzEmotes &TwitchServer::getFfzEmotes() const const FfzEmotes &TwitchIrcServer::getFfzEmotes() const
{ {
return this->ffz; return this->ffz;
} }

View file

@ -20,11 +20,11 @@ class Paths;
class PubSub; class PubSub;
class TwitchChannel; class TwitchChannel;
class TwitchServer final : public AbstractIrcServer, public Singleton class TwitchIrcServer final : public AbstractIrcServer, public Singleton
{ {
public: public:
TwitchServer(); TwitchIrcServer();
virtual ~TwitchServer() override = default; virtual ~TwitchIrcServer() override = default;
virtual void initialize(Settings &settings, Paths &paths) override; virtual void initialize(Settings &settings, Paths &paths) override;
@ -44,8 +44,8 @@ public:
const FfzEmotes &getFfzEmotes() const; const FfzEmotes &getFfzEmotes() const;
protected: protected:
virtual void initializeConnection(IrcConnection *connection, bool isRead, virtual void initializeConnection(IrcConnection *connection,
bool isWrite) override; ConnectionType type) override;
virtual std::shared_ptr<Channel> createChannel( virtual std::shared_ptr<Channel> createChannel(
const QString &channelName) override; const QString &channelName) override;
@ -75,7 +75,6 @@ private:
std::chrono::steady_clock::time_point lastErrorTimeSpeed_; std::chrono::steady_clock::time_point lastErrorTimeSpeed_;
std::chrono::steady_clock::time_point lastErrorTimeAmount_; std::chrono::steady_clock::time_point lastErrorTimeAmount_;
bool singleConnection_ = false;
TwitchBadges twitchBadges; TwitchBadges twitchBadges;
BttvEmotes bttv; BttvEmotes bttv;
FfzEmotes ffz; FfzEmotes ffz;

View file

@ -10,7 +10,7 @@
#include "providers/chatterino/ChatterinoBadges.hpp" #include "providers/chatterino/ChatterinoBadges.hpp"
#include "providers/twitch/TwitchBadges.hpp" #include "providers/twitch/TwitchBadges.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Emotes.hpp" #include "singletons/Emotes.hpp"
#include "singletons/Resources.hpp" #include "singletons/Resources.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"

View file

@ -1,7 +1,7 @@
#include "singletons/NativeMessaging.hpp" #include "singletons/NativeMessaging.hpp"
#include "Application.hpp" #include "Application.hpp"
#include "providers/twitch/TwitchServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Paths.hpp" #include "singletons/Paths.hpp"
#include "util/PostToThread.hpp" #include "util/PostToThread.hpp"

View file

@ -136,7 +136,7 @@ void Paths::initSubDirectories()
this->messageLogDirectory = makePath("Logs"); this->messageLogDirectory = makePath("Logs");
this->miscDirectory = makePath("Misc"); this->miscDirectory = makePath("Misc");
this->twitchProfileAvatars = makePath("ProfileAvatars"); this->twitchProfileAvatars = makePath("ProfileAvatars");
QDir().mkdir(this->twitchProfileAvatars + "/twitch"); //QDir().mkdir(this->twitchProfileAvatars + "/twitch");
} }
Paths *getPaths() Paths *getPaths()

View file

@ -201,6 +201,10 @@ public:
/// Misc /// Misc
BoolSetting betaUpdates = {"/misc/beta", false}; BoolSetting betaUpdates = {"/misc/beta", false};
#ifdef Q_OS_LINUX
BoolSetting useKeyring = {"/misc/useKeyring", true};
#endif
IntSetting startUpNotification = {"/misc/startUpNotification", 0}; IntSetting startUpNotification = {"/misc/startUpNotification", 0};
QStringSetting currentVersion = {"/misc/currentVersion", ""}; QStringSetting currentVersion = {"/misc/currentVersion", ""};
BoolSetting loadTwitchMessageHistoryOnConnect = { BoolSetting loadTwitchMessageHistoryOnConnect = {
@ -210,6 +214,15 @@ public:
QStringSetting cachePath = {"/cache/path", ""}; 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: private:
void updateModerationActions(); void updateModerationActions();
}; };

View file

@ -6,7 +6,7 @@
#include "controllers/notifications/NotificationController.hpp" #include "controllers/notifications/NotificationController.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchCommon.hpp"
#include "providers/twitch/TwitchServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Paths.hpp" #include "singletons/Paths.hpp"
#include "util/StreamLink.hpp" #include "util/StreamLink.hpp"
#include "widgets/helper/CommonTexts.hpp" #include "widgets/helper/CommonTexts.hpp"

View file

@ -4,7 +4,10 @@
#include "debug/AssertInGuiThread.hpp" #include "debug/AssertInGuiThread.hpp"
#include "debug/Log.hpp" #include "debug/Log.hpp"
#include "messages/MessageElement.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/Fonts.hpp"
#include "singletons/Paths.hpp" #include "singletons/Paths.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
@ -611,6 +614,20 @@ void WindowManager::encodeChannel(IndirectChannel channel, QJsonObject &obj)
obj.insert("type", "whispers"); obj.insert("type", "whispers");
} }
break; break;
case Channel::Type::Irc:
{
if (auto ircChannel =
dynamic_cast<IrcChannel *>(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; 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(); return Channel::getEmpty();
} }

View file

@ -126,6 +126,20 @@ public:
return LayoutCreator<T2>(item); return LayoutCreator<T2>(item);
} }
template <typename Slot, typename Func>
LayoutCreator<T> connect(Slot slot, QObject *receiver, Func func)
{
QObject::connect(this->getElement(), slot, receiver, func);
return *this;
}
template <typename Func>
LayoutCreator<T> onClick(QObject *receiver, Func func)
{
QObject::connect(this->getElement(), &T::clicked, receiver, func);
return *this;
}
private: private:
T *item_; T *item_;
@ -169,4 +183,12 @@ private:
} }
}; };
template <typename T, typename... Args>
LayoutCreator<T> makeDialog(Args &&... args)
{
T *t = new T(std::forward<Args>(args)...);
t->setAttribute(Qt::WA_DeleteOnClose);
return LayoutCreator<T>(t);
}
} // namespace chatterino } // namespace chatterino

13
src/util/Overloaded.hpp Normal file
View file

@ -0,0 +1,13 @@
#pragma once
namespace chatterino {
template <class... Ts>
struct Overloaded : Ts... {
using Ts::operator()...;
};
template <class... Ts>
Overloaded(Ts...)->Overloaded<Ts...>;
} // namespace chatterino

View file

@ -62,8 +62,9 @@ private:
if (other) if (other)
{ {
this->conn_ = this->conn_ =
QObject::connect(other, &QObject::destroyed, QObject::connect(other, &QObject::destroyed, qApp,
[this](QObject *) { this->set(nullptr); }); [this](QObject *) { this->set(nullptr); },
Qt::DirectConnection);
} }
this->t_ = other; this->t_ = other;

View file

@ -7,7 +7,7 @@ namespace chatterino {
static void setBoolItem(QStandardItem *item, bool value, static void setBoolItem(QStandardItem *item, bool value,
bool userCheckable = true, bool selectable = true) bool userCheckable = true, bool selectable = true)
{ {
item->setFlags((Qt::ItemFlags)( item->setFlags(Qt::ItemFlags(
Qt::ItemIsEnabled | (selectable ? Qt::ItemIsSelectable : 0) | Qt::ItemIsEnabled | (selectable ? Qt::ItemIsSelectable : 0) |
(userCheckable ? Qt::ItemIsUserCheckable : 0))); (userCheckable ? Qt::ItemIsUserCheckable : 0)));
item->setCheckState(value ? Qt::Checked : Qt::Unchecked); 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) bool editable = true, bool selectable = true)
{ {
item->setData(value, Qt::EditRole); item->setData(value, Qt::EditRole);
item->setFlags((Qt::ItemFlags)(Qt::ItemIsEnabled | item->setFlags(Qt::ItemFlags(Qt::ItemIsEnabled |
(selectable ? Qt::ItemIsSelectable : 0) | (selectable ? Qt::ItemIsSelectable : 0) |
(editable ? (Qt::ItemIsEditable) : 0))); (editable ? (Qt::ItemIsEditable) : 0)));
} }
static QStandardItem *emptyItem() static QStandardItem *emptyItem()
{ {
auto *item = new QStandardItem(); auto *item = new QStandardItem();
item->setFlags((Qt::ItemFlags)0); item->setFlags(Qt::ItemFlags(0));
return item; return item;
} }

View file

@ -1,10 +1,11 @@
#include "widgets/Window.hpp" #include "widgets/Window.hpp"
#include "Application.hpp" #include "Application.hpp"
#include "common/Credentials.hpp"
#include "common/Modes.hpp" #include "common/Modes.hpp"
#include "common/Version.hpp" #include "common/Version.hpp"
#include "controllers/accounts/AccountController.hpp" #include "controllers/accounts/AccountController.hpp"
#include "providers/twitch/TwitchServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
#include "singletons/Theme.hpp" #include "singletons/Theme.hpp"
#include "singletons/Updates.hpp" #include "singletons/Updates.hpp"
@ -108,7 +109,7 @@ bool Window::event(QEvent *event)
break; break;
default:; default:;
}; }
return BaseWindow::event(event); return BaseWindow::event(event);
} }

View file

@ -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<int>(&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

View file

@ -0,0 +1,32 @@
#pragma once
#include <boost/optional.hpp>
#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

View file

@ -0,0 +1,261 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>IrcConnectionEditor</class>
<widget class="QDialog" name="IrcConnectionEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>329</width>
<height>414</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="serverLabel">
<property name="text">
<string>Host:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="serverLineEdit">
<property name="placeholderText">
<string>irc.example.com</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="portLabel">
<property name="text">
<string>Port:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="portSpinBox">
<property name="maximum">
<number>65636</number>
</property>
<property name="value">
<number>6697</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="securityLabel">
<property name="text">
<string>SSL:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="securityCheckBox">
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="0">
<widget class="QLabel" name="userNameLabel">
<property name="text">
<string>User Name:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="userNameLineEdit"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="nickNameLabel">
<property name="text">
<string>Nick Name:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="nickNameLineEdit"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="realNameLabel">
<property name="text">
<string>Real Name:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QLineEdit" name="realNameLineEdit"/>
</item>
<item row="8" column="0">
<widget class="QLabel" name="loginMethodLabel">
<property name="text">
<string>Login method:</string>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="passwordLabel">
<property name="text">
<string>Password:</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QLineEdit" name="passwordLineEdit">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="7" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="8" column="1">
<widget class="QComboBox" name="loginMethodComboBox">
<item>
<property name="text">
<string>Anonymous</string>
</property>
</item>
<item>
<property name="text">
<string>Custom</string>
</property>
</item>
<item>
<property name="text">
<string>Server Password (/PASS $password)</string>
</property>
</item>
<item>
<property name="text">
<string>SASL</string>
</property>
</item>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="connectCommandsLabel">
<property name="text">
<string>Send IRC commands
on connect:</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QPlainTextEdit" name="connectCommandsEditor">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="11" column="1">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>serverLineEdit</tabstop>
<tabstop>portSpinBox</tabstop>
<tabstop>securityCheckBox</tabstop>
<tabstop>userNameLineEdit</tabstop>
<tabstop>nickNameLineEdit</tabstop>
<tabstop>realNameLineEdit</tabstop>
<tabstop>loginMethodComboBox</tabstop>
<tabstop>passwordLineEdit</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>IrcConnectionEditor</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>254</x>
<y>248</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>IrcConnectionEditor</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>254</x>
<y>248</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View file

@ -1,10 +1,11 @@
#include "SelectChannelDialog.hpp" #include "SelectChannelDialog.hpp"
#include "Application.hpp" #include "Application.hpp"
#include "providers/twitch/TwitchServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Theme.hpp" #include "singletons/Theme.hpp"
#include "util/LayoutCreator.hpp" #include "util/LayoutCreator.hpp"
#include "widgets/Notebook.hpp" #include "widgets/Notebook.hpp"
#include "widgets/dialogs/IrcConnectionEditor.hpp"
#include "widgets/helper/NotebookTab.hpp" #include "widgets/helper/NotebookTab.hpp"
#include <QDialogButtonBox> #include <QDialogButtonBox>
@ -14,7 +15,12 @@
#include <QLineEdit> #include <QLineEdit>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QTableView>
#include "providers/irc/Irc2.hpp"
#include "widgets/helper/EditableModelView.hpp"
#define TAB_TWITCH 0 #define TAB_TWITCH 0
#define TAB_IRC 1
namespace chatterino { namespace chatterino {
@ -122,21 +128,69 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent)
} }
// irc // irc
/* {
LayoutCreator<QWidget> obj(new QWidget());
auto outerBox = obj.setLayoutType<QFormLayout>();
{ {
LayoutCreator<QWidget> obj(new QWidget()); auto view = this->ui_.irc.servers = new EditableModelView(
auto vbox = obj.setLayoutType<QVBoxLayout>(); Irc::getInstance().newConnectionModel(this));
auto form = vbox.emplace<QFormLayout>();
form->addRow(new QLabel("User name:"), new QLineEdit()); view->setTitles({"host", "port", "ssl", "user", "nick", "real",
form->addRow(new QLabel("First nick choice:"), new QLineEdit()); "password", "login command"});
form->addRow(new QLabel("Second nick choice:"), new QLineEdit()); view->getTableView()->horizontalHeader()->resizeSection(0, 140);
form->addRow(new QLabel("Third nick choice:"), new QLineEdit());
auto tab = notebook->addPage(obj.getElement()); view->getTableView()->horizontalHeader()->setSectionHidden(1, true);
tab->setCustomTitle("Irc"); 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); layout->setStretchFactor(notebook.getElement(), 1);
@ -151,7 +205,7 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent)
[=](bool) { this->close(); }); [=](bool) { this->close(); });
} }
this->setScaleIndependantSize(300, 310); this->setMinimumSize(300, 310);
this->ui_.notebook->selectIndex(TAB_TWITCH); this->ui_.notebook->selectIndex(TAB_TWITCH);
this->ui_.twitch.channel->setFocus(); this->ui_.twitch.channel->setFocus();
@ -161,10 +215,24 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent)
auto *shortcut_cancel = new QShortcut(QKeySequence("Esc"), this); auto *shortcut_cancel = new QShortcut(QKeySequence("Esc"), this);
QObject::connect(shortcut_cancel, &QShortcut::activated, QObject::connect(shortcut_cancel, &QShortcut::activated,
[=] { this->close(); }); [=] { this->close(); });
// restore ui state
this->ui_.notebook->selectIndex(getSettings()->lastSelectChannelTab);
this->ui_.irc.servers->getTableView()->selectRow(
getSettings()->lastSelectIrcConn);
} }
void SelectChannelDialog::ok() 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->hasSelectedChannel_ = true;
this->close(); this->close();
} }
@ -204,6 +272,32 @@ void SelectChannelDialog::setSelectedChannel(IndirectChannel _channel)
this->ui_.twitch.whispers->setFocus(); this->ui_.twitch.whispers->setFocus();
} }
break; break;
case Channel::Type::Irc:
{
this->ui_.notebook->selectIndex(TAB_IRC);
this->ui_.irc.channel->setText(_channel.get()->getName());
if (auto ircChannel =
dynamic_cast<IrcChannel *>(_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: default:
{ {
this->ui_.notebook->selectIndex(TAB_TWITCH); this->ui_.notebook->selectIndex(TAB_TWITCH);
@ -245,6 +339,27 @@ IndirectChannel SelectChannelDialog::getSelectedChannel() const
return app->twitch.server->whispersChannel; 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_; return this->selectedChannel_;
@ -258,7 +373,7 @@ bool SelectChannelDialog::hasSeletedChannel() const
bool SelectChannelDialog::EventFilter::eventFilter(QObject *watched, bool SelectChannelDialog::EventFilter::eventFilter(QObject *watched,
QEvent *event) QEvent *event)
{ {
auto *widget = (QWidget *)watched; auto *widget = static_cast<QWidget *>(watched);
if (event->type() == QEvent::FocusIn) if (event->type() == QEvent::FocusIn)
{ {

View file

@ -12,6 +12,7 @@
namespace chatterino { namespace chatterino {
class Notebook; class Notebook;
class EditableModelView;
class SelectChannelDialog final : public BaseWindow class SelectChannelDialog final : public BaseWindow
{ {
@ -47,6 +48,10 @@ private:
QRadioButton *mentions; QRadioButton *mentions;
QRadioButton *watching; QRadioButton *watching;
} twitch; } twitch;
struct {
QLineEdit *channel;
EditableModelView *servers;
} irc;
} ui_; } ui_;
EventFilter tabFilter_; EventFilter tabFilter_;

View file

@ -12,7 +12,7 @@
#include "messages/layouts/MessageLayout.hpp" #include "messages/layouts/MessageLayout.hpp"
#include "messages/layouts/MessageLayoutElement.hpp" #include "messages/layouts/MessageLayoutElement.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
#include "singletons/Theme.hpp" #include "singletons/Theme.hpp"
#include "singletons/TooltipPreviewImage.hpp" #include "singletons/TooltipPreviewImage.hpp"

View file

@ -98,6 +98,9 @@ AboutPage::AboutPage()
addLicense(form.getElement(), "Websocketpp", addLicense(form.getElement(), "Websocketpp",
"https://www.zaphoyd.com/websocketpp/", "https://www.zaphoyd.com/websocketpp/",
":/licenses/websocketpp.txt"); ":/licenses/websocketpp.txt");
addLicense(form.getElement(), "QtKeychain",
"https://github.com/frankosterfeld/qtkeychain",
":/licenses/qtkeychain.txt");
} }
auto attributions = layout.emplace<QGroupBox>("Attributions..."); auto attributions = layout.emplace<QGroupBox>("Attributions...");

View file

@ -451,6 +451,16 @@ void GeneralPage::initLayout(SettingsLayout &layout)
layout.addCheckbox("Open links in incognito/private mode", layout.addCheckbox("Open links in incognito/private mode",
s.openLinksIncognito); 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, layout.addCheckbox("Show moderation messages", s.hideModerationActions,
true); true);
layout.addCheckbox("Random username color for users who never set a color", 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", layout.addCheckbox("Load message history on connect",
s.loadTwitchMessageHistoryOnConnect); s.loadTwitchMessageHistoryOnConnect);
layout.addCheckbox("Show unhandled irc messages",
s.showUnhandledIrcMessages);
layout.addTitle("Cache"); layout.addTitle("Cache");
layout.addDescription( layout.addDescription(
"Files that are used often (such as emotes) are saved to disk to " "Files that are used often (such as emotes) are saved to disk to "

View file

@ -7,7 +7,7 @@
#include "providers/twitch/EmoteValue.hpp" #include "providers/twitch/EmoteValue.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchMessageBuilder.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp"
#include "providers/twitch/TwitchServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
#include "singletons/Theme.hpp" #include "singletons/Theme.hpp"
#include "singletons/WindowManager.hpp" #include "singletons/WindowManager.hpp"

View file

@ -6,7 +6,7 @@
#include "controllers/notifications/NotificationController.hpp" #include "controllers/notifications/NotificationController.hpp"
#include "controllers/pings/PingController.hpp" #include "controllers/pings/PingController.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Resources.hpp" #include "singletons/Resources.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
#include "singletons/Theme.hpp" #include "singletons/Theme.hpp"
@ -211,9 +211,12 @@ void SplitHeader::initializeLayout()
}), }),
// dropdown // dropdown
this->dropdownButton_ = makeWidget<Button>([&](auto w) { this->dropdownButton_ = makeWidget<Button>([&](auto w) {
auto menu = this->createMainMenu(); /// XXX: this never gets disconnected
this->mainMenu_ = menu.get(); this->split_->channelChanged.connect([this] {
w->setMenu(std::move(menu)); auto menu = this->createMainMenu();
this->mainMenu_ = menu.get();
this->dropdownButton_->setMenu(std::move(menu));
});
}), }),
// add split // add split
this->addButton_ = makeWidget<Button>([&](auto w) { this->addButton_ = makeWidget<Button>([&](auto w) {
@ -275,13 +278,17 @@ std::unique_ptr<QMenu> SplitHeader::createMainMenu()
}); });
#endif #endif
menu->addAction(OPEN_IN_BROWSER, this->split_, &Split::openInBrowser); if (dynamic_cast<TwitchChannel *>(this->split_->getChannel().get()))
{
menu->addAction(OPEN_IN_BROWSER, this->split_, &Split::openInBrowser);
#ifndef USEWEBENGINE #ifndef USEWEBENGINE
menu->addAction(OPEN_PLAYER_IN_BROWSER, this->split_, menu->addAction(OPEN_PLAYER_IN_BROWSER, this->split_,
&Split::openBrowserPlayer); &Split::openBrowserPlayer);
#endif #endif
menu->addAction(OPEN_IN_STREAMLINK, this->split_, &Split::openInStreamlink); menu->addAction(OPEN_IN_STREAMLINK, this->split_,
menu->addSeparator(); &Split::openInStreamlink);
menu->addSeparator();
}
{ {
// "How to..." sub menu // "How to..." sub menu
@ -294,30 +301,35 @@ std::unique_ptr<QMenu> SplitHeader::createMainMenu()
// sub menu // sub menu
auto moreMenu = new QMenu("More", this); auto moreMenu = new QMenu("More", this);
moreMenu->addAction("Toggle moderation mode", this->split_, [this]() { moreMenu->addAction("Toggle moderation mode", this->split_, [this]() {
this->split_->setModerationMode(!this->split_->getModerationMode()); this->split_->setModerationMode(!this->split_->getModerationMode());
}); });
moreMenu->addAction("Show viewer list", this->split_, if (dynamic_cast<TwitchChannel *>(this->split_->getChannel().get()))
&Split::showViewerList); {
moreMenu->addAction("Show viewer list", this->split_,
&Split::showViewerList);
moreMenu->addAction("Subscribe", this->split_, &Split::openSubPage); moreMenu->addAction("Subscribe", this->split_, &Split::openSubPage);
auto action = new QAction(this); auto action = new QAction(this);
action->setText("Notify when live"); action->setText("Notify when live");
action->setCheckable(true); action->setCheckable(true);
QObject::connect(moreMenu, &QMenu::aboutToShow, this, [action, this]() { QObject::connect(moreMenu, &QMenu::aboutToShow, this, [action, this]() {
action->setChecked(getApp()->notifications->isChannelNotified( action->setChecked(getApp()->notifications->isChannelNotified(
this->split_->getChannel()->getName(), Platform::Twitch)); this->split_->getChannel()->getName(), Platform::Twitch));
}); });
action->connect(action, &QAction::triggered, this, [this]() { action->connect(action, &QAction::triggered, this, [this]() {
getApp()->notifications->updateChannelNotification( getApp()->notifications->updateChannelNotification(
this->split_->getChannel()->getName(), Platform::Twitch); this->split_->getChannel()->getName(), Platform::Twitch);
}); });
moreMenu->addAction(action); moreMenu->addAction(action);
}
if (dynamic_cast<TwitchChannel *>(this->split_->getChannel().get()))
{ {
auto action = new QAction(this); auto action = new QAction(this);
action->setText("Mute highlight sound"); action->setText("Mute highlight sound");
@ -336,11 +348,16 @@ std::unique_ptr<QMenu> SplitHeader::createMainMenu()
} }
moreMenu->addSeparator(); moreMenu->addSeparator();
moreMenu->addAction("Reconnect", this, SLOT(reconnect())); if (this->split_->getChannel()->canReconnect())
moreMenu->addAction("Reload channel emotes", this, moreMenu->addAction("Reconnect", this, SLOT(reconnect()));
SLOT(reloadChannelEmotes()));
moreMenu->addAction("Reload subscriber emotes", this, if (dynamic_cast<TwitchChannel *>(this->split_->getChannel().get()))
SLOT(reloadSubscriberEmotes())); {
moreMenu->addAction("Reload channel emotes", this,
SLOT(reloadChannelEmotes()));
moreMenu->addAction("Reload subscriber emotes", this,
SLOT(reloadSubscriberEmotes()));
}
moreMenu->addSeparator(); moreMenu->addSeparator();
moreMenu->addAction("Clear messages", this->split_, &Split::clear); moreMenu->addAction("Clear messages", this->split_, &Split::clear);
// moreMenu->addSeparator(); // moreMenu->addSeparator();
@ -729,7 +746,7 @@ void SplitHeader::reloadSubscriberEmotes()
void SplitHeader::reconnect() void SplitHeader::reconnect()
{ {
getApp()->twitch.server->connect(); this->split_->getChannel()->reconnect();
} }
} // namespace chatterino } // namespace chatterino

View file

@ -4,7 +4,7 @@
#include "controllers/commands/CommandController.hpp" #include "controllers/commands/CommandController.hpp"
#include "messages/Link.hpp" #include "messages/Link.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
#include "singletons/Theme.hpp" #include "singletons/Theme.hpp"
#include "util/LayoutCreator.hpp" #include "util/LayoutCreator.hpp"