+ Authentication with out a server | Template
+
+
+
+
+
+
This example is the "web side" of "OAuth authorization code flow" without a server.
+
Hnady for scripts on a raspberry pi, or server thats not practical to spool up a web server on to handle the ?code return.
+
It's not disimilar to how some of the Google Auth node examples work, you start a script, the script goes "open this URL in a browser", then you are given the ?code to copy paste into the script and the script fetchs the token and refresh token
+
+
+
+
+
+
diff --git a/examples/ban_request_manager/README.md b/examples/ban_request_manager/README.md
new file mode 100644
index 0000000..d6fdf2a
--- /dev/null
+++ b/examples/ban_request_manager/README.md
@@ -0,0 +1,32 @@
+## What is this example
+
+An example project to manage unban requests on any channel the logged in user is a moderator on
+
+This is an example on how to
+
+- Obtain a token
+- Use that token to get the channels that logged in user is a moderator for
+- Upon selecting a channel, get the unban requests for that channel
+- Update/Resolve the unban requests
+
+Resoling an unban request to "unban the user" will mark the request as accepted and unban the user, so you do not need to make a second API request to unban the user.
+
+## TRY THIS EXAMPLE NOW!
+
+This example is also available via GitHub Pages!
+
+Give it a [whirl here](https://barrycarlyon.github.io/twitch_misc/examples/ban_request_manager/)
+
+## Reference Documentation
+
+- [Get Moderated Channels](https://dev.twitch.tv/docs/api/reference/#get-moderated-channels)
+- [Get Unban Requests](https://dev.twitch.tv/docs/api/reference/#get-unban-requests)
+- [Resolve Unban Request](https://dev.twitch.tv/docs/api/reference/#resolve-unban-requests)
+
+## Running the example
+
+If you have PHP installed
+
+> sudo php -S 127.0.0.1:80
+
+or just throw the code up on a webpage somewhere
diff --git a/examples/ban_request_manager/ban_manager.js b/examples/ban_request_manager/ban_manager.js
new file mode 100644
index 0000000..9b4a643
--- /dev/null
+++ b/examples/ban_request_manager/ban_manager.js
@@ -0,0 +1,256 @@
+// go populate this with a client_id
+var client_id = 'hozgh446gdilj5knsrsxxz8tahr3koz';
+var redirect = window.location.origin + '/twitch_misc/';
+
+// setup a memory space for the token/userID
+// and the state machine
+var access_token = '';
+var user_id = '';
+var channel_id = '';
+var unban_request_id = '';
+
+let status = document.getElementById('status');
+
+// setup authorise link
+document.getElementById('authorize').setAttribute('href', 'https://id.twitch.tv/oauth2/authorize?client_id=' + client_id + '&redirect_uri=' + encodeURIComponent(redirect) + '&response_type=token&scope=user:read:moderated_channels+moderator:read:unban_requests+moderator:manage:unban_requests');
+
+async function processToken(token) {
+ access_token = token;
+
+ status.textContent = 'Got Token. Loading Things';
+
+ // who are we
+ let user_resp = await fetch(
+ 'https://api.twitch.tv/helix/users',
+ {
+ method: 'GET',
+ headers: {
+ 'Client-ID': client_id,
+ 'Authorization': `Bearer ${access_token}`,
+ 'Accept': 'application/json'
+ }
+ }
+ );
+
+ if (user_resp.status != 200) {
+ status.textContent = `Failed to obtain User information ${user_resp.status} - ${await user_resp.text()}`;
+ return;
+ }
+
+ let user_data = await user_resp.json();
+ if (user_data.data.length != 1) {
+ status.textContent = `Failed to obtain a User`;
+ return;
+ }
+
+ user_id = user_data.data[0].id;
+ status.textContent = `Hello ${user_id} - ${user_data.data[0].login}`;
+
+ getPageChannels();
+}
+
+// get "targets"
+async function getPageChannels(after) {
+ let channels_url = new URL('https://api.twitch.tv/helix/moderation/channels');
+ let p = [
+ [ 'user_id', user_id ],
+ [ 'first', 100 ]
+ ]
+ if (after) {
+ p.push([ 'after', after ]);
+ }
+ channels_url.search = new URLSearchParams(p);
+ let channels_req = await fetch(
+ channels_url,
+ {
+ method: 'GET',
+ headers: {
+ 'Client-ID': client_id,
+ 'Authorization': `Bearer ${access_token}`,
+ 'Accept': 'application/json'
+ }
+ }
+ );
+
+ if (channels_req.status != 200) {
+ status.textContent = `Failed to get channels you mod: ${channels_req.status} - ${await channels_req.text()}`;
+ return;
+ }
+
+ let channels_resp = await channels_req.json();
+ let { data, pagination } = channels_resp;
+
+ // draw targets
+ channel_select.textContent = '';
+ channel_select.removeAttribute('disabled');
+
+ let opt = document.createElement('option');
+ opt.value = user_id;
+ opt.textContent = 'You';
+ channel_select.append(opt);
+
+ data.forEach(channel => {
+ let { broadcaster_id, broadcaster_name } = channel;
+
+ let opt = document.createElement('option');
+ opt.value = broadcaster_id;
+ opt.textContent = broadcaster_name;
+ channel_select.append(opt);
+ });
+
+ // next?
+ channel_id = user_id;
+ initIt();
+}
+
+channel_select.addEventListener('change', (e) => {
+ channel_id = e.target.value;
+ console.log('CID', channel_id);
+ status.textContent = `Loading into ${channel_id}`;
+
+ initIt();
+});
+
+async function initIt() {
+ // open up websocket for eventsub for real time
+
+ resetUnbanRequests();
+}
+
+async function resetUnbanRequests() {
+ requests.textContent = '';
+ // load current data
+ loadUnbanRequests();
+}
+async function loadUnbanRequests(after) {
+ let unbans_url = new URL('https://api.twitch.tv/helix/moderation/unban_requests');
+ let p = [
+ [ 'broadcaster_id', channel_id ],
+ [ 'moderator_id', user_id ],
+ [ 'status', 'pending' ],
+ [ 'first', 100 ]
+ ]
+ if (after) {
+ p.push([ 'after', after ]);
+ }
+ unbans_url.search = new URLSearchParams(p);
+
+ let unbans_req = await fetch(
+ unbans_url,
+ {
+ method: 'GET',
+ headers: {
+ 'Client-ID': client_id,
+ 'Authorization': `Bearer ${access_token}`,
+ 'Accept': 'application/json'
+ }
+ }
+ );
+
+ if (unbans_req.status != 200) {
+ status.textContent = `Failed to get requests: ${unbans_req.status} - ${await unbans_req.text()}`;
+ return;
+ }
+
+ let { data, pagination } = await unbans_req.json();
+
+ data.forEach(unban => {
+ let { broadcaster_login, user_login, user_name } = unban;
+ let { id, text } = unban;
+ let { created_at } = unban;
+
+ let dsp = '';
+
+ // add to le stack
+ let dat = new Date(created_at);
+ let y = dat.getFullYear();
+ let m = dat.getMonth() + 1;
+ let d = dat.getDate();
+
+ dsp += `${y}/${m}/${d}`;
+
+ dsp += ' ';
+
+ let h = dat.getHours();
+ if (h < 10) { h = `0${h}`; }
+ let i = dat.getMinutes();
+ if (i < 10) { i = `0${i}`; }
+ dsp += `${h}:${i}`;
+
+ let r = requests.insertRow();
+ var c = r.insertCell();
+ c.textContent = user_name;
+ var c = r.insertCell();
+ c.textContent = dsp;
+ var c = r.insertCell();
+ c.textContent = text;
+
+ var c = r.insertCell();
+ var b = document.createElement('a');
+ b.href = `https://www.twitch.tv/popout/${broadcaster_login}/viewercard/${user_login}?popout=`;
+ b.textContent = 'UserCard';
+ b.target = '_blank';
+ c.append(b);
+
+ var c = r.insertCell();
+ var b = document.createElement('div');
+ b.textContent = 'Act';
+ b.classList.add('alink');
+ b.setAttribute('data-ban-id', id);
+ c.append(b);
+
+ b.addEventListener('click', (e) => {
+ act.showModal();
+ //act_act.setAttribute('data-ban-id', e.target.getAttribute('data-ban-id'));
+ unban_request_id = e.target.getAttribute('data-ban-id');
+ });
+ });
+}
+
+act_accept.addEventListener('click', (e) => {
+ processRequest('approved');
+});
+act_reject.addEventListener('click', (e) => {
+ processRequest('denied');
+});
+act_noact.addEventListener('click', (e) => {
+ act.close();
+ unban_request_id = '';
+});
+
+async function processRequest(status) {
+ // add a spinner?
+ let resolution_text = resolution_text_text.value;
+
+ let take_action_req = await fetch(
+ 'https://api.twitch.tv/helix/moderation/unban_requests',
+ {
+ method: 'PATCH',
+ headers: {
+ 'Client-ID': client_id,
+ 'Authorization': `Bearer ${access_token}`,
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ broadcaster_id: channel_id,
+ moderator_id: user_id,
+ unban_request_id,
+ status,
+ resolution_text
+ })
+ }
+ );
+
+ act.close();
+ unban_request_id = '';
+
+ if (take_action_req.status != 200) {
+ status.textContent = `Failed to take action ${status} - ${take_action_req.status} - ${await take_action_req.text()}`;
+ return;
+ }
+ status.textContent = 'Completed the requested action';
+
+ // trigger reload?
+ resetUnbanRequests();
+}
diff --git a/examples/ban_request_manager/eventsub.js b/examples/ban_request_manager/eventsub.js
new file mode 100644
index 0000000..570bc0f
--- /dev/null
+++ b/examples/ban_request_manager/eventsub.js
@@ -0,0 +1,207 @@
+class initSocket {
+ counter = 0
+ closeCodes = {
+ 4000: 'Internal Server Error',
+ 4001: 'Client sent inbound traffic',
+ 4002: 'Client failed ping-pong',
+ 4003: 'Connection unused',
+ 4004: 'Reconnect grace time expired',
+ 4005: 'Network Timeout',
+ 4006: 'Network error',
+ 4007: 'Invalid Reconnect'
+ }
+
+ constructor(connect) {
+ this._events = {};
+
+ if (connect) {
+ this.connect();
+ }
+ }
+
+ connect(url, is_reconnect) {
+ this.eventsub = {};
+ this.counter++;
+
+ url = url ? url : 'wss://eventsub.wss.twitch.tv/ws';
+ is_reconnect = is_reconnect ? is_reconnect : false;
+
+ log(`Connecting to ${url}|${is_reconnect}`);
+ this.eventsub = new WebSocket(url);
+ this.eventsub.is_reconnecting = is_reconnect;
+ this.eventsub.counter = this.counter;
+
+ this.eventsub.addEventListener('open', () => {
+ log(`Opened Connection to Twitch`);
+ });
+ // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close_event
+ // https://github.com/Luka967/websocket-close-codes
+ this.eventsub.addEventListener('close', (close) => {
+ console.log('EventSub close', close, this.eventsub);
+ log(`${this.eventsub.twitch_websocket_id}/${this.eventsub.counter} Connection Closed: ${close.code} Reason - ${this.closeCodes[close.code]}`);
+
+ if (!this.eventsub.is_reconnecting) {
+ log(`${this.eventsub.twitch_websocket_id}/${this.eventsub.counter} Is not reconnecting, auto reconnect`);
+ //new initSocket();
+ this.connect();
+ }
+
+ if (close.code == 1006) {
+ // do a single retry
+ this.eventsub.is_reconnecting = true;
+ }
+ });
+ // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/error_event
+ this.eventsub.addEventListener('error', (err) => {
+ console.log(err);
+ log(`${this.eventsub.twitch_websocket_id}/${this.eventsub.counter} Connection Error`);
+ });
+ // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/message_event
+ this.eventsub.addEventListener('message', (message) => {
+ //log('Message');
+ console.log(this.eventsub.counter, message);
+ let { data } = message;
+ data = JSON.parse(data);
+
+ let { metadata, payload } = data;
+ let { message_id, message_type, message_timestamp } = metadata;
+ //log(`Recv ${message_id} - ${message_type}`);
+
+ switch (message_type) {
+ case 'session_welcome':
+ let { session } = payload;
+ let { id, keepalive_timeout_seconds } = session;
+
+ log(`${this.eventsub.counter} This is Socket ID ${id}`);
+ this.eventsub.twitch_websocket_id = id;
+
+ log(`${this.eventsub.counter} This socket declared silence as ${keepalive_timeout_seconds} seconds`);
+
+ if (!this.eventsub.is_reconnecting) {
+ log('Dirty disconnect or first spawn');
+ this.emit('connected', id);
+ // now you would spawn your topics
+ } else {
+ this.emit('reconnected', id);
+ // no need to spawn topics as carried over
+ }
+
+ this.silence(keepalive_timeout_seconds);
+
+ break;
+ case 'session_keepalive':
+ //log(`Recv KeepAlive - ${message_type}`);
+ this.emit('session_keepalive');
+ this.silence();
+ break;
+
+ case 'notification':
+ console.log('notification', metadata, payload);
+ log(`${this.eventsub.twitch_websocket_id}/${this.eventsub.counter} Recv notification ${JSON.stringify(payload)}`);
+
+ let { subscription, event } = payload;
+ let { type } = subscription;
+
+ this.emit('notification', { metadata, payload });
+ this.emit(type, { metadata, payload });
+ this.silence();
+
+ break;
+
+ case 'session_reconnect':
+ this.eventsub.is_reconnecting = true;
+
+ let reconnect_url = payload.session.reconnect_url;
+
+ console.log('Connect to new url', reconnect_url);
+ log(`${this.eventsub.twitch_websocket_id}/${this.eventsub.counter} Reconnect request ${reconnect_url}`)
+
+ //this.eventsub.close();
+ //new initSocket(reconnect_url, true);
+ this.connect(reconnect_url, true);
+
+ break;
+ case 'websocket_disconnect':
+ log(`${this.eventsub.counter} Recv Disconnect`);
+ console.log('websocket_disconnect', payload);
+
+ break;
+
+ case 'revocation':
+ log(`${this.eventsub.counter} Recv Topic Revocation`);
+ console.log('revocation', payload);
+ this.emit('revocation', { metadata, payload });
+ break;
+
+ default:
+ console.log(`${this.eventsub.counter} unexpected`, metadata, payload);
+ break;
+ }
+ });
+ }
+
+ trigger() {
+ this.eventsub.send('cat');
+ }
+ close() {
+ this.eventsub.close();
+ }
+
+ silenceHandler = false;
+ silenceTime = 10;// default per docs is 10 so set that as a good default
+ silence(keepalive_timeout_seconds) {
+ if (keepalive_timeout_seconds) {
+ this.silenceTime = keepalive_timeout_seconds;
+ this.silenceTime++;// add a little window as it's too anal
+ }
+ clearTimeout(this.silenceHandler);
+ this.silenceHandler = setTimeout(() => {
+ this.emit('session_silenced');// -> self reconnecting
+ this.close();// close it and let it self loop
+ }, (this.silenceTime * 1000));
+ }
+
+ on(name, listener) {
+ if (!this._events[name]) {
+ this._events[name] = [];
+ }
+
+ this._events[name].push(listener);
+ }
+ emit(name, data) {
+ if (!this._events[name]) {
+ return;
+ }
+
+ const fireCallbacks = (callback) => {
+ callback(data);
+ };
+
+ this._events[name].forEach(fireCallbacks);
+ }
+}
+
+function log(msg) {
+ if (!document.getElementById('log')) {
+ return;
+ }
+
+ let div = document.createElement('div');
+ document.getElementById('log').prepend(div);
+
+ let tim = document.createElement('span');
+ div.append(tim);
+ let t = [
+ new Date().getHours(),
+ new Date().getMinutes(),
+ new Date().getSeconds()
+ ]
+ t.forEach((v,i) => {
+ t[i] = v < 10 ? '0'+v : v;
+ });
+ tim.textContent = t.join(':');
+
+ let sp = document.createElement('span');
+ div.append(sp);
+ sp.textContent = msg;
+}
diff --git a/examples/ban_request_manager/index.html b/examples/ban_request_manager/index.html
new file mode 100644
index 0000000..7e8ea09
--- /dev/null
+++ b/examples/ban_request_manager/index.html
@@ -0,0 +1,68 @@
+
+
+
+ Ban Request Manager | Twitch API Example
+
+
+
+
+
+
+ Authorize
+
Pending
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/browse_categories/README.md b/examples/browse_categories/README.md
new file mode 100644
index 0000000..571ce86
--- /dev/null
+++ b/examples/browse_categories/README.md
@@ -0,0 +1,48 @@
+## What is this example
+
+This is a very rough example of how to build a page which collects the data needed to generate [The Directory](https://www.twitch.tv/directory)
+
+It uses Implicit auth to obtain a token, but this is just for the [GitHub pages demo](https://barrycarlyon.github.io/twitch_misc/examples/browse_categories/).
+
+If you are building this yourself and as a "server" application, you can use any kind of token as it's all public data.
+
+Normally for a server solution (you first wouldn't be doing it client side or with fetch), you'd normally use a [Client Credentaisl/App Access/Server to Server token](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth#oauth-client-credentials-flow)
+
+## TRY THIS EXAMPLE NOW!
+
+This example is also available via GitHub Pages!
+
+Give it a [whirl here](https://barrycarlyon.github.io/twitch_misc/examples/browse_categories/)
+
+## Reference Documentation
+
+- [OAuth Implicit Code Flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth#oauth-implicit-code-flow)
+- [Get Top Games](https://dev.twitch.tv/docs/api/reference#get-top-games)
+- [Get Streams](https://dev.twitch.tv/docs/api/reference#get-streams) - Using GameID's to filter
+
+## But what about rate limits?
+
+This example runs in a browser and we are using implicit auth to get a token to use.
+As a result we are using frontend JS to make the API calls, and browsers will limit the number of requests made to the same domain (api.twitch.tv in this example), so we can't "hammer" enough to get close to the rate limit.
+
+But that is something to consider if you are making these calls server side.
+
+## Setting up the config
+
+- Visit [Twitch Dev Console](https://dev.twitch.tv/console/)
+- Visit Applications
+- Manage your Application, or create one if you don't have one
+- Copy the Client ID into `client_id` JavaScript Variable
+- You'll need to throw this webpage into a website somewhere, and update the `redirect` in the html file and on the dev console accordingly.
+
+## Running the example
+
+If you have PHP installed
+
+> sudo php -S 127.0.0.1:80
+
+or just throw the code up on a webpage somewhere
+
+## Screenshot
+
+![Example](example.png)
diff --git a/examples/browse_categories/example.png b/examples/browse_categories/example.png
new file mode 100644
index 0000000..72516fb
Binary files /dev/null and b/examples/browse_categories/example.png differ
diff --git a/examples/browse_categories/index.html b/examples/browse_categories/index.html
new file mode 100644
index 0000000..44f2d59
--- /dev/null
+++ b/examples/browse_categories/index.html
@@ -0,0 +1,169 @@
+
+
+
+ Browse Categories | Twitch API Example
+
+
+
+
This example first uses Implicit Auth to get a token to use then will return a page similar to The Directorty. Generally calls will be done/cached server side with an App Access Token
+
+
Get the code for this example on Github or just View the source instead
+
+
After authenticating to get a Key, it calls Get Top Games then calls the first few pages of Get Streams for that game to get an approx viewer count.
Status output as each page of streams for that gameID loads
+
+
+
GameID
+
Status
+
1
+
2
+
3
+
4
+
...
+
+
+
+
The Directory
+
+
+
+
+
diff --git a/examples/calendar/README.md b/examples/calendar/README.md
new file mode 100644
index 0000000..86441bc
--- /dev/null
+++ b/examples/calendar/README.md
@@ -0,0 +1,51 @@
+## What is this example
+
+This is an example for just poking the [Calendar/Schedule](https://dev.twitch.tv/docs/api/reference#get-channel-stream-schedule) part of the Twitch API
+
+It uses Implicit auth to obtain a token, but this is just for the [GitHub pages demo](https://barrycarlyon.github.io/twitch_misc/examples/calendar/).
+
+If you are building this yourself and as a "server" application, you can use any kind of token as it's all public data.
+
+Normally for a server solution (you first wouldn't be doing it client side or with fetch), you'd normally use a [Client Credentaisl/App Access/Server to Server token](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth#oauth-client-credentials-flow)
+
+This example generates a QR code to the iCalendar URL, the Javascript library [davidshimjs/qrcodejs](https://github.com/davidshimjs/qrcodejs) is used for this.
+
+## TRY THIS EXAMPLE NOW!
+
+This example is also available via GitHub Pages!
+
+Give it a [whirl here](https://barrycarlyon.github.io/twitch_misc/examples/calendar/)
+
+## Reference Documentation
+
+- [OAuth Implicit Code Flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth#oauth-implicit-code-flow)
+- [Get Channel Stream Schedule](https://dev.twitch.tv/docs/api/reference#get-channel-stream-schedule)
+- [Get Channel iCalendar](https://dev.twitch.tv/docs/api/reference#get-channel-icalendar)
+- [Get Users](https://dev.twitch.tv/docs/api/reference#get-users) - For Converting `logins` to `user_ids`
+
+## But what about rate limits?
+
+This example runs in a browser and we are using implicit auth to get a token to use.
+As a result we are using frontend JS to make the API calls, and browsers will limit the number of requests made to the same domain (api.twitch.tv in this example), so we can't "hammer" enough to get close to the rate limit.
+
+But that is something to consider if you are making these calls server side.
+
+## Setting up the config
+
+- Visit [Twitch Dev Console](https://dev.twitch.tv/console/)
+- Visit Applications
+- Manage your Application, or create one if you don't have one
+- Copy the Client ID into `client_id` JavaScript Variable
+- You'll need to throw this webpage into a website somewhere, and update the `redirect` in the html file and on the dev console accordingly.
+
+## Running the example
+
+If you have PHP installed
+
+> sudo php -S 127.0.0.1:80
+
+or just throw the code up on a webpage somewhere
+
+## Screenshot
+
+![Example](example.png)
diff --git a/examples/calendar/example.png b/examples/calendar/example.png
new file mode 100644
index 0000000..e0637c8
Binary files /dev/null and b/examples/calendar/example.png differ
diff --git a/examples/calendar/index.html b/examples/calendar/index.html
new file mode 100644
index 0000000..dfb433f
--- /dev/null
+++ b/examples/calendar/index.html
@@ -0,0 +1,202 @@
+
+
+
+ Calendar Stuff | Twitch API Example
+
+
+
+
+
+
This example will first ask you to login with Implicit Auth to get an API Token to use. It'll then fetch your iCalendar URL, generate a QR Code for that URL and display you schedule segments. You can then also lookup another streamer.
+
Get the code for this example on Github or just View the source instead
After testing you can Disconnect the "Barry's GitHub Examples" Application on the Connections page
+
+
+
+
+
+
+
Calendar URL
+
+
QR Code for iCal URL
+
+
+
+
WebCal URL: should invoke default calendar program
+
+
QR Code for WebCal URL
+
+
+
+
+
Schedule segments for this Streamer, for this Week only
+
+
+
+
Start
+
End
+
Title
+
Category (If Any)
+
+
+
+
+
+
+
+
+
diff --git a/examples/channel_dashboard/README.md b/examples/channel_dashboard/README.md
new file mode 100644
index 0000000..97cbd16
--- /dev/null
+++ b/examples/channel_dashboard/README.md
@@ -0,0 +1,52 @@
+## What is this example
+
+This is an example of a Twitch Dashboard.
+
+It uses Implicit auth to obtain a token, generally for a Server Side Solution you'd use a regular Code flow token, and keep the keys on file and refresh those keys.
+
+## TRY THIS EXAMPLE NOW!
+
+This example is also available via GitHub Pages!
+
+Give it a [whirl here](https://barrycarlyon.github.io/twitch_misc/examples/channel_dashboard/)
+
+## Reference Documentation
+
+
+- [Get Users](https://dev.twitch.tv/docs/api/reference#get-users) - We need the User ID to call other endpoints
+- [Get Channel Information](https://dev.twitch.tv/docs/api/reference#get-channel-information)
+- [Modify Channel Information](https://dev.twitch.tv/docs/api/reference#modify-channel-information)
+- [Get User Installed Extensions](https://dev.twitch.tv/docs/api/reference#get-user-extensions) - See what Extensions are installed to the channel
+- [Get User Active Extensions](https://dev.twitch.tv/docs/api/reference#get-user-active-extensions)
+- [Update User Extensions](https://dev.twitch.tv/docs/api/reference#update-user-extensions)
+- [Search Categories](https://dev.twitch.tv/docs/api/reference#search-categoriess) - To convert a game name to a game ID
+
+## But what about rate limits?
+
+This example runs in a browser and we are using implicit auth to get a token to use.
+As a result we are using frontend JS to make the API calls, and browsers will limit the number of requests made to the same domain (api.twitch.tv in this example), so we can't "hammer" enough to get close to the rate limit.
+
+But that is something to consider if you are making these calls server side.
+
+## Languages
+
+The language list is likely not quite correct I lifted it from Stack Overflow!
+
+- https://stackoverflow.com/questions/5580876/navigator-language-list-of-all-languages
+- https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
+
+## Setting up the config
+
+- Visit [Twitch Dev Console](https://dev.twitch.tv/console/)
+- Visit Applications
+- Manage your Application, or create one if you don't have one
+- Copy the Client ID into `client_id` JavaScript Variable
+- You'll need to throw this webpage into a website somewhere, and update the `redirect` in the html file and on the dev console accordingly.
+
+## Running the example
+
+If you have PHP installed
+
+> sudo php -S 127.0.0.1:80
+
+or just throw the code up on a webpage somewhere
diff --git a/examples/channel_dashboard/index.html b/examples/channel_dashboard/index.html
new file mode 100644
index 0000000..7b87f92
--- /dev/null
+++ b/examples/channel_dashboard/index.html
@@ -0,0 +1,734 @@
+
+
+
+ Channel Dashboard | Twitch API Example
+
+
+
+
+
+
This example first uses Implicit Auth to get a token to use then will Various endpoints.
+
+
We then utilise the following
+
+
+
Get Users - we need the UserID for other endpoints
This example first uses Implicit Auth to get a token to use then will Various endpoints. Generally calls will be done/cached server side with an App Access Token
+
+
Get the code for this example on Github or just View the source instead
+
+ Authorize
+
+
+
+
+
+
+
+
+
diff --git a/examples/channel_points/README.md b/examples/channel_points/README.md
new file mode 100644
index 0000000..be73e04
--- /dev/null
+++ b/examples/channel_points/README.md
@@ -0,0 +1,31 @@
+## What is this example
+
+A simple Channel Points tool to show rewards on the authenticating channel, create, update and delete those rewards.
+
+It does two requests when Getting Custom reards, first to get and list _all_ rewards, then a second request to see which rewards the clientID can manage.
+
+## Notes
+
+The Rewards API only lets an API caller manage Rewards and Redemptions for Rewards that the Calling Client ID has created.
+
+So if a streamer creates a reward via the dashboard, you an API consume will be able to see the reward exists and recieve redeems as they occur, but you won't be able to look up historical data, or refund a redeem for example.
+
+## TRY THIS EXAMPLE NOW!
+
+This example is also available via GitHub Pages!
+
+Give it a [whirl here](https://barrycarlyon.github.io/twitch_misc/examples/channel_points/)
+
+## Reference Documentation
+
+- [Get Custom Rewards](https://dev.twitch.tv/docs/api/reference#get-custom-reward)
+- [Create Custom Rewards](https://dev.twitch.tv/docs/api/reference#create-custom-rewards)
+- [Delete Custom Rewards](https://dev.twitch.tv/docs/api/reference#delete-custom-reward)
+
+## Running the example
+
+If you have PHP installed
+
+> sudo php -S 127.0.0.1:80
+
+or just throw the code up on a webpage somewhere
diff --git a/examples/channel_points/channel_points.js b/examples/channel_points/channel_points.js
new file mode 100644
index 0000000..cc8c19e
--- /dev/null
+++ b/examples/channel_points/channel_points.js
@@ -0,0 +1,574 @@
+// go populate this with a client_id
+var client_id = 'hozgh446gdilj5knsrsxxz8tahr3koz';
+var redirect = window.location.origin + '/twitch_misc/';
+// setup a memory space for the token/userID
+var access_token = '';
+var user_id = '';
+
+var loading = document.getElementById('loading');
+var output = document.getElementById('output');
+
+document.getElementById('authorize').setAttribute('href', 'https://id.twitch.tv/oauth2/authorize?client_id=' + client_id + '&redirect_uri=' + encodeURIComponent(redirect) + '&response_type=token&scope=channel:read:redemptions+channel:manage:redemptions')
+
+function provideAccessToken(token) {
+ fetch(
+ 'https://id.twitch.tv/oauth2/validate',
+ {
+ method: 'GET',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Accept': 'application/json'
+ }
+ }
+ )
+ .then(resp => resp.json())
+ .then(resp => {
+ if (resp.client_id) {
+ client_id = resp.client_id;
+ processToken(token);
+
+ return;
+ }
+
+ document.getElementById('loading').textContent = 'The token seems invalid';
+ })
+ .catch(err => {
+ console.log(err);
+ document.getElementById('loading').textContent = 'An Error Occured loading Validating Token: error';
+ });
+}
+provide_access_token.addEventListener('submit', (e) => {
+ e.preventDefault();
+ provideAccessToken(input_access_token.value);
+});
+
+function processToken(token) {
+ access_token = token;
+
+ message('Got Token');
+ message('Loading User from Token');
+
+ fetch(
+ 'https://api.twitch.tv/helix/users',
+ {
+ "headers": {
+ "Client-ID": client_id,
+ "Authorization": `Bearer ${access_token}`
+ }
+ }
+ )
+ .then(resp => resp.json())
+ .then(resp => {
+ if (resp.data.length == 1) {
+ message('Got User ID');
+
+ user_id = resp.data[0].id;
+
+ getRewards();
+ // create eventsub for real time updates?
+ eventsub();
+ } else {
+ document.getElementById('loading').textContent = 'An Error Occured loading Profile: not returned';
+ }
+ })
+ .catch(err => {
+ console.log(err);
+ document.getElementById('loading').textContent = 'An Error Occured loading Profile: error';
+ });
+}
+
+function message(words) {
+ let p = document.createElement('p');
+ document.getElementById('loading').prepend(p);
+ p.textContent = words;
+}
+
+function getRewards() {
+ message('Loading Rewards');
+
+ output.textContent = '';
+
+ let params = [
+ [ 'broadcaster_id', user_id ],
+ [ 'first', 100 ]
+ ];
+
+ let url = new URL('https://api.twitch.tv/helix/channel_points/custom_rewards');
+ url.search = new URLSearchParams(params).toString();
+
+ fetch(
+ url,
+ {
+ "method": "GET",
+ "headers": {
+ "Client-ID": client_id,
+ "Authorization": `Bearer ${access_token}`
+ }
+ }
+ )
+ .then(resp => resp.json())
+ .then(resp => {
+ message(`Obtained ${resp.data.length} rewards`);
+ if (resp.data.length >= 1) {
+
+ resp.data.forEach(reward => {
+ let tr = document.createElement('tr');
+ output.append(tr);
+
+ tr.setAttribute('id', reward.id);
+ tr.style.backgroundColor = reward.background_color;
+
+ cell('Cannot', tr);
+ cell(reward.id, tr);
+
+ let itd = cell('', tr);
+ if (reward.image && reward.image.url_1x) {
+ let iti = document.createElement('img');
+ itd.append(iti);
+ iti.setAttribute('src', reward.image.url_1x);
+ }
+
+ let titleCell = cell('', tr);
+ var d = document.createElement('div');
+ titleCell.append(d);
+ var i = document.createElement('input');
+ d.append(i);
+ i.value = reward.title;
+ i.setAttribute('id', `field_${reward.id}_title`);
+ var u = document.createElement('input');
+ d.append(u);
+ u.setAttribute('type', 'button');
+ u.setAttribute('data-linked', `field_${reward.id}_title`);
+ u.setAttribute('data-update', 'title');
+ u.setAttribute('data-id', reward.id);
+ u.value = 'U';
+ cell(reward.cost, tr);
+
+ var td = tr.insertCell();
+ let enabler = colorCell((reward.is_enabled ? 'Enabled' : 'Disabled'), td, reward.is_enabled);
+ enabler.setAttribute('data-toggle', 'is_enabled');
+ enabler.setAttribute('data-id', reward.id);
+ var td = tr.insertCell();
+ let pauser = colorCell((reward.is_paused ? 'Paused' : 'Running'), td, !reward.is_paused);
+ pauser.setAttribute('data-toggle', 'is_paused');
+ pauser.setAttribute('data-id', reward.id);
+ cell((reward.should_redemptions_skip_request_queue ? 'Skips' : 'Need Mod'), tr);
+ let promptCell = cell('', tr);
+ var d = document.createElement('div');
+ promptCell.append(d);
+ var i = document.createElement('input');
+ d.append(i);
+ i.value = reward.prompt;
+ i.setAttribute('id', `field_${reward.id}_prompt`);
+ var u = document.createElement('input');
+ d.append(u);
+ u.setAttribute('type', 'button');
+ u.setAttribute('data-linked', `field_${reward.id}_prompt`);
+ u.setAttribute('data-update', 'prompt');
+ u.setAttribute('data-id', reward.id);
+ u.value = 'U';
+ var td = tr.insertCell();
+ colorCell((reward.is_user_input_required ? 'IsReq' : ''), td, reward.is_user_input_required);
+
+ let del = cell('x', tr);
+ del.classList.add('delete_reward');
+ del.setAttribute('data-id', reward.id);
+ });
+
+ getRewardsManage();
+ } else {
+ message('An Error Occured loading Rewards: no rewards');
+ }
+ })
+ .catch(err => {
+ console.log(err);
+ message('An Error Occured loading Rewards: error');
+ });
+}
+
+function getRewardsManage() {
+ message('Loading Rewards Can Manage');
+
+ let params = [
+ [ 'broadcaster_id', user_id ],
+ [ 'first', 100 ],
+ [ 'only_manageable_rewards', 'true' ]
+ ];
+
+ let url = new URL('https://api.twitch.tv/helix/channel_points/custom_rewards');
+ url.search = new URLSearchParams(params).toString();
+
+ fetch(
+ url,
+ {
+ "method": "GET",
+ "headers": {
+ "Client-ID": client_id,
+ "Authorization": `Bearer ${access_token}`
+ }
+ }
+ )
+ .then(resp => resp.json())
+ .then(resp => {
+ message(`Obtained ${resp.data.length} manageable rewards`);
+
+ resp.data.forEach((reward) => {
+ let { id } = reward;
+ let el = document.getElementById(id);
+ if (el) {
+ el.querySelector('td').textContent = 'Can';
+ el.querySelector('td').style.backgroundColor = 'green';
+ }
+ });
+ })
+ .catch(err => {
+ console.log(err);
+ message('An Error Occured loading Manageable Rewards: error');
+ });
+}
+
+function colorCell(text, td, value) {
+ td.textContent = text;
+ if (value) {
+ td.style.backgroundColor = 'green';
+ } else {
+ td.style.backgroundColor = 'red';
+ }
+ return td;
+}
+
+function cell(value, row) {
+ let td = document.createElement('td');
+ row.append(td);
+ td.textContent = value;
+ return td;
+}
+
+
+document.getElementById('reward_create_form').addEventListener('submit', (e) => {
+ e.preventDefault();
+
+ let payload = {
+ title: reward_title.value,
+ cost: reward_cost.value,
+
+ prompt: reward_prompt.value,
+ is_enabled: reward_is_enabled.checked,
+
+ background_color: reward_background_color.value,
+
+ is_user_input_required: reward_is_user_input_required.checked,
+
+ is_max_per_stream_enabled: is_max_per_stream_enabled.checked,
+ is_max_per_user_per_stream_enabled: is_max_per_user_per_stream_enabled.checked,
+ is_global_cooldown_enabled: is_global_cooldown_enabled.checked,
+
+ should_redemptions_skip_request_queue: reward_should_redemptions_skip_request_queue.checked
+ }
+ //max_per_stream
+ //max_per_user_per_stream
+ //global_cooldown_seconds
+ if (payload.is_max_per_stream_enabled) {
+ payload.max_per_stream = max_per_stream.value;
+ }
+ if (payload.is_max_per_user_per_stream_enabled) {
+ payload.max_per_user_per_stream = max_per_user_per_stream.value;
+ }
+ if (payload.is_global_cooldown_enabled) {
+ payload.global_cooldown_seconds = global_cooldown_seconds.value;
+ }
+
+ for (key in payload) {
+ if (payload[key] === '') {
+ delete payload[key];
+ }
+ }
+
+ console.log(payload);//return;
+
+ let url = new URL('https://api.twitch.tv/helix/channel_points/custom_rewards');
+ url.search = new URLSearchParams([
+ [ 'broadcaster_id', user_id ]
+ ]).toString();
+
+ message('Attempting to create a reward');
+ fetch(
+ url,
+ {
+ "method": "POST",
+ "headers": {
+ "Client-ID": client_id,
+ "Authorization": `Bearer ${access_token}`,
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify(payload)
+ }
+ )
+ .then(r => r.json().then(data => ({ status: r.status, body: data })))
+ .then(resp => {
+ message(`Reward Create Result: ${resp.status}`);
+ if (resp.status != 204) {
+ message(`Response: ${resp.body.message}`);
+ }
+ getRewards();
+ })
+ .catch(err => {
+ console.log(err);
+ message('An Error Occured creating a reward: error');
+ });
+});
+
+function deleteReward(id) {
+ message(`Attempt to delete reward ${id}`);
+ let url = new URL('https://api.twitch.tv/helix/channel_points/custom_rewards');
+ url.search = new URLSearchParams([
+ [ 'broadcaster_id', user_id ],
+ [ 'id', id ]
+ ]);
+
+ fetch(
+ url,
+ {
+ "method": "DELETE",
+ "headers": {
+ "Client-ID": client_id,
+ "Authorization": `Bearer ${access_token}`,
+ }
+ }
+ )
+ .then(async resp => {
+ message(`Reward Delete Result: ${resp.status}`);
+ if (resp.status == 204) {
+ getRewards();
+ } else {
+ message(`Reward Delete Result: ${await resp.text()}`);
+ }
+ })
+ .catch(err => {
+ console.log(err);
+ message('An Error Occured deleting a reward: error');
+ });
+}
+
+// ui
+is_max_per_stream_enabled.addEventListener('change', (e) => {
+ if (e.target.checked) {
+ max_per_stream.removeAttribute('disabled');
+ } else {
+ max_per_stream.setAttribute('disabled', 'disabled');
+ }
+});
+max_per_stream.setAttribute('disabled', 'disabled');
+
+is_max_per_user_per_stream_enabled.addEventListener('change', (e) => {
+ if (e.target.checked) {
+ max_per_user_per_stream.removeAttribute('disabled');
+ } else {
+ max_per_user_per_stream.setAttribute('disabled', 'disabled');
+ }
+});
+max_per_user_per_stream.setAttribute('disabled', 'disabled');
+
+is_global_cooldown_enabled.addEventListener('change', (e) => {
+ if (e.target.checked) {
+ global_cooldown_seconds.removeAttribute('disabled');
+ } else {
+ global_cooldown_seconds.setAttribute('disabled', 'disabled');
+ }
+});
+global_cooldown_seconds.setAttribute('disabled', 'disabled');
+
+output.addEventListener('click', (e) => {
+ let rewardID = e.target.getAttribute('data-id');
+
+ if (e.target.classList.contains('delete_reward')) {
+ deleteReward(e.target.getAttribute('data-id'));
+ } else if (e.target.getAttribute('data-toggle')) {
+ // we have a jerb
+ let toggle = e.target.getAttribute('data-toggle');
+
+ // build a patch
+ togglePatcher(rewardID, toggle);
+ } else if (e.target.getAttribute('data-update')) {
+ let pl = {};
+ pl[e.target.getAttribute('data-update')] = document.getElementById(e.target.getAttribute('data-linked')).value;
+ patcher(rewardID, pl);
+ }
+});
+async function togglePatcher(id, field) {
+ // get current value
+ let url = new URL('https://api.twitch.tv/helix/channel_points/custom_rewards');
+ url.search = new URLSearchParams([
+ [ 'broadcaster_id', user_id ],
+ [ 'id', id ]
+ ]);
+
+ let currentReq = await fetch(
+ url,
+ {
+ "method": 'GET',
+ "headers": {
+ "Client-ID": client_id,
+ "Authorization": `Bearer ${access_token}`,
+ }
+ }
+ );
+ if (currentReq.status != 200) {
+ message(`Reward Update GET Error: ${await currentReq.text()}`);
+ // balls
+ return;
+ }
+ let currentData = await currentReq.json();
+ // find it
+ let { data } = currentData;
+ if (data.length != 1) {
+ message(`Reward Update Error: Failed to get current value`);
+ return;
+ }
+ // find the field
+ let currentValue = data[0][field];
+ // invert it
+ let pl = {};
+ pl[field] = !currentValue;
+ console.log('Patching', pl);
+ // and patch it
+ patcher(id, pl);
+}
+
+async function patcher(id, pl) {
+ let url = new URL('https://api.twitch.tv/helix/channel_points/custom_rewards');
+ url.search = new URLSearchParams([
+ [ 'broadcaster_id', user_id ],
+ [ 'id', id ]
+ ]);
+
+ let patchReq = await fetch(
+ url,
+ {
+ "method": 'PATCH',
+ "headers": {
+ "Client-ID": client_id,
+ "Authorization": `Bearer ${access_token}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(pl)
+ }
+ );
+ if (patchReq.status != 200) {
+ // balls
+ message(`Reward Update Error: ${await patchReq.text()}`);
+ }
+ getRewards();
+}
+
+function eventsub() {
+ let socket_space = new initSocket(true);
+ socket_space.on('connected', (id) => {
+ message(`Connected to WebSocket with ${id}`);
+ requestHooks(id, user_id);
+ });
+ socket_space.on('session_keepalive', () => {
+ console.log('keepalive', new Date());
+ });
+
+ socket_space.on('channel.channel_points_custom_reward.update', ({ metadata, payload }) => {
+ let { event } = payload;
+
+ let { id } = event;
+ message(`Process a channel.channel_points_custom_reward.update on ${id}`);
+
+ let { image, title, cost } = event;
+
+ let { is_enabled, is_paused } = event;
+ let { should_redemptions_skip_request_queue } = event;
+
+ let { prompt, is_user_input_required } = event;
+
+ let row = document.getElementById(id);
+ if (!row) {
+ // not found
+ return;
+ }
+
+ let cells = row.querySelectorAll('td');
+
+ // 2 is the image
+ //cells[2].IMAGE
+ cells[2].textContent = '';
+ if (image && image.url_1x) {
+ let iti = document.createElement('img');
+ cells[2].append(iti);
+ iti.setAttribute('src', image.url_1x);
+ }
+
+ // 3 is the title
+ let inp = document.getElementById(`field_${id}_title`);
+ if (inp) {
+ inp.vlaue = title;
+ }
+ // 4 is the price
+ cells[4].textContent = cost;
+ // 5 is enabled 6 is paused
+ colorCell((is_enabled ? 'Enabled' : 'Disabled'), cells[5], is_enabled);
+ colorCell((is_paused ? 'Paused' : 'Running'), cells[6], !is_paused);
+ // 7 skip
+ cells[7].textContent = (should_redemptions_skip_request_queue ? 'Skips' : 'Need Mod');
+ // 8 prompt
+ //cells[8].textContent = prompt;
+ let promptCell = document.getElementById(`field_${id}_prompt`);
+ promptCell.value = prompt;
+ // 9 user input
+ colorCell((is_user_input_required ? 'IsReq' : ''), cells[9], is_user_input_required);
+
+ });
+}
+
+
+function requestHooks(session_id, user_id) {
+ message('Requesting Topics');
+ let topics = {
+ 'channel.channel_points_custom_reward.add': { version: 1, condition: { broadcaster_user_id: user_id } },
+ 'channel.channel_points_custom_reward.update': { version: 1, condition: { broadcaster_user_id: user_id } },
+ 'channel.channel_points_custom_reward.remove': { version: 1, condition: { broadcaster_user_id: user_id } },
+ }
+
+ message(`Spawn Topics for ${user_id}`);
+
+ for (let type in topics) {
+ message(`Attempt create ${type} - ${user_id}`);
+ let { version, condition } = topics[type];
+
+ fetch(
+ 'https://api.twitch.tv/helix/eventsub/subscriptions',
+ {
+ "method": "POST",
+ "headers": {
+ "Client-ID": client_id,
+ "Authorization": "Bearer " + access_token,
+ 'Content-Type': 'application/json'
+ },
+ "body": JSON.stringify({
+ type,
+ version,
+ condition,
+ transport: {
+ method: "websocket",
+ session_id
+ }
+ })
+ }
+ )
+ .then(resp => resp.json())
+ .then(resp => {
+ if (resp.error) {
+ message(`Error with eventsub Call ${type} Call: ${resp.message ? resp.message : ''}`);
+ } else {
+ message(`Eventsub Created ${type}`);
+ }
+ })
+ .catch(err => {
+ console.log(err);
+ message(`Error with eventsub Call ${type} Call: ${err.message ? err.message : ''}`);
+ });
+ }
+}
\ No newline at end of file
diff --git a/examples/channel_points/eventsub.js b/examples/channel_points/eventsub.js
new file mode 100644
index 0000000..2325c41
--- /dev/null
+++ b/examples/channel_points/eventsub.js
@@ -0,0 +1,207 @@
+class initSocket {
+ counter = 0
+ closeCodes = {
+ 4000: 'Internal Server Error',
+ 4001: 'Client sent inbound traffic',
+ 4002: 'Client failed ping-pong',
+ 4003: 'Connection unused',
+ 4004: 'Reconnect grace time expired',
+ 4005: 'Network Timeout',
+ 4006: 'Network error',
+ 4007: 'Invalid Reconnect'
+ }
+
+ constructor(connect) {
+ this._events = {};
+
+ if (connect) {
+ this.connect();
+ }
+ }
+
+ connect(url, is_reconnect) {
+ this.eventsub = {};
+ this.counter++;
+
+ url = url ? url : 'wss://eventsub.wss.twitch.tv/ws';
+ is_reconnect = is_reconnect ? is_reconnect : false;
+
+ log(`Connecting to ${url}|${is_reconnect}`);
+ this.eventsub = new WebSocket(url);
+ this.eventsub.is_reconnecting = is_reconnect;
+ this.eventsub.counter = this.counter;
+
+ this.eventsub.addEventListener('open', () => {
+ log(`Opened Connection to Twitch`);
+ });
+ // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close_event
+ // https://github.com/Luka967/websocket-close-codes
+ this.eventsub.addEventListener('close', (close) => {
+ console.log('EventSub close', close, this.eventsub);
+ log(`${this.eventsub.twitch_websocket_id}/${this.eventsub.counter} Connection Closed: ${close.code} Reason - ${this.closeCodes[close.code]}`);
+
+ if (!this.eventsub.is_reconnecting) {
+ log(`${this.eventsub.twitch_websocket_id}/${this.eventsub.counter} Is not reconnecting, auto reconnect`);
+ //new initSocket();
+ this.connect();
+ }
+
+ if (close.code == 1006) {
+ // do a single retry
+ this.eventsub.is_reconnecting = true;
+ }
+ });
+ // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/error_event
+ this.eventsub.addEventListener('error', (err) => {
+ console.log(err);
+ log(`${this.eventsub.twitch_websocket_id}/${this.eventsub.counter} Connection Error`);
+ });
+ // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/message_event
+ this.eventsub.addEventListener('message', (message) => {
+ //log('Message');
+ console.log(this.eventsub.counter, message);
+ let { data } = message;
+ data = JSON.parse(data);
+
+ let { metadata, payload } = data;
+ let { message_id, message_type, message_timestamp } = metadata;
+ //log(`Recv ${message_id} - ${message_type}`);
+
+ switch (message_type) {
+ case 'session_welcome':
+ let { session } = payload;
+ let { id, keepalive_timeout_seconds } = session;
+
+ log(`${this.eventsub.counter} This is Socket ID ${id}`);
+ this.eventsub.twitch_websocket_id = id;
+
+ log(`${this.eventsub.counter} This socket declared silence as ${keepalive_timeout_seconds} seconds`);
+
+ if (!this.eventsub.is_reconnecting) {
+ log('Dirty disconnect or first spawn');
+ this.emit('connected', id);
+ // now you would spawn your topics
+ } else {
+ this.emit('reconnected', id);
+ // no need to spawn topics as carried over
+ }
+
+ this.silence(keepalive_timeout_seconds);
+
+ break;
+ case 'session_keepalive':
+ //log(`Recv KeepAlive - ${message_type}`);
+ this.emit('session_keepalive');
+ this.silence();
+ break;
+
+ case 'notification':
+ console.log('notification', metadata, payload);
+ log(`${this.eventsub.twitch_websocket_id}/${this.eventsub.counter} Recv notification ${JSON.stringify(payload)}`);
+
+ let { subscription, event } = payload;
+ let { type } = subscription;
+
+ this.emit('notification', { metadata, payload });
+ this.emit(type, { metadata, payload });
+ this.silence();
+
+ break;
+
+ case 'session_reconnect':
+ this.eventsub.is_reconnecting = true;
+
+ let reconnect_url = payload.session.reconnect_url;
+
+ console.log('Connect to new url', reconnect_url);
+ log(`${this.eventsub.twitch_websocket_id}/${this.eventsub.counter} Reconnect request ${reconnect_url}`)
+
+ //this.eventsub.close();
+ //new initSocket(reconnect_url, true);
+ this.connect(reconnect_url, true);
+
+ break;
+ case 'websocket_disconnect':
+ log(`${this.eventsub.counter} Recv Disconnect`);
+ console.log('websocket_disconnect', payload);
+
+ break;
+
+ case 'revocation':
+ log(`${this.eventsub.counter} Recv Topic Revocation`);
+ console.log('revocation', payload);
+ this.emit('revocation', { metadata, payload });
+ break;
+
+ default:
+ console.log(`${this.eventsub.counter} unexpected`, metadata, payload);
+ break;
+ }
+ });
+ }
+
+ trigger() {
+ this.eventsub.send('cat');
+ }
+ close() {
+ this.eventsub.close();
+ }
+
+ silenceHandler = false;
+ silenceTime = 10;// default per docs is 10 so set that as a good default
+ silence(keepalive_timeout_seconds) {
+ if (keepalive_timeout_seconds) {
+ this.silenceTime = keepalive_timeout_seconds;
+ this.silenceTime++;// add a little window as it's too anal
+ }
+ clearTimeout(this.silenceHandler);
+ this.silenceHandler = setTimeout(() => {
+ this.emit('session_silenced');// -> self reconnecting
+ this.close();// close it and let it self loop
+ }, (this.silenceTime * 1000));
+ }
+
+ on(name, listener) {
+ if (!this._events[name]) {
+ this._events[name] = [];
+ }
+
+ this._events[name].push(listener);
+ }
+ emit(name, data) {
+ if (!this._events[name]) {
+ return;
+ }
+
+ const fireCallbacks = (callback) => {
+ callback(data);
+ };
+
+ this._events[name].forEach(fireCallbacks);
+ }
+}
+
+function log(msg) {
+ if (!document.getElementById('log')) {
+ return;
+ }
+
+ let div = document.createElement('div');
+ document.getElementById('log').prepend(div);
+
+ let tim = document.createElement('span');
+ div.append(tim);
+ let t = [
+ new Date().getHours(),
+ new Date().getMinutes(),
+ new Date().getSeconds()
+ ]
+ t.forEach((v,i) => {
+ t[i] = v < 10 ? '0'+v : v;
+ });
+ tim.textContent = t.join(':');
+
+ let sp = document.createElement('span');
+ div.append(sp);
+ sp.textContent = msg;
+}
diff --git a/examples/channel_points/index.html b/examples/channel_points/index.html
new file mode 100644
index 0000000..932870e
--- /dev/null
+++ b/examples/channel_points/index.html
@@ -0,0 +1,152 @@
+
+
+
+ Channel Points | Twitch API Example
+
+
+
+
+
+
This example first uses Implicit Auth to get a token to use then will Various endpoints and permission to read YOUR data
+
+
Get the code for this example on Github or just View the source instead
+
+
+
+
+
+
diff --git a/examples/chatters/README.md b/examples/chatters/README.md
new file mode 100644
index 0000000..08710c8
--- /dev/null
+++ b/examples/chatters/README.md
@@ -0,0 +1,38 @@
+## What is this example
+
+A simple example to Demo the Chatters Endpoint
+
+It uses Implicit auth to obtain a token, but this is just for the [GitHub pages demo](https://barrycarlyon.github.io/twitch_misc/examples/chatters/).
+
+## TRY THIS EXAMPLE NOW!
+
+This example is also available via GitHub Pages!
+
+Give it a [whirl here](https://barrycarlyon.github.io/twitch_misc/examples/chatters/)
+
+## Reference Documentation
+
+- [Get Chatters](https://dev.twitch.tv/docs/api/reference/#get-chatters)
+
+## But what about rate limits?
+
+This example runs in a browser and we are using implicit auth to get a token to use.
+As a result we are using frontend JS to make the API calls, and browsers will limit the number of requests made to the same domain (api.twitch.tv in this example), so we can't "hammer" enough to get close to the rate limit.
+
+But that is something to consider if you are making these calls server side.
+
+## Setting up the config
+
+- Visit [Twitch Dev Console](https://dev.twitch.tv/console/)
+- Visit Applications
+- Manage your Application, or create one if you don't have one
+- Copy the Client ID into `client_id` JavaScript Variable
+- You'll need to throw this webpage into a website somewhere, and update the `redirect` in the html file and on the dev console accordingly.
+
+## Running the example
+
+If you have PHP installed
+
+> sudo php -S 127.0.0.1:80
+
+or just throw the code up on a webpage somewhere
diff --git a/examples/chatters/index.html b/examples/chatters/index.html
new file mode 100644
index 0000000..af507b2
--- /dev/null
+++ b/examples/chatters/index.html
@@ -0,0 +1,91 @@
+
+
+
+ Chatters Example | Twitch API Example
+
+
+
+
This example first uses Implicit Auth to get a token to use then will retrun a page similar to The Directorty.
+
+
After authenticating to get a Key, it calls Get Chatters.
+
+
+
+
diff --git a/examples/clips_navigator/README.md b/examples/clips_navigator/README.md
new file mode 100644
index 0000000..3ebc855
--- /dev/null
+++ b/examples/clips_navigator/README.md
@@ -0,0 +1,34 @@
+## What is this example
+
+This is a very rough example of how to build a page to fetch and display clips between a user specified time frame.
+
+It uses Implicit auth to obtain a token, but this is just for the [GitHub pages demo](https://barrycarlyon.github.io/twitch_misc/examples/clips_navigator/).
+
+
+## TRY THIS EXAMPLE NOW!
+
+This example is also available via GitHub Pages!
+
+Give it a [whirl here](https://barrycarlyon.github.io/twitch_misc/examples/clips_navigator/)
+
+## Reference Documentation
+
+- [OAuth Implicit Code Flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth#oauth-implicit-code-flow)
+- [Get Clips](https://dev.twitch.tv/docs/api/reference#get-clips)
+- [Clips Embedding](https://dev.twitch.tv/docs/embed/video-and-clips#non-interactive-iframes-for-clips)
+
+## Setting up the config
+
+- Visit [Twitch Dev Console](https://dev.twitch.tv/console/)
+- Visit Applications
+- Manage your Application, or create one if you don't have one
+- Copy the Client ID into `client_id` JavaScript Variable
+- You'll need to throw this webpage into a website somewhere, and update the `redirect` in the html file and on the dev console accordingly.
+
+## Running the example
+
+If you have PHP installed
+
+> sudo php -S 127.0.0.1:80
+
+or just throw the code up on a webpage somewhere
\ No newline at end of file
diff --git a/examples/clips_navigator/index.html b/examples/clips_navigator/index.html
new file mode 100644
index 0000000..50a3a0c
--- /dev/null
+++ b/examples/clips_navigator/index.html
@@ -0,0 +1,306 @@
+
+
+
+ Clips Naivgator | Twitch API Example
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/extension_config/README.md b/examples/extension_config/README.md
new file mode 100644
index 0000000..8deee6f
--- /dev/null
+++ b/examples/extension_config/README.md
@@ -0,0 +1,20 @@
+## What is this example
+
+This example is a simple web based tool to read/write from the Extension config service.
+
+It sorta replaces the Config Service tool that is in the Developer Rig. Which calls the deprecated Extension APIs.
+
+## TRY THIS EXAMPLE NOW!
+
+This example is also available via GitHub Pages!
+
+Give it a [whirl here](https://barrycarlyon.github.io/twitch_misc/examples/extension_config/)
+
+## Reference Documentation
+
+- [Get Extension Configuration Segment](https://dev.twitch.tv/docs/api/reference#get-extension-configuration-segment)
+- [Set Extension Configuration Segment](https://dev.twitch.tv/docs/api/reference#set-extension-configuration-segment)
+
+## Desktop Alternative
+
+Checkout: https://github.com/BarryCarlyon/twitch_extension_tools
diff --git a/examples/extension_config/index.html b/examples/extension_config/index.html
new file mode 100644
index 0000000..15a3d2d
--- /dev/null
+++ b/examples/extension_config/index.html
@@ -0,0 +1,299 @@
+
+
+
+ Extension Config Tool | Twitch API Example
+
+
+
+
+
+
+
+
+
+
This tool serves as a Replacement for the Config Service control options in the Rig. As the Dev Rig likely won't be upgraded and calls the deprecated API's
+
+
This tools lets you read/write to the Twitch Extensions Configuration Service. For a desktop version that will rememebr your Extension Settings checkout BarryCarlyons' Twitch Extension Tools
After testing you can Disconnect the "Barry's GitHub Examples" Application on the Connections page
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Who
+
Move Guest
+
Live/Backstage
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/guest_star/obs_assist/README.md b/examples/guest_star/obs_assist/README.md
new file mode 100644
index 0000000..12cb48b
--- /dev/null
+++ b/examples/guest_star/obs_assist/README.md
@@ -0,0 +1,72 @@
+## What is this example
+
+This is an example tool to help streamers interact Guest Start with OBS 29+
+
+## Instruction for Use
+
+- Connect to OBS
+
+First it'll ask you to connect to OBS 29+ via OBS WebSocket.
+So you will need to have configured the OBS WebSocket if you have not already.
+
+- Select a Scene
+
+Then Select a Scene to operate against
+
+- Login with Twitch
+
+Click on Connect with Twitch to autheticate, it'll request Read Only permission for your channels Guest Star settings.
+
+- Add/Remove Slots
+
+It'll see how many Slots you have configured on Guest Star, and provide Add/Remove buttons for each slot.
+
+- Click Add or Remove
+
+This button will then add (or remove) a browser source to the selected OBS scene.
+It'll be 640x360 with "OBS Controls Volume" enabled.
+
+You'll then want to move it in OBS as needed (or resize)
+
+## TRY THIS EXAMPLE NOW!
+
+This example is also available via GitHub Pages!
+
+Give it a [whirl here](https://barrycarlyon.github.io/twitch_misc/examples/guest_star/obs_assist/)
+
+## Reference Documentation
+
+### Twitch
+
+- [Implict Authentication](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#implicit-grant-flow) - to get a Twitch Token to use
+- [Get Users](https://dev.twitch.tv/docs/api/reference#get-users) - we need to know who you are on Twitch
+- [Get Channel Guest Star Settings](https://dev.twitch.tv/docs/api/reference/#get-channel-guest-star-settings) - to get the Slots and Layout configured
+
+### OBS
+
+The following calls from [OBS Websocket Protocol](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md) are utilised.
+And to utilise the OBS websocket we use [obs-websocket-js](https://github.com/obs-websocket-community-projects/obs-websocket-js)
+
+#### Requests
+
+Asking or telling OBS to do things
+
+- [GetSceneList](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#getscenelist) - to get what scenes you have
+- [GetInputList](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#getinputlist) - to see what inputs you have
+- [GetSceneItemList](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#GetSceneItemList) - getting what is added to a given scene
+- [CreateInput](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#createinput) - to add/create a Guest Star Slot into OBS
+- [RemoveInput](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#removeinput) - to remove/delete a Guest Star Slot into OBS
+- [GetInputVolume](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#GetInputVolume) - to get the input volume of the source in OBS
+- [SetInputVolume](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#SetInputVolume) - to set the input volume of the source in OBS
+- [GetInputMute](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#GetInputMute) - to get the inputs mute status of the source in OBS
+- [ToggleInputMute](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#ToggleInputMute) - to toggle the inputs mute status of the source in OBS
+- [GetSceneItemEnabled](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#GetSceneItemEnabled) - is a Source visible or not
+- [SetSceneItemEnabled](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#SetSceneItemEnabled) - setting if a Source visible or not
+
+#### Events
+
+Asking OBS to send us information _as it happens_
+
+- [InputVolumeChanged](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#inputvolumechanged) - Monitor for Volume changes
+- [InputMuteStateChanged](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#InputMuteStateChanged) - Monitor for Mute state changes
+- [SceneItemEnableStateChanged](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#SceneItemEnableStateChanged) - Monitor for Visibility state changes
diff --git a/examples/guest_star/obs_assist/index.html b/examples/guest_star/obs_assist/index.html
new file mode 100644
index 0000000..6035dee
--- /dev/null
+++ b/examples/guest_star/obs_assist/index.html
@@ -0,0 +1,63 @@
+
+
+ OBS Assistant | Guest Star | Twitch API Example
+
+
+
+
This example will first ask you to login with Implicit Auth to get an API Token to use.
+
Get the code for this example on Github or just View the source instead
+
After testing you can Disconnect the "Barry's GitHub Examples" Application on the Connections page
+
+
Note: this tool should work perfectly fine IF running on the same computer as OBS.
+
+
+
+
Please Authorize to continue
+
+
+
+
+
+
OBS Scenes
+
Which scene would you like to add/remove Guest Star from
+
+
+
+
+
+
diff --git a/examples/poll_tool/poll_tool.js b/examples/poll_tool/poll_tool.js
new file mode 100644
index 0000000..1ed0842
--- /dev/null
+++ b/examples/poll_tool/poll_tool.js
@@ -0,0 +1,292 @@
+// go populate this with a client_id
+var client_id = 'hozgh446gdilj5knsrsxxz8tahr3koz';
+var redirect = window.location.origin + '/twitch_misc/';
+
+// setup a memory space for the token/userID
+var access_token = '';
+var user_id = '';
+var poll_id = '';
+var socket_space = false;
+
+let status = document.getElementById('status');
+
+// setup authorise link
+document.getElementById('authorize').setAttribute('href', 'https://id.twitch.tv/oauth2/authorize?client_id=' + client_id + '&redirect_uri=' + encodeURIComponent(redirect) + '&response_type=token&scope=channel:manage:polls');
+
+async function processToken(token) {
+ access_token = token;
+
+ status.textContent = 'Got Token. Loading Things';
+
+ // we need the userID
+ let user_resp = await fetch(
+ 'https://api.twitch.tv/helix/users',
+ {
+ method: 'GET',
+ headers: {
+ 'Client-ID': client_id,
+ 'Authorization': `Bearer ${access_token}`,
+ 'Accept': 'application/json'
+ }
+ }
+ );
+
+ if (user_resp.status != 200) {
+ status.textContent = `Failed to obtain User information ${user_resp.status} - ${await user_resp.text()}`;
+ return;
+ }
+
+ let user_data = await user_resp.json();
+ if (user_data.data.length != 1) {
+ status.textContent = `Failed to obtain a User`;
+ return;
+ }
+
+ user_id = user_data.data[0].id;
+ status.textContent = `Hello ${user_id} - ${user_data.data[0].login}`;
+
+ socket_space = new initSocket(true);
+ socket_space.on('connected', (id) => {
+ log(`Connected to WebSocket with ${id}`);
+ requestHooks(id, user_id);
+ });
+ socket_space.on('session_keepalive', () => {
+ document.getElementById('keepalive').textContent = new Date();
+ });
+
+ socket_space.on('channel.poll.begin', (msg) => {
+ writeESLog('begin');
+ let { metadata, payload } = msg;
+ let { event } = payload;
+ drawNewPoll(event);
+ });
+ socket_space.on('channel.poll.progress', (msg) => {
+ writeESLog('progress');
+ let { metadata, payload } = msg;
+ let { event } = payload;
+ drawUpdatePoll(event);
+ });
+ socket_space.on('channel.poll.end', (msg) => {
+ writeESLog('end');
+ let { metadata, payload } = msg;
+ let { event } = payload;
+ drawEndPoll(event);
+ });
+}
+
+document.getElementById('add_choice').addEventListener('click', (e) => {
+ let choices = document.getElementById('poll_choices').querySelectorAll('input');
+ if (choices.length < 5) {
+ // add one
+ let copy = choices[choices.length - 1].cloneNode();
+ copy.value = '';
+ document.getElementById('poll_choices').append(copy);
+ }
+});
+document.getElementById('poll_form').addEventListener('submit', (e) => {
+ e.preventDefault();
+
+ submitPoll();
+});
+document.getElementById('poll_end').addEventListener('click', endPoll);
+
+async function endPoll() {
+ let poll_resp = await fetch(
+ `https://api.twitch.tv/helix/polls`,
+ {
+ method: 'PATCH',
+ headers: {
+ 'Client-ID': client_id,
+ 'Authorization': `Bearer ${access_token}`,
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'
+ },
+ body: JSON.stringify({
+ broadcaster_id: user_id,
+ id: poll_id,
+ status: 'TERMINATED'
+ })
+ }
+ );
+
+ if (poll_resp.status == 200) {
+ status.textContent = `TERMINATED a poll`;
+ } else {
+ status.textContent = `Failed to TERMINATED a poll ${poll_resp.status} - ${await poll_resp.text()}`;
+ }
+}
+
+async function submitPoll() {
+ let payload = {
+ broadcaster_id: user_id,
+ title: document.getElementById('poll_title').value,
+ duration: parseInt(document.getElementById('poll_duration').value),
+ choices: [],
+ channel_points_voting_enabled: false
+ }
+
+ let poll_channel_points = parseInt(document.getElementById('poll_channel_points').value);
+ if (!isNaN(poll_channel_points) && poll_channel_points > 0) {
+ payload.channel_points_voting_enabled = true;
+ payload.channel_points_per_vote = poll_channel_points;
+ }
+
+ let choices = document.getElementById('poll_choices').querySelectorAll('input');
+ choices.forEach(choice => {
+ let text = choice.value.trim();
+
+ if (text.length > 0) {
+ payload.choices.push({
+ title: text
+ });
+ }
+ });
+
+ status.textContent = `Creating a poll`;
+
+ let poll_resp = await fetch(
+ 'https://api.twitch.tv/helix/polls',
+ {
+ method: 'POST',
+ headers: {
+ 'Client-ID': client_id,
+ 'Authorization': `Bearer ${access_token}`,
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'
+ },
+ body: JSON.stringify(payload)
+ }
+ );
+
+ if (poll_resp.status == 200) {
+ status.textContent = `Created a poll`;
+
+ // store poll ID so I can kill it
+ let poll_data = await poll_resp.json();
+ poll_id = poll_data.data[0].id;
+ } else {
+ status.textContent = `Failed to create a poll ${poll_resp.status} - ${await poll_resp.text()}`;
+ }
+}
+
+
+function requestHooks(session_id, user_id) {
+ let topics = {
+ 'channel.poll.begin': { version: 1, condition: { broadcaster_user_id: user_id } },
+ 'channel.poll.progress': { version: 1, condition: { broadcaster_user_id: user_id } },
+ 'channel.poll.end': { version: 1, condition: { broadcaster_user_id: user_id } },
+ }
+
+ log(`Spawn Topics for ${user_id}`);
+
+ for (let type in topics) {
+ log(`Attempt create ${type} - ${user_id}`);
+ let { version, condition } = topics[type];
+
+ fetch(
+ 'https://api.twitch.tv/helix/eventsub/subscriptions',
+ {
+ "method": "POST",
+ "headers": {
+ "Client-ID": client_id,
+ "Authorization": "Bearer " + access_token,
+ 'Content-Type': 'application/json'
+ },
+ "body": JSON.stringify({
+ type,
+ version,
+ condition,
+ transport: {
+ method: "websocket",
+ session_id
+ }
+ })
+ }
+ )
+ .then(resp => resp.json())
+ .then(resp => {
+ if (resp.error) {
+ log(`Error with eventsub Call ${type} Call: ${resp.message ? resp.message : ''}`);
+ } else {
+ log(`Created ${type}`);
+ }
+ })
+ .catch(err => {
+ console.log(err);
+ log(`Error with eventsub Call ${type} Call: ${err.message ? err.message : ''}`);
+ });
+ }
+}
+
+
+
+function drawNewPoll(poll) {
+ let { title, choices, started_at, ends_at } = poll;
+
+ let right = document.getElementById('right');
+ right.textContent = '';
+
+ let h2 = document.createElement('h2');
+ h2.textContent = title;
+ right.append(h2);
+
+ let tbl = document.createElement('table');
+ right.append(tbl);
+
+ choices.forEach(choice => {
+ let { id, title } = choice;
+
+ let r = tbl.insertRow();
+ let t = r.insertCell();
+ t.textContent = title;
+ let s = r.insertCell();
+ s.textContent = 0;
+ s.setAttribute('id', `poll_choice_${id}`);
+ });
+}
+function drawUpdatePoll(poll) {
+ let { choices } = poll;
+
+ let total_votes = 0;
+ choices.forEach(choice => {
+ let { id, title, channel_points_votes, votes } = choice;
+ total_votes += channel_points_votes + votes;
+
+ let el = document.getElementById(`poll_choice_${id}`);
+ el.textContent = (channel_points_votes + votes);
+ });
+}
+function drawEndPoll(poll) {
+ let { status } = poll;
+ if (status == "archived") {
+ return;
+ }
+
+ drawUpdatePoll(poll);
+
+ let winner = { score: 0, id: '', title: '' };
+
+ let { choices } = poll;
+ choices.forEach(choice => {
+ let { id, title, channel_points_votes, votes } = choice;
+ let score = channel_points_votes + votes;
+ if (score > winner.score) {
+ winner = {
+ score: score,
+ id,
+ title
+ }
+ }
+ });
+
+ let el = document.getElementById(`poll_choice_${winner.id}`);
+ if (el) {
+ el.classList.add('winner');
+ }
+}
+
+function writeESLog(evt) {
+ let l = document.createElement('p');
+ l.textContent = `${new Date().toString()} - ${evt}`;
+ eventsub_log.prepend(l);
+}
diff --git a/examples/sponsor_finder/index.html b/examples/sponsor_finder/index.html
new file mode 100644
index 0000000..8a3b36d
--- /dev/null
+++ b/examples/sponsor_finder/index.html
@@ -0,0 +1,141 @@
+
+
+
+ Sponsor Finder| Twitch API Example
+
+
+
+
This example first uses Implicit Auth to get a token to use then will return a page similar to The Directorty. Generally calls will be done/cached server side with an App Access Token
+
+
Get the code for this example on Github or just View the source instead
+
+
+
+
diff --git a/examples/stream_key/README.md b/examples/stream_key/README.md
new file mode 100644
index 0000000..974c9dd
--- /dev/null
+++ b/examples/stream_key/README.md
@@ -0,0 +1,34 @@
+## What is this example
+
+This is a very rough example of how to build a page to fetch a user Stream Key/display the stream key to the user logging in.
+
+It uses Implicit auth to obtain a token, but this is just for the [GitHub pages demo](https://barrycarlyon.github.io/twitch_misc/examples/stream_key/).
+
+If you are building this yourself and as a "server" application, you'd normally use a [Authorization code grant flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth#authorization-code-grant-flow). To allow you to refetch the StreamKey ad infinitum.
+
+## TRY THIS EXAMPLE NOW!
+
+This example is also available via GitHub Pages!
+
+Give it a [whirl here](https://barrycarlyon.github.io/twitch_misc/examples/stream_key/)
+
+## Reference Documentation
+
+- [OAuth Implicit Code Flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth#oauth-implicit-code-flow)
+- [Get Streams Key](https://dev.twitch.tv/docs/api/reference#get-stream-key)
+
+## Setting up the config
+
+- Visit [Twitch Dev Console](https://dev.twitch.tv/console/)
+- Visit Applications
+- Manage your Application, or create one if you don't have one
+- Copy the Client ID into `client_id` JavaScript Variable
+- You'll need to throw this webpage into a website somewhere, and update the `redirect` in the html file and on the dev console accordingly.
+
+## Running the example
+
+If you have PHP installed
+
+> sudo php -S 127.0.0.1:80
+
+or just throw the code up on a webpage somewhere
\ No newline at end of file
diff --git a/examples/stream_key/index.html b/examples/stream_key/index.html
new file mode 100644
index 0000000..44f3832
--- /dev/null
+++ b/examples/stream_key/index.html
@@ -0,0 +1,87 @@
+
+
+
+ Get My Stream Key | Twitch API Example
+
+
+
+
This example first uses Implicit Auth with scope channel:read:stream_key to get a token to use.
+
+
Get the code for this example on Github or just View the source instead
+
+ Authorize
+
+
+
+
+
diff --git a/examples/tag_counter/index.html b/examples/tag_counter/index.html
new file mode 100644
index 0000000..234e136
--- /dev/null
+++ b/examples/tag_counter/index.html
@@ -0,0 +1,168 @@
+
+
+
+ Tag Counter | Twitch API Example
+
+
+
+
+
+
This example first uses Implicit Auth to get a token to use then will return a page similar to The Directorty. Generally calls will be done/cached server side with an App Access Token
+
+
Get the code for this example on Github or just View the source instead
+
+
+
+
+
diff --git a/examples/team/README.md b/examples/team/README.md
new file mode 100644
index 0000000..a2c08f0
--- /dev/null
+++ b/examples/team/README.md
@@ -0,0 +1,58 @@
+## What is this example
+
+This is an example of using the API to do some things with "Team" data. In this example it's a rough copy of a Team Page from Twitch.
+
+It uses Implicit auth to obtain a token, but this is just for the [GitHub pages demo](https://barrycarlyon.github.io/twitch_misc/examples/team/).
+
+If you are building this yourself and as a "server" application, you can use any kind of token as it's all public data.
+
+## Notes
+
+This example is a bit long winded as it's designed for fronend/no server usage.
+
+Normally you would use an App Access Token on a server.
+And on that server you would use server side caching on a number of end points.
+
+Periodically load [The Team](https://dev.twitch.tv/docs/api/reference#get-teams) from the API and store the information about the team and the users currently in the team in a databaes table/cache.
+
+Periodically load from the cache the users in the team and then load their streams from the [Streams API](https://dev.twitch.tv/docs/api/reference#get-streams). Alternatively since you are buildin a Teams Product, you would use [EventSub](https://dev.twitch.tv/docs/eventsub) and utilise the [Steam Online](https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types#streamonline), [Steam Offline](https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types#streamoffline), and [Channel Update](https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types#channelupdate), topics to get updates when they happen instead of polling the API.
+
+So with EventSub you only poke [Get Teams](https://dev.twitch.tv/docs/api/reference#get-teams) periodically. And add/remove [EventSub Subscriptions](https://dev.twitch.tv/docs/api/reference#create-eventsub-subscription) as needed.
+
+## TRY THIS EXAMPLE NOW!
+
+This example is also available via GitHub Pages!
+
+Give it a [whirl here](https://barrycarlyon.github.io/twitch_misc/examples/team/)
+
+## Reference Documentation
+
+- [Embed Everything](https://dev.twitch.tv/docs/embed/everything)
+- [Player JS API](https://dev.twitch.tv/docs/embed/video-and-clips#interactive-frames-for-live-streams-and-vods)
+- [Get Team Information](https://dev.twitch.tv/docs/api/reference#get-teams)
+- [Get Streams](https://dev.twitch.tv/docs/api/reference#get-streams)
+- [Get Channel Information](https://dev.twitch.tv/docs/api/reference#get-channel-information)
+- [Get Users (for user Lookup)](https://dev.twitch.tv/docs/api/reference#get-users)
+- [Get Games (for game Boxart)](https://dev.twitch.tv/docs/api/reference#get-games)
+
+## Setting up the config
+
+- Visit [Twitch Dev Console](https://dev.twitch.tv/console/)
+- Visit Applications
+- Manage your Application, or create one if you don't have one
+- Copy the Client ID into `client_id` JavaScript Variable
+- You'll need to throw this webpage into a website somewhere, and update the `redirect` in the html file and on the dev console accordingly.
+
+## Running the example
+
+If you have PHP installed
+
+> sudo php -S 127.0.0.1:80
+
+or just throw the code up on a webpage somewhere
+
+## Screenshot
+
+This example will first load the "Loaded" team. Left is the ["Twitch" layout](https://www.twitch.tv/team/loadedllc) and right is the [output this example generates](https://barrycarlyon.github.io/twitch_misc/examples/team/).
+
+![Example](example.png)
diff --git a/examples/team/example.png b/examples/team/example.png
new file mode 100644
index 0000000..64d1325
Binary files /dev/null and b/examples/team/example.png differ
diff --git a/examples/team/index.html b/examples/team/index.html
new file mode 100644
index 0000000..2b832ce
--- /dev/null
+++ b/examples/team/index.html
@@ -0,0 +1,750 @@
+
+
+
+ Teams Page | Twitch API Example
+
+
+
+
+
+
This example first uses Implicit Auth to get a token to use then will Various endpoints. Generally calls will be done/cached server side with an App Access Token
+
+
Get the code for this example on Github or just View the source instead
+
+
+
+
diff --git a/examples/token_checker/README.md b/examples/token_checker/README.md
new file mode 100644
index 0000000..3890510
--- /dev/null
+++ b/examples/token_checker/README.md
@@ -0,0 +1,30 @@
+## What is this example
+
+A simple Token Validator
+
+This is an example on how to
+
+- Validate a Token
+- Check the tokens type
+- Get some user data about the token if it's a user token
+- Revoke that token
+
+## TRY THIS EXAMPLE NOW!
+
+This example is also available via GitHub Pages!
+
+Give it a [whirl here](https://barrycarlyon.github.io/twitch_misc/examples/token_checker/)
+
+## Reference Documentation
+
+- [Validate a request](https://dev.twitch.tv/docs/authentication/validate-tokens/)
+- [Obtaining a User by Access Token](https://dev.twitch.tv/docs/api/reference#get-users)
+- [Revoking a token](https://dev.twitch.tv/docs/authentication/revoke-tokens/)
+
+## Running the example
+
+If you have PHP installed
+
+> sudo php -S 127.0.0.1:80
+
+or just throw the code up on a webpage somewhere
diff --git a/examples/token_checker/index.html b/examples/token_checker/index.html
new file mode 100644
index 0000000..1d94d0d
--- /dev/null
+++ b/examples/token_checker/index.html
@@ -0,0 +1,187 @@
+
+
+
+ Token Checker | Twitch API Example
+
+
+
+
This example demonstrates how to Check a token for it's type, what ClientID generated it, what scopes it has, whose token it is and lets you revoke the token
+
+
Get the code for this example on Github or just View the source instead
+
+
+
+
+
diff --git a/examples/twitchemotes/README.md b/examples/twitchemotes/README.md
new file mode 100644
index 0000000..46e5e5d
--- /dev/null
+++ b/examples/twitchemotes/README.md
@@ -0,0 +1,41 @@
+## What is this example
+
+This is a tool for looking at a Channels Art. Such as Emotes and Badges.
+
+It uses Implicit auth to obtain a token, generally for a Server Side Solution you'd use an App Access/Client Credentials Token
+
+## TRY THIS EXAMPLE NOW!
+
+This example is also available via GitHub Pages!
+
+Give it a [whirl here](https://barrycarlyon.github.io/twitch_misc/examples/twitchemotes/)
+
+## Reference Documentation
+
+
+- [Get Users](https://dev.twitch.tv/docs/api/reference#get-users) - We need the User ID to call other endpoints
+- [Get Channel Chat Badges](https://dev.twitch.tv/docs/api/reference/#get-channel-chat-badges)
+- [Get Channel Emotes](https://dev.twitch.tv/docs/api/reference/#get-channel-emotes)
+
+## But what about rate limits?
+
+This example runs in a browser and we are using implicit auth to get a token to use.
+As a result we are using frontend JS to make the API calls, and browsers will limit the number of requests made to the same domain (api.twitch.tv in this example), so we can't "hammer" enough to get close to the rate limit.
+
+But that is something to consider if you are making these calls server side to many users.
+
+## Setting up the config
+
+- Visit [Twitch Dev Console](https://dev.twitch.tv/console/)
+- Visit Applications
+- Manage your Application, or create one if you don't have one
+- Copy the Client ID into `client_id` JavaScript Variable
+- You'll need to throw this webpage into a website somewhere, and update the `redirect` in the html file and on the dev console accordingly.
+
+## Running the example
+
+If you have PHP installed
+
+> sudo php -S 127.0.0.1:80
+
+or just throw the code up on a webpage somewhere
diff --git a/examples/twitchemotes/index.html b/examples/twitchemotes/index.html
new file mode 100644
index 0000000..11e4157
--- /dev/null
+++ b/examples/twitchemotes/index.html
@@ -0,0 +1,336 @@
+
+
+
+ TwitchEmotes | Twitch API Example
+
+
+
+
+
Twitch Emotes
+
We need a token to work with so please Authorize to continue
+
+
+
+
diff --git a/examples/vod_player/README.md b/examples/vod_player/README.md
new file mode 100644
index 0000000..e15a0cf
--- /dev/null
+++ b/examples/vod_player/README.md
@@ -0,0 +1,42 @@
+## What is this example
+
+This is an example of a VOD player that will skip muted segments, now that the Get Videos API returns this data
+
+It uses Implicit auth to obtain a token, but this is just for the [GitHub pages demo](https://barrycarlyon.github.io/twitch_misc/examples/vod_player/).
+
+If you are building this yourself and as a "server" application, you can use any kind of token as it's all public data.
+
+## TRY THIS EXAMPLE NOW!
+
+This example is also available via GitHub Pages!
+
+Give it a [whirl here](https://barrycarlyon.github.io/twitch_misc/examples/vod_player/)
+
+## Reference Documentation
+
+- [Embed Everything](https://dev.twitch.tv/docs/embed/everything)
+- [Player JS API](https://dev.twitch.tv/docs/embed/video-and-clips#interactive-frames-for-live-streams-and-vods)
+- [Get Videos](https://dev.twitch.tv/docs/api/reference#get-videos)
+- [Get users (for user Lookup)](https://dev.twitch.tv/docs/api/reference#get-users)
+
+## Setting up the config
+
+- Visit [Twitch Dev Console](https://dev.twitch.tv/console/)
+- Visit Applications
+- Manage your Application, or create one if you don't have one
+- Copy the Client ID into `client_id` JavaScript Variable
+- You'll need to throw this webpage into a website somewhere, and update the `redirect` in the html file and on the dev console accordingly.
+
+## Running the example
+
+If you have PHP installed
+
+> sudo php -S 127.0.0.1:80
+
+or just throw the code up on a webpage somewhere
+
+## Screenshot
+
+This shows a recent vod from GabrielAndDresden where the player (now paused for screenshot purposes) has skipped the first segment that is muted
+
+![Example](example.png)
diff --git a/examples/vod_player/example.png b/examples/vod_player/example.png
new file mode 100644
index 0000000..3ffa52c
Binary files /dev/null and b/examples/vod_player/example.png differ
diff --git a/examples/vod_player/index.html b/examples/vod_player/index.html
new file mode 100644
index 0000000..510dda4
--- /dev/null
+++ b/examples/vod_player/index.html
@@ -0,0 +1,461 @@
+
+
+
+ Vod Player | Twitch API Example
+
+
+
+
+
+
This example first uses Implicit Auth to get a token to use then will create a Player that will play a Vod from GabrielAndDresden that will skip muted segments. You could also look up a Different Channel