fp/services/our/src/tasks/consolidate_twitch_channel_rewards.human.ts.noexec

297 lines
9.8 KiB
Plaintext

// src/tasks/consolidate_twitch_channel_rewards.ts
import type { Task, Helpers } from "graphile-worker";
import { PrismaClient, User, type Pick } from '../../generated/prisma'
import { withAccelerate } from "@prisma/extension-accelerate"
import { env } from "../config/env";
import { constants } from "../config/constants";
import { getRateLimiter } from "../utils/rateLimiter";
const prisma = new PrismaClient().$extends(withAccelerate())
const cprPath = env.TWITCH_MOCK ? constants.twitch.dev.paths.channelPointRewards : constants.twitch.prod.paths.channelPointRewards
interface Payload {
userId: number;
}
export interface TwitchChannelPointReward {
id: string;
broadcaster_id: string;
broadcaster_login: string;
broadcaster_name: string;
image: string | null;
background_color: string;
is_enabled: boolean;
cost: number;
title: string;
prompt: string;
is_user_input_required: boolean;
max_per_stream_setting: {
is_enabled: boolean;
max_per_stream: number;
};
max_per_user_per_stream_setting: {
is_enabled: boolean;
max_per_user_per_stream: number;
};
global_cooldown_setting: {
is_enabled: boolean;
global_cooldown_seconds: number;
};
is_paused: boolean;
is_in_stock: boolean;
default_image: {
url_1x: string;
url_2x: string;
url_4x: string;
};
should_redemptions_skip_request_queue: boolean;
redemptions_redeemed_current_stream: number | null;
cooldown_expires_at: string | null;
}
function getAuthToken(user: User) {
const authToken = env.TWITCH_MOCK ? env.TWITCH_MOCK_USER_ACCESS_TOKEN : user.twitchToken?.accessToken
return authToken
}
function assertPayload(payload: any): asserts payload is Payload {
if (typeof payload !== "object" || !payload) throw new Error("invalid payload");
if (typeof payload.userId !== "number") throw new Error("invalid payload.userId");
}
async function getTwitchChannelPointRewards(user: User) {
if (!user) throw new Error(`getTwitchChannelPointRewards called with falsy user`);
if (!user.twitchToken) throw new Error(`user.twitchToken is not existing, when it needs to.`);
const authToken = getAuthToken(user)
const limiter = getRateLimiter()
await limiter.consume('twitch', 1)
// Create the custom Channel Point Reward on Twitch.
// @see https://dev.twitch.tv/docs/api/reference/#create-custom-rewards
// POST https://api.twitch.tv/helix/channel_points/custom_rewards
const query = new URLSearchParams({
broadcaster_id: user.twitchId
})
const res = await fetch(`${env.TWITCH_API_ORIGIN}${cprPath}?${query}`, {
headers: {
'Authorization': `Bearer ${authToken}`,
'Client-Id': env.TWITCH_CLIENT_ID
}
})
if (!res.ok) {
console.error(`failed to get a custom channel point rewards for user id=${user.id}`)
console.error(res.statusText)
throw new Error(res.statusText);
}
const data = await res.json()
return data
}
async function postTwitchChannelPointRewards(user: User, pick: Pick) {
const authToken = getAuthToken(user)
const limiter = getRateLimiter()
await limiter.consume('twitch', 1)
// Create the custom Channel Point Reward on Twitch.
// @see https://dev.twitch.tv/docs/api/reference/#create-custom-rewards
// POST https://api.twitch.tv/helix/channel_points/custom_rewards
const query = new URLSearchParams({
broadcaster_id: user.twitchId
})
const res = await fetch(`${env.TWITCH_API_ORIGIN}${cprPath}?${query}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Client-Id': env.TWITCH_CLIENT_ID
},
body: JSON.stringify({
cost: user.redeemCost,
title: pick.waifu.name
})
})
if (!res.ok) {
console.error(`failed to create a custom channel point reward for userId=${user.id}`)
console.error(res.statusText)
throw new Error(res.statusText);
}
// Associate the twitch channel point reward with our Pick
const data = await res.json()
const twitchChannelPointRewardId = data.data.at(0).id
await prisma.pick.update({
where: {
id: pick.id
},
data: {
twitchChannelPointRewardId
}
})
}
// * filter rewards which we previously created
const isWaifusChannelPointReward = (reward: TwitchChannelPointReward, picks: Pick[]) => {
return picks.some((pick) => pick.twitchChannelPointRewardId === reward.id)
}
// * filter rewards which should no longer be displayed
// * delete
// * filter rewards which have the wrong redeemCost
// * update so they have the correct redeemCost
const isOutOfDateReward = (
reward: TwitchChannelPointReward,
picks: Pick[],
waifuChoicePoolSize: number
): boolean => {
const currentPicks = picks.slice(0, waifuChoicePoolSize);
console.log('currentPicks as follows')
console.log(currentPicks)
return !currentPicks.some(pick => pick.twitchChannelPointRewardId === reward.id);
};
const isWrongRedeemCost = (reward: TwitchChannelPointReward, redeemCost: number) => reward.cost !== redeemCost
// @see https://dev.twitch.tv/docs/api/reference/#delete-custom-reward
// DELETE https://api.twitch.tv/helix/channel_points/custom_rewards
async function deleteTwitchChannelPointReward(user: User, reward: TwitchChannelPointReward) {
const limiter = getRateLimiter()
await limiter.consume('twitch', 1)
const authToken = getAuthToken(user)
const query = new URLSearchParams({
broadcaster_id: user.twitchId,
id: reward.id
})
const res = await fetch(`${env.TWITCH_API_ORIGIN}${cprPath}?${query}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`,
'Client-Id': env.TWITCH_CLIENT_ID
}
})
if (!res.ok) {
throw new Error(`Failed to delete twitch channel point reward.id=${reward.id} for user.id=${user.id} (user.twitchId=${user.twitchId}) `);
}
}
// @see https://dev.twitch.tv/docs/api/reference/#update-custom-reward
async function updateTwitchChannelPointReward(user: User, reward: TwitchChannelPointReward) {
const limiter = getRateLimiter()
await limiter.consume('twitch', 1)
const authToken = getAuthToken(user)
const query = new URLSearchParams({
broadcaster_id: user.twitchId,
id: reward.id
})
const res = await fetch(`${env.TWITCH_API_ORIGIN}${cprPath}?${query}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${authToken}`,
'Client-Id': env.TWITCH_CLIENT_ID
},
body: JSON.stringify({
cost: user.redeemCost
})
})
if (!res.ok) {
throw new Error(`Failed to update twitch channel point reward.id=${reward.id} with redeemCost=${user.redeemCost} for user.id=${user.id} (user.twitchId=${user.twitchId}) `);
}
}
/**
*
* consolidate_twitch_channel_rewards
*
* This script is meant to run via crontab.
* It finds Users with Picks that lack a twitchChannelPointRewardId, then
*
*
* @param payload
* @param helpers
*/
export default async function consolidate_twitch_channel_rewards(payload: any, helpers: Helpers) {
assertPayload(payload);
const { userId } = payload;
// helpers.logger.info(`Hello, ${name}`);
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId
},
include: {
twitchToken: true
}
})
// * get the current number of picks
const picks = await prisma.pick.findMany({
where: {
userId
},
take: constants.twitch.maxChannelPointRewards,
orderBy: {
createdAt: 'desc'
}
})
// * get the user's configured redeemCost
const redeemCost = user.redeemCost
const twitchId = user.twitchId
// * get the user's currently configured twitch channel point rewards
const twitchChannelPointRewards = await getTwitchChannelPointRewards(user)
const tcpr = twitchChannelPointRewards.data.map((cpr) => ({ id: cpr.id, cost: cpr.cost, is_in_stock: cpr.is_in_stock, title: cpr.title }))
// * identify the actions we need to do to get the channel point rewards up-to-date
const twitchRewards = await getTwitchChannelPointRewards(user);
const twitchRewardsData = twitchRewards.data;
console.log(`User ${userId} has ${picks.length} picks. And ${twitchRewardsData.length} twitch rewards. waifuChoicePoolSize=${user.waifuChoicePoolSize}, maxOnScreenWaifus=${user.maxOnScreenWaifus}`)
const currentPicks = picks.slice(0, user.waifuChoicePoolSize);
const outOfDate = twitchRewardsData.filter((reward: TwitchChannelPointReward) =>
picks.some(p => p.twitchChannelPointRewardId === reward.id) &&
!currentPicks.some(p => p.twitchChannelPointRewardId === reward.id)
);
console.log(`outOfDate as follows`)
console.log(outOfDate)
const costMismatched = twitchRewardsData
.filter((r: TwitchChannelPointReward) => isWrongRedeemCost(r, user.redeemCost));
helpers.logger.info(`There are ${outOfDate.length} out of date Channel Point Rewards. outOfDate=${JSON.stringify(outOfDate.map((ood) => ({ title: ood.title, cost: ood.cost, id: ood.id })))}`)
helpers.logger.info(`costMismatched=${JSON.stringify(costMismatched)}`)
// * make the REST request(s) to get the twitch channel point rewards up-to-date
for (const reward of outOfDate) {
console.log(`deleting reward.id=${reward.id} with reward.title=${reward.title}`)
await deleteTwitchChannelPointReward(user, reward)
}
for (const reward of costMismatched) {
console.log(`updating reward.id=${reward.id} with reward.title=${reward.title}`)
await updateTwitchChannelPointReward(user, reward)
}
};