Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated for 2022 Javascript and modern Twitch API #5

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
# Twitch PubSub Javascript Sample

Here you find a simple JavaScript PubSub implementation with client-side authentication using the Implicit Grant Flow. The project structure section will give you a quick overview of this code.

## Update (2022)

Modified for more modern coding practices. Replaced Twitch Kraken with Helix API. Removed jQuery, using vanilla JS. Removed Bootstrap CSS and added some basic styles directly.

## Structure

`index.html`
Provides the view of all incoming and outgoing pubsub messages.

`main.js`
Provides the actual functionality of the PubSub client. If you are not authenticated, you will see the Connect to Twitch button and will be prompted through the authentication flow. After authenticating, you will be able to send and receive raw PubSub messages.
Provides the actual functionality of the PubSub client. If you are not authenticated, you will see the Connect to Twitch button and will be prompted through the authentication flow. After authenticating, you will be able to send and receive raw PubSub messages.

## License

Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Modified 2022 Arno Richter (https://arnorichter.de)

Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at

Expand Down
87 changes: 55 additions & 32 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,42 +1,65 @@
<!DOCTYPE html>
<!--
Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Modified 2022 Arno Richter (https://arnorichter.de)

Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at

http://aws.amazon.com/apache2.0/
http://aws.amazon.com/apache2.0/

or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css">
<title>Twitch PubSub Example</title>
</head>
<body>
<h2 class="text-center">Twitch PubSub Example</h2>
<div class="container">
<div class="row">
<div style="display:none" class="auth" class="text-center">
<p>First, connect with your Twitch Account:</p>
<a id="auth-link"><img src="http://ttv-api.s3.amazonaws.com/assets/connect_dark.png" /></a>
</div>
<div style="display:none" class="socket">
<textarea class="ws-output" rows="20" style="font-family:Courier;width:100%"></textarea>
<form id="topic-form" class="text-right form-inline" >
<label id="topic-label" for="topic-text"></label>
<input type="text" id="topic-text" placeholder="Topic">
<button type="submit" class="btn">Listen</button>
</form>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="main.js"></script>
<script>
</script>
</body>
</html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Twitch PubSub Example</title>

<style>
* { box-sizing: border-box; }
html {
font: 100%/1.4 system-ui, sans-serif;
}

textarea {
font-family: monospace;
font-size: 1rem;
width: 100%;
}

input { font-size: 1rem; }

.endpoints li {
cursor: pointer;
}

.hidden {
display: none;
}
</style>
</head>
<body>
<h2>Twitch PubSub Example</h2>

<div class="container">
<div class="auth hidden">
<p>First, connect with your Twitch Account:</p>
<a id="auth-link">Connect</a>
</div>
<div class="socket hidden">
<textarea class="ws-output" rows="20"></textarea>
<form id="topic-form">
<label id="topic-label" for="topic-text">Enter a topic to listen to.</label>
<input type="text" id="topic-text" placeholder="Topic" />
<button type="submit">Listen</button>
</form>
<ul class="endpoints">
<li>channel-bits-events-v2.<span class="user-id"></span></li>
<li>channel-points-channel-v1.<span class="user-id"></span></li>
<li>channel-subscribe-events-v1.<span class="user-id"></span></li>
</ul>
</div>
</div>
<script src="main.js" defer></script>
</body>
</html>
222 changes: 117 additions & 105 deletions main.js
Original file line number Diff line number Diff line change
@@ -1,122 +1,134 @@
var clientId = '<YOUR CLIENT ID HERE>';
var redirectURI = '<YOUR REDIRECT URL HERE>';
var scope = 'user_read+chat_login';
var ws;

function parseFragment(hash) {
var hashMatch = function(expr) {
var match = hash.match(expr);
return match ? match[1] : null;
};
var state = hashMatch(/state=(\w+)/);
if (sessionStorage.twitchOAuthState == state)
sessionStorage.twitchOAuthToken = hashMatch(/access_token=(\w+)/);
return
};
const clientId = 'ja7y2ey1ddv1ayx5v9urjyv4xiu9el'; // YOUR CLIENT ID HERE
const redirectURI = 'https://oelna.github.io/pubsub-javascript-sample'; // YOUR REDIRECT URL HERE
const scope = 'user_read+channel:read:subscriptions+channel:read:redemptions+bits:read';
let ws;
const wsOutput = document.querySelector('.ws-output');

function parseFragment (hash) {
const hashMatch = function (expr) {
const match = hash.match(expr);
return match ? match[1] : null;
};
const state = hashMatch(/state=(\w+)/);
if (sessionStorage.twitchOAuthState == state)
sessionStorage.twitchOAuthToken = hashMatch(/access_token=(\w+)/);
return;
}

function authUrl() {
sessionStorage.twitchOAuthState = nonce(15);
var url = 'https://api.twitch.tv/kraken/oauth2/authorize' +
'?response_type=token' +
'&client_id=' + clientId +
'&redirect_uri=' + redirectURI +
'&state=' + sessionStorage.twitchOAuthState +
'&scope=' + scope;
return url
function authUrl () {
sessionStorage.twitchOAuthState = nonce(15);
const url = 'https://id.twitch.tv/oauth2/authorize' +
'?response_type=token' +
'&client_id=' + clientId +
'&redirect_uri=' + redirectURI +
'&state=' + sessionStorage.twitchOAuthState +
'&scope=' + scope;
return url;
}

// Source: https://www.thepolyglotdeveloper.com/2015/03/create-a-random-nonce-string-using-javascript/
function nonce(length) {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < length; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
function nonce (length) {
let text = "";
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < length; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}

function heartbeat() {
message = {
type: 'PING'
};
$('.ws-output').append('SENT: ' + JSON.stringify(message) + '\n');
ws.send(JSON.stringify(message));
function heartbeat () {
message = {
'type': 'PING'
}
wsOutput.append('SENT: ' + JSON.stringify(message) + '\n');
ws.send(JSON.stringify(message));
}

function listen(topic) {
message = {
type: 'LISTEN',
nonce: nonce(15),
data: {
topics: [topic],
auth_token: sessionStorage.twitchOAuthToken
}
};
$('.ws-output').append('SENT: ' + JSON.stringify(message) + '\n');
ws.send(JSON.stringify(message));
function listen (topic) {
message = {
'type': 'LISTEN',
'nonce': nonce(15),
'data': {
'topics': [topic],
'auth_token': sessionStorage.twitchOAuthToken
}
}
wsOutput.append('SENT: ' + JSON.stringify(message) + '\n');
ws.send(JSON.stringify(message));
}

function connect() {
var heartbeatInterval = 1000 * 60; //ms between PING's
var reconnectInterval = 1000 * 3; //ms to wait before reconnect
var heartbeatHandle;

ws = new WebSocket('wss://pubsub-edge.twitch.tv');

ws.onopen = function(event) {
$('.ws-output').append('INFO: Socket Opened\n');
heartbeat();
heartbeatHandle = setInterval(heartbeat, heartbeatInterval);
};

ws.onerror = function(error) {
$('.ws-output').append('ERR: ' + JSON.stringify(error) + '\n');
};

ws.onmessage = function(event) {
message = JSON.parse(event.data);
$('.ws-output').append('RECV: ' + JSON.stringify(message) + '\n');
if (message.type == 'RECONNECT') {
$('.ws-output').append('INFO: Reconnecting...\n');
setTimeout(connect, reconnectInterval);
}
};
function connect () {
const heartbeatInterval = 1000 * 60; //ms between PING's
const reconnectInterval = 1000 * 3; //ms to wait before reconnect
let heartbeatHandle;

ws = new WebSocket('wss://pubsub-edge.twitch.tv');

ws.onopen = function (event) {
wsOutput.append('INFO: Socket Opened\n');
heartbeat();
heartbeatHandle = setInterval(heartbeat, heartbeatInterval);
}

ws.onerror = function (error) {
wsOutput.append('ERR: ' + JSON.stringify(error) + '\n');
}

ws.onmessage = function (event) {
message = JSON.parse(event.data);
wsOutput.append('RECV: ' + JSON.stringify(message) + '\n');
if (message.type == 'RECONNECT') {
wsOutput.append('INFO: Reconnecting …\n');
setTimeout(connect, reconnectInterval);
}
}

ws.onclose = function () {
wsOutput.append('INFO: Socket Closed\n');
clearInterval(heartbeatHandle);
wsOutput.append('INFO: Reconnecting …\n');
setTimeout(connect, reconnectInterval);
}
}

ws.onclose = function() {
$('.ws-output').append('INFO: Socket Closed\n');
clearInterval(heartbeatHandle);
$('.ws-output').append('INFO: Reconnecting...\n');
setTimeout(connect, reconnectInterval);
};
if (document.location.hash.match(/access_token=(\w+)/)) {
parseFragment(document.location.hash);
}

if (sessionStorage.twitchOAuthToken) {
connect();
document.querySelector('.socket').classList.remove('hidden');

fetch('https://api.twitch.tv/helix/users', {
method: 'GET',
headers: new Headers({
"Accept": "application/json",
"Client-ID": clientId,
"Authorization": "Bearer " + sessionStorage.twitchOAuthToken
})
})
.then(function (response) { return response.json(); })
.then(function (user) {
if (user && user.data && user.data.length > 0) {
document.querySelectorAll('.user-id').forEach(function (ele, i) {
ele.textContent = user.data[0].id;
});
}
});
} else {
const url = authUrl()
document.querySelector('#auth-link').setAttribute('href', url);
document.querySelector('.auth').classList.remove('hidden');
}

$(function() {
if (document.location.hash.match(/access_token=(\w+)/))
parseFragment(document.location.hash);
if (sessionStorage.twitchOAuthToken) {
connect();
$('.socket').show()
$.ajax({
url: "https://api.twitch.tv/kraken/user",
method: "GET",
headers: {
"Client-ID": clientId,
"Authorization": "OAuth " + sessionStorage.twitchOAuthToken
}})
.done(function(user) {
$('#topic-label').text("Enter a topic to listen to. For example, to listen to whispers enter topic 'whispers."+user._id+"'");
});
} else {
var url = authUrl()
$('#auth-link').attr("href", url);
$('.auth').show()
}
document.querySelector('#topic-form').addEventListener('submit', function (e) {
e.preventDefault();
listen(document.querySelector('#topic-text').value);
});

$('#topic-form').submit(function() {
listen($('#topic-text').val());
event.preventDefault();
document.querySelectorAll('.endpoints li').forEach(function (ele, i) {
ele.addEventListener('click', function (e) {
e.preventDefault();
document.querySelector('#topic-text').value = this.textContent;
});
});