Merge branch 'master' into moderation

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

3
.gitmodules vendored
View file

@ -22,6 +22,9 @@
[submodule "lib/rapidjson"]
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

@ -0,0 +1,20 @@
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -46,6 +46,7 @@
<file>licenses/pajlada_settings.txt</file>
<file>licenses/pajlada_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 <boost/variant.hpp>
#define FORMAT_NAME \
([&] { \
assert(!provider.contains(":")); \
return QString("chatterino:%1:%2").arg(provider).arg(name_); \
})()
namespace chatterino {
namespace {
bool useKeyring()
{
if (getPaths()->isPortable())
{
return false;
}
else
{
#ifdef Q_OS_LINUX
return getSettings()->useKeyring;
#else
return true;
#endif
}
}
// Insecure storage:
QString insecurePath()
{
return combinePath(getPaths()->settingsDirectory, "credentials.json");
}
QJsonDocument loadInsecure()
{
QFile file(insecurePath());
file.open(QIODevice::ReadOnly);
return QJsonDocument::fromJson(file.readAll());
}
void storeInsecure(const QJsonDocument &doc)
{
QSaveFile file(insecurePath());
file.open(QIODevice::WriteOnly);
file.write(doc.toJson());
file.commit();
}
QJsonDocument &insecureInstance()
{
static auto store = loadInsecure();
return store;
}
void queueInsecureSave()
{
static bool isQueued = false;
if (!isQueued)
{
isQueued = true;
QTimer::singleShot(200, qApp, [] {
storeInsecure(insecureInstance());
isQueued = false;
});
}
}
// QKeychain runs jobs asyncronously, so we have to assure that set/erase
// jobs gets executed in order.
struct SetJob {
QString name;
QString credential;
};
struct EraseJob {
QString name;
};
using Job = boost::variant<SetJob, EraseJob>;
static std::queue<Job> &jobQueue()
{
static std::queue<Job> jobs;
return jobs;
}
static void runNextJob()
{
auto &&queue = jobQueue();
if (!queue.empty())
{
// we were gonna use std::visit here but macos is shit
auto &&item = queue.front();
if (item.which() == 0) // set job
{
auto set = boost::get<SetJob>(item);
auto job = new QKeychain::WritePasswordJob("chatterino");
job->setAutoDelete(true);
job->setKey(set.name);
job->setTextData(set.credential);
QObject::connect(job, &QKeychain::Job::finished, qApp,
[](auto) { runNextJob(); });
job->start();
}
else // erase job
{
auto erase = boost::get<EraseJob>(item);
auto job = new QKeychain::DeletePasswordJob("chatterino");
job->setAutoDelete(true);
job->setKey(erase.name);
QObject::connect(job, &QKeychain::Job::finished, qApp,
[](auto) { runNextJob(); });
job->start();
}
queue.pop();
}
}
static void queueJob(Job &&job)
{
auto &&queue = jobQueue();
queue.push(std::move(job));
if (queue.size() == 1)
{
runNextJob();
}
}
} // namespace
Credentials &Credentials::getInstance()
{
static Credentials creds;
return creds;
}
Credentials::Credentials()
{
}
void Credentials::get(const QString &provider, const QString &name_,
QObject *receiver,
std::function<void(const QString &)> &&onLoaded)
{
assertInGuiThread();
auto name = FORMAT_NAME;
if (useKeyring())
{
auto job = new QKeychain::ReadPasswordJob("chatterino");
job->setAutoDelete(true);
job->setKey(name);
QObject::connect(job, &QKeychain::Job::finished, receiver,
[job, onLoaded = std::move(onLoaded)](auto) mutable {
onLoaded(job->textData());
},
Qt::DirectConnection);
job->start();
}
else
{
auto &instance = insecureInstance();
onLoaded(instance.object().find(name).value().toString());
}
}
void Credentials::set(const QString &provider, const QString &name_,
const QString &credential)
{
assertInGuiThread();
/// On linux, we try to use a keychain but show a message to disable it when it fails.
/// XXX: add said message
auto name = FORMAT_NAME;
if (useKeyring())
{
queueJob(SetJob{name, credential});
}
else
{
auto &instance = insecureInstance();
instance.object()[name] = credential;
queueInsecureSave();
}
}
void Credentials::erase(const QString &provider, const QString &name_)
{
assertInGuiThread();
auto name = FORMAT_NAME;
if (useKeyring())
{
queueJob(EraseJob{name});
}
else
{
auto &instance = insecureInstance();
if (auto it = instance.object().find(name);
it != instance.object().end())
{
instance.object().erase(it);
}
queueInsecureSave();
}
}
} // namespace chatterino

View file

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

View file

@ -102,19 +102,26 @@ public:
int rowCount(const QModelIndex &parent) const override
{
(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
{
std::lock_guard<std::mutex> lock1(this->connectionMutex_);
std::lock_guard<std::mutex> lock2(this->channelMutex);
void AbstractIrcServer::open(ConnectionType type)
{
std::lock_guard<std::mutex> lock(this->connectionMutex_);
for (std::weak_ptr<Channel> &weak : this->channels.values())
if (type == Write)
{
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();
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,47 +158,50 @@ 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
{
std::lock_guard<std::mutex> lock2(this->connectionMutex_);
if (this->readConnection_)
{
if (this->readConnection_->isConnected())
{
this->readConnection_->sendRaw("JOIN #" + channelName);
}
}
if (this->writeConnection_)
if (this->writeConnection_ && this->hasSeparateWriteConnection())
{
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,11 +322,15 @@ void AbstractIrcServer::onSocketError()
std::shared_ptr<Channel> AbstractIrcServer::getCustomChannel(
const QString &channelName)
{
(void)channelName;
return nullptr;
}
QString AbstractIrcServer::cleanChannelName(const QString &dirtyChannelName)
{
if (dirtyChannelName.startsWith('#'))
return dirtyChannelName.mid(1);
else
return dirtyChannelName;
}
@ -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();
@ -590,9 +597,14 @@ void IrcMessageHandler::handleJoinMessage(Communi::IrcMessage *message)
if (TwitchChannel *twitchChannel =
dynamic_cast<TwitchChannel *>(channel.get()))
{
if (message->nick() !=
getApp()->accounts->twitch.getCurrent()->getUserName() &&
getSettings()->showJoins.getValue())
{
twitchChannel->addJoinedUser(message->nick());
}
}
}
void IrcMessageHandler::handlePartMessage(Communi::IrcMessage *message)
@ -603,9 +615,14 @@ void IrcMessageHandler::handlePartMessage(Communi::IrcMessage *message)
if (TwitchChannel *twitchChannel =
dynamic_cast<TwitchChannel *>(channel.get()))
{
if (message->nick() !=
getApp()->accounts->twitch.getCurrent()->getUserName() &&
getSettings()->showJoins.getValue())
{
twitchChannel->addPartedUser(message->nick());
}
}
}
} // namespace chatterino

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,18 +287,21 @@ std::shared_ptr<Channel> TwitchServer::getChannelOrEmptyByID(
return Channel::getEmpty();
}
QString TwitchServer::cleanChannelName(const QString &dirtyChannelName)
QString TwitchIrcServer::cleanChannelName(const QString &dirtyChannelName)
{
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,
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

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

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,7 +17,7 @@ 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 |
item->setFlags(Qt::ItemFlags(Qt::ItemIsEnabled |
(selectable ? Qt::ItemIsSelectable : 0) |
(editable ? (Qt::ItemIsEditable) : 0)));
}
@ -25,7 +25,7 @@ static void setStringItem(QStandardItem *item, const QString &value,
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 vbox = obj.setLayoutType<QVBoxLayout>();
auto form = vbox.emplace<QFormLayout>();
auto outerBox = obj.setLayoutType<QFormLayout>();
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());
{
auto view = this->ui_.irc.servers = new EditableModelView(
Irc::getInstance().newConnectionModel(this));
view->setTitles({"host", "port", "ssl", "user", "nick", "real",
"password", "login command"});
view->getTableView()->horizontalHeader()->resizeSection(0, 140);
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");
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

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

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) {
/// XXX: this never gets disconnected
this->split_->channelChanged.connect([this] {
auto menu = this->createMainMenu();
this->mainMenu_ = menu.get();
w->setMenu(std::move(menu));
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
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);
#endif
menu->addAction(OPEN_IN_STREAMLINK, this->split_, &Split::openInStreamlink);
menu->addAction(OPEN_IN_STREAMLINK, this->split_,
&Split::openInStreamlink);
menu->addSeparator();
}
{
// "How to..." sub menu
@ -294,10 +301,13 @@ std::unique_ptr<QMenu> SplitHeader::createMainMenu()
// sub menu
auto moreMenu = new QMenu("More", this);
moreMenu->addAction("Toggle moderation mode", this->split_, [this]() {
this->split_->setModerationMode(!this->split_->getModerationMode());
});
if (dynamic_cast<TwitchChannel *>(this->split_->getChannel().get()))
{
moreMenu->addAction("Show viewer list", this->split_,
&Split::showViewerList);
@ -317,7 +327,9 @@ std::unique_ptr<QMenu> SplitHeader::createMainMenu()
});
moreMenu->addAction(action);
}
if (dynamic_cast<TwitchChannel *>(this->split_->getChannel().get()))
{
auto action = new QAction(this);
action->setText("Mute highlight sound");
@ -336,11 +348,16 @@ std::unique_ptr<QMenu> SplitHeader::createMainMenu()
}
moreMenu->addSeparator();
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();
@ -729,7 +746,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"