Special Agent Squeaky

我是如何创建将 Twitch 剪辑发布到 Discord 的 !clip 命令的

我是如何为我的 Twitch 聊天创建 !clip 命令的,该命令可以创建剪辑并将其发布到 Discord!
嘿!您正在查看我网站的自动翻译版本!由于我网站的官方语言是英语(因为我只懂英语),我只想说,由于网站是自动翻译的,所以有些翻译可能会很奇怪,损坏甚至丢失!祝你有美好的一天!
请注意,这篇博文是在 June 2020 上发布的,所以根据您阅读它的时间,它的某些部分今天可能已经过时了。不幸的是,我无法让这些博客文章始终保持最新状态,以确保信息仍然准确。
So, I recently created a !clip command for my Twitch chat, that automatically created a clip and posted it to my Discord, using a Lambda function in Amazon Web Services. If you are interested in how I did that, then this guide might be for you!

If you don't want the hassle of creating your own

If you don't have the time or want to deal with the hassle of creating your own !clip command, I have created a service that creates one for people who support me. If you are interested, please read more about it on the tool page below.

Watch the video

There is also a YouTube video that goes through each step. It is not as detailed as this written guide, but could give a brief overview of what's needed.

Introduction to the written guide

So how do you make a coding guide fun, useful and most importantly comprehendible to understand? So, as an attempt to make it as clear as possible, I have broken down this guide into 9 steps, which we will go through together one by one.
However there are some prerequisites for this to be even feasible:
  • We obviously need to have a Twitch account
  • We need to have a Discord server
  • We need to have an Amazon Web Services account
  • And the more knowledge we have about web development, the easier it will be to understand the steps
I won't go through how to create all these accounts, but it is fairly straightforward for each service.
Technically, this is how the solution works:
  • A chatbot listens to the !clip command
  • The chatbot sends an HTTP request to an AWS Lambda function
  • The Lambda function will talk to the Twitch APIs and ask Twitch to create a 30 second long clip
  • Then the function will post the link to that clip to the Discord server through a webhook
Also, please note that this is how I did this. It is possible to swap different parts out for other alternative solutions. For example, I use Amazon Web Services Lambdas (I'll explain what that is later on), but you can for example use Google Cloud Platform's cloud functions or Microsoft Azure Functions, or create your own standalone web server.
I also use Node.js for my Lambda code, but AWS Lambdas support multiple languages, etc. I use StreamElements chatbot, but you can use Streamlabs or Nightbot, etc.
But at least you will see how I personally did this.
One last comment. In this guide I will be showing passwords and secrets in plain text. Don't do this yourself. This information is sensitive so when you do this guide, please do not share these secrets with anyone. I can however do this, since I have already deleted everything, making the passwords useless.
Okay, so let's go!

Pricing and how much this will cost us

But before we jump in, let's talk about pricing and how much this would cost us.
So using Discord webhook and posting to Discord is free.
And, as of when I am doing this guide, AWS Lambda functions is in the AWS free tier, meaning if you do less than a million requests, it is also free! But do double check this, if you are from the future!

Step 1 - Creating a Discord webhook

In order to send data to a Discord server channel, we need to create a webhook. This is pretty straight forward.
As an Discord server administrator, simply go to the channel's settings of the channel you want the clip to be posted to and create a new webhook.
You can assign a name to the webhook, upload an image, but what we really want here is the webhook URL.
Please note that the Webhook URL is sensitive information, so do not share this to anyone, or else anyone can post data to your Discord channel!
The URL will look something similar to:
https://discordapp.com/api/webhooks/abc123/abc123
After you have created your webhook save this whole URL, as we will be using this later on. I just copied the URL, opened up Notepad and pasted it there.

Step 2 - Registering a Twitch application

In order to create clips programmatically, there first has to be a registered Twitch application.
Creating an application is pretty straight forward. Visit your "Twitch developer dashboard" and click on "Register Your App".
Pick an appropriate name (I took "ClipCommand") and for the "OAuth Redirect URL" type in "http://localhost/". Be very thorough with this as the next step won't work if there is a typo. Why it is localhost will make sense in a moment as well, and as the category I simply picked "Application Integration".
Once the application has been registered, go back into the application and you will see both the application's "Client ID" and "Secret". Copy and save both of them, as we will need them later on.
Also remember that both the "Client ID" and the "Secret", is sensitive information, so do not share this to anyone as well.

Step 3 - Giving the Twitch application the rights to create clips on our behalf

Now that we have registered our application, we need to give the application the rights to create clips on our behalf as a Twitch user. This will mean that any clips the application creates will look as if we manually did it with our user account.
In order to give our application the rights, we need to follow something called the "OAuth Authorization Code Flow". Our end goal here is to get something called the "Authorization Code", which we will be using later on.
Since we control both the user and the application, we can cheat the flow a bit and do this purely locally by using the host name "localhost" - that's why it was important the "OAuth Redirect URL" when registering our application was "http://localhost/".
So basically, we just need to enter this URL in our browser:
https://id.twitch.tv/oauth2/authorize?response_type=code&client_id=##CLIENT_ID##&redirect_uri=http://localhost/&scope=clips:edit
Replace the ##CLIENT_ID## with your application's "Client ID" above.
So in my particular example, the actual URL would be:
https://id.twitch.tv/oauth2/authorize?response_type=code&client_id=dooippdh2yqudw5uerq7ree4odbhyw&redirect_uri=http://localhost/&scope=clips:edit
Simply paste the URL in your browser and you will be directed to an "OAuth Authorization Screen", where you will be asked if our application ("ClipCommand") may create clips on our behalf.
After pressing "Authorize", Twitch will redirect us to "http://localhost/" - which probably will look like a broken page. But don't be afraid, because what we need is in the new URL in the browser, which will look something like this:
http://localhost/?code=3qei509yg5v2uzar2aibtalo3vwiet&scope=clips%3Aedit
What we need here is to grab the value of the query parameter code. This is the "Authorization Code" we were looking for.
So in our case, our "Authorization Code" is 3qei509yg5v2uzar2aibtalo3vwiet. Save this and as always, this is sensitive information so do not share this with anyone.

Step 4 - Getting the refresh token for our code

With the above "Authorization Code" we can now get something called the "User Access Token". This is done by simply doing a specific HTTP POST request to Twitch's OAuth API:
https://id.twitch.tv/oauth2/token?client_id=##CLIENT_ID##&client_secret=##CLIENT_SECRET##&code=##AUTH_CODE##&grant_type=authorization_code&redirect_uri=http://localhost/
Again, replace the ##CLIENT_ID## and ##CLIENT_SECRET## with your application's "Client ID" and "Secret" above, and ##AUTH_CODE## with the "Authorization Code" you got above.
If you are a web developer, you know there are a bunch of online and offline tools that can do HTTP requests. Pick any you want.
What's important here is that when doing this request we will get both an "Access token" and a "Refresh token". The "Access token" is only valid for slightly over 4 hours (you can see this by inspecting the "expires_in" value). That's no good for us, since we need to be able to create clips at any time, without doing this whole process over again. So that's why we are only interested in the "Refresh token".
In this guide though, I have prepared a JavaScript snippet that does this for us which we simply need to run, for example, over at JSFiddle:
var clientID = prompt("Enter the application's Client ID"); var secret = prompt("Enter the application's Secret"); var authCode = prompt("Enter the authorization code"); var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState === 4) { if (this.status === 200) { console.log("response", this.responseText); var data = JSON.parse(this.responseText); alert("Your refresh token is:\n\n" + data.refresh_token + "\n\n"); } else { alert("Something went wrong please check the browser request logs."); } } }; var url = "https://id.twitch.tv/oauth2/token?client_id=" + clientID + "&client_secret=" + secret + "&code=" + authCode + "&grant_type=authorization_code&redirect_uri=http://localhost/"; xhttp.open("POST", url, true); xhttp.send();
After running this, it should end with displaying the "Refresh token" for us.
We can use this "Refresh Token" each time we do a request to Twitch, in order to generate a new "Access token". This will allow us to send requests on behalf of the user even though it's gone more than 4 hours between each time.
So technically this means that each time we want Twitch to create a clip, we will be sending two requests; one to refresh and get a new access token, and then a second request to actually create a clip. However, normally you don't refresh the token unless you know it's expired, but in this guide, we will do both.

Step 5 - Getting the Twitch channel's Broadcast ID

Each Twitch channel has a fancy name, such as "SpecialAgentSqueaky", however technically each channel also has a "Broadcast ID" and this "Broadcast ID" is what is used when dealing with the Twitch APIs.
This can easily be fetched, again using the Twitch APIs, by visiting this URL in a browser:
https://api.twitch.tv/kraken/users/?api_version=5&client_id=##CLIENT_ID##&login=##CHANNEL_NAME##
Again, replace ##CLIENT_ID## with your application's client ID and ##CHANNEL_NAME## with your own channel.
In the response we are looking for _id. That is the channel's Broadcast ID.
As usual, I have prepared a JavaScript snippet that does this for us:
var clientID = prompt("Enter the application's Client ID"); var channelName = prompt("Enter Twitch channel name"); var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState === 4) { if (this.status === 200) { console.log("response", this.responseText); var data = JSON.parse(this.responseText); alert("The Broadcast ID is:\n\n" + data.users[0]._id + "\n\n"); } else { alert("Something went wrong please check the browser request logs."); } } }; var url = "https://api.twitch.tv/kraken/users/?api_version=5&client_id=" + clientID + "&login=" + channelName; xhttp.open("GET", url, true); xhttp.send();
Save the Broadcast ID, as we will be using this later.

Step 6 - Creating the Lambda function

This step requires that you are somewhat familiar with Amazon Web Services and Lambda functions.
A Lambda function is basically code we have uploaded to a cloud infrastructure (in our case Amazon Web Services) that does not require a traditional web server to run, but still can be invoked by an event. In our case, the event is a simple HTTP request.
In this section, we will simply create and prepare a Lambda function, which is pretty straight forward in Amazon Web Services. We simply head over to our "Lambda management console" and hit the "Create function".
Create one from scratch and enter a function name. In our case we will be using Node.js. However you can do this any language you want. The principles are the same.
This will create a new Lambda function, with a generic "Hello from Lambda" default code. This is great, but there is no way to actually trigger this function yet. So we need to add an "API gateway" and make it "Open".
Once we save the function, it will give us a generic URL which will invoke the function.
In my specific case, my URL became:
https://pk35ltpn14.execute-api.eu-north-1.amazonaws.com/default/my-clip-command
So if we simply visit the URL in our browser, we will see a "Hello from Lambda!" response, as expected.
This is okay for now, as we will come back and add the real code later on. Just save the URL for the next step.
Finally, we also need to increase the timeout time for how long a lambda function can be executed.
Since Twitch needs time to actually create the clip in their systems, our final code Lambda will do a small delay (specified in the DELAY_TO_POST_TO_DISCORD setting) before posting the message to Discord, giving time necessary for Twitch to create the video and thumbnail.
To increase the timeout, scroll down to "Basic settings" and change the "Timeout" setting from 3 seconds to, for example, 1 minute.

Step 7 - Creating the !clip command for our chat bot

Since I am using StreamElements, I will be showing how I did it using their bot. However, the principle is pretty much the same for any other chatbot, for example Streamlabs or Nightbot.
All we need to do is simply create a !clip command that triggers the URL of our newly created Lambda function.
So for StreamElements, simply head over to the "Chat commands" and then "Custom commands" section of the bot and create a new command with the keyword !clip.
As the response, use the $urlfetch function with the URL, but we will also append the user's name as a query parameter. So the full command will be:
$(urlfetch https://pk35ltpn14.execute-api.eu-north-1.amazonaws.com/default/my-clip-command?user=${user.name})
Save the command, and if we type !clip in chat now, we should see "Hello from Lambda!" popping up.

Step 8 - Getting the Discord emote ID

Step 8 is really a bonus step, and we might be getting a little ahead of ourselves here, but this step adds more flavor for when clips are being posted in Discord, so just bare with me, and you will soon understand why.
So let's say we want to use a specific emote when posting to Discord. In order to do that, we need to figure out the emote's specific ID. Each emote has a very long unique ID in Discord.
To get that ID, simply open up Discord, post the emote we want to use, then right click on the mote and pick "Copy URL". Then paste in the URL so we can grab the ID. Save the ID for later use.

Step 9 - Actually adding the code that ties everything together

Now comes the part where we actually tie everything together, and where we will be using all the IDs we have fetched along the way.
So we all know any code can be written in a million different ways, and the code I wrote for this project is pretty ad hoc - but it works. Feel free to read, learn, modify and rewrite the whole code if you want. It's totally up to you.
However in this guide, we are basically just going to copy the entire block of code I have written and paste it directly into the Lambda function.
So let's start by copying and pasting the code found below in the guide.
Once that is done, there is a section at the top of the code, we will simply add each value into their respective variables. Most variables are self explanatory, but take particular note on how I use the Discord emote ID in the message sent to Discord.
When everything is replaced, simply save the function and test the command in your chat.
Everything should be working!

The full Lambda function code

Here is the full source code of the Lambda function.
// License MIT, Author Special Agent Squeaky (specialagentsqueaky.com), Last updated 2020-11-25 const https = require("https"); /* * Please add all the necessary values below * -------------------------------------------------- */ const APP_CLIENT_ID = ""; const APP_CLIENT_SECRET = ""; const APP_REFRESH_TOKEN = ""; const DISCORD_WEBHOOK_ID = ""; const DISCORD_WEBHOOK_TOKEN = ""; const CHANNEL_BROADCAST_ID = ""; const DELAY_TO_POST_TO_DISCORD = 4 * 1000; // Twitch needs time to create the clip, so this defines how long time in ms until a message is posted to Discord const POST_MESSAGE_TWITCH_CHAT = () => { // It is possible to use channel emotes here, but the bot needs to be a subscriber return "A new clip was created in the Discord server! :)"; }; const POST_MESSAGE_DISCORD = ( username, clipURL ) => { return "A new clip was created" + (username ? " by @" + username : "") + "! :)\n\n" + clipURL; }; /* * -------------------------------------------------- */ const ERROR_TYPE_TWITCH_CHANNEL_OFFLINE = 1; async function getRefreshedAccessToken() { const response = await doRequest( "POST", "id.twitch.tv", "/oauth2/token?grant_type=refresh_token&refresh_token=" + APP_REFRESH_TOKEN + "&client_id=" + APP_CLIENT_ID + "&client_secret=" + APP_CLIENT_SECRET, undefined, undefined ); const json = JSON.parse(response); return json.access_token; } async function createTwitchClip( accessToken ) { try { const response = await doRequest( "POST", "api.twitch.tv", "/helix/clips?has_delay=false&broadcaster_id=" + CHANNEL_BROADCAST_ID, undefined, { "Authorization": "Bearer " + accessToken, "Client-ID": APP_CLIENT_ID, } ); const json = JSON.parse(response); console.log("create-twitch-clip-json", json); const clipData = json.data[0]; const clipID = clipData.id; const clipURL = "https://clips.twitch.tv/" + clipID; console.log("create-twitch-clip-clip-data=", clipData); console.log("create-twitch-clip-clip-id=", clipID); return { clipID, clipURL, }; } catch( error ) { if( typeof error === "string" && error.indexOf("Clipping is not possible for an offline channel.") !== -1 ) { const newError = new Error("Someone tried to clip while the channel is offline :ugh:"); newError.type = ERROR_TYPE_TWITCH_CHANNEL_OFFLINE; throw newError; } throw error; } } async function sendToDiscord( message ) { const postData = JSON.stringify({ "content": message, }); const path = "/api/webhooks/" + DISCORD_WEBHOOK_ID + "/" + DISCORD_WEBHOOK_TOKEN; await doRequest( "POST", "discordapp.com", path, postData, { "Content-Type": "application/json", } ); } function doRequest( method, hostname, path, postData, headers ) { return new Promise(( resolve, reject ) => { const options = { method, hostname, path, port: 443, headers, }; const request = https.request(options, ( response ) => { response.setEncoding("utf8"); let returnData = ""; response.on("data", ( chunk ) => { returnData += chunk; }); response.on("end", () => { if( response.statusCode < 200 || response.statusCode >= 300 ) { reject(returnData); } else { resolve(returnData); } }); response.on("error", ( error ) => { reject(error); }); }); if( postData ) { request.write(postData); } request.end(); }); } function wait( time ) { console.log("waiting"); return new Promise(( resolve, reject ) => { setTimeout(() => { console.log("wait done"); resolve(); }, time); }); } async function main( username ) { let accessToken; let responseClipURL; let messageDiscord; try { accessToken = await getRefreshedAccessToken(); } catch( error ) { console.error("problem-fetching-access-token", error); return "Unexpected problem when fetching the access token."; } try { console.log("accesstoken", accessToken); const response = await createTwitchClip(accessToken); const clipID = response.clipID; responseClipURL = response.clipURL; await wait(DELAY_TO_POST_TO_DISCORD); } catch( error ) { console.error("problem-creating-clip", error); if( typeof error === "string" && error.indexOf("{") === 0 ) { error = JSON.parse(error); // Twitch broke =( if( error.error === "Service Unavailable" && error.status === 503 ) { return "Twitch API didn't want to create a clip right now, you need to manually create the clip :("; } } if( error.type === ERROR_TYPE_TWITCH_CHANNEL_OFFLINE ) { return "I can't clip while the channel is offline :("; } return "Unexpected problem when creating the clip."; } try { messageDiscord = POST_MESSAGE_DISCORD(username, responseClipURL); await sendToDiscord(messageDiscord); } catch( error ) { console.error("problem-sending-to-discord", error); return "Unexpected problem when posting to Discord."; } try { const messageWeb = POST_MESSAGE_TWITCH_CHAT(); return messageWeb; } catch( error ) { console.error("problem-getting-twitch-chat-response", error); return "Unexpected problem getting response to Twitch chat"; } } exports.handler = async ( event ) => { const username = event["queryStringParameters"] && event["queryStringParameters"]["user"]; console.log("username", username); const message = await main(username); const response = { statusCode: 200, headers: { "content-type": "text/plain; charset=UTF-8" }, body: message, }; return response; };
我刚刚在 YouTube 上发布了一个新的游戏视频!请随时查看!
ENGLISHESPAÑOLPORTUGUÊSDEUTSCHFRANÇAISITALIANOРУССКОМ日本한국인中国人
本网站自动使用谷歌分析进行聚合网络分析跟踪,这也会创建浏览器 cookie。如果您不想被跟踪,请以隐身模式访问本网站。