Skip to content

Commit 6c2a8f5

Browse files
author
Leadbetter
committed
initial commit
0 parents  commit 6c2a8f5

File tree

4 files changed

+270
-0
lines changed

4 files changed

+270
-0
lines changed

.gitignore

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
lerna-debug.log*
8+
9+
# Diagnostic reports (https://nodejs.org/api/report.html)
10+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11+
12+
# Runtime data
13+
pids
14+
*.pid
15+
*.seed
16+
*.pid.lock
17+
18+
# Directory for instrumented libs generated by jscoverage/JSCover
19+
lib-cov
20+
21+
# Coverage directory used by tools like istanbul
22+
coverage
23+
*.lcov
24+
25+
# nyc test coverage
26+
.nyc_output
27+
28+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29+
.grunt
30+
31+
# Bower dependency directory (https://bower.io/)
32+
bower_components
33+
34+
# node-waf configuration
35+
.lock-wscript
36+
37+
# Compiled binary addons (https://nodejs.org/api/addons.html)
38+
build/Release
39+
40+
# Dependency directories
41+
node_modules/
42+
jspm_packages/
43+
44+
# Snowpack dependency directory (https://snowpack.dev/)
45+
web_modules/
46+
47+
# TypeScript cache
48+
*.tsbuildinfo
49+
50+
# Optional npm cache directory
51+
.npm
52+
53+
# Optional eslint cache
54+
.eslintcache
55+
56+
# Microbundle cache
57+
.rpt2_cache/
58+
.rts2_cache_cjs/
59+
.rts2_cache_es/
60+
.rts2_cache_umd/
61+
62+
# Optional REPL history
63+
.node_repl_history
64+
65+
# Output of 'npm pack'
66+
*.tgz
67+
68+
# Yarn Integrity file
69+
.yarn-integrity
70+
71+
# dotenv environment variables file
72+
.env
73+
.env.test
74+
75+
# parcel-bundler cache (https://parceljs.org/)
76+
.cache
77+
.parcel-cache
78+
79+
# Next.js build output
80+
.next
81+
out
82+
83+
# Nuxt.js build / generate output
84+
.nuxt
85+
dist
86+
87+
# Gatsby files
88+
.cache/
89+
# Comment in the public line in if your project uses Gatsby and not Next.js
90+
# https://nextjs.org/blog/next-9-1#public-directory-support
91+
# public
92+
93+
# vuepress build output
94+
.vuepress/dist
95+
96+
# Serverless directories
97+
.serverless/
98+
99+
# FuseBox cache
100+
.fusebox/
101+
102+
# DynamoDB Local files
103+
.dynamodb/
104+
105+
# TernJS port file
106+
.tern-port
107+
108+
# Stores VSCode versions used for testing VSCode extensions
109+
.vscode-test
110+
111+
# yarn v2
112+
.yarn/cache
113+
.yarn/unplugged
114+
.yarn/build-state.yml
115+
.yarn/install-state.gz
116+
.pnp.*

README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Sample Code for Twitch EventSub Webhooks using NodeJS
2+
3+
The purpose of this sample is to provide guidance on how to start using Twitch's new webhooks that are a part of [EventSub](https://dev.twitch.tv/docs/eventsub). The sample is built in NodeJS using the Express.js framework.
4+
5+
**This sample will cover**
6+
7+
1. Webhook Subscription creation
8+
2. Signature Verification
9+
3. Handling the callback verification challenge
10+
4. Handling webhook event data
11+
12+
## How to Use
13+
### Installation
14+
Run `npm install` to install the proyect's dependencies.
15+
16+
### Setting an https endpoint
17+
All callback URLs set for Twitch's webhook notifications need to support https. To facilitate developmemnt in a local environment we recommend setting up an ngrok tunnel. Follow [this link](https://ngrok.com/download) and download the latest version for your OS.
18+
19+
Once you have ngrok installed run `./ngrok http 3000` to start a tunnel.
20+
21+
### Set .env file
22+
Create a .env file with the following variables
23+
24+
```
25+
TWITCH_CLIENT_ID=
26+
TWITCH_ACCESS_TOKEN=
27+
NGROK_TUNNEL_URL=
28+
```
29+
Set the above values accordingly based on your Twitch app and the ngrok tunnel created in the previous step.
30+
31+
### Start the Server
32+
run `npm start` to run the sample.
33+
34+
35+
## Webhook Subscription Creation
36+
Once the server is up and running you can send a POST to `http://localhost:3000/createWebhook/<twitchID>` to create a new *channel.follow* webhook.
37+
38+
You can modify the *createWebHookBody* payload in the index.js to try out other webhooks as well.
39+
40+
In order to create a new webhook subscription you'll need to send a POST request to the https://api.twitch.tv/helix/eventsub/subscriptions endpoint.
41+
42+
Once the subscription is created you should expect an almost immediate call to you callback URL with a challenge that will be logged into the console and echoed back to Twitch for the process to be completed.
43+
44+
## Signature Verification
45+
Everytime you callback is reached by Twitch we will include a sha256 signature encrypted with the secret you provided during subscription creation. Signature verification is done by crafting the same message that is encrypted on Twitch's side; signing the crafted message with the shared secret; and comparing if the messages match.
46+
47+
We are covering this step in the `verifySignature` function in the index.js file.
48+
49+
## Handling Notifications
50+
The `app.post('/notification',...)` function will handle all notifications sent to your callback URL. It will reply to Twitch with the proper responses to complete the webhook flows succesfuly plus logging payloads to the console.

index.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
require('dotenv').config()
2+
const express = require('express')
3+
const bodyParser = require('body-parser')
4+
const app = express()
5+
app.use(bodyParser.json({
6+
verify: (req, res, buf) => {
7+
// Small modification to the JSON bodyParser to expose the raw body in the request object
8+
// The raw body is required at signature verification
9+
req.rawBody = buf
10+
}
11+
}))
12+
const https = require('https')
13+
const crypto = require('crypto')
14+
15+
const port = 3000 // Set the express server's port to the corresponding port of your ngrok tunnel
16+
17+
let clientId = process.env['TWITCH_CLIENT_ID']
18+
let authToken = process.env['TWITCH_ACCESS_TOKEN']
19+
let ngrokUrl = process.env['NGROK_TUNNEL_URL']
20+
21+
app.post('/createWebhook/:broadcasterId', (req, res) => {
22+
var createWebHookParams = {
23+
host: "api.twitch.tv",
24+
path: "helix/eventsub/subscriptions",
25+
method: 'POST',
26+
headers: {
27+
"Content-Type": "application/json",
28+
"Client-ID": clientId,
29+
"Authorization": "Bearer "+ authToken
30+
}
31+
}
32+
var createWebHookBody = {
33+
"type": "channel.follow",
34+
"version": "1",
35+
"condition": {
36+
"broadcaster_user_id": req.params.broadcasterId
37+
},
38+
"transport": {
39+
"method": "webhook",
40+
// For testing purposes you can use an ngrok https tunnel as your callback URL
41+
"callback": ngrokUrl+"/notification", // If you change the /notification path make sure to also adjust in line 69
42+
"secret": "keepItSecretKeepItSafe" // Replace with your own secret
43+
}
44+
}
45+
var responseData = ""
46+
var webhookReq = https.request(createWebHookParams, (result) => {
47+
result.setEncoding('utf8')
48+
result.on('data', function(d) {
49+
responseData = responseData + d
50+
})
51+
.on('end', function(result) {
52+
var responseBody = JSON.parse(responseData)
53+
res.send(responseBody)
54+
})
55+
})
56+
webhookReq.on('error', (e) => { console.log("Error") })
57+
webhookReq.write(JSON.stringify(createWebHookBody))
58+
webhookReq.end()
59+
})
60+
61+
function verifySignature(messageSignature, messageID, messageTimestamp, body) {
62+
let message = messageID + messageTimestamp + body
63+
let signature = crypto.createHmac('sha256', "keepItSecretKeepItSafe").update(message) // Remember to use the same secret set at creation
64+
let expectedSignatureHeader = "sha256=" + signature.digest("hex")
65+
66+
return expectedSignatureHeader === messageSignature
67+
}
68+
69+
app.post('/notification', (req, res) => {
70+
if (!verifySignature(req.header("Twitch-Eventsub-Message-Signature"),
71+
req.header("Twitch-Eventsub-Message-Id"),
72+
req.header("Twitch-Eventsub-Message-Timestamp"),
73+
req.rawBody)) {
74+
res.status(403).send("Forbidden") // Reject requests with invalid signatures
75+
} else {
76+
if (req.header("Twitch-Eventsub-Message-Type") === "webhook_callback_verification") {
77+
console.log(req.body.challenge)
78+
res.send(req.body.challenge) // Returning a 200 status with the received challenge to complete webhook creation flow
79+
80+
} else if (req.header("Twitch-Eventsub-Message-Type") === "notification") {
81+
console.log(req.body.event) // Implement your own use case with the event data at this block
82+
res.send("") // Default .send is a 200 status
83+
}
84+
}
85+
})
86+
87+
app.listen(port, () => {
88+
console.log(`Twitch Webhook Example listening at http://localhost:${port}`)
89+
})

package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "webhooks_sample",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"start": "node index.js"
8+
},
9+
"author": "",
10+
"license": "ISC",
11+
"dependencies": {
12+
"dotenv": "^8.2.0",
13+
"express": "^4.17.1"
14+
}
15+
}

0 commit comments

Comments
 (0)