2019-09-18 13:03:16 +02:00
|
|
|
#include "TwitchIrcServer.hpp"
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2019-09-22 16:16:08 +02:00
|
|
|
#include <IrcCommand>
|
|
|
|
#include <cassert>
|
|
|
|
|
2019-09-08 22:27:57 +02:00
|
|
|
#include "Application.hpp"
|
|
|
|
#include "common/Common.hpp"
|
2019-12-21 10:11:23 +01:00
|
|
|
#include "common/Env.hpp"
|
2020-11-21 16:20:10 +01:00
|
|
|
#include "common/QLogging.hpp"
|
2019-09-08 22:27:57 +02:00
|
|
|
#include "controllers/accounts/AccountController.hpp"
|
|
|
|
#include "messages/Message.hpp"
|
|
|
|
#include "messages/MessageBuilder.hpp"
|
|
|
|
#include "providers/twitch/IrcMessageHandler.hpp"
|
2022-05-07 17:22:39 +02:00
|
|
|
#include "providers/twitch/PubSubManager.hpp"
|
2019-09-08 22:27:57 +02:00
|
|
|
#include "providers/twitch/TwitchAccount.hpp"
|
|
|
|
#include "providers/twitch/TwitchChannel.hpp"
|
|
|
|
#include "providers/twitch/TwitchHelpers.hpp"
|
2022-06-17 20:52:20 +02:00
|
|
|
#include "util/Helpers.hpp"
|
2019-09-08 22:27:57 +02:00
|
|
|
#include "util/PostToThread.hpp"
|
|
|
|
|
2021-05-08 15:57:00 +02:00
|
|
|
#include <QMetaEnum>
|
|
|
|
|
2019-09-08 22:27:57 +02:00
|
|
|
// using namespace Communi;
|
|
|
|
using namespace std::chrono_literals;
|
|
|
|
|
2022-05-07 17:22:39 +02:00
|
|
|
#define TWITCH_PUBSUB_URL "wss://pubsub-edge.twitch.tv"
|
|
|
|
|
2019-09-08 22:27:57 +02:00
|
|
|
namespace chatterino {
|
|
|
|
|
2019-09-18 13:03:16 +02:00
|
|
|
TwitchIrcServer::TwitchIrcServer()
|
2019-09-08 22:27:57 +02:00
|
|
|
: whispersChannel(new Channel("/whispers", Channel::Type::TwitchWhispers))
|
|
|
|
, mentionsChannel(new Channel("/mentions", Channel::Type::TwitchMentions))
|
|
|
|
, watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching)
|
2021-05-09 16:17:04 +02:00
|
|
|
, liveChannel(new Channel("/live", Channel::Type::TwitchLive))
|
2019-09-08 22:27:57 +02:00
|
|
|
{
|
2020-03-01 12:05:08 +01:00
|
|
|
this->initializeIrc();
|
|
|
|
|
2022-05-07 17:22:39 +02:00
|
|
|
this->pubsub = new PubSub(TWITCH_PUBSUB_URL);
|
2019-09-08 22:27:57 +02:00
|
|
|
|
|
|
|
// 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)
|
2019-09-08 22:27:57 +02:00
|
|
|
{
|
2020-11-08 12:02:19 +01:00
|
|
|
getApp()->accounts->twitch.currentUserChanged.connect([this]() {
|
|
|
|
postToThread([this] {
|
|
|
|
this->connect();
|
2022-05-07 17:22:39 +02:00
|
|
|
this->pubsub->setAccount(getApp()->accounts->twitch.getCurrent());
|
2020-11-08 12:02:19 +01:00
|
|
|
});
|
|
|
|
});
|
2019-09-08 22:27:57 +02:00
|
|
|
|
|
|
|
this->bttv.loadEmotes();
|
|
|
|
this->ffz.loadEmotes();
|
2022-05-22 17:51:23 +02:00
|
|
|
|
|
|
|
/* Refresh all twitch channel's live status in bulk every 30 seconds after starting chatterino */
|
|
|
|
QObject::connect(&this->bulkLiveStatusTimer_, &QTimer::timeout, [=] {
|
|
|
|
this->bulkRefreshLiveStatus();
|
|
|
|
});
|
|
|
|
this->bulkLiveStatusTimer_.start(30 * 1000);
|
2019-09-08 22:27:57 +02:00
|
|
|
}
|
|
|
|
|
2019-09-18 13:03:16 +02:00
|
|
|
void TwitchIrcServer::initializeConnection(IrcConnection *connection,
|
|
|
|
ConnectionType type)
|
2019-09-08 22:27:57 +02:00
|
|
|
{
|
|
|
|
std::shared_ptr<TwitchAccount> account =
|
|
|
|
getApp()->accounts->twitch.getCurrent();
|
|
|
|
|
2020-11-21 16:20:10 +01:00
|
|
|
qCDebug(chatterinoTwitch) << "logging in as" << account->getUserName();
|
2019-09-08 22:27:57 +02:00
|
|
|
|
2021-07-17 17:18:17 +02:00
|
|
|
// 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);
|
|
|
|
|
2019-09-08 22:27:57 +02:00
|
|
|
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)
|
2019-12-21 10:11:23 +01:00
|
|
|
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-08 22:27:57 +02:00
|
|
|
}
|
|
|
|
|
2019-09-18 13:03:16 +02:00
|
|
|
std::shared_ptr<Channel> TwitchIrcServer::createChannel(
|
|
|
|
const QString &channelName)
|
2019-09-08 22:27:57 +02:00
|
|
|
{
|
2021-07-13 13:23:50 +02:00
|
|
|
auto channel =
|
|
|
|
std::shared_ptr<TwitchChannel>(new TwitchChannel(channelName));
|
2019-09-08 22:27:57 +02:00
|
|
|
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)
|
2019-09-08 22:27:57 +02:00
|
|
|
{
|
2019-10-07 22:42:34 +02:00
|
|
|
IrcMessageHandler::instance().handlePrivMessage(message, *this);
|
2019-09-08 22:27:57 +02:00
|
|
|
}
|
|
|
|
|
2019-09-18 13:03:16 +02:00
|
|
|
void TwitchIrcServer::readConnectionMessageReceived(
|
|
|
|
Communi::IrcMessage *message)
|
2019-09-08 22:27:57 +02:00
|
|
|
{
|
2019-09-18 13:03:16 +02:00
|
|
|
AbstractIrcServer::readConnectionMessageReceived(message);
|
|
|
|
|
2019-09-08 22:27:57 +02:00
|
|
|
if (message->type() == Communi::IrcMessage::Type::Private)
|
|
|
|
{
|
|
|
|
// We already have a handler for private messages
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const QString &command = message->command();
|
|
|
|
|
2019-10-07 22:42:34 +02:00
|
|
|
auto &handler = IrcMessageHandler::instance();
|
2019-09-08 22:27:57 +02:00
|
|
|
|
|
|
|
// Below commands enabled through the twitch.tv/membership CAP REQ
|
2021-07-17 15:09:21 +02:00
|
|
|
if (command == "JOIN")
|
2019-09-08 22:27:57 +02:00
|
|
|
{
|
|
|
|
handler.handleJoinMessage(message);
|
|
|
|
}
|
|
|
|
else if (command == "PART")
|
|
|
|
{
|
|
|
|
handler.handlePartMessage(message);
|
|
|
|
}
|
2019-09-21 11:38:08 +02:00
|
|
|
else if (command == "USERSTATE")
|
|
|
|
{
|
|
|
|
// Received USERSTATE upon JOINing a channel
|
|
|
|
handler.handleUserStateMessage(message);
|
|
|
|
}
|
2019-09-21 11:45:47 +02:00
|
|
|
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);
|
|
|
|
}
|
2021-01-09 18:05:02 +01:00
|
|
|
else if (command == "RECONNECT")
|
|
|
|
{
|
|
|
|
this->addGlobalSystemMessage(
|
|
|
|
"Twitch Servers requested us to reconnect, reconnecting");
|
|
|
|
this->connect();
|
|
|
|
}
|
2021-07-17 17:18:17 +02:00
|
|
|
else if (command == "GLOBALUSERSTATE")
|
|
|
|
{
|
|
|
|
handler.handleGlobalUserStateMessage(message);
|
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
}
|
|
|
|
|
2019-09-18 13:03:16 +02:00
|
|
|
void TwitchIrcServer::writeConnectionMessageReceived(
|
|
|
|
Communi::IrcMessage *message)
|
2019-09-08 22:27:57 +02:00
|
|
|
{
|
|
|
|
const QString &command = message->command();
|
|
|
|
|
2019-10-07 22:42:34 +02:00
|
|
|
auto &handler = IrcMessageHandler::instance();
|
2019-09-08 22:27:57 +02:00
|
|
|
// Below commands enabled through the twitch.tv/commands CAP REQ
|
|
|
|
if (command == "USERSTATE")
|
|
|
|
{
|
2021-08-03 17:55:04 +02:00
|
|
|
// Received USERSTATE upon sending PRIVMSG messages
|
2019-09-08 22:27:57 +02:00
|
|
|
handler.handleUserStateMessage(message);
|
|
|
|
}
|
2019-09-22 16:16:08 +02:00
|
|
|
else if (command == "NOTICE")
|
|
|
|
{
|
2021-08-03 17:55:04 +02:00
|
|
|
// List of expected NOTICE messages on write connection
|
|
|
|
// https://git.kotmisia.pl/Mm2PL/docs/src/branch/master/irc_msg_ids.md#command-results
|
2019-09-22 16:16:08 +02:00
|
|
|
handler.handleNoticeMessage(
|
|
|
|
static_cast<Communi::IrcNoticeMessage *>(message));
|
|
|
|
}
|
2021-01-09 18:05:02 +01:00
|
|
|
else if (command == "RECONNECT")
|
|
|
|
{
|
|
|
|
this->addGlobalSystemMessage(
|
|
|
|
"Twitch Servers requested us to reconnect, reconnecting");
|
|
|
|
this->connect();
|
|
|
|
}
|
2019-09-08 22:27:57 +02:00
|
|
|
}
|
|
|
|
|
2019-09-18 13:03:16 +02:00
|
|
|
std::shared_ptr<Channel> TwitchIrcServer::getCustomChannel(
|
2019-09-08 22:27:57 +02:00
|
|
|
const QString &channelName)
|
|
|
|
{
|
|
|
|
if (channelName == "/whispers")
|
|
|
|
{
|
|
|
|
return this->whispersChannel;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (channelName == "/mentions")
|
|
|
|
{
|
|
|
|
return this->mentionsChannel;
|
|
|
|
}
|
|
|
|
|
2021-05-09 16:17:04 +02:00
|
|
|
if (channelName == "/live")
|
|
|
|
{
|
|
|
|
return this->liveChannel;
|
|
|
|
}
|
|
|
|
|
2019-09-08 22:27:57 +02:00
|
|
|
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(
|
2019-09-08 22:27:57 +02:00
|
|
|
std::function<void(ChannelPtr)> func)
|
|
|
|
{
|
|
|
|
this->forEachChannel(func);
|
|
|
|
|
|
|
|
func(this->whispersChannel);
|
|
|
|
func(this->mentionsChannel);
|
2021-05-09 16:17:04 +02:00
|
|
|
func(this->liveChannel);
|
2019-09-08 22:27:57 +02:00
|
|
|
}
|
|
|
|
|
2019-09-18 13:03:16 +02:00
|
|
|
std::shared_ptr<Channel> TwitchIrcServer::getChannelOrEmptyByID(
|
2019-09-08 22:27:57 +02:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2022-05-28 20:10:10 +02:00
|
|
|
void TwitchIrcServer::bulkRefreshLiveStatus()
|
|
|
|
{
|
|
|
|
auto twitchChans = std::make_shared<QHash<QString, TwitchChannel *>>();
|
2022-05-22 17:51:23 +02:00
|
|
|
|
2022-05-28 20:10:10 +02:00
|
|
|
this->forEachChannel([twitchChans](ChannelPtr chan) {
|
|
|
|
auto tc = dynamic_cast<TwitchChannel *>(chan.get());
|
|
|
|
if (tc && !tc->roomId().isEmpty())
|
2022-05-22 17:51:23 +02:00
|
|
|
{
|
2022-05-28 20:10:10 +02:00
|
|
|
twitchChans->insert(tc->roomId(), tc);
|
2022-05-22 17:51:23 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2022-05-28 20:10:10 +02:00
|
|
|
// iterate over batches of channel IDs
|
2022-06-17 20:52:20 +02:00
|
|
|
for (const auto &batch : splitListIntoBatches(twitchChans->keys()))
|
2022-05-22 17:51:23 +02:00
|
|
|
{
|
|
|
|
getHelix()->fetchStreams(
|
2022-05-28 20:10:10 +02:00
|
|
|
batch, {},
|
|
|
|
[twitchChans](std::vector<HelixStream> streams) {
|
2022-05-22 17:51:23 +02:00
|
|
|
for (const auto &stream : streams)
|
|
|
|
{
|
2022-05-28 20:10:10 +02:00
|
|
|
// remaining channels will be used later to set their stream status as offline
|
|
|
|
// so we use take(id) to remove it
|
|
|
|
auto tc = twitchChans->take(stream.userId);
|
|
|
|
if (tc == nullptr)
|
|
|
|
{
|
2022-05-22 17:51:23 +02:00
|
|
|
continue;
|
2022-05-28 20:10:10 +02:00
|
|
|
}
|
2022-05-22 17:51:23 +02:00
|
|
|
|
2022-05-28 20:10:10 +02:00
|
|
|
tc->parseLiveStatus(true, stream);
|
2022-05-22 17:51:23 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
[]() {
|
|
|
|
// failure
|
2022-05-28 20:10:10 +02:00
|
|
|
},
|
|
|
|
[batch, twitchChans] {
|
|
|
|
// All the channels that were not present in fetchStreams response should be assumed to be offline
|
|
|
|
// It is necessary to update their stream status in case they've gone live -> offline
|
|
|
|
// Otherwise some of them will be marked as live forever
|
|
|
|
for (const auto &chID : batch)
|
|
|
|
{
|
|
|
|
auto tc = twitchChans->value(chID);
|
|
|
|
// early out in case channel does not exist anymore
|
|
|
|
if (tc == nullptr)
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
tc->parseLiveStatus(false, {});
|
|
|
|
}
|
2022-05-22 17:51:23 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-18 13:03:16 +02:00
|
|
|
QString TwitchIrcServer::cleanChannelName(const QString &dirtyChannelName)
|
2019-09-08 22:27:57 +02:00
|
|
|
{
|
2019-09-18 13:03:16 +02:00
|
|
|
if (dirtyChannelName.startsWith('#'))
|
|
|
|
return dirtyChannelName.mid(1).toLower();
|
|
|
|
else
|
|
|
|
return dirtyChannelName.toLower();
|
2019-09-08 22:27:57 +02:00
|
|
|
}
|
|
|
|
|
2019-09-18 13:03:16 +02:00
|
|
|
bool TwitchIrcServer::hasSeparateWriteConnection() const
|
2019-09-08 22:27:57 +02:00
|
|
|
{
|
|
|
|
return true;
|
|
|
|
// return getSettings()->twitchSeperateWriteConnection;
|
|
|
|
}
|
|
|
|
|
2019-09-18 13:03:16 +02:00
|
|
|
void TwitchIrcServer::onMessageSendRequested(TwitchChannel *channel,
|
|
|
|
const QString &message, bool &sent)
|
2019-09-08 22:27:57 +02:00
|
|
|
{
|
|
|
|
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 =
|
2020-09-26 13:06:37 +02:00
|
|
|
makeSystemMessage("You are sending messages too quickly.");
|
2019-09-08 22:27:57 +02:00
|
|
|
|
|
|
|
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 =
|
2020-09-26 13:06:37 +02:00
|
|
|
makeSystemMessage("You are sending too many messages.");
|
2019-09-08 22:27:57 +02:00
|
|
|
|
|
|
|
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
|
2019-09-08 22:27:57 +02:00
|
|
|
{
|
|
|
|
return this->bttv;
|
|
|
|
}
|
2019-09-18 13:03:16 +02:00
|
|
|
const FfzEmotes &TwitchIrcServer::getFfzEmotes() const
|
2019-09-08 22:27:57 +02:00
|
|
|
{
|
|
|
|
return this->ffz;
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace chatterino
|