feat: add channel for messages caught by AutoMod (#4986)

Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
iProdigy 2023-12-03 14:07:30 -08:00 committed by GitHub
parent 812186dc4c
commit 44abe6b487
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 107 additions and 24 deletions

View file

@ -3,6 +3,7 @@
## Unversioned ## Unversioned
- Major: Allow use of Twitch follower emotes in other channels if subscribed. (#4922) - Major: Allow use of Twitch follower emotes in other channels if subscribed. (#4922)
- Major: Add `/automod` split to track automod caught messages across all open channels the user moderates. (#4986)
- Minor: Migrate to the new Get Channel Followers Helix endpoint, fixing follower count not showing up in usercards. (#4809) - Minor: Migrate to the new Get Channel Followers Helix endpoint, fixing follower count not showing up in usercards. (#4809)
- Minor: The account switcher is now styled to match your theme. (#4817) - Minor: The account switcher is now styled to match your theme. (#4817)
- Minor: Add an invisible resize handle to the bottom of frameless user info popups and reply thread popups. (#4795) - Minor: Add an invisible resize handle to the bottom of frameless user info popups and reply thread popups. (#4795)

View file

@ -546,9 +546,15 @@ void Application::initPubSub()
msg.senderUserID, msg.senderUserLogin, msg.senderUserID, msg.senderUserLogin,
senderDisplayName, senderColor}; senderDisplayName, senderColor};
postToThread([chan, action] { postToThread([chan, action] {
const auto p = makeAutomodMessage(action); const auto p =
makeAutomodMessage(action, chan->getName());
chan->addMessage(p.first); chan->addMessage(p.first);
chan->addMessage(p.second); chan->addMessage(p.second);
getApp()->twitch->automodChannel->addMessage(
p.first);
getApp()->twitch->automodChannel->addMessage(
p.second);
}); });
} }
// "ALLOWED" and "DENIED" statuses remain unimplemented // "ALLOWED" and "DENIED" statuses remain unimplemented
@ -573,7 +579,7 @@ void Application::initPubSub()
} }
postToThread([chan, action] { postToThread([chan, action] {
const auto p = makeAutomodMessage(action); const auto p = makeAutomodMessage(action, chan->getName());
chan->addMessage(p.first); chan->addMessage(p.first);
chan->addMessage(p.second); chan->addMessage(p.second);
}); });

View file

@ -295,7 +295,8 @@ bool Channel::isWritable() const
{ {
using Type = Channel::Type; using Type = Channel::Type;
auto type = this->getType(); auto type = this->getType();
return type != Type::TwitchMentions && type != Type::TwitchLive; return type != Type::TwitchMentions && type != Type::TwitchLive &&
type != Type::TwitchAutomod;
} }
void Channel::sendMessage(const QString &message) void Channel::sendMessage(const QString &message)
@ -330,7 +331,8 @@ bool Channel::isLive() const
bool Channel::shouldIgnoreHighlights() const bool Channel::shouldIgnoreHighlights() const
{ {
return this->type_ == Type::TwitchMentions || return this->type_ == Type::TwitchAutomod ||
this->type_ == Type::TwitchMentions ||
this->type_ == Type::TwitchWhispers; this->type_ == Type::TwitchWhispers;
} }

View file

@ -38,6 +38,7 @@ public:
TwitchWatching, TwitchWatching,
TwitchMentions, TwitchMentions,
TwitchLive, TwitchLive,
TwitchAutomod,
TwitchEnd, TwitchEnd,
Irc, Irc,
Misc Misc

View file

@ -30,7 +30,7 @@ namespace chatterino {
enum class WindowType; enum class WindowType;
struct SplitDescriptor { struct SplitDescriptor {
// Twitch or mentions or watching or whispers or IRC // Twitch or mentions or watching or live or automod or whispers or IRC
QString type_; QString type_;
// Twitch Channel name or IRC channel name // Twitch Channel name or IRC channel name

View file

@ -141,13 +141,14 @@ MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action)
} }
std::pair<MessagePtr, MessagePtr> makeAutomodMessage( std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
const AutomodAction &action) const AutomodAction &action, const QString &channelName)
{ {
MessageBuilder builder, builder2; MessageBuilder builder, builder2;
// //
// Builder for AutoMod message with explanation // Builder for AutoMod message with explanation
builder.message().loginName = "automod"; builder.message().loginName = "automod";
builder.message().channelName = channelName;
builder.message().flags.set(MessageFlag::PubSub); builder.message().flags.set(MessageFlag::PubSub);
builder.message().flags.set(MessageFlag::Timeout); builder.message().flags.set(MessageFlag::Timeout);
builder.message().flags.set(MessageFlag::AutoMod); builder.message().flags.set(MessageFlag::AutoMod);
@ -193,6 +194,12 @@ std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
// //
// Builder for offender's message // Builder for offender's message
builder2.message().channelName = channelName;
builder2
.emplace<TextElement>("#" + channelName,
MessageElementFlag::ChannelName,
MessageColor::System)
->setLink({Link::JumpToChannel, channelName});
builder2.emplace<TimestampElement>(); builder2.emplace<TimestampElement>();
builder2.emplace<TwitchModerationElement>(); builder2.emplace<TwitchModerationElement>();
builder2.message().loginName = action.target.login; builder2.message().loginName = action.target.login;

View file

@ -54,7 +54,7 @@ const ImageUploaderResultTag imageUploaderResultMessage{};
MessagePtr makeSystemMessage(const QString &text); MessagePtr makeSystemMessage(const QString &text);
MessagePtr makeSystemMessage(const QString &text, const QTime &time); MessagePtr makeSystemMessage(const QString &text, const QTime &time);
std::pair<MessagePtr, MessagePtr> makeAutomodMessage( std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
const AutomodAction &action); const AutomodAction &action, const QString &channelName);
MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action); MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action);
struct MessageParseArgs { struct MessageParseArgs {

View file

@ -43,6 +43,7 @@ TwitchIrcServer::TwitchIrcServer()
: whispersChannel(new Channel("/whispers", Channel::Type::TwitchWhispers)) : whispersChannel(new Channel("/whispers", Channel::Type::TwitchWhispers))
, mentionsChannel(new Channel("/mentions", Channel::Type::TwitchMentions)) , mentionsChannel(new Channel("/mentions", Channel::Type::TwitchMentions))
, liveChannel(new Channel("/live", Channel::Type::TwitchLive)) , liveChannel(new Channel("/live", Channel::Type::TwitchLive))
, automodChannel(new Channel("/automod", Channel::Type::TwitchAutomod))
, watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching) , watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching)
{ {
this->initializeIrc(); this->initializeIrc();
@ -272,6 +273,11 @@ std::shared_ptr<Channel> TwitchIrcServer::getCustomChannel(
return this->liveChannel; return this->liveChannel;
} }
if (channelName == "/automod")
{
return this->automodChannel;
}
static auto getTimer = [](ChannelPtr channel, int msBetweenMessages, static auto getTimer = [](ChannelPtr channel, int msBetweenMessages,
bool addInitialMessages) { bool addInitialMessages) {
if (addInitialMessages) if (addInitialMessages)
@ -383,6 +389,7 @@ void TwitchIrcServer::forEachChannelAndSpecialChannels(
func(this->whispersChannel); func(this->whispersChannel);
func(this->mentionsChannel); func(this->mentionsChannel);
func(this->liveChannel); func(this->liveChannel);
func(this->automodChannel);
} }
std::shared_ptr<Channel> TwitchIrcServer::getChannelOrEmptyByID( std::shared_ptr<Channel> TwitchIrcServer::getChannelOrEmptyByID(

View file

@ -77,6 +77,7 @@ public:
const ChannelPtr whispersChannel; const ChannelPtr whispersChannel;
const ChannelPtr mentionsChannel; const ChannelPtr mentionsChannel;
const ChannelPtr liveChannel; const ChannelPtr liveChannel;
const ChannelPtr automodChannel;
IndirectChannel watchingChannel; IndirectChannel watchingChannel;
PubSub *pubsub; PubSub *pubsub;

View file

@ -603,6 +603,10 @@ void WindowManager::encodeChannel(IndirectChannel channel, QJsonObject &obj)
obj.insert("name", channel.get()->getName()); obj.insert("name", channel.get()->getName());
} }
break; break;
case Channel::Type::TwitchAutomod: {
obj.insert("type", "automod");
}
break;
case Channel::Type::TwitchMentions: { case Channel::Type::TwitchMentions: {
obj.insert("type", "mentions"); obj.insert("type", "mentions");
} }
@ -676,6 +680,10 @@ IndirectChannel WindowManager::decodeChannel(const SplitDescriptor &descriptor)
{ {
return app->twitch->liveChannel; return app->twitch->liveChannel;
} }
else if (descriptor.type_ == "automod")
{
return app->twitch->automodChannel;
}
else if (descriptor.type_ == "irc") else if (descriptor.type_ == "irc")
{ {
return Irc::instance().getOrAddChannel(descriptor.server_, return Irc::instance().getOrAddChannel(descriptor.server_,

View file

@ -29,6 +29,10 @@ LoggingChannel::LoggingChannel(const QString &_channelName,
{ {
this->subDirectory = "Live"; this->subDirectory = "Live";
} }
else if (channelName.startsWith("/automod"))
{
this->subDirectory = "AutoMod";
}
else else
{ {
this->subDirectory = this->subDirectory =
@ -96,7 +100,8 @@ void LoggingChannel::addMessage(MessagePtr message)
} }
QString str; QString str;
if (channelName.startsWith("/mentions")) if (channelName.startsWith("/mentions") ||
channelName.startsWith("/automod"))
{ {
str.append("#" + message->channelName + " "); str.append("#" + message->channelName + " ");
} }

View file

@ -1313,8 +1313,9 @@ SplitNotebook::SplitNotebook(Window *parent)
{ {
for (auto *split : sc->getSplits()) for (auto *split : sc->getSplits())
{ {
if (split->getChannel()->getType() != auto type = split->getChannel()->getType();
Channel::Type::TwitchMentions) if (type != Channel::Type::TwitchMentions &&
type != Channel::Type::TwitchAutomod)
{ {
if (split->getChannelView().scrollToMessage( if (split->getChannelView().scrollToMessage(
message)) message))

View file

@ -140,10 +140,27 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent)
live_lbl->setVisible(enabled); live_lbl->setVisible(enabled);
}); });
// automod_btn
auto automod_btn = vbox.emplace<QRadioButton>("AutoMod").assign(
&this->ui_.twitch.automod);
auto automod_lbl =
vbox.emplace<QLabel>("Shows when AutoMod catches a message in any "
"channel you moderate.")
.hidden();
automod_lbl->setWordWrap(true);
automod_btn->installEventFilter(&this->tabFilter_);
QObject::connect(automod_btn.getElement(), &QRadioButton::toggled,
[=](bool enabled) mutable {
automod_lbl->setVisible(enabled);
});
vbox->addStretch(1); vbox->addStretch(1);
// tabbing order // tabbing order
QWidget::setTabOrder(live_btn.getElement(), channel_btn.getElement()); QWidget::setTabOrder(automod_btn.getElement(),
channel_btn.getElement());
QWidget::setTabOrder(channel_btn.getElement(), QWidget::setTabOrder(channel_btn.getElement(),
whispers_btn.getElement()); whispers_btn.getElement());
QWidget::setTabOrder(whispers_btn.getElement(), QWidget::setTabOrder(whispers_btn.getElement(),
@ -151,6 +168,7 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent)
QWidget::setTabOrder(mentions_btn.getElement(), QWidget::setTabOrder(mentions_btn.getElement(),
watching_btn.getElement()); watching_btn.getElement());
QWidget::setTabOrder(watching_btn.getElement(), live_btn.getElement()); QWidget::setTabOrder(watching_btn.getElement(), live_btn.getElement());
QWidget::setTabOrder(live_btn.getElement(), automod_btn.getElement());
// tab // tab
auto tab = notebook->addPage(obj.getElement()); auto tab = notebook->addPage(obj.getElement());
@ -311,6 +329,11 @@ void SelectChannelDialog::setSelectedChannel(IndirectChannel _channel)
this->ui_.twitch.live->setFocus(); this->ui_.twitch.live->setFocus();
} }
break; break;
case Channel::Type::TwitchAutomod: {
this->ui_.notebook->selectIndex(TAB_TWITCH);
this->ui_.twitch.automod->setFocus();
}
break;
case Channel::Type::Irc: { case Channel::Type::Irc: {
this->ui_.notebook->selectIndex(TAB_IRC); this->ui_.notebook->selectIndex(TAB_IRC);
this->ui_.irc.channel->setText(_channel.get()->getName()); this->ui_.irc.channel->setText(_channel.get()->getName());
@ -378,6 +401,10 @@ IndirectChannel SelectChannelDialog::getSelectedChannel() const
{ {
return app->twitch->liveChannel; return app->twitch->liveChannel;
} }
else if (this->ui_.twitch.automod->isChecked())
{
return app->twitch->automodChannel;
}
} }
break; break;
case TAB_IRC: { case TAB_IRC: {
@ -442,9 +469,9 @@ bool SelectChannelDialog::EventFilter::eventFilter(QObject *watched,
this->dialog->ui_.twitch.whispers->setFocus(); this->dialog->ui_.twitch.whispers->setFocus();
return true; return true;
} }
else if (widget == this->dialog->ui_.twitch.live) else if (widget == this->dialog->ui_.twitch.automod)
{ {
// Special case for when current selection is "Live" (the last entry in the list), next wrap is Channel, but we need to select its edit box // Special case for when current selection is "AutoMod" (the last entry in the list), next wrap is Channel, but we need to select its edit box
this->dialog->ui_.twitch.channel->setFocus(); this->dialog->ui_.twitch.channel->setFocus();
return true; return true;
} }
@ -463,7 +490,7 @@ bool SelectChannelDialog::EventFilter::eventFilter(QObject *watched,
if (widget == this->dialog->ui_.twitch.channelName) if (widget == this->dialog->ui_.twitch.channelName)
{ {
// Special case for when current selection is the "Channel" entry's edit box since the Edit box actually has the focus // Special case for when current selection is the "Channel" entry's edit box since the Edit box actually has the focus
this->dialog->ui_.twitch.live->setFocus(); this->dialog->ui_.twitch.automod->setFocus();
return true; return true;
} }

View file

@ -49,6 +49,7 @@ private:
QRadioButton *mentions; QRadioButton *mentions;
QRadioButton *watching; QRadioButton *watching;
QRadioButton *live; QRadioButton *live;
QRadioButton *automod;
} twitch; } twitch;
struct { struct {
QLineEdit *channel; QLineEdit *channel;

View file

@ -1282,14 +1282,16 @@ MessageElementFlags ChannelView::getFlags() const
flags.set(MessageElementFlag::ModeratorTools); flags.set(MessageElementFlag::ModeratorTools);
} }
if (this->underlyingChannel_ == app->twitch->mentionsChannel || if (this->underlyingChannel_ == app->twitch->mentionsChannel ||
this->underlyingChannel_ == app->twitch->liveChannel) this->underlyingChannel_ == app->twitch->liveChannel ||
this->underlyingChannel_ == app->twitch->automodChannel)
{ {
flags.set(MessageElementFlag::ChannelName); flags.set(MessageElementFlag::ChannelName);
flags.unset(MessageElementFlag::ChannelPointReward); flags.unset(MessageElementFlag::ChannelPointReward);
} }
} }
if (this->sourceChannel_ == app->twitch->mentionsChannel) if (this->sourceChannel_ == app->twitch->mentionsChannel ||
this->sourceChannel_ == app->twitch->automodChannel)
{ {
flags.set(MessageElementFlag::ChannelName); flags.set(MessageElementFlag::ChannelName);
} }
@ -2347,11 +2349,13 @@ void ChannelView::addMessageContextMenuItems(QMenu *menu,
this->split_; this->split_;
bool isMentions = bool isMentions =
this->channel()->getType() == Channel::Type::TwitchMentions; this->channel()->getType() == Channel::Type::TwitchMentions;
if (isSearch || isMentions || isReplyOrUserCard) bool isAutomod = this->channel()->getType() == Channel::Type::TwitchAutomod;
if (isSearch || isMentions || isReplyOrUserCard || isAutomod)
{ {
const auto &messagePtr = layout->getMessagePtr(); const auto &messagePtr = layout->getMessagePtr();
menu->addAction("&Go to message", [this, &messagePtr, isSearch, menu->addAction("&Go to message", [this, &messagePtr, isSearch,
isMentions, isReplyOrUserCard] { isMentions, isReplyOrUserCard,
isAutomod] {
if (isSearch) if (isSearch)
{ {
if (const auto &search = if (const auto &search =
@ -2360,16 +2364,17 @@ void ChannelView::addMessageContextMenuItems(QMenu *menu,
search->goToMessage(messagePtr); search->goToMessage(messagePtr);
} }
} }
else if (isMentions) else if (isMentions || isAutomod)
{ {
getApp()->windows->scrollToMessage(messagePtr); getApp()->windows->scrollToMessage(messagePtr);
} }
else if (isReplyOrUserCard) else if (isReplyOrUserCard)
{ {
// If the thread is in the mentions channel, // If the thread is in the mentions or automod channel,
// we need to find the original split. // we need to find the original split.
if (this->split_->getChannel()->getType() == const auto type = this->split_->getChannel()->getType();
Channel::Type::TwitchMentions) if (type == Channel::Type::TwitchMentions ||
type == Channel::Type::TwitchAutomod)
{ {
getApp()->windows->scrollToMessage(messagePtr); getApp()->windows->scrollToMessage(messagePtr);
} }
@ -2606,6 +2611,8 @@ bool ChannelView::mayContainMessage(const MessagePtr &message)
return message->flags.has(MessageFlag::Highlighted); return message->flags.has(MessageFlag::Highlighted);
case Channel::Type::TwitchLive: case Channel::Type::TwitchLive:
return message->flags.has(MessageFlag::System); return message->flags.has(MessageFlag::System);
case Channel::Type::TwitchAutomod:
return message->flags.has(MessageFlag::AutoMod);
case Channel::Type::TwitchEnd: // TODO: not used? case Channel::Type::TwitchEnd: // TODO: not used?
case Channel::Type::None: // Unspecific case Channel::Type::None: // Unspecific
case Channel::Type::Misc: // Unspecific case Channel::Type::Misc: // Unspecific

View file

@ -134,7 +134,9 @@ void SearchPopup::goToMessage(const MessagePtr &message)
{ {
for (const auto &view : this->searchChannels_) for (const auto &view : this->searchChannels_)
{ {
if (view.get().channel()->getType() == Channel::Type::TwitchMentions) const auto type = view.get().channel()->getType();
if (type == Channel::Type::TwitchMentions ||
type == Channel::Type::TwitchAutomod)
{ {
getApp()->windows->scrollToMessage(message); getApp()->windows->scrollToMessage(message);
return; return;
@ -166,6 +168,10 @@ void SearchPopup::updateWindowTitle()
{ {
historyName = "multiple channels'"; historyName = "multiple channels'";
} }
else if (this->channelName_ == "/automod")
{
historyName = "automod";
}
else if (this->channelName_ == "/mentions") else if (this->channelName_ == "/mentions")
{ {
historyName = "mentions"; historyName = "mentions";

View file

@ -1454,7 +1454,10 @@ void Split::showSearch(bool singleChannel)
auto container = dynamic_cast<SplitContainer *>(notebook.getPageAt(i)); auto container = dynamic_cast<SplitContainer *>(notebook.getPageAt(i));
for (auto split : container->getSplits()) for (auto split : container->getSplits())
{ {
popup->addChannel(split->getChannelView()); if (split->channel_.getType() != Channel::Type::TwitchAutomod)
{
popup->addChannel(split->getChannelView());
}
} }
} }