Added support for Twitch's Chat Replies (#3722)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -9,7 +9,6 @@
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchHelpers.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
#include "providers/twitch/TwitchMessageBuilder.hpp"
|
||||
#include "singletons/Resources.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "singletons/WindowManager.hpp"
|
||||
@@ -19,6 +18,7 @@
|
||||
|
||||
#include <IrcMessage>
|
||||
|
||||
#include <memory>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace {
|
||||
@@ -242,6 +242,87 @@ void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message,
|
||||
false, message->isAction());
|
||||
}
|
||||
|
||||
std::vector<MessagePtr> IrcMessageHandler::parseMessageWithReply(
|
||||
Channel *channel, Communi::IrcMessage *message,
|
||||
const std::vector<MessagePtr> &otherLoaded)
|
||||
{
|
||||
std::vector<MessagePtr> builtMessages;
|
||||
|
||||
auto command = message->command();
|
||||
|
||||
if (command == "PRIVMSG")
|
||||
{
|
||||
auto privMsg = static_cast<Communi::IrcPrivateMessage *>(message);
|
||||
auto tc = dynamic_cast<TwitchChannel *>(channel);
|
||||
if (!tc)
|
||||
{
|
||||
return this->parsePrivMessage(channel, privMsg);
|
||||
}
|
||||
|
||||
MessageParseArgs args;
|
||||
TwitchMessageBuilder builder(channel, message, args, privMsg->content(),
|
||||
privMsg->isAction());
|
||||
|
||||
this->populateReply(tc, message, otherLoaded, builder);
|
||||
|
||||
if (!builder.isIgnored())
|
||||
{
|
||||
builtMessages.emplace_back(builder.build());
|
||||
builder.triggerHighlights();
|
||||
}
|
||||
}
|
||||
else if (command == "USERNOTICE")
|
||||
{
|
||||
return this->parseUserNoticeMessage(channel, message);
|
||||
}
|
||||
else if (command == "NOTICE")
|
||||
{
|
||||
return this->parseNoticeMessage(
|
||||
static_cast<Communi::IrcNoticeMessage *>(message));
|
||||
}
|
||||
|
||||
return builtMessages;
|
||||
}
|
||||
|
||||
void IrcMessageHandler::populateReply(
|
||||
TwitchChannel *channel, Communi::IrcMessage *message,
|
||||
const std::vector<MessagePtr> &otherLoaded, TwitchMessageBuilder &builder)
|
||||
{
|
||||
const auto &tags = message->tags();
|
||||
if (const auto it = tags.find("reply-parent-msg-id"); it != tags.end())
|
||||
{
|
||||
const QString replyID = it.value().toString();
|
||||
auto threadIt = channel->threads_.find(replyID);
|
||||
if (threadIt != channel->threads_.end())
|
||||
{
|
||||
const auto owned = threadIt->second.lock();
|
||||
if (owned)
|
||||
{
|
||||
// Thread already exists (has a reply)
|
||||
builder.setThread(owned);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Thread does not yet exist, find root reply and create thread.
|
||||
// Linear search is justified by the infrequent use of replies
|
||||
for (auto &otherMsg : otherLoaded)
|
||||
{
|
||||
if (otherMsg->id == replyID)
|
||||
{
|
||||
// Found root reply message
|
||||
std::shared_ptr<MessageThread> newThread =
|
||||
std::make_shared<MessageThread>(otherMsg);
|
||||
|
||||
builder.setThread(newThread);
|
||||
// Store weak reference to thread in channel
|
||||
channel->addReplyThread(newThread);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
|
||||
const QString &target,
|
||||
const QString &content,
|
||||
@@ -276,7 +357,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
|
||||
auto channel = dynamic_cast<TwitchChannel *>(chan.get());
|
||||
|
||||
const auto &tags = _message->tags();
|
||||
if (const auto &it = tags.find("custom-reward-id"); it != tags.end())
|
||||
if (const auto it = tags.find("custom-reward-id"); it != tags.end())
|
||||
{
|
||||
const auto rewardId = it.value().toString();
|
||||
if (!channel->isChannelPointRewardKnown(rewardId))
|
||||
@@ -301,6 +382,31 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
|
||||
|
||||
TwitchMessageBuilder builder(chan.get(), _message, args, content, isAction);
|
||||
|
||||
if (const auto it = tags.find("reply-parent-msg-id"); it != tags.end())
|
||||
{
|
||||
const QString replyID = it.value().toString();
|
||||
auto threadIt = channel->threads_.find(replyID);
|
||||
if (threadIt != channel->threads_.end() && !threadIt->second.expired())
|
||||
{
|
||||
// Thread already exists (has a reply)
|
||||
builder.setThread(threadIt->second.lock());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Thread does not yet exist, find root reply and create thread.
|
||||
auto root = channel->findMessage(replyID);
|
||||
if (root)
|
||||
{
|
||||
// Found root reply message
|
||||
const auto newThread = std::make_shared<MessageThread>(root);
|
||||
|
||||
builder.setThread(newThread);
|
||||
// Store weak reference to thread in channel
|
||||
channel->addReplyThread(newThread);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isSub || !builder.isIgnored())
|
||||
{
|
||||
if (isSub)
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
#include <IrcMessage>
|
||||
#include "common/Channel.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchMessageBuilder.hpp"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
@@ -20,6 +24,10 @@ public:
|
||||
std::vector<MessagePtr> parseMessage(Channel *channel,
|
||||
Communi::IrcMessage *message);
|
||||
|
||||
std::vector<MessagePtr> parseMessageWithReply(
|
||||
Channel *channel, Communi::IrcMessage *message,
|
||||
const std::vector<MessagePtr> &otherLoaded);
|
||||
|
||||
// parsePrivMessage arses a single IRC PRIVMSG into 0-1 Chatterino messages
|
||||
std::vector<MessagePtr> parsePrivMessage(
|
||||
Channel *channel, Communi::IrcPrivateMessage *message);
|
||||
@@ -59,6 +67,10 @@ private:
|
||||
void addMessage(Communi::IrcMessage *message, const QString &target,
|
||||
const QString &content, TwitchIrcServer &server,
|
||||
bool isResub, bool isAction);
|
||||
|
||||
void populateReply(TwitchChannel *channel, Communi::IrcMessage *message,
|
||||
const std::vector<MessagePtr> &otherLoaded,
|
||||
TwitchMessageBuilder &builder);
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/Common.hpp"
|
||||
#include "common/Env.hpp"
|
||||
#include "common/NetworkRequest.hpp"
|
||||
@@ -182,12 +181,34 @@ TwitchChannel::TwitchChannel(const QString &name)
|
||||
this->refreshBTTVChannelEmotes(false);
|
||||
});
|
||||
|
||||
this->messageRemovedFromStart.connect([this](MessagePtr &msg) {
|
||||
if (msg->replyThread)
|
||||
{
|
||||
if (msg->replyThread->liveCount(msg) == 0)
|
||||
{
|
||||
this->threads_.erase(msg->replyThread->rootId());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// timers
|
||||
QObject::connect(&this->chattersListTimer_, &QTimer::timeout, [=] {
|
||||
this->refreshChatters();
|
||||
});
|
||||
this->chattersListTimer_.start(5 * 60 * 1000);
|
||||
|
||||
QObject::connect(&this->threadClearTimer_, &QTimer::timeout, [=] {
|
||||
// We periodically check for any dangling reply threads that missed
|
||||
// being cleaned up on messageRemovedFromStart. This could occur if
|
||||
// some other part of the program, like a user card, held a reference
|
||||
// to the message.
|
||||
//
|
||||
// It seems difficult to actually replicate a situation where things
|
||||
// are actually cleaned up, but I've verified that cleanups DO happen.
|
||||
this->cleanUpReplyThreads();
|
||||
});
|
||||
this->threadClearTimer_.start(5 * 60 * 1000);
|
||||
|
||||
// debugging
|
||||
#if 0
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
@@ -311,46 +332,34 @@ boost::optional<ChannelPointReward> TwitchChannel::channelPointReward(
|
||||
return it->second;
|
||||
}
|
||||
|
||||
void TwitchChannel::sendMessage(const QString &message)
|
||||
void TwitchChannel::showLoginMessage()
|
||||
{
|
||||
const auto linkColor = MessageColor(MessageColor::Link);
|
||||
const auto accountsLink = Link(Link::OpenAccountsPage, QString());
|
||||
const auto currentUser = getApp()->accounts->twitch.getCurrent();
|
||||
const auto expirationText =
|
||||
QStringLiteral("You need to log in to send messages. You can link your "
|
||||
"Twitch account");
|
||||
const auto loginPromptText = QStringLiteral("in the settings.");
|
||||
|
||||
auto builder = MessageBuilder();
|
||||
builder.message().flags.set(MessageFlag::System);
|
||||
builder.message().flags.set(MessageFlag::DoNotTriggerNotification);
|
||||
|
||||
builder.emplace<TimestampElement>();
|
||||
builder.emplace<TextElement>(expirationText, MessageElementFlag::Text,
|
||||
MessageColor::System);
|
||||
builder
|
||||
.emplace<TextElement>(loginPromptText, MessageElementFlag::Text,
|
||||
linkColor)
|
||||
->setLink(accountsLink);
|
||||
|
||||
this->addMessage(builder.release());
|
||||
}
|
||||
|
||||
QString TwitchChannel::prepareMessage(const QString &message) const
|
||||
{
|
||||
auto app = getApp();
|
||||
|
||||
if (!app->accounts->twitch.isLoggedIn())
|
||||
{
|
||||
if (message.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const auto linkColor = MessageColor(MessageColor::Link);
|
||||
const auto accountsLink = Link(Link::OpenAccountsPage, QString());
|
||||
const auto currentUser = getApp()->accounts->twitch.getCurrent();
|
||||
const auto expirationText =
|
||||
QString("You need to log in to send messages. You can link your "
|
||||
"Twitch account");
|
||||
const auto loginPromptText = QString("in the settings.");
|
||||
|
||||
auto builder = MessageBuilder();
|
||||
builder.message().flags.set(MessageFlag::System);
|
||||
builder.message().flags.set(MessageFlag::DoNotTriggerNotification);
|
||||
|
||||
builder.emplace<TimestampElement>();
|
||||
builder.emplace<TextElement>(expirationText, MessageElementFlag::Text,
|
||||
MessageColor::System);
|
||||
builder
|
||||
.emplace<TextElement>(loginPromptText, MessageElementFlag::Text,
|
||||
linkColor)
|
||||
->setLink(accountsLink);
|
||||
|
||||
this->addMessage(builder.release());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(chatterinoTwitch)
|
||||
<< "[TwitchChannel" << this->getName() << "] Send message:" << message;
|
||||
|
||||
// Do last message processing
|
||||
QString parsedMessage = app->emotes->emojis.replaceShortCodes(message);
|
||||
|
||||
// This is to make sure that combined emoji go through properly, see
|
||||
@@ -361,7 +370,7 @@ void TwitchChannel::sendMessage(const QString &message)
|
||||
|
||||
if (parsedMessage.isEmpty())
|
||||
{
|
||||
return;
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!this->hasHighRateLimit())
|
||||
@@ -395,6 +404,32 @@ void TwitchChannel::sendMessage(const QString &message)
|
||||
}
|
||||
}
|
||||
|
||||
return parsedMessage;
|
||||
}
|
||||
|
||||
void TwitchChannel::sendMessage(const QString &message)
|
||||
{
|
||||
auto app = getApp();
|
||||
if (!app->accounts->twitch.isLoggedIn())
|
||||
{
|
||||
if (!message.isEmpty())
|
||||
{
|
||||
this->showLoginMessage();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(chatterinoTwitch)
|
||||
<< "[TwitchChannel" << this->getName() << "] Send message:" << message;
|
||||
|
||||
// Do last message processing
|
||||
QString parsedMessage = this->prepareMessage(message);
|
||||
if (parsedMessage.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool messageSent = false;
|
||||
this->sendMessageSignal.invoke(this->getName(), parsedMessage, messageSent);
|
||||
|
||||
@@ -405,6 +440,40 @@ void TwitchChannel::sendMessage(const QString &message)
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchChannel::sendReply(const QString &message, const QString &replyId)
|
||||
{
|
||||
auto app = getApp();
|
||||
if (!app->accounts->twitch.isLoggedIn())
|
||||
{
|
||||
if (!message.isEmpty())
|
||||
{
|
||||
this->showLoginMessage();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(chatterinoTwitch) << "[TwitchChannel" << this->getName()
|
||||
<< "] Send reply message:" << message;
|
||||
|
||||
// Do last message processing
|
||||
QString parsedMessage = this->prepareMessage(message);
|
||||
if (parsedMessage.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool messageSent = false;
|
||||
this->sendReplySignal.invoke(this->getName(), parsedMessage, replyId,
|
||||
messageSent);
|
||||
|
||||
if (messageSent)
|
||||
{
|
||||
qCDebug(chatterinoTwitch) << "sent";
|
||||
this->lastSentMessage_ = parsedMessage;
|
||||
}
|
||||
}
|
||||
|
||||
bool TwitchChannel::isMod() const
|
||||
{
|
||||
return this->mod_;
|
||||
@@ -794,8 +863,10 @@ void TwitchChannel::loadRecentMessages()
|
||||
}
|
||||
}
|
||||
|
||||
for (auto builtMessage :
|
||||
handler.parseMessage(shared.get(), message))
|
||||
auto builtMessages = handler.parseMessageWithReply(
|
||||
shared.get(), message, allBuiltMessages);
|
||||
|
||||
for (auto builtMessage : builtMessages)
|
||||
{
|
||||
builtMessage->flags.set(MessageFlag::RecentMessage);
|
||||
allBuiltMessages.emplace_back(builtMessage);
|
||||
@@ -922,6 +993,39 @@ void TwitchChannel::fetchDisplayName()
|
||||
[] {});
|
||||
}
|
||||
|
||||
void TwitchChannel::addReplyThread(const std::shared_ptr<MessageThread> &thread)
|
||||
{
|
||||
this->threads_[thread->rootId()] = thread;
|
||||
}
|
||||
|
||||
const std::unordered_map<QString, std::weak_ptr<MessageThread>>
|
||||
&TwitchChannel::threads() const
|
||||
{
|
||||
return this->threads_;
|
||||
}
|
||||
|
||||
void TwitchChannel::cleanUpReplyThreads()
|
||||
{
|
||||
for (auto it = this->threads_.begin(), last = this->threads_.end();
|
||||
it != last;)
|
||||
{
|
||||
bool doErase = true;
|
||||
if (auto thread = it->second.lock())
|
||||
{
|
||||
doErase = thread->liveCount() == 0;
|
||||
}
|
||||
|
||||
if (doErase)
|
||||
{
|
||||
it = this->threads_.erase(it);
|
||||
}
|
||||
else
|
||||
{
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchChannel::refreshBadges()
|
||||
{
|
||||
auto url = Url{"https://badges.twitch.tv/v1/badges/channels/" +
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/Aliases.hpp"
|
||||
#include "common/Atomic.hpp"
|
||||
#include "common/Channel.hpp"
|
||||
@@ -7,6 +8,7 @@
|
||||
#include "common/ChatterSet.hpp"
|
||||
#include "common/Outcome.hpp"
|
||||
#include "common/UniqueAccess.hpp"
|
||||
#include "messages/MessageThread.hpp"
|
||||
#include "providers/twitch/ChannelPointReward.hpp"
|
||||
#include "providers/twitch/TwitchEmotes.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
@@ -74,12 +76,15 @@ public:
|
||||
int slowMode = 0;
|
||||
};
|
||||
|
||||
explicit TwitchChannel(const QString &channelName);
|
||||
|
||||
void initialize();
|
||||
|
||||
// Channel methods
|
||||
virtual bool isEmpty() const override;
|
||||
virtual bool canSendMessage() const override;
|
||||
virtual void sendMessage(const QString &message) override;
|
||||
virtual void sendReply(const QString &message, const QString &replyId);
|
||||
virtual bool isMod() const override;
|
||||
bool isVip() const;
|
||||
bool isStaff() const;
|
||||
@@ -118,6 +123,17 @@ public:
|
||||
// Cheers
|
||||
boost::optional<CheerEmote> cheerEmote(const QString &string);
|
||||
|
||||
// Replies
|
||||
/**
|
||||
* Stores the given thread in this channel.
|
||||
*
|
||||
* Note: This method not take ownership of the MessageThread; this
|
||||
* TwitchChannel instance will store a weak_ptr to the thread.
|
||||
*/
|
||||
void addReplyThread(const std::shared_ptr<MessageThread> &thread);
|
||||
const std::unordered_map<QString, std::weak_ptr<MessageThread>> &threads()
|
||||
const;
|
||||
|
||||
// Signals
|
||||
pajlada::Signals::NoArgSignal roomIdChanged;
|
||||
pajlada::Signals::NoArgSignal userStateChanged;
|
||||
@@ -138,9 +154,6 @@ private:
|
||||
QString localizedName;
|
||||
} nameOptions;
|
||||
|
||||
protected:
|
||||
explicit TwitchChannel(const QString &channelName);
|
||||
|
||||
private:
|
||||
// Methods
|
||||
void refreshLiveStatus();
|
||||
@@ -151,6 +164,8 @@ private:
|
||||
void refreshCheerEmotes();
|
||||
void loadRecentMessages();
|
||||
void fetchDisplayName();
|
||||
void cleanUpReplyThreads();
|
||||
void showLoginMessage();
|
||||
|
||||
void setLive(bool newLiveStatus);
|
||||
void setMod(bool value);
|
||||
@@ -164,6 +179,8 @@ private:
|
||||
const QString &getDisplayName() const override;
|
||||
const QString &getLocalizedName() const override;
|
||||
|
||||
QString prepareMessage(const QString &message) const;
|
||||
|
||||
// Data
|
||||
const QString subscriptionUrl_;
|
||||
const QString channelUrl_;
|
||||
@@ -171,6 +188,7 @@ private:
|
||||
int chatterCount_;
|
||||
UniqueAccess<StreamStatus> streamStatus_;
|
||||
UniqueAccess<RoomModes> roomModes_;
|
||||
std::unordered_map<QString, std::weak_ptr<MessageThread>> threads_;
|
||||
|
||||
protected:
|
||||
Atomic<std::shared_ptr<const EmoteMap>> bttvEmotes_;
|
||||
@@ -194,6 +212,7 @@ private:
|
||||
QString lastSentMessage_;
|
||||
QObject lifetimeGuard_;
|
||||
QTimer chattersListTimer_;
|
||||
QTimer threadClearTimer_;
|
||||
QElapsedTimer titleRefreshedTimer_;
|
||||
QElapsedTimer clipCreationTimer_;
|
||||
bool isClipCreationInProgress{false};
|
||||
|
||||
@@ -121,6 +121,11 @@ std::shared_ptr<Channel> TwitchIrcServer::createChannel(
|
||||
[this, channel = channel.get()](auto &chan, auto &msg, bool &sent) {
|
||||
this->onMessageSendRequested(channel, msg, sent);
|
||||
});
|
||||
channel->sendReplySignal.connect(
|
||||
[this, channel = channel.get()](auto &chan, auto &msg, auto &replyId,
|
||||
bool &sent) {
|
||||
this->onReplySendRequested(channel, msg, replyId, sent);
|
||||
});
|
||||
|
||||
return std::shared_ptr<Channel>(channel);
|
||||
}
|
||||
@@ -370,66 +375,90 @@ bool TwitchIrcServer::hasSeparateWriteConnection() const
|
||||
// return getSettings()->twitchSeperateWriteConnection;
|
||||
}
|
||||
|
||||
bool TwitchIrcServer::prepareToSend(TwitchChannel *channel)
|
||||
{
|
||||
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)
|
||||
{
|
||||
auto errorMessage =
|
||||
makeSystemMessage("You are sending messages too quickly.");
|
||||
|
||||
channel->addMessage(errorMessage);
|
||||
|
||||
this->lastErrorTimeSpeed_ = now;
|
||||
}
|
||||
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)
|
||||
{
|
||||
auto errorMessage =
|
||||
makeSystemMessage("You are sending too many messages.");
|
||||
|
||||
channel->addMessage(errorMessage);
|
||||
|
||||
this->lastErrorTimeAmount_ = now;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
lastMessage.push(now);
|
||||
return true;
|
||||
}
|
||||
|
||||
void TwitchIrcServer::onMessageSendRequested(TwitchChannel *channel,
|
||||
const QString &message, bool &sent)
|
||||
{
|
||||
sent = false;
|
||||
|
||||
bool canSend = this->prepareToSend(channel);
|
||||
if (!canSend)
|
||||
{
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
this->sendMessage(channel->getName(), message);
|
||||
sent = true;
|
||||
}
|
||||
|
||||
void TwitchIrcServer::onReplySendRequested(TwitchChannel *channel,
|
||||
const QString &message,
|
||||
const QString &replyId, bool &sent)
|
||||
{
|
||||
sent = false;
|
||||
|
||||
bool canSend = this->prepareToSend(channel);
|
||||
if (!canSend)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this->sendRawMessage("@reply-parent-msg-id=" + replyId + " PRIVMSG #" +
|
||||
channel->getName() + " :" + message);
|
||||
|
||||
sent = true;
|
||||
}
|
||||
|
||||
const BttvEmotes &TwitchIrcServer::getBttvEmotes() const
|
||||
{
|
||||
return this->bttv;
|
||||
|
||||
@@ -67,6 +67,10 @@ protected:
|
||||
private:
|
||||
void onMessageSendRequested(TwitchChannel *channel, const QString &message,
|
||||
bool &sent);
|
||||
void onReplySendRequested(TwitchChannel *channel, const QString &message,
|
||||
const QString &replyId, bool &sent);
|
||||
|
||||
bool prepareToSend(TwitchChannel *channel);
|
||||
|
||||
std::mutex lastMessageMutex_;
|
||||
std::queue<std::chrono::steady_clock::time_point> lastMessagePleb_;
|
||||
|
||||
@@ -48,6 +48,66 @@ const QSet<QString> zeroWidthEmotes{
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
namespace {
|
||||
|
||||
QString stylizeUsername(const QString &username, const Message &message)
|
||||
{
|
||||
auto app = getApp();
|
||||
|
||||
const QString &localizedName = message.localizedName;
|
||||
bool hasLocalizedName = !localizedName.isEmpty();
|
||||
|
||||
// The full string that will be rendered in the chat widget
|
||||
QString usernameText;
|
||||
|
||||
switch (getSettings()->usernameDisplayMode.getValue())
|
||||
{
|
||||
case UsernameDisplayMode::Username: {
|
||||
usernameText = username;
|
||||
}
|
||||
break;
|
||||
|
||||
case UsernameDisplayMode::LocalizedName: {
|
||||
if (hasLocalizedName)
|
||||
{
|
||||
usernameText = localizedName;
|
||||
}
|
||||
else
|
||||
{
|
||||
usernameText = username;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
case UsernameDisplayMode::UsernameAndLocalizedName: {
|
||||
if (hasLocalizedName)
|
||||
{
|
||||
usernameText = username + "(" + localizedName + ")";
|
||||
}
|
||||
else
|
||||
{
|
||||
usernameText = username;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
auto nicknames = getCSettings().nicknames.readOnly();
|
||||
|
||||
for (const auto &nickname : *nicknames)
|
||||
{
|
||||
if (nickname.match(usernameText))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return usernameText;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TwitchMessageBuilder::TwitchMessageBuilder(
|
||||
Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage,
|
||||
const MessageParseArgs &_args)
|
||||
@@ -138,6 +198,70 @@ MessagePtr TwitchMessageBuilder::build()
|
||||
this->message().flags.set(MessageFlag::FirstMessage);
|
||||
}
|
||||
|
||||
// reply threads
|
||||
if (this->thread_)
|
||||
{
|
||||
// set references
|
||||
this->message().replyThread = this->thread_;
|
||||
this->thread_->addToThread(this->weakOf());
|
||||
|
||||
// enable reply flag
|
||||
this->message().flags.set(MessageFlag::ReplyMessage);
|
||||
|
||||
const auto &threadRoot = this->thread_->root();
|
||||
|
||||
QString usernameText =
|
||||
stylizeUsername(threadRoot->loginName, *threadRoot.get());
|
||||
|
||||
this->emplace<ReplyCurveElement>();
|
||||
|
||||
// construct reply elements
|
||||
this->emplace<TextElement>(
|
||||
"Replying to", MessageElementFlag::RepliedMessage,
|
||||
MessageColor::System, FontStyle::ChatMediumSmall)
|
||||
->setLink({Link::ViewThread, this->thread_->rootId()});
|
||||
|
||||
this->emplace<TextElement>(
|
||||
"@" + usernameText + ":", MessageElementFlag::RepliedMessage,
|
||||
threadRoot->usernameColor, FontStyle::ChatMediumSmall)
|
||||
->setLink({Link::UserInfo, threadRoot->displayName});
|
||||
|
||||
this->emplace<SingleLineTextElement>(
|
||||
threadRoot->messageText, MessageElementFlag::RepliedMessage,
|
||||
this->textColor_, FontStyle::ChatMediumSmall)
|
||||
->setLink({Link::ViewThread, this->thread_->rootId()});
|
||||
}
|
||||
else if (this->tags.find("reply-parent-msg-id") != this->tags.end())
|
||||
{
|
||||
// Message is a reply but we couldn't find the original message.
|
||||
// Render the message using the additional reply tags
|
||||
|
||||
auto replyDisplayName = this->tags.find("reply-parent-display-name");
|
||||
auto replyBody = this->tags.find("reply-parent-msg-body");
|
||||
|
||||
if (replyDisplayName != this->tags.end() &&
|
||||
replyBody != this->tags.end())
|
||||
{
|
||||
auto name = replyDisplayName->toString();
|
||||
auto body = parseTagString(replyBody->toString());
|
||||
|
||||
this->emplace<ReplyCurveElement>();
|
||||
|
||||
this->emplace<TextElement>(
|
||||
"Replying to", MessageElementFlag::RepliedMessage,
|
||||
MessageColor::System, FontStyle::ChatMediumSmall);
|
||||
|
||||
this->emplace<TextElement>(
|
||||
"@" + name + ":", MessageElementFlag::RepliedMessage,
|
||||
this->textColor_, FontStyle::ChatMediumSmall)
|
||||
->setLink({Link::UserInfo, name});
|
||||
|
||||
this->emplace<SingleLineTextElement>(
|
||||
body, MessageElementFlag::RepliedMessage, this->textColor_,
|
||||
FontStyle::ChatMediumSmall);
|
||||
}
|
||||
}
|
||||
|
||||
// timestamp
|
||||
this->message().serverReceivedTime = calculateMessageTime(this->ircMessage);
|
||||
this->emplace<TimestampElement>(this->message().serverReceivedTime.time());
|
||||
@@ -217,6 +341,23 @@ MessagePtr TwitchMessageBuilder::build()
|
||||
ColorProvider::instance().color(ColorType::Whisper);
|
||||
}
|
||||
|
||||
if (this->thread_)
|
||||
{
|
||||
auto &img = getResources().buttons.replyThreadDark;
|
||||
this->emplace<CircularImageElement>(Image::fromPixmap(img, 0.15), 2,
|
||||
Qt::gray,
|
||||
MessageElementFlag::ReplyButton)
|
||||
->setLink({Link::ViewThread, this->thread_->rootId()});
|
||||
}
|
||||
else
|
||||
{
|
||||
auto &img = getResources().buttons.replyDark;
|
||||
this->emplace<CircularImageElement>(Image::fromPixmap(img, 0.15), 2,
|
||||
Qt::gray,
|
||||
MessageElementFlag::ReplyButton)
|
||||
->setLink({Link::ReplyToMessage, this->message().id});
|
||||
}
|
||||
|
||||
return this->release();
|
||||
}
|
||||
|
||||
@@ -559,53 +700,7 @@ void TwitchMessageBuilder::appendUsername()
|
||||
}
|
||||
}
|
||||
|
||||
bool hasLocalizedName = !localizedName.isEmpty();
|
||||
|
||||
// The full string that will be rendered in the chat widget
|
||||
QString usernameText;
|
||||
|
||||
switch (getSettings()->usernameDisplayMode.getValue())
|
||||
{
|
||||
case UsernameDisplayMode::Username: {
|
||||
usernameText = username;
|
||||
}
|
||||
break;
|
||||
|
||||
case UsernameDisplayMode::LocalizedName: {
|
||||
if (hasLocalizedName)
|
||||
{
|
||||
usernameText = localizedName;
|
||||
}
|
||||
else
|
||||
{
|
||||
usernameText = username;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
case UsernameDisplayMode::UsernameAndLocalizedName: {
|
||||
if (hasLocalizedName)
|
||||
{
|
||||
usernameText = username + "(" + localizedName + ")";
|
||||
}
|
||||
else
|
||||
{
|
||||
usernameText = username;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
auto nicknames = getCSettings().nicknames.readOnly();
|
||||
|
||||
for (const auto &nickname : *nicknames)
|
||||
{
|
||||
if (nickname.match(usernameText))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
QString usernameText = stylizeUsername(username, this->message());
|
||||
|
||||
if (this->args.isSentWhisper)
|
||||
{
|
||||
@@ -1479,4 +1574,9 @@ void TwitchMessageBuilder::listOfUsersSystemMessage(QString prefix,
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchMessageBuilder::setThread(std::shared_ptr<MessageThread> thread)
|
||||
{
|
||||
this->thread_ = std::move(thread);
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "common/Aliases.hpp"
|
||||
#include "common/Outcome.hpp"
|
||||
#include "messages/MessageThread.hpp"
|
||||
#include "messages/SharedMessageBuilder.hpp"
|
||||
#include "providers/twitch/ChannelPointReward.hpp"
|
||||
#include "providers/twitch/PubSubActions.hpp"
|
||||
@@ -45,6 +46,8 @@ public:
|
||||
void triggerHighlights() override;
|
||||
MessagePtr build() override;
|
||||
|
||||
void setThread(std::shared_ptr<MessageThread> thread);
|
||||
|
||||
static void appendChannelPointRewardMessage(
|
||||
const ChannelPointReward &reward, MessageBuilder *builder, bool isMod,
|
||||
bool isBroadcaster);
|
||||
@@ -105,6 +108,7 @@ private:
|
||||
int bitsLeft;
|
||||
bool bitsStacked = false;
|
||||
bool historicalMessage_ = false;
|
||||
std::shared_ptr<MessageThread> thread_;
|
||||
|
||||
QString userId_;
|
||||
bool senderIsBroadcaster{};
|
||||
|
||||
Reference in New Issue
Block a user