Merge branch 'master' into irc-support
This commit is contained in:
@@ -79,7 +79,38 @@ namespace {
|
||||
|
||||
return {Success, std::move(emotes)};
|
||||
}
|
||||
std::pair<Outcome, EmoteMap> parseChannelEmotes(const QJsonObject &jsonRoot)
|
||||
|
||||
boost::optional<EmotePtr> parseModBadge(const QJsonObject &jsonRoot)
|
||||
{
|
||||
boost::optional<EmotePtr> modBadge;
|
||||
|
||||
auto room = jsonRoot.value("room").toObject();
|
||||
auto modUrls = room.value("mod_urls").toObject();
|
||||
if (!modUrls.isEmpty())
|
||||
{
|
||||
auto modBadge1x = getEmoteLink(modUrls, "1");
|
||||
auto modBadge2x = getEmoteLink(modUrls, "2");
|
||||
auto modBadge3x = getEmoteLink(modUrls, "4");
|
||||
|
||||
auto modBadgeImageSet = ImageSet{
|
||||
Image::fromUrl(modBadge1x, 1),
|
||||
modBadge2x.string.isEmpty() ? Image::getEmpty()
|
||||
: Image::fromUrl(modBadge2x, 0.5),
|
||||
modBadge3x.string.isEmpty() ? Image::getEmpty()
|
||||
: Image::fromUrl(modBadge3x, 0.25),
|
||||
};
|
||||
|
||||
modBadge = std::make_shared<Emote>(Emote{
|
||||
{""},
|
||||
modBadgeImageSet,
|
||||
Tooltip{"Twitch Channel Moderator"},
|
||||
modBadge1x,
|
||||
});
|
||||
}
|
||||
return modBadge;
|
||||
}
|
||||
|
||||
EmoteMap parseChannelEmotes(const QJsonObject &jsonRoot)
|
||||
{
|
||||
auto jsonSets = jsonRoot.value("sets").toObject();
|
||||
auto emotes = EmoteMap();
|
||||
@@ -110,7 +141,7 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
return {Success, std::move(emotes)};
|
||||
return emotes;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -151,19 +182,26 @@ void FfzEmotes::loadEmotes()
|
||||
.execute();
|
||||
}
|
||||
|
||||
void FfzEmotes::loadChannel(const QString &channelId,
|
||||
std::function<void(EmoteMap &&)> callback)
|
||||
void FfzEmotes::loadChannel(
|
||||
const QString &channelId, std::function<void(EmoteMap &&)> emoteCallback,
|
||||
std::function<void(boost::optional<EmotePtr>)> modBadgeCallback)
|
||||
{
|
||||
log("[FFZEmotes] Reload FFZ Channel Emotes for channel {}\n", channelId);
|
||||
|
||||
NetworkRequest("https://api.frankerfacez.com/v1/room/id/" + channelId)
|
||||
|
||||
.timeout(20000)
|
||||
.onSuccess([callback = std::move(callback)](auto result) -> Outcome {
|
||||
auto pair = parseChannelEmotes(result.parseJson());
|
||||
if (pair.first)
|
||||
callback(std::move(pair.second));
|
||||
return pair.first;
|
||||
.onSuccess([emoteCallback = std::move(emoteCallback),
|
||||
modBadgeCallback =
|
||||
std::move(modBadgeCallback)](auto result) -> Outcome {
|
||||
auto json = result.parseJson();
|
||||
auto emoteMap = parseChannelEmotes(json);
|
||||
auto modBadge = parseModBadge(json);
|
||||
|
||||
emoteCallback(std::move(emoteMap));
|
||||
modBadgeCallback(std::move(modBadge));
|
||||
|
||||
return Success;
|
||||
})
|
||||
.onError([channelId](int result) {
|
||||
if (result == 203)
|
||||
|
||||
@@ -22,8 +22,10 @@ public:
|
||||
std::shared_ptr<const EmoteMap> emotes() const;
|
||||
boost::optional<EmotePtr> emote(const EmoteName &name) const;
|
||||
void loadEmotes();
|
||||
static void loadChannel(const QString &channelId,
|
||||
std::function<void(EmoteMap &&)> callback);
|
||||
static void loadChannel(
|
||||
const QString &channelId,
|
||||
std::function<void(EmoteMap &&)> emoteCallback,
|
||||
std::function<void(boost::optional<EmotePtr>)> modBadgeCallback);
|
||||
|
||||
private:
|
||||
Atomic<std::shared_ptr<const EmoteMap>> global_;
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
#include "FfzModBadge.hpp"
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QImageReader>
|
||||
#include <QJsonObject>
|
||||
#include <QPainter>
|
||||
#include <QString>
|
||||
|
||||
#include "common/NetworkRequest.hpp"
|
||||
#include "common/Outcome.hpp"
|
||||
#include "messages/Emote.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
FfzModBadge::FfzModBadge(const QString &channelName)
|
||||
: channelName_(channelName)
|
||||
{
|
||||
}
|
||||
|
||||
void FfzModBadge::loadCustomModBadge()
|
||||
{
|
||||
static QString partialUrl("https://cdn.frankerfacez.com/room-badge/mod/");
|
||||
|
||||
QString url = partialUrl + channelName_ + "/1";
|
||||
NetworkRequest(url)
|
||||
|
||||
.onSuccess([this, url](auto result) -> Outcome {
|
||||
auto data = result.getData();
|
||||
|
||||
QBuffer buffer(const_cast<QByteArray *>(&data));
|
||||
buffer.open(QIODevice::ReadOnly);
|
||||
QImageReader reader(&buffer);
|
||||
if (reader.imageCount() == 0)
|
||||
return Failure;
|
||||
|
||||
QPixmap badgeOverlay = QPixmap::fromImageReader(&reader);
|
||||
QPixmap badgePixmap(18, 18);
|
||||
|
||||
// the default mod badge green color
|
||||
badgePixmap.fill(QColor("#34AE0A"));
|
||||
QPainter painter(&badgePixmap);
|
||||
QRectF rect(0, 0, 18, 18);
|
||||
painter.drawPixmap(rect, badgeOverlay, rect);
|
||||
|
||||
auto emote = Emote{{""},
|
||||
ImageSet{Image::fromPixmap(badgePixmap)},
|
||||
Tooltip{"Twitch Channel Moderator"},
|
||||
Url{url}};
|
||||
|
||||
this->badge_ = std::make_shared<Emote>(emote);
|
||||
// getBadge.execute();
|
||||
|
||||
return Success;
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
EmotePtr FfzModBadge::badge() const
|
||||
{
|
||||
return this->badge_;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -1,25 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <boost/optional.hpp>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
struct Emote;
|
||||
using EmotePtr = std::shared_ptr<const Emote>;
|
||||
|
||||
class FfzModBadge
|
||||
{
|
||||
public:
|
||||
FfzModBadge(const QString &channelName);
|
||||
|
||||
void loadCustomModBadge();
|
||||
|
||||
EmotePtr badge() const;
|
||||
|
||||
private:
|
||||
const QString channelName_;
|
||||
EmotePtr badge_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -1,380 +1,380 @@
|
||||
#include "AbstractIrcServer.hpp"
|
||||
|
||||
#include "common/Channel.hpp"
|
||||
#include "common/Common.hpp"
|
||||
#include "debug/Log.hpp"
|
||||
#include "messages/LimitedQueueSnapshot.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
|
||||
#include <QCoreApplication>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
const int RECONNECT_BASE_INTERVAL = 2000;
|
||||
// 60 falloff counter means it will try to reconnect at most every 60*2 seconds
|
||||
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, [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);
|
||||
this->readConnection_->moveToThread(QCoreApplication::instance()->thread());
|
||||
|
||||
QObject::connect(
|
||||
this->readConnection_.get(), &Communi::IrcConnection::messageReceived,
|
||||
this, [this](auto msg) { this->readConnectionMessageReceived(msg); });
|
||||
QObject::connect(this->readConnection_.get(),
|
||||
&Communi::IrcConnection::privateMessageReceived, this,
|
||||
[this](auto msg) { this->privateMessageReceived(msg); });
|
||||
QObject::connect(
|
||||
this->readConnection_.get(), &Communi::IrcConnection::connected, this,
|
||||
[this] { this->onReadConnected(this->readConnection_.get()); });
|
||||
QObject::connect(this->readConnection_.get(),
|
||||
&Communi::IrcConnection::disconnected, this,
|
||||
[this] { this->onDisconnected(); });
|
||||
QObject::connect(this->readConnection_.get(),
|
||||
&Communi::IrcConnection::socketError, this,
|
||||
[this] { this->onSocketError(); });
|
||||
|
||||
// listen to reconnect request
|
||||
this->readConnection_->reconnectRequested.connect(
|
||||
[this] { this->connect(); });
|
||||
// this->writeConnection->reconnectRequested.connect([this] {
|
||||
// this->connect(); });
|
||||
this->reconnectTimer_.setInterval(RECONNECT_BASE_INTERVAL);
|
||||
this->reconnectTimer_.setSingleShot(true);
|
||||
QObject::connect(&this->reconnectTimer_, &QTimer::timeout, [this] {
|
||||
this->reconnectTimer_.setInterval(RECONNECT_BASE_INTERVAL *
|
||||
this->falloffCounter_);
|
||||
|
||||
this->falloffCounter_ =
|
||||
std::min(MAX_FALLOFF_COUNTER, this->falloffCounter_ + 1);
|
||||
|
||||
if (!this->readConnection_->isConnected())
|
||||
{
|
||||
log("Trying to reconnect... {}", this->falloffCounter_);
|
||||
this->connect();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void AbstractIrcServer::connect()
|
||||
{
|
||||
this->disconnect();
|
||||
|
||||
if (this->hasSeparateWriteConnection())
|
||||
{
|
||||
this->initializeConnection(this->writeConnection_.get(), Write);
|
||||
this->initializeConnection(this->readConnection_.get(), Read);
|
||||
}
|
||||
else
|
||||
{
|
||||
this->initializeConnection(this->readConnection_.get(), Both);
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractIrcServer::open(ConnectionType type)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->connectionMutex_);
|
||||
|
||||
if (type == Write)
|
||||
{
|
||||
this->writeConnection_->open();
|
||||
}
|
||||
if (type & Read)
|
||||
{
|
||||
this->readConnection_->open();
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
const QString &message)
|
||||
{
|
||||
this->sendRawMessage("PRIVMSG #" + channelName + " :" + message);
|
||||
}
|
||||
|
||||
void AbstractIrcServer::sendRawMessage(const QString &rawMessage)
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(this->connectionMutex_);
|
||||
|
||||
if (this->hasSeparateWriteConnection())
|
||||
{
|
||||
this->writeConnection_->sendRaw(rawMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
this->readConnection_->sendRaw(rawMessage);
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractIrcServer::writeConnectionMessageReceived(
|
||||
Communi::IrcMessage *message)
|
||||
{
|
||||
(void)message;
|
||||
}
|
||||
|
||||
ChannelPtr AbstractIrcServer::getOrAddChannel(const QString &dirtyChannelName)
|
||||
{
|
||||
auto channelName = this->cleanChannelName(dirtyChannelName);
|
||||
|
||||
// try get channel
|
||||
ChannelPtr chan = this->getChannelOrEmpty(channelName);
|
||||
if (chan != Channel::getEmpty())
|
||||
{
|
||||
return chan;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(this->channelMutex);
|
||||
|
||||
// value doesn't exist
|
||||
chan = this->createChannel(channelName);
|
||||
if (!chan)
|
||||
{
|
||||
return Channel::getEmpty();
|
||||
}
|
||||
|
||||
this->channels.insert(channelName, chan);
|
||||
this->connections_.emplace_back(chan->destroyed.connect([this,
|
||||
channelName] {
|
||||
// fourtf: issues when the server itself is destroyed
|
||||
|
||||
log("[AbstractIrcServer::addChannel] {} was destroyed", channelName);
|
||||
this->channels.remove(channelName);
|
||||
|
||||
if (this->readConnection_)
|
||||
{
|
||||
this->readConnection_->sendRaw("PART #" + channelName);
|
||||
}
|
||||
|
||||
if (this->writeConnection_ && this->hasSeparateWriteConnection())
|
||||
{
|
||||
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_ && this->hasSeparateWriteConnection())
|
||||
{
|
||||
if (this->readConnection_->isConnected())
|
||||
{
|
||||
this->writeConnection_->sendRaw("JOIN #" + channelName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chan;
|
||||
}
|
||||
|
||||
ChannelPtr AbstractIrcServer::getChannelOrEmpty(const QString &dirtyChannelName)
|
||||
{
|
||||
auto channelName = this->cleanChannelName(dirtyChannelName);
|
||||
|
||||
std::lock_guard<std::mutex> lock(this->channelMutex);
|
||||
|
||||
// try get special channel
|
||||
ChannelPtr chan = this->getCustomChannel(channelName);
|
||||
if (chan)
|
||||
{
|
||||
return chan;
|
||||
}
|
||||
|
||||
// value exists
|
||||
auto it = this->channels.find(channelName);
|
||||
if (it != this->channels.end())
|
||||
{
|
||||
chan = it.value().lock();
|
||||
|
||||
if (chan)
|
||||
{
|
||||
return chan;
|
||||
}
|
||||
}
|
||||
|
||||
return Channel::getEmpty();
|
||||
}
|
||||
|
||||
std::vector<std::weak_ptr<Channel>> AbstractIrcServer::getChannels()
|
||||
{
|
||||
std::lock_guard lock(this->channelMutex);
|
||||
std::vector<std::weak_ptr<Channel>> channels;
|
||||
|
||||
for (auto &&weak : this->channels.values())
|
||||
{
|
||||
channels.push_back(weak);
|
||||
}
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
void AbstractIrcServer::onReadConnected(IrcConnection *connection)
|
||||
{
|
||||
(void)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");
|
||||
reconnected->flags.set(MessageFlag::ConnectedMessage);
|
||||
|
||||
for (std::weak_ptr<Channel> &weak : this->channels.values())
|
||||
{
|
||||
std::shared_ptr<Channel> chan = weak.lock();
|
||||
if (!chan)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
LimitedQueueSnapshot<MessagePtr> snapshot = chan->getMessageSnapshot();
|
||||
|
||||
bool replaceMessage =
|
||||
snapshot.size() > 0 && snapshot[snapshot.size() - 1]->flags.has(
|
||||
MessageFlag::DisconnectedMessage);
|
||||
|
||||
if (replaceMessage)
|
||||
{
|
||||
chan->replaceMessage(snapshot[snapshot.size() - 1], reconnected);
|
||||
continue;
|
||||
}
|
||||
|
||||
chan->addMessage(connectedMsg);
|
||||
}
|
||||
|
||||
this->falloffCounter_ = 1;
|
||||
}
|
||||
|
||||
void AbstractIrcServer::onWriteConnected(IrcConnection *connection)
|
||||
{
|
||||
(void)connection;
|
||||
}
|
||||
|
||||
void AbstractIrcServer::onDisconnected()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->channelMutex);
|
||||
|
||||
MessageBuilder b(systemMessage, "disconnected");
|
||||
b->flags.set(MessageFlag::DisconnectedMessage);
|
||||
auto disconnectedMsg = b.release();
|
||||
|
||||
for (std::weak_ptr<Channel> &weak : this->channels.values())
|
||||
{
|
||||
std::shared_ptr<Channel> chan = weak.lock();
|
||||
if (!chan)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
chan->addMessage(disconnectedMsg);
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractIrcServer::onSocketError()
|
||||
{
|
||||
this->reconnectTimer_.start();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void AbstractIrcServer::addFakeMessage(const QString &data)
|
||||
{
|
||||
auto fakeMessage = Communi::IrcMessage::fromData(
|
||||
data.toUtf8(), this->readConnection_.get());
|
||||
|
||||
if (fakeMessage->command() == "PRIVMSG")
|
||||
{
|
||||
this->privateMessageReceived(
|
||||
static_cast<Communi::IrcPrivateMessage *>(fakeMessage));
|
||||
}
|
||||
else
|
||||
{
|
||||
this->readConnectionMessageReceived(fakeMessage);
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractIrcServer::privateMessageReceived(
|
||||
Communi::IrcPrivateMessage *message)
|
||||
{
|
||||
(void)message;
|
||||
}
|
||||
|
||||
void AbstractIrcServer::readConnectionMessageReceived(
|
||||
Communi::IrcMessage *message)
|
||||
{
|
||||
}
|
||||
|
||||
void AbstractIrcServer::forEachChannel(std::function<void(ChannelPtr)> func)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->channelMutex);
|
||||
|
||||
for (std::weak_ptr<Channel> &weak : this->channels.values())
|
||||
{
|
||||
ChannelPtr chan = weak.lock();
|
||||
if (!chan)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
func(chan);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
#include "AbstractIrcServer.hpp"
|
||||
|
||||
#include "common/Channel.hpp"
|
||||
#include "common/Common.hpp"
|
||||
#include "debug/Log.hpp"
|
||||
#include "messages/LimitedQueueSnapshot.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
|
||||
#include <QCoreApplication>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
const int RECONNECT_BASE_INTERVAL = 2000;
|
||||
// 60 falloff counter means it will try to reconnect at most every 60*2 seconds
|
||||
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, [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);
|
||||
this->readConnection_->moveToThread(QCoreApplication::instance()->thread());
|
||||
|
||||
QObject::connect(
|
||||
this->readConnection_.get(), &Communi::IrcConnection::messageReceived,
|
||||
this, [this](auto msg) { this->readConnectionMessageReceived(msg); });
|
||||
QObject::connect(this->readConnection_.get(),
|
||||
&Communi::IrcConnection::privateMessageReceived, this,
|
||||
[this](auto msg) { this->privateMessageReceived(msg); });
|
||||
QObject::connect(
|
||||
this->readConnection_.get(), &Communi::IrcConnection::connected, this,
|
||||
[this] { this->onReadConnected(this->readConnection_.get()); });
|
||||
QObject::connect(this->readConnection_.get(),
|
||||
&Communi::IrcConnection::disconnected, this,
|
||||
[this] { this->onDisconnected(); });
|
||||
QObject::connect(this->readConnection_.get(),
|
||||
&Communi::IrcConnection::socketError, this,
|
||||
[this] { this->onSocketError(); });
|
||||
|
||||
// listen to reconnect request
|
||||
this->readConnection_->reconnectRequested.connect(
|
||||
[this] { this->connect(); });
|
||||
// this->writeConnection->reconnectRequested.connect([this] {
|
||||
// this->connect(); });
|
||||
this->reconnectTimer_.setInterval(RECONNECT_BASE_INTERVAL);
|
||||
this->reconnectTimer_.setSingleShot(true);
|
||||
QObject::connect(&this->reconnectTimer_, &QTimer::timeout, [this] {
|
||||
this->reconnectTimer_.setInterval(RECONNECT_BASE_INTERVAL *
|
||||
this->falloffCounter_);
|
||||
|
||||
this->falloffCounter_ =
|
||||
std::min(MAX_FALLOFF_COUNTER, this->falloffCounter_ + 1);
|
||||
|
||||
if (!this->readConnection_->isConnected())
|
||||
{
|
||||
log("Trying to reconnect... {}", this->falloffCounter_);
|
||||
this->connect();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void AbstractIrcServer::connect()
|
||||
{
|
||||
this->disconnect();
|
||||
|
||||
if (this->hasSeparateWriteConnection())
|
||||
{
|
||||
this->initializeConnection(this->writeConnection_.get(), Write);
|
||||
this->initializeConnection(this->readConnection_.get(), Read);
|
||||
}
|
||||
else
|
||||
{
|
||||
this->initializeConnection(this->readConnection_.get(), Both);
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractIrcServer::open(ConnectionType type)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->connectionMutex_);
|
||||
|
||||
if (type == Write)
|
||||
{
|
||||
this->writeConnection_->open();
|
||||
}
|
||||
if (type & Read)
|
||||
{
|
||||
this->readConnection_->open();
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
const QString &message)
|
||||
{
|
||||
this->sendRawMessage("PRIVMSG #" + channelName + " :" + message);
|
||||
}
|
||||
|
||||
void AbstractIrcServer::sendRawMessage(const QString &rawMessage)
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(this->connectionMutex_);
|
||||
|
||||
if (this->hasSeparateWriteConnection())
|
||||
{
|
||||
this->writeConnection_->sendRaw(rawMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
this->readConnection_->sendRaw(rawMessage);
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractIrcServer::writeConnectionMessageReceived(
|
||||
Communi::IrcMessage *message)
|
||||
{
|
||||
(void)message;
|
||||
}
|
||||
|
||||
ChannelPtr AbstractIrcServer::getOrAddChannel(const QString &dirtyChannelName)
|
||||
{
|
||||
auto channelName = this->cleanChannelName(dirtyChannelName);
|
||||
|
||||
// try get channel
|
||||
ChannelPtr chan = this->getChannelOrEmpty(channelName);
|
||||
if (chan != Channel::getEmpty())
|
||||
{
|
||||
return chan;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(this->channelMutex);
|
||||
|
||||
// value doesn't exist
|
||||
chan = this->createChannel(channelName);
|
||||
if (!chan)
|
||||
{
|
||||
return Channel::getEmpty();
|
||||
}
|
||||
|
||||
this->channels.insert(channelName, chan);
|
||||
this->connections_.emplace_back(chan->destroyed.connect([this,
|
||||
channelName] {
|
||||
// fourtf: issues when the server itself is destroyed
|
||||
|
||||
log("[AbstractIrcServer::addChannel] {} was destroyed", channelName);
|
||||
this->channels.remove(channelName);
|
||||
|
||||
if (this->readConnection_)
|
||||
{
|
||||
this->readConnection_->sendRaw("PART #" + channelName);
|
||||
}
|
||||
|
||||
if (this->writeConnection_ && this->hasSeparateWriteConnection())
|
||||
{
|
||||
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_ && this->hasSeparateWriteConnection())
|
||||
{
|
||||
if (this->readConnection_->isConnected())
|
||||
{
|
||||
this->writeConnection_->sendRaw("JOIN #" + channelName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chan;
|
||||
}
|
||||
|
||||
ChannelPtr AbstractIrcServer::getChannelOrEmpty(const QString &dirtyChannelName)
|
||||
{
|
||||
auto channelName = this->cleanChannelName(dirtyChannelName);
|
||||
|
||||
std::lock_guard<std::mutex> lock(this->channelMutex);
|
||||
|
||||
// try get special channel
|
||||
ChannelPtr chan = this->getCustomChannel(channelName);
|
||||
if (chan)
|
||||
{
|
||||
return chan;
|
||||
}
|
||||
|
||||
// value exists
|
||||
auto it = this->channels.find(channelName);
|
||||
if (it != this->channels.end())
|
||||
{
|
||||
chan = it.value().lock();
|
||||
|
||||
if (chan)
|
||||
{
|
||||
return chan;
|
||||
}
|
||||
}
|
||||
|
||||
return Channel::getEmpty();
|
||||
}
|
||||
|
||||
std::vector<std::weak_ptr<Channel>> AbstractIrcServer::getChannels()
|
||||
{
|
||||
std::lock_guard lock(this->channelMutex);
|
||||
std::vector<std::weak_ptr<Channel>> channels;
|
||||
|
||||
for (auto &&weak : this->channels.values())
|
||||
{
|
||||
channels.push_back(weak);
|
||||
}
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
void AbstractIrcServer::onReadConnected(IrcConnection *connection)
|
||||
{
|
||||
(void)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");
|
||||
reconnected->flags.set(MessageFlag::ConnectedMessage);
|
||||
|
||||
for (std::weak_ptr<Channel> &weak : this->channels.values())
|
||||
{
|
||||
std::shared_ptr<Channel> chan = weak.lock();
|
||||
if (!chan)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
LimitedQueueSnapshot<MessagePtr> snapshot = chan->getMessageSnapshot();
|
||||
|
||||
bool replaceMessage =
|
||||
snapshot.size() > 0 && snapshot[snapshot.size() - 1]->flags.has(
|
||||
MessageFlag::DisconnectedMessage);
|
||||
|
||||
if (replaceMessage)
|
||||
{
|
||||
chan->replaceMessage(snapshot[snapshot.size() - 1], reconnected);
|
||||
continue;
|
||||
}
|
||||
|
||||
chan->addMessage(connectedMsg);
|
||||
}
|
||||
|
||||
this->falloffCounter_ = 1;
|
||||
}
|
||||
|
||||
void AbstractIrcServer::onWriteConnected(IrcConnection *connection)
|
||||
{
|
||||
(void)connection;
|
||||
}
|
||||
|
||||
void AbstractIrcServer::onDisconnected()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->channelMutex);
|
||||
|
||||
MessageBuilder b(systemMessage, "disconnected");
|
||||
b->flags.set(MessageFlag::DisconnectedMessage);
|
||||
auto disconnectedMsg = b.release();
|
||||
|
||||
for (std::weak_ptr<Channel> &weak : this->channels.values())
|
||||
{
|
||||
std::shared_ptr<Channel> chan = weak.lock();
|
||||
if (!chan)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
chan->addMessage(disconnectedMsg);
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractIrcServer::onSocketError()
|
||||
{
|
||||
this->reconnectTimer_.start();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void AbstractIrcServer::addFakeMessage(const QString &data)
|
||||
{
|
||||
auto fakeMessage = Communi::IrcMessage::fromData(
|
||||
data.toUtf8(), this->readConnection_.get());
|
||||
|
||||
if (fakeMessage->command() == "PRIVMSG")
|
||||
{
|
||||
this->privateMessageReceived(
|
||||
static_cast<Communi::IrcPrivateMessage *>(fakeMessage));
|
||||
}
|
||||
else
|
||||
{
|
||||
this->readConnectionMessageReceived(fakeMessage);
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractIrcServer::privateMessageReceived(
|
||||
Communi::IrcPrivateMessage *message)
|
||||
{
|
||||
(void)message;
|
||||
}
|
||||
|
||||
void AbstractIrcServer::readConnectionMessageReceived(
|
||||
Communi::IrcMessage *message)
|
||||
{
|
||||
}
|
||||
|
||||
void AbstractIrcServer::forEachChannel(std::function<void(ChannelPtr)> func)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->channelMutex);
|
||||
|
||||
for (std::weak_ptr<Channel> &weak : this->channels.values())
|
||||
{
|
||||
ChannelPtr chan = weak.lock();
|
||||
if (!chan)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
func(chan);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -1,95 +1,95 @@
|
||||
#pragma once
|
||||
|
||||
#include "providers/irc/IrcConnection2.hpp"
|
||||
|
||||
#include <IrcMessage>
|
||||
#include <pajlada/signals/signal.hpp>
|
||||
#include <pajlada/signals/signalholder.hpp>
|
||||
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class Channel;
|
||||
using ChannelPtr = std::shared_ptr<Channel>;
|
||||
|
||||
class AbstractIrcServer : public QObject
|
||||
{
|
||||
public:
|
||||
enum ConnectionType { Read = 1, Write = 2, Both = 3 };
|
||||
|
||||
virtual ~AbstractIrcServer() = default;
|
||||
|
||||
// connection
|
||||
void connect();
|
||||
void disconnect();
|
||||
|
||||
void sendMessage(const QString &channelName, const QString &message);
|
||||
void sendRawMessage(const QString &rawMessage);
|
||||
|
||||
// channels
|
||||
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;
|
||||
|
||||
void addFakeMessage(const QString &data);
|
||||
|
||||
// iteration
|
||||
void forEachChannel(std::function<void(ChannelPtr)> func);
|
||||
|
||||
protected:
|
||||
AbstractIrcServer();
|
||||
|
||||
virtual void initializeConnection(IrcConnection *connection,
|
||||
ConnectionType type) = 0;
|
||||
virtual std::shared_ptr<Channel> createChannel(
|
||||
const QString &channelName) = 0;
|
||||
|
||||
virtual void privateMessageReceived(Communi::IrcPrivateMessage *message);
|
||||
virtual void readConnectionMessageReceived(Communi::IrcMessage *message);
|
||||
virtual void writeConnectionMessageReceived(Communi::IrcMessage *message);
|
||||
|
||||
virtual void onReadConnected(IrcConnection *connection);
|
||||
virtual void onWriteConnected(IrcConnection *connection);
|
||||
virtual void onDisconnected();
|
||||
virtual void onSocketError();
|
||||
|
||||
virtual std::shared_ptr<Channel> getCustomChannel(
|
||||
const QString &channelName);
|
||||
|
||||
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();
|
||||
|
||||
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;
|
||||
|
||||
std::mutex connectionMutex_;
|
||||
|
||||
// bool autoReconnect_ = false;
|
||||
pajlada::Signals::SignalHolder connections_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
#pragma once
|
||||
|
||||
#include "providers/irc/IrcConnection2.hpp"
|
||||
|
||||
#include <IrcMessage>
|
||||
#include <pajlada/signals/signal.hpp>
|
||||
#include <pajlada/signals/signalholder.hpp>
|
||||
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class Channel;
|
||||
using ChannelPtr = std::shared_ptr<Channel>;
|
||||
|
||||
class AbstractIrcServer : public QObject
|
||||
{
|
||||
public:
|
||||
enum ConnectionType { Read = 1, Write = 2, Both = 3 };
|
||||
|
||||
virtual ~AbstractIrcServer() = default;
|
||||
|
||||
// connection
|
||||
void connect();
|
||||
void disconnect();
|
||||
|
||||
void sendMessage(const QString &channelName, const QString &message);
|
||||
void sendRawMessage(const QString &rawMessage);
|
||||
|
||||
// channels
|
||||
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;
|
||||
|
||||
void addFakeMessage(const QString &data);
|
||||
|
||||
// iteration
|
||||
void forEachChannel(std::function<void(ChannelPtr)> func);
|
||||
|
||||
protected:
|
||||
AbstractIrcServer();
|
||||
|
||||
virtual void initializeConnection(IrcConnection *connection,
|
||||
ConnectionType type) = 0;
|
||||
virtual std::shared_ptr<Channel> createChannel(
|
||||
const QString &channelName) = 0;
|
||||
|
||||
virtual void privateMessageReceived(Communi::IrcPrivateMessage *message);
|
||||
virtual void readConnectionMessageReceived(Communi::IrcMessage *message);
|
||||
virtual void writeConnectionMessageReceived(Communi::IrcMessage *message);
|
||||
|
||||
virtual void onReadConnected(IrcConnection *connection);
|
||||
virtual void onWriteConnected(IrcConnection *connection);
|
||||
virtual void onDisconnected();
|
||||
virtual void onSocketError();
|
||||
|
||||
virtual std::shared_ptr<Channel> getCustomChannel(
|
||||
const QString &channelName);
|
||||
|
||||
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();
|
||||
|
||||
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;
|
||||
|
||||
std::mutex connectionMutex_;
|
||||
|
||||
// bool autoReconnect_ = false;
|
||||
pajlada::Signals::SignalHolder connections_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -1,68 +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(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
|
||||
#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(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
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/Channel.hpp"
|
||||
#include "common/ChannelChatters.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
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
|
||||
#pragma once
|
||||
|
||||
#include "common/Channel.hpp"
|
||||
#include "common/ChannelChatters.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
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
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
#include "IrcConnection2.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
IrcConnection::IrcConnection(QObject *parent)
|
||||
: Communi::IrcConnection(parent)
|
||||
{
|
||||
// send ping every x seconds
|
||||
this->pingTimer_.setInterval(5000);
|
||||
this->pingTimer_.start();
|
||||
QObject::connect(&this->pingTimer_, &QTimer::timeout, [this] {
|
||||
if (this->isConnected())
|
||||
{
|
||||
if (!this->recentlyReceivedMessage_.load())
|
||||
{
|
||||
this->sendRaw("PING chatterino/ping");
|
||||
this->reconnectTimer_.start();
|
||||
}
|
||||
this->recentlyReceivedMessage_ = false;
|
||||
}
|
||||
});
|
||||
|
||||
// reconnect after x seconds without receiving a message
|
||||
this->reconnectTimer_.setInterval(5000);
|
||||
this->reconnectTimer_.setSingleShot(true);
|
||||
QObject::connect(&this->reconnectTimer_, &QTimer::timeout, [this] {
|
||||
if (this->isConnected())
|
||||
{
|
||||
reconnectRequested.invoke();
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(this, &Communi::IrcConnection::messageReceived,
|
||||
[this](Communi::IrcMessage *) {
|
||||
this->recentlyReceivedMessage_ = true;
|
||||
|
||||
if (this->reconnectTimer_.isActive())
|
||||
{
|
||||
this->reconnectTimer_.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
#include "IrcConnection2.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
IrcConnection::IrcConnection(QObject *parent)
|
||||
: Communi::IrcConnection(parent)
|
||||
{
|
||||
// send ping every x seconds
|
||||
this->pingTimer_.setInterval(5000);
|
||||
this->pingTimer_.start();
|
||||
QObject::connect(&this->pingTimer_, &QTimer::timeout, [this] {
|
||||
if (this->isConnected())
|
||||
{
|
||||
if (!this->recentlyReceivedMessage_.load())
|
||||
{
|
||||
this->sendRaw("PING chatterino/ping");
|
||||
this->reconnectTimer_.start();
|
||||
}
|
||||
this->recentlyReceivedMessage_ = false;
|
||||
}
|
||||
});
|
||||
|
||||
// reconnect after x seconds without receiving a message
|
||||
this->reconnectTimer_.setInterval(5000);
|
||||
this->reconnectTimer_.setSingleShot(true);
|
||||
QObject::connect(&this->reconnectTimer_, &QTimer::timeout, [this] {
|
||||
if (this->isConnected())
|
||||
{
|
||||
reconnectRequested.invoke();
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(this, &Communi::IrcConnection::messageReceived,
|
||||
[this](Communi::IrcMessage *) {
|
||||
this->recentlyReceivedMessage_ = true;
|
||||
|
||||
if (this->reconnectTimer_.isActive())
|
||||
{
|
||||
this->reconnectTimer_.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <pajlada/signals/signal.hpp>
|
||||
|
||||
#include <IrcConnection>
|
||||
#include <QTimer>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class IrcConnection : public Communi::IrcConnection
|
||||
{
|
||||
public:
|
||||
IrcConnection(QObject *parent = nullptr);
|
||||
|
||||
pajlada::Signals::NoArgSignal reconnectRequested;
|
||||
|
||||
private:
|
||||
QTimer pingTimer_;
|
||||
QTimer reconnectTimer_;
|
||||
std::atomic<bool> recentlyReceivedMessage_{true};
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
#pragma once
|
||||
|
||||
#include <pajlada/signals/signal.hpp>
|
||||
|
||||
#include <IrcConnection>
|
||||
#include <QTimer>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class IrcConnection : public Communi::IrcConnection
|
||||
{
|
||||
public:
|
||||
IrcConnection(QObject *parent = nullptr);
|
||||
|
||||
pajlada::Signals::NoArgSignal reconnectRequested;
|
||||
|
||||
private:
|
||||
QTimer pingTimer_;
|
||||
QTimer reconnectTimer_;
|
||||
std::atomic<bool> recentlyReceivedMessage_{true};
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
+259
-259
@@ -1,259 +1,259 @@
|
||||
#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 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:
|
||||
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
|
||||
#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 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:
|
||||
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
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include "providers/irc/AbstractIrcServer.hpp"
|
||||
#include "providers/irc/IrcAccount.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
struct IrcServerData;
|
||||
|
||||
class IrcServer : public AbstractIrcServer
|
||||
{
|
||||
public:
|
||||
explicit IrcServer(const IrcServerData &data);
|
||||
IrcServer(const IrcServerData &data,
|
||||
const std::vector<std::weak_ptr<Channel>> &restoreChannels);
|
||||
~IrcServer() override;
|
||||
|
||||
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_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
#pragma once
|
||||
|
||||
#include "providers/irc/AbstractIrcServer.hpp"
|
||||
#include "providers/irc/IrcAccount.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
struct IrcServerData;
|
||||
|
||||
class IrcServer : public AbstractIrcServer
|
||||
{
|
||||
public:
|
||||
explicit IrcServer(const IrcServerData &data);
|
||||
IrcServer(const IrcServerData &data,
|
||||
const std::vector<std::weak_ptr<Channel>> &restoreChannels);
|
||||
~IrcServer() override;
|
||||
|
||||
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_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -48,9 +48,15 @@ void ChatroomChannel::refreshFFZChannelEmotes()
|
||||
{
|
||||
return;
|
||||
}
|
||||
FfzEmotes::loadChannel(this->chatroomOwnerId, [this](auto &&emoteMap) {
|
||||
this->ffzEmotes_.set(std::make_shared<EmoteMap>(std::move(emoteMap)));
|
||||
});
|
||||
FfzEmotes::loadChannel(
|
||||
this->chatroomOwnerId,
|
||||
[this](auto &&emoteMap) {
|
||||
this->ffzEmotes_.set(
|
||||
std::make_shared<EmoteMap>(std::move(emoteMap)));
|
||||
},
|
||||
[this](auto &&modBadge) {
|
||||
this->ffzCustomModBadge_.set(std::move(modBadge));
|
||||
});
|
||||
}
|
||||
|
||||
const QString &ChatroomChannel::getDisplayName() const
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,58 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include <IrcMessage>
|
||||
#include "messages/Message.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class TwitchIrcServer;
|
||||
class Channel;
|
||||
|
||||
class IrcMessageHandler
|
||||
{
|
||||
IrcMessageHandler() = default;
|
||||
|
||||
public:
|
||||
static IrcMessageHandler &getInstance();
|
||||
|
||||
// parseMessage parses a single IRC message into 0+ Chatterino messages
|
||||
std::vector<MessagePtr> parseMessage(Channel *channel,
|
||||
Communi::IrcMessage *message);
|
||||
|
||||
// parsePrivMessage arses a single IRC PRIVMSG into 0-1 Chatterino messages
|
||||
std::vector<MessagePtr> parsePrivMessage(
|
||||
Channel *channel, Communi::IrcPrivateMessage *message);
|
||||
void handlePrivMessage(Communi::IrcPrivateMessage *message,
|
||||
TwitchIrcServer &server);
|
||||
|
||||
void handleRoomStateMessage(Communi::IrcMessage *message);
|
||||
void handleClearChatMessage(Communi::IrcMessage *message);
|
||||
void handleClearMessageMessage(Communi::IrcMessage *message);
|
||||
void handleUserStateMessage(Communi::IrcMessage *message);
|
||||
void handleWhisperMessage(Communi::IrcMessage *message);
|
||||
|
||||
// parseUserNoticeMessage parses a single IRC USERNOTICE message into 0+
|
||||
// chatterino messages
|
||||
std::vector<MessagePtr> parseUserNoticeMessage(
|
||||
Channel *channel, Communi::IrcMessage *message);
|
||||
void handleUserNoticeMessage(Communi::IrcMessage *message,
|
||||
TwitchIrcServer &server);
|
||||
|
||||
void handleModeMessage(Communi::IrcMessage *message);
|
||||
|
||||
// parseNoticeMessage parses a single IRC NOTICE message into 0+ chatterino
|
||||
// messages
|
||||
std::vector<MessagePtr> parseNoticeMessage(
|
||||
Communi::IrcNoticeMessage *message);
|
||||
void handleNoticeMessage(Communi::IrcNoticeMessage *message);
|
||||
|
||||
void handleJoinMessage(Communi::IrcMessage *message);
|
||||
void handlePartMessage(Communi::IrcMessage *message);
|
||||
|
||||
private:
|
||||
void addMessage(Communi::IrcMessage *message, const QString &target,
|
||||
const QString &content, TwitchIrcServer &server, bool isResub,
|
||||
bool isAction);
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
#pragma once
|
||||
|
||||
#include <IrcMessage>
|
||||
#include "messages/Message.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class TwitchIrcServer;
|
||||
class Channel;
|
||||
|
||||
class IrcMessageHandler
|
||||
{
|
||||
IrcMessageHandler() = default;
|
||||
|
||||
public:
|
||||
static IrcMessageHandler &getInstance();
|
||||
|
||||
// parseMessage parses a single IRC message into 0+ Chatterino messages
|
||||
std::vector<MessagePtr> parseMessage(Channel *channel,
|
||||
Communi::IrcMessage *message);
|
||||
|
||||
// parsePrivMessage arses a single IRC PRIVMSG into 0-1 Chatterino messages
|
||||
std::vector<MessagePtr> parsePrivMessage(
|
||||
Channel *channel, Communi::IrcPrivateMessage *message);
|
||||
void handlePrivMessage(Communi::IrcPrivateMessage *message,
|
||||
TwitchIrcServer &server);
|
||||
|
||||
void handleRoomStateMessage(Communi::IrcMessage *message);
|
||||
void handleClearChatMessage(Communi::IrcMessage *message);
|
||||
void handleClearMessageMessage(Communi::IrcMessage *message);
|
||||
void handleUserStateMessage(Communi::IrcMessage *message);
|
||||
void handleWhisperMessage(Communi::IrcMessage *message);
|
||||
|
||||
// parseUserNoticeMessage parses a single IRC USERNOTICE message into 0+
|
||||
// chatterino messages
|
||||
std::vector<MessagePtr> parseUserNoticeMessage(
|
||||
Channel *channel, Communi::IrcMessage *message);
|
||||
void handleUserNoticeMessage(Communi::IrcMessage *message,
|
||||
TwitchIrcServer &server);
|
||||
|
||||
void handleModeMessage(Communi::IrcMessage *message);
|
||||
|
||||
// parseNoticeMessage parses a single IRC NOTICE message into 0+ chatterino
|
||||
// messages
|
||||
std::vector<MessagePtr> parseNoticeMessage(
|
||||
Communi::IrcNoticeMessage *message);
|
||||
void handleNoticeMessage(Communi::IrcNoticeMessage *message);
|
||||
|
||||
void handleJoinMessage(Communi::IrcMessage *message);
|
||||
void handlePartMessage(Communi::IrcMessage *message);
|
||||
|
||||
private:
|
||||
void addMessage(Communi::IrcMessage *message, const QString &target,
|
||||
const QString &content, TwitchIrcServer &server,
|
||||
bool isResub, bool isAction);
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -95,7 +95,7 @@ void TwitchAccount::loadIgnores()
|
||||
"/blocks");
|
||||
|
||||
NetworkRequest(url)
|
||||
|
||||
|
||||
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
|
||||
.onSuccess([=](auto result) -> Outcome {
|
||||
auto document = result.parseRapidJson();
|
||||
@@ -168,7 +168,7 @@ void TwitchAccount::ignoreByID(
|
||||
"/blocks/" + targetUserID);
|
||||
|
||||
NetworkRequest(url, NetworkRequestType::Put)
|
||||
|
||||
|
||||
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
|
||||
.onError([=](int errorCode) {
|
||||
onFinished(IgnoreResult_Failed,
|
||||
@@ -245,7 +245,7 @@ void TwitchAccount::unignoreByID(
|
||||
"/blocks/" + targetUserID);
|
||||
|
||||
NetworkRequest(url, NetworkRequestType::Delete)
|
||||
|
||||
|
||||
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
|
||||
.onError([=](int errorCode) {
|
||||
onFinished(
|
||||
@@ -279,7 +279,7 @@ void TwitchAccount::checkFollow(const QString targetUserID,
|
||||
"/follows/channels/" + targetUserID);
|
||||
|
||||
NetworkRequest(url)
|
||||
|
||||
|
||||
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
|
||||
.onError([=](int errorCode) {
|
||||
if (errorCode == 203)
|
||||
@@ -308,7 +308,7 @@ void TwitchAccount::followUser(const QString userID,
|
||||
"/follows/channels/" + userID);
|
||||
|
||||
NetworkRequest(requestUrl, NetworkRequestType::Put)
|
||||
|
||||
|
||||
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
|
||||
.onSuccess([successCallback](auto result) -> Outcome {
|
||||
// TODO: Properly check result of follow request
|
||||
@@ -326,7 +326,7 @@ void TwitchAccount::unfollowUser(const QString userID,
|
||||
"/follows/channels/" + userID);
|
||||
|
||||
NetworkRequest(requestUrl, NetworkRequestType::Delete)
|
||||
|
||||
|
||||
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
|
||||
.onError([successCallback](int code) {
|
||||
if (code >= 200 && code <= 299)
|
||||
@@ -368,7 +368,7 @@ void TwitchAccount::loadEmotes()
|
||||
"/emotes");
|
||||
|
||||
NetworkRequest(url)
|
||||
|
||||
|
||||
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
|
||||
.onError([=](int errorCode) {
|
||||
log("[TwitchAccount::loadEmotes] Error {}", errorCode);
|
||||
@@ -409,7 +409,7 @@ void TwitchAccount::autoModAllow(const QString msgID)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Content-Length", QByteArray::number(qba.size()))
|
||||
.payload(qba)
|
||||
|
||||
|
||||
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
|
||||
.onError([=](int errorCode) {
|
||||
log("[TwitchAccounts::autoModAllow] Error {}", errorCode);
|
||||
@@ -429,7 +429,7 @@ void TwitchAccount::autoModDeny(const QString msgID)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Content-Length", QByteArray::number(qba.size()))
|
||||
.payload(qba)
|
||||
|
||||
|
||||
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
|
||||
.onError([=](int errorCode) {
|
||||
log("[TwitchAccounts::autoModDeny] Error {}", errorCode);
|
||||
|
||||
@@ -1,234 +1,234 @@
|
||||
#include "providers/twitch/TwitchAccountManager.hpp"
|
||||
|
||||
#include "common/Common.hpp"
|
||||
#include "debug/Log.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
TwitchAccountManager::TwitchAccountManager()
|
||||
: anonymousUser_(new TwitchAccount(ANONYMOUS_USERNAME, "", "", ""))
|
||||
{
|
||||
this->currentUserChanged.connect([this] {
|
||||
auto currentUser = this->getCurrent();
|
||||
currentUser->loadIgnores();
|
||||
});
|
||||
|
||||
this->accounts.itemRemoved.connect([this](const auto &acc) { //
|
||||
this->removeUser(acc.item.get());
|
||||
});
|
||||
}
|
||||
|
||||
std::shared_ptr<TwitchAccount> TwitchAccountManager::getCurrent()
|
||||
{
|
||||
if (!this->currentUser_)
|
||||
{
|
||||
return this->anonymousUser_;
|
||||
}
|
||||
|
||||
return this->currentUser_;
|
||||
}
|
||||
|
||||
std::vector<QString> TwitchAccountManager::getUsernames() const
|
||||
{
|
||||
std::vector<QString> userNames;
|
||||
|
||||
std::lock_guard<std::mutex> lock(this->mutex_);
|
||||
|
||||
for (const auto &user : this->accounts)
|
||||
{
|
||||
userNames.push_back(user->getUserName());
|
||||
}
|
||||
|
||||
return userNames;
|
||||
}
|
||||
|
||||
std::shared_ptr<TwitchAccount> TwitchAccountManager::findUserByUsername(
|
||||
const QString &username) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->mutex_);
|
||||
|
||||
for (const auto &user : this->accounts)
|
||||
{
|
||||
if (username.compare(user->getUserName(), Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool TwitchAccountManager::userExists(const QString &username) const
|
||||
{
|
||||
return this->findUserByUsername(username) != nullptr;
|
||||
}
|
||||
|
||||
void TwitchAccountManager::reloadUsers()
|
||||
{
|
||||
auto keys = pajlada::Settings::SettingManager::getObjectKeys("/accounts");
|
||||
|
||||
UserData userData;
|
||||
|
||||
bool listUpdated = false;
|
||||
|
||||
for (const auto &uid : keys)
|
||||
{
|
||||
if (uid == "current")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
auto username = pajlada::Settings::Setting<QString>::get(
|
||||
"/accounts/" + uid + "/username");
|
||||
auto userID = pajlada::Settings::Setting<QString>::get("/accounts/" +
|
||||
uid + "/userID");
|
||||
auto clientID = pajlada::Settings::Setting<QString>::get(
|
||||
"/accounts/" + uid + "/clientID");
|
||||
auto oauthToken = pajlada::Settings::Setting<QString>::get(
|
||||
"/accounts/" + uid + "/oauthToken");
|
||||
|
||||
if (username.isEmpty() || userID.isEmpty() || clientID.isEmpty() ||
|
||||
oauthToken.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
userData.username = username.trimmed();
|
||||
userData.userID = userID.trimmed();
|
||||
userData.clientID = clientID.trimmed();
|
||||
userData.oauthToken = oauthToken.trimmed();
|
||||
|
||||
switch (this->addUser(userData))
|
||||
{
|
||||
case AddUserResponse::UserAlreadyExists:
|
||||
{
|
||||
log("User {} already exists", userData.username);
|
||||
// Do nothing
|
||||
}
|
||||
break;
|
||||
case AddUserResponse::UserValuesUpdated:
|
||||
{
|
||||
log("User {} already exists, and values updated!",
|
||||
userData.username);
|
||||
if (userData.username == this->getCurrent()->getUserName())
|
||||
{
|
||||
log("It was the current user, so we need to reconnect "
|
||||
"stuff!");
|
||||
this->currentUserChanged.invoke();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case AddUserResponse::UserAdded:
|
||||
{
|
||||
log("Added user {}", userData.username);
|
||||
listUpdated = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (listUpdated)
|
||||
{
|
||||
this->userListUpdated.invoke();
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchAccountManager::load()
|
||||
{
|
||||
this->reloadUsers();
|
||||
|
||||
this->currentUsername.connect([this](const QString &newUsername) {
|
||||
auto user = this->findUserByUsername(newUsername);
|
||||
if (user)
|
||||
{
|
||||
log("[AccountManager:currentUsernameChanged] User successfully "
|
||||
"updated to {}",
|
||||
newUsername);
|
||||
this->currentUser_ = user;
|
||||
}
|
||||
else
|
||||
{
|
||||
log("[AccountManager:currentUsernameChanged] User successfully "
|
||||
"updated to anonymous");
|
||||
this->currentUser_ = this->anonymousUser_;
|
||||
}
|
||||
|
||||
this->currentUserChanged.invoke();
|
||||
});
|
||||
}
|
||||
|
||||
bool TwitchAccountManager::isLoggedIn() const
|
||||
{
|
||||
if (!this->currentUser_)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Once `TwitchAccount` class has a way to check, we should also return
|
||||
// false if the credentials are incorrect
|
||||
return !this->currentUser_->isAnon();
|
||||
}
|
||||
|
||||
bool TwitchAccountManager::removeUser(TwitchAccount *account)
|
||||
{
|
||||
auto userID(account->getUserId());
|
||||
if (!userID.isEmpty())
|
||||
{
|
||||
pajlada::Settings::SettingManager::removeSetting(
|
||||
fS("/accounts/uid{}", userID));
|
||||
}
|
||||
|
||||
if (account->getUserName() == this->currentUsername)
|
||||
{
|
||||
// The user that was removed is the current user, log into the anonymous
|
||||
// user
|
||||
this->currentUsername = "";
|
||||
}
|
||||
|
||||
this->userListUpdated.invoke();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
TwitchAccountManager::AddUserResponse TwitchAccountManager::addUser(
|
||||
const TwitchAccountManager::UserData &userData)
|
||||
{
|
||||
auto previousUser = this->findUserByUsername(userData.username);
|
||||
if (previousUser)
|
||||
{
|
||||
bool userUpdated = false;
|
||||
|
||||
if (previousUser->setOAuthClient(userData.clientID))
|
||||
{
|
||||
userUpdated = true;
|
||||
}
|
||||
|
||||
if (previousUser->setOAuthToken(userData.oauthToken))
|
||||
{
|
||||
userUpdated = true;
|
||||
}
|
||||
|
||||
if (userUpdated)
|
||||
{
|
||||
return AddUserResponse::UserValuesUpdated;
|
||||
}
|
||||
else
|
||||
{
|
||||
return AddUserResponse::UserAlreadyExists;
|
||||
}
|
||||
}
|
||||
|
||||
auto newUser =
|
||||
std::make_shared<TwitchAccount>(userData.username, userData.oauthToken,
|
||||
userData.clientID, userData.userID);
|
||||
|
||||
// std::lock_guard<std::mutex> lock(this->mutex);
|
||||
|
||||
this->accounts.insertItem(newUser);
|
||||
|
||||
return AddUserResponse::UserAdded;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
#include "providers/twitch/TwitchAccountManager.hpp"
|
||||
|
||||
#include "common/Common.hpp"
|
||||
#include "debug/Log.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
TwitchAccountManager::TwitchAccountManager()
|
||||
: anonymousUser_(new TwitchAccount(ANONYMOUS_USERNAME, "", "", ""))
|
||||
{
|
||||
this->currentUserChanged.connect([this] {
|
||||
auto currentUser = this->getCurrent();
|
||||
currentUser->loadIgnores();
|
||||
});
|
||||
|
||||
this->accounts.itemRemoved.connect([this](const auto &acc) { //
|
||||
this->removeUser(acc.item.get());
|
||||
});
|
||||
}
|
||||
|
||||
std::shared_ptr<TwitchAccount> TwitchAccountManager::getCurrent()
|
||||
{
|
||||
if (!this->currentUser_)
|
||||
{
|
||||
return this->anonymousUser_;
|
||||
}
|
||||
|
||||
return this->currentUser_;
|
||||
}
|
||||
|
||||
std::vector<QString> TwitchAccountManager::getUsernames() const
|
||||
{
|
||||
std::vector<QString> userNames;
|
||||
|
||||
std::lock_guard<std::mutex> lock(this->mutex_);
|
||||
|
||||
for (const auto &user : this->accounts)
|
||||
{
|
||||
userNames.push_back(user->getUserName());
|
||||
}
|
||||
|
||||
return userNames;
|
||||
}
|
||||
|
||||
std::shared_ptr<TwitchAccount> TwitchAccountManager::findUserByUsername(
|
||||
const QString &username) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->mutex_);
|
||||
|
||||
for (const auto &user : this->accounts)
|
||||
{
|
||||
if (username.compare(user->getUserName(), Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool TwitchAccountManager::userExists(const QString &username) const
|
||||
{
|
||||
return this->findUserByUsername(username) != nullptr;
|
||||
}
|
||||
|
||||
void TwitchAccountManager::reloadUsers()
|
||||
{
|
||||
auto keys = pajlada::Settings::SettingManager::getObjectKeys("/accounts");
|
||||
|
||||
UserData userData;
|
||||
|
||||
bool listUpdated = false;
|
||||
|
||||
for (const auto &uid : keys)
|
||||
{
|
||||
if (uid == "current")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
auto username = pajlada::Settings::Setting<QString>::get(
|
||||
"/accounts/" + uid + "/username");
|
||||
auto userID = pajlada::Settings::Setting<QString>::get("/accounts/" +
|
||||
uid + "/userID");
|
||||
auto clientID = pajlada::Settings::Setting<QString>::get(
|
||||
"/accounts/" + uid + "/clientID");
|
||||
auto oauthToken = pajlada::Settings::Setting<QString>::get(
|
||||
"/accounts/" + uid + "/oauthToken");
|
||||
|
||||
if (username.isEmpty() || userID.isEmpty() || clientID.isEmpty() ||
|
||||
oauthToken.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
userData.username = username.trimmed();
|
||||
userData.userID = userID.trimmed();
|
||||
userData.clientID = clientID.trimmed();
|
||||
userData.oauthToken = oauthToken.trimmed();
|
||||
|
||||
switch (this->addUser(userData))
|
||||
{
|
||||
case AddUserResponse::UserAlreadyExists:
|
||||
{
|
||||
log("User {} already exists", userData.username);
|
||||
// Do nothing
|
||||
}
|
||||
break;
|
||||
case AddUserResponse::UserValuesUpdated:
|
||||
{
|
||||
log("User {} already exists, and values updated!",
|
||||
userData.username);
|
||||
if (userData.username == this->getCurrent()->getUserName())
|
||||
{
|
||||
log("It was the current user, so we need to reconnect "
|
||||
"stuff!");
|
||||
this->currentUserChanged.invoke();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case AddUserResponse::UserAdded:
|
||||
{
|
||||
log("Added user {}", userData.username);
|
||||
listUpdated = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (listUpdated)
|
||||
{
|
||||
this->userListUpdated.invoke();
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchAccountManager::load()
|
||||
{
|
||||
this->reloadUsers();
|
||||
|
||||
this->currentUsername.connect([this](const QString &newUsername) {
|
||||
auto user = this->findUserByUsername(newUsername);
|
||||
if (user)
|
||||
{
|
||||
log("[AccountManager:currentUsernameChanged] User successfully "
|
||||
"updated to {}",
|
||||
newUsername);
|
||||
this->currentUser_ = user;
|
||||
}
|
||||
else
|
||||
{
|
||||
log("[AccountManager:currentUsernameChanged] User successfully "
|
||||
"updated to anonymous");
|
||||
this->currentUser_ = this->anonymousUser_;
|
||||
}
|
||||
|
||||
this->currentUserChanged.invoke();
|
||||
});
|
||||
}
|
||||
|
||||
bool TwitchAccountManager::isLoggedIn() const
|
||||
{
|
||||
if (!this->currentUser_)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Once `TwitchAccount` class has a way to check, we should also return
|
||||
// false if the credentials are incorrect
|
||||
return !this->currentUser_->isAnon();
|
||||
}
|
||||
|
||||
bool TwitchAccountManager::removeUser(TwitchAccount *account)
|
||||
{
|
||||
auto userID(account->getUserId());
|
||||
if (!userID.isEmpty())
|
||||
{
|
||||
pajlada::Settings::SettingManager::removeSetting(
|
||||
fS("/accounts/uid{}", userID));
|
||||
}
|
||||
|
||||
if (account->getUserName() == this->currentUsername)
|
||||
{
|
||||
// The user that was removed is the current user, log into the anonymous
|
||||
// user
|
||||
this->currentUsername = "";
|
||||
}
|
||||
|
||||
this->userListUpdated.invoke();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
TwitchAccountManager::AddUserResponse TwitchAccountManager::addUser(
|
||||
const TwitchAccountManager::UserData &userData)
|
||||
{
|
||||
auto previousUser = this->findUserByUsername(userData.username);
|
||||
if (previousUser)
|
||||
{
|
||||
bool userUpdated = false;
|
||||
|
||||
if (previousUser->setOAuthClient(userData.clientID))
|
||||
{
|
||||
userUpdated = true;
|
||||
}
|
||||
|
||||
if (previousUser->setOAuthToken(userData.oauthToken))
|
||||
{
|
||||
userUpdated = true;
|
||||
}
|
||||
|
||||
if (userUpdated)
|
||||
{
|
||||
return AddUserResponse::UserValuesUpdated;
|
||||
}
|
||||
else
|
||||
{
|
||||
return AddUserResponse::UserAlreadyExists;
|
||||
}
|
||||
}
|
||||
|
||||
auto newUser =
|
||||
std::make_shared<TwitchAccount>(userData.username, userData.oauthToken,
|
||||
userData.clientID, userData.userID);
|
||||
|
||||
// std::lock_guard<std::mutex> lock(this->mutex);
|
||||
|
||||
this->accounts.insertItem(newUser);
|
||||
|
||||
return AddUserResponse::UserAdded;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -1,75 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/ChatterinoSetting.hpp"
|
||||
#include "common/SignalVector.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "util/SharedPtrElementLess.hpp"
|
||||
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
//
|
||||
// Warning: This class is not supposed to be created directly.
|
||||
// Get yourself an instance from our friends over at
|
||||
// AccountManager.hpp
|
||||
//
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class TwitchAccount;
|
||||
class AccountController;
|
||||
|
||||
class TwitchAccountManager
|
||||
{
|
||||
TwitchAccountManager();
|
||||
|
||||
public:
|
||||
struct UserData {
|
||||
QString username;
|
||||
QString userID;
|
||||
QString clientID;
|
||||
QString oauthToken;
|
||||
};
|
||||
|
||||
// Returns the current twitchUsers, or the anonymous user if we're not
|
||||
// currently logged in
|
||||
std::shared_ptr<TwitchAccount> getCurrent();
|
||||
|
||||
std::vector<QString> getUsernames() const;
|
||||
|
||||
std::shared_ptr<TwitchAccount> findUserByUsername(
|
||||
const QString &username) const;
|
||||
bool userExists(const QString &username) const;
|
||||
|
||||
void reloadUsers();
|
||||
void load();
|
||||
|
||||
bool isLoggedIn() const;
|
||||
|
||||
pajlada::Settings::Setting<QString> currentUsername{"/accounts/current",
|
||||
""};
|
||||
pajlada::Signals::NoArgSignal currentUserChanged;
|
||||
pajlada::Signals::NoArgSignal userListUpdated;
|
||||
|
||||
SortedSignalVector<std::shared_ptr<TwitchAccount>,
|
||||
SharedPtrElementLess<TwitchAccount>>
|
||||
accounts;
|
||||
|
||||
private:
|
||||
enum class AddUserResponse {
|
||||
UserAlreadyExists,
|
||||
UserValuesUpdated,
|
||||
UserAdded,
|
||||
};
|
||||
AddUserResponse addUser(const UserData &data);
|
||||
bool removeUser(TwitchAccount *account);
|
||||
|
||||
std::shared_ptr<TwitchAccount> currentUser_;
|
||||
|
||||
std::shared_ptr<TwitchAccount> anonymousUser_;
|
||||
mutable std::mutex mutex_;
|
||||
|
||||
friend class AccountController;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
#pragma once
|
||||
|
||||
#include "common/ChatterinoSetting.hpp"
|
||||
#include "common/SignalVector.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "util/SharedPtrElementLess.hpp"
|
||||
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
//
|
||||
// Warning: This class is not supposed to be created directly.
|
||||
// Get yourself an instance from our friends over at
|
||||
// AccountManager.hpp
|
||||
//
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class TwitchAccount;
|
||||
class AccountController;
|
||||
|
||||
class TwitchAccountManager
|
||||
{
|
||||
TwitchAccountManager();
|
||||
|
||||
public:
|
||||
struct UserData {
|
||||
QString username;
|
||||
QString userID;
|
||||
QString clientID;
|
||||
QString oauthToken;
|
||||
};
|
||||
|
||||
// Returns the current twitchUsers, or the anonymous user if we're not
|
||||
// currently logged in
|
||||
std::shared_ptr<TwitchAccount> getCurrent();
|
||||
|
||||
std::vector<QString> getUsernames() const;
|
||||
|
||||
std::shared_ptr<TwitchAccount> findUserByUsername(
|
||||
const QString &username) const;
|
||||
bool userExists(const QString &username) const;
|
||||
|
||||
void reloadUsers();
|
||||
void load();
|
||||
|
||||
bool isLoggedIn() const;
|
||||
|
||||
pajlada::Settings::Setting<QString> currentUsername{"/accounts/current",
|
||||
""};
|
||||
pajlada::Signals::NoArgSignal currentUserChanged;
|
||||
pajlada::Signals::NoArgSignal userListUpdated;
|
||||
|
||||
SortedSignalVector<std::shared_ptr<TwitchAccount>,
|
||||
SharedPtrElementLess<TwitchAccount>>
|
||||
accounts;
|
||||
|
||||
private:
|
||||
enum class AddUserResponse {
|
||||
UserAlreadyExists,
|
||||
UserValuesUpdated,
|
||||
UserAdded,
|
||||
};
|
||||
AddUserResponse addUser(const UserData &data);
|
||||
bool removeUser(TwitchAccount *account);
|
||||
|
||||
std::shared_ptr<TwitchAccount> currentUser_;
|
||||
|
||||
std::shared_ptr<TwitchAccount> anonymousUser_;
|
||||
mutable std::mutex mutex_;
|
||||
|
||||
friend class AccountController;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -16,7 +16,7 @@ void TwitchApi::findUserId(const QString user,
|
||||
QString requestUrl("https://api.twitch.tv/kraken/users?login=" + user);
|
||||
|
||||
NetworkRequest(requestUrl)
|
||||
|
||||
|
||||
.authorizeTwitchV5(getDefaultClientID())
|
||||
.timeout(30000)
|
||||
.onSuccess([successCallback](auto result) mutable -> Outcome {
|
||||
@@ -65,7 +65,7 @@ void TwitchApi::findUserName(const QString userid,
|
||||
QString requestUrl("https://api.twitch.tv/kraken/users/" + userid);
|
||||
|
||||
NetworkRequest(requestUrl)
|
||||
|
||||
|
||||
.authorizeTwitchV5(getDefaultClientID())
|
||||
.timeout(30000)
|
||||
.onSuccess([successCallback](auto result) mutable -> Outcome {
|
||||
|
||||
@@ -18,7 +18,7 @@ void TwitchBadges::loadTwitchBadges()
|
||||
"https://badges.twitch.tv/v1/badges/global/display?language=en");
|
||||
|
||||
NetworkRequest(url)
|
||||
|
||||
|
||||
.onSuccess([this](auto result) -> Outcome {
|
||||
auto root = result.parseJson();
|
||||
auto badgeSets = this->badgeSets_.access();
|
||||
|
||||
@@ -88,7 +88,6 @@ TwitchChannel::TwitchChannel(const QString &name,
|
||||
, globalFfz_(ffz)
|
||||
, bttvEmotes_(std::make_shared<EmoteMap>())
|
||||
, ffzEmotes_(std::make_shared<EmoteMap>())
|
||||
, ffzCustomModBadge_(name)
|
||||
, mod_(false)
|
||||
{
|
||||
log("[TwitchChannel:{}] Opened", name);
|
||||
@@ -139,7 +138,6 @@ void TwitchChannel::initialize()
|
||||
{
|
||||
this->refreshChatters();
|
||||
this->refreshBadges();
|
||||
this->ffzCustomModBadge_.loadCustomModBadge();
|
||||
}
|
||||
|
||||
bool TwitchChannel::isEmpty() const
|
||||
@@ -165,10 +163,17 @@ void TwitchChannel::refreshBTTVChannelEmotes()
|
||||
void TwitchChannel::refreshFFZChannelEmotes()
|
||||
{
|
||||
FfzEmotes::loadChannel(
|
||||
this->roomId(), [this, weak = weakOf<Channel>(this)](auto &&emoteMap) {
|
||||
this->roomId(),
|
||||
[this, weak = weakOf<Channel>(this)](auto &&emoteMap) {
|
||||
if (auto shared = weak.lock())
|
||||
this->ffzEmotes_.set(
|
||||
std::make_shared<EmoteMap>(std::move(emoteMap)));
|
||||
},
|
||||
[this, weak = weakOf<Channel>(this)](auto &&modBadge) {
|
||||
if (auto shared = weak.lock())
|
||||
{
|
||||
this->ffzCustomModBadge_.set(std::move(modBadge));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -685,22 +690,23 @@ void TwitchChannel::refreshBadges()
|
||||
|
||||
void TwitchChannel::refreshCheerEmotes()
|
||||
{
|
||||
/*auto url = Url{"https://api.twitch.tv/kraken/bits/actions?channel_id=" +
|
||||
this->getRoomId()};
|
||||
auto request = NetworkRequest::twitchRequest(url.string);
|
||||
request.setCaller(QThread::currentThread());
|
||||
|
||||
request.onSuccess(
|
||||
[this, weak = weakOf<Channel>(this)](auto result) -> Outcome {
|
||||
QString url("https://api.twitch.tv/kraken/bits/actions?channel_id=" +
|
||||
this->roomId());
|
||||
NetworkRequest::twitchRequest(url)
|
||||
.onSuccess([this,
|
||||
weak = weakOf<Channel>(this)](auto result) -> Outcome {
|
||||
auto cheerEmoteSets = ParseCheermoteSets(result.parseRapidJson());
|
||||
std::vector<CheerEmoteSet> emoteSets;
|
||||
|
||||
for (auto &set : cheerEmoteSets) {
|
||||
for (auto &set : cheerEmoteSets)
|
||||
{
|
||||
auto cheerEmoteSet = CheerEmoteSet();
|
||||
cheerEmoteSet.regex = QRegularExpression(
|
||||
"^" + set.prefix.toLower() + "([1-9][0-9]*)$");
|
||||
"^" + set.prefix + "([1-9][0-9]*)$",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
|
||||
for (auto &tier : set.tiers) {
|
||||
for (auto &tier : set.tiers)
|
||||
{
|
||||
CheerEmote cheerEmote;
|
||||
|
||||
cheerEmote.color = QColor(tier.color);
|
||||
@@ -733,7 +739,7 @@ void TwitchChannel::refreshCheerEmotes()
|
||||
std::sort(cheerEmoteSet.cheerEmotes.begin(),
|
||||
cheerEmoteSet.cheerEmotes.end(),
|
||||
[](const auto &lhs, const auto &rhs) {
|
||||
return lhs.minBits < rhs.minBits; //
|
||||
return lhs.minBits > rhs.minBits;
|
||||
});
|
||||
|
||||
emoteSets.emplace_back(cheerEmoteSet);
|
||||
@@ -741,10 +747,8 @@ void TwitchChannel::refreshCheerEmotes()
|
||||
*this->cheerEmoteSets_.access() = std::move(emoteSets);
|
||||
|
||||
return Success;
|
||||
});
|
||||
|
||||
request.execute();
|
||||
*/
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
boost::optional<EmotePtr> TwitchChannel::twitchBadge(
|
||||
@@ -765,9 +769,34 @@ boost::optional<EmotePtr> TwitchChannel::twitchBadge(
|
||||
|
||||
boost::optional<EmotePtr> TwitchChannel::ffzCustomModBadge() const
|
||||
{
|
||||
if (auto badge = this->ffzCustomModBadge_.badge())
|
||||
return badge;
|
||||
return this->ffzCustomModBadge_.get();
|
||||
}
|
||||
|
||||
boost::optional<CheerEmote> TwitchChannel::cheerEmote(const QString &string)
|
||||
{
|
||||
auto sets = this->cheerEmoteSets_.access();
|
||||
for (const auto &set : *sets)
|
||||
{
|
||||
auto match = set.regex.match(string);
|
||||
if (!match.hasMatch())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
QString amount = match.captured(1);
|
||||
bool ok = false;
|
||||
int bitAmount = amount.toInt(&ok);
|
||||
if (!ok)
|
||||
{
|
||||
log("Error parsing bit amount in cheerEmote");
|
||||
}
|
||||
for (const auto &emote : set.cheerEmotes)
|
||||
{
|
||||
if (bitAmount >= emote.minBits)
|
||||
{
|
||||
return emote;
|
||||
}
|
||||
}
|
||||
}
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#include "common/ChannelChatters.hpp"
|
||||
#include "common/Outcome.hpp"
|
||||
#include "common/UniqueAccess.hpp"
|
||||
#include "providers/ffz/FfzModBadge.hpp"
|
||||
#include "common/UsernameSet.hpp"
|
||||
#include "providers/twitch/TwitchEmotes.hpp"
|
||||
|
||||
#include <rapidjson/document.h>
|
||||
@@ -96,6 +96,9 @@ public:
|
||||
boost::optional<EmotePtr> twitchBadge(const QString &set,
|
||||
const QString &version) const;
|
||||
|
||||
// Cheers
|
||||
boost::optional<CheerEmote> cheerEmote(const QString &string);
|
||||
|
||||
// Signals
|
||||
pajlada::Signals::NoArgSignal roomIdChanged;
|
||||
pajlada::Signals::NoArgSignal userStateChanged;
|
||||
@@ -145,13 +148,13 @@ protected:
|
||||
FfzEmotes &globalFfz_;
|
||||
Atomic<std::shared_ptr<const EmoteMap>> bttvEmotes_;
|
||||
Atomic<std::shared_ptr<const EmoteMap>> ffzEmotes_;
|
||||
Atomic<boost::optional<EmotePtr>> ffzCustomModBadge_;
|
||||
|
||||
private:
|
||||
// Badges
|
||||
UniqueAccess<std::map<QString, std::map<QString, EmotePtr>>>
|
||||
badgeSets_; // "subscribers": { "0": ... "3": ... "6": ...
|
||||
UniqueAccess<std::vector<CheerEmoteSet>> cheerEmoteSets_;
|
||||
FfzModBadge ffzCustomModBadge_;
|
||||
|
||||
bool mod_ = false;
|
||||
bool vip_ = false;
|
||||
|
||||
@@ -1,373 +1,373 @@
|
||||
#include "TwitchIrcServer.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/Common.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "controllers/highlights/HighlightController.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "providers/twitch/ChatroomChannel.hpp"
|
||||
#include "providers/twitch/IrcMessageHandler.hpp"
|
||||
#include "providers/twitch/PubsubClient.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchHelpers.hpp"
|
||||
#include "providers/twitch/TwitchMessageBuilder.hpp"
|
||||
#include "util/PostToThread.hpp"
|
||||
|
||||
#include <IrcCommand>
|
||||
#include <cassert>
|
||||
|
||||
// using namespace Communi;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
namespace {
|
||||
bool isChatroom(const QString &channel)
|
||||
{
|
||||
if (channel.left(10) == "chatrooms:")
|
||||
{
|
||||
auto reflist = channel.splitRef(':');
|
||||
if (reflist.size() == 3)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TwitchIrcServer::TwitchIrcServer()
|
||||
: whispersChannel(new Channel("/whispers", Channel::Type::TwitchWhispers))
|
||||
, mentionsChannel(new Channel("/mentions", Channel::Type::TwitchMentions))
|
||||
, watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching)
|
||||
{
|
||||
this->pubsub = new PubSub;
|
||||
|
||||
// getSettings()->twitchSeperateWriteConnection.connect([this](auto, auto) {
|
||||
// this->connect(); },
|
||||
// this->signalHolder_,
|
||||
// false);
|
||||
}
|
||||
|
||||
void TwitchIrcServer::initialize(Settings &settings, Paths &paths)
|
||||
{
|
||||
getApp()->accounts->twitch.currentUserChanged.connect(
|
||||
[this]() { postToThread([this] { this->connect(); }); });
|
||||
|
||||
this->twitchBadges.loadTwitchBadges();
|
||||
this->bttv.loadEmotes();
|
||||
this->ffz.loadEmotes();
|
||||
}
|
||||
|
||||
void TwitchIrcServer::initializeConnection(IrcConnection *connection,
|
||||
ConnectionType type)
|
||||
{
|
||||
std::shared_ptr<TwitchAccount> account =
|
||||
getApp()->accounts->twitch.getCurrent();
|
||||
|
||||
qDebug() << "logging in as" << account->getUserName();
|
||||
|
||||
QString username = account->getUserName();
|
||||
QString oauthToken = account->getOAuthToken();
|
||||
|
||||
if (!oauthToken.startsWith("oauth:"))
|
||||
{
|
||||
oauthToken.prepend("oauth:");
|
||||
}
|
||||
|
||||
connection->setUserName(username);
|
||||
connection->setNickName(username);
|
||||
connection->setRealName(username);
|
||||
|
||||
if (!account->isAnon())
|
||||
{
|
||||
connection->setPassword(oauthToken);
|
||||
}
|
||||
|
||||
connection->setSecure(true);
|
||||
|
||||
// https://dev.twitch.tv/docs/irc/guide/#connecting-to-twitch-irc
|
||||
// SSL disabled: irc://irc.chat.twitch.tv:6667
|
||||
// SSL enabled: irc://irc.chat.twitch.tv:6697
|
||||
connection->setHost("irc.chat.twitch.tv");
|
||||
connection->setPort(6697);
|
||||
|
||||
this->open(type);
|
||||
}
|
||||
|
||||
std::shared_ptr<Channel> TwitchIrcServer::createChannel(
|
||||
const QString &channelName)
|
||||
{
|
||||
std::shared_ptr<TwitchChannel> channel;
|
||||
if (isChatroom(channelName))
|
||||
{
|
||||
channel = std::static_pointer_cast<TwitchChannel>(
|
||||
std::shared_ptr<ChatroomChannel>(new ChatroomChannel(
|
||||
channelName, this->twitchBadges, this->bttv, this->ffz)));
|
||||
}
|
||||
else
|
||||
{
|
||||
channel = std::shared_ptr<TwitchChannel>(new TwitchChannel(
|
||||
channelName, this->twitchBadges, this->bttv, this->ffz));
|
||||
}
|
||||
channel->initialize();
|
||||
|
||||
channel->sendMessageSignal.connect(
|
||||
[this, channel = channel.get()](auto &chan, auto &msg, bool &sent) {
|
||||
this->onMessageSendRequested(channel, msg, sent);
|
||||
});
|
||||
|
||||
return std::shared_ptr<Channel>(channel);
|
||||
}
|
||||
|
||||
void TwitchIrcServer::privateMessageReceived(
|
||||
Communi::IrcPrivateMessage *message)
|
||||
{
|
||||
IrcMessageHandler::getInstance().handlePrivMessage(message, *this);
|
||||
}
|
||||
|
||||
void TwitchIrcServer::readConnectionMessageReceived(
|
||||
Communi::IrcMessage *message)
|
||||
{
|
||||
AbstractIrcServer::readConnectionMessageReceived(message);
|
||||
|
||||
if (message->type() == Communi::IrcMessage::Type::Private)
|
||||
{
|
||||
// We already have a handler for private messages
|
||||
return;
|
||||
}
|
||||
|
||||
const QString &command = message->command();
|
||||
|
||||
auto &handler = IrcMessageHandler::getInstance();
|
||||
|
||||
// Below commands enabled through the twitch.tv/membership CAP REQ
|
||||
if (command == "MODE")
|
||||
{
|
||||
handler.handleModeMessage(message);
|
||||
}
|
||||
else if (command == "JOIN")
|
||||
{
|
||||
handler.handleJoinMessage(message);
|
||||
}
|
||||
else if (command == "PART")
|
||||
{
|
||||
handler.handlePartMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchIrcServer::writeConnectionMessageReceived(
|
||||
Communi::IrcMessage *message)
|
||||
{
|
||||
const QString &command = message->command();
|
||||
|
||||
auto &handler = IrcMessageHandler::getInstance();
|
||||
|
||||
// Below commands enabled through the twitch.tv/commands CAP REQ
|
||||
if (command == "USERSTATE")
|
||||
{
|
||||
handler.handleUserStateMessage(message);
|
||||
}
|
||||
else if (command == "WHISPER")
|
||||
{
|
||||
handler.handleWhisperMessage(message);
|
||||
}
|
||||
else if (command == "USERNOTICE")
|
||||
{
|
||||
handler.handleUserNoticeMessage(message, *this);
|
||||
}
|
||||
else if (command == "ROOMSTATE")
|
||||
{
|
||||
handler.handleRoomStateMessage(message);
|
||||
}
|
||||
else if (command == "CLEARCHAT")
|
||||
{
|
||||
handler.handleClearChatMessage(message);
|
||||
}
|
||||
else if (command == "CLEARMSG")
|
||||
{
|
||||
handler.handleClearMessageMessage(message);
|
||||
}
|
||||
else if (command == "NOTICE")
|
||||
{
|
||||
handler.handleNoticeMessage(
|
||||
static_cast<Communi::IrcNoticeMessage *>(message));
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchIrcServer::onReadConnected(IrcConnection *connection)
|
||||
{
|
||||
AbstractIrcServer::onReadConnected(connection);
|
||||
|
||||
// twitch.tv/tags enables IRCv3 tags on messages. See https://dev.twitch.tv/docs/irc/tags/
|
||||
// twitch.tv/membership enables the JOIN/PART/MODE/NAMES commands. See https://dev.twitch.tv/docs/irc/membership/
|
||||
connection->sendRaw("CAP REQ :twitch.tv/tags twitch.tv/membership");
|
||||
}
|
||||
|
||||
void TwitchIrcServer::onWriteConnected(IrcConnection *connection)
|
||||
{
|
||||
AbstractIrcServer::onWriteConnected(connection);
|
||||
|
||||
// twitch.tv/tags enables IRCv3 tags on messages. See https://dev.twitch.tv/docs/irc/tags/
|
||||
// twitch.tv/commands enables a bunch of miscellaneous command capabilities. See https://dev.twitch.tv/docs/irc/commands/
|
||||
connection->sendRaw("CAP REQ :twitch.tv/tags twitch.tv/commands");
|
||||
}
|
||||
|
||||
std::shared_ptr<Channel> TwitchIrcServer::getCustomChannel(
|
||||
const QString &channelName)
|
||||
{
|
||||
if (channelName == "/whispers")
|
||||
{
|
||||
return this->whispersChannel;
|
||||
}
|
||||
|
||||
if (channelName == "/mentions")
|
||||
{
|
||||
return this->mentionsChannel;
|
||||
}
|
||||
|
||||
if (channelName == "$$$")
|
||||
{
|
||||
static auto channel =
|
||||
std::make_shared<Channel>("$$$", chatterino::Channel::Type::Misc);
|
||||
static auto getTimer = [&] {
|
||||
for (auto i = 0; i < 1000; i++)
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(QString::number(i + 1)));
|
||||
}
|
||||
|
||||
auto timer = new QTimer;
|
||||
QObject::connect(timer, &QTimer::timeout, [] {
|
||||
channel->addMessage(
|
||||
makeSystemMessage(QTime::currentTime().toString()));
|
||||
});
|
||||
timer->start(500);
|
||||
return timer;
|
||||
}();
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void TwitchIrcServer::forEachChannelAndSpecialChannels(
|
||||
std::function<void(ChannelPtr)> func)
|
||||
{
|
||||
this->forEachChannel(func);
|
||||
|
||||
func(this->whispersChannel);
|
||||
func(this->mentionsChannel);
|
||||
}
|
||||
|
||||
std::shared_ptr<Channel> TwitchIrcServer::getChannelOrEmptyByID(
|
||||
const QString &channelId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->channelMutex);
|
||||
|
||||
for (const auto &weakChannel : this->channels)
|
||||
{
|
||||
auto channel = weakChannel.lock();
|
||||
if (!channel)
|
||||
continue;
|
||||
|
||||
auto twitchChannel = std::dynamic_pointer_cast<TwitchChannel>(channel);
|
||||
if (!twitchChannel)
|
||||
continue;
|
||||
|
||||
if (twitchChannel->roomId() == channelId &&
|
||||
twitchChannel->getName().splitRef(":").size() < 3)
|
||||
{
|
||||
return twitchChannel;
|
||||
}
|
||||
}
|
||||
|
||||
return Channel::getEmpty();
|
||||
}
|
||||
|
||||
QString TwitchIrcServer::cleanChannelName(const QString &dirtyChannelName)
|
||||
{
|
||||
if (dirtyChannelName.startsWith('#'))
|
||||
return dirtyChannelName.mid(1).toLower();
|
||||
else
|
||||
return dirtyChannelName.toLower();
|
||||
}
|
||||
|
||||
bool TwitchIrcServer::hasSeparateWriteConnection() const
|
||||
{
|
||||
return true;
|
||||
// return getSettings()->twitchSeperateWriteConnection;
|
||||
}
|
||||
|
||||
void TwitchIrcServer::onMessageSendRequested(TwitchChannel *channel,
|
||||
const QString &message, bool &sent)
|
||||
{
|
||||
sent = false;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(this->lastMessageMutex_);
|
||||
|
||||
// std::queue<std::chrono::steady_clock::time_point>
|
||||
auto &lastMessage = channel->hasHighRateLimit()
|
||||
? this->lastMessageMod_
|
||||
: this->lastMessagePleb_;
|
||||
size_t maxMessageCount = channel->hasHighRateLimit() ? 99 : 19;
|
||||
auto minMessageOffset = (channel->hasHighRateLimit() ? 100ms : 1100ms);
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
// check if you are sending messages too fast
|
||||
if (!lastMessage.empty() && lastMessage.back() + minMessageOffset > now)
|
||||
{
|
||||
if (this->lastErrorTimeSpeed_ + 30s < now)
|
||||
{
|
||||
auto errorMessage =
|
||||
makeSystemMessage("sending messages too fast");
|
||||
|
||||
channel->addMessage(errorMessage);
|
||||
|
||||
this->lastErrorTimeSpeed_ = now;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// remove messages older than 30 seconds
|
||||
while (!lastMessage.empty() && lastMessage.front() + 32s < now)
|
||||
{
|
||||
lastMessage.pop();
|
||||
}
|
||||
|
||||
// check if you are sending too many messages
|
||||
if (lastMessage.size() >= maxMessageCount)
|
||||
{
|
||||
if (this->lastErrorTimeAmount_ + 30s < now)
|
||||
{
|
||||
auto errorMessage =
|
||||
makeSystemMessage("sending too many messages");
|
||||
|
||||
channel->addMessage(errorMessage);
|
||||
|
||||
this->lastErrorTimeAmount_ = now;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
lastMessage.push(now);
|
||||
}
|
||||
|
||||
this->sendMessage(channel->getName(), message);
|
||||
sent = true;
|
||||
}
|
||||
|
||||
const BttvEmotes &TwitchIrcServer::getBttvEmotes() const
|
||||
{
|
||||
return this->bttv;
|
||||
}
|
||||
const FfzEmotes &TwitchIrcServer::getFfzEmotes() const
|
||||
{
|
||||
return this->ffz;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
#include "TwitchIrcServer.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/Common.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "controllers/highlights/HighlightController.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "providers/twitch/ChatroomChannel.hpp"
|
||||
#include "providers/twitch/IrcMessageHandler.hpp"
|
||||
#include "providers/twitch/PubsubClient.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchHelpers.hpp"
|
||||
#include "providers/twitch/TwitchMessageBuilder.hpp"
|
||||
#include "util/PostToThread.hpp"
|
||||
|
||||
#include <IrcCommand>
|
||||
#include <cassert>
|
||||
|
||||
// using namespace Communi;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
namespace {
|
||||
bool isChatroom(const QString &channel)
|
||||
{
|
||||
if (channel.left(10) == "chatrooms:")
|
||||
{
|
||||
auto reflist = channel.splitRef(':');
|
||||
if (reflist.size() == 3)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TwitchIrcServer::TwitchIrcServer()
|
||||
: whispersChannel(new Channel("/whispers", Channel::Type::TwitchWhispers))
|
||||
, mentionsChannel(new Channel("/mentions", Channel::Type::TwitchMentions))
|
||||
, watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching)
|
||||
{
|
||||
this->pubsub = new PubSub;
|
||||
|
||||
// getSettings()->twitchSeperateWriteConnection.connect([this](auto, auto) {
|
||||
// this->connect(); },
|
||||
// this->signalHolder_,
|
||||
// false);
|
||||
}
|
||||
|
||||
void TwitchIrcServer::initialize(Settings &settings, Paths &paths)
|
||||
{
|
||||
getApp()->accounts->twitch.currentUserChanged.connect(
|
||||
[this]() { postToThread([this] { this->connect(); }); });
|
||||
|
||||
this->twitchBadges.loadTwitchBadges();
|
||||
this->bttv.loadEmotes();
|
||||
this->ffz.loadEmotes();
|
||||
}
|
||||
|
||||
void TwitchIrcServer::initializeConnection(IrcConnection *connection,
|
||||
ConnectionType type)
|
||||
{
|
||||
std::shared_ptr<TwitchAccount> account =
|
||||
getApp()->accounts->twitch.getCurrent();
|
||||
|
||||
qDebug() << "logging in as" << account->getUserName();
|
||||
|
||||
QString username = account->getUserName();
|
||||
QString oauthToken = account->getOAuthToken();
|
||||
|
||||
if (!oauthToken.startsWith("oauth:"))
|
||||
{
|
||||
oauthToken.prepend("oauth:");
|
||||
}
|
||||
|
||||
connection->setUserName(username);
|
||||
connection->setNickName(username);
|
||||
connection->setRealName(username);
|
||||
|
||||
if (!account->isAnon())
|
||||
{
|
||||
connection->setPassword(oauthToken);
|
||||
}
|
||||
|
||||
connection->setSecure(true);
|
||||
|
||||
// https://dev.twitch.tv/docs/irc/guide/#connecting-to-twitch-irc
|
||||
// SSL disabled: irc://irc.chat.twitch.tv:6667
|
||||
// SSL enabled: irc://irc.chat.twitch.tv:6697
|
||||
connection->setHost("irc.chat.twitch.tv");
|
||||
connection->setPort(6697);
|
||||
|
||||
this->open(type);
|
||||
}
|
||||
|
||||
std::shared_ptr<Channel> TwitchIrcServer::createChannel(
|
||||
const QString &channelName)
|
||||
{
|
||||
std::shared_ptr<TwitchChannel> channel;
|
||||
if (isChatroom(channelName))
|
||||
{
|
||||
channel = std::static_pointer_cast<TwitchChannel>(
|
||||
std::shared_ptr<ChatroomChannel>(new ChatroomChannel(
|
||||
channelName, this->twitchBadges, this->bttv, this->ffz)));
|
||||
}
|
||||
else
|
||||
{
|
||||
channel = std::shared_ptr<TwitchChannel>(new TwitchChannel(
|
||||
channelName, this->twitchBadges, this->bttv, this->ffz));
|
||||
}
|
||||
channel->initialize();
|
||||
|
||||
channel->sendMessageSignal.connect(
|
||||
[this, channel = channel.get()](auto &chan, auto &msg, bool &sent) {
|
||||
this->onMessageSendRequested(channel, msg, sent);
|
||||
});
|
||||
|
||||
return std::shared_ptr<Channel>(channel);
|
||||
}
|
||||
|
||||
void TwitchIrcServer::privateMessageReceived(
|
||||
Communi::IrcPrivateMessage *message)
|
||||
{
|
||||
IrcMessageHandler::getInstance().handlePrivMessage(message, *this);
|
||||
}
|
||||
|
||||
void TwitchIrcServer::readConnectionMessageReceived(
|
||||
Communi::IrcMessage *message)
|
||||
{
|
||||
AbstractIrcServer::readConnectionMessageReceived(message);
|
||||
|
||||
if (message->type() == Communi::IrcMessage::Type::Private)
|
||||
{
|
||||
// We already have a handler for private messages
|
||||
return;
|
||||
}
|
||||
|
||||
const QString &command = message->command();
|
||||
|
||||
auto &handler = IrcMessageHandler::getInstance();
|
||||
|
||||
// Below commands enabled through the twitch.tv/membership CAP REQ
|
||||
if (command == "MODE")
|
||||
{
|
||||
handler.handleModeMessage(message);
|
||||
}
|
||||
else if (command == "JOIN")
|
||||
{
|
||||
handler.handleJoinMessage(message);
|
||||
}
|
||||
else if (command == "PART")
|
||||
{
|
||||
handler.handlePartMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchIrcServer::writeConnectionMessageReceived(
|
||||
Communi::IrcMessage *message)
|
||||
{
|
||||
const QString &command = message->command();
|
||||
|
||||
auto &handler = IrcMessageHandler::getInstance();
|
||||
|
||||
// Below commands enabled through the twitch.tv/commands CAP REQ
|
||||
if (command == "USERSTATE")
|
||||
{
|
||||
handler.handleUserStateMessage(message);
|
||||
}
|
||||
else if (command == "WHISPER")
|
||||
{
|
||||
handler.handleWhisperMessage(message);
|
||||
}
|
||||
else if (command == "USERNOTICE")
|
||||
{
|
||||
handler.handleUserNoticeMessage(message, *this);
|
||||
}
|
||||
else if (command == "ROOMSTATE")
|
||||
{
|
||||
handler.handleRoomStateMessage(message);
|
||||
}
|
||||
else if (command == "CLEARCHAT")
|
||||
{
|
||||
handler.handleClearChatMessage(message);
|
||||
}
|
||||
else if (command == "CLEARMSG")
|
||||
{
|
||||
handler.handleClearMessageMessage(message);
|
||||
}
|
||||
else if (command == "NOTICE")
|
||||
{
|
||||
handler.handleNoticeMessage(
|
||||
static_cast<Communi::IrcNoticeMessage *>(message));
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchIrcServer::onReadConnected(IrcConnection *connection)
|
||||
{
|
||||
AbstractIrcServer::onReadConnected(connection);
|
||||
|
||||
// twitch.tv/tags enables IRCv3 tags on messages. See https://dev.twitch.tv/docs/irc/tags/
|
||||
// twitch.tv/membership enables the JOIN/PART/MODE/NAMES commands. See https://dev.twitch.tv/docs/irc/membership/
|
||||
connection->sendRaw("CAP REQ :twitch.tv/tags twitch.tv/membership");
|
||||
}
|
||||
|
||||
void TwitchIrcServer::onWriteConnected(IrcConnection *connection)
|
||||
{
|
||||
AbstractIrcServer::onWriteConnected(connection);
|
||||
|
||||
// twitch.tv/tags enables IRCv3 tags on messages. See https://dev.twitch.tv/docs/irc/tags/
|
||||
// twitch.tv/commands enables a bunch of miscellaneous command capabilities. See https://dev.twitch.tv/docs/irc/commands/
|
||||
connection->sendRaw("CAP REQ :twitch.tv/tags twitch.tv/commands");
|
||||
}
|
||||
|
||||
std::shared_ptr<Channel> TwitchIrcServer::getCustomChannel(
|
||||
const QString &channelName)
|
||||
{
|
||||
if (channelName == "/whispers")
|
||||
{
|
||||
return this->whispersChannel;
|
||||
}
|
||||
|
||||
if (channelName == "/mentions")
|
||||
{
|
||||
return this->mentionsChannel;
|
||||
}
|
||||
|
||||
if (channelName == "$$$")
|
||||
{
|
||||
static auto channel =
|
||||
std::make_shared<Channel>("$$$", chatterino::Channel::Type::Misc);
|
||||
static auto getTimer = [&] {
|
||||
for (auto i = 0; i < 1000; i++)
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(QString::number(i + 1)));
|
||||
}
|
||||
|
||||
auto timer = new QTimer;
|
||||
QObject::connect(timer, &QTimer::timeout, [] {
|
||||
channel->addMessage(
|
||||
makeSystemMessage(QTime::currentTime().toString()));
|
||||
});
|
||||
timer->start(500);
|
||||
return timer;
|
||||
}();
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void TwitchIrcServer::forEachChannelAndSpecialChannels(
|
||||
std::function<void(ChannelPtr)> func)
|
||||
{
|
||||
this->forEachChannel(func);
|
||||
|
||||
func(this->whispersChannel);
|
||||
func(this->mentionsChannel);
|
||||
}
|
||||
|
||||
std::shared_ptr<Channel> TwitchIrcServer::getChannelOrEmptyByID(
|
||||
const QString &channelId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->channelMutex);
|
||||
|
||||
for (const auto &weakChannel : this->channels)
|
||||
{
|
||||
auto channel = weakChannel.lock();
|
||||
if (!channel)
|
||||
continue;
|
||||
|
||||
auto twitchChannel = std::dynamic_pointer_cast<TwitchChannel>(channel);
|
||||
if (!twitchChannel)
|
||||
continue;
|
||||
|
||||
if (twitchChannel->roomId() == channelId &&
|
||||
twitchChannel->getName().splitRef(":").size() < 3)
|
||||
{
|
||||
return twitchChannel;
|
||||
}
|
||||
}
|
||||
|
||||
return Channel::getEmpty();
|
||||
}
|
||||
|
||||
QString TwitchIrcServer::cleanChannelName(const QString &dirtyChannelName)
|
||||
{
|
||||
if (dirtyChannelName.startsWith('#'))
|
||||
return dirtyChannelName.mid(1).toLower();
|
||||
else
|
||||
return dirtyChannelName.toLower();
|
||||
}
|
||||
|
||||
bool TwitchIrcServer::hasSeparateWriteConnection() const
|
||||
{
|
||||
return true;
|
||||
// return getSettings()->twitchSeperateWriteConnection;
|
||||
}
|
||||
|
||||
void TwitchIrcServer::onMessageSendRequested(TwitchChannel *channel,
|
||||
const QString &message, bool &sent)
|
||||
{
|
||||
sent = false;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(this->lastMessageMutex_);
|
||||
|
||||
// std::queue<std::chrono::steady_clock::time_point>
|
||||
auto &lastMessage = channel->hasHighRateLimit()
|
||||
? this->lastMessageMod_
|
||||
: this->lastMessagePleb_;
|
||||
size_t maxMessageCount = channel->hasHighRateLimit() ? 99 : 19;
|
||||
auto minMessageOffset = (channel->hasHighRateLimit() ? 100ms : 1100ms);
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
// check if you are sending messages too fast
|
||||
if (!lastMessage.empty() && lastMessage.back() + minMessageOffset > now)
|
||||
{
|
||||
if (this->lastErrorTimeSpeed_ + 30s < now)
|
||||
{
|
||||
auto errorMessage =
|
||||
makeSystemMessage("sending messages too fast");
|
||||
|
||||
channel->addMessage(errorMessage);
|
||||
|
||||
this->lastErrorTimeSpeed_ = now;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// remove messages older than 30 seconds
|
||||
while (!lastMessage.empty() && lastMessage.front() + 32s < now)
|
||||
{
|
||||
lastMessage.pop();
|
||||
}
|
||||
|
||||
// check if you are sending too many messages
|
||||
if (lastMessage.size() >= maxMessageCount)
|
||||
{
|
||||
if (this->lastErrorTimeAmount_ + 30s < now)
|
||||
{
|
||||
auto errorMessage =
|
||||
makeSystemMessage("sending too many messages");
|
||||
|
||||
channel->addMessage(errorMessage);
|
||||
|
||||
this->lastErrorTimeAmount_ = now;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
lastMessage.push(now);
|
||||
}
|
||||
|
||||
this->sendMessage(channel->getName(), message);
|
||||
sent = true;
|
||||
}
|
||||
|
||||
const BttvEmotes &TwitchIrcServer::getBttvEmotes() const
|
||||
{
|
||||
return this->bttv;
|
||||
}
|
||||
const FfzEmotes &TwitchIrcServer::getFfzEmotes() const
|
||||
{
|
||||
return this->ffz;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -1,85 +1,85 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/Atomic.hpp"
|
||||
#include "common/Channel.hpp"
|
||||
#include "common/Singleton.hpp"
|
||||
#include "pajlada/signals/signalholder.hpp"
|
||||
#include "providers/bttv/BttvEmotes.hpp"
|
||||
#include "providers/ffz/FfzEmotes.hpp"
|
||||
#include "providers/irc/AbstractIrcServer.hpp"
|
||||
#include "providers/twitch/TwitchBadges.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <queue>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class Settings;
|
||||
class Paths;
|
||||
class PubSub;
|
||||
class TwitchChannel;
|
||||
|
||||
class TwitchIrcServer final : public AbstractIrcServer, public Singleton
|
||||
{
|
||||
public:
|
||||
TwitchIrcServer();
|
||||
virtual ~TwitchIrcServer() override = default;
|
||||
|
||||
virtual void initialize(Settings &settings, Paths &paths) override;
|
||||
|
||||
void forEachChannelAndSpecialChannels(std::function<void(ChannelPtr)> func);
|
||||
|
||||
std::shared_ptr<Channel> getChannelOrEmptyByID(const QString &channelID);
|
||||
|
||||
Atomic<QString> lastUserThatWhisperedMe;
|
||||
|
||||
const ChannelPtr whispersChannel;
|
||||
const ChannelPtr mentionsChannel;
|
||||
IndirectChannel watchingChannel;
|
||||
|
||||
PubSub *pubsub;
|
||||
|
||||
const BttvEmotes &getBttvEmotes() const;
|
||||
const FfzEmotes &getFfzEmotes() const;
|
||||
|
||||
protected:
|
||||
virtual void initializeConnection(IrcConnection *connection,
|
||||
ConnectionType type) override;
|
||||
virtual std::shared_ptr<Channel> createChannel(
|
||||
const QString &channelName) override;
|
||||
|
||||
virtual void privateMessageReceived(
|
||||
Communi::IrcPrivateMessage *message) override;
|
||||
virtual void readConnectionMessageReceived(
|
||||
Communi::IrcMessage *message) override;
|
||||
virtual void writeConnectionMessageReceived(
|
||||
Communi::IrcMessage *message) override;
|
||||
|
||||
virtual void onReadConnected(IrcConnection *connection) override;
|
||||
virtual void onWriteConnected(IrcConnection *connection) override;
|
||||
|
||||
virtual std::shared_ptr<Channel> getCustomChannel(
|
||||
const QString &channelname) override;
|
||||
|
||||
virtual QString cleanChannelName(const QString &dirtyChannelName) override;
|
||||
virtual bool hasSeparateWriteConnection() const override;
|
||||
|
||||
private:
|
||||
void onMessageSendRequested(TwitchChannel *channel, const QString &message,
|
||||
bool &sent);
|
||||
|
||||
std::mutex lastMessageMutex_;
|
||||
std::queue<std::chrono::steady_clock::time_point> lastMessagePleb_;
|
||||
std::queue<std::chrono::steady_clock::time_point> lastMessageMod_;
|
||||
std::chrono::steady_clock::time_point lastErrorTimeSpeed_;
|
||||
std::chrono::steady_clock::time_point lastErrorTimeAmount_;
|
||||
|
||||
TwitchBadges twitchBadges;
|
||||
BttvEmotes bttv;
|
||||
FfzEmotes ffz;
|
||||
|
||||
pajlada::Signals::SignalHolder signalHolder_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
#pragma once
|
||||
|
||||
#include "common/Atomic.hpp"
|
||||
#include "common/Channel.hpp"
|
||||
#include "common/Singleton.hpp"
|
||||
#include "pajlada/signals/signalholder.hpp"
|
||||
#include "providers/bttv/BttvEmotes.hpp"
|
||||
#include "providers/ffz/FfzEmotes.hpp"
|
||||
#include "providers/irc/AbstractIrcServer.hpp"
|
||||
#include "providers/twitch/TwitchBadges.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <queue>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class Settings;
|
||||
class Paths;
|
||||
class PubSub;
|
||||
class TwitchChannel;
|
||||
|
||||
class TwitchIrcServer final : public AbstractIrcServer, public Singleton
|
||||
{
|
||||
public:
|
||||
TwitchIrcServer();
|
||||
virtual ~TwitchIrcServer() override = default;
|
||||
|
||||
virtual void initialize(Settings &settings, Paths &paths) override;
|
||||
|
||||
void forEachChannelAndSpecialChannels(std::function<void(ChannelPtr)> func);
|
||||
|
||||
std::shared_ptr<Channel> getChannelOrEmptyByID(const QString &channelID);
|
||||
|
||||
Atomic<QString> lastUserThatWhisperedMe;
|
||||
|
||||
const ChannelPtr whispersChannel;
|
||||
const ChannelPtr mentionsChannel;
|
||||
IndirectChannel watchingChannel;
|
||||
|
||||
PubSub *pubsub;
|
||||
|
||||
const BttvEmotes &getBttvEmotes() const;
|
||||
const FfzEmotes &getFfzEmotes() const;
|
||||
|
||||
protected:
|
||||
virtual void initializeConnection(IrcConnection *connection,
|
||||
ConnectionType type) override;
|
||||
virtual std::shared_ptr<Channel> createChannel(
|
||||
const QString &channelName) override;
|
||||
|
||||
virtual void privateMessageReceived(
|
||||
Communi::IrcPrivateMessage *message) override;
|
||||
virtual void readConnectionMessageReceived(
|
||||
Communi::IrcMessage *message) override;
|
||||
virtual void writeConnectionMessageReceived(
|
||||
Communi::IrcMessage *message) override;
|
||||
|
||||
virtual void onReadConnected(IrcConnection *connection) override;
|
||||
virtual void onWriteConnected(IrcConnection *connection) override;
|
||||
|
||||
virtual std::shared_ptr<Channel> getCustomChannel(
|
||||
const QString &channelname) override;
|
||||
|
||||
virtual QString cleanChannelName(const QString &dirtyChannelName) override;
|
||||
virtual bool hasSeparateWriteConnection() const override;
|
||||
|
||||
private:
|
||||
void onMessageSendRequested(TwitchChannel *channel, const QString &message,
|
||||
bool &sent);
|
||||
|
||||
std::mutex lastMessageMutex_;
|
||||
std::queue<std::chrono::steady_clock::time_point> lastMessagePleb_;
|
||||
std::queue<std::chrono::steady_clock::time_point> lastMessageMod_;
|
||||
std::chrono::steady_clock::time_point lastErrorTimeSpeed_;
|
||||
std::chrono::steady_clock::time_point lastErrorTimeAmount_;
|
||||
|
||||
TwitchBadges twitchBadges;
|
||||
BttvEmotes bttv;
|
||||
FfzEmotes ffz;
|
||||
|
||||
pajlada::Signals::SignalHolder signalHolder_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -143,9 +143,21 @@ bool TwitchMessageBuilder::isIgnored() const
|
||||
return false;
|
||||
}
|
||||
|
||||
inline QMediaPlayer *getPlayer()
|
||||
{
|
||||
if (isGuiThread())
|
||||
{
|
||||
static auto player = new QMediaPlayer;
|
||||
return player;
|
||||
}
|
||||
else
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchMessageBuilder::triggerHighlights()
|
||||
{
|
||||
static auto player = new QMediaPlayer;
|
||||
static QUrl currentPlayerUrl;
|
||||
|
||||
if (this->historicalMessage_)
|
||||
@@ -165,21 +177,24 @@ void TwitchMessageBuilder::triggerHighlights()
|
||||
|
||||
if (this->highlightSound_ && resolveFocus)
|
||||
{
|
||||
// update the media player url if necessary
|
||||
QUrl highlightSoundUrl =
|
||||
getSettings()->customHighlightSound
|
||||
? QUrl::fromLocalFile(
|
||||
getSettings()->pathHighlightSound.getValue())
|
||||
: QUrl("qrc:/sounds/ping2.wav");
|
||||
|
||||
if (currentPlayerUrl != highlightSoundUrl)
|
||||
if (auto player = getPlayer())
|
||||
{
|
||||
player->setMedia(highlightSoundUrl);
|
||||
// update the media player url if necessary
|
||||
QUrl highlightSoundUrl =
|
||||
getSettings()->customHighlightSound
|
||||
? QUrl::fromLocalFile(
|
||||
getSettings()->pathHighlightSound.getValue())
|
||||
: QUrl("qrc:/sounds/ping2.wav");
|
||||
|
||||
currentPlayerUrl = highlightSoundUrl;
|
||||
if (currentPlayerUrl != highlightSoundUrl)
|
||||
{
|
||||
player->setMedia(highlightSoundUrl);
|
||||
|
||||
currentPlayerUrl = highlightSoundUrl;
|
||||
}
|
||||
|
||||
player->play();
|
||||
}
|
||||
|
||||
player->play();
|
||||
}
|
||||
|
||||
if (this->highlightAlert_)
|
||||
@@ -274,7 +289,7 @@ MessagePtr TwitchMessageBuilder::build()
|
||||
if (iterator != this->tags.end())
|
||||
{
|
||||
this->hasBits_ = true;
|
||||
// bits = iterator.value().toString();
|
||||
this->bits = iterator.value().toString();
|
||||
}
|
||||
|
||||
// twitch emotes
|
||||
@@ -340,7 +355,7 @@ void TwitchMessageBuilder::addWords(
|
||||
auto i = int();
|
||||
auto currentTwitchEmote = twitchEmotes.begin();
|
||||
|
||||
for (const auto &word : words)
|
||||
for (auto word : words)
|
||||
{
|
||||
// check if it's a twitch emote twitch emote
|
||||
while (currentTwitchEmote != twitchEmotes.end() &&
|
||||
@@ -361,9 +376,19 @@ void TwitchMessageBuilder::addWords(
|
||||
MessageElementFlag::TwitchEmote);
|
||||
|
||||
i += word.length() + 1;
|
||||
|
||||
int len = std::get<2>(*currentTwitchEmote).string.length();
|
||||
currentTwitchEmote++;
|
||||
|
||||
continue;
|
||||
if (len < word.length())
|
||||
{
|
||||
word = word.mid(len);
|
||||
this->message().elements.back()->setTrailingSpace(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// split words
|
||||
@@ -952,7 +977,7 @@ void TwitchMessageBuilder::parseHighlights()
|
||||
{
|
||||
HighlightPhrase selfHighlight(
|
||||
currentUsername, getSettings()->enableSelfHighlightTaskbar,
|
||||
getSettings()->enableSelfHighlightSound, false);
|
||||
getSettings()->enableSelfHighlightSound, false, false);
|
||||
activeHighlights.emplace_back(std::move(selfHighlight));
|
||||
}
|
||||
|
||||
@@ -1171,7 +1196,7 @@ void TwitchMessageBuilder::appendTwitchBadges()
|
||||
{
|
||||
if (auto customModBadge = this->twitchChannel->ffzCustomModBadge())
|
||||
{
|
||||
this->emplace<BadgeElement>(
|
||||
this->emplace<ModBadgeElement>(
|
||||
customModBadge.get(),
|
||||
MessageElementFlag::BadgeChannelAuthority)
|
||||
->setTooltip((*customModBadge)->tooltip.string);
|
||||
@@ -1285,56 +1310,29 @@ void TwitchMessageBuilder::appendChatterinoBadges()
|
||||
}
|
||||
}
|
||||
|
||||
Outcome TwitchMessageBuilder::tryParseCheermote(const QString & /*string*/)
|
||||
Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string)
|
||||
{
|
||||
// auto app = getApp();
|
||||
//// Try to parse custom cheermotes
|
||||
// const auto &channelResources = app->resources->channels[this->roomID_];
|
||||
// if (channelResources.loaded) {
|
||||
// for (const auto &cheermoteSet : channelResources.cheermoteSets) {
|
||||
// auto match = cheermoteSet.regex.match(string);
|
||||
// if (!match.hasMatch()) {
|
||||
// continue;
|
||||
// }
|
||||
// QString amount = match.captured(1);
|
||||
// bool ok = false;
|
||||
// int numBits = amount.toInt(&ok);
|
||||
// if (!ok) {
|
||||
// Log("Error parsing bit amount in tryParseCheermote");
|
||||
// return Failure;
|
||||
// }
|
||||
|
||||
// auto savedIt = cheermoteSet.cheermotes.end();
|
||||
|
||||
// // Fetch cheermote that matches our numBits
|
||||
// for (auto it = cheermoteSet.cheermotes.begin(); it !=
|
||||
// cheermoteSet.cheermotes.end();
|
||||
// ++it) {
|
||||
// if (numBits >= it->minBits) {
|
||||
// savedIt = it;
|
||||
// } else {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (savedIt == cheermoteSet.cheermotes.end()) {
|
||||
// Log("Error getting a cheermote from a cheermote set for the
|
||||
// bit amount {}",
|
||||
// numBits);
|
||||
// return Failure;
|
||||
// }
|
||||
|
||||
// const auto &cheermote = *savedIt;
|
||||
|
||||
// this->emplace<EmoteElement>(cheermote.animatedEmote,
|
||||
// MessageElementFlag::BitsAnimated);
|
||||
// this->emplace<TextElement>(amount, MessageElementFlag::Text,
|
||||
// cheermote.color);
|
||||
|
||||
// return Success;
|
||||
// }
|
||||
//}
|
||||
|
||||
return Failure;
|
||||
auto cheerOpt = this->twitchChannel->cheerEmote(string);
|
||||
if (!cheerOpt)
|
||||
{
|
||||
return Failure;
|
||||
}
|
||||
auto &cheerEmote = *cheerOpt;
|
||||
if (cheerEmote.staticEmote)
|
||||
{
|
||||
this->emplace<EmoteElement>(cheerEmote.staticEmote,
|
||||
MessageElementFlag::BitsStatic);
|
||||
}
|
||||
if (cheerEmote.animatedEmote)
|
||||
{
|
||||
this->emplace<EmoteElement>(cheerEmote.animatedEmote,
|
||||
MessageElementFlag::BitsAnimated);
|
||||
}
|
||||
if (cheerEmote.color != QColor())
|
||||
{
|
||||
this->emplace<TextElement>(this->bits, MessageElementFlag::BitsAmount,
|
||||
cheerEmote.color);
|
||||
}
|
||||
return Success;
|
||||
}
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -78,6 +78,7 @@ private:
|
||||
|
||||
QString roomID_;
|
||||
bool hasBits_ = false;
|
||||
QString bits;
|
||||
bool historicalMessage_ = false;
|
||||
|
||||
QString userId_;
|
||||
|
||||
Reference in New Issue
Block a user