Skip to content

Instantly share code, notes, and snippets.

@RealityRipple
Last active April 17, 2023 03:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RealityRipple/f50bd19b1831af0ed8ae9e908dba3e40 to your computer and use it in GitHub Desktop.
Save RealityRipple/f50bd19b1831af0ed8ae9e908dba3e40 to your computer and use it in GitHub Desktop.
thisStream Twitch Leaderboard Display
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>thisStream</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Palanquin+Dark:wght@600&display=swap');
.text
{
font-size: 32px;
line-height: 32px;
padding-bottom: 16px;
color: #FFFFFF;
text-shadow: 2px 2px 3px #000000;
font-family: 'Palanquin Dark', sans-serif;
}
body
{
/* to left-align the message remove these two lines: */
justify-content: right; /* can also be center */
align-items: right;
}
</style>
<script>
'use strict';
/* thisStream Leaderboard Display
* ==============================
*
* v0.3 BETA
*
* <https://realityripple.com/Tools/Twitch/thisStream/>
*
*
* Help
* ----
*
* Please contact RealityRipple for assistance, bug reports, or questions.
*
* <https://realityripple.com>
* <https://twitch.tv/realityripple>
* <https://discord.gg/fcxJ9tq3XS>
*
* Variables
* ---------
*
* Subscriptions:
*
* %SUB_T1% number of tier 1 subs this stream
* %SUB_T2% number of tier 2 subs this stream)
* %SUB_T3% number of tier 3 subs this stream
* %SUB_PRIME% number of prime subs this stream
*
* %SUB_USER% latest subscriber this stream
* %SUB_USER_AGE% latest subscriber's number of months
*
* %SUB_ALL% number of subscribers
*
*
* Gift Subs:
*
* %GIFT_T1% number of tier 1 gifts this stream
* %GIFT_T2% number of tier 2 gifts this stream
* %GIFT_T3% number of tier 3 gifts this stream
*
* %GIFT_LEADER% user with the highest number of gifts this stream
* %GIFT_LEADER_ALL% highest number of gifts sent by a single user this stream
*
* %GIFT_USER% latest gifter this stream
* %GIFT_USER_COUNT% number of latest gift
* %GIFT_USER_ALL% total number of gifts by the latest gifter this stream
*
*
* Cheers:
*
* %BITS_ALL% number of bits cheered this stream
*
* %BITS_LEADER% user with the highest number of bits cheered this stream
* %BITS_LEADER_ALL% highest number of bits cheered from a single user this stream
*
* %BITS_USER% latest cheerer this stream
* %BITS_USER_COUNT% number of bits in the latest cheer
* %BITS_USER_ALL% number of bits cheered by the latest cheerer this stream
*
*
* Follows:
*
* %FOLLOWER% latest follower this stream
* %FOLLOWER_COUNT% number of followers this stream
* %FOLLOWER_ALL% number of followers
*
*
*/
/* jshint esversion: 11, bitwise: false, eqeqeq: true, loopfunc: true, forin: true, freeze: true, futurehostile: true, leanswitch: true, noarg: true, nocomma: true, nonbsp: true, nonew: true, noreturnawait: true, quotmark: single, shadow: outer, singleGroups: false, strict: global, trailingcomma: false, undef: true, unused: true, varstmt: true */
const cfg = {
channel: 'CHANNEL_NAME',
login: {
client: '7qsxhop32gcznr2u8ulnlayujqb4nq',
oauth_refresh: 'OAUTH_REFRESH' //Requires moderator:read:followers and channel:read:subscriptions
},
interval: 5000, // milliseconds between transitions
reveal: 4000, // milliseconds for each message to appear or disappear
singular: 's', // a letter to remove from the end of a message if the numeric value is equal to 1
hideEmpty: true, // if a variable is empty (or 0), the message containing that variable will be skipped
emptyName: 'Nobody', // name to use if a variable is empty and hideEmpty is false
idle: 'Welcome', // message to display when every item in the list is empty (or hidden due to hideEmpty)
numLocale: 'en-US', // locale to use for number formatting (thousands separators)
list: [
'%FOLLOWER_ALL% Followers',
'%FOLLOWER_COUNT% New Followers',
'%SUB_PRIME% New Prime Subs',
'%SUB_T1+GIFT_T1% New Tier 1 Subs',
'%SUB_T2+GIFT_T2% New Tier 2 Subs',
'%SUB_T3+GIFT_T3% New Tier 3 Subs',
'Gift Leader %GIFT_LEADER% with %GIFT_LEADER_ALL% Gift Subs',
'%SUB_ALL% Subscribers',
'%BITS_ALL% Bits',
'Cheer Leader %BITS_LEADER% with %BITS_LEADER_ALL% Bits'
],
event: { // events can be arrays of strings to show one after another, or false to do nothing
follow: false, // ['Welcome %FOLLOWER%'],
sub: {
'1000': false, // ['%SUB_USER% Subscribed'],
'2000': false, // ['%SUB_USER% Subbed at Tier 2'],
'3000': false, // ['%SUB_USER% Subbed at Tier 3'],
'Prime': false // ['%SUB_USER% Subbed with Prime']
},
resub: {
'1000': false, // ['%SUB_USER% Resubscribed for %SUB_USER_AGE% months'],
'2000': false, // ['%SUB_USER% Resubbed at Tier 2 for %SUB_USER_AGE% months'],
'3000': false, // ['%SUB_USER% Resubbed at Tier 3 for %SUB_USER_AGE% months'],
'Prime': false // ['%SUB_USER% Resubbed with Prime for %SUB_USER_AGE% months']
},
gift: {
'1000': false, // ['%GIFT_USER% Gifted a Subscription', 'Today, %GIFT_USER% has Gifted %GIFT_USER_ALL% Subs'],
'2000': false, // ['%GIFT_USER% Gifted a Tier 2 Sub', 'Today, %GIFT_USER% has Gifted %GIFT_USER_ALL% Subs'],
'3000': false // ['%GIFT_USER% Gifted a Tier 3 Sub', 'Today, %GIFT_USER% has Gifted %GIFT_USER_ALL% Subs']
},
giftbomb: {
'1000': false, // ['%GIFT_USER% Gifted %GIFT_USER_COUNT% Subscriptions', 'Today, %GIFT_USER% has Gifted %GIFT_USER_ALL% Subs'],
'2000': false, // ['%GIFT_USER% Gifted %GIFT_USER_COUNT% Tier 2 Subs', 'Today, %GIFT_USER% has Gifted %GIFT_USER_ALL% Subs'],
'3000': false // ['%GIFT_USER% Gifted %GIFT_USER_COUNT% Tier 3 Subs', 'Today, %GIFT_USER% has Gifted %GIFT_USER_ALL% Subs']
},
cheer: false // ['%BITS_USER% Cheered %BITS_USER_COUNT% Bits', 'Today, %BITS_USER% has Cheered %BITS_USER_ALL% Bits']
}
};
//////////////////////////////////////////////////////////////////////////////
// don't mess with things below this line without knowing what you're doing //
//////////////////////////////////////////////////////////////////////////////
if (typeof cfg === 'undefined')
throw new Error('Corrupted Configuration detected.');
document.title = cfg.channel + ' thisStream';
let eList = {
sub: {
'1000': 0,
'2000': 0,
'3000': 0,
'Prime': 0,
latest: {
name: null,
age: 0
},
count: 0
},
gift: [], // list of every gift as {id, name, gifts, tier, time}
cheer: [], // list of every cheer as {id, name, bits, time}
follow: [] // list of every follow as {id, name, time}
};
let fCount = 0; // count of followers
let dead = false; // hard disconnect (usually caused by an error)
const wWS = 5; // websocket retry backoff base (ms = base ^ (1.0 + (0.2 per retry, max 1.0)))
const cURLs = {
twitch: {
ws: {
irc: 'wss://irc-ws.chat.twitch.tv',
eventSub: 'wss://eventsub.wss.twitch.tv/ws'
},
oauth: 'https://id.twitch.tv/oauth2/authorize?client_id=%CLIENT_ID%&redirect_uri=%URL%&response_type=code&scope=%SCOPE%&state=redirto_%ORIGIN%&force_verify=true',
eventSub: {
get: 'https://api.twitch.tv/helix/eventsub/subscriptions',
delete: 'https://api.twitch.tv/helix/eventsub/subscriptions?id=%ID%'
},
users: 'https://api.twitch.tv/helix/users?login=%USER%',
followers: 'https://api.twitch.tv/helix/channels/followers?broadcaster_id=%CHANNEL_ID%',
subscriptions: 'https://api.twitch.tv/helix/subscriptions?broadcaster_id=%CHANNEL_ID%&first=1'
},
rr: {
oauth: 'https://realityripple.com/Tools/Twitch/thisStream/oauth.php',
refresh: 'https://realityripple.com/Tools/Twitch/thisStream/oauth.php?refresh=%REFRESH_TOKEN%'
}
};
const display = function()
{
let lIDX = 0;
let showQueue = [];
async function _displayStatus(m)
{
if (m === null)
m = cfg.idle;
let t = 0;
let h = '';
const fullT = cfg.reveal + cfg.interval;
const rate = Math.floor(cfg.reveal / m.length);
for (let i = 0; i < m.length; i++)
{
t += rate;
let a = fullT + 'ms ease-in ' + t + 'ms infinite antsy';
if (m[i] === ' ')
h += '<span class="text" style="animation: ' + a + '; white-space: pre;">' + m[i] + '</span>';
else
h += '<span class="text" style="animation: ' + a + ';">' + m[i] + '</span>';
}
if (document.getElementById('status').innerHTML !== h)
{
let ani = /style="animation: ([0-9]+)ms ease-in ([0-9]+)ms infinite antsy;"/g;
if (ani.test(document.getElementById('status').innerHTML))
{
document.getElementById('status').classList.add('trans');
await _hideOld(ani);
await _showNew(ani, h);
document.getElementById('status').classList.remove('trans');
document.getElementById('status').innerHTML = h;
}
else
document.getElementById('status').innerHTML = h;
}
}
async function showStatus()
{
if (dead)
{
await _displayStatus('Connection Closed');
return;
}
if (showQueue.length > 0)
{
let v = showQueue.shift();
await _displayStatus(v);
window.setTimeout(display.status, cfg.interval);
return;
}
if (cfg.list === false || cfg.list.length === 0)
{
await _displayStatus(null);
window.setTimeout(display.status, cfg.interval);
return;
}
let v2 = cfg.list[lIDX];
let s = _fillVars(v2);
let t = 0;
while (s === false)
{
lIDX++;
if (lIDX >= cfg.list.length)
lIDX = 0;
v2 = cfg.list[lIDX];
s = _fillVars(v2);
t++;
if (t >= cfg.list.length)
{
lIDX = 0;
await _displayStatus(null);
window.setTimeout(display.status, cfg.interval);
return;
}
}
await _displayStatus(s);
window.setTimeout(display.status, cfg.interval);
lIDX++;
if (lIDX >= cfg.list.length)
lIDX = 0;
}
async function _hideOld(ani)
{
let r = document.getElementById('status').innerHTML.matchAll(ani);
let max = 0;
let x = r.next();
while(!x.done)
{
max = Math.floor(x.value[1] / 10) + Math.floor(x.value[2] / 2);
x = r.next();
}
document.getElementById('status').innerHTML = document.getElementById('status').innerHTML.replaceAll
(
ani,
function(m, p1, p2)
{
return 'style="animation: ' + Math.floor(p1 / 10) + 'ms ease-in ' + Math.floor(p2 / 2) + 'ms 1 forwards hide;"';
}
);
await shared.sleep(max);
}
async function _showNew(ani, h)
{
let r = h.matchAll(ani);
let max = 0;
let x = r.next();
while(!x.done)
{
max = Math.floor(x.value[1] / 10) + Math.floor(x.value[2] / 2);
x = r.next();
}
document.getElementById('status').innerHTML = h.replaceAll
(
ani,
function(m, p1, p2)
{
return 'style="animation: ' + Math.floor(p1 / 10) + 'ms ease-in ' + Math.floor(p2 / 2) + 'ms 1 both show;"';
}
);
await shared.sleep(max);
}
const _gift = function()
{
function getGiftLeaderID()
{
let lb = {};
for (let i = 0; i < eList.gift.length; i++)
{
if (!lb.hasOwnProperty(eList.gift[i].id))
lb[eList.gift[i].id] = 0;
lb[eList.gift[i].id]+= eList.gift[i].gifts;
}
let high = 0;
let highID = null;
let ids = Object.keys(lb);
for (let i = 0; i < ids.length; i++)
{
if (lb[ids[i]] <= high)
continue;
high = lb[ids[i]];
highID = ids[i];
}
return highID;
}
function getGiftLatestID()
{
let age = 0;
let ageID = null;
for (let i = 0; i < eList.gift.length; i++)
{
if (eList.gift[i].time <= age)
continue;
age = eList.gift[i].time;
ageID = eList.gift[i].id;
}
return ageID;
}
function getGiftLatestCount()
{
let age = 0;
let ageGifts = 0;
for (let i = 0; i < eList.gift.length; i++)
{
if (eList.gift[i].time <= age)
continue;
age = eList.gift[i].time;
ageGifts = eList.gift[i].gifts;
}
return ageGifts;
}
function getGifterName(hID)
{
if (hID === null)
return null;
for (let i = eList.gift.length - 1; i >= 0; i--)
{
if (eList.gift[i].id === hID)
return eList.gift[i].name;
}
return 'User#' + hID;
}
function getGifterCount(hID)
{
if (hID === null)
return 0;
let r = 0;
for (let i = eList.gift.length - 1; i >= 0; i--)
{
if (eList.gift[i].id === hID)
r += eList.gift[i].gifts;
}
return r;
}
return {
leader: getGiftLeaderID,
latestID: getGiftLatestID,
latestCount: getGiftLatestCount,
name: getGifterName,
count: getGifterCount
};
}();
const _cheer = function()
{
function getCheerLeaderID()
{
let lb = {};
for (let i = 0; i < eList.cheer.length; i++)
{
if (!lb.hasOwnProperty(eList.cheer[i].id))
lb[eList.cheer[i].id] = 0;
lb[eList.cheer[i].id]+= eList.cheer[i].bits;
}
let high = 0;
let highID = null;
let ids = Object.keys(lb);
for (let i = 0; i < ids.length; i++)
{
if (lb[ids[i]] <= high)
continue;
high = lb[ids[i]];
highID = ids[i];
}
return highID;
}
function getCheerLatestID()
{
let age = 0;
let ageID = null;
for (let i = 0; i < eList.cheer.length; i++)
{
if (eList.cheer[i].time <= age)
continue;
age = eList.cheer[i].time;
ageID = eList.cheer[i].id;
}
return ageID;
}
function getCheerLatestCount()
{
let age = 0;
let ageBits = 0;
for (let i = 0; i < eList.cheer.length; i++)
{
if (eList.cheer[i].time <= age)
continue;
age = eList.cheer[i].time;
ageBits = eList.cheer[i].bits;
}
return ageBits;
}
function getCheererName(hID)
{
if (hID === null)
return null;
for (let i = eList.cheer.length - 1; i >= 0; i--)
{
if (eList.cheer[i].id === hID)
return eList.cheer[i].name;
}
return 'User#' + hID;
}
function getCheererCount(hID)
{
if (hID === null)
return 0;
let r = 0;
for (let i = eList.cheer.length - 1; i >= 0; i--)
{
if (eList.cheer[i].id === hID)
r += eList.cheer[i].bits;
}
return r;
}
return {
leader: getCheerLeaderID,
latestID: getCheerLatestID,
latestCount: getCheerLatestCount,
name: getCheererName,
count: getCheererCount
};
}();
function _getFollowerLatest()
{
let age = 0;
let name = null;
for (let i = 0; i < eList.follow.length; i++)
{
if (eList.follow[i].time <= age)
continue;
age = eList.follow[i].time;
name = eList.follow[i].name;
}
return name;
}
function _regMatch(match, p1)
{
let r = 0;
let l = p1.split('+');
for (let i = 0; i < l.length; i++)
{
switch(l[i])
{
case 'SUB_T1':
r+= eList.sub['1000'];
break;
case 'SUB_T2':
r+= eList.sub['2000'];
break;
case 'SUB_T3':
r+= eList.sub['3000'];
break;
case 'SUB_PRIME':
r+= eList.sub['Prime'];
break;
case 'SUB_USER':
r = eList.sub.latest.name;
break;
case 'SUB_USER_AGE':
r+= eList.sub.latest.age;
break;
case 'SUB_ALL':
r+= eList.sub.count;
break;
case 'GIFT_T1':
let v1 = 0;
for (let j = 0; j < eList.gift.length; j++)
{
if (eList.gift[j].tier !== '1000')
continue;
v1+= eList.gift[j].gifts;
}
r+= v1;
break;
case 'GIFT_T2':
let v2 = 0;
for (let j = 0; j < eList.gift.length; j++)
{
if (eList.gift[j].tier !== '2000')
continue;
v2+= eList.gift[j].gifts;
}
r+= v2;
break;
case 'GIFT_T3':
let v3 = 0;
for (let j = 0; j < eList.gift.length; j++)
{
if (eList.gift[j].tier !== '3000')
continue;
v3+= eList.gift[j].gifts;
}
r+= v3;
break;
case 'GIFT_LEADER':
r = _gift.name(_gift.leader());
break;
case 'GIFT_LEADER_ALL':
r+= _gift.count(_gift.leader());
break;
case 'GIFT_USER':
r = _gift.name(_gift.latestID());
break;
case 'GIFT_USER_COUNT':
r+= _gift.latestCount();
break;
case 'GIFT_USER_ALL':
r+= _gift.count(_gift.latestID());
break;
case 'BITS_ALL':
if (eList.cheer !== false && eList.cheer.length > 0)
{
for (let j = 0; j < eList.cheer.length; j++)
{
r+= eList.cheer[j].bits;
}
}
break;
case 'BITS_LEADER':
r = _cheer.name(_cheer.leader());
break;
case 'BITS_LEADER_ALL':
r+= _cheer.count(_cheer.leader());
break;
case 'BITS_USER':
r = _cheer.name(_cheer.latestID());
break;
case 'BITS_USER_COUNT':
r+= _cheer.latestCount();
break;
case 'BITS_USER_ALL':
r+= _cheer.count(_cheer.latestID());
break;
case 'FOLLOWER':
r = _getFollowerLatest();
break;
case 'FOLLOWER_COUNT':
r+= eList.follow.length;
break;
case 'FOLLOWER_ALL':
r+= fCount;
break;
}
}
if (r === null)
return '%DO_NOT_DISPLAY%';
if (r === 0)
return '%0%';
if (r === 1)
return '%1%';
if (!isNaN(r) && r > 999)
{
r = r.toLocaleString(cfg.numLocale);
}
return r;
}
function _fillVars(s)
{
let r = s;
r = r.replaceAll(new RegExp('%([A-Z0-9_+]+)%', 'g'), _regMatch);
if (r.indexOf('%DO_NOT_DISPLAY%') > -1)
{
if (cfg.hideEmpty)
return false;
r = r.replaceAll('%DO_NOT_DISPLAY%', cfg.emptyName);
}
if (r.indexOf('%0%') > -1)
{
if (cfg.hideEmpty)
return false;
r = r.replaceAll('%0%', '0');
}
if (r.indexOf('%1%') > -1)
{
if (cfg.singular !== false)
{
r = r.replaceAll('%1%', '1');
if (r.slice(-1) === cfg.singular)
r = r.slice(0, -1);
}
}
return r;
}
function addToQueue(a)
{
let toAdd = [];
for (let i = 0; i < a.length; i++)
{
let s = _fillVars(a[i]);
if (toAdd.includes(s))
continue;
if (showQueue.includes(s))
continue;
toAdd.push(s);
}
showQueue.push(...toAdd);
}
return {
status: showStatus,
enqueue: addToQueue
};
}();
const twitch = function()
{
async function getFollowerCount(chID)
{
const url = cURLs.twitch.followers.replaceAll('%CHANNEL_ID%', chID);
const h = {
'Authorization': 'Bearer ' + cfg.login.oauth,
'Client-Id': cfg.login.client
};
const r = await shared.httpRequest(url, h);
if (r === false)
return;
const j = JSON.parse(r);
if (!j.hasOwnProperty('total'))
return;
fCount += j.total;
}
async function getSubscriberCount(chID)
{
const url = cURLs.twitch.subscriptions.replaceAll('%CHANNEL_ID%', chID);
const h = {
'Authorization': 'Bearer ' + cfg.login.oauth,
'Client-Id': cfg.login.client
};
const r = await shared.httpRequest(url, h);
if (r === false)
return;
const j = JSON.parse(r);
if (!j.hasOwnProperty('total'))
return;
eList.sub.count += j.total;
}
const eventSub = function()
{
let _wsURL = cURLs.twitch.ws.eventSub;
let _wsRetry = null;
let _ws = null;
let _oldWS = null;
let _ch = 0;
let _sess = null;
let _lTimeout = 5000;
let _tTimeout = false;
function task(chID = null)
{
if (chID !== null)
_ch = chID;
_ws = new WebSocket(_wsURL);
_ws.onopen = _wsOpen;
_ws.onclose = _wsClose;
_ws.onmessage = _wsMessage;
}
function _wsOpen()
{
_ws.onopen = null;
_tTimeout = setTimeout(_wsTimeout, _lTimeout);
}
function _wsClose()
{
if (_tTimeout !== false)
{
clearTimeout(_tTimeout);
_tTimeout = false;
}
_ws.onopen = null;
_ws.onmessage = null;
_ws.onclose = null;
if (dead)
return;
if (_wsURL.length > cURLs.twitch.ws.eventSub.length && _wsURL.slice(0, cURLs.twitch.ws.eventSub.length) === cURLs.twitch.ws.eventSub)
_wsURL = cURLs.twitch.ws.eventSub;
const wsWait = Math.floor(wWS ** (1 + _wsRetry) * 1000);
if (_wsRetry < 2)
_wsRetry += 0.2;
window.setTimeout(task, wsWait);
}
async function _wsMessage(ev)
{
if (_tTimeout !== false)
{
clearTimeout(_tTimeout);
_tTimeout = false;
}
if (dead)
{
_ws.close();
return;
}
_tTimeout = setTimeout(_wsTimeout, _lTimeout);
const r = JSON.parse(ev.data);
if (!r.hasOwnProperty('metadata'))
return;
if (!r.hasOwnProperty('payload'))
return;
switch (r.metadata.message_type)
{
case 'session_welcome':
if (_oldWS !== null)
{
_oldWS.close();
_oldWS = null;
}
if (!r.payload.hasOwnProperty('session'))
return;
if (r.payload.session.hasOwnProperty('id'))
_sess = r.payload.session.id;
if (r.payload.session.hasOwnProperty('keepalive_timeout_seconds'))
{
_lTimeout = parseInt(r.payload.session.keepalive_timeout_seconds, 10) * 2 * 1000;
if (_tTimeout !== false)
{
clearTimeout(_tTimeout);
_tTimeout = false;
}
_tTimeout = setTimeout(_wsTimeout, _lTimeout);
}
const haveSubs = await _handleSubs();
if (!haveSubs)
{
if (_tTimeout !== false)
{
clearTimeout(_tTimeout);
_tTimeout = false;
}
_ws.onopen = null;
_ws.onmessage = null;
_ws.onclose = null;
_ws.close();
_ws = null;
return;
}
break;
case 'session_keepalive':
_wsRetry = 0;
break;
case 'notification':
if (!r.metadata.hasOwnProperty('subscription_type'))
return;
if (!r.payload.hasOwnProperty('event'))
return;
if (!r.payload.event.hasOwnProperty('user_id'))
return;
if (!r.payload.event.hasOwnProperty('user_name'))
return;
switch (r.metadata.subscription_type)
{
case 'channel.follow':
eList.follow.push({id: r.payload.event.user_id, name: r.payload.event.user_name, time: new Date().getTime()});
fCount++;
if (cfg.event.follow === false || cfg.event.follow.length === 0)
return;
display.enqueue(cfg.event.follow);
break;
}
break;
case 'session_reconnect':
if (!r.payload.hasOwnProperty('session'))
return;
if (_tTimeout !== false)
{
clearTimeout(_tTimeout);
_tTimeout = false;
}
if (r.payload.session.hasOwnProperty('id'))
_sess = r.payload.session.id;
if (r.payload.session.hasOwnProperty('keepalive_timeout_seconds') && r.payload.session.keepalive_timeout_seconds !== null)
_lTimeout = parseInt(r.payload.session.keepalive_timeout_seconds, 10) * 2 * 1000;
if (r.payload.session.hasOwnProperty('reconnect_url'))
_wsURL = r.payload.session.reconnect_url;
_oldWS = _ws;
_oldWS.onclose = null;
_oldWS.onmessage = null;
_ws = null;
const wsWait = Math.floor(wWS ** (1 + _wsRetry) * 1000);
if (_wsRetry < 2)
_wsRetry += 0.2;
window.setTimeout(task, wsWait);
break;
case 'revocation':
if (!r.metadata.hasOwnProperty('subscription_type'))
return;
const subType = r.metadata.subscription_type;
let subVer = '1';
if (r.metadata.hasOwnProperty('subscription_version'))
subVer = r.metadata.subscription_version;
await _subscribe(subType, subVer);
break;
}
}
function _wsTimeout()
{
_lTimeout = 5000;
if (dead === true)
return;
if (_tTimeout !== false)
{
clearTimeout(_tTimeout);
_tTimeout = false;
}
if (_ws !== null)
{
_ws.onopen = null;
_ws.onmessage = null;
_ws.onclose = null;
_ws.close();
}
if (_wsURL.length > cURLs.twitch.ws.eventSub.length && _wsURL.slice(0, cURLs.twitch.ws.eventSub.length) === cURLs.twitch.ws.eventSub)
_wsURL = cURLs.twitch.ws.eventSub;
task();
}
async function _handleSubs()
{
const aSubs = [
{id: 'channel.follow', v: '2'}
];
const aActive = await _unsubscribe();
let didSomething = aActive.length > 0;
for (let i = 0, l = aSubs.length; i < l; i++)
{
if (await _handleSub(aSubs[i], aActive) !== false)
didSomething = true;
}
return didSomething;
}
async function _handleSub(sub, aActive)
{
if (aActive.includes(sub.id))
return false;
if (!sub.hasOwnProperty('v'))
return _subscribe(sub.id);
return _subscribe(sub.id, sub.v);
}
async function _unsubscribe()
{
const h = {
'Authorization': 'Bearer ' + cfg.login.oauth,
'Client-Id': cfg.login.client
};
const r = await shared.httpRequest(cURLs.twitch.eventSub.get, h);
const j = JSON.parse(r);
if (!j.hasOwnProperty('data'))
return [];
if (!j.hasOwnProperty('total'))
return [];
if (j.total < 1)
return [];
let rList = [];
for (let i = 0; i < j.total; i++)
{
if (!j.data[i].hasOwnProperty('id'))
continue;
if (!j.data[i].hasOwnProperty('status'))
continue;
switch (j.data[i].status)
{
case 'enabled':
if (!j.data[i].hasOwnProperty('type'))
continue;
if (!j.data[i].hasOwnProperty('transport'))
continue;
if (!j.data[i].transport.hasOwnProperty('method'))
continue;
if (j.data[i].transport.method !== 'websocket')
continue;
if (!j.data[i].transport.hasOwnProperty('session_id'))
continue;
if (j.data[i].transport.session_id !== _sess)
continue;
rList.push(j.data[i].type);
break;
case 'authorization_revoked':
case 'moderator_removed':
case 'user_removed':
case 'version_removed':
case 'websocket_disconnected':
case 'websocket_failed_ping_pong':
case 'websocket_received_inbound_traffic':
case 'websocket_connection_unused':
case 'websocket_internal_error':
case 'websocket_network_timeout':
case 'websocket_network_error':
shared.httpSend('DELETE', cURLs.twitch.eventSub.delete.replaceAll('%ID%', j.data[i].id), null, h);
break;
}
}
return rList;
}
async function _subscribe(type, version = '1')
{
let d = {
'type': type,
'version': version,
'condition': {'broadcaster_user_id': _ch},
'transport': {'method': 'websocket', 'session_id': _sess}
};
switch (type)
{
case 'channel.follow':
d.condition.moderator_user_id = _ch;
break;
}
const h = {
'Authorization': 'Bearer ' + cfg.login.oauth,
'Client-Id': cfg.login.client,
'Content-Type': 'application/json'
};
const r = await shared.httpSend('POST', cURLs.twitch.eventSub.get, JSON.stringify(d), h);
return r !== false;
}
return task;
}();
const irc = function()
{
let _wsRetry = 0;
let _firstRS = true;
let _ws = null;
let _tTimeout = false;
function task()
{
_firstRS = true;
_ws = new WebSocket(cURLs.twitch.ws.irc);
_ws.onopen = _wsOpen;
_ws.onclose = _wsClose;
_ws.onmessage = _wsMessage;
}
function _wsOpen()
{
if (_ws === null)
return;
_ws.onopen = null;
_tTimeout = setTimeout(_wsTimeout, 5000);
_ws.send('CAP REQ :twitch.tv/commands twitch.tv/tags');
_ws.send('PASS 1234');
_ws.send('NICK justinfan' + shared.rnd(0xFFFFFFFF));
_ws.send('JOIN #' + cfg.channel);
}
function _wsClose()
{
if (_tTimeout !== false)
{
clearTimeout(_tTimeout);
_tTimeout = false;
}
if (_ws === null)
return;
_ws.onopen = null;
_ws.onmessage = null;
_ws.onclose = null;
_ws = null;
if (dead)
return;
const wsWait = Math.floor(wWS ** (1 + _wsRetry) * 1000);
if (_wsRetry < 2)
_wsRetry += 0.2;
window.setTimeout(task, wsWait);
}
async function _wsMessage(ev)
{
if (_ws === null)
return;
if (dead)
{
_ws.onopen = null;
_ws.onmessage = null;
_ws.onclose = null;
_ws.close();
_ws = null;
return;
}
_wsRetry = 0;
const data = ev.data.split('\r\n');
for (let i = 0, l = data.length; i < l; i++)
{
if (data[i].length === 0)
continue;
let cmd = _parseMsg(data[i]);
if (cmd === false)
{
console.log('Unparsed IRC Command: ', data[i]);
continue;
}
switch(cmd.command)
{
case 'PING':
_ws.send('PONG ' + cmd.params[0]);
await twitch.updateOAuth();
break;
case 'PRIVMSG':
_parseCheer(cmd);
break;
case 'NOTICE':
if (cmd.params.length > 1 && cmd.params[1] === 'Login authentication failed')
{
dead = true;
shared.blargIAmDead(1);
}
break;
case 'ROOMSTATE':
if (!cmd.hasOwnProperty('tags'))
continue;
if (!cmd.tags.hasOwnProperty('room-id'))
continue;
let chID = cmd.tags['room-id'];
if (_firstRS)
{
if (_tTimeout !== false)
{
clearTimeout(_tTimeout);
_tTimeout = false;
}
_firstRS = false;
await twitch.followCount(chID);
await twitch.subCount(chID);
twitch.eventSub(chID);
}
break;
case 'USERNOTICE':
if (document.visibilityState === 'hidden')
continue;
if (!cmd.hasOwnProperty('tags'))
continue;
if (!cmd.tags.hasOwnProperty('msg-id'))
continue;
switch (cmd.tags['msg-id'])
{
case 'sub':
case 'resub':
case 'subgift':
case 'submysterygift':
_parseSub(cmd);
break;
}
break;
}
}
}
function _wsTimeout()
{
if (_ws === null)
return;
if (dead === true)
return;
dead = true;
if (_tTimeout !== false)
{
clearTimeout(_tTimeout);
_tTimeout = false;
}
_ws.close();
_ws = null;
shared.blargIAmDead(3);
}
return task;
}();
function _parseUser(cmd, latinOnly = false)
{
let data = cmd.prefix;
let ret = {};
if (cmd.hasOwnProperty('tags') && cmd.tags.hasOwnProperty('display-name'))
ret['display-name'] = cmd.tags['display-name'];
const gReg = new RegExp(/[^A-Za-z0-9_]/, 'g');
if (data.includes('!'))
{
ret.nick = data.slice(0, data.indexOf('!'));
data = data.slice(data.indexOf('!') + 1);
if (!ret.hasOwnProperty('display-name') || (latinOnly && ret['display-name'] !== ret['display-name'].replaceAll(gReg, '')))
ret['display-name'] = ret.nick;
}
if (data.includes('@'))
{
ret.host = data.slice(0, data.indexOf('@'));
data = data.slice(data.indexOf('@') + 1);
if (!ret.hasOwnProperty('display-name') || (latinOnly && ret['display-name'] !== ret['display-name'].replaceAll(gReg, '')))
ret['display-name'] = ret.host;
}
ret.user = data;
return ret;
}
function _parseMsg(line)
{
let cmd = {};
if (line.slice(0, 1) === '@')
{
line = line.slice(1);
if (!line.includes(' '))
return false;
cmd.tags = {};
const t = line.slice(0, line.indexOf(' '));
line = line.slice(line.indexOf(' ') + 1);
const a = t.split(';');
for (let i = 0, l = a.length; i < l; i++)
{
const k = a[i].slice(0, a[i].indexOf('='));
let v = a[i].slice(a[i].indexOf('=') + 1);
v = v.replace(/\\s/g, ' ');
cmd.tags[k] = v;
}
}
if (line.slice(0, 1) === ':')
{
line = line.slice(1);
if (!line.includes(' '))
return false;
cmd.prefix = line.slice(0, line.indexOf(' '));
line = line.slice(line.indexOf(' ') + 1);
}
if (!line.includes(' '))
{
cmd.command = line;
return cmd;
}
cmd.command = line.slice(0, line.indexOf(' '));
line = line.slice(line.indexOf(' ') + 1);
cmd.params = [];
if (!line.includes(' '))
{
cmd.params.push(line);
return cmd;
}
while (line.includes(' '))
{
if (line.slice(0, 1) === ':')
{
cmd.params.push(line.slice(1));
return cmd;
}
cmd.params.push(line.slice(0, line.indexOf(' ')));
line = line.slice(line.indexOf(' ') + 1);
}
if (line.slice(0, 1) === ':')
line = line.slice(1);
cmd.params.push(line);
return cmd;
}
const _parseSub = function()
{
let _bList = {
bombs: [],
bulk: [],
check: []
};
let _tGC = null;
function task(cmd)
{
if (!cmd.tags.hasOwnProperty('msg-id'))
return;
if (!cmd.tags.hasOwnProperty('msg-param-sub-plan'))
return;
let mTime = new Date().getTime();
if (cmd.tags.hasOwnProperty('tmi-sent-ts'))
mTime = parseInt(cmd.tags['tmi-sent-ts'], 10);
const subPlan = cmd.tags['msg-param-sub-plan'];
const mID = cmd.tags['msg-id'];
let mpoi = false;
if (cmd.tags.hasOwnProperty('msg-param-origin-id'))
mpoi = cmd.tags['msg-param-origin-id'];
if (mpoi !== false)
{
if (_bList.bombs.includes(mpoi))
return;
if (mID === 'subgift')
{
if (_bList.bulk.includes(mpoi))
return;
if (_bList.check.includes(mpoi))
{
_bList.bulk.push(mpoi);
_bList.check.splice(_bList.check.indexOf(mpoi), 1);
_resetGC();
return;
}
}
}
let mpcm = 1;
if (cmd.tags.hasOwnProperty('msg-param-cumulative-months'))
mpcm = parseInt(cmd.tags['msg-param-cumulative-months'], 10);
let mpmgc = 1;
if (cmd.tags.hasOwnProperty('msg-param-mass-gift-count'))
mpmgc = parseInt(cmd.tags['msg-param-mass-gift-count'], 10);
let dn = cfg.emptyName;
let ln = cfg.emptyName;
if (cmd.tags.hasOwnProperty('login'))
ln = cmd.tags.login;
if (cmd.tags.hasOwnProperty('display-name'))
dn = cmd.tags['display-name'];
if (ln === cfg.emptyName || dn === cfg.emptyName)
{
const u = _parseUser(cmd, true);
if (ln === cfg.emptyName && u.hasOwnProperty('host'))
ln = u.host;
if (dn === cfg.emptyName && u.hasOwnProperty('display-name'))
dn = u['display-name'];
}
let uid = '0';
if (cmd.tags.hasOwnProperty('user-id'))
uid = cmd.tags['user-id'];
let mprdn = cfg.emptyName;
if (cmd.tags.hasOwnProperty('msg-param-recipient-display-name'))
mprdn = cmd.tags['msg-param-recipient-display-name'];
if (mpoi !== false)
{
if (_bList.bombs.includes(mpoi))
return;
if (mID === 'subgift')
{
if (_bList.bulk.includes(mpoi))
return;
if (_bList.check.includes(mpoi))
{
_bList.bulk.push(mpoi);
_bList.check.splice(_bList.check.indexOf(mpoi), 1);
_resetGC();
return;
}
if (!cmd.tags.hasOwnProperty('msg-param-sender-count') || cmd.tags['msg-param-sender-count'] === '0')
{
_bList.check.push(mpoi);
window.setTimeout(_show, 2000, mID, subPlan, mpoi, mpcm, mpmgc, uid, dn, mTime);
_resetGC();
return;
}
}
}
_show(mID, subPlan, mpoi, mpcm, mpmgc, uid, dn, mTime);
}
function _show(id, subPlan, mpoi, mpcm, mpmgc, uid, dn, mTime)
{
if (mpoi !== false)
{
if (_bList.bombs.includes(mpoi))
return;
if (id === 'subgift' && _bList.bulk.includes(mpoi))
{
_resetGC();
return;
}
}
switch (id)
{
case 'sub':
eList.sub[subPlan]++;
eList.sub.count++;
eList.sub.latest.name = dn;
eList.sub.latest.age = 1;
if (cfg.event.sub === false)
return;
if (!cfg.event.sub.hasOwnProperty(subPlan))
return;
if (cfg.event.sub[subPlan] === false)
return;
display.enqueue(cfg.event.sub[subPlan]);
break;
case 'resub':
eList.sub[subPlan]++;
eList.sub.count++;
eList.sub.latest.name = dn;
eList.sub.latest.age = mpcm;
if (cfg.event.resub === false)
return;
if (!cfg.event.resub.hasOwnProperty(subPlan))
return;
if (cfg.event.resub[subPlan] === false)
return;
display.enqueue(cfg.event.resub[subPlan]);
break;
case 'subgift':
eList.gift.push({id: uid, name: dn, gifts: 1, tier: subPlan, time: mTime});
eList.sub.count++;
if (cfg.event.gift === false)
return;
if (!cfg.event.gift.hasOwnProperty(subPlan))
return;
if (cfg.event.gift[subPlan] === false)
return;
display.enqueue(cfg.event.gift[subPlan]);
break;
case 'submysterygift':
if (mpoi !== false)
{
_bList.bombs.push(mpoi);
if (_bList.bulk.includes(mpoi))
_bList.bulk.splice(_bList.bulk.indexOf(mpoi), 1);
if (_bList.check.includes(mpoi))
_bList.check.splice(_bList.check.indexOf(mpoi), 1);
_resetGC();
}
eList.gift.push({id: uid, name: dn, gifts: mpmgc, tier: subPlan, time: mTime});
eList.sub.count += mpmgc;
if (cfg.event.giftbomb === false)
return;
if (!cfg.event.giftbomb.hasOwnProperty(subPlan))
return;
if (cfg.event.giftbomb[subPlan] === false)
return;
display.enqueue(cfg.event.giftbomb[subPlan]);
break;
}
}
function _resetGC()
{
if (_tGC !== null)
{
window.clearTimeout(_tGC);
_tGC = null;
}
_tGC = window.setTimeout(_tmrGC, 10000);
}
function _tmrGC()
{
if (_tGC === null)
return;
window.clearTimeout(_tGC);
_tGC = null;
_bList.check = [];
_bList.bulk = [];
}
return task;
}();
function _parseCheer(cmd)
{
if (!cmd.hasOwnProperty('tags'))
return false;
if (!cmd.tags.hasOwnProperty('bits'))
return false;
let mTime = new Date().getTime();
if (cmd.tags.hasOwnProperty('tmi-sent-ts'))
mTime = parseInt(cmd.tags['tmi-sent-ts'], 10);
const bits = parseInt(cmd.tags.bits, 10);
if (bits < 1)
return false;
let uID = '0';
if (cmd.tags.hasOwnProperty('user-id'))
uID = cmd.tags['user-id'];
let u = _parseUser(cmd);
eList.cheer.push({id: uID, name: u['display-name'], bits: bits, time: mTime});
if (cfg.event.cheer === false || cfg.event.cheer.length === 0)
return;
display.enqueue(cfg.event.cheer);
}
async function _getOAuthToken(t)
{
const url = cURLs.rr.refresh.replaceAll('%REFRESH_TOKEN%', t);
const r = await shared.httpRequest(url, {}, true, false);
if (r === false)
return false;
if (r === null)
{
shared.blargIAmDead(1);
window.localStorage.removeItem(login.path() + '.oauth');
window.localStorage.removeItem(login.path() + '.refresh');
window.localStorage.removeItem(login.path() + '.refreshed');
return false;
}
const j = JSON.parse(r);
if (!j.hasOwnProperty('access_token'))
return false;
if (!j.hasOwnProperty('refresh_token'))
return false;
return j;
}
async function updateOAuth()
{
const lsOAuth = window.localStorage.getItem(login.path() + '.oauth');
const lsRefresh = window.localStorage.getItem(login.path() + '.refresh');
const lsRefreshed = window.localStorage.getItem(login.path() + '.refreshed');
if (lsOAuth !== null)
cfg.login.oauth = lsOAuth;
if (lsRefresh !== null)
cfg.login.oauth_refresh = lsRefresh;
if (lsRefreshed !== null)
cfg.login.oauth_refreshed = lsRefreshed;
if (cfg.login.oauth_refreshed > 0)
{
const tokAge = Math.floor(new Date().getTime() / 1000) - cfg.login.oauth_refreshed;
if (tokAge < 60 * 60)
return;
}
const ret = await _getOAuthToken(cfg.login.oauth_refresh);
if (ret === false)
return;
cfg.login.oauth = ret.access_token;
cfg.login.oauth_refresh = ret.refresh_token;
cfg.login.oauth_refreshed = Math.floor(new Date().getTime() / 1000);
window.localStorage.setItem(login.path() + '.oauth', cfg.login.oauth);
window.localStorage.setItem(login.path() + '.refresh', cfg.login.oauth_refresh);
window.localStorage.setItem(login.path() + '.refreshed', cfg.login.oauth_refreshed);
}
return {
followCount: getFollowerCount,
subCount: getSubscriberCount,
eventSub: eventSub,
irc: irc,
updateOAuth: updateOAuth
};
}();
const shared = function()
{
function rnd(m)
{
let r = new Uint32Array(1);
window.crypto.getRandomValues(r);
const f = r[0] / 4294967295;
if (m === undefined)
return f;
if (m < 1)
return f * m;
return Math.floor(f * m);
}
function sleep(ms)
{
return new Promise(resolve => setTimeout(resolve, ms));
}
function _httpRequest_RSC(x)
{
if (x.readyState < 2)
return null;
if (Math.floor(x.status / 100) !== 2)
{
x.onreadystatechange = null;
return false;
}
if (x.readyState !== 4)
return null;
if (x.responseText === '')
return null;
x.onreadystatechange = null;
if (x.responseText === null)
return false;
return x.responseText;
}
function httpRequest(url, hdrs = {}, nullOn401 = false, nocache = true)
{
const p = new Promise(
function(resolve)
{
const x = new XMLHttpRequest();
if (nocache)
{
if (url.includes('?'))
url+= '&';
else
url+= '?';
url+= 'nocache=' + rnd(0xFFFFFFFF);
}
x.open('GET', url);
for (const hK in hdrs)
{
if (!hdrs.hasOwnProperty(hK))
continue;
x.setRequestHeader(hK, hdrs[hK]);
}
x.onreadystatechange = function()
{
const r = _httpRequest_RSC(x);
if (r === null)
return;
if (r === false && nullOn401 && x.status === 401)
{
resolve(null);
return;
}
resolve(r);
};
x.send();
}
);
return p;
}
function httpSend(type, url, body = null, hdrs = {})
{
const p = new Promise(
function(resolve)
{
const x = new XMLHttpRequest();
x.open(type, url);
for (const hK in hdrs)
{
if (!hdrs.hasOwnProperty(hK))
continue;
x.setRequestHeader(hK, hdrs[hK]);
}
x.onreadystatechange = function()
{
const r = _httpRequest_RSC(x);
if (r === null)
{
if (type === 'DELETE')
resolve(true);
return;
}
resolve(r);
};
if (body === null)
x.send();
else
x.send(body);
}
);
return p;
}
function findInMaybeRange(r, v)
{
const t = typeof r;
switch (t)
{
case 'undefined':
return false;
case 'boolean':
return r === true;
case 'number':
if (r > 0 && v >= r)
return true;
return false;
case 'object':
if (r === null)
return false;
for (const k in r)
{
if (!r.hasOwnProperty(k))
continue;
let lower = 0;
let upper = 0;
if (k.slice(-1) === '+')
{
lower = parseInt(k.slice(0, -1), 10);
upper = Number.MAX_SAFE_INTEGER;
}
else if (k.indexOf('-') !== -1)
{
lower = parseInt(k.slice(0, k.indexOf('-')), 10);
upper = parseInt(k.slice(k.indexOf('-') + 1), 10);
}
else
{
lower = parseInt(k, 10);
upper = parseInt(k, 10);
}
if (v >= lower && v <= upper)
return r[k];
}
}
return false;
}
function blargIAmDead(e)
{
let showButton = false;
switch (e)
{
case 1:
if (login.inUse)
{
document.body.innerHTML = '<div style="position: absolute; top: 0; left: 0; bottom: 0; right: 0; background-color: rgba(255, 0, 0, 0.75); color: #FFFF00; text-shadow: 2px 2px 4px #000000; font-size: 300%; font-weight: bold; font-family: sans-serif; text-align: center; padding-top: 3em;">thisStream Error:<br><br>Unable to Connect to Twitch<br><br>Please Log In Again</div>';
showButton = true;
login.showOut(true);
}
else
document.body.innerHTML = '<div style="position: absolute; top: 0; left: 0; bottom: 0; right: 0; background-color: rgba(255, 0, 0, 0.75); color: #FFFF00; text-shadow: 2px 2px 4px #000000; font-size: 300%; font-weight: bold; font-family: sans-serif; text-align: center; padding-top: 3em;">thisStream Error:<br><br>Unable to Connect to Twitch<br><br>Please Update Your OAuth Refresh Token</div>';
break;
case 2:
document.body.innerHTML = '<div style="position: absolute; top: 0; left: 0; bottom: 0; right: 0; background-color: rgba(255, 0, 0, 0.75); color: #FFFF00; text-shadow: 2px 2px 4px #000000; font-size: 300%; font-weight: bold; font-family: sans-serif; text-align: center; padding-top: 3em;">thisStream Error:<br><br>Corrupted Configuration<br><br>Please Check Your Browser\'s Error Console</div>';
break;
case 3:
document.body.innerHTML = '<div style="position: absolute; top: 0; left: 0; bottom: 0; right: 0; background-color: rgba(255, 0, 0, 0.75); color: #FFFF00; text-shadow: 2px 2px 4px #000000; font-size: 300%; font-weight: bold; font-family: sans-serif; text-align: center; padding-top: 3em;">thisStream Error:<br><br>The Connection to the IRC Channel was Incomplete<br><br>Please Check your Channel</div>';
break;
}
window.setTimeout(function(){document.body.innerHTML = ''; if (showButton) login.showIn();}, 15000);
}
return {
rnd: rnd,
sleep: sleep,
httpRequest: httpRequest,
httpSend: httpSend,
findInMaybeRange: findInMaybeRange,
blargIAmDead: blargIAmDead
};
}();
const login = function()
{
let _tL = false;
const _visTime = 5000;
function _activeScope()
{
let r = [];
r.push('moderator:read:followers');
r.push('channel:read:subscriptions');
return r;
}
function _uScope()
{
return encodeURIComponent(_activeScope().join(' '));
}
function lsPath()
{
return 'twitch.' + _activeScope().join('+');
}
function shouldUseLogin()
{
login.inUse = false;
if (cfg.hasOwnProperty('channel') && cfg.channel !== false && cfg.channel !== 'CHANNEL_NAME' && cfg.hasOwnProperty('login') && ((cfg.login.hasOwnProperty('oauth') && cfg.login.oauth !== false && cfg.login.oauth !== 'OAUTH_ID') || (cfg.login.hasOwnProperty('oauth_refresh') && cfg.login.oauth_refresh !== false && cfg.login.oauth_refresh !== 'OAUTH_REFRESH')))
return false;
login.inUse = true;
let lsChannel = window.localStorage.getItem(login.path() + '.channel');
let lsClient = window.localStorage.getItem(login.path() + '.client');
let lsRefresh = window.localStorage.getItem(login.path() + '.refresh');
if (lsChannel === null || lsRefresh === null)
{
const h = _getHashParams();
if (!h.hasOwnProperty('channel') || !h.hasOwnProperty('client') || !h.hasOwnProperty('oauth_refresh'))
{
login.showIn();
return true;
}
lsChannel = h.channel;
lsClient = h.client;
lsRefresh = h.oauth_refresh;
window.localStorage.setItem(login.path() + '.channel', lsChannel);
window.localStorage.setItem(login.path() + '.refresh', lsRefresh);
window.localStorage.removeItem(login.path() + '.refreshed');
window.localStorage.setItem(login.path() + '.client', lsClient);
}
cfg.channel = lsChannel;
cfg.login.oauth_refresh = lsRefresh;
cfg.login.client = lsClient;
document.title = cfg.channel + ' thisStream';
showLogoutButton();
return false;
}
function _getHashParams()
{
const d = function(s) {
const a = /\+/g;
return decodeURIComponent(s.replace(a, ' '));
};
let hashParams = {};
const r = /([^&;=]+)=?([^&;]*)/g;
const q = window.location.hash.substring(1);
let e;
while ((e = r.exec(q)) !== null)
{
hashParams[d(e[1])] = d(e[2]);
}
return hashParams;
}
function doLogin()
{
window.localStorage.removeItem(login.path() + '.channel');
window.localStorage.removeItem(login.path() + '.oauth');
window.localStorage.removeItem(login.path() + '.refresh');
window.localStorage.removeItem(login.path() + '.refreshed');
window.localStorage.removeItem(login.path() + '.client');
const o = encodeURIComponent(btoa(window.location));
const c = encodeURIComponent('7qsxhop32gcznr2u8ulnlayujqb4nq');
const r = encodeURIComponent(cURLs.rr.oauth);
const u = cURLs.twitch.oauth.replaceAll('%CLIENT_ID%', c).replaceAll('%URL%', r).replaceAll('%SCOPE%', _uScope()).replaceAll('%ORIGIN%', o);
window.location = u;
}
function showLoginButton()
{
document.title = 'Log In to Access thisStream';
if (document.getElementById('cmdLogout'))
document.body.removeChild(document.getElementById('cmdLogout'));
window.localStorage.removeItem(login.path() + '.channel');
window.localStorage.removeItem(login.path() + '.oauth');
window.localStorage.removeItem(login.path() + '.refresh');
window.localStorage.removeItem(login.path() + '.refreshed');
window.localStorage.removeItem(login.path() + '.client');
let cmdLogin = document.createElement('button');
cmdLogin.setAttribute('id', 'cmdLogin');
cmdLogin.setAttribute('type', 'button');
cmdLogin.setAttribute('onclick', 'login.begin();');
let sStyle = 'z-index: 1000;';
sStyle += ' position: absolute;';
sStyle += ' top: 45%;';
sStyle += ' left: calc(50% - 6.5em);';
sStyle += ' width: 13em;';
sStyle += ' font-size: 3vw;';
sStyle += ' padding: 0.5em;';
cmdLogin.setAttribute('style', sStyle);
cmdLogin.innerHTML = 'Authenticate thisStream';
document.body.appendChild(cmdLogin);
}
function showLogoutButton(v = false)
{
if (document.getElementById('cmdLogout'))
document.body.removeChild(document.getElementById('cmdLogout'));
let cmdLogout = document.createElement('button');
if (_tL !== false)
{
window.clearTimeout(_tL);
_tL = false;
}
cmdLogout.setAttribute('id', 'cmdLogout');
cmdLogout.setAttribute('type', 'button');
let sStyle = 'z-index: 1000;';
sStyle += ' transition: opacity 0.5s;';
sStyle += ' position: absolute;';
sStyle += ' top: 1em;';
sStyle += ' right: 1em;';
sStyle += ' width: 5em;';
sStyle += ' font-size: 1vw;';
sStyle += ' padding: 0.5em;';
document.addEventListener('mouseover', _fadeInLogout);
if (v)
{
cmdLogout.setAttribute('onclick', 'login.begin();');
cmdLogout.innerHTML = 'Re-Auth';
sStyle += ' opacity: 1;';
_tL = window.setTimeout(_fadeOutLogout, _visTime);
}
else
{
cmdLogout.setAttribute('onclick', 'login.showIn();');
cmdLogout.innerHTML = 'Log Out';
sStyle += ' opacity: 0;';
}
cmdLogout.setAttribute('style', sStyle);
document.body.appendChild(cmdLogout);
}
function _fadeInLogout()
{
if (document.getElementById('cmdLogout'))
document.getElementById('cmdLogout').style.opacity = '1';
if (_tL !== false)
{
window.clearTimeout(_tL);
_tL = false;
}
_tL = window.setTimeout(_fadeOutLogout, _visTime);
}
function _fadeOutLogout()
{
if (_tL !== false)
{
window.clearTimeout(_tL);
_tL = false;
}
if (document.getElementById('cmdLogout'))
document.getElementById('cmdLogout').style.opacity = '0';
}
return {
inUse: false,
use: shouldUseLogin,
begin: doLogin,
showIn: showLoginButton,
showOut: showLogoutButton,
path: lsPath
};
}();
async function startup()
{
if (typeof cfg === 'undefined')
{
shared.blargIAmDead(2);
return;
}
if (login.use() === true)
return;
if (cfg.login.hasOwnProperty('oauth_refresh') && cfg.login.oauth_refresh !== false && cfg.login.oauth_refresh !== 'OAUTH_REFRESH')
await twitch.updateOAuth();
window.setTimeout(display.status, 50);
twitch.irc();
}
window.addEventListener('load', startup);
</script>
<style>
body
{
margin: 0;
display: flex;
}
#status
{
padding-left: 16px;
padding-right: 16px;
overflow: hidden;
white-space: nowrap;
display: inline-block;
}
#status.trans
{
-webkit-mask-image: linear-gradient(to bottom, transparent 0, black 6px, black calc(100% - 12px), transparent calc(100% - 6px), transparent 100%);
}
span
{
display: inline-block;
box-sizing: border-box;
}
@keyframes hide
{
0%
{
transform: translateY(0);
}
100%
{
transform: translateY(120%);
}
}
@keyframes show
{
0%
{
transform: translateY(-120%);
}
100%
{
transform: translateY(0%);
}
}
@keyframes idle
{
0%, 100%
{
transform: translateY(0%);
}
}
@keyframes antsy
{
0%, 80%, 90%, 100%
{
transform: translateX(0%);
}
85%
{
transform: translateX(-5%);
}
95%
{
transform: translateX(5%);
}
}
button
{
background-color: #7D5BBE;
transition: background 0.12s ease-in, color 0.12s ease-in;
white-space: nowrap;
cursor: pointer;
color: #FFFFFF;
border-radius: 4px;
border: none;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
button:focus, button:hover
{
background-color: #772CE8;
}
button:focus
{
outline: none;
}
button:active
{
background-color: #5C16C5;
}
</style>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div id="status"></div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment