diff --git a/CHANGELOG.md b/CHANGELOG.md index d85623f12..4dd89ea9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,7 @@ - Minor: Add setting to limit message input length. (#3418) - Minor: Make built-in commands work in IRC channels. (#4160) - Minor: Add support for `echo-message` capabilities for IRC. (#4157) +- Minor: Add proper support for IRC private messages. (#4158) - Minor: Improved look of tabs when using a layout other than top. (#3925, #4152) - Bugfix: Fixed channels with two leading `#`s not being usable on IRC (#4154) - Bugfix: Fixed `Add new account` dialog causing main chatterino window to be non movable. (#4121) diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index 697c62112..8eb66df07 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -12,6 +12,8 @@ #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" #include "messages/MessageElement.hpp" +#include "providers/irc/IrcChannel2.hpp" +#include "providers/irc/IrcServer.hpp" #include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp" @@ -234,102 +236,107 @@ QString runWhisperCommand(const QStringList &words, const ChannelPtr &channel) auto target = words.at(1); stripChannelName(target); auto message = words.mid(2).join(' '); - - if (useIrcForWhisperCommand()) + if (channel->isTwitchChannel()) { - if (channel->isTwitchChannel()) + // this covers all twitch channels and twitch-like channels + if (useIrcForWhisperCommand()) { appendWhisperMessageWordsLocally(words); sendWhisperMessage(words.join(' ')); + return ""; } - else - { - channel->addMessage(makeSystemMessage( - "You can only send whispers from Twitch channels.")); - } + getHelix()->getUserByName( + target, + [channel, currentUser, target, message, + words](const auto &targetUser) { + getHelix()->sendWhisper( + currentUser->getUserId(), targetUser.id, message, + [words] { + appendWhisperMessageWordsLocally(words); + }, + [channel, target, targetUser](auto error, auto message) { + using Error = HelixWhisperError; + + QString errorMessage = "Failed to send whisper - "; + + switch (error) + { + case Error::NoVerifiedPhone: { + errorMessage += + "Due to Twitch restrictions, you are now " + "required to have a verified phone number " + "to send whispers. You can add a phone " + "number in Twitch settings. " + "https://www.twitch.tv/settings/security"; + }; + break; + + case Error::RecipientBlockedUser: { + errorMessage += + "The recipient doesn't allow whispers " + "from strangers or you directly."; + }; + break; + + case Error::WhisperSelf: { + errorMessage += "You cannot whisper yourself."; + }; + break; + + case Error::Forwarded: { + errorMessage += message; + } + break; + + case Error::Ratelimited: { + errorMessage += + "You may only whisper a maximum of 40 " + "unique recipients per day. Within the " + "per day limit, you may whisper a " + "maximum of 3 whispers per second and " + "a maximum of 100 whispers per minute."; + } + break; + + case Error::UserMissingScope: { + // TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE + errorMessage += "Missing required scope. " + "Re-login with your " + "account and try again."; + } + break; + + case Error::UserNotAuthorized: { + // TODO(pajlada): Phrase MISSING_PERMISSION + errorMessage += "You don't have permission to " + "perform that action."; + } + break; + + case Error::Unknown: { + errorMessage += + "An unknown error has occurred."; + } + break; + } + channel->addMessage(makeSystemMessage(errorMessage)); + }); + }, + [channel] { + channel->addMessage( + makeSystemMessage("No user matching that username.")); + }); return ""; } - - getHelix()->getUserByName( - target, - [channel, currentUser, target, message, words](const auto &targetUser) { - getHelix()->sendWhisper( - currentUser->getUserId(), targetUser.id, message, - [words] { - appendWhisperMessageWordsLocally(words); - }, - [channel, target, targetUser](auto error, auto message) { - using Error = HelixWhisperError; - - QString errorMessage = "Failed to send whisper - "; - - switch (error) - { - case Error::NoVerifiedPhone: { - errorMessage += - "Due to Twitch restrictions, you are now " - "required to have a verified phone number " - "to send whispers. You can add a phone " - "number in Twitch settings. " - "https://www.twitch.tv/settings/security"; - }; - break; - - case Error::RecipientBlockedUser: { - errorMessage += - "The recipient doesn't allow whispers " - "from strangers or you directly."; - }; - break; - - case Error::WhisperSelf: { - errorMessage += "You cannot whisper yourself."; - }; - break; - - case Error::Forwarded: { - errorMessage += message; - } - break; - - case Error::Ratelimited: { - errorMessage += - "You may only whisper a maximum of 40 " - "unique recipients per day. Within the " - "per day limit, you may whisper a " - "maximum of 3 whispers per second and " - "a maximum of 100 whispers per minute."; - } - break; - - case Error::UserMissingScope: { - // TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE - errorMessage += "Missing required scope. " - "Re-login with your " - "account and try again."; - } - break; - - case Error::UserNotAuthorized: { - // TODO(pajlada): Phrase MISSING_PERMISSION - errorMessage += "You don't have permission to " - "perform that action."; - } - break; - - case Error::Unknown: { - errorMessage += "An unknown error has occurred."; - } - break; - } - channel->addMessage(makeSystemMessage(errorMessage)); - }); - }, - [channel] { - channel->addMessage( - makeSystemMessage("No user matching that username.")); - }); - + // we must be on IRC + auto *ircChannel = dynamic_cast(channel.get()); + if (ircChannel == nullptr) + { + // give up + return ""; + } + auto *server = ircChannel->server(); + server->sendWhisper(target, message); return ""; } diff --git a/src/providers/irc/IrcCommands.cpp b/src/providers/irc/IrcCommands.cpp index 99bd5cb8c..65264663f 100644 --- a/src/providers/irc/IrcCommands.cpp +++ b/src/providers/irc/IrcCommands.cpp @@ -43,7 +43,7 @@ Outcome invokeIrcCommand(const QString &commandName, const QString &allParams, if (cmd == "msg") { - sendRaw("PRIVMSG " + params[0] + " :" + paramsAfter(0)); + channel.server()->sendWhisper(params[0], paramsAfter(0)); } else if (cmd == "away") { diff --git a/src/providers/irc/IrcMessageBuilder.cpp b/src/providers/irc/IrcMessageBuilder.cpp index 4109a4eb8..68469d967 100644 --- a/src/providers/irc/IrcMessageBuilder.cpp +++ b/src/providers/irc/IrcMessageBuilder.cpp @@ -6,6 +6,7 @@ #include "controllers/ignores/IgnoreController.hpp" #include "controllers/ignores/IgnorePhrase.hpp" #include "messages/Message.hpp" +#include "messages/MessageColor.hpp" #include "providers/chatterino/ChatterinoBadges.hpp" #include "singletons/Emotes.hpp" #include "singletons/Resources.hpp" @@ -41,6 +42,15 @@ IrcMessageBuilder::IrcMessageBuilder( { } +IrcMessageBuilder::IrcMessageBuilder( + const Communi::IrcPrivateMessage *_ircMessage, + const MessageParseArgs &_args) + : SharedMessageBuilder(Channel::getEmpty().get(), _ircMessage, _args, + _ircMessage->content(), false) + , whisperTarget_(_ircMessage->target()) +{ +} + MessagePtr IrcMessageBuilder::build() { // PARSE @@ -93,7 +103,32 @@ void IrcMessageBuilder::appendUsername() this->emplace("->", MessageElementFlag::Username, MessageColor::System, FontStyle::ChatMedium); - this->emplace("you:", MessageElementFlag::Username); + if (this->whisperTarget_.isEmpty()) + { + this->emplace("you:", MessageElementFlag::Username); + } + else + { + this->emplace(this->whisperTarget_ + ":", + MessageElementFlag::Username, + getRandomColor(this->whisperTarget_), + FontStyle::ChatMediumBold); + } + } + else if (this->args.isSentWhisper) + { + this->emplace(usernameText, MessageElementFlag::Username, + this->usernameColor_, + FontStyle::ChatMediumBold); + + // Separator + this->emplace("->", MessageElementFlag::Username, + MessageColor::System, FontStyle::ChatMedium); + + this->emplace( + this->whisperTarget_ + ":", MessageElementFlag::Username, + getRandomColor(this->whisperTarget_), FontStyle::ChatMediumBold) + ->setLink({Link::UserWhisper, this->whisperTarget_}); } else { diff --git a/src/providers/irc/IrcMessageBuilder.hpp b/src/providers/irc/IrcMessageBuilder.hpp index ef1f089f3..c165dde70 100644 --- a/src/providers/irc/IrcMessageBuilder.hpp +++ b/src/providers/irc/IrcMessageBuilder.hpp @@ -36,10 +36,23 @@ public: explicit IrcMessageBuilder(const Communi::IrcNoticeMessage *_ircMessage, const MessageParseArgs &_args); + /** + * @brief used for whisper messages (i.e. PRIVMSG messages with our nick as the target) + **/ + explicit IrcMessageBuilder(const Communi::IrcPrivateMessage *_ircMessage, + const MessageParseArgs &_args); + MessagePtr build() override; private: void appendUsername(); + + /** + * @brief holds the name of the target for the private/direct IRC message + * + * This might not be our nick + */ + QString whisperTarget_; }; } // namespace chatterino diff --git a/src/providers/irc/IrcServer.cpp b/src/providers/irc/IrcServer.cpp index 964b22d91..ad0883f14 100644 --- a/src/providers/irc/IrcServer.cpp +++ b/src/providers/irc/IrcServer.cpp @@ -5,6 +5,8 @@ #include "common/QLogging.hpp" #include "messages/Message.hpp" +#include "messages/MessageColor.hpp" +#include "messages/MessageElement.hpp" #include "providers/irc/Irc2.hpp" #include "providers/irc/IrcChannel2.hpp" #include "providers/irc/IrcMessageBuilder.hpp" @@ -190,6 +192,36 @@ void IrcServer::onReadConnected(IrcConnection *connection) void IrcServer::privateMessageReceived(Communi::IrcPrivateMessage *message) { + // Note: This doesn't use isPrivate() because it only applies to messages targeting our user, + // Servers or bouncers may send messages which have our user as the source + // (like with echo-message CAP), we need to take care of this. + if (!message->target().startsWith("#")) + { + MessageParseArgs args; + if (message->isOwn()) + { + // The server sent us a whisper which has our user as the source + args.isSentWhisper = true; + } + else + { + args.isReceivedWhisper = true; + } + + IrcMessageBuilder builder(message, args); + + auto msg = builder.build(); + + for (auto &&weak : this->channels) + { + if (auto shared = weak.lock()) + { + shared->addMessage(msg); + } + } + return; + } + auto target = message->target(); target = target.startsWith('#') ? target.mid(1) : target; @@ -299,6 +331,38 @@ void IrcServer::readConnectionMessageReceived(Communi::IrcMessage *message) } } +void IrcServer::sendWhisper(const QString &target, const QString &message) +{ + this->sendRawMessage(QString("PRIVMSG %1 :%2").arg(target, message)); + if (this->hasEcho()) + { + return; + } + + MessageParseArgs args; + args.isSentWhisper = true; + + MessageBuilder b; + + b.emplace(); + b.emplace(this->nick(), MessageElementFlag::Text, + MessageColor::Text, FontStyle::ChatMediumBold); + b.emplace("->", MessageElementFlag::Text, + MessageColor::System); + b.emplace(target + ":", MessageElementFlag::Text, + MessageColor::Text, FontStyle::ChatMediumBold); + b.emplace(message, MessageElementFlag::Text); + + auto msg = b.release(); + for (auto &&weak : this->channels) + { + if (auto shared = weak.lock()) + { + shared->addMessage(msg); + } + } +} + bool IrcServer::hasEcho() const { return this->hasEcho_; diff --git a/src/providers/irc/IrcServer.hpp b/src/providers/irc/IrcServer.hpp index 417879f9f..93a4349e6 100644 --- a/src/providers/irc/IrcServer.hpp +++ b/src/providers/irc/IrcServer.hpp @@ -21,6 +21,10 @@ public: const QString &userFriendlyIdentifier(); bool hasEcho() const; + /** + * @brief sends a whisper to the target user (PRIVMSG where a user is the target) + */ + void sendWhisper(const QString &target, const QString &message); // AbstractIrcServer interface protected: