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

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

777 lines
22 KiB
C++
Raw Normal View History

#include "providers/twitch/TwitchIrcServer.hpp"
2018-06-26 14:09:39 +02:00
#include "Application.hpp"
#include "common/Channel.hpp"
#include "common/Env.hpp"
#include "common/QLogging.hpp"
2018-06-26 14:09:39 +02:00
#include "controllers/accounts/AccountController.hpp"
2018-11-03 21:26:57 +01:00
#include "messages/Message.hpp"
#include "messages/MessageBuilder.hpp"
#include "providers/bttv/BttvEmotes.hpp"
#include "providers/bttv/BttvLiveUpdates.hpp"
#include "providers/ffz/FfzEmotes.hpp"
#include "providers/seventv/eventapi/Subscription.hpp"
#include "providers/seventv/SeventvEmotes.hpp"
#include "providers/seventv/SeventvEventAPI.hpp"
#include "providers/twitch/api/Helix.hpp"
#include "providers/twitch/ChannelPointReward.hpp"
2018-06-26 14:09:39 +02:00
#include "providers/twitch/IrcMessageHandler.hpp"
#include "providers/twitch/TwitchAccount.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "singletons/Settings.hpp"
#include "util/Helpers.hpp"
2018-06-26 14:09:39 +02:00
#include "util/PostToThread.hpp"
#include <IrcCommand>
#include <QMetaEnum>
#include <cassert>
2018-06-24 12:59:47 +02:00
using namespace std::chrono_literals;
namespace {
using namespace chatterino;
const QString BTTV_LIVE_UPDATES_URL = "wss://sockets.betterttv.net/ws";
const QString SEVENTV_EVENTAPI_URL = "wss://events.7tv.io/v3";
void sendHelixMessage(const std::shared_ptr<TwitchChannel> &channel,
const QString &message, const QString &replyParentId = {})
{
auto broadcasterID = channel->roomId();
if (broadcasterID.isEmpty())
{
channel->addMessage(makeSystemMessage(
"Sending messages in this channel isn't possible."));
return;
}
getHelix()->sendChatMessage(
{
.broadcasterID = broadcasterID,
.senderID =
getIApp()->getAccounts()->twitch.getCurrent()->getUserId(),
.message = message,
.replyParentMessageID = replyParentId,
},
[weak = std::weak_ptr(channel)](const auto &res) {
auto chan = weak.lock();
if (!chan)
{
return;
}
if (res.isSent)
{
return;
}
auto errorMessage = [&] {
if (res.dropReason)
{
return makeSystemMessage(res.dropReason->message);
}
return makeSystemMessage("Your message was not sent.");
}();
chan->addMessage(errorMessage);
},
[weak = std::weak_ptr(channel)](auto error, auto message) {
auto chan = weak.lock();
if (!chan)
{
return;
}
if (message.isEmpty())
{
message = "(empty message)";
}
using Error = decltype(error);
auto errorMessage = [&]() -> QString {
switch (error)
{
case Error::MissingText:
return "You can't send an empty message.";
case Error::BadRequest:
return "Failed to send message: " + message;
case Error::Forbidden:
return "You are not allowed to send messages in this "
"channel.";
case Error::MessageTooLarge:
return "Your message was too long.";
case Error::UserMissingScope:
return "Missing required scope. Re-login with your "
"account and try again.";
case Error::Forwarded:
return message;
case Error::Unknown:
default:
return "Unknown error: " + message;
}
}();
chan->addMessage(makeSystemMessage(errorMessage));
});
}
/// Returns true if chat messages should be sent over Helix
bool shouldSendHelixChat()
{
switch (getSettings()->chatSendProtocol)
{
case ChatSendProtocol::Helix:
return true;
case ChatSendProtocol::Default:
case ChatSendProtocol::IRC:
return false;
default:
assert(false && "Invalid chat protocol value");
return false;
}
}
} // namespace
2018-02-05 15:11:50 +01:00
namespace chatterino {
2019-09-18 13:03:16 +02:00
TwitchIrcServer::TwitchIrcServer()
2018-07-06 17:30:12 +02:00
: whispersChannel(new Channel("/whispers", Channel::Type::TwitchWhispers))
, mentionsChannel(new Channel("/mentions", Channel::Type::TwitchMentions))
, liveChannel(new Channel("/live", Channel::Type::TwitchLive))
, automodChannel(new Channel("/automod", Channel::Type::TwitchAutomod))
, watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching)
2018-02-05 15:11:50 +01:00
{
this->initializeIrc();
if (getSettings()->enableBTTVLiveUpdates &&
getSettings()->enableBTTVChannelEmotes)
{
this->bttvLiveUpdates =
std::make_unique<BttvLiveUpdates>(BTTV_LIVE_UPDATES_URL);
}
if (getSettings()->enableSevenTVEventAPI &&
getSettings()->enableSevenTVChannelEmotes)
{
this->seventvEventAPI =
std::make_unique<SeventvEventAPI>(SEVENTV_EVENTAPI_URL);
}
2018-08-02 14:23:27 +02:00
// getSettings()->twitchSeperateWriteConnection.connect([this](auto, auto) {
// this->connect(); },
// this->signalHolder_,
// false);
2018-02-05 15:11:50 +01:00
}
void TwitchIrcServer::initialize(Settings &settings, const Paths &paths)
2018-02-05 15:11:50 +01:00
{
getIApp()->getAccounts()->twitch.currentUserChanged.connect([this]() {
postToThread([this] {
this->connect();
});
});
this->reloadBTTVGlobalEmotes();
this->reloadFFZGlobalEmotes();
this->reloadSevenTVGlobalEmotes();
2018-02-05 15:11:50 +01:00
}
2019-09-18 13:03:16 +02:00
void TwitchIrcServer::initializeConnection(IrcConnection *connection,
ConnectionType type)
2018-02-05 15:11:50 +01:00
{
2018-05-26 20:26:25 +02:00
std::shared_ptr<TwitchAccount> account =
getIApp()->getAccounts()->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);
2018-02-05 15:11:50 +01:00
QString username = account->getUserName();
QString oauthToken = account->getOAuthToken();
2018-02-05 15:11:50 +01:00
if (!oauthToken.startsWith("oauth:"))
{
oauthToken.prepend("oauth:");
}
2018-02-05 15:11:50 +01:00
connection->setUserName(username);
connection->setNickName(username);
connection->setRealName(username);
2018-02-05 15:11:50 +01:00
if (!account->isAnon())
{
connection->setPassword(oauthToken);
}
// https://dev.twitch.tv/docs/irc#connecting-to-the-twitch-irc-server
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);
2018-02-05 15:11:50 +01:00
}
2019-09-18 13:03:16 +02:00
std::shared_ptr<Channel> TwitchIrcServer::createChannel(
const QString &channelName)
2018-02-05 15:11:50 +01:00
{
auto channel = std::make_shared<TwitchChannel>(channelName);
2018-08-13 13:54:39 +02:00
channel->initialize();
// We can safely ignore these signal connections since the TwitchIrcServer is only
// ever destroyed when the full Application state is about to be destroyed, at which point
// no Channel's should live
// NOTE: CHANNEL_LIFETIME
std::ignore = channel->sendMessageSignal.connect(
[this, channel = std::weak_ptr(channel)](auto &chan, auto &msg,
bool &sent) {
auto c = channel.lock();
if (!c)
{
return;
}
this->onMessageSendRequested(c, msg, sent);
2018-06-06 18:57:22 +02:00
});
std::ignore = channel->sendReplySignal.connect(
[this, channel = std::weak_ptr(channel)](auto &chan, auto &msg,
auto &replyId, bool &sent) {
auto c = channel.lock();
if (!c)
{
return;
}
this->onReplySendRequested(c, msg, replyId, sent);
});
return channel;
2018-02-05 15:11:50 +01:00
}
2019-09-18 13:03:16 +02:00
void TwitchIrcServer::privateMessageReceived(
Communi::IrcPrivateMessage *message)
2018-02-05 15:11:50 +01:00
{
IrcMessageHandler::instance().handlePrivMessage(message, *this);
2018-02-05 15:11:50 +01:00
}
2019-09-18 13:03:16 +02:00
void TwitchIrcServer::readConnectionMessageReceived(
Communi::IrcMessage *message)
2018-02-05 15:11:50 +01:00
{
2019-09-18 13:03:16 +02:00
AbstractIrcServer::readConnectionMessageReceived(message);
2018-06-04 21:05:18 +02:00
if (message->type() == Communi::IrcMessage::Type::Private)
{
2018-02-05 15:11:50 +01:00
// We already have a handler for private messages
return;
}
2018-02-05 15:11:50 +01:00
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")
2018-02-05 15:11:50 +01:00
{
handler.handleJoinMessage(message);
2018-02-05 15:11:50 +01:00
}
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->markChannelsConnected();
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")
2018-02-05 15:11:50 +01:00
{
// Received USERSTATE upon sending PRIVMSG messages
handler.handleUserStateMessage(message);
2018-02-05 15:11:50 +01:00
}
else if (command == "NOTICE")
{
// List of expected NOTICE messages on write connection
// https://git.kotmisia.pl/Mm2PL/docs/src/branch/master/irc_msg_ids.md#command-results
handler.handleNoticeMessage(
static_cast<Communi::IrcNoticeMessage *>(message));
}
else if (command == "RECONNECT")
{
this->addGlobalSystemMessage(
"Twitch Servers requested us to reconnect, reconnecting");
this->connect();
}
2018-02-05 15:11:50 +01:00
}
2019-09-18 13:03:16 +02:00
std::shared_ptr<Channel> TwitchIrcServer::getCustomChannel(
2018-02-05 15:11:50 +01:00
const QString &channelName)
{
if (channelName == "/whispers")
{
return this->whispersChannel;
2018-02-05 15:11:50 +01:00
}
2018-02-05 15:11:50 +01:00
if (channelName == "/mentions")
{
return this->mentionsChannel;
2018-02-05 15:11:50 +01:00
}
if (channelName == "/live")
{
return this->liveChannel;
}
if (channelName == "/automod")
{
return this->automodChannel;
}
static auto getTimer = [](ChannelPtr channel, int msBetweenMessages,
bool addInitialMessages) {
if (addInitialMessages)
{
for (auto i = 0; i < 1000; i++)
{
channel->addMessage(makeSystemMessage(QString::number(i + 1)));
}
}
auto *timer = new QTimer;
QObject::connect(timer, &QTimer::timeout, [channel] {
channel->addMessage(
makeSystemMessage(QTime::currentTime().toString()));
});
timer->start(msBetweenMessages);
return timer;
};
if (channelName == "$$$")
{
static auto channel = std::make_shared<Channel>(
channelName, chatterino::Channel::Type::Misc);
getTimer(channel, 500, true);
return channel;
}
if (channelName == "$$$:e")
{
static auto channel = std::make_shared<Channel>(
channelName, chatterino::Channel::Type::Misc);
getTimer(channel, 500, false);
return channel;
}
if (channelName == "$$$$")
{
static auto channel = std::make_shared<Channel>(
channelName, chatterino::Channel::Type::Misc);
getTimer(channel, 250, true);
return channel;
}
if (channelName == "$$$$:e")
{
static auto channel = std::make_shared<Channel>(
channelName, chatterino::Channel::Type::Misc);
getTimer(channel, 250, false);
return channel;
}
if (channelName == "$$$$$")
{
static auto channel = std::make_shared<Channel>(
channelName, chatterino::Channel::Type::Misc);
getTimer(channel, 100, true);
return channel;
}
if (channelName == "$$$$$:e")
{
static auto channel = std::make_shared<Channel>(
channelName, chatterino::Channel::Type::Misc);
getTimer(channel, 100, false);
return channel;
}
if (channelName == "$$$$$$")
{
static auto channel = std::make_shared<Channel>(
channelName, chatterino::Channel::Type::Misc);
getTimer(channel, 50, true);
return channel;
}
if (channelName == "$$$$$$:e")
{
static auto channel = std::make_shared<Channel>(
channelName, chatterino::Channel::Type::Misc);
getTimer(channel, 50, false);
return channel;
}
if (channelName == "$$$$$$$")
{
static auto channel = std::make_shared<Channel>(
channelName, chatterino::Channel::Type::Misc);
getTimer(channel, 25, true);
return channel;
}
if (channelName == "$$$$$$$:e")
{
static auto channel = std::make_shared<Channel>(
channelName, chatterino::Channel::Type::Misc);
getTimer(channel, 25, false);
2018-11-03 21:26:57 +01:00
return channel;
}
2018-02-05 15:11:50 +01:00
return nullptr;
}
2019-09-18 13:03:16 +02:00
void TwitchIrcServer::forEachChannelAndSpecialChannels(
std::function<void(ChannelPtr)> func)
{
2018-06-22 23:24:45 +02:00
this->forEachChannel(func);
func(this->whispersChannel);
func(this->mentionsChannel);
func(this->liveChannel);
func(this->automodChannel);
}
2019-09-18 13:03:16 +02:00
std::shared_ptr<Channel> TwitchIrcServer::getChannelOrEmptyByID(
2018-07-15 20:28:54 +02:00
const QString &channelId)
{
2018-06-22 23:28:20 +02:00
std::lock_guard<std::mutex> lock(this->channelMutex);
2018-06-22 23:28:20 +02:00
for (const auto &weakChannel : this->channels)
{
auto channel = weakChannel.lock();
2018-07-14 14:24:18 +02:00
if (!channel)
{
2018-07-14 14:24:18 +02:00
continue;
}
2018-06-22 23:28:20 +02:00
auto twitchChannel = std::dynamic_pointer_cast<TwitchChannel>(channel);
2018-07-14 14:24:18 +02:00
if (!twitchChannel)
{
2018-07-14 14:24:18 +02:00
continue;
}
if (twitchChannel->roomId() == channelId &&
2023-02-19 20:19:18 +01:00
twitchChannel->getName().count(':') < 2)
2018-08-11 17:15:17 +02:00
{
2018-06-22 23:28:20 +02:00
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('#'))
{
2019-09-18 13:03:16 +02:00
return dirtyChannelName.mid(1).toLower();
}
2019-09-18 13:03:16 +02:00
else
{
2019-09-18 13:03:16 +02:00
return dirtyChannelName.toLower();
}
}
2019-09-18 13:03:16 +02:00
bool TwitchIrcServer::hasSeparateWriteConnection() const
{
return true;
// return getSettings()->twitchSeperateWriteConnection;
}
bool TwitchIrcServer::prepareToSend(
const std::shared_ptr<TwitchChannel> &channel)
2018-06-24 12:59:47 +02:00
{
std::lock_guard<std::mutex> guard(this->lastMessageMutex_);
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)
2018-06-24 12:59:47 +02:00
{
auto errorMessage =
makeSystemMessage("You are sending messages too quickly.");
channel->addMessage(errorMessage);
this->lastErrorTimeSpeed_ = now;
2018-06-24 12:59:47 +02:00
}
return false;
}
// 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)
2018-06-24 12:59:47 +02:00
{
auto errorMessage =
makeSystemMessage("You are sending too many messages.");
channel->addMessage(errorMessage);
this->lastErrorTimeAmount_ = now;
2018-06-24 12:59:47 +02:00
}
return false;
}
lastMessage.push(now);
return true;
}
void TwitchIrcServer::onMessageSendRequested(
const std::shared_ptr<TwitchChannel> &channel, const QString &message,
bool &sent)
{
sent = false;
bool canSend = this->prepareToSend(channel);
if (!canSend)
{
return;
2018-06-24 12:59:47 +02:00
}
if (shouldSendHelixChat())
{
sendHelixMessage(channel, message);
}
else
{
this->sendMessage(channel->getName(), message);
}
2018-06-24 12:59:47 +02:00
sent = true;
}
void TwitchIrcServer::onReplySendRequested(
const std::shared_ptr<TwitchChannel> &channel, const QString &message,
const QString &replyId, bool &sent)
{
sent = false;
bool canSend = this->prepareToSend(channel);
if (!canSend)
{
return;
}
if (shouldSendHelixChat())
{
sendHelixMessage(channel, message, replyId);
}
else
{
this->sendRawMessage("@reply-parent-msg-id=" + replyId + " PRIVMSG #" +
channel->getName() + " :" + message);
}
sent = true;
}
const IndirectChannel &TwitchIrcServer::getWatchingChannel() const
{
return this->watchingChannel;
}
QString TwitchIrcServer::getLastUserThatWhisperedMe() const
{
return this->lastUserThatWhisperedMe.get();
}
void TwitchIrcServer::reloadBTTVGlobalEmotes()
{
getIApp()->getBttvEmotes()->loadEmotes();
}
void TwitchIrcServer::reloadAllBTTVChannelEmotes()
{
this->forEachChannel([](const auto &chan) {
if (auto *channel = dynamic_cast<TwitchChannel *>(chan.get()))
{
channel->refreshBTTVChannelEmotes(false);
}
});
}
void TwitchIrcServer::reloadFFZGlobalEmotes()
{
getIApp()->getFfzEmotes()->loadEmotes();
}
void TwitchIrcServer::reloadAllFFZChannelEmotes()
{
this->forEachChannel([](const auto &chan) {
if (auto *channel = dynamic_cast<TwitchChannel *>(chan.get()))
{
channel->refreshFFZChannelEmotes(false);
}
});
}
void TwitchIrcServer::reloadSevenTVGlobalEmotes()
{
getIApp()->getSeventvEmotes()->loadGlobalEmotes();
}
void TwitchIrcServer::reloadAllSevenTVChannelEmotes()
{
this->forEachChannel([](const auto &chan) {
if (auto *channel = dynamic_cast<TwitchChannel *>(chan.get()))
{
channel->refreshSevenTVChannelEmotes(false);
}
});
}
void TwitchIrcServer::forEachSeventvEmoteSet(
const QString &emoteSetId, std::function<void(TwitchChannel &)> func)
{
this->forEachChannel([emoteSetId, func](const auto &chan) {
if (auto *channel = dynamic_cast<TwitchChannel *>(chan.get());
channel->seventvEmoteSetID() == emoteSetId)
{
func(*channel);
}
});
}
void TwitchIrcServer::forEachSeventvUser(
const QString &userId, std::function<void(TwitchChannel &)> func)
{
this->forEachChannel([userId, func](const auto &chan) {
if (auto *channel = dynamic_cast<TwitchChannel *>(chan.get());
channel->seventvUserID() == userId)
{
func(*channel);
}
});
}
void TwitchIrcServer::dropSeventvChannel(const QString &userID,
const QString &emoteSetID)
{
if (!this->seventvEventAPI)
{
return;
}
std::lock_guard<std::mutex> lock(this->channelMutex);
// ignore empty values
bool skipUser = userID.isEmpty();
bool skipSet = emoteSetID.isEmpty();
bool foundUser = skipUser;
bool foundSet = skipSet;
for (std::weak_ptr<Channel> &weak : this->channels)
{
ChannelPtr chan = weak.lock();
if (!chan)
{
continue;
}
auto *channel = dynamic_cast<TwitchChannel *>(chan.get());
if (!foundSet && channel->seventvEmoteSetID() == emoteSetID)
{
foundSet = true;
}
if (!foundUser && channel->seventvUserID() == userID)
{
foundUser = true;
}
if (foundSet && foundUser)
{
break;
}
}
if (!foundUser)
{
this->seventvEventAPI->unsubscribeUser(userID);
}
if (!foundSet)
{
this->seventvEventAPI->unsubscribeEmoteSet(emoteSetID);
}
}
2018-02-05 15:11:50 +01:00
} // namespace chatterino