diff --git a/eventsub/websockets/web/activity_feed/README.md b/eventsub/websockets/web/activity_feed/README.md new file mode 100644 index 0000000..b59ec01 --- /dev/null +++ b/eventsub/websockets/web/activity_feed/README.md @@ -0,0 +1,42 @@ +## What is this example + +This is an exmaple of how to setup a EventSub WebSockets connection inside a Web Browser utilising the Chat Over EventSub Topics to build an activity feed of sorts + +It will use implicit auth to obtain an access token and then connect to EventSub WebSockets + +## TRY THIS EXAMPLE NOW! + +This example is also available via GitHub Pages! + +Give it a [whirl here](https://barrycarlyon.github.io/twitch_misc/eventsub/websockets/web/activity_feed/) + +## Reference Documentation + +For what is used in this example + +- [OAuth Implicit Code Flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth#implicit-grant-flow) +- [EventSub](https://dev.twitch.tv/docs/eventsub) +- [EventSub WebSockets](https://dev.twitch.tv/docs/eventsub/handling-websocket-events) + +## Notes + +This example gives an opinion on how to prcocess and collect the relevant events that would be untilised to run an overlay or a notification program for example. + +Specifically how to collect sub bombs (aka Community Gifts) into their groupings. + +Note the use of trim in the processName function, some users can have a space in their display name (at the end) +Or in the middle such as `Riot Games` but you are not likely to see middle space names in the data. + +## Running the example + +This is so rough that you need to upload it somewhere or know how to start a WebServer on 127.0.0.1 port 80 locally + +If you have PHP installed + +> sudo php -S 127.0.0.1:80 + +Will get you going real quick + +## Disconnecting the App + +If you use the GitHub Live example to test, you can Disconnect the "Barry's GitHub Examples" Application on the [Connections page](https://www.twitch.tv/settings/connections) diff --git a/eventsub/websockets/web/activity_feed/feed.js b/eventsub/websockets/web/activity_feed/feed.js new file mode 100644 index 0000000..92f80fb --- /dev/null +++ b/eventsub/websockets/web/activity_feed/feed.js @@ -0,0 +1,564 @@ +function runLineNotification({ payload }) { + let { event } = payload; + + let { system_message, notice_type, message } = event; + + let { text, fragments } = message; + + // channel + let { broadcaster_user_name, broadcaster_user_login, broadcaster_user_id } = event; + // entity + let { chatter_user_name, chatter_user_login, chatter_user_id, color, chatter_is_anonymous } = event; + + switch (notice_type) { + case 'sub': + let { sub } = event; + var { duration_months, is_prime, sub_tier } = sub; + + var r = activity_feed.insertRow(0); + r.setAttribute('title', system_message); + var cell = r.insertCell(); + cell.textContent = dateTime(); + var cell = r.insertCell(); + cell.textContent = broadcaster_user_login; + + var cell = r.insertCell(); + cell.style.color = color; + cell.textContent = processName(chatter_user_name, chatter_user_login); + + var cell = r.insertCell(); + cell.textContent = `New Sub ${processTier(sub_tier)}`; + + var cell = r.insertCell(); + // counts + var cell = r.insertCell(); + if (is_prime) { + cell.textContent = `With Twitch Prime`; + } else if (duration_months > 1) { + cell.textContent = `In Advance for ${duration_months}`; + } + + break; + case 'resub': + let { resub } = event; + + var { cumulative_months, streak_months, duration_months } = resub; + var { sub_tier, is_gift, is_prime } = resub; + + var r = activity_feed.insertRow(0); + r.setAttribute('title', system_message); + var cell = r.insertCell(); + cell.textContent = dateTime(); + var cell = r.insertCell(); + cell.textContent = broadcaster_user_login; + + var cell = r.insertCell(); + cell.style.color = color; + cell.textContent = processName(chatter_user_name, chatter_user_login); + + var cell = r.insertCell(); + cell.textContent = `Resub ${processTier(sub_tier)}`; + var cell = r.insertCell(); + + let durational = `${cumulative_months} months`; + if (cumulative_months % 12 === 0) { + durational = `${cumulative_months / 12} years`; + } + + if (streak_months) { + cell.textContent = `${durational} for ${streak_months} streak`; + } else { + cell.textContent = `${durational}`; + } + + var cell = r.insertCell(); + if (is_prime) { + cell.textContent = 'With Twitch Prime: '; + } + buildFromFragments(cell, fragments); + + break; + + case 'sub_gift': + let { sub_gift } = event; + + let { community_gift_id } = sub_gift; + var { sub_tier, cumulative_total, duration_months } = sub_gift; + + var { recipient_user_name, recipient_user_login, recipient_user_id } = sub_gift; + + if (community_gift_id) { + // this gift is part of a bomb + // duration is useless here as gift bombs are only a monther + + // look for the sub table + let target = document.getElementById(`victims_for_${community_gift_id}`); + if (target) { + //let sr = target.insertRow(); + //var cell = sr.insertCell(); + let cell = document.createElement('li') + target.append(cell); + cell.textContent = processName(recipient_user_name, recipient_user_login); + } else { + //go = false; + console.log('target doesnt exist', notice_type, event); + // now the real question what would be the best way to _wait_ + // do we draw the line with what we can + // or _wait_ I don't LIKE this but it works... + setTimeout(() => { + runLineNotification({ payload }) + }, 500); + + let hasProc = document.getElementById(`processing_for_${community_gift_id}`); + if (!hasProc) { + var proc = activity_feed.insertRow(0); + proc.setAttribute('id', `processing_for_${community_gift_id}`); + var cell = proc.insertCell(); + cell.setAttribute('colspan', 50); + cell.style.backgroundColor = 'red'; + cell.style.textAlign = 'center'; + cell.textContent = `Processing ${community_gift_id}`; + } + } + } else { + var r = activity_feed.insertRow(0); + r.setAttribute('title', system_message); + var cell = r.insertCell(); + cell.textContent = dateTime(); + var cell = r.insertCell(); + cell.textContent = broadcaster_user_login; + + var cell = r.insertCell(); + cell.style.color = color; + cell.textContent = processName(chatter_user_name, chatter_user_login, chatter_is_anonymous); + + var cell = r.insertCell(); + cell.textContent = `Direct Gift ${processTier(sub_tier)}`; + + var cell = r.insertCell(); + // counts N/A + var cell = r.insertCell(); + + let text = `Gifted ${processName(recipient_user_name, recipient_user_login)}`; + if (duration_months > 1) { + text = `${text} for ${duration_months} months`; + } + if (cumulative_total) { + text = `${text} it is their ${cumulative_total} gift to the channel`; + } + + cell.textContent = text; + } + + break; + case 'community_sub_gift': + let { community_sub_gift } = event; + // gift bomb occuring + var { cumulative_total } = community_sub_gift;// can be blank/null + var { id, sub_tier, total } = community_sub_gift; + + var r = activity_feed.insertRow(0); + r.setAttribute('title', system_message); + var cell = r.insertCell(); + cell.textContent = dateTime(); + var cell = r.insertCell(); + cell.textContent = broadcaster_user_login; + + var cell = r.insertCell(); + cell.style.color = color; + cell.textContent = processName(chatter_user_name, chatter_user_login, chatter_is_anonymous); + + var cell = r.insertCell(); + cell.textContent = `Community Gift ${processTier(sub_tier)}`; + + var cell = r.insertCell(); + if (cumulative_total) { + cell.textContent = `${total} gift${total > 1 ? 's' : ''} for ${cumulative_total} cumulative in the channel`; + } else { + cell.textContent = `${total} gift${total > 1 ? 's' : ''}`; + } + + var cell = r.insertCell(); + //var t = document.createElement('table'); + var t = document.createElement('ul'); + cell.append(t); + t.setAttribute('id', `victims_for_${id}`); + + let hasProc = document.getElementById(`processing_for_${id}`); + if (hasProc) { + hasProc.remove(); + } + + break; + + case 'pay_it_forward': + // direct gift forward + let { pay_it_forward } = event; + + // chatter recieved a gift from + var { gifter_user_name, gifter_user_login, gifter_user_id, gifter_is_anonymous } = pay_it_forward; + // chatter is giving to + var { recipient_user_name, recipient_user_login, recipient_user_id } = pay_it_forward; + + var r = activity_feed.insertRow(0); + r.setAttribute('title', system_message); + var cell = r.insertCell(); + cell.textContent = dateTime(); + var cell = r.insertCell(); + cell.textContent = broadcaster_user_login; + + var cell = r.insertCell(); + cell.style.color = color; + cell.textContent = processName(chatter_user_name, chatter_user_login); + + var cell = r.insertCell(); + cell.textContent = 'Paying it Forward'; + var cell = r.insertCell(); + // counts + var cell = r.insertCell(); + // message + if (!recipient_user_id) { + // its a bomb + cell.textContent = `Community Gifting in response to a gift from ${processName(gifter_user_name, gifter_user_login, gifter_is_anonymous)}`; + } else { + cell.textContent = `Gifting ${processName(recipient_user_name, recipient_user_login)} in response to a gift from ${processName(gifter_user_name, gifter_user_login, gifter_is_anonymous)}`; + } + // raises a sub_gift + + break; + + case 'gift_paid_upgrade': + let { gift_paid_upgrade } = event; + + // chatter recieved a gift from + var { gifter_user_name, gifter_user_login, gifter_user_id, gifter_is_anonymous } = gift_paid_upgrade; + // and is self upgrading + var r = activity_feed.insertRow(0); + r.setAttribute('title', system_message); + var cell = r.insertCell(); + cell.textContent = dateTime(); + var cell = r.insertCell(); + cell.textContent = broadcaster_user_login; + + var cell = r.insertCell(); + cell.style.color = color; + cell.textContent = processName(chatter_user_name, chatter_user_login); + var cell = r.insertCell(); + cell.textContent = 'Gift Upgrade'; + var cell = r.insertCell(); + // counts + var cell = r.insertCell(); + // message + cell.textContent = `Continuing their sub they got from ${processName(gifter_user_name, gifter_user_login, gifter_is_anonymous)}`; + + break; + case 'prime_paid_upgrade': + let { prime_paid_upgrade } = event; + + var { sub_tier } = prime_paid_upgrade; + + var r = activity_feed.insertRow(0); + r.setAttribute('title', system_message); + var cell = r.insertCell(); + cell.textContent = dateTime(); + var cell = r.insertCell(); + cell.textContent = broadcaster_user_login; + + var cell = r.insertCell(); + cell.style.color = color; + cell.textContent = processName(chatter_user_name, chatter_user_login); + var cell = r.insertCell(); + cell.textContent = `Prime Upgrade: ${processTier(sub_tier)}`; + var cell = r.insertCell(); + // counts + var cell = r.insertCell(); + // message + cell.textContent = `Upgraded from Twitch Prime`; + + break; + + case 'raid': + let { raid } = event; + let { profile_image_url, user_id, user_login, user_name, viewer_count } = raid; + + var r = activity_feed.insertRow(0); + r.setAttribute('title', system_message); + var cell = r.insertCell(); + cell.textContent = dateTime(); + var cell = r.insertCell(); + cell.textContent = broadcaster_user_login; + + var cell = r.insertCell(); + cell.style.color = color; + // the raiders is also the chatter.... soooo.... + //cell.textContent = processName(chatter_user_name, chatter_user_login); + cell.textContent = processName(user_name, user_login); + + var cell = r.insertCell(); + cell.textContent = 'Raid'; + var cell = r.insertCell(); + var cell = r.insertCell(); + cell.textContent = `Raiding with ${viewer_count} viewers`; + + break; + + case 'bits_badge_tier': + // to consider... + let { bits_badge_tier } = event; + var { tier } = bits_badge_tier; + + var r = activity_feed.insertRow(0); + r.setAttribute('title', system_message); + var cell = r.insertCell(); + cell.textContent = dateTime(); + var cell = r.insertCell(); + cell.textContent = broadcaster_user_login; + + var cell = r.insertCell(); + cell.style.color = color; + cell.textContent = processName(chatter_user_name, chatter_user_login); + + var cell = r.insertCell(); + cell.textContent = 'Bits Tier'; + var cell = r.insertCell(); + cell.textContent = tier; + var cell = r.insertCell(); + buildFromFragments(cell, fragments); + + break; + + case 'announcement': + // skip + break; + default: + go = false; + console.log('Unexpected', notice_type, event); + } +} + +function runLineMessage({ payload }) { + let { event } = payload; + + let { message_type, message } = event; + + let { text, fragments } = message; + + // channel + let { broadcaster_user_name, broadcaster_user_login, broadcaster_user_id } = event; + // entity + let { chatter_user_name, chatter_user_login, chatter_user_id, color } = event; + + let { cheer, channel_points_custom_reward_id } = event; + + let title_of_event = ''; + switch (message_type) { + case 'channel_points_sub_only': + title_of_event = 'SubOnly Message'; + break; + case 'channel_points_highlighted': + title_of_event = 'Highlighted'; + break; + case 'user_intro': + title_of_event = 'User Intro'; + break; + case 'power_ups_gigantified_emote': + title_of_event = 'Big Emote'; + break; + case 'power_ups_message_effect': + title_of_event = 'Pretty Chat'; + break; + default: + if (channel_points_custom_reward_id) { + title_of_event = 'ChannelPoints'; + } + } + + if (title_of_event != '') { + var r = activity_feed.insertRow(0); + + var cell = r.insertCell(); + cell.textContent = dateTime(); + var cell = r.insertCell(); + cell.textContent = broadcaster_user_login; + + var cell = r.insertCell(); + cell.style.color = color; + cell.textContent = processName(chatter_user_name, chatter_user_login); + + // what span tier + var cell = r.insertCell(); + cell.textContent = title_of_event; + // counts + var cell = r.insertCell(); + //message + var cell = r.insertCell(); + buildFromFragments(cell, fragments); + } else { + if (cheer) { + // it's a cheer message + // bits is the total bits used + let { bits } = cheer; + var r = activity_feed.insertRow(0); + + var cell = r.insertCell(); + cell.textContent = dateTime(); + var cell = r.insertCell(); + cell.textContent = broadcaster_user_login; + + var cell = r.insertCell(); + cell.style.color = color; + cell.textContent = processName(chatter_user_name, chatter_user_login); + // what span tier + var cell = r.insertCell(); + cell.textContent = 'Cheer'; + // count + var cell = r.insertCell(); + cell.textContent = bits; + //message + var cell = r.insertCell(); + buildFromFragments(cell, fragments); + } else { + //go = false; + //console.log('Unexpected', message_type, event); + } + } +} + + + +function dateTime() { + let n = new Date(); + let d = []; + d.push(n.getHours()); + d.push(n.getMinutes()); + d.push(n.getSeconds()); + for (var x=0;x { + let { prefix, tiers } = cheermote; + if (tiers && tiers.length > 0) { + prefix = prefix.toLowerCase(); + knownCheermotes[prefix] = {}; + + tiers.forEach(tier => { + let { can_cheer, id, images } = tier; + if (can_cheer) { + let image = images.dark.animated["1.5"]; + knownCheermotes[prefix][id] = image; + } + }); + } + }); +} diff --git a/eventsub/websockets/web/activity_feed/framework.js b/eventsub/websockets/web/activity_feed/framework.js new file mode 100644 index 0000000..860c756 --- /dev/null +++ b/eventsub/websockets/web/activity_feed/framework.js @@ -0,0 +1,256 @@ +// These are set for the GitHub Pages Example +// Substitute as needed +var client_id = 'hozgh446gdilj5knsrsxxz8tahr3koz'; +var redirect = window.location.origin + '/twitch_misc/'; +var access_token = ''; +var socket_space = ''; +var session_id = ''; +var my_user_id = ''; + +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:chat'); + +if (document.location.hash && document.location.hash != '') { + log('Checking for token'); + var parsedHash = new URLSearchParams(window.location.hash.slice(1)); + if (parsedHash.get('access_token')) { + log('Got a token'); + processToken(parsedHash.get('access_token')); + } +} + +function log(line) { + console.log(line); +} + +function processToken(token) { + access_token = token; + + authorize.style.display = 'none';//remove link to avoid relinks.... + + fetch( + 'https://api.twitch.tv/helix/users', + { + "headers": { + "Client-ID": client_id, + "Authorization": `Bearer ${access_token}` + } + } + ) + .then(resp => resp.json()) + .then(resp => { + socket_space = new initSocket(true); + // and build schnanaigans + socket_space.on('connected', (id) => { + log(`Connected to WebSocket with ${id}`); + session_id = id; + my_user_id = resp.data[0].id; + + requestHooks(resp.data[0].id, my_user_id); + }); + + socket_space.on('session_silenced', () => { + document.getElementById('keepalive').textContent = 'Session mystery died due to silence detected'; + }); + socket_space.on('session_keepalive', () => { + document.getElementById('keepalive').textContent = new Date(); + }); + + socket_space.on('channel.chat.notification', runLineNotification); + socket_space.on('channel.chat.message', runLineMessage); + }) + .catch(err => { + console.log(err); + log('Error with Users Call'); + }); +} + +function addUser(username) { + let url = new URL('https://api.twitch.tv/helix/users'); + url.search = new URLSearchParams([['login', username]]).toString(); + + fetch( + url, + { + "headers": { + "Client-ID": client_id, + "Authorization": `Bearer ${access_token}` + } + } + ) + .then(resp => resp.json()) + .then(resp => { + log(`Got ${resp.data[0].id} for ${username}`); + requestHooks(resp.data[0].id, my_user_id); + loadCheermotes(resp.data[0].id); + }) + .catch(err => { + console.log(err); + log('Error with Users Call'); + }); +} + + +function requestHooks(broadcaster_user_id, user_id) { + let topics = { + 'channel.chat.notification': { version: "1", condition: { broadcaster_user_id, user_id } }, + 'channel.chat.message': { version: "1", condition: { broadcaster_user_id, user_id } } + } + + log(`Spawn Topics for ${user_id}`); + + for (let type in topics) { + log(`Attempt create ${type} - ${broadcaster_user_id} via ${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}`); + document.getElementById('subscriptions_cost').textContent = `${resp.total}/${resp.total_cost}/${resp.max_total_cost}`; + } + }) + .catch(err => { + console.log(err); + log(`Error with eventsub Call ${type} Call: ${err.message ? err.message : ''}`); + }); + } +} + +document.getElementById('subscriptions_refresh').addEventListener('click', (e) => { + fetchSubs(); +}); + +let subscriptions = document.getElementById('subscriptions'); +function fetchSubs(after) { + let url = new URL('https://api.twitch.tv/helix/eventsub/subscriptions'); + let params = { + first: 100, + status: 'enabled' + }; + if (after) { + params.after = after; + } + + url.search = new URLSearchParams(params).toString(); + + fetch( + url, + { + "method": "GET", + "headers": { + "Client-ID": client_id, + "Authorization": `Bearer ${access_token}`, + 'Content-Type': 'application/json' + } + } + ) + .then(resp => resp.json()) + .then(resp => { + + subscriptions.textContent = ''; + + resp.data.forEach(sub => { + let tr = document.createElement('tr'); + subscriptions.append(tr); + + add(tr, sub.id); + add(tr, sub.type); + + let keys = Object.keys(sub.condition); + if (sub.condition[keys[0]]) { + add(tr, sub.condition[keys[0]]); + } else { + add(tr, sub.condition[keys[1]]); + } + add(tr, sub.cost); + + add(tr, sub.status); + + let td = document.createElement('td'); + tr.append(td); + td.textContent = 'Delete'; + td.classList.add('delete_button'); + td.addEventListener('click', (e) => { + deleteSub(sub.id) + .then(resp => { + console.log('Delete', resp.status); + + if (resp.status) { + td.textContent = 'Deleted'; + } else { + td.textContent = `Err ${resp.status}`; + } + }) + .catch(err => { + console.log(err); + log(`Error with eventsub delete`); + }); + }); + }); + + document.getElementById('subscriptions_cost').textContent = `${resp.total}/${resp.total_cost}/${resp.max_total_cost}`; + + if (resp.pagination) { + if (resp.pagination.cursor) { + fetchSubs(resp.pagination.cursor); + } + } + }) + .catch(err => { + console.log(err); + log(`Error with eventsub Fetch`); + }); +} + +function add(tr, text) { + let td = document.createElement('td'); + td.textContent = text; + tr.append(td); +} + +function deleteSub(id) { + let url = new URL('https://api.twitch.tv/helix/eventsub/subscriptions'); + url.search = new URLSearchParams([['id', id]]).toString(); + + return fetch( + url, + { + "method": "DELETE", + "headers": { + "Client-ID": client_id, + "Authorization": `Bearer ${access_token}`, + 'Content-Type': 'application/json' + } + } + ) +} + +document.getElementById('form').addEventListener('submit', (e) => { + e.preventDefault(); + let username = document.getElementById('add_user').value; + log(`Lets lookup and add ${username}`); + addUser(username); + document.getElementById('add_user').value = ''; +}); \ No newline at end of file diff --git a/eventsub/websockets/web/activity_feed/index.html b/eventsub/websockets/web/activity_feed/index.html new file mode 100644 index 0000000..eceeaba --- /dev/null +++ b/eventsub/websockets/web/activity_feed/index.html @@ -0,0 +1,72 @@ + + + + Activity Feed | Twitch API Example + + + + + + +

This example demonstrates how to connect to, create subscriptions and recieve data from EventSub WebSockets

+

It'll use Implicit auth to obtain a token to use

+ +

Authorize and Connect

+ +
+

Add another broadcaster channel to your socket to listen to

+
+
+ + + +
+
+
+ +
Last KeepAlive: Total/Cost/MaxCost:
+ +
+ + + + + + + + + + + + +
TimeChannelWhomWhatCountsMessage
+
+ +
+
Click to Refresh Subscriptions
+ + + + + + + + + + + + +
Subscription IDTopicCondition User IDCostStatus
+
+ + + + + + \ No newline at end of file diff --git a/eventsub/websockets/web/basic/README.md b/eventsub/websockets/web/basic/README.md new file mode 100644 index 0000000..9034ce1 --- /dev/null +++ b/eventsub/websockets/web/basic/README.md @@ -0,0 +1,36 @@ +## What is this example + +This is an exmaple of how to setup a EventSub WebSockets connection inside a Web Browser. + +It will use implicit auth to obtain an access token and then connect to EventSub WebSockets + +## TRY THIS EXAMPLE NOW! + +This example is also available via GitHub Pages! + +Give it a [whirl here](https://barrycarlyon.github.io/twitch_misc/eventsub/websockets/web/basic/) + +## Reference Documentation + +- [OAuth Implicit Code Flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth#implicit-grant-flow) +- [EventSub](https://dev.twitch.tv/docs/eventsub) +- [EventSub WebSockets](https://dev.twitch.tv/docs/eventsub/handling-websocket-events) + +## What this example doesn't do + +This example doesn't handle "long periods of silence where something has gone wrong and you need to reconnect". +So make sure you honor the returned value in the welcome message of `keepalive_timeout_seconds` + +## Running the example + +This is so rough that you need to upload it somewhere or know how to start a WebServer on 127.0.0.1 port 80 locally + +If you have PHP installed + +> sudo php -S 127.0.0.1:80 + +Will get you going real quick + +## Disconnecting the App + +If you use the GitHub Live example to test, you can Disconnect the "Barry's GitHub Examples" Application on the [Connections page](https://www.twitch.tv/settings/connections) diff --git a/eventsub/websockets/web/basic/index.html b/eventsub/websockets/web/basic/index.html new file mode 100644 index 0000000..5cf0879 --- /dev/null +++ b/eventsub/websockets/web/basic/index.html @@ -0,0 +1,376 @@ + + + + EventSub WebSockets with Implicit Auth Example + + + + + + +

This example demonstrates how to connect to, create subscriptions and recieve data from EventSub WebSockets

+

It will just "dumb log" an EventSub notification.

+

It'll use Implicit auth to obtain a token to use

+ + + +
+
LOG
+
+ +
+

From the RFC - https://discuss.dev.twitch.tv/t/rfc-0016-eventsub-websockets/32652

+ +

The above may be out of date check the Subscription limits for changes

+

A Websocket can subscribe to 10 cost 1 subscriptions

+
+
+

Add another user to your socket to listen to

+
+
+ + + +
+
+
+ +
Last KeepAlive:
+
Click to Refresh Subscriptions
+
+ + + + + + + + + + + + +
Subscription IDTopicCondition User IDCostStatus
+
+ +
+ + + + + + diff --git a/eventsub/websockets/web/charity/README.md b/eventsub/websockets/web/charity/README.md new file mode 100644 index 0000000..c6bab03 --- /dev/null +++ b/eventsub/websockets/web/charity/README.md @@ -0,0 +1,44 @@ +## What is this example + +This is an exmaple of how to setup a EventSub Websockets connection inside a Web Browser. + +It will use implicit auth to obtain an access token and then connect to EventSub Websockets. + +This example will draw a simple example of a Chairty goal bar and draw donations as they occur. + +It would also handle drawing multiple bars if multiple charities were running. +Which, sure, isn't a thing right now, but the API is designed in a way that it could be a thing. + +![A Screenshot](example.png) + +## TRY THIS EXAMPLE NOW! + +This example is also available via GitHub Pages! + +Give it a [whirl here](https://barrycarlyon.github.io/twitch_misc/eventsub/websockets/web/charity/) + +## Reference Documentation + +- [OAuth Implicit Code Flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth#implicit-grant-flow) +- [EventSub](https://dev.twitch.tv/docs/eventsub) +- [EventSub WebSockets](https://dev.twitch.tv/docs/eventsub/handling-websocket-events) +- [Get Charity Campaign](https://dev.twitch.tv/docs/api/reference#get-charity-campaign) + +## What this example doesn't do + +This example doesn't handle "long periods of silence where something has gone wrong and you need to reconnect". +So make sure you honor the returned value in the welcome message of `keepalive_timeout_seconds` + +## Running the example + +This is so rough that you need to upload it somewhere or know how to start a WebServer on 127.0.0.1 port 80 locally + +If you have PHP installed + +> sudo php -S 127.0.0.1:80 + +Will get you going real quick + +## Disconnecting the App + +If you use the GitHub Live example to test, you can Disconnect the "Barry's GitHub Examples" Application on the [Connections page](https://www.twitch.tv/settings/connections) diff --git a/eventsub/websockets/web/charity/example.png b/eventsub/websockets/web/charity/example.png new file mode 100644 index 0000000..98e6631 Binary files /dev/null and b/eventsub/websockets/web/charity/example.png differ diff --git a/eventsub/websockets/web/charity/index.html b/eventsub/websockets/web/charity/index.html new file mode 100644 index 0000000..b9a863d --- /dev/null +++ b/eventsub/websockets/web/charity/index.html @@ -0,0 +1,465 @@ + + + + Charity EventSub Websockets with Implicit Auth Example + + + + + + + + +

This example demonstrates how to connect to, create subscriptions and recieve data from EventSub Websockets

+

This will draw some basic overlay style charity stuff.

+

It'll use Implicit auth to obtain a token to use

+ + + +
+
LOG
+
+ +
Last KeepAlive:
+ +
+
+ + + + + + diff --git a/eventsub/websockets/web/chat/README.md b/eventsub/websockets/web/chat/README.md new file mode 100644 index 0000000..9021bcc --- /dev/null +++ b/eventsub/websockets/web/chat/README.md @@ -0,0 +1,76 @@ +## What is this example + +This is an exmaple of how to setup a EventSub WebSockets connection inside a Web Browser utilising the Chat Over EventSub Topics. + +It will use implicit auth to obtain an access token and then connect to EventSub WebSockets + +## TRY THIS EXAMPLE NOW! + +This example is also available via GitHub Pages! + +Give it a [whirl here](https://barrycarlyon.github.io/twitch_misc/eventsub/websockets/web/chat/) + +## How Does Authentication work + +### If you are using EventSub over Websockets: + +For example you are making a third party chat client, or you are a game client running from the game. + +Just generated user access token with the following scopes + +- `user:read:chat` from the user you wish to read chat as + +You then use that user access token to create subscriptions to your socket. + +Thats it + +### If you are using EventSub over Webhooks/Conduits: + +For example you are a channel bot that handles moderation + +Prior generated user access token with the following scopes or permissions: + +- `user:read:chat` from the user account you wish to read chat as (usually the bot) +- `user:bot` from the user account you wish to act as a bot as +- moderator status in the channel you wish to connect to + +You then ignore the user access token(s) and use an [App Access/Client Credentials](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#client-credentials-grant-flow) token to create subscriptions to your Webhook. + +### If you are using EventSub over Webhooks/Conduits (more common approach): + +For example you are the channel bot that just sends/reads chat, such as a game (where a control server is in use) or hydrationBot (as water bot doesn't and shouldn't be given perms) + +Prior generated user access token with the following scopes or permissions: + +- `user:read:chat` from the user account you wish to read chat as (usually the bot) +- `user:bot` from the user account you wish to act as a bot as +- `channel:bot` from the channel you wish to connect to + +You then ignore the user access token(s) and use an [App Access/Client Credentials](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#client-credentials-grant-flow) token to create subscriptions to your Webhook. + +## Reference Documentation + +For what is used in this example + +- [OAuth Implicit Code Flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth#implicit-grant-flow) +- [EventSub](https://dev.twitch.tv/docs/eventsub) +- [EventSub WebSockets](https://dev.twitch.tv/docs/eventsub/handling-websocket-events) + +## What this example doesn't do + +This example doesn't handle "long periods of silence where something has gone wrong and you need to reconnect". +So make sure you honor the returned value in the welcome message of `keepalive_timeout_seconds` + +## Running the example + +This is so rough that you need to upload it somewhere or know how to start a WebServer on 127.0.0.1 port 80 locally + +If you have PHP installed + +> sudo php -S 127.0.0.1:80 + +Will get you going real quick + +## Disconnecting the App + +If you use the GitHub Live example to test, you can Disconnect the "Barry's GitHub Examples" Application on the [Connections page](https://www.twitch.tv/settings/connections) diff --git a/eventsub/websockets/web/chat/index.html b/eventsub/websockets/web/chat/index.html new file mode 100644 index 0000000..5888832 --- /dev/null +++ b/eventsub/websockets/web/chat/index.html @@ -0,0 +1,680 @@ + + + + EventSub WebSockets Chat edition with Implicit Auth Example + + + + + + +

This example demonstrates how to connect to, create subscriptions and recieve data from EventSub WebSockets

+

It'll use Implicit auth to obtain a token to use

+ +

Authorize and Connect

+ +
+
+
+
+

Click on a window to switch which is visible/on top. (Barrys Crap Tabs). Left is a brain dump right is a easy to read.

+ +
+

Add another broadcaster channel to your socket to listen to

+
+
+ + + +
+
+
+ +
Last KeepAlive: Total/Cost/MaxCost:
+
Click to Refresh Subscriptions
+
+ + + + + + + + + + + + +
Subscription IDTopicCondition User IDCostStatus
+
+ +
+ + + + + + diff --git a/eventsub/websockets/web/creatorgoals/README.md b/eventsub/websockets/web/creatorgoals/README.md new file mode 100644 index 0000000..3cc486f --- /dev/null +++ b/eventsub/websockets/web/creatorgoals/README.md @@ -0,0 +1,43 @@ +## What is this example + +This is an exmaple of how to setup a EventSub Websockets connection inside a Web Browser. + +It will use implicit auth to obtain an access token and then connect to EventSub Websockets. + +This example will draw a creator goal bar and update it as events occur. +And handles a goal ending and starting. + +It would also handle drawing multiple bars if multiple goals were running. +Which, sure, isn't a thing right now, but the API is designed in a way that it could be a thing. + +## TRY THIS EXAMPLE NOW! + +This example is also available via GitHub Pages! + +Give it a [whirl here](https://barrycarlyon.github.io/twitch_misc/eventsub/websockets/web/creatorgoals/) + +## Reference Documentation + +- [OAuth Implicit Code Flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth#implicit-grant-flow) +- [EventSub](https://dev.twitch.tv/docs/eventsub) +- [EventSub WebSockets](https://dev.twitch.tv/docs/eventsub/handling-websocket-events) +- [Get Creator Goals](https://dev.twitch.tv/docs/api/reference#get-creator-goals) + +## What this example doesn't do + +This example doesn't handle "long periods of silence where something has gone wrong and you need to reconnect". +So make sure you honor the returned value in the welcome message of `keepalive_timeout_seconds` + +## Running the example + +This is so rough that you need to upload it somewhere or know how to start a WebServer on 127.0.0.1 port 80 locally + +If you have PHP installed + +> sudo php -S 127.0.0.1:80 + +Will get you going real quick + +## Disconnecting the App + +If you use the GitHub Live example to test, you can Disconnect the "Barry's GitHub Examples" Application on the [Connections page](https://www.twitch.tv/settings/connections) diff --git a/eventsub/websockets/web/creatorgoals/index.html b/eventsub/websockets/web/creatorgoals/index.html new file mode 100644 index 0000000..c276fc4 --- /dev/null +++ b/eventsub/websockets/web/creatorgoals/index.html @@ -0,0 +1,336 @@ + + + + EventSub Websockets with Implicit Auth Example + + + + + + + + +

This example demonstrates how to connect to, create subscriptions and recieve data from EventSub Websockets

+

It will just "dumb log" an EventSub notification.

+

It'll use Implicit auth to obtain a token to use

+ + + +
+
LOG
+
+ +
Last KeepAlive:
+ +
+ + + + + + \ No newline at end of file diff --git a/eventsub/websockets/web/eventsub.js b/eventsub/websockets/web/eventsub.js new file mode 100644 index 0000000..9c572e6 --- /dev/null +++ b/eventsub/websockets/web/eventsub.js @@ -0,0 +1,208 @@ +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 function lets you test the disconnect on send method + 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; +}