mirror-chatterino2/src/providers/twitch/TwitchIrcServer.cpp

401 lines
11 KiB
C++
Raw Normal View History

2019-09-18 13:03:16 +02:00
#include "TwitchIrcServer.hpp"
#include <IrcCommand>
#include <cassert>
#include "Application.hpp"
#include "common/Common.hpp"
#include "common/Env.hpp"
#include "common/QLogging.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "messages/Message.hpp"
#include "messages/MessageBuilder.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 "util/PostToThread.hpp"
#include <QMetaEnum>
// using namespace Communi;
using namespace std::chrono_literals;
namespace chatterino {
2019-09-18 13:03:16 +02:00
TwitchIrcServer::TwitchIrcServer()
: whispersChannel(new Channel("/whispers", Channel::Type::TwitchWhispers))
, mentionsChannel(new Channel("/mentions", Channel::Type::TwitchMentions))
, watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching)
, liveChannel(new Channel("/live", Channel::Type::TwitchLive))
{
this->initializeIrc();
this->pubsub = new PubSub;
// getSettings()->twitchSeperateWriteConnection.connect([this](auto, auto) {
// this->connect(); },
// this->signalHolder_,
// false);
}
2019-09-18 13:03:16 +02:00
void TwitchIrcServer::initialize(Settings &settings, Paths &paths)
{
getApp()->accounts->twitch.currentUserChanged.connect([this]() {
postToThread([this] {
this->connect();
});
});
this->bttv.loadEmotes();
this->ffz.loadEmotes();
}
2019-09-18 13:03:16 +02:00
void TwitchIrcServer::initializeConnection(IrcConnection *connection,
ConnectionType type)
{
std::shared_ptr<TwitchAccount> account =
getApp()->accounts->twitch.getCurrent();
qCDebug(chatterinoTwitch) << "logging in as" << account->getUserName();
// 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
// twitch.tv/membership enables the JOIN/PART/NAMES commands. See https://dev.twitch.tv/docs/irc/membership
// This is enabled so we receive USERSTATE messages when joining channels / typing messages, along with the other command capabilities
QStringList caps{"twitch.tv/tags", "twitch.tv/commands"};
if (type != ConnectionType::Write)
{
caps.push_back("twitch.tv/membership");
}
connection->network()->setSkipCapabilityValidation(true);
connection->network()->setRequestedCapabilities(caps);
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);
}
// https://dev.twitch.tv/docs/irc/guide/#connecting-to-twitch-irc
2019-10-04 13:06:15 +02:00
// SSL disabled: irc://irc.chat.twitch.tv:6667 (or port 80)
// SSL enabled: irc://irc.chat.twitch.tv:6697 (or port 443)
connection->setHost(Env::get().twitchServerHost);
connection->setPort(Env::get().twitchServerPort);
connection->setSecure(Env::get().twitchServerSecure);
2019-09-18 13:03:16 +02:00
this->open(type);
}
2019-09-18 13:03:16 +02:00
std::shared_ptr<Channel> TwitchIrcServer::createChannel(
const QString &channelName)
{
auto channel =
std::shared_ptr<TwitchChannel>(new TwitchChannel(channelName));
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);
}
2019-09-18 13:03:16 +02:00
void TwitchIrcServer::privateMessageReceived(
Communi::IrcPrivateMessage *message)
{
IrcMessageHandler::instance().handlePrivMessage(message, *this);
}
2019-09-18 13:03:16 +02:00
void TwitchIrcServer::readConnectionMessageReceived(
Communi::IrcMessage *message)
{
2019-09-18 13:03:16 +02:00
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::instance();
// Below commands enabled through the twitch.tv/membership CAP REQ
2021-07-17 15:09:21 +02:00
if (command == "JOIN")
{
handler.handleJoinMessage(message);
}
else if (command == "PART")
{
handler.handlePartMessage(message);
}
else if (command == "USERSTATE")
{
// Received USERSTATE upon JOINing a channel
handler.handleUserStateMessage(message);
}
else if (command == "ROOMSTATE")
{
// Received ROOMSTATE upon JOINing a channel
handler.handleRoomStateMessage(message);
}
2019-09-21 11:54:30 +02:00
else if (command == "CLEARCHAT")
{
handler.handleClearChatMessage(message);
}
2019-09-21 11:56:37 +02:00
else if (command == "CLEARMSG")
{
handler.handleClearMessageMessage(message);
}
else if (command == "USERNOTICE")
{
handler.handleUserNoticeMessage(message, *this);
}
else if (command == "NOTICE")
{
handler.handleNoticeMessage(
static_cast<Communi::IrcNoticeMessage *>(message));
}
else if (command == "WHISPER")
{
handler.handleWhisperMessage(message);
}
else if (command == "RECONNECT")
{
this->addGlobalSystemMessage(
"Twitch Servers requested us to reconnect, reconnecting");
this->connect();
}
else if (command == "GLOBALUSERSTATE")
{
handler.handleGlobalUserStateMessage(message);
}
}
2019-09-18 13:03:16 +02:00
void TwitchIrcServer::writeConnectionMessageReceived(
Communi::IrcMessage *message)
{
const QString &command = message->command();
auto &handler = IrcMessageHandler::instance();
// Below commands enabled through the twitch.tv/commands CAP REQ
if (command == "USERSTATE")
{
// Received USERSTATE upon PRIVMSGing
handler.handleUserStateMessage(message);
}
else if (command == "NOTICE")
{
static std::unordered_set<std::string> readConnectionOnlyIDs{
"host_on",
"host_off",
"host_target_went_offline",
"emote_only_on",
"emote_only_off",
"slow_on",
"slow_off",
"subs_on",
"subs_off",
"r9k_on",
"r9k_off",
// Display for user who times someone out. This implies you're a
// moderator, at which point you will be connected to PubSub and receive
// a better message from there.
"timeout_success",
"ban_success",
// Channel suspended notices
"msg_channel_suspended",
};
handler.handleNoticeMessage(
static_cast<Communi::IrcNoticeMessage *>(message));
}
else if (command == "RECONNECT")
{
this->addGlobalSystemMessage(
"Twitch Servers requested us to reconnect, reconnecting");
this->connect();
}
}
2019-09-18 13:03:16 +02:00
std::shared_ptr<Channel> TwitchIrcServer::getCustomChannel(
const QString &channelName)
{
if (channelName == "/whispers")
{
return this->whispersChannel;
}
if (channelName == "/mentions")
{
return this->mentionsChannel;
}
if (channelName == "/live")
{
return this->liveChannel;
}
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;
}
2019-09-18 13:03:16 +02:00
void TwitchIrcServer::forEachChannelAndSpecialChannels(
std::function<void(ChannelPtr)> func)
{
this->forEachChannel(func);
func(this->whispersChannel);
func(this->mentionsChannel);
func(this->liveChannel);
}
2019-09-18 13:03:16 +02:00
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();
}
2019-09-18 13:03:16 +02:00
QString TwitchIrcServer::cleanChannelName(const QString &dirtyChannelName)
{
2019-09-18 13:03:16 +02:00
if (dirtyChannelName.startsWith('#'))
return dirtyChannelName.mid(1).toLower();
else
return dirtyChannelName.toLower();
}
2019-09-18 13:03:16 +02:00
bool TwitchIrcServer::hasSeparateWriteConnection() const
{
return true;
// return getSettings()->twitchSeperateWriteConnection;
}
2019-09-18 13:03:16 +02:00
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("You are sending messages too quickly.");
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("You are sending too many messages.");
channel->addMessage(errorMessage);
this->lastErrorTimeAmount_ = now;
}
return;
}
lastMessage.push(now);
}
this->sendMessage(channel->getName(), message);
sent = true;
}
2019-09-18 13:03:16 +02:00
const BttvEmotes &TwitchIrcServer::getBttvEmotes() const
{
return this->bttv;
}
2019-09-18 13:03:16 +02:00
const FfzEmotes &TwitchIrcServer::getFfzEmotes() const
{
return this->ffz;
}
} // namespace chatterino