Last active
April 17, 2023 03:18
-
-
Save RealityRipple/f50bd19b1831af0ed8ae9e908dba3e40 to your computer and use it in GitHub Desktop.
thisStream Twitch Leaderboard Display
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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