From 75c40fe107bfe4d992b07173a32b2a69092d0879 Mon Sep 17 00:00:00 2001 From: Dennis Rijsdijk Date: Wed, 29 May 2024 23:24:26 +0200 Subject: [PATCH 1/5] feat: firebot events for custom channel reward approved/rejected --- .../events/builtin/twitch-event-source.js | 50 +++++++++++++++++++ .../events/twitch-events/reward-redemption.ts | 35 +++++++++++++ .../twitch-api/eventsub/eventsub-client.ts | 30 ++++++++++- .../builtin/twitch/reward/reward-id.ts | 7 ++- .../builtin/twitch/reward/reward-message.ts | 7 ++- .../builtin/twitch/reward/reward-name.ts | 6 ++- .../twitch/reward/reward-redemption-id.ts | 6 ++- 7 files changed, 134 insertions(+), 7 deletions(-) diff --git a/src/backend/events/builtin/twitch-event-source.js b/src/backend/events/builtin/twitch-event-source.js index d4da0b7c2..6dda7b826 100644 --- a/src/backend/events/builtin/twitch-event-source.js +++ b/src/backend/events/builtin/twitch-event-source.js @@ -436,6 +436,56 @@ module.exports = { } } }, + { + id: "channel-reward-redemption-fulfilled", + name: "Channel Reward Redemption Approved", + description: "When a CUSTOM channel reward redemption is Completed/Approved", + cached: false, + cacheMetaKey: "username", + cacheTtlInSecs: 1, + queued: false, + manualMetadata: { + username: "firebot", + userDisplayName: "Firebot", + userId: "", + rewardName: "Test Reward", + rewardImage: "https://static-cdn.jtvnw.net/automatic-reward-images/highlight-1.png", + rewardCost: 200, + messageText: "Test message" + }, + activityFeed: { + icon: "fad fa-circle", + getMessage: (eventData) => { + const showUserIdName = eventData.username.toLowerCase() !== eventData.userDisplayName.toLowerCase(); + return `**${eventData.userDisplayName}${showUserIdName ? ` (${eventData.username})` : ""}**'s redemption of **${eventData.rewardName}** was approved. ${eventData.messageText && !!eventData.messageText.length ? `*${eventData.messageText}*` : ''}`; + } + } + }, + { + id: "channel-reward-redemption-canceled", + name: "Channel Reward Redemption Rejected", + description: "When a CUSTOM channel reward redemption is Rejected/Refunded", + cached: false, + cacheMetaKey: "username", + cacheTtlInSecs: 1, + queued: false, + manualMetadata: { + username: "firebot", + userDisplayName: "Firebot", + userId: "", + rewardName: "Test Reward", + rewardImage: "https://static-cdn.jtvnw.net/automatic-reward-images/highlight-1.png", + rewardCost: 200, + messageText: "Test message" + }, + activityFeed: { + icon: "fad fa-circle", + getMessage: (eventData) => { + const showUserIdName = eventData.username.toLowerCase() !== eventData.userDisplayName.toLowerCase(); + return `**${eventData.userDisplayName}${showUserIdName ? ` (${eventData.username})` : ""}**'s redemption of **${eventData.rewardName}** was rejected. ${eventData.messageText && !!eventData.messageText.length ? `*${eventData.messageText}*` : ''}`; + } + } + }, { id: "whisper", name: "Whisper", diff --git a/src/backend/events/twitch-events/reward-redemption.ts b/src/backend/events/twitch-events/reward-redemption.ts index b40b7481a..b2b6b462d 100644 --- a/src/backend/events/twitch-events/reward-redemption.ts +++ b/src/backend/events/twitch-events/reward-redemption.ts @@ -52,4 +52,39 @@ export function handleRewardRedemption( rewardManager.triggerChannelReward(rewardId, redemptionMeta); eventManager.triggerEvent("twitch", "channel-reward-redemption", redemptionMeta); }, 100); +} + +export function handleRewardUpdated( + redemptionId: string, + status: string, + messageText: string, + userId: string, + username: string, + userDisplayName: string, + rewardId: string, + rewardTitle: string, + rewardPrompt: string, + rewardCost: number, + rewardImageUrl: string +): void { + const redemptionMeta = { + username, + userId, + userDisplayName, + messageText, + args: (messageText ?? "").split(" "), + redemptionId, + rewardId, + rewardImage: rewardImageUrl, + rewardName: rewardTitle, + rewardDescription: rewardPrompt, + rewardCost: rewardCost + }; + + // Possible values for status are 'fulfilled' and 'canceled' according to Twitch docs + if (status === 'fulfilled') { + eventManager.triggerEvent("twitch", "channel-reward-redemption-fulfilled", redemptionMeta); + } else { + eventManager.triggerEvent("twitch", "channel-reward-redemption-canceled", redemptionMeta); + } } \ No newline at end of file diff --git a/src/backend/twitch-api/eventsub/eventsub-client.ts b/src/backend/twitch-api/eventsub/eventsub-client.ts index 28800f895..b721a89b3 100644 --- a/src/backend/twitch-api/eventsub/eventsub-client.ts +++ b/src/backend/twitch-api/eventsub/eventsub-client.ts @@ -100,7 +100,35 @@ class TwitchEventSubClient { }); this._subscriptions.push(customRewardRedemptionSubscription); - const customRewardRedemptionUpdateSubscription = this._eventSubListener.onChannelRedemptionUpdate(streamer.userId, async () => { + const customRewardRedemptionUpdateSubscription = this._eventSubListener.onChannelRedemptionUpdate(streamer.userId, async (event) => { + const reward = await twitchApi.channelRewards.getCustomChannelReward(event.rewardId); + let imageUrl = ""; + + if (reward && reward.defaultImage) { + const images = reward.defaultImage; + if (images.url4x) { + imageUrl = images.url4x; + } else if (images.url2x) { + imageUrl = images.url2x; + } else if (images.url1x) { + imageUrl = images.url1x; + } + } + + twitchEventsHandler.rewardRedemption.handleRewardUpdated( + event.id, + event.status, + event.input, + event.userId, + event.userName, + event.userDisplayName, + event.rewardId, + event.rewardTitle, + event.rewardPrompt, + event.rewardCost, + imageUrl + ); + rewardManager.refreshChannelRewardRedemptions(); }); this._subscriptions.push(customRewardRedemptionUpdateSubscription); diff --git a/src/backend/variables/builtin/twitch/reward/reward-id.ts b/src/backend/variables/builtin/twitch/reward/reward-id.ts index c3213023c..576d8abce 100644 --- a/src/backend/variables/builtin/twitch/reward/reward-id.ts +++ b/src/backend/variables/builtin/twitch/reward/reward-id.ts @@ -2,9 +2,12 @@ import { ReplaceVariable } from "../../../../../types/variables"; import { EffectTrigger } from "../../../../../shared/effect-constants"; import { OutputDataType, VariableCategory } from "../../../../../shared/variable-constants"; - const triggers = {}; -triggers[EffectTrigger.EVENT] = ["twitch:channel-reward-redemption"]; +triggers[EffectTrigger.EVENT] = [ + "twitch:channel-reward-redemption", + "twitch:channel-reward-redemption-fulfilled", + "twitch:channel-reward-redemption-canceled" +]; triggers[EffectTrigger.CHANNEL_REWARD] = true; triggers[EffectTrigger.PRESET_LIST] = true; triggers[EffectTrigger.MANUAL] = true; diff --git a/src/backend/variables/builtin/twitch/reward/reward-message.ts b/src/backend/variables/builtin/twitch/reward/reward-message.ts index 9396c92aa..bef6cb940 100644 --- a/src/backend/variables/builtin/twitch/reward/reward-message.ts +++ b/src/backend/variables/builtin/twitch/reward/reward-message.ts @@ -2,9 +2,12 @@ import { ReplaceVariable } from "../../../../../types/variables"; import { EffectTrigger } from "../../../../../shared/effect-constants"; import { OutputDataType, VariableCategory } from "../../../../../shared/variable-constants"; - const triggers = {}; -triggers[EffectTrigger.EVENT] = ["twitch:channel-reward-redemption"]; +triggers[EffectTrigger.EVENT] = [ + "twitch:channel-reward-redemption", + "twitch:channel-reward-redemption-fulfilled", + "twitch:channel-reward-redemption-canceled" +]; triggers[EffectTrigger.CHANNEL_REWARD] = true; triggers[EffectTrigger.PRESET_LIST] = true; triggers[EffectTrigger.MANUAL] = true; diff --git a/src/backend/variables/builtin/twitch/reward/reward-name.ts b/src/backend/variables/builtin/twitch/reward/reward-name.ts index d2710ca09..4fa79021e 100644 --- a/src/backend/variables/builtin/twitch/reward/reward-name.ts +++ b/src/backend/variables/builtin/twitch/reward/reward-name.ts @@ -3,7 +3,11 @@ import { EffectTrigger } from "../../../../../shared/effect-constants"; import { OutputDataType, VariableCategory } from "../../../../../shared/variable-constants"; const triggers = {}; -triggers[EffectTrigger.EVENT] = ["twitch:channel-reward-redemption"]; +triggers[EffectTrigger.EVENT] = [ + "twitch:channel-reward-redemption", + "twitch:channel-reward-redemption-fulfilled", + "twitch:channel-reward-redemption-canceled" +]; triggers[EffectTrigger.CHANNEL_REWARD] = true; triggers[EffectTrigger.PRESET_LIST] = true; triggers[EffectTrigger.MANUAL] = true; diff --git a/src/backend/variables/builtin/twitch/reward/reward-redemption-id.ts b/src/backend/variables/builtin/twitch/reward/reward-redemption-id.ts index 2a4e84357..6cc559b12 100644 --- a/src/backend/variables/builtin/twitch/reward/reward-redemption-id.ts +++ b/src/backend/variables/builtin/twitch/reward/reward-redemption-id.ts @@ -2,7 +2,11 @@ import { EffectTrigger } from "../../../../../shared/effect-constants"; import { OutputDataType, VariableCategory } from "../../../../../shared/variable-constants"; const triggers = {}; -triggers[EffectTrigger.EVENT] = ["twitch:channel-reward-redemption"]; +triggers[EffectTrigger.EVENT] = [ + "twitch:channel-reward-redemption", + "twitch:channel-reward-redemption-fulfilled", + "twitch:channel-reward-redemption-canceled" +]; triggers[EffectTrigger.CHANNEL_REWARD] = true; triggers[EffectTrigger.PRESET_LIST] = true; triggers[EffectTrigger.MANUAL] = true; From 853f8ade7056c99ee44fe7c0fdc463b5a51e599b Mon Sep 17 00:00:00 2001 From: Dennis Rijsdijk Date: Thu, 30 May 2024 23:18:51 +0200 Subject: [PATCH 2/5] feat: per-reward effects for approve/reject when reward queue is enabled on twitch --- .../channel-rewards/channel-reward-manager.ts | 52 ++++++++++++++----- .../events/twitch-events/reward-redemption.ts | 2 + .../add-edit-channel-reward.js | 30 ++++++++++- src/types/channel-rewards.d.ts | 2 + 4 files changed, 72 insertions(+), 14 deletions(-) diff --git a/src/backend/channel-rewards/channel-reward-manager.ts b/src/backend/channel-rewards/channel-reward-manager.ts index f1bee5012..1aa4477e3 100644 --- a/src/backend/channel-rewards/channel-reward-manager.ts +++ b/src/backend/channel-rewards/channel-reward-manager.ts @@ -8,6 +8,7 @@ import { CustomReward, RewardRedemption, RewardRedemptionsApprovalRequest } from import { EffectTrigger } from "../../shared/effect-constants"; import { RewardRedemptionMetadata, SavedChannelReward } from "../../types/channel-rewards"; import { TriggerType } from "../common/EffectType"; +import { EffectList } from "../../types/effects"; class ChannelRewardManager { channelRewards: Record = {}; @@ -259,7 +260,29 @@ class ChannelRewardManager { return channelReward ? channelReward.id : null; } - async triggerChannelReward(rewardId: string, metadata: RewardRedemptionMetadata, manual = false): Promise { + private async triggerRewardEffects(metadata: RewardRedemptionMetadata, effectList?: EffectList, manual = false): Promise { + if (effectList == null || effectList.list == null) { + return; + } + + const effectRunner = require("../common/effect-runner"); + + const processEffectsRequest = { + trigger: { + type: manual ? EffectTrigger.MANUAL : EffectTrigger.CHANNEL_REWARD, + metadata: metadata + }, + effects: effectList + }; + + try { + return effectRunner.processEffects(processEffectsRequest); + } catch (reason) { + console.log(`error when running effects: ${reason}`); + } + } + + async triggerChannelReward(rewardId: string, metadata: RewardRedemptionMetadata, manual = false): Promise { const savedReward = this.channelRewards[rewardId]; if (savedReward == null || savedReward.effects == null || savedReward.effects.list == null) { return; @@ -325,22 +348,25 @@ class ChannelRewardManager { } } + return this.triggerRewardEffects(metadata, savedReward.effects, manual); + } - const effectRunner = require("../common/effect-runner"); + async triggerChannelRewardFulfilled(rewardId: string, metadata: RewardRedemptionMetadata, manual = false): Promise { + const savedReward = this.channelRewards[rewardId]; + if (savedReward == null) { + return; + } - const processEffectsRequest = { - trigger: { - type: manual ? EffectTrigger.MANUAL : EffectTrigger.CHANNEL_REWARD, - metadata: metadata - }, - effects: savedReward.effects - }; + return this.triggerRewardEffects(metadata, savedReward.effectsFulfilled, manual); + } - try { - return effectRunner.processEffects(processEffectsRequest); - } catch (reason) { - console.log(`error when running effects: ${reason}`); + async triggerChannelRewardCanceled(rewardId: string, metadata: RewardRedemptionMetadata, manual = false): Promise { + const savedReward = this.channelRewards[rewardId]; + if (savedReward == null) { + return; } + + return this.triggerRewardEffects(metadata, savedReward.effectsCanceled, manual); } async refreshChannelRewardRedemptions(): Promise { diff --git a/src/backend/events/twitch-events/reward-redemption.ts b/src/backend/events/twitch-events/reward-redemption.ts index b2b6b462d..ce7839443 100644 --- a/src/backend/events/twitch-events/reward-redemption.ts +++ b/src/backend/events/twitch-events/reward-redemption.ts @@ -83,8 +83,10 @@ export function handleRewardUpdated( // Possible values for status are 'fulfilled' and 'canceled' according to Twitch docs if (status === 'fulfilled') { + rewardManager.triggerChannelRewardFulfilled(rewardId, redemptionMeta); eventManager.triggerEvent("twitch", "channel-reward-redemption-fulfilled", redemptionMeta); } else { + rewardManager.triggerChannelRewardCanceled(rewardId, redemptionMeta); eventManager.triggerEvent("twitch", "channel-reward-redemption-canceled", redemptionMeta); } } \ No newline at end of file diff --git a/src/gui/app/directives/modals/channel-rewards/add-edit-channel-reward.js b/src/gui/app/directives/modals/channel-rewards/add-edit-channel-reward.js index a8be8bcf6..d0d7e259a 100644 --- a/src/gui/app/directives/modals/channel-rewards/add-edit-channel-reward.js +++ b/src/gui/app/directives/modals/channel-rewards/add-edit-channel-reward.js @@ -223,13 +223,33 @@ -
+ + + +
+ + + + + + +
@@ -309,6 +329,14 @@ $ctrl.reward.effects = effects; }; + $ctrl.fulfilledEffectListUpdated = function(effects) { + $ctrl.reward.effectsFulfilled = effects; + }; + + $ctrl.canceledEffectListUpdated = function(effects) { + $ctrl.reward.effectsCanceled = effects; + }; + $ctrl.$onInit = () => { if ($ctrl.resolve.reward != null) { $ctrl.reward = JSON.parse(angular.toJson($ctrl.resolve.reward)); diff --git a/src/types/channel-rewards.d.ts b/src/types/channel-rewards.d.ts index 15b950226..8f055f92e 100644 --- a/src/types/channel-rewards.d.ts +++ b/src/types/channel-rewards.d.ts @@ -7,6 +7,8 @@ export type SavedChannelReward = { twitchData: CustomReward, manageable: boolean, effects?: EffectList, + effectsFulfilled?: EffectList, + effectsCanceled?: EffectList, restrictionData?: RestrictionData, autoApproveRedemptions?: boolean, }; From bb96d1bb1e3149055edd2466a6570401205d54b2 Mon Sep 17 00:00:00 2001 From: Dennis Rijsdijk Date: Thu, 30 May 2024 23:55:30 +0200 Subject: [PATCH 3/5] chore: clarify verbiage regarding twitch reward queue --- .../modals/channel-rewards/add-edit-channel-reward.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/gui/app/directives/modals/channel-rewards/add-edit-channel-reward.js b/src/gui/app/directives/modals/channel-rewards/add-edit-channel-reward.js index d0d7e259a..08c698caf 100644 --- a/src/gui/app/directives/modals/channel-rewards/add-edit-channel-reward.js +++ b/src/gui/app/directives/modals/channel-rewards/add-edit-channel-reward.js @@ -95,8 +95,9 @@
- +

If enabled, only future viewer requests will skip the queue for review.

+

Requests will immediately be approved by Twitch and cannot be refunded.

From 8768c40f372e88488561f81e176eadaf11f48df6 Mon Sep 17 00:00:00 2001 From: Dennis Rijsdijk Date: Mon, 15 Jul 2024 22:52:41 +0200 Subject: [PATCH 4/5] chore: UI consistency pass --- .../add-edit-channel-reward.js | 94 +++++++++++++------ .../app/services/channel-rewards.service.js | 1 + 2 files changed, 66 insertions(+), 29 deletions(-) diff --git a/src/gui/app/directives/modals/channel-rewards/add-edit-channel-reward.js b/src/gui/app/directives/modals/channel-rewards/add-edit-channel-reward.js index 08c698caf..25440e385 100644 --- a/src/gui/app/directives/modals/channel-rewards/add-edit-channel-reward.js +++ b/src/gui/app/directives/modals/channel-rewards/add-edit-channel-reward.js @@ -10,7 +10,7 @@