Archived
🎉 First commit 1.0
This commit is contained in:
19 files changed
+2833
No files matched your search
@@ -0,0 +1,4 @@
|
|||||||
|
ENV=development
|
||||||
|
# bcberiBot -> alexbcberio-dev
|
||||||
|
TWITCH_CLIENT_ID="s5t9lw4oefuli34l0a871f4dkihz9a"
|
||||||
|
TWITCH_CLIENT_SECRET="3ez6s8o2taem1rnz7fjsrqlih6ru5j"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
- [x] User timeout: display a card with user profile picture of the person being timed out + author (https://betterttv.com/emotes/5f6b8deb088f5c0b0f46c54c)
|
||||||
|
- [x] Highlight message: same as usual but its displayed on the screen.
|
||||||
|
- [x] Hidrate: beber agüita.
|
||||||
|
- [ ] VIP role for a certain amount of time. (implement reedem)
|
||||||
|
- [ ] UWU image
|
||||||
|
- [ ] OWO image
|
||||||
|
- [ ] Mutear al streamer (mutea el micro desde el OBS)
|
||||||
|
- [ ] Sudo: ..., lot of points. Forces to the streamer to do something, special animation. It appears a terminal in the screen starting a ssh session using a certificate (simulate the welcome screen), once logged in it simulates the user writing each character with a small delay between them (random delay), then the user presses enter and its prompt for the "sudo password" enters it and the terminal keeps open for a certain amount of time (random), finally "logout" is written and the session is ended. All the times the user has to write play a keypress sound effecft (also multiple ones will be recorded and used depending on the amount of characters in the specific word being written)
|
||||||
+160
@@ -0,0 +1,160 @@
|
|||||||
|
*::before,
|
||||||
|
*,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: "Open Sans", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background-color: #333;
|
||||||
|
color: #ddd;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert p {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert img {
|
||||||
|
display: block;
|
||||||
|
max-height: 200px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Karaoke time
|
||||||
|
*/
|
||||||
|
|
||||||
|
.light {
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
height: 140vh;
|
||||||
|
width: 40%;
|
||||||
|
margin-left: 30%;
|
||||||
|
margin-top: -1rem;
|
||||||
|
clip-path: polygon(
|
||||||
|
45% 0,
|
||||||
|
55% 0,
|
||||||
|
100% 100%,
|
||||||
|
0% 100%
|
||||||
|
);
|
||||||
|
|
||||||
|
transform-origin: top center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-left {
|
||||||
|
animation:
|
||||||
|
rotateLights 1.75s infinite ease-in-out alternate,
|
||||||
|
lightsColor .75s infinite linear alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-right {
|
||||||
|
animation:
|
||||||
|
rotateLights 1.75s infinite ease-in-out alternate-reverse,
|
||||||
|
lightsColor .75s infinite linear alternate-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotateLights {
|
||||||
|
from {
|
||||||
|
transform: rotateZ(-30deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotateZ(30deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes lightsColor {
|
||||||
|
from {
|
||||||
|
background-color: var(--light-color-left);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
background-color: var(--light-color-right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Russian roulette
|
||||||
|
*/
|
||||||
|
|
||||||
|
.shoot {
|
||||||
|
transform-origin: center;
|
||||||
|
animation: shoot .3s ease-in-out -.05s alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shoot {
|
||||||
|
from {
|
||||||
|
transform: rotateZ(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: rotateZ(4deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotateZ(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Card
|
||||||
|
*/
|
||||||
|
.card {
|
||||||
|
position: absolute;
|
||||||
|
left: calc(50% - 25rem / 2);
|
||||||
|
top: .5rem;
|
||||||
|
width: 25rem;
|
||||||
|
height: 6.25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--card-border-color);
|
||||||
|
border-radius: .5rem;
|
||||||
|
background-color: var(--card-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.open {
|
||||||
|
animation: cardAnimation .75s cubic-bezier(0.18, 0.89, 0.32, 1.28) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.close {
|
||||||
|
animation: cardAnimation .75s cubic-bezier(0.18, 0.89, 0.32, 1.28) reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cardAnimation {
|
||||||
|
from {
|
||||||
|
top: -6.25rem;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
top: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-light {
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-image {
|
||||||
|
height: 3.5rem;
|
||||||
|
margin: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-body {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-body .title,
|
||||||
|
.card .card-body .message {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-body .title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-body .message {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0 .5rem;
|
||||||
|
}
|
||||||
+438
@@ -0,0 +1,438 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
|
||||||
|
let ws;
|
||||||
|
let env;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
ws = new WebSocket(`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}`);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log("Connected");
|
||||||
|
|
||||||
|
ws.onmessage = checkEvent;
|
||||||
|
}
|
||||||
|
ws.onclose = reconnect;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconnect() {
|
||||||
|
// !TEMP: only for development mode
|
||||||
|
if (env === "dev") {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
console.log("Reconnecting in 5 seconds...");
|
||||||
|
setTimeout(init, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = [];
|
||||||
|
let handlingEvents = false;
|
||||||
|
|
||||||
|
async function checkEvent(e) {
|
||||||
|
if (!env) {
|
||||||
|
env = JSON.parse(e.data).env.toLowerCase();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = JSON.parse(e.data);
|
||||||
|
|
||||||
|
if (message.song) {
|
||||||
|
updateSong(message.song)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
events.push(message);
|
||||||
|
|
||||||
|
if (events.length === 1) {
|
||||||
|
do {
|
||||||
|
const data = events[0];
|
||||||
|
|
||||||
|
if (data.channelId) {
|
||||||
|
switch (data.rewardId) {
|
||||||
|
// karaoke time
|
||||||
|
case "27faa7e4-f496-4e91-92ae-a51f99b9e854":
|
||||||
|
await karaokeTime(data.userDisplayName, data.message);
|
||||||
|
break;
|
||||||
|
// ruleta rusa
|
||||||
|
case "a73247ee-e33e-4e9b-9105-bd9d11e111fc":
|
||||||
|
await russianRoulette(data.userDisplayName);
|
||||||
|
break;
|
||||||
|
// timeout a un amigo
|
||||||
|
case "638c642d-23d8-4264-9702-e77eeba134de":
|
||||||
|
await timeoutFriend(data);
|
||||||
|
break;
|
||||||
|
// highlight message
|
||||||
|
case "a26c0d9e-fd2c-4943-bc94-c5c2f2c974e4":
|
||||||
|
await highlightMessage(data);
|
||||||
|
break;
|
||||||
|
case "a215d6a0-2c11-4503-bb29-1ca98ef046ac":
|
||||||
|
await giveTempVip(data);
|
||||||
|
data.message = `@${data.userDisplayName} ha encontrado diamantes!`;
|
||||||
|
await createCard(data.rewardName, data.message, data.backgroundColor, data.rewardImage);
|
||||||
|
break;
|
||||||
|
// robar el vip
|
||||||
|
case "ac750bd6-fb4c-4259-b06d-56953601243b":
|
||||||
|
await createCard(data.rewardName, data.message, data.backgroundColor, data.rewardImage);
|
||||||
|
break;
|
||||||
|
// hidratate
|
||||||
|
case "232e951f-93d1-4138-a0e3-9e822b4852e0":
|
||||||
|
data.message = `@${data.userDisplayName} ha invitado a una ronda.`;
|
||||||
|
sendWsActions({
|
||||||
|
action: "say",
|
||||||
|
message: "FunnyCatTastingTHEWATER FunnyCatTastingTHEWATER FunnyCatTastingTHEWATER"
|
||||||
|
});
|
||||||
|
await createCard(data.rewardName, data.message, data.backgroundColor, data.rewardImage);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
await createCard(data.rewardName, data.message ? data.message : "", data.backgroundColor, data.rewardImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
events.shift();
|
||||||
|
await waitTime();
|
||||||
|
|
||||||
|
} while (events.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(e.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitTime() {
|
||||||
|
const WAIT_TIME_MS = 500;
|
||||||
|
|
||||||
|
return new Promise(res => {
|
||||||
|
setTimeout(res, WAIT_TIME_MS);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function karaokeTime(username, message) {
|
||||||
|
return new Promise(res => {
|
||||||
|
console.log(username, message);
|
||||||
|
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.classList.add("alert");
|
||||||
|
|
||||||
|
const lightLeft = document.createElement("div");
|
||||||
|
lightLeft.classList.add("light", "light-left");
|
||||||
|
div.appendChild(lightLeft);
|
||||||
|
|
||||||
|
const lightRight = document.createElement("div");
|
||||||
|
lightRight.classList.add("light", "light-right");
|
||||||
|
div.appendChild(lightRight);
|
||||||
|
|
||||||
|
const randomLeft = tinycolor.random().setAlpha(.45).saturate(100).toRgbString();
|
||||||
|
const randomRight = tinycolor(randomLeft).spin(Math.floor(Math.random() * 90) + 90).toRgbString();
|
||||||
|
|
||||||
|
insertCssVariables({
|
||||||
|
"--light-color-left": randomLeft,
|
||||||
|
"--light-color-right": randomRight
|
||||||
|
});
|
||||||
|
|
||||||
|
const img = createImg("/img/karaoke-time.png");
|
||||||
|
const p = createText(`${username} ha sugerido cantar un tema`);
|
||||||
|
|
||||||
|
div.appendChild(img);
|
||||||
|
div.appendChild(p);
|
||||||
|
img.onload = () => {
|
||||||
|
const audio = createAudio("/sfx/karaoke-time.mp3");
|
||||||
|
|
||||||
|
audio.onended = function() {
|
||||||
|
div.remove();
|
||||||
|
res();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(div);
|
||||||
|
audio.play();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
let rrAttemps = 0;
|
||||||
|
function russianRoulette(username) {
|
||||||
|
return new Promise(res => {
|
||||||
|
const win = rando(5 - rrAttemps) !== 0;
|
||||||
|
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.classList.add("alert");
|
||||||
|
div.style.margin = ".5rem";
|
||||||
|
|
||||||
|
const img = createImg("/img/toy-gun.png");
|
||||||
|
|
||||||
|
const p = createText();
|
||||||
|
|
||||||
|
if (win) {
|
||||||
|
p.innerText = `${username} ha sido afortunado y aún sigue entre nosotros`;
|
||||||
|
rrAttemps = 0;
|
||||||
|
} else {
|
||||||
|
p.innerText = `${username} se ha ido a un mundo mejor, siempre te recordaremos`;
|
||||||
|
rrAttemps++;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.appendChild(img);
|
||||||
|
div.appendChild(p);
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
const audio = createAudio(`/sfx/toy-gun/${win ? 'stuck' : 'shot'}.mp3`);
|
||||||
|
|
||||||
|
document.body.appendChild(div);
|
||||||
|
|
||||||
|
const actions = [];
|
||||||
|
if (!win) {
|
||||||
|
img.classList.add("shoot");
|
||||||
|
|
||||||
|
actions.push({
|
||||||
|
action: "timeout",
|
||||||
|
username: username,
|
||||||
|
time: "60",
|
||||||
|
reason: "F en la ruleta."
|
||||||
|
});
|
||||||
|
|
||||||
|
actions.push({
|
||||||
|
action: "say",
|
||||||
|
message: `PepeHands ${username} no ha sobrevivido para contarlo.`
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
actions.push({
|
||||||
|
action: "say",
|
||||||
|
message: `rdCool Clap ${username}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actions.length > 0) {
|
||||||
|
sendWsActions(actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
audio.onended = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
div.remove();
|
||||||
|
res();
|
||||||
|
}, 1250);
|
||||||
|
}
|
||||||
|
|
||||||
|
audio.play();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function timeoutFriend(data) {
|
||||||
|
const senderUser = data.userDisplayName;
|
||||||
|
const receptorUser = data.message.split(" ")[0];
|
||||||
|
|
||||||
|
sendWsActions({
|
||||||
|
action: "timeout",
|
||||||
|
username: receptorUser,
|
||||||
|
time: "60",
|
||||||
|
reason: `Timeout dado por @${senderUser} con puntos del canal.`
|
||||||
|
});
|
||||||
|
|
||||||
|
const cardMessage = `@${senderUser} ha expulsado a @${receptorUser} por 60 segundos.`;
|
||||||
|
await createCard(data.rewardName, cardMessage, data.backgroundColor, data.rewardImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function highlightMessage(data) {
|
||||||
|
const urlRegex = /(https?:\/\/)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
|
||||||
|
|
||||||
|
if (urlRegex.test(data.message)) {
|
||||||
|
sendWsActions({
|
||||||
|
action: "timeout",
|
||||||
|
username: data.userDisplayName,
|
||||||
|
time: "10",
|
||||||
|
reason: "No esta permitido enviar enlaces en mensajes destacados."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await createCard(data.rewardName, data.message, data.backgroundColor, data.rewardImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function giveTempVip(data) {
|
||||||
|
const username = data.userDisplayName;
|
||||||
|
const channel = data.channelId;
|
||||||
|
|
||||||
|
sendWsActions([{
|
||||||
|
action: "addVip",
|
||||||
|
channel,
|
||||||
|
username
|
||||||
|
}, {
|
||||||
|
scheduledAt: Date.now() + 1000 * 60 * 60 * 24 * 7,
|
||||||
|
action: "removeVip",
|
||||||
|
channel,
|
||||||
|
username
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// send actions to be performed by the server
|
||||||
|
function sendWsActions(actions) {
|
||||||
|
if (!Array.isArray(actions)) {
|
||||||
|
actions = [actions];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (env === "dev") {
|
||||||
|
console.log("Dev mode, actions not sent: ", actions);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
|
||||||
|
if (actions.length > 0) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
actions: actions
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCard(title, message, color, image) {
|
||||||
|
const maxMessageLength = 120;
|
||||||
|
const darkenLighten = 10;
|
||||||
|
|
||||||
|
return new Promise(res => {
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.classList.add("card", "open");
|
||||||
|
|
||||||
|
const img = createImg(image);
|
||||||
|
img.classList.add("card-image");
|
||||||
|
card.appendChild(img);
|
||||||
|
|
||||||
|
const body = document.createElement("div");
|
||||||
|
body.classList.add("card-body");
|
||||||
|
card.appendChild(body);
|
||||||
|
|
||||||
|
const titl = document.createElement("h1");
|
||||||
|
titl.classList.add("title");
|
||||||
|
titl.innerText = title;
|
||||||
|
body.appendChild(titl);
|
||||||
|
|
||||||
|
if (message.length > maxMessageLength) {
|
||||||
|
while (message.length > maxMessageLength) {
|
||||||
|
message = message.split(" ").slice(0, -1).join(" ");
|
||||||
|
}
|
||||||
|
message += "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
const msg = createText(message);
|
||||||
|
msg.classList.add("message");
|
||||||
|
body.appendChild(msg);
|
||||||
|
|
||||||
|
color = tinycolor(color);
|
||||||
|
|
||||||
|
if (!color.isValid()) {
|
||||||
|
color = tinycolor("#2EC90C");
|
||||||
|
}
|
||||||
|
|
||||||
|
let backgroundColor = color;
|
||||||
|
let borderColor = new tinycolor(backgroundColor.toHexString());
|
||||||
|
|
||||||
|
if (backgroundColor.isLight()) {
|
||||||
|
card.classList.add("card-light");
|
||||||
|
borderColor.darken(darkenLighten).toHexString();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
borderColor.lighten(darkenLighten).toHexString();
|
||||||
|
}
|
||||||
|
|
||||||
|
insertCssVariables({
|
||||||
|
"--card-background-color": backgroundColor.toHexString(),
|
||||||
|
"--card-border-color": borderColor.toHexString()
|
||||||
|
});
|
||||||
|
|
||||||
|
card.addEventListener("animationend", () => {
|
||||||
|
if (card.classList.contains("open")) {
|
||||||
|
card.classList.remove("open");
|
||||||
|
|
||||||
|
const fairTime = message.split(" ").length / 5;
|
||||||
|
const timeOpen = Math.min(Math.max(fairTime, 4), 8);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
card.classList.add("close");
|
||||||
|
}, timeOpen * 1000);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
card.remove();
|
||||||
|
res();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
document.body.appendChild(card);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates a img and sets its src
|
||||||
|
function createImg(path) {
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.src = path;
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates a paragraph and sets a text inside
|
||||||
|
function createText(txt) {
|
||||||
|
const p = document.createElement("p");
|
||||||
|
|
||||||
|
if (txt) {
|
||||||
|
p.innerText = txt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates and initializes an audio element with given audio src
|
||||||
|
function createAudio(path) {
|
||||||
|
const audio = new Audio(path);
|
||||||
|
audio.volume = .025;
|
||||||
|
|
||||||
|
return audio;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get a internal style sheet or create it if it does not exist, used to set css variables on :root
|
||||||
|
function cssSheet() {
|
||||||
|
const targetValue = "cssVariables";
|
||||||
|
let style = document.querySelector(`style[data-target="${targetValue}"]`);
|
||||||
|
|
||||||
|
if (style) {
|
||||||
|
return style.sheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
style = document.createElement("style");
|
||||||
|
style.setAttribute("data-target", targetValue);
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
return style.sheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add css rules to :root element
|
||||||
|
function insertCssVariables(rules) {
|
||||||
|
const sheet = cssSheet();
|
||||||
|
|
||||||
|
while (sheet.rules.length > 0) {
|
||||||
|
sheet.deleteRule(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rulesTxt = ":root {";
|
||||||
|
for (const name in rules) {
|
||||||
|
rulesTxt += `${name}: ${rules[name]};`;
|
||||||
|
}
|
||||||
|
rulesTxt += "}";
|
||||||
|
|
||||||
|
sheet.insertRule(rulesTxt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// playing song overlay
|
||||||
|
function updateSong({ title, artist, coverArt}) {
|
||||||
|
const playing = document.getElementById("playing");
|
||||||
|
if (
|
||||||
|
!title &&
|
||||||
|
!artist
|
||||||
|
) {
|
||||||
|
playing.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
playing.style.display = null;
|
||||||
|
playing.querySelector(".coverArt").src = coverArt;
|
||||||
|
playing.querySelector(".trackName").innerText = title;
|
||||||
|
playing.querySelector(".trackArtist").innerText = artist;
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 216 KiB |
@@ -0,0 +1,116 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Websocket</title>
|
||||||
|
|
||||||
|
<script src="/libs/js/tinycolor-min.js" defer></script>
|
||||||
|
<script src="/libs/js/randojs.js" defer></script>
|
||||||
|
|
||||||
|
<script src="app.js" defer></script>
|
||||||
|
<link rel="stylesheet" href="app.css">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--playerWidth: 20rem;
|
||||||
|
--imgSize: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playing {
|
||||||
|
position: absolute;
|
||||||
|
bottom: .5rem;
|
||||||
|
left: .5rem;
|
||||||
|
width: var(--playerWidth);
|
||||||
|
padding: .5rem;
|
||||||
|
background-color: rgba(0, 0, 0, .5);
|
||||||
|
border-radius: .5rem;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playing > div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playing .previous {
|
||||||
|
animation: 1s translateLeft forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playing .next {
|
||||||
|
animation: 1s translateRight forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes translateLeft {
|
||||||
|
from {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
80%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translate3d(-100%, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes translateRight {
|
||||||
|
from {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
0%,
|
||||||
|
10% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate3d(-100%, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#playing img {
|
||||||
|
display: block;
|
||||||
|
width: var(--imgSize);
|
||||||
|
height: var(--imgSize);
|
||||||
|
margin-right: calc(var(--imgSize) / 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
#playing h1,
|
||||||
|
#playing h2 {
|
||||||
|
width: calc(var(--playerWidth) - var(--imgSize) * 1.2 - 1em * 1.5);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="playing" style="display: none !important">
|
||||||
|
<div>
|
||||||
|
<img class="coverArt" src="https://picsum.photos/id/248/300/300.jpg">
|
||||||
|
<div>
|
||||||
|
<h1 class="trackName">Track Name</h1>
|
||||||
|
<h2 class="trackArtist">Track Artist</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="previous">
|
||||||
|
<img class="coverArt" src="https://picsum.photos/id/141/300/300.jpg">
|
||||||
|
<div>
|
||||||
|
<h1 class="trackName">Name previous</h1>
|
||||||
|
<h2 class="trackArtist">Artist previous</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="next">
|
||||||
|
<img class="coverArt" src="https://picsum.photos/id/415/300/300.jpg">
|
||||||
|
<div>
|
||||||
|
<h1 class="trackName">Name next</h1>
|
||||||
|
<h2 class="trackArtist">Artist next</h2>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
function rando(a,b,e){var g=function(f){return"undefined"===typeof f},k=function(f){return"number"===typeof f&&!isNaN(f)},d=function(f){return!g(f)&&null!==f&&f.constructor===Array},c=function(){try{for(var f,q=[],r;30>(r="."+q.join("")).length;){f=(window.crypto||window.msCrypto).getRandomValues(new Uint32Array(5));for(var p=0;p<f.length;p++){var t=4E9>f[p]?f[p].toString().slice(1):"";0<t.length&&(q[q.length]=t)}}return Number(r)}catch(v){return Math.random()}};try{if(null!==a&&null!==b&&null!==
|
||||||
|
e){if(g(a))return c();if(window.jQuery&&a instanceof jQuery&&g(b)){if(0==a.length)return!1;var n=rando(0,a.length-1);return{index:n,value:a.eq(n)}}if(k(a)&&k(b)&&"string"===typeof e&&"float"==e.toLowerCase().trim()){if(a>b){var m=b;b=a;a=m}return c()*(b-a)+a}if(d(a)&&0<a.length&&g(b)){var l=c()*a.length<<0;return{index:l,value:a[l]}}if("object"===typeof a&&g(b)){l=a;var h=Object.keys(l);if(0<h.length){var u=h[h.length*c()<<0];return{key:u,value:l[u]}}}if((!0===a&&!1===b||!1===a&&!0===b)&&g(e))return.5>
|
||||||
|
rando();if(k(a)&&g(b))return 0<=a?rando(0,a):rando(a,0);if(k(a)&&"string"===typeof b&&"float"==b.toLowerCase().trim()&&g(e))return 0<=a?rando(0,a,"float"):rando(a,0,"float");if(k(a)&&k(b)&&g(e))return a>b&&(m=b,b=a,a=m),a=Math.floor(a),b=Math.floor(b),Math.floor(c()*(b-a+1)+a);if("string"===typeof a&&0<a.length&&g(b))return a.charAt(rando(0,a.length-1))}return!1}catch(f){return!1}}
|
||||||
|
function randoSequence(a,b){var e=function(h){return"undefined"===typeof h},g=function(h){return"number"===typeof h&&!isNaN(h)},k=function(h){return!e(h)&&null!==h&&h.constructor===Array};try{if(e(a)||null===a||null===b)return!1;var d=[];if(window.jQuery&&a instanceof jQuery&&e(b)){if(0<a.length){d=randoSequence(0,a.length-1);for(var c=0;c<d.length;c++)d[c]={index:d[c],value:a.eq(d[c])}}return d}if(e(b))if(k(a)&&e(b))for(c=0;c<a.length;c++)d[d.length]={index:c,value:a[c]};else if("object"===typeof a&&
|
||||||
|
e(b))for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(d[d.length]={key:n,value:a[n]});else if("string"===typeof a&&e(b))for(c=0;c<a.length;c++)d[d.length]=a.charAt(c);else return g(a)&&e(b)?0<=a?randoSequence(0,a):randoSequence(a,0):!1;else{if(!g(a)||!g(b)||0<a%1||0<b%1)return!1;if(a>b){var m=b;b=a;a=m}for(c=a;c<=b;c++)d[d.length]=c}for(c=d.length-1;0<c;c--){var l=rando(c);m=d[c];d[c]=d[l];d[l]=m}return d}catch(h){return!1}};
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
// TinyColor v1.4.2
|
||||||
|
// https://github.com/bgrins/TinyColor
|
||||||
|
// 2020-09-25, Brian Grinstead, MIT License
|
||||||
|
!function(a){function b(a,d){if(a=a?a:"",d=d||{},a instanceof b)return a;if(!(this instanceof b))return new b(a,d);var e=c(a);this._originalInput=a,this._r=e.r,this._g=e.g,this._b=e.b,this._a=e.a,this._roundA=P(100*this._a)/100,this._format=d.format||e.format,this._gradientType=d.gradientType,this._r<1&&(this._r=P(this._r)),this._g<1&&(this._g=P(this._g)),this._b<1&&(this._b=P(this._b)),this._ok=e.ok,this._tc_id=O++}function c(a){var b={r:0,g:0,b:0},c=1,e=null,g=null,i=null,j=!1,k=!1;return"string"==typeof a&&(a=K(a)),"object"==typeof a&&(J(a.r)&&J(a.g)&&J(a.b)?(b=d(a.r,a.g,a.b),j=!0,k="%"===String(a.r).substr(-1)?"prgb":"rgb"):J(a.h)&&J(a.s)&&J(a.v)?(e=G(a.s),g=G(a.v),b=h(a.h,e,g),j=!0,k="hsv"):J(a.h)&&J(a.s)&&J(a.l)&&(e=G(a.s),i=G(a.l),b=f(a.h,e,i),j=!0,k="hsl"),a.hasOwnProperty("a")&&(c=a.a)),c=z(c),{ok:j,format:a.format||k,r:Q(255,R(b.r,0)),g:Q(255,R(b.g,0)),b:Q(255,R(b.b,0)),a:c}}function d(a,b,c){return{r:255*A(a,255),g:255*A(b,255),b:255*A(c,255)}}function e(a,b,c){a=A(a,255),b=A(b,255),c=A(c,255);var d,e,f=R(a,b,c),g=Q(a,b,c),h=(f+g)/2;if(f==g)d=e=0;else{var i=f-g;switch(e=h>.5?i/(2-f-g):i/(f+g),f){case a:d=(b-c)/i+(c>b?6:0);break;case b:d=(c-a)/i+2;break;case c:d=(a-b)/i+4}d/=6}return{h:d,s:e,l:h}}function f(a,b,c){function d(a,b,c){return 0>c&&(c+=1),c>1&&(c-=1),1/6>c?a+6*(b-a)*c:.5>c?b:2/3>c?a+(b-a)*(2/3-c)*6:a}var e,f,g;if(a=A(a,360),b=A(b,100),c=A(c,100),0===b)e=f=g=c;else{var h=.5>c?c*(1+b):c+b-c*b,i=2*c-h;e=d(i,h,a+1/3),f=d(i,h,a),g=d(i,h,a-1/3)}return{r:255*e,g:255*f,b:255*g}}function g(a,b,c){a=A(a,255),b=A(b,255),c=A(c,255);var d,e,f=R(a,b,c),g=Q(a,b,c),h=f,i=f-g;if(e=0===f?0:i/f,f==g)d=0;else{switch(f){case a:d=(b-c)/i+(c>b?6:0);break;case b:d=(c-a)/i+2;break;case c:d=(a-b)/i+4}d/=6}return{h:d,s:e,v:h}}function h(b,c,d){b=6*A(b,360),c=A(c,100),d=A(d,100);var e=a.floor(b),f=b-e,g=d*(1-c),h=d*(1-f*c),i=d*(1-(1-f)*c),j=e%6,k=[d,h,g,g,i,d][j],l=[i,d,d,h,g,g][j],m=[g,g,i,d,d,h][j];return{r:255*k,g:255*l,b:255*m}}function i(a,b,c,d){var e=[F(P(a).toString(16)),F(P(b).toString(16)),F(P(c).toString(16))];return d&&e[0].charAt(0)==e[0].charAt(1)&&e[1].charAt(0)==e[1].charAt(1)&&e[2].charAt(0)==e[2].charAt(1)?e[0].charAt(0)+e[1].charAt(0)+e[2].charAt(0):e.join("")}function j(a,b,c,d,e){var f=[F(P(a).toString(16)),F(P(b).toString(16)),F(P(c).toString(16)),F(H(d))];return e&&f[0].charAt(0)==f[0].charAt(1)&&f[1].charAt(0)==f[1].charAt(1)&&f[2].charAt(0)==f[2].charAt(1)&&f[3].charAt(0)==f[3].charAt(1)?f[0].charAt(0)+f[1].charAt(0)+f[2].charAt(0)+f[3].charAt(0):f.join("")}function k(a,b,c,d){var e=[F(H(d)),F(P(a).toString(16)),F(P(b).toString(16)),F(P(c).toString(16))];return e.join("")}function l(a,c){c=0===c?0:c||10;var d=b(a).toHsl();return d.s-=c/100,d.s=B(d.s),b(d)}function m(a,c){c=0===c?0:c||10;var d=b(a).toHsl();return d.s+=c/100,d.s=B(d.s),b(d)}function n(a){return b(a).desaturate(100)}function o(a,c){c=0===c?0:c||10;var d=b(a).toHsl();return d.l+=c/100,d.l=B(d.l),b(d)}function p(a,c){c=0===c?0:c||10;var d=b(a).toRgb();return d.r=R(0,Q(255,d.r-P(255*-(c/100)))),d.g=R(0,Q(255,d.g-P(255*-(c/100)))),d.b=R(0,Q(255,d.b-P(255*-(c/100)))),b(d)}function q(a,c){c=0===c?0:c||10;var d=b(a).toHsl();return d.l-=c/100,d.l=B(d.l),b(d)}function r(a,c){var d=b(a).toHsl(),e=(d.h+c)%360;return d.h=0>e?360+e:e,b(d)}function s(a){var c=b(a).toHsl();return c.h=(c.h+180)%360,b(c)}function t(a){var c=b(a).toHsl(),d=c.h;return[b(a),b({h:(d+120)%360,s:c.s,l:c.l}),b({h:(d+240)%360,s:c.s,l:c.l})]}function u(a){var c=b(a).toHsl(),d=c.h;return[b(a),b({h:(d+90)%360,s:c.s,l:c.l}),b({h:(d+180)%360,s:c.s,l:c.l}),b({h:(d+270)%360,s:c.s,l:c.l})]}function v(a){var c=b(a).toHsl(),d=c.h;return[b(a),b({h:(d+72)%360,s:c.s,l:c.l}),b({h:(d+216)%360,s:c.s,l:c.l})]}function w(a,c,d){c=c||6,d=d||30;var e=b(a).toHsl(),f=360/d,g=[b(a)];for(e.h=(e.h-(f*c>>1)+720)%360;--c;)e.h=(e.h+f)%360,g.push(b(e));return g}function x(a,c){c=c||6;for(var d=b(a).toHsv(),e=d.h,f=d.s,g=d.v,h=[],i=1/c;c--;)h.push(b({h:e,s:f,v:g})),g=(g+i)%1;return h}function y(a){var b={};for(var c in a)a.hasOwnProperty(c)&&(b[a[c]]=c);return b}function z(a){return a=parseFloat(a),(isNaN(a)||0>a||a>1)&&(a=1),a}function A(b,c){D(b)&&(b="100%");var d=E(b);return b=Q(c,R(0,parseFloat(b))),d&&(b=parseInt(b*c,10)/100),a.abs(b-c)<1e-6?1:b%c/parseFloat(c)}function B(a){return Q(1,R(0,a))}function C(a){return parseInt(a,16)}function D(a){return"string"==typeof a&&-1!=a.indexOf(".")&&1===parseFloat(a)}function E(a){return"string"==typeof a&&-1!=a.indexOf("%")}function F(a){return 1==a.length?"0"+a:""+a}function G(a){return 1>=a&&(a=100*a+"%"),a}function H(b){return a.round(255*parseFloat(b)).toString(16)}function I(a){return C(a)/255}function J(a){return!!V.CSS_UNIT.exec(a)}function K(a){a=a.replace(M,"").replace(N,"").toLowerCase();var b=!1;if(T[a])a=T[a],b=!0;else if("transparent"==a)return{r:0,g:0,b:0,a:0,format:"name"};var c;return(c=V.rgb.exec(a))?{r:c[1],g:c[2],b:c[3]}:(c=V.rgba.exec(a))?{r:c[1],g:c[2],b:c[3],a:c[4]}:(c=V.hsl.exec(a))?{h:c[1],s:c[2],l:c[3]}:(c=V.hsla.exec(a))?{h:Line truncated
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,358 @@
|
|||||||
|
import { RefreshableAuthProvider, StaticAuthProvider } from "twitch-auth";
|
||||||
|
|
||||||
|
import { ApiClient } from "twitch";
|
||||||
|
import { ChatClient } from "twitch-chat-client";
|
||||||
|
import { PubSubClient } from "twitch-pubsub-client";
|
||||||
|
import WebSocket from "ws";
|
||||||
|
import express from "express";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const TOKENS_FILE = "./tokens.json";
|
||||||
|
const SCHEDULED_FILE = "./scheduled.json";
|
||||||
|
const DEV_MODE = process.argv.includes("--dev");
|
||||||
|
|
||||||
|
const scheduledActions = [];
|
||||||
|
let saInterval;
|
||||||
|
|
||||||
|
const channel = "alexbcberio";
|
||||||
|
|
||||||
|
let apiClient;
|
||||||
|
let chatClient;
|
||||||
|
|
||||||
|
//! Important: store users & channels by id, not by username
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
let tokenData;
|
||||||
|
try {
|
||||||
|
tokenData = JSON.parse(await fs.readFile(TOKENS_FILE));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`${TOKENS_FILE} not found, cannot init chatbot.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!tokenData.refresh_token ||
|
||||||
|
!tokenData.access_token
|
||||||
|
) {
|
||||||
|
console.error(`Missing parameters in ${TOKENS_FILE}, refresh_token or access_token.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const authProvider = new RefreshableAuthProvider(
|
||||||
|
new StaticAuthProvider(process.env.TWITCH_CLIENT_ID, tokenData.access_token), {
|
||||||
|
clientSecret: process.env.TWITCH_CLIENT_SECRET,
|
||||||
|
refreshToken: tokenData.refresh_token,
|
||||||
|
expiry: tokenData.expiryTimestamp === null ? null : new Date(tokenData.expiryTimestamp),
|
||||||
|
onRefresh: async ({ accessToken, refreshToken, expiryDate }) => {
|
||||||
|
console.log("Tokens refreshed");
|
||||||
|
const newTokenData = {
|
||||||
|
access_token: accessToken,
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
expiryTimestamp: expiryDate === null ? null : expiryDate.getTime()
|
||||||
|
};
|
||||||
|
await fs.writeFile(TOKENS_FILE, JSON.stringify(newTokenData));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
apiClient = new ApiClient({ authProvider });
|
||||||
|
|
||||||
|
const pubSubClient = new PubSubClient();
|
||||||
|
const userId = await pubSubClient.registerUserListener(apiClient, channel);
|
||||||
|
/*const listener = */await pubSubClient.onRedemption(userId, onRedemption);
|
||||||
|
|
||||||
|
console.log("[Twitch PubSub] Connected & registered");
|
||||||
|
|
||||||
|
chatClient = new ChatClient(authProvider, { channels: [channel] });
|
||||||
|
|
||||||
|
chatClient.onConnect(async () => {
|
||||||
|
console.log("[ChatClient] Connected");
|
||||||
|
|
||||||
|
// *Check this, not working
|
||||||
|
if (!saInterval) {
|
||||||
|
let savedActions = [];
|
||||||
|
try {
|
||||||
|
savedActions = JSON.parse(await fs.readFile(SCHEDULED_FILE));
|
||||||
|
} catch (e) {
|
||||||
|
// probably file does not exist
|
||||||
|
}
|
||||||
|
scheduledActions.push.apply(scheduledActions, savedActions);
|
||||||
|
scheduledActions.sort((a, b) => a.scheduledAt - b.scheduledAt);
|
||||||
|
|
||||||
|
setTimeout(checkScheduledActions, 1000 * 5);
|
||||||
|
saInterval = setInterval(checkScheduledActions, 1000 * 60);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
chatClient.onDisconnect(e => {
|
||||||
|
console.log(`[ChatClient] Disconnected ${e.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
chatClient.onNoPermission((channel, message) => {
|
||||||
|
console.log(`[ChatClient] No permission on ${channel}: ${message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
chatClient.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
async function onRedemption(message) {
|
||||||
|
console.log(`Reward: "${message.rewardName}" (${message.rewardId}) redeemed by ${message.userDisplayName}`);
|
||||||
|
const reward = message._data.data.redemption.reward;
|
||||||
|
|
||||||
|
let msg = {
|
||||||
|
id: message.id,
|
||||||
|
channelId: message.channelId,
|
||||||
|
rewardId: message.rewardId,
|
||||||
|
rewardName: message.rewardName,
|
||||||
|
rewardImage: message.rewardImage ? message.rewardImage.url_4x : "https://static-cdn.jtvnw.net/custom-reward-images/default-4.png",
|
||||||
|
message: message.message,
|
||||||
|
userDisplayName: message.userDisplayName,
|
||||||
|
// non directly available values from PubSubRedemptionMessage
|
||||||
|
backgroundColor: reward.background_color
|
||||||
|
};
|
||||||
|
|
||||||
|
switch(msg.rewardId) {
|
||||||
|
// robar vip
|
||||||
|
case "ac750bd6-fb4c-4259-b06d-56953601243b":
|
||||||
|
msg = await stealVip(msg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg) {
|
||||||
|
broadcast(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const wsServer = new WebSocket.Server({
|
||||||
|
noServer: true
|
||||||
|
});
|
||||||
|
|
||||||
|
let sockets = [];
|
||||||
|
wsServer.on("connection", (socket, req) => {
|
||||||
|
console.log(`[WS] ${req.socket.remoteAddress} New connection established`);
|
||||||
|
sockets.push(socket);
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
env: DEV_MODE ? "dev" : "prod"
|
||||||
|
}));
|
||||||
|
|
||||||
|
socket.on("message", async (msg) => {
|
||||||
|
const data = JSON.parse(msg);
|
||||||
|
|
||||||
|
// broadcast message
|
||||||
|
if (!data.actions || data.actions.length === 0) {
|
||||||
|
sockets
|
||||||
|
.filter(s => s !== socket)
|
||||||
|
.forEach(s => s.send(msg));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const action of data.actions) {
|
||||||
|
if (!action.scheduledAt) {
|
||||||
|
await handleClientAction(action);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
scheduledActions.push(action);
|
||||||
|
scheduledActions.sort((a, b) => a.scheduledAt - b.scheduledAt);
|
||||||
|
saveScheduledActions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[WS] Received message with ${data.actions.length} actions:`, data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("close", () => {
|
||||||
|
sockets = sockets.filter(s => s !== socket);
|
||||||
|
console.log("[WS] Connection closed");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleClientAction(action) {
|
||||||
|
|
||||||
|
if (action.channel && !isNaN(action.channel)) {
|
||||||
|
action.channel = await getUsernameFromId(parseInt(action.channel));
|
||||||
|
}
|
||||||
|
if (action.username && !isNaN(action.username)) {
|
||||||
|
action.username = await getUsernameFromId(parseInt(action.username));
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(action.action) {
|
||||||
|
case "say":
|
||||||
|
say(channel, action.message);
|
||||||
|
break;
|
||||||
|
case "timeout":
|
||||||
|
await timeout(channel, action.username, action.time, action.reason);
|
||||||
|
break;
|
||||||
|
case "broadcast":
|
||||||
|
broadcast(action.message);
|
||||||
|
break;
|
||||||
|
case "addVip":
|
||||||
|
await addVip(action.channel, action.username);
|
||||||
|
break;
|
||||||
|
case "removeVip":
|
||||||
|
await removeVip(action.channel, action.username);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(`Couldn't handle action:`, action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ssaTimeout;
|
||||||
|
function saveScheduledActions() {
|
||||||
|
if (ssaTimeout) {
|
||||||
|
clearTimeout(ssaTimeout);
|
||||||
|
ssaTimeout = null;
|
||||||
|
console.log("[Scheduled] Removed save timeout.");
|
||||||
|
}
|
||||||
|
|
||||||
|
ssaTimeout = setTimeout(async () => {
|
||||||
|
await fs.writeFile(SCHEDULED_FILE, JSON.stringify(scheduledActions));
|
||||||
|
console.log("[Scheduled] Saved actions.");
|
||||||
|
ssaTimeout = null;
|
||||||
|
}, 1000 * 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
let checkingScheduled = false;
|
||||||
|
async function checkScheduledActions() {
|
||||||
|
if (checkingScheduled) return;
|
||||||
|
checkingScheduled = true;
|
||||||
|
|
||||||
|
let hasToSave = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < scheduledActions.length && scheduledActions[i].scheduledAt <= Date.now(); i++) {
|
||||||
|
hasToSave = true;
|
||||||
|
|
||||||
|
const action = scheduledActions.splice(i, 1)[0];
|
||||||
|
await handleClientAction(action);
|
||||||
|
console.log(`[Scheduled] Executed: ${JSON.stringify(action)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasToSave) {
|
||||||
|
saveScheduledActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
checkingScheduled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// send a chat message
|
||||||
|
function say(channel, message) {
|
||||||
|
chatClient.say(channel, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// timeouts a user in a channel
|
||||||
|
async function timeout(channel, username, time, reason) {
|
||||||
|
if (!time) {
|
||||||
|
time = 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reason) {
|
||||||
|
reason = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await chatClient.timeout(channel, username, time, reason);
|
||||||
|
} catch (e) {
|
||||||
|
// user cannot be timed out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// broadcast a message to all clients
|
||||||
|
function broadcast(msg) {
|
||||||
|
sockets.forEach(s => s.send(JSON.stringify(msg)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// adds a user to vips
|
||||||
|
async function addVip(channel, username, message) {
|
||||||
|
if (!message) {
|
||||||
|
message = `Otorgado VIP a @${username}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await chatClient.addVip(channel, username);
|
||||||
|
say(channel, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hasVip(channel, username) {
|
||||||
|
if (!username) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vips = await chatClient.getVips(channel);
|
||||||
|
return vips.includes(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
// removes a user from vips
|
||||||
|
async function removeVip(channel, username, message) {
|
||||||
|
if (!message) {
|
||||||
|
message = `Se ha acabado el chollo, VIP de @${username} eliminado.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await chatClient.removeVip(channel, username);
|
||||||
|
say(channel, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUsernameFromId(userId) {
|
||||||
|
const user = await apiClient.helix.users.getUserById(userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove vip from a user to grant it to yourself
|
||||||
|
async function stealVip(msg) {
|
||||||
|
const channel = await getUsernameFromId(parseInt(msg.channelId));
|
||||||
|
const addVipUser = msg.userDisplayName;
|
||||||
|
const removeVipUser = msg.message;
|
||||||
|
|
||||||
|
if (await hasVip(channel, removeVipUser)) {
|
||||||
|
await removeVip(channel, removeVipUser);
|
||||||
|
await addVip(channel, addVipUser);
|
||||||
|
|
||||||
|
const scheduledRemoveVipIndex = scheduledActions.findIndex(s => s.action === "removeVip" && s.username === removeVipUser);
|
||||||
|
|
||||||
|
if (scheduledRemoveVipIndex > -1) {
|
||||||
|
scheduledActions[scheduledRemoveVipIndex].username = addVipUser;
|
||||||
|
saveScheduledActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.message = `@${addVipUser} ha robado el VIP a @${removeVipUser}.`;
|
||||||
|
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Webserver
|
||||||
|
*/
|
||||||
|
const server = app.listen(!DEV_MODE ? 8080 : 8081, '0.0.0.0');
|
||||||
|
|
||||||
|
server.on("listening", () => {
|
||||||
|
console.log(`[Webserver] Listening on port ${server.address().port}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on("upgrade", (req, socket, head) => {
|
||||||
|
wsServer.handleUpgrade(req, socket, head, socket => {
|
||||||
|
wsServer.emit("connection", socket, req);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("*", async (req, res) => {
|
||||||
|
try {
|
||||||
|
let rpath = req.path;
|
||||||
|
|
||||||
|
if (rpath.endsWith("/")) {
|
||||||
|
rpath += "index.html";
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendFile(path.join(process.cwd(), "client", rpath));
|
||||||
|
} catch (e) {
|
||||||
|
res.sendStatus(404);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"ignore": [
|
||||||
|
"*.json",
|
||||||
|
"*.(jpe?g|png)",
|
||||||
|
"*.mp3"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "twitch-redemption-test",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node -r dotenv/config index.js",
|
||||||
|
"dev": "nodemon -r dotenv/config index.js -- --dev"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^8.2.0",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"nodemon": "^2.0.5",
|
||||||
|
"twitch": "^4.2.6",
|
||||||
|
"twitch-auth": "^4.2.6",
|
||||||
|
"twitch-chat-client": "^4.2.6",
|
||||||
|
"twitch-pubsub-client": "^4.2.6",
|
||||||
|
"ws": "^7.3.1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": "^5.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"access_token":"dse84wjqfyfz3i8a3k909epii51snh","refresh_token":"8t5wcdmkcd41fkv9nt4x9ahob1qsgm2eybwhvpj16lbzhabs7u","expiryTimestamp":1623803544550}
|
||||||
Reference in new issue
Block a user