Merge pull request #1318 from Chatterino/irc-support

Irc support
This commit is contained in:
fourtf 2019-09-18 14:24:48 +02:00 committed by GitHub
commit 9b6f53ab75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 2130 additions and 305 deletions

3
.gitmodules vendored
View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

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 <variant>
#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 = std::variant<SetJob, EraseJob>;
static std::queue<Job> &jobQueue()
{
static std::queue<Job> jobs;
return jobs;
}
static void runNextJob()
{
auto &&queue = jobQueue();
if (!queue.empty())
{
std::visit(
Overloaded{
[](const SetJob &set) {
qDebug() << "set";
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();
},
[](const EraseJob &erase) {
qDebug() << "erase";
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.front());
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())
{
qDebug() << "queue set";
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())
{
qDebug() << "queue erase";
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
{
(void)parent;
return this->rows_.size();
}
int columnCount(const QModelIndex &parent) const override
{
(void)parent;
return this->columnCount_;
}
QVariant data(const QModelIndex &index, int role) const override
{
int row = index.row(), column = index.column();
assert(row >= 0 && row < this->rows_.size() && column >= 0 &&
column < this->columnCount_);
if (row < 0 || column < 0 || row >= this->rows_.size() ||
column >= this->columnCount_)
{
return QVariant();
}
return rows_[row].items[column]->data(role);
}
@ -123,8 +130,11 @@ public:
int role) override
{
int row = index.row(), column = index.column();
assert(row >= 0 && row < this->rows_.size() && column >= 0 &&
column < this->columnCount_);
if (row < 0 || column < 0 || row >= this->rows_.size() ||
column >= this->columnCount_)
{
return false;
}
Row &rowItem = this->rows_[row];
@ -185,6 +195,13 @@ public:
Qt::ItemFlags flags(const QModelIndex &index) const override
{
int row = index.row(), column = index.column();
if (row < 0 || column < 0 || row >= this->rows_.size() ||
column >= this->columnCount_)
{
return Qt::NoItemFlags;
}
assert(row >= 0 && row < this->rows_.size() && column >= 0 &&
column < this->columnCount_);
@ -207,6 +224,8 @@ public:
bool removeRows(int row, int count, const QModelIndex &parent) override
{
(void)parent;
if (count != 1)
{
return false;
@ -237,18 +256,22 @@ protected:
std::vector<QStandardItem *> &row,
int proposedIndex)
{
(void)item, (void)row;
return proposedIndex;
}
virtual void afterRemoved(const TVectorItem &item,
std::vector<QStandardItem *> &row, int index)
{
(void)item, (void)row, (void)index;
}
virtual void customRowSetData(const std::vector<QStandardItem *> &row,
int column, const QVariant &value, int role,
int rowIndex)
{
(void)row, (void)column, (void)value, (void)role, (void)rowIndex;
}
void insertCustomRow(std::vector<QStandardItem *> row, int index)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,11 +1,33 @@
#pragma once
#include "common/Channel.hpp"
#include "common/ChannelChatters.hpp"
namespace chatterino {
// class IrcChannel
//{
// public:
// IrcChannel();
//};
//
class Irc;
class IrcServer;
class IrcChannel : public Channel, public ChannelChatters
{
public:
explicit IrcChannel(const QString &name, IrcServer *server);
void sendMessage(const QString &message) override;
// server may be nullptr
IrcServer *server();
// Channel methods
virtual bool canReconnect() const override;
virtual void reconnect() override;
private:
void setServer(IrcServer *server);
IrcServer *server_;
friend class Irc;
};
} // namespace chatterino

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())
{
this->sendRaw("PING");
this->sendRaw("PING chatterino/ping");
this->reconnectTimer_.start();
}
this->recentlyReceivedMessage_ = false;

View file

@ -1,12 +1,260 @@
#include "IrcServer.hpp"
#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 {
// IrcServer::IrcServer(const QString &hostname, int port)
//{
// this->initConnection();
//}
//
IrcServer::IrcServer(const IrcServerData &data)
: data_(new IrcServerData(data))
{
this->connect();
}
IrcServer::IrcServer(const IrcServerData &data,
const std::vector<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

View file

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

View file

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

View file

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

View file

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

View file

@ -79,6 +79,7 @@ TwitchChannel::TwitchChannel(const QString &name,
TwitchBadges &globalTwitchBadges, BttvEmotes &bttv,
FfzEmotes &ffz)
: Channel(name, Channel::Type::Twitch)
, ChannelChatters(*static_cast<Channel *>(this))
, subscriptionUrl_("https://www.twitch.tv/subs/" + name)
, channelUrl_("https://twitch.tv/" + name)
, popoutPlayerUrl_("https://player.twitch.tv/?channel=" + name)
@ -280,69 +281,14 @@ bool TwitchChannel::hasHighRateLimit() const
return this->isMod() || this->isBroadcaster() || this->isVIP();
}
void TwitchChannel::addRecentChatter(const MessagePtr &message)
bool TwitchChannel::canReconnect() const
{
this->chatters_.access()->insert(message->displayName);
return true;
}
void TwitchChannel::addJoinedUser(const QString &user)
void TwitchChannel::reconnect()
{
auto app = getApp();
if (user == app->accounts->twitch.getCurrent()->getUserName() ||
!getSettings()->showJoins.getValue())
{
return;
}
auto joinedUsers = this->joinedUsers_.access();
joinedUsers->append(user);
if (!this->joinedUsersMergeQueued_)
{
this->joinedUsersMergeQueued_ = true;
QTimer::singleShot(500, &this->lifetimeGuard_, [this] {
auto joinedUsers = this->joinedUsers_.access();
MessageBuilder builder(systemMessage,
"Users joined: " + joinedUsers->join(", "));
builder->flags.set(MessageFlag::Collapsed);
joinedUsers->clear();
this->addMessage(builder.release());
this->joinedUsersMergeQueued_ = false;
});
}
}
void TwitchChannel::addPartedUser(const QString &user)
{
auto app = getApp();
if (user == app->accounts->twitch.getCurrent()->getUserName() ||
!getSettings()->showJoins.getValue())
{
return;
}
auto partedUsers = this->partedUsers_.access();
partedUsers->append(user);
if (!this->partedUsersMergeQueued_)
{
this->partedUsersMergeQueued_ = true;
QTimer::singleShot(500, &this->lifetimeGuard_, [this] {
auto partedUsers = this->partedUsers_.access();
MessageBuilder builder(systemMessage,
"Users parted: " + partedUsers->join(", "));
builder->flags.set(MessageFlag::Collapsed);
this->addMessage(builder.release());
partedUsers->clear();
this->partedUsersMergeQueued_ = false;
});
}
getApp()->twitch.server->connect();
}
QString TwitchChannel::roomId() const
@ -384,11 +330,6 @@ AccessGuard<const TwitchChannel::StreamStatus>
return this->streamStatus_.accessConst();
}
AccessGuard<const UsernameSet> TwitchChannel::accessChatters() const
{
return this->chatters_.accessConst();
}
const TwitchBadges &TwitchChannel::globalTwitchBadges() const
{
return this->globalTwitchBadges_;
@ -691,7 +632,7 @@ void TwitchChannel::refreshChatters()
auto pair = parseChatters(result.parseJson());
if (pair.first)
{
*this->chatters_.access() = std::move(pair.second);
this->setChatters(std::move(pair.second));
}
return pair.first;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -199,6 +199,10 @@ public:
/// Misc
BoolSetting betaUpdates = {"/misc/beta", false};
#ifdef Q_OS_LINUX
BoolSetting useKeyring = {"/misc/useKeyring", true};
#endif
IntSetting startUpNotification = {"/misc/startUpNotification", 0};
QStringSetting currentVersion = {"/misc/currentVersion", ""};
BoolSetting loadTwitchMessageHistoryOnConnect = {
@ -208,6 +212,15 @@ public:
QStringSetting cachePath = {"/cache/path", ""};
/// Debug
BoolSetting showUnhandledIrcMessages = {"/debug/showUnhandledIrcMessages",
false};
/// UI
// Purely QOL settings are here (like last item in a list).
IntSetting lastSelectChannelTab = {"/ui/lastSelectChannelTab", 0};
IntSetting lastSelectIrcConn = {"/ui/lastSelectIrcConn", 0};
private:
void updateModerationActions();
};

View file

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

View file

@ -4,7 +4,10 @@
#include "debug/AssertInGuiThread.hpp"
#include "debug/Log.hpp"
#include "messages/MessageElement.hpp"
#include "providers/twitch/TwitchServer.hpp"
#include "providers/irc/Irc2.hpp"
#include "providers/irc/IrcChannel2.hpp"
#include "providers/irc/IrcServer.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Fonts.hpp"
#include "singletons/Paths.hpp"
#include "singletons/Settings.hpp"
@ -611,6 +614,20 @@ void WindowManager::encodeChannel(IndirectChannel channel, QJsonObject &obj)
obj.insert("type", "whispers");
}
break;
case Channel::Type::Irc:
{
if (auto ircChannel =
dynamic_cast<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;
}
else if (type == "irc")
{
return Irc::getInstance().getOrAddChannel(
obj.value("server").toInt(-1), obj.value("channel").toString());
}
return Channel::getEmpty();
}

View file

@ -126,6 +126,20 @@ public:
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:
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

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)
{
this->conn_ =
QObject::connect(other, &QObject::destroyed,
[this](QObject *) { this->set(nullptr); });
QObject::connect(other, &QObject::destroyed, qApp,
[this](QObject *) { this->set(nullptr); },
Qt::DirectConnection);
}
this->t_ = other;

View file

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

View file

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

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 "Application.hpp"
#include "providers/twitch/TwitchServer.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Theme.hpp"
#include "util/LayoutCreator.hpp"
#include "widgets/Notebook.hpp"
#include "widgets/dialogs/IrcConnectionEditor.hpp"
#include "widgets/helper/NotebookTab.hpp"
#include <QDialogButtonBox>
@ -14,7 +15,12 @@
#include <QLineEdit>
#include <QVBoxLayout>
#include <QTableView>
#include "providers/irc/Irc2.hpp"
#include "widgets/helper/EditableModelView.hpp"
#define TAB_TWITCH 0
#define TAB_IRC 1
namespace chatterino {
@ -122,21 +128,69 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent)
}
// irc
/*
{
LayoutCreator<QWidget> obj(new QWidget());
auto outerBox = obj.setLayoutType<QFormLayout>();
{
LayoutCreator<QWidget> obj(new QWidget());
auto vbox = obj.setLayoutType<QVBoxLayout>();
auto form = vbox.emplace<QFormLayout>();
auto view = this->ui_.irc.servers = new EditableModelView(
Irc::getInstance().newConnectionModel(this));
form->addRow(new QLabel("User name:"), new QLineEdit());
form->addRow(new QLabel("First nick choice:"), new QLineEdit());
form->addRow(new QLabel("Second nick choice:"), new QLineEdit());
form->addRow(new QLabel("Third nick choice:"), new QLineEdit());
view->setTitles({"host", "port", "ssl", "user", "nick", "real",
"password", "login command"});
view->getTableView()->horizontalHeader()->resizeSection(0, 140);
auto tab = notebook->addPage(obj.getElement());
tab->setCustomTitle("Irc");
view->getTableView()->horizontalHeader()->setSectionHidden(1, true);
view->getTableView()->horizontalHeader()->setSectionHidden(2, true);
view->getTableView()->horizontalHeader()->setSectionHidden(4, true);
view->getTableView()->horizontalHeader()->setSectionHidden(5, true);
view->addButtonPressed.connect([] {
auto unique = IrcServerData{};
unique.id = Irc::getInstance().uniqueId();
auto editor = new IrcConnectionEditor(unique);
if (editor->exec() == QDialog::Accepted)
{
Irc::getInstance().connections.appendItem(editor->data());
}
});
QObject::connect(
view->getTableView(), &QTableView::doubleClicked,
[](const QModelIndex &index) {
auto editor = new IrcConnectionEditor(
Irc::getInstance()
.connections.getVector()[size_t(index.row())]);
if (editor->exec() == QDialog::Accepted)
{
auto data = editor->data();
auto &&conns =
Irc::getInstance().connections.getVector();
int i = 0;
for (auto &&conn : conns)
{
if (conn.id == data.id)
{
Irc::getInstance().connections.removeItem(
i, Irc::noEraseCredentialCaller);
Irc::getInstance().connections.insertItem(data,
i);
}
i++;
}
}
});
outerBox->addRow("Server:", view);
}
*/
outerBox->addRow("Channel:", this->ui_.irc.channel = new QLineEdit);
auto tab = notebook->addPage(obj.getElement());
tab->setCustomTitle("Irc (Beta)");
}
layout->setStretchFactor(notebook.getElement(), 1);
@ -151,7 +205,7 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent)
[=](bool) { this->close(); });
}
this->setScaleIndependantSize(300, 310);
this->setMinimumSize(300, 310);
this->ui_.notebook->selectIndex(TAB_TWITCH);
this->ui_.twitch.channel->setFocus();
@ -161,10 +215,24 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent)
auto *shortcut_cancel = new QShortcut(QKeySequence("Esc"), this);
QObject::connect(shortcut_cancel, &QShortcut::activated,
[=] { this->close(); });
// restore ui state
this->ui_.notebook->selectIndex(getSettings()->lastSelectChannelTab);
this->ui_.irc.servers->getTableView()->selectRow(
getSettings()->lastSelectIrcConn);
}
void SelectChannelDialog::ok()
{
// save ui state
getSettings()->lastSelectChannelTab =
this->ui_.notebook->getSelectedIndex();
getSettings()->lastSelectIrcConn = this->ui_.irc.servers->getTableView()
->selectionModel()
->currentIndex()
.row();
// accept and close
this->hasSelectedChannel_ = true;
this->close();
}
@ -204,6 +272,32 @@ void SelectChannelDialog::setSelectedChannel(IndirectChannel _channel)
this->ui_.twitch.whispers->setFocus();
}
break;
case Channel::Type::Irc:
{
this->ui_.notebook->selectIndex(TAB_IRC);
this->ui_.irc.channel->setText(_channel.get()->getName());
if (auto ircChannel =
dynamic_cast<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:
{
this->ui_.notebook->selectIndex(TAB_TWITCH);
@ -245,6 +339,27 @@ IndirectChannel SelectChannelDialog::getSelectedChannel() const
return app->twitch.server->whispersChannel;
}
}
break;
case TAB_IRC:
{
int row = this->ui_.irc.servers->getTableView()
->selectionModel()
->currentIndex()
.row();
auto &&vector = Irc::getInstance().connections.getVector();
if (row >= 0 && row < int(vector.size()))
{
return Irc::getInstance().getOrAddChannel(
vector[size_t(row)].id, this->ui_.irc.channel->text());
}
else
{
return Channel::getEmpty();
}
}
//break;
}
return this->selectedChannel_;
@ -258,7 +373,7 @@ bool SelectChannelDialog::hasSeletedChannel() const
bool SelectChannelDialog::EventFilter::eventFilter(QObject *watched,
QEvent *event)
{
auto *widget = (QWidget *)watched;
auto *widget = static_cast<QWidget *>(watched);
if (event->type() == QEvent::FocusIn)
{

View file

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

View file

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

View file

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

View file

@ -383,6 +383,16 @@ void GeneralPage::initLayout(SettingsLayout &layout)
layout.addCheckbox("Open links in incognito/private mode",
s.openLinksIncognito);
}
#ifdef Q_OS_LINUX
if (!getPaths()->isPortable())
{
layout.addCheckbox(
"Use libsecret/KWallet/Gnome keychain to secure passwords",
s.useKeyring);
}
#endif
layout.addCheckbox("Show moderation messages", s.hideModerationActions,
true);
layout.addCheckbox("Random username color for users who never set a color",
@ -419,6 +429,9 @@ void GeneralPage::initLayout(SettingsLayout &layout)
layout.addCheckbox("Load message history on connect",
s.loadTwitchMessageHistoryOnConnect);
layout.addCheckbox("Show unhandled irc messages",
s.showUnhandledIrcMessages);
layout.addTitle("Cache");
layout.addDescription(
"Files that are used often (such as emotes) are saved to disk to "

View file

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

View file

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

View file

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