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/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..ce7839443 100644 --- a/src/backend/events/twitch-events/reward-redemption.ts +++ b/src/backend/events/twitch-events/reward-redemption.ts @@ -52,4 +52,41 @@ 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') { + 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/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; 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..ed9de4460 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 @@