ObserverHerb / Celeste

A Twitch bot written in C++/Qt with a focus on multimedia and native performance
GNU General Public License v3.0
6 stars 0 forks source link

Commands Editor: Add field for redemption trigger #170

Open ObserverHerb opened 8 months ago

ObserverHerb commented 8 months ago

Add a text field that allows entering the redemption name for commands that are triggered by redemptions.

ObserverHerb commented 6 months ago

Can I query for the channel's list of redemptions and populate a list in real-time?

ObserverHerb commented 1 month ago

This is suspended since I dropped Twitch affiliate.

ObserverHerb commented 1 month ago
diff --git a/bot.cpp b/bot.cpp
index 2df34aa..b99ca3b 100644
--- a/bot.cpp
+++ b/bot.cpp
@@ -46,11 +46,12 @@ const char *TWITCH_API_OPERATION_EMOTE_ONLY="emote only";
 const char *TWITCH_API_OPERATION_STREAM_TITLE="stream title";
 const char *TWITCH_API_OPERATION_STREAM_CATEGORY="stream category";
 const char *TWITCH_API_OPERATION_LOAD_BADGES="badges";
-const char *TWITCH_API_OPERATION_SHOUTOUT="shoutout";
+const char *TWITCH_API_OPERATION_FETCH_REDEMPTIONS="redemption list";
 const char *TWITCH_API_ERROR_TEMPLATE_INCOMPLETE="Response from requesting %1 was incomplete";
 const char *TWITCH_API_ERROR_TEMPLATE_UNKNOWN="Something went wrong obtaining %1";
 const char *TWITCH_API_ERROR_TEMPLATE_JSON_PARSE="Error parsing %1 JSON: %2";
 const char *TWITCH_API_ERROR_AUTH="Auth token or client ID missing or invalid";
+const char *TWITCH_API_ERROR_SERVER="Twitch choked";
 const char16_t *CHAT_BADGE_BROADCASTER=u"broadcaster";
 const char16_t *CHAT_BADGE_MODERATOR=u"moderator";
 const char16_t *CHAT_TAG_DISPLAY_NAME=u"display-name";
@@ -136,6 +137,11 @@ Bot::Bot(Music::Player &musicPlayer,Security &security,QObject *parent) : QObjec
    connect(&vibeKeeper,&Music::Player::Print,this,&Bot::Print);
 }

+void Bot::Initialize()
+{
+   FetchRedemptionList();
+}
+
 void Bot::DeclareCommand(const Command &&command,NativeCommandFlag flag)
 {
    commands.insert({{command.Name(),command}});
@@ -606,6 +612,61 @@ void Bot::RestoreMusic()
    vibeKeeper.DuckVolume(false);
 }

+void Bot::FetchRedemptionList()
+{
+   Network::Request::Send({Twitch::Endpoint(Twitch::ENDPOINT_REDEMPTION_LIST)},Network::Method::GET,[this](QNetworkReply *reply) {
+       switch (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt())
+       {
+       case 400:
+           emit Print(u"Missing information or too many IDs specified"_s,TWITCH_API_OPERATION_FETCH_REDEMPTIONS);
+           return;
+       case 401:
+           emit Print(TWITCH_API_ERROR_AUTH,TWITCH_API_OPERATION_FETCH_REDEMPTIONS);
+           return;
+       case 403:
+           emit Print(u"The broadcaster does not qualify for redemptions"_s,TWITCH_API_OPERATION_FETCH_REDEMPTIONS);
+           return;
+       case 404:
+           emit Print(u"Specified redemptions were not found"_s,TWITCH_API_OPERATION_FETCH_REDEMPTIONS);
+           return;
+       case 500:
+           emit Print(TWITCH_API_ERROR_SERVER,TWITCH_API_OPERATION_FETCH_REDEMPTIONS);
+           return;
+       }
+
+       if (reply->error())
+       {
+           emit Print(u"Failed to obtain list of redemptions for unknown reason"_s,TWITCH_API_OPERATION_FETCH_REDEMPTIONS);
+           return;
+       }
+
+       const JSON::ParseResult parsedJSON=JSON::Parse(reply->readAll());
+       if (!parsedJSON)
+       {
+           emit Print(QString(TWITCH_API_ERROR_TEMPLATE_JSON_PARSE).arg(TWITCH_API_OPERATION_FETCH_REDEMPTIONS,parsedJSON.error));
+           return;
+       }
+
+       const QJsonObject object=parsedJSON().object();
+       auto jsonFieldData=object.find(JSON::Keys::DATA);
+       if (jsonFieldData == object.end()) return;
+       QStringList redemptionNames;
+       for (const QJsonValue &redemptions : jsonFieldData->toArray())
+       {
+           const QJsonObject objectRedemption=redemptions.toObject();
+           auto jsonFieldRedemptionName=objectRedemption.find("title");
+           if (jsonFieldRedemptionName == objectRedemption.end()) continue;
+           redemptionNames.append(jsonFieldRedemptionName->toString());
+       }
+       emit RedemptionList(redemptionNames);
+   },{
+       {QUERY_PARAMETER_BROADCASTER_ID,security.AdministratorID()}
+   },{
+       {NETWORK_HEADER_AUTHORIZATION,security.Bearer(security.OAuthToken())},
+       {NETWORK_HEADER_CLIENT_ID,security.ClientID()}
+   });
+}
+
 void Bot::DispatchArrival(const QString &login)
 {
    if (auto viewer=viewers.find(login); viewer != viewers.end())
@@ -1103,6 +1164,8 @@ void Bot::DispatchShoutout(Command command)

 void Bot::DispatchShoutout(const QString &streamer)
 {
+   static const char *TWITCH_API_OPERATION_SHOUTOUT="shoutout";
+
    Viewer::Remote *profile=new Viewer::Remote(security,streamer);
    connect(profile,&Viewer::Remote::Recognized,profile,[this](const Viewer::Local &profile) {
        // native Twitch shoutout
diff --git a/bot.h b/bot.h
index d9c4d79..a0b68ed 100644
--- a/bot.h
+++ b/bot.h
@@ -133,6 +133,7 @@ protected:
    void AdjustVibeVolume(Command command);
    void StreamTitle(const QString &title);
    void StreamCategory(const QString &category);
+   void FetchRedemptionList();
 signals:
    void Print(const QString &message,const QString operation=QString(),const QString subsystem=QString("bot core"));
    void ChatMessage(std::shared_ptr<Chat::Message> message);
@@ -160,7 +161,9 @@ signals:
    void AnnounceTextWall(const QString &message,const QString &audioPath);
    void AnnounceDeniedCommand(const QString &videoPath);
    void Welcomed(const QString &user);
+   void RedemptionList(const QStringList &redemptions);
 public slots:
+   void Initialize();
    void ParseChatMessage(const QString &prefix,const QString &source,const QStringList &parameters,const QString &message);
    void DispatchCommandViaSubsystem(JSON::SignalPayload *response,const QString &name,const QString &login);
    void Ping();
diff --git a/main.cpp b/main.cpp
index 89ecd6e..4234385 100644
--- a/main.cpp
+++ b/main.cpp
@@ -250,6 +250,9 @@ int main(int argc,char *argv[])
        celeste.connect(&celeste,&Bot::Pulse,&pulsar,QOverload<const QString&,const QString&>::of(&Pulsar::Pulse));
        celeste.connect(&celeste,&Bot::Welcomed,&metrics,&UI::Metrics::Dialog::Acknowledged);
        celeste.connect(&celeste,&Bot::Panic,&window,&Window::ShowPanicText);
+       celeste.connect(&celeste,&Bot::RedemptionList,&celeste,[](const QStringList &redemptionNames) {
+           UI::Commands::Entry::redemptionNames=redemptionNames;
+       });
        celeste.connect(&celeste,&Bot::Panic,&celeste,[&celeste]() {
            celeste.disconnect();
        });
@@ -300,6 +303,7 @@ int main(int argc,char *argv[])
        });
        channel->connect(channel,&Channel::Denied,&security,&Security::AuthorizeUser);
        security.connect(&security,&Security::Initialized,channel,&Channel::Connect);
+       security.connect(&security,&Security::Initialized,&celeste,&Bot::Initialize);
        application.connect(&application,&QApplication::aboutToQuit,[&log,&socket,channel]() {
            socket.connect(&socket,&IRCSocket::disconnected,&log,&Log::Archive);
            channel->disconnect(); // stops attempting to reconnect by removing all connections to signals
diff --git a/security.cpp b/security.cpp
index 92b65d4..73d2f67 100644
--- a/security.cpp
+++ b/security.cpp
@@ -231,7 +231,7 @@ void Security::ObtainAdministratorProfile()
    Viewer::Remote *profile=new Viewer::Remote(*this,settingAdministrator);
    connect(profile,&Viewer::Remote::Recognized,profile,[this](Viewer::Local profile) {
        administratorID=profile.ID();
-       emit Initialize();
+       Initialize();
    });
    connect(profile,&Viewer::Remote::Unrecognized,this,[this]() {
        AuthorizeUser();
diff --git a/security.h b/security.h
index f96024d..71fbd5f 100644
--- a/security.h
+++ b/security.h
@@ -53,6 +53,7 @@ private:
    bool tokensInitialized;
    QTimer tokenValidationTimer;
    bool authorizing;
+   void Initialize();
 signals:
    void TokenRequestFailed();
    void Listening();
@@ -66,5 +67,4 @@ private slots:
    void RewireConnected();
    void RewireError(QMqttClient::ClientError error);
    void RewireMessage(QMqttMessage messasge);
-   void Initialize();
 };
diff --git a/twitch.h b/twitch.h
index 2692d15..8b65856 100644
--- a/twitch.h
+++ b/twitch.h
@@ -15,6 +15,7 @@ namespace Twitch
    inline const char *ENDPOINT_BADGES="chat/badges/global";
    inline const char *ENDPOINT_SHOUTOUTS="chat/shoutouts";
    inline const char *ENDPOINT_USERS="users";
+   inline const char *ENDPOINT_REDEMPTION_LIST="channel_points/custom_rewards";
    inline const char *ENDPOINT_EVENTSUB="eventsub/subscriptions";
    inline const char *ENDPOINT_EVENTSUB_SUBSCRIPTIONS="eventsub/subscriptions";

diff --git a/widgets.cpp b/widgets.cpp
index 1a45f06..8785328 100644
--- a/widgets.cpp
+++ b/widgets.cpp
@@ -310,6 +310,8 @@ namespace UI
            QDialog::hideEvent(event);
        }

+       QStringList Entry::redemptionNames;
+
        Entry::Entry(Feedback::Error &errorReport,QWidget *parent) : QWidget(parent),
            layout(this),
            details(this),
@@ -325,6 +327,7 @@ namespace UI
            random(u"Choose Random Media"_s,std::bind_front(&Entry::SetUpRandomCheckBox,this),&details),
            duplicates(u"Allow Duplicates"_s,std::bind_front(&Entry::SetUpDuplicatesCheckBox,this),&details),
            protect(u"Protect"_s,std::bind_front(&Entry::SetUpProtectCheckBox,this),&details),
+           redemption(u"Redemption"_s,std::bind_front(&Entry::SetUpRedemptionList,this),&details),
            message(u"Message"_s,std::bind_front(&Entry::SetUpMessageTextEdit,this),&details),
            errorReport(errorReport)
        {
@@ -531,6 +534,7 @@ namespace UI
                protect.Hide();
                duplicates.Hide();
                random.Hide();
+               redemption.Hide();
                message.Hide();
                browse.Hide();
                aliases.Hide();
@@ -546,6 +550,7 @@ namespace UI
                protect.Show();
                duplicates.Show();
                random.Show();
+               redemption.Show();
                message.Show();
                browse.Show();
                aliases.Show();
@@ -629,6 +634,14 @@ namespace UI
            detailsLayout.addWidget(widget,3,3,1,1);
        }

+       void Entry::SetUpRedemptionList(QComboBox *widget)
+       {
+           widget->setEditable(true);
+           widget->lineEdit()->setPlaceholderText("Redemption");
+           widget->addItems(redemptionNames);
+           detailsLayout.addWidget(widget,4,0,1,4);
+       }
+
        void Entry::SetUpMessageTextEdit(QTextEdit *widget)
        {
            widget->setPlaceholderText(u"Message to display in announcement"_s);
@@ -637,7 +650,7 @@ namespace UI
            connect(widget,&QTextEdit::textChanged,this,&Entry::ValidateMessage);
            connect(widget,&QTextEdit::textChanged,this,&Entry::UpdateMessage);
            widget->viewport()->installEventFilter(this);
-           detailsLayout.addWidget(widget,4,0,1,4);
+           detailsLayout.addWidget(widget,5,0,1,4);
        }

        void Entry::SetUpBrowseButton(QPushButton *widget)
diff --git a/widgets.h b/widgets.h
index e1e3f7a..c26967c 100644
--- a/widgets.h
+++ b/widgets.h
@@ -343,6 +343,7 @@ namespace UI
            QString Message() const;
            bool Protected() const;
            void ToggleFold();
+           static QStringList redemptionNames;
        protected:
            QGridLayout layout;
            QFrame details;
@@ -358,6 +359,7 @@ namespace UI
            EphemeralWidget<QCheckBox> random;
            EphemeralWidget<QCheckBox> duplicates;
            EphemeralWidget<QCheckBox> protect;
+           EphemeralWidget<QComboBox> redemption;
            EphemeralWidget<QTextEdit> message;
            Feedback::Error &errorReport;
            void UpdateName();
@@ -380,6 +382,7 @@ namespace UI
            void SetUpProtectCheckBox(QCheckBox *widget);
            void SetUpRandomCheckBox(QCheckBox *widget);
            void SetUpDuplicatesCheckBox(QCheckBox *widget);
+           void SetUpRedemptionList(QComboBox *widget);
            void SetUpMessageTextEdit(QTextEdit *widget);
            void SetUpBrowseButton(QPushButton *widget);
            void SetUpAliasesButton(QPushButton *widget);

It appears I already had a POC for this.