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
- 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: 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)

View file

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

View file

@ -295,7 +295,8 @@ bool Channel::isWritable() const
{
using Type = Channel::Type;
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)
@ -330,7 +331,8 @@ bool Channel::isLive() const
bool Channel::shouldIgnoreHighlights() const
{
return this->type_ == Type::TwitchMentions ||
return this->type_ == Type::TwitchAutomod ||
this->type_ == Type::TwitchMentions ||
this->type_ == Type::TwitchWhispers;
}

View file

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

View file

@ -30,7 +30,7 @@ namespace chatterino {
enum class WindowType;
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_;
// Twitch Channel name or IRC channel name

View file

@ -141,13 +141,14 @@ MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action)
}
std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
const AutomodAction &action)
const AutomodAction &action, const QString &channelName)
{
MessageBuilder builder, builder2;
//
// Builder for AutoMod message with explanation
builder.message().loginName = "automod";
builder.message().channelName = channelName;
builder.message().flags.set(MessageFlag::PubSub);
builder.message().flags.set(MessageFlag::Timeout);
builder.message().flags.set(MessageFlag::AutoMod);
@ -193,6 +194,12 @@ std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
//
// 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<TwitchModerationElement>();
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, const QTime &time);
std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
const AutomodAction &action);
const AutomodAction &action, const QString &channelName);
MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action);
struct MessageParseArgs {

View file

@ -43,6 +43,7 @@ TwitchIrcServer::TwitchIrcServer()
: 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)
{
this->initializeIrc();
@ -272,6 +273,11 @@ std::shared_ptr<Channel> TwitchIrcServer::getCustomChannel(
return this->liveChannel;
}
if (channelName == "/automod")
{
return this->automodChannel;
}
static auto getTimer = [](ChannelPtr channel, int msBetweenMessages,
bool addInitialMessages) {
if (addInitialMessages)
@ -383,6 +389,7 @@ void TwitchIrcServer::forEachChannelAndSpecialChannels(
func(this->whispersChannel);
func(this->mentionsChannel);
func(this->liveChannel);
func(this->automodChannel);
}
std::shared_ptr<Channel> TwitchIrcServer::getChannelOrEmptyByID(

View file

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

View file

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

View file

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

View file

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

View file

@ -140,10 +140,27 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent)
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);
// tabbing order
QWidget::setTabOrder(live_btn.getElement(), channel_btn.getElement());
QWidget::setTabOrder(automod_btn.getElement(),
channel_btn.getElement());
QWidget::setTabOrder(channel_btn.getElement(),
whispers_btn.getElement());
QWidget::setTabOrder(whispers_btn.getElement(),
@ -151,6 +168,7 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent)
QWidget::setTabOrder(mentions_btn.getElement(),
watching_btn.getElement());
QWidget::setTabOrder(watching_btn.getElement(), live_btn.getElement());
QWidget::setTabOrder(live_btn.getElement(), automod_btn.getElement());
// tab
auto tab = notebook->addPage(obj.getElement());
@ -311,6 +329,11 @@ void SelectChannelDialog::setSelectedChannel(IndirectChannel _channel)
this->ui_.twitch.live->setFocus();
}
break;
case Channel::Type::TwitchAutomod: {
this->ui_.notebook->selectIndex(TAB_TWITCH);
this->ui_.twitch.automod->setFocus();
}
break;
case Channel::Type::Irc: {
this->ui_.notebook->selectIndex(TAB_IRC);
this->ui_.irc.channel->setText(_channel.get()->getName());
@ -378,6 +401,10 @@ IndirectChannel SelectChannelDialog::getSelectedChannel() const
{
return app->twitch->liveChannel;
}
else if (this->ui_.twitch.automod->isChecked())
{
return app->twitch->automodChannel;
}
}
break;
case TAB_IRC: {
@ -442,9 +469,9 @@ bool SelectChannelDialog::EventFilter::eventFilter(QObject *watched,
this->dialog->ui_.twitch.whispers->setFocus();
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();
return true;
}
@ -463,7 +490,7 @@ bool SelectChannelDialog::EventFilter::eventFilter(QObject *watched,
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
this->dialog->ui_.twitch.live->setFocus();
this->dialog->ui_.twitch.automod->setFocus();
return true;
}

View file

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

View file

@ -1282,14 +1282,16 @@ MessageElementFlags ChannelView::getFlags() const
flags.set(MessageElementFlag::ModeratorTools);
}
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.unset(MessageElementFlag::ChannelPointReward);
}
}
if (this->sourceChannel_ == app->twitch->mentionsChannel)
if (this->sourceChannel_ == app->twitch->mentionsChannel ||
this->sourceChannel_ == app->twitch->automodChannel)
{
flags.set(MessageElementFlag::ChannelName);
}
@ -2347,11 +2349,13 @@ void ChannelView::addMessageContextMenuItems(QMenu *menu,
this->split_;
bool isMentions =
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();
menu->addAction("&Go to message", [this, &messagePtr, isSearch,
isMentions, isReplyOrUserCard] {
isMentions, isReplyOrUserCard,
isAutomod] {
if (isSearch)
{
if (const auto &search =
@ -2360,16 +2364,17 @@ void ChannelView::addMessageContextMenuItems(QMenu *menu,
search->goToMessage(messagePtr);
}
}
else if (isMentions)
else if (isMentions || isAutomod)
{
getApp()->windows->scrollToMessage(messagePtr);
}
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.
if (this->split_->getChannel()->getType() ==
Channel::Type::TwitchMentions)
const auto type = this->split_->getChannel()->getType();
if (type == Channel::Type::TwitchMentions ||
type == Channel::Type::TwitchAutomod)
{
getApp()->windows->scrollToMessage(messagePtr);
}
@ -2606,6 +2611,8 @@ bool ChannelView::mayContainMessage(const MessagePtr &message)
return message->flags.has(MessageFlag::Highlighted);
case Channel::Type::TwitchLive:
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::None: // Unspecific
case Channel::Type::Misc: // Unspecific

View file

@ -134,7 +134,9 @@ void SearchPopup::goToMessage(const MessagePtr &message)
{
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);
return;
@ -166,6 +168,10 @@ void SearchPopup::updateWindowTitle()
{
historyName = "multiple channels'";
}
else if (this->channelName_ == "/automod")
{
historyName = "automod";
}
else if (this->channelName_ == "/mentions")
{
historyName = "mentions";

View file

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