diff --git a/.env-sample b/.env-sample index 94c145b..5b84ae3 100644 --- a/.env-sample +++ b/.env-sample @@ -1 +1,2 @@ -TWITCH_ACCESS_TOKEN= \ No newline at end of file +TwitchAccessToken= +TwitchClientId= \ No newline at end of file diff --git a/index.js b/index.js deleted file mode 100644 index 0bf45ba..0000000 --- a/index.js +++ /dev/null @@ -1,182 +0,0 @@ -const got = require('got') -require('dotenv').config() - -let token = process.env['TWITCH_ACCESS_TOKEN'] - -const customRewardBody = { - title: "Sample: Follow me!", - prompt: "Follows the requesting user!", - cost: 10 * 1000 * 1000, - is_enabled: true, - is_global_cooldown_enabled: true, - global_cooldown_seconds: 10 * 60, -} - - -let clientId = "" -let userId = "" -let headers = {} -let rewardId = "" -let pollingInterval - -// validates the provided token and validates the token has the correct scope(s). additionally, uses the response to pull the correct client_id and broadcaster_id -const validateToken = async () => { - let r - try { - let { body } = await got(`https://id.twitch.tv/oauth2/validate`, { - headers: { - "Authorization": `Bearer ${token}` - } - }) - r = JSON.parse(body) - } catch (error) { - console.log('Invalid token. Please get a new token using twitch token -u -s "channel:manage:redemptions user:edit:follows"') - return false - } - - if(r.scopes.indexOf("channel:manage:redemptions") == -1 || r.scopes.indexOf("user:edit:follows") == -1 || !r.hasOwnProperty('user_id')){ - console.log('Invalid scopes. Please get a new token using twitch token -u -s "channel:manage:redemptions user:edit:follows"') - return false - } - - // update the global variables to returned values - clientId = r.client_id - userId = r.user_id - headers = { - "Authorization": `Bearer ${token}`, - "Client-ID": clientId, - "Content-Type": "application/json" - } - - return true -} - -// returns an object containing the custom rewards, or if an error, null -const getCustomRewards = async () => { - try { - let { body } = await got(`https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id=${userId}`, { headers: headers }) - return JSON.parse(body).data - } catch (error) { - console.log(error) - return null - } -} - -// if the custom reward doesn't exist, creates it. returns true if successful, false if not -const addCustomReward = async () => { - try { - let { body } = await got.post(`https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id=${userId}`, { - headers: headers, - body: JSON.stringify(customRewardBody), - responseType: 'json', - }) - - rewardId = body.data[0].id - return true - } catch (error) { - console.log("Failed to add the reward. Please try again.") - return false - } -} - -// function for polling every 15 seconds to check for user redemptions -const pollForRedemptions = async () => { - try { - let { body } = await got(`https://api.twitch.tv/helix/channel_points/custom_rewards/redemptions?broadcaster_id=${userId}&reward_id=${rewardId}&status=UNFULFILLED`, { - headers: headers, - responseType: 'json', - }) - - let redemptions = body.data - let successfulRedemptions = [] - let failedRedemptions = [] - - for (let redemption of redemptions) { - // can't follow yourself :) - if (redemption.broadcaster_id == redemption.user_id) { - failedRedemptions.push(redemption.id) - continue - } - // if failed, add to the failed redemptions - if (await followUser(redemption.broadcaster_id, redemption.user_id) == false) { - failedRedemptions.push(redemption.id) - continue - } - // otherwise, add to the successful redemption list - successfulRedemptions.push(redemption.id) - } - - // do this in parallel - await Promise.all([ - fulfillRewards(successfulRedemptions, "FULFILLED"), - fulfillRewards(failedRedemptions, "CANCELED") - ]) - - console.log(`Processed ${successfulRedemptions.length + failedRedemptions.length} redemptions.`) - - // instead of an interval, we wait 15 seconds between completion and the next call - pollingInterval = setTimeout(pollForRedemptions, 15 * 1000) - } catch (error) { - console.log("Unable to fetch redemptions.") - } -} - -// Follows from the user (fromUser) to another user (toUser). Returns true on success, false on failure -const followUser = async (fromUser, toUser) => { - try { - await got.post(`https://api.twitch.tv/helix/users/follows?from_id=${fromUser}&to_id=${toUser}`, { headers: headers }) - return true - } catch (error) { - console.log(`Unable to follow user ${toUser}`) - return false - } -} - -const fulfillRewards = async (ids, status) => { - // if empty, just cancel - if (ids.length == 0) { - return - } - - // transforms the list of ids to ids=id for the API call - ids = ids.map(v => `id=${v}`) - - try { - await got.patch(`https://api.twitch.tv/helix/channel_points/custom_rewards/redemptions?broadcaster_id=${userId}&reward_id=${rewardId}&${ids.join("&")}`, { - headers, - json: { - status: status - } - }) - } catch (error) { - console.log(error) - } -} - -// main function - sets up the reward and sets the interval for polling -const main = async () => { - if (await validateToken() == false) { - return - } - - let rewards = await getCustomRewards() - if (rewards != null) { - rewards.forEach(v => { - // since the title is enforced as unique, it will be a good identifier to use to get the right ID on cold-boot - if (v.title == customRewardBody.title) { - rewardId = v.id - } - }) - }else{ - console.log("The streamer does not have access to Channel Points. They need to be a Twitch Affiliate or Partner."); - } - // if the reward isn't set up, add it - if (rewardId == "" && await addCustomReward() == false) { - return - } - - pollForRedemptions() -} - -// start the script -main() diff --git a/package.json b/package.json index cf511ac..3b6a211 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,13 @@ "description": "", "main": "index.js", "scripts": { - "start":"node index.js" + "start": "node src/index.js" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "dotenv": "^8.2.0", - "got": "^11.8.0" + "axios": "^0.24.0" } -} +} \ No newline at end of file diff --git a/src/Class/TwitchTools.js b/src/Class/TwitchTools.js new file mode 100644 index 0000000..1859303 --- /dev/null +++ b/src/Class/TwitchTools.js @@ -0,0 +1,159 @@ +const axios = require("axios") + +class TwitchTools { + constructor(TwitchAccessToken, TwitchClientId, customRewardBody = {}) { + this.headers = { + "Authorization": `Bearer ${TwitchAccessToken}`, + "Client-ID": TwitchClientId, + "Content-Type": "application/json" + } + + this.customRewardBody = customRewardBody + + this.followUser = async (fromUser, toUser) => { + try { + await axios.post(`https://api.twitch.tv/helix/users/follows?from_id=${fromUser}&to_id=${toUser}`, { + headers: this.headers + }) + return true + } catch (error) { + console.log(`Unable to follow user ${toUser}`) + return false + } + } + + } + + async validateToken() { + try { + await axios.get(`https://id.twitch.tv/oauth2/validate`, { + headers: this.headers + }) + + return true + } catch (err) { + console.log('Invalid token. Please get a new token using twitch token -u -s "channel:manage:redemptions user:edit:follows"'); + return null + } + } + + async GetUserIdTwitch() { + try { + const responseTwitch_id = await axios.get(`https://id.twitch.tv/oauth2/validate`, { + headers: this.headers + }) + + return responseTwitch_id.data.user_id + } catch (err) { + return null + } + } + + async getCustomRewards() { + try { + // user_id + const userId = await this.GetUserIdTwitch() + + if (userId !== null) { + const responseTwitch_CustomRewards = await axios.get(`https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id=${userId}`, { + headers: this.headers + }) + + return responseTwitch_CustomRewards.data + } + + return `UserId null` + } catch (error) { + console.log(error.response.data.message); + return null + } + } + + async addCustomReward() { + try { + const userId = await this.GetUserIdTwitch() + + const reponseTwtich_AddCustomReward = await axios.post(`https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id=${userId}`, this.customRewardBody, { + headers: this.headers + }) + + return { + rewardId: reponseTwtich_AddCustomReward.data[0].id, + success: true + } + } catch (error) { + console.log(error.response.data.message); + return false + } + } + + async pollForRedemptions() { + try { + const userId = await this.GetUserIdTwitch() + const data = await this.addCustomReward() + + const responseTwitch_PollForRedemptions = await axios.post(`https://api.twitch.tv/helix/channel_points/custom_rewards/redemptions?broadcaster_id=${userId}&reward_id=${data.rewardId}&status=UNFULFILLED`, { + headers: this.headers + }) + + let redemptions = responseTwitch_PollForRedemptions.data + let successfulRedemptions = [] + let failedRedemptions = [] + + for (let redemption of redemptions) { + // can't follow yourself :) + if (redemption.broadcaster_id == redemption.user_id) { + failedRedemptions.push(redemption.id) + continue + } + // if failed, add to the failed redemptions + if (await this.followUser(redemption.broadcaster_id, redemption.user_id) == false) { + failedRedemptions.push(redemption.id) + continue + } + // otherwise, add to the successful redemption list + successfulRedemptions.push(redemption.id) + } + + // do this in parallel + await Promise.all([ + this.fulfillRewards(successfulRedemptions, "FULFILLED"), + this.fulfillRewards(failedRedemptions, "CANCELED") + ]) + + console.log(`Processed ${successfulRedemptions.length + failedRedemptions.length} redemptions.`) + + // instead of an interval, we wait 15 seconds between completion and the next call + setTimeout(this.pollForRedemptions, 15 * 1000) + } catch (error) { + console.log("Unable to fetch redemptions.") + } + } + + async fulfillRewards(ids, status) { + // if empty, just cancel + if (ids.length == 0) { + return + } + + // transforms the list of ids to ids=id for the API call + ids = ids.map(v => `id=${v}`) + + const userId = await this.GetUserIdTwitch() + const data = await this.addCustomReward() + + try { + await axios.patch(`https://api.twitch.tv/helix/channel_points/custom_rewards/redemptions?broadcaster_id=${userId}&reward_id=${data.rewardId}&${ids.join("&")}`, { + headers: this.headers, + json: { + status: status + } + }) + } catch (error) { + console.log(error) + } + } + +} + +module.exports = TwitchTools \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..14bcf88 --- /dev/null +++ b/src/index.js @@ -0,0 +1,40 @@ +const TwitchTools = require("./Class/TwitchTools"); +require("dotenv").config() + +const customRewardBody = { + title: "Sample: Follow me!", + prompt: "Follows the requesting user!", + cost: 10 * 1000 * 1000, + is_enabled: true, + is_global_cooldown_enabled: true, + global_cooldown_seconds: 10 * 60, +} + +const ToolsTwitch = new TwitchTools(process.env.TwitchAccessToken, process.env.TwitchClientId, customRewardBody) + +// main function - sets up the reward and sets the interval for polling +const main = async () => { + if (await ToolsTwitch.validateToken() === null) return + let rewardId = "" + + let rewards = await ToolsTwitch.getCustomRewards() + if (rewards !== null) { + rewards.forEach(v => { + // since the title is enforced as unique, it will be a good identifier to use to get the right ID on cold-boot + if (v.title == customRewardBody.title) { + rewardId = v.id + } + }) + } else { + console.log("The streamer does not have access to Channel Points. They need to be a Twitch Affiliate or Partner."); + } + // if the reward isn't set up, add it + if (rewardId == "" && await ToolsTwitch.addCustomReward() === false) { + return + } + + ToolsTwitch.pollForRedemptions() +} + +// start the script +main() \ No newline at end of file diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..8310e9c --- /dev/null +++ b/test/test.js @@ -0,0 +1,23 @@ +const TwitchTools = require("../src/Class/TwitchTools"); + +// customRewardBody +const customRewardBody = { + title: "Sample: Follow me!", + prompt: "Follows the requesting user!", + cost: 10 * 1000 * 1000, + is_enabled: true, + is_global_cooldown_enabled: true, + global_cooldown_seconds: 10 * 60, +} + + +const ToolsTwitch = new TwitchTools('10z462rarqn42whrqd0cbpuhfyifdk','faxsoydixvpqrncuis1a76u6czvxhr') + +// test validadeToken +// ToolsTwitch.GetUserIdTwitch().then((res) => console.log(res)) + +// test getCustomRewards +// ToolsTwitch.getCustomRewards().then(res => console.log(res)) + +// test addCustomReward +ToolsTwitch.addCustomReward(customRewardBody).then(res => console.log(res)) \ No newline at end of file