├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE.md ├── README.md ├── client ├── chat.js ├── chat │ ├── App.svelte │ ├── Avatar.svelte │ ├── Message.svelte │ ├── MessageWithEmotes.svelte │ └── Team.svelte ├── faudrey_voir.js ├── libs │ ├── PerspectiveTransform.js │ ├── counter.js │ ├── glitch.js │ ├── player.js │ ├── random.js │ ├── shuffle.js │ └── sound.js ├── poll.js ├── poll │ ├── App.svelte │ ├── Item.svelte │ ├── Items.svelte │ ├── Paillettes.svelte │ ├── Poll.svelte │ └── boom.js ├── snake.js ├── snake │ └── App.svelte ├── streamer-highlight.js ├── team-ranking.js ├── team-ranking │ └── App.svelte ├── wall-frame.js └── wall-of-fame.js ├── package.json ├── public ├── chat.html ├── css │ └── tailwind.css ├── faudrey_voir.html ├── images │ ├── crown.png │ ├── skarab42.png │ └── teams │ │ ├── 42.svg │ │ ├── amazonwebservices.svg │ │ ├── android.svg │ │ ├── angularjs.svg │ │ ├── apache.svg │ │ ├── appcelerator.svg │ │ ├── apple.svg │ │ ├── atom.svg │ │ ├── babel.svg │ │ ├── backbonejs.svg │ │ ├── behance.svg │ │ ├── bitbucket.svg │ │ ├── bootstrap.svg │ │ ├── bower.svg │ │ ├── c.svg │ │ ├── cakephp.svg │ │ ├── ceylon.svg │ │ ├── chauve.svg │ │ ├── chrome.svg │ │ ├── codeigniter.svg │ │ ├── coffeescript.svg │ │ ├── confluence.svg │ │ ├── couchdb.svg │ │ ├── cplusplus.svg │ │ ├── csharp.svg │ │ ├── css3.svg │ │ ├── cucumber.svg │ │ ├── d3js.svg │ │ ├── dart.svg │ │ ├── debian.svg │ │ ├── deno.svg │ │ ├── devicon.svg │ │ ├── django.svg │ │ ├── docker.svg │ │ ├── doctrine.svg │ │ ├── dot-net.svg │ │ ├── drupal.svg │ │ ├── electron.svg │ │ ├── elm.svg │ │ ├── ember.svg │ │ ├── erlang.svg │ │ ├── express.svg │ │ ├── facebook.svg │ │ ├── firefox.svg │ │ ├── foundation.svg │ │ ├── gatling.svg │ │ ├── gimp.svg │ │ ├── git.svg │ │ ├── github.svg │ │ ├── gitlab.svg │ │ ├── go.svg │ │ ├── google.svg │ │ ├── gradle.svg │ │ ├── grunt.svg │ │ ├── gulp.svg │ │ ├── handlebars.svg │ │ ├── heroku.svg │ │ ├── html5.svg │ │ ├── ie10.svg │ │ ├── illustrator.svg │ │ ├── inkscape.svg │ │ ├── intellij.svg │ │ ├── ionic.svg │ │ ├── jasmine.svg │ │ ├── java.svg │ │ ├── javascript.svg │ │ ├── jeet.svg │ │ ├── jetbrains.svg │ │ ├── jquery.svg │ │ ├── js.svg │ │ ├── kotlin.svg │ │ ├── krakenjs.svg │ │ ├── laravel.svg │ │ ├── lerobot.svg │ │ ├── less.svg │ │ ├── linkedin.svg │ │ ├── linux.svg │ │ ├── meteor.svg │ │ ├── mocha.svg │ │ ├── mongodb.svg │ │ ├── moodle.svg │ │ ├── mysql.svg │ │ ├── nginx.svg │ │ ├── node-red.svg │ │ ├── nodejs.svg │ │ ├── nodered.svg │ │ ├── nodewebkit.svg │ │ ├── npm.svg │ │ ├── ocaml.svg │ │ ├── oracle.svg │ │ ├── photoshop.svg │ │ ├── php.svg │ │ ├── phpstorm.svg │ │ ├── postgresql.svg │ │ ├── protractor.svg │ │ ├── prout.svg │ │ ├── pycharm.svg │ │ ├── python.svg │ │ ├── rails.svg │ │ ├── react.svg │ │ ├── redhat.svg │ │ ├── redis.svg │ │ ├── ruby.svg │ │ ├── rubymine.svg │ │ ├── rust.svg │ │ ├── safari.svg │ │ ├── sass.svg │ │ ├── scribe.svg │ │ ├── sequelize.svg │ │ ├── sketch.svg │ │ ├── slack.svg │ │ ├── sourcetree.svg │ │ ├── ssh.svg │ │ ├── stackoverflow.svg │ │ ├── stylus.svg │ │ ├── swift.svg │ │ ├── symfony.svg │ │ ├── tomcat.svg │ │ ├── travis.svg │ │ ├── trello.svg │ │ ├── twitter.svg │ │ ├── typescript.svg │ │ ├── ubuntu.svg │ │ ├── unity.svg │ │ ├── unitysound.svg │ │ ├── vagrant.svg │ │ ├── vim.svg │ │ ├── visualstudio.svg │ │ ├── vuejs.svg │ │ ├── webpack.svg │ │ ├── webstorm.svg │ │ ├── windows8.svg │ │ ├── wordpress.svg │ │ ├── yarn.svg │ │ ├── yii.svg │ │ └── zend.svg ├── poll.html ├── snake.html ├── streamer-highlight.html ├── team-ranking.html ├── wall-frame.html └── wall-of-fame.html ├── rollup.config.js ├── server ├── config.js ├── db │ ├── index.js │ ├── migrations │ │ ├── 20201222053009-create-chat-message.js │ │ ├── 20201222124224-create-users.js │ │ ├── 20201228145932-remove-team.js │ │ └── 20201230103814-team-ranking.js │ ├── models │ │ ├── ChatMessage.js │ │ ├── TeamRanking.js │ │ └── Viewer.js │ ├── seeders │ │ ├── 20201222061628-old-chat-messages.js │ │ └── 20201222124532-old-users.js │ ├── tableFactory.js │ └── umzug.js ├── index.js ├── io.js ├── libs │ ├── chat.js │ ├── firebase.js │ ├── logs.js │ ├── removeDiacritics.js │ ├── stream.js │ ├── teamRanking.js │ ├── teams.js │ ├── users.js │ └── viewers.js ├── store │ ├── alias.js │ ├── clips.js │ ├── create.js │ ├── logs.js │ ├── pause-channels.js │ ├── poll.js │ ├── teams.js │ ├── treasure-chest.js │ ├── twitch.js │ ├── users.js │ └── wall-frame.js └── twitch │ ├── AuthProvider.js │ ├── Client.js │ ├── index.js │ └── plugins │ ├── cooldown.js │ ├── on-message │ ├── channel-points.js │ ├── chat.js │ ├── commands.js │ ├── commands │ │ ├── _say.js │ │ ├── _shazam.js │ │ ├── add-points.js │ │ ├── alias.js │ │ ├── blink.js │ │ ├── dl.js │ │ ├── error.js │ │ ├── frame.js │ │ ├── fv.js │ │ ├── gps.js │ │ ├── index.js │ │ ├── move.js │ │ ├── paillettes.js │ │ ├── pause.js │ │ ├── play.js │ │ ├── points.js │ │ ├── poll.js │ │ ├── reset-ranking.js │ │ ├── say.js │ │ ├── tts-output │ │ │ └── output.mp3 │ │ ├── tts.js │ │ └── waf.js │ ├── config │ │ ├── download.js │ │ ├── wall-of-fame.js │ │ └── welcomeSentences.js │ ├── faudrey_voir.js │ ├── poll.js │ ├── start-time.js │ ├── streamer-highlight.js │ ├── terminal-chat.js │ ├── timestamp.js │ ├── user-first-seen.js │ ├── user-rewards.js │ ├── utils │ │ └── index.js │ ├── viewer-badges.js │ ├── viewer-log.js │ ├── viewer-save.js │ └── wall-of-fame.js │ └── roles.js ├── spawnServer.js ├── tailwind ├── config.js └── style.css └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | es2021: true, 6 | node: true, 7 | }, 8 | plugins: ["svelte3", "prettier"], 9 | extends: ["eslint:recommended", "plugin:prettier/recommended"], 10 | overrides: [ 11 | { 12 | files: ["**/*.svelte"], 13 | processor: "svelte3/svelte3", 14 | }, 15 | ], 16 | parserOptions: { 17 | ecmaVersion: 12, 18 | sourceType: "module", 19 | }, 20 | rules: { 21 | "no-console": "warn", 22 | "no-debugger": "warn", 23 | }, 24 | globals: { 25 | Twitch: true, 26 | PerspectiveTransform: true, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/js/ 3 | tts-keys/ 4 | 5 | *_.* 6 | *_ 7 | 8 | .env.local 9 | .env.*.local -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "singleQuote": false, 6 | "trailingComma": "es5", 7 | "svelteStrictMode": true, 8 | "svelteBracketNewLine": true 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sébastien Mischler (skarab42) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SkaraBot - Twitch Bot 2 | 3 | WIP Projet développé en live sur la chaîne [twitch.tv/skarab42](https://www.twitch.tv/skarab42). 4 | 5 | # Project setup 6 | 7 | ```bash 8 | # Clone this repository 9 | git clone https://github.com/skarab42/skarabot 10 | 11 | # change directory to cloned path 12 | cd skarabot 13 | 14 | # install all dependencies 15 | yarn install 16 | 17 | # run application in development mode 18 | yarn watch 19 | 20 | # run application in production mode 21 | yarn start 22 | 23 | # build application in production mode 24 | yarn build 25 | 26 | # lint/prettify 27 | yarn lint 28 | ``` 29 | -------------------------------------------------------------------------------- /client/chat.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import App from "./chat/App.svelte"; 4 | 5 | new App({ target: document.body }); 6 | -------------------------------------------------------------------------------- /client/chat/App.svelte: -------------------------------------------------------------------------------- 1 | 39 | 40 | 48 | 49 | 50 | 51 | 55 | 56 |
57 | {#each messages as data (data.id)} 58 | 59 | {/each} 60 |
61 | -------------------------------------------------------------------------------- /client/chat/Avatar.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | {#if viewer.avatarURL} 12 |
13 |
14 |
15 | {/if} 16 | -------------------------------------------------------------------------------- /client/chat/Message.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 |
33 |
34 | 35 | 36 |
{name}
37 |
38 |
{time}
39 |
40 | 41 |
42 | -------------------------------------------------------------------------------- /client/chat/MessageWithEmotes.svelte: -------------------------------------------------------------------------------- 1 | 33 | 34 |
35 | {#each tokens as token} 36 | {#if token.type === 'emote'} 37 | {token.name} 38 | {:else} 39 |
{token.text}
40 | {/if} 41 | {/each} 42 |
43 | -------------------------------------------------------------------------------- /client/chat/Team.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | {#if svg} 17 |
18 | {@html svg} 19 |
20 | {/if} 21 | -------------------------------------------------------------------------------- /client/faudrey_voir.js: -------------------------------------------------------------------------------- 1 | // const { playSound } = require("./libs/sound"); 2 | const socket = require("socket.io-client")(); 3 | const glitch = require("./libs/glitch"); 4 | 5 | const $player = document.querySelector("#player"); 6 | 7 | function twitchClipPlayer(clip) { 8 | const url = "https://clips.twitch.tv/embed"; 9 | const $iframe = document.createElement("iframe"); 10 | $iframe.setAttribute( 11 | "src", 12 | `${url}?clip=${clip.id}?mute=false&autoplay=true&parent=localhost` 13 | ); 14 | $iframe.setAttribute("allowfullscreen", `false`); 15 | $iframe.setAttribute("frameborder", `0`); 16 | $iframe.setAttribute("scrolling", `no`); 17 | $iframe.setAttribute("width", `100%`); 18 | $iframe.setAttribute("height", `100%`); 19 | 20 | $player.innerHTML = ""; 21 | $player.append($iframe); 22 | $player.style.display = "block"; 23 | 24 | setTimeout(() => { 25 | glitch.stop(); 26 | }, 5000); 27 | 28 | setTimeout(() => { 29 | glitch.start(clip.channel); 30 | }, 12000); 31 | 32 | setTimeout(() => { 33 | $iframe && $iframe.remove(); 34 | document.body.style.display = "none"; 35 | $player.style.display = "block"; 36 | $player.innerHTML = ""; 37 | glitch.stop(); 38 | }, clip.duration * 1000); 39 | } 40 | 41 | function faudreyVoir({ name, clip }) { 42 | glitch.start(`0101 ${name} 01000101`); 43 | document.body.style.display = "block"; 44 | twitchClipPlayer(clip); 45 | } 46 | 47 | socket.on("faudrey_voir", faudreyVoir); 48 | // socket.on("play.sound", playSound); 49 | -------------------------------------------------------------------------------- /client/libs/counter.js: -------------------------------------------------------------------------------- 1 | const ms = require("ms"); 2 | 3 | const $counter = document.querySelector("#counter"); 4 | const $countdown = document.querySelector("#countdown"); 5 | 6 | let countdown = 0; 7 | let countdownId = null; 8 | 9 | exports.start = function (minutes, tick = null) { 10 | clearInterval(countdownId); 11 | countdown = minutes * 60 * 1000; 12 | $countdown.innerHTML = ms(countdown); 13 | $counter.style.display = "block"; 14 | countdownId = setInterval(() => { 15 | $countdown.innerHTML = ms(countdown); 16 | countdown -= 1000; 17 | if (countdown <= 0) { 18 | countdown += 42000; 19 | } 20 | tick && tick(); 21 | }, 1000); 22 | }; 23 | 24 | exports.stop = function () { 25 | clearInterval(countdownId); 26 | $counter.style.display = "none"; 27 | }; 28 | -------------------------------------------------------------------------------- /client/libs/glitch.js: -------------------------------------------------------------------------------- 1 | function getHeight() { 2 | return ( 3 | window.innerHeight || 4 | document.documentElement.clientHeight || 5 | document.body.clientHeight 6 | ); 7 | } 8 | 9 | function stop() { 10 | const root = document.querySelector("#root"); 11 | const text = document.querySelector("#text"); 12 | 13 | root.style.display = "none"; 14 | text.style.display = "none"; 15 | } 16 | 17 | const defaultMessage = "0110Intrus!on@#§¬"; 18 | 19 | function start(message = defaultMessage, timeout = 1500) { 20 | stop(); 21 | 22 | const root = document.querySelector("#root"); 23 | const text = document.querySelector("#text"); 24 | 25 | root.style.display = "block"; 26 | text.style.display = "block"; 27 | 28 | text.innerHTML = defaultMessage; 29 | text.dataset.text = defaultMessage; 30 | 31 | setTimeout(() => { 32 | text.innerHTML = message; 33 | text.dataset.text = message; 34 | }, timeout); 35 | 36 | setTimeout(() => { 37 | text.style.display = "none"; 38 | }, 5000); 39 | 40 | for (let i = 0; i < getHeight() / 10; i++) { 41 | const line = document.createElement("div"); 42 | line.className = `line line-${i}`; 43 | line.style.top = `${i * 10}px`; 44 | const time = Math.random() * 5; 45 | line.style.animation = `lines ${time}s infinite`; 46 | document.body.appendChild(line); 47 | } 48 | } 49 | 50 | module.exports = { 51 | start, 52 | stop, 53 | }; 54 | -------------------------------------------------------------------------------- /client/libs/player.js: -------------------------------------------------------------------------------- 1 | const { default: animejs } = require("animejs"); 2 | const socket = require("socket.io-client")(); 3 | const random = require("./random"); 4 | 5 | const playDuration = 30; // seconds 6 | const videoWidth = 600; 7 | const videoSize = { 8 | width: videoWidth, 9 | height: videoWidth / 1.777777, 10 | }; 11 | 12 | const $counter = document.querySelector("#counter"); 13 | const $video = document.querySelector("#video"); 14 | const $title = document.querySelector("#title"); 15 | 16 | let player; 17 | 18 | function anime(targets) { 19 | let top = window.innerHeight / 2 - videoSize.height / 2 - 40; 20 | 21 | if ($counter.style.display !== "none") { 22 | top += 42; 23 | } 24 | 25 | animejs({ 26 | targets, 27 | keyframes: [ 28 | { top, duration: 2000 }, 29 | { scale: 2, duration: 1000 }, 30 | ], 31 | }); 32 | } 33 | 34 | function showVideo(show = true) { 35 | $video.style.display = show ? "block" : "none"; 36 | $video.style.top = `-${videoSize.height}px`; 37 | $video.style.left = `${window.innerWidth / 2 - videoSize.width / 2}px`; 38 | show && anime($video); 39 | } 40 | 41 | function setTitle(user) { 42 | $title.innerHTML = user ? `${user.name} présente ...` : ""; 43 | } 44 | 45 | function removePlayer() { 46 | if (!player) return; 47 | showVideo(false); 48 | player._iframe.remove(); 49 | player = null; 50 | } 51 | 52 | function playAndDestroy(video, next) { 53 | const min = parseInt(video.duration * 0.25); 54 | const max = parseInt(video.duration * 0.75); 55 | const timestamp = parseInt(random(min, max)); 56 | 57 | player = new Twitch.Player("player", { 58 | autoplay: false, 59 | video: video.id, 60 | ...videoSize, 61 | mute: true, 62 | }); 63 | 64 | player.addEventListener(Twitch.Player.PLAYING, () => { 65 | setTitle(video.user); 66 | showVideo(true); 67 | setTimeout(() => { 68 | socket.emit("video-play", video); 69 | }, playDuration / 2); 70 | setTimeout(() => { 71 | removePlayer(); 72 | next(); 73 | }, playDuration * 1000); 74 | }); 75 | 76 | player.addEventListener(Twitch.Player.READY, () => { 77 | player.seek(timestamp); 78 | player.setVolume(0); 79 | player.play(); 80 | }); 81 | } 82 | 83 | module.exports = { 84 | play: playAndDestroy, 85 | remove: removePlayer, 86 | }; 87 | -------------------------------------------------------------------------------- /client/libs/random.js: -------------------------------------------------------------------------------- 1 | module.exports = function random(min, max) { 2 | return Math.random() * (max - min) + min; 3 | }; 4 | -------------------------------------------------------------------------------- /client/libs/shuffle.js: -------------------------------------------------------------------------------- 1 | module.exports = function shuffle(o) { 2 | for ( 3 | var j, x, i = o.length; 4 | i; 5 | j = parseInt(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x 6 | ); 7 | return o; 8 | }; 9 | -------------------------------------------------------------------------------- /client/libs/sound.js: -------------------------------------------------------------------------------- 1 | function playSound({ file, volume = 0.5 } = {}) { 2 | const audio = new Audio(`download/${file}`); 3 | audio.volume = volume; 4 | audio.play(); 5 | } 6 | 7 | module.exports = { 8 | playSound, 9 | }; 10 | -------------------------------------------------------------------------------- /client/poll.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import App from "./poll/App.svelte"; 4 | 5 | new App({ target: document.body }); 6 | -------------------------------------------------------------------------------- /client/poll/App.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/poll/Item.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
9 |
{item.name}
10 |
11 | {item.percent}% 12 |
13 |
14 | 19 |
20 | -------------------------------------------------------------------------------- /client/poll/Items.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 | {#each items as item} 9 | 10 | {/each} 11 |
12 | -------------------------------------------------------------------------------- /client/poll/Paillettes.svelte: -------------------------------------------------------------------------------- 1 | 64 | 65 |
66 | -------------------------------------------------------------------------------- /client/poll/boom.js: -------------------------------------------------------------------------------- 1 | import confetti from "canvas-confetti"; 2 | 3 | export default function boom(currentName) { 4 | const canvas = document.getElementById(`canvas-${currentName}`); 5 | const myConfetti = confetti.create(canvas); 6 | 7 | myConfetti({ 8 | particleCount: 500, 9 | startVelocity: 15, 10 | spread: 75, 11 | angle: 60, 12 | origin: { 13 | x: 0, 14 | y: 1, 15 | }, 16 | }); 17 | myConfetti({ 18 | particleCount: 500, 19 | startVelocity: 15, 20 | spread: 50, 21 | angle: 90, 22 | origin: { 23 | x: 0.5, 24 | y: 1, 25 | }, 26 | }); 27 | myConfetti({ 28 | particleCount: 500, 29 | startVelocity: 15, 30 | spread: 75, 31 | angle: 120, 32 | origin: { 33 | x: 1, 34 | y: 1, 35 | }, 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /client/snake.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import App from "./snake/App.svelte"; 4 | 5 | new App({ target: document.body }); 6 | -------------------------------------------------------------------------------- /client/streamer-highlight.js: -------------------------------------------------------------------------------- 1 | const socket = require("socket.io-client")(); 2 | const shuffle = require("./libs/shuffle"); 3 | const counter = require("./libs/counter"); 4 | const player = require("./libs/player"); 5 | 6 | let queue = []; 7 | let lock = false; 8 | 9 | function showOverlay(show = true) { 10 | document.body.classList[show ? "add" : "remove"]("overlay"); 11 | } 12 | 13 | function clearQueue() { 14 | queue = []; 15 | } 16 | 17 | function next() { 18 | lock = false; 19 | processQueue(); 20 | } 21 | 22 | function processQueue() { 23 | showOverlay(queue.length); 24 | 25 | if (!queue.length || lock) return; 26 | lock = true; 27 | 28 | player.play(queue.shift(), next); 29 | } 30 | 31 | function videoPush(video) { 32 | queue.push(video); 33 | processQueue(); 34 | } 35 | 36 | function pauseStart({ minutes, videos }) { 37 | videos.forEach(videoPush); 38 | showOverlay(true); 39 | counter.start(minutes, () => { 40 | !queue.length && shuffle(videos).forEach(videoPush); 41 | }); 42 | } 43 | 44 | function pauseStop() { 45 | showOverlay(false); 46 | player.remove(); 47 | counter.stop(); 48 | clearQueue(); 49 | } 50 | 51 | socket.on("video.push", videoPush); 52 | socket.on("pause.start", pauseStart); 53 | socket.on("pause.stop", pauseStop); 54 | -------------------------------------------------------------------------------- /client/team-ranking.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import App from "./team-ranking/App.svelte"; 4 | 5 | new App({ target: document.body }); 6 | -------------------------------------------------------------------------------- /client/team-ranking/App.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 | 44 | 45 | 61 | 62 | 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "skarabot", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "node --trace-warnings server", 7 | "watch": "rollup -c -w", 8 | "build": "yarn tailwind && rollup -c", 9 | "tailwind": "npx tailwindcss -c ./tailwind/config.js -i ./tailwind/style.css -o ./public/css/tailwind.css", 10 | "prettify": "prettier --write ./server ./client", 11 | "lint": "eslint --fix ./server" 12 | }, 13 | "dependencies": { 14 | "@google-cloud/text-to-speech": "^3.3.1", 15 | "PerspectiveTransform.js": "https://github.com/edankwan/PerspectiveTransform.js", 16 | "animejs": "^3.2.1", 17 | "axios": "^0.21.1", 18 | "chalk": "^4.1.0", 19 | "conf": "^7.1.2", 20 | "env-paths": "^2.2.0", 21 | "fs-extra": "^9.0.1", 22 | "js-video-url-parser": "^0.4.3", 23 | "localtunnel": "^2.0.0", 24 | "ms": "^2.1.2", 25 | "obs-websocket-js": "^4.0.2", 26 | "open": "^7.3.0", 27 | "polka": "^0.5.2", 28 | "say": "^0.16.0", 29 | "sequelize": "^6.3.5", 30 | "sirv": "^1.0.6", 31 | "socket.io": "^2.3.0", 32 | "sound-play": "^1.1.0", 33 | "soundex": "^0.2.1", 34 | "sqlite3": "^5.0.0", 35 | "twitch": "^4.3.6", 36 | "twitch-auth": "^4.3.6", 37 | "twitch-chat-client": "^4.3.6", 38 | "umzug": "^3.0.0-beta.14", 39 | "uuid": "^8.3.1", 40 | "voice-recognition": "^1.0.6", 41 | "youtube-player": "^5.5.2" 42 | }, 43 | "devDependencies": { 44 | "@rollup/plugin-commonjs": "^15.1.0", 45 | "@rollup/plugin-node-resolve": "^9.0.0", 46 | "autoprefixer": "^10.3.1", 47 | "canvas-confetti": "^1.3.2", 48 | "chokidar": "^3.4.2", 49 | "eslint": "^7.16.0", 50 | "eslint-config-prettier": "^7.1.0", 51 | "eslint-plugin-prettier": "^3.3.0", 52 | "eslint-plugin-svelte3": "^3.0.0", 53 | "postcss": "^8.3.6", 54 | "prettier": "^2.2.1", 55 | "prettier-plugin-svelte": "^1.4.2", 56 | "rollup": "^2.29.0", 57 | "rollup-plugin-cleaner": "^1.0.0", 58 | "rollup-plugin-livereload": "^2.0.0", 59 | "rollup-plugin-svelte": "^6.1.1", 60 | "rollup-plugin-terser": "^7.0.2", 61 | "sequelize-cli": "^6.2.0", 62 | "svelte": "^3.29.7", 63 | "svelte-icons": "^2.1.0", 64 | "tailwindcss": "^2.2.7" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /public/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Chat - skarabot 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/images/crown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skarab42/skarabot/ff9872b4f1e3124ec63951e942d9a55053d612f3/public/images/crown.png -------------------------------------------------------------------------------- /public/images/skarab42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skarab42/skarabot/ff9872b4f1e3124ec63951e942d9a55053d612f3/public/images/skarab42.png -------------------------------------------------------------------------------- /public/images/teams/amazonwebservices.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/android.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/angularjs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/appcelerator.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/apple.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/atom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/backbonejs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/behance.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/bitbucket.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/bootstrap.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/bower.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/c.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/cakephp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/ceylon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/chauve.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/chrome.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/codeigniter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/coffeescript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/confluence.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/couchdb.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/cplusplus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/csharp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/css3.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/cucumber.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/d3js.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/dart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/devicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/django.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/docker.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/doctrine.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/dot-net.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/drupal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/electron.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/elm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/ember.svg: -------------------------------------------------------------------------------- 1 | ® -------------------------------------------------------------------------------- /public/images/teams/erlang.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/express.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/facebook.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/firefox.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/foundation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/gatling.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/gimp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/git.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/gitlab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/go.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/google.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/gradle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/grunt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/gulp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/handlebars.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/heroku.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/html5.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/ie10.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/illustrator.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/inkscape.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/intellij.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/ionic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/jasmine.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/java.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/javascript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/jeet.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/jetbrains.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/jquery.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/js.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/kotlin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/laravel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/lerobot.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/less.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/linkedin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/linux.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/meteor.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/mocha.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/mongodb.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/moodle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/mysql.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/nginx.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/node-red.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/nodejs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/nodered.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/nodewebkit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/npm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/ocaml.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/oracle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/photoshop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/php.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/phpstorm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/protractor.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/prout.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/pycharm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/python.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/rails.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/redhat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/redis.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/ruby.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/rubymine.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/rust.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/scribe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/sequelize.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/sketch.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/slack.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/sourcetree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/ssh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/stackoverflow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/stylus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/swift.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/symfony.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/tomcat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/travis.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/trello.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/typescript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/ubuntu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/unity.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/vagrant.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/vim.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/visualstudio.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/vuejs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/webpack.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/webstorm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/windows8.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/wordpress.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/yarn.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/yii.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/teams/zend.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/poll.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Poll - skarabot 7 | 8 | 9 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/snake.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | snake - skarabot 7 | 8 | 9 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /public/streamer-highlight.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Streamer Highlight - skarabot 6 | 42 | 43 | 44 |
45 |
46 |
47 |
48 |
49 | Les fleurs en bouquet fanent mais jamais ne renaissent
50 | 51 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /public/team-ranking.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Team Ranking - skarabot 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/wall-frame.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Wall Frame - skarabot 6 | 28 | 29 | 30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/wall-of-fame.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Wall Of Fame - skarabot 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // import livereload from "rollup-plugin-livereload"; 2 | import resolve from "@rollup/plugin-node-resolve"; 3 | import commonjs from "@rollup/plugin-commonjs"; 4 | import { terser } from "rollup-plugin-terser"; 5 | import cleaner from "rollup-plugin-cleaner"; 6 | import svelte from "rollup-plugin-svelte"; 7 | import spawnServer from "./spawnServer"; 8 | import path from "path"; 9 | import fs from "fs"; 10 | 11 | const watch = process.env.ROLLUP_WATCH; 12 | 13 | const inputDir = "client"; 14 | const outputDir = "public"; 15 | const serverPath = "server/index.js"; 16 | 17 | const input = []; 18 | 19 | const inputPath = path.resolve(__dirname, inputDir); 20 | 21 | fs.readdirSync(inputPath).forEach((file) => { 22 | if (!fs.lstatSync(path.join(inputPath, file)).isDirectory()) { 23 | input.push(`${inputDir}/${file}`); 24 | } 25 | }); 26 | 27 | export default { 28 | input, 29 | output: { 30 | format: "es", 31 | sourcemap: true, 32 | dir: `${outputDir}/js/`, 33 | }, 34 | plugins: [ 35 | commonjs(), 36 | resolve({ browser: true, dedupe: ["svelte"] }), 37 | svelte({ dev: watch }), 38 | // watch && livereload(outputDir), 39 | watch && spawnServer(serverPath), 40 | !watch && cleaner({ targets: [`${outputDir}/js/`] }), 41 | !watch && terser(), 42 | ], 43 | }; 44 | -------------------------------------------------------------------------------- /server/config.js: -------------------------------------------------------------------------------- 1 | const { name } = require("../package"); 2 | const envPaths = require("env-paths"); 3 | const path = require("path"); 4 | 5 | const port = 4224; 6 | const host = "localhost"; 7 | const address = `${host}:${port}`; 8 | const userDir = envPaths(name).data; 9 | 10 | const watch = process.argv.includes("--watch") || process.argv.includes("-w"); 11 | 12 | module.exports = { 13 | watch, 14 | server: { 15 | host, 16 | port, 17 | address, 18 | userDir, 19 | publicPath: "public", 20 | }, 21 | db: { 22 | dialect: "sqlite", 23 | storage: path.join(userDir, "db.sqlite"), 24 | logging: false, // watch ? console.log : false, 25 | }, 26 | twitch: { 27 | clientId: "i1zqws5ibrt0vdaiuenhcpoe4t5rnb", 28 | redirectURI: `http://${address}/token`, 29 | forceVerify: false, 30 | channels: ["skarab42"], 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /server/db/index.js: -------------------------------------------------------------------------------- 1 | const { Sequelize } = require("sequelize"); 2 | const { db } = require("../config"); 3 | 4 | module.exports = new Sequelize(db); 5 | -------------------------------------------------------------------------------- /server/db/migrations/20201222053009-create-chat-message.js: -------------------------------------------------------------------------------- 1 | const { Sequelize } = require("sequelize"); 2 | const tableFactory = require("../tableFactory"); 3 | 4 | const chatMessagesTable = tableFactory({ 5 | time: { 6 | type: Sequelize.DATE, 7 | allowNull: false, 8 | }, 9 | viewerId: { 10 | type: Sequelize.INTEGER, 11 | allowNull: false, 12 | }, 13 | message: { 14 | type: Sequelize.TEXT, 15 | allowNull: false, 16 | }, 17 | }); 18 | 19 | module.exports = { 20 | up: async ({ context }) => { 21 | await context.createTable("ChatMessages", chatMessagesTable); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /server/db/migrations/20201222124224-create-users.js: -------------------------------------------------------------------------------- 1 | const { Sequelize } = require("sequelize"); 2 | const tableFactory = require("../tableFactory"); 3 | 4 | const viewersTable = tableFactory({ 5 | name: { 6 | type: Sequelize.STRING, 7 | allowNull: false, 8 | unique: true, 9 | }, 10 | avatarURL: { 11 | type: Sequelize.STRING, 12 | allowNull: true, 13 | }, 14 | team: { 15 | type: Sequelize.STRING, 16 | allowNull: true, 17 | }, 18 | teamColor: { 19 | type: Sequelize.STRING, 20 | allowNull: true, 21 | }, 22 | badges: { 23 | type: Sequelize.STRING, 24 | allowNull: false, 25 | defaultValue: "{}", 26 | }, 27 | messageCount: { 28 | type: Sequelize.INTEGER, 29 | allowNull: false, 30 | defaultValue: 0, 31 | }, 32 | viewCount: { 33 | type: Sequelize.INTEGER, 34 | allowNull: false, 35 | defaultValue: 0, 36 | }, 37 | points: { 38 | type: Sequelize.INTEGER, 39 | allowNull: false, 40 | defaultValue: 0, 41 | }, 42 | position: { 43 | type: Sequelize.STRING, 44 | allowNull: false, 45 | defaultValue: '{"x":0,"y":0}', 46 | }, 47 | lastHighlight: { 48 | type: Sequelize.DATE, 49 | allowNull: true, 50 | }, 51 | }); 52 | 53 | module.exports = { 54 | up: async ({ context }) => { 55 | await context.createTable("Viewers", viewersTable); 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /server/db/migrations/20201228145932-remove-team.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async ({ context }) => { 5 | await context.removeColumn("Viewers", "team"); 6 | await context.removeColumn("Viewers", "teamColor"); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /server/db/migrations/20201230103814-team-ranking.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { Sequelize } = require("sequelize"); 4 | const tableFactory = require("../tableFactory"); 5 | 6 | const teamRankingTable = tableFactory({ 7 | team: { 8 | type: Sequelize.STRING, 9 | allowNull: false, 10 | }, 11 | messageCount: { 12 | type: Sequelize.INTEGER, 13 | allowNull: false, 14 | }, 15 | totalMessageCount: { 16 | type: Sequelize.INTEGER, 17 | allowNull: false, 18 | }, 19 | }); 20 | 21 | module.exports = { 22 | up: async ({ context }) => { 23 | await context.createTable("TeamRanking", teamRankingTable); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /server/db/models/ChatMessage.js: -------------------------------------------------------------------------------- 1 | const { DataTypes } = require("sequelize"); 2 | const sequelize = require("../index"); 3 | const Viewer = require("./Viewer"); 4 | 5 | const ChatMessage = sequelize.define("ChatMessage", { 6 | time: { 7 | type: DataTypes.DATE, 8 | allowNull: false, 9 | }, 10 | viewerId: { 11 | type: DataTypes.INTEGER, 12 | allowNull: false, 13 | }, 14 | message: { 15 | type: DataTypes.TEXT, 16 | allowNull: false, 17 | }, 18 | }); 19 | 20 | Viewer.hasMany(ChatMessage); 21 | ChatMessage.belongsTo(Viewer, { as: "viewer" }); 22 | 23 | module.exports = ChatMessage; 24 | -------------------------------------------------------------------------------- /server/db/models/TeamRanking.js: -------------------------------------------------------------------------------- 1 | const { DataTypes } = require("sequelize"); 2 | const sequelize = require("../index"); 3 | 4 | const TeamRanking = sequelize.define( 5 | "TeamRanking", 6 | { 7 | team: { 8 | type: DataTypes.STRING, 9 | allowNull: false, 10 | }, 11 | messageCount: { 12 | type: DataTypes.INTEGER, 13 | allowNull: false, 14 | }, 15 | totalMessageCount: { 16 | type: DataTypes.INTEGER, 17 | allowNull: false, 18 | }, 19 | }, 20 | { 21 | freezeTableName: true, 22 | } 23 | ); 24 | 25 | module.exports = TeamRanking; 26 | -------------------------------------------------------------------------------- /server/db/models/Viewer.js: -------------------------------------------------------------------------------- 1 | const { DataTypes } = require("sequelize"); 2 | const sequelize = require("../index"); 3 | 4 | const Viewer = sequelize.define("Viewer", { 5 | name: { 6 | type: DataTypes.STRING, 7 | allowNull: false, 8 | unique: true, 9 | }, 10 | avatarURL: { 11 | type: DataTypes.STRING, 12 | allowNull: true, 13 | }, 14 | badges: { 15 | type: DataTypes.STRING, 16 | allowNull: false, 17 | defaultValue: "{}", 18 | get() { 19 | return JSON.parse(this.getDataValue("badges")); 20 | }, 21 | set(badges) { 22 | this.setDataValue("badges", JSON.stringify(badges)); 23 | }, 24 | }, 25 | messageCount: { 26 | type: DataTypes.INTEGER, 27 | allowNull: false, 28 | defaultValue: 0, 29 | }, 30 | viewCount: { 31 | type: DataTypes.INTEGER, 32 | allowNull: false, 33 | defaultValue: 0, 34 | }, 35 | points: { 36 | type: DataTypes.INTEGER, 37 | allowNull: false, 38 | defaultValue: 0, 39 | }, 40 | position: { 41 | type: DataTypes.STRING, 42 | allowNull: false, 43 | defaultValue: '{"x":0,"y":0}', 44 | get() { 45 | return JSON.parse(this.getDataValue("position")); 46 | }, 47 | set(position) { 48 | this.setDataValue("position", JSON.stringify(position)); 49 | }, 50 | }, 51 | lastHighlight: { 52 | type: DataTypes.DATE, 53 | allowNull: true, 54 | }, 55 | }); 56 | 57 | module.exports = Viewer; 58 | -------------------------------------------------------------------------------- /server/db/seeders/20201222061628-old-chat-messages.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { computeMessage } = require("../../libs/chat"); 4 | 5 | const chatMessages = []; 6 | 7 | try { 8 | require.resolve("../../store/logs"); 9 | const logs = require("../../libs/logs"); 10 | const now = new Date(); 11 | 12 | logs.getAll().forEach(({ time, data }) => { 13 | chatMessages.push({ 14 | viewerId: parseInt(data.userId), 15 | time: new Date(time), 16 | message: computeMessage(data.emotes), 17 | createdAt: now, 18 | updatedAt: now, 19 | }); 20 | }); 21 | } catch (error) { 22 | // console.log(">>> ERROR >>>", error.message); 23 | } 24 | 25 | module.exports = { 26 | up: async ({ context }) => { 27 | if (chatMessages.length) { 28 | await context.bulkInsert("ChatMessages", chatMessages); 29 | } 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /server/db/seeders/20201222124532-old-users.js: -------------------------------------------------------------------------------- 1 | const viewers = []; 2 | 3 | try { 4 | require.resolve("../../store/users"); 5 | const users = require("../../libs/users"); 6 | const baseURL = "https://static-cdn.jtvnw.net/jtv_user_pictures/"; 7 | 8 | Object.values(users.getAll()).forEach((user) => { 9 | const avatarURL = user.avatarURL && user.avatarURL.replace(baseURL, ""); 10 | 11 | viewers.push({ 12 | id: parseInt(user.id), 13 | name: user.name, 14 | avatarURL, 15 | team: user.team || null, 16 | teamColor: user.color || null, 17 | badges: "{}", 18 | messageCount: parseInt(user.messageCount), 19 | viewCount: parseInt(user.viewCount), 20 | points: parseInt(user.points), 21 | position: JSON.stringify({ 22 | x: parseInt(user.position.x), 23 | y: parseInt(user.position.y), 24 | }), 25 | lastHighlight: new Date(user.lastHighlight), 26 | createdAt: new Date(user.firstSeen), 27 | updatedAt: new Date(user.lastSeen), 28 | }); 29 | }); 30 | } catch (error) { 31 | // console.log(">>> ERROR >>>", error.message); 32 | } 33 | 34 | module.exports = { 35 | up: async ({ context }) => { 36 | if (viewers.length) { 37 | await context.bulkInsert("Viewers", viewers); 38 | } 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /server/db/tableFactory.js: -------------------------------------------------------------------------------- 1 | const { Sequelize } = require("sequelize"); 2 | 3 | function tableFactory(table) { 4 | return { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | ...table, 11 | createdAt: { 12 | type: Sequelize.DATE, 13 | defaultValue: Sequelize.NOW, 14 | }, 15 | updatedAt: { 16 | type: Sequelize.DATE, 17 | defaultValue: Sequelize.NOW, 18 | }, 19 | }; 20 | } 21 | 22 | module.exports = tableFactory; 23 | -------------------------------------------------------------------------------- /server/db/umzug.js: -------------------------------------------------------------------------------- 1 | const { Umzug, SequelizeStorage } = require("umzug"); 2 | const { watch } = require("../config"); 3 | const sequelize = require("./index"); 4 | 5 | function factory(pattern, modelName) { 6 | return new Umzug({ 7 | context: sequelize.getQueryInterface(), 8 | migrations: { glob: [pattern, { cwd: __dirname }] }, 9 | storage: new SequelizeStorage({ sequelize, modelName }), 10 | logger: watch ? console : undefined, 11 | }); 12 | } 13 | 14 | const migrations = factory("migrations/*.js", "SequelizeMigrations"); 15 | const seeders = factory("seeders/*.js", "SequelizeSeeders"); 16 | 17 | module.exports = { 18 | async up() { 19 | await migrations.up(); 20 | await seeders.up(); 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const { twitchClient, twitchAuth } = require("./twitch"); 2 | const { name, version } = require("../package.json"); 3 | const OBSWebSocket = require("obs-websocket-js"); 4 | const firebase = require("./libs/firebase"); 5 | const umzug = require("./db/umzug"); 6 | const config = require("./config"); 7 | const socketIO = require("./io"); 8 | const polka = require("polka"); 9 | const sirv = require("sirv"); 10 | 11 | const userServ = sirv(config.server.userDir, { dev: true }); 12 | const publicServ = sirv(config.server.publicPath, { dev: true }); 13 | 14 | (async () => { 15 | await umzug.up(); 16 | 17 | const { server } = polka() 18 | .use(userServ) 19 | .use(publicServ) 20 | .use(twitchAuth) 21 | .get("/teamChange", firebase.onTeamChange) 22 | .listen(config.server.port, async (err) => { 23 | if (err) throw err; 24 | /* eslint-disable no-console */ 25 | console.log(`> ${name} v${version}`); 26 | console.log(`> running on ${config.server.address}`); 27 | /* eslint-enable no-console */ 28 | }); 29 | 30 | const obs = new OBSWebSocket(); 31 | const socket = socketIO({ server, twitchClient }); 32 | 33 | obs.connect({ address: "localhost:4444" }); 34 | 35 | twitchClient 36 | .setSocketIO(socket) 37 | .onMessage(require("./twitch/plugins/on-message/timestamp")) 38 | .onMessage(require("./twitch/plugins/on-message/start-time")) 39 | .onMessage(require("./twitch/plugins/on-message/viewer-log")) 40 | .onMessage(require("./twitch/plugins/on-message/viewer-badges")) 41 | .onMessage(require("./twitch/plugins/on-message/user-first-seen")) 42 | .onMessage(require("./twitch/plugins/on-message/user-rewards")) 43 | .onMessage(require("./twitch/plugins/on-message/terminal-chat")) 44 | .onMessage(require("./twitch/plugins/on-message/chat")) 45 | .onMessage(require("./twitch/plugins/on-message/poll")) 46 | // .onMessage(require("./twitch/plugins/on-message/wall-of-fame")) 47 | .onMessage(require("./twitch/plugins/on-message/streamer-highlight")) 48 | .onMessage(require("./twitch/plugins/on-message/faudrey_voir")) 49 | .onMessage(require("./twitch/plugins/on-message/commands")) 50 | .onMessage(require("./twitch/plugins/on-message/channel-points")) 51 | .onMessage(require("./twitch/plugins/on-message/viewer-save")); 52 | })(); 53 | -------------------------------------------------------------------------------- /server/libs/chat.js: -------------------------------------------------------------------------------- 1 | const ChatMessage = require("../db/models/ChatMessage"); 2 | const Viewer = require("../db/models/Viewer"); 3 | 4 | async function addMessage(message) { 5 | const messageModel = await ChatMessage.create(message); 6 | return getMessageById(messageModel.id); 7 | } 8 | 9 | function computeMessage(emotes) { 10 | return emotes 11 | .map((emote) => { 12 | if (emote.type === "emote") { 13 | return `[:emote|${emote.id}|${emote.name}]`; 14 | } 15 | return emote.text; 16 | }) 17 | .join(""); 18 | } 19 | 20 | async function getMessageById(id) { 21 | return await ChatMessage.findOne({ 22 | where: { id }, 23 | include: [{ model: Viewer, as: "viewer" }], 24 | }); 25 | } 26 | 27 | async function getLastMessages({ limit = 10 } = {}) { 28 | const messages = await ChatMessage.findAll({ 29 | include: [{ model: Viewer, as: "viewer" }], 30 | order: [["time", "DESC"]], 31 | limit, 32 | }); 33 | // const userIds = []; 34 | return messages.map((message) => message.toJSON()); 35 | } 36 | 37 | module.exports = { 38 | addMessage, 39 | computeMessage, 40 | getLastMessages, 41 | }; 42 | -------------------------------------------------------------------------------- /server/libs/firebase.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | 3 | let fetchedViewers = {}; 4 | 5 | const dev = false; 6 | const funcURL = dev 7 | ? "http://localhost:5001/skara-bot/us-central1" 8 | : "https://us-central1-skara-bot.cloudfunctions.net"; 9 | 10 | async function fetchViewers(ids) { 11 | const url = `${funcURL}/getViewers`; 12 | const res = await axios(url, { method: "POST", data: ids }); 13 | fetchedViewers = { ...fetchedViewers, ...res.data }; 14 | return res.data; 15 | } 16 | 17 | async function fetchViewer(id, force = false) { 18 | if (!force && fetchedViewers[id]) { 19 | return fetchedViewers[id]; 20 | } 21 | const viewers = await fetchViewers([id]); 22 | return viewers[id]; 23 | } 24 | 25 | async function onTeamChange(req) { 26 | const io = require("../io")(); 27 | const id = req.query.id; 28 | const viewer = await fetchViewer(id, true); 29 | io.emit("viewer.teamChange", { id, ...viewer }); 30 | } 31 | 32 | module.exports = { 33 | onTeamChange, 34 | fetchViewers, 35 | fetchViewer, 36 | fetchedViewers, 37 | }; 38 | -------------------------------------------------------------------------------- /server/libs/logs.js: -------------------------------------------------------------------------------- 1 | const logsStore = require("../store/logs"); 2 | const users = require("../libs/users"); 3 | const { v4: uuid } = require("uuid"); 4 | 5 | function create(type, data) { 6 | return { id: uuid(), time: Date.now(), type, data }; 7 | } 8 | 9 | function getAll() { 10 | return logsStore.get("logs", []).map((log) => { 11 | return { ...log, user: users.get(log.data.userId) }; 12 | }); 13 | } 14 | 15 | function set(logs) { 16 | logsStore.set( 17 | `logs`, 18 | logs.map((log) => { 19 | let logCopy = { ...log }; 20 | delete logCopy.user; 21 | return logCopy; 22 | }) 23 | ); 24 | } 25 | 26 | function add(type, data) { 27 | const logs = getAll(); 28 | const log = create(type, data); 29 | logs.push(log); 30 | set(logs); 31 | return log; 32 | } 33 | 34 | function del(id) { 35 | const logs = getAll().filter((l) => l.id !== id); 36 | logsStore.set(`logs`, logs); 37 | return logs; 38 | } 39 | 40 | module.exports = { 41 | create, 42 | add, 43 | getAll, 44 | delete: del, 45 | }; 46 | -------------------------------------------------------------------------------- /server/libs/stream.js: -------------------------------------------------------------------------------- 1 | const { twitchClient } = require("../twitch"); 2 | const { twitch } = require("../config"); 3 | 4 | let startTime = 0; 5 | 6 | async function getStartTime() { 7 | if (startTime) return startTime; 8 | 9 | const channel = twitch.channels[0]; 10 | const stream = await twitchClient.api.helix.streams.getStreamByUserName( 11 | channel 12 | ); 13 | 14 | if (stream) { 15 | const startedAt = stream._data["started_at"]; 16 | const gmt = Math.abs(new Date().getTimezoneOffset()) * 60; 17 | startTime = new Date(startedAt).getTime() + gmt; 18 | } 19 | 20 | return startTime; 21 | } 22 | 23 | module.exports = { 24 | getStartTime, 25 | }; 26 | -------------------------------------------------------------------------------- /server/libs/teamRanking.js: -------------------------------------------------------------------------------- 1 | const TeamRanking = require("../db/models/TeamRanking"); 2 | const { Op } = require("sequelize"); 3 | 4 | async function addTeam(team) { 5 | return await TeamRanking.create(team); 6 | } 7 | 8 | async function getTeamByName(team) { 9 | return await TeamRanking.findOne({ where: { team } }); 10 | } 11 | 12 | async function getRanking({ limit = 5, order = "DESC" } = {}) { 13 | return await TeamRanking.findAll({ 14 | where: { messageCount: { [Op.gt]: 0 } }, 15 | order: [["messageCount", order]], 16 | limit, 17 | }); 18 | } 19 | 20 | async function resetRanking() { 21 | return await TeamRanking.update( 22 | { 23 | messageCount: 0, 24 | }, 25 | { 26 | fields: ["messageCount"], 27 | where: { messageCount: { [Op.gt]: 0 } }, 28 | } 29 | ); 30 | } 31 | 32 | module.exports = { 33 | addTeam, 34 | getTeamByName, 35 | getRanking, 36 | resetRanking, 37 | }; 38 | -------------------------------------------------------------------------------- /server/libs/teams.js: -------------------------------------------------------------------------------- 1 | const teamsStore = require("../store/teams"); 2 | 3 | function create(team) { 4 | return { 5 | name: null, 6 | icon: null, 7 | ...team, 8 | }; 9 | } 10 | 11 | function set(name, team) { 12 | teamsStore.set(`list.${name}`, team); 13 | return team; 14 | } 15 | 16 | function get(name, defaultteam = null) { 17 | return teamsStore.get(`list.${name}`, defaultteam); 18 | } 19 | 20 | function update(newteam) { 21 | const team = { ...get(newteam.name), ...newteam }; 22 | set(team.name, team); 23 | return team; 24 | } 25 | 26 | function del(name) { 27 | teamsStore.delete(`list.${name}`); 28 | } 29 | 30 | function getAll() { 31 | return teamsStore.get("list", {}); 32 | } 33 | 34 | module.exports = { 35 | create, 36 | set, 37 | get, 38 | getAll, 39 | update, 40 | delete: del, 41 | }; 42 | -------------------------------------------------------------------------------- /server/libs/users.js: -------------------------------------------------------------------------------- 1 | const usersStore = require("../store/users"); 2 | 3 | function create(user) { 4 | return { 5 | id: null, 6 | name: null, 7 | avatarURL: null, 8 | firstSeen: 0, 9 | lastSeen: 0, 10 | lastHighlight: 0, 11 | viewCount: 0, 12 | messageCount: 1, 13 | points: 0, 14 | position: { x: 0, y: 0 }, 15 | ...user, 16 | }; 17 | } 18 | 19 | function set(id, user) { 20 | usersStore.set(`list.${id}`, user); 21 | return user; 22 | } 23 | 24 | function get(id, defaultUser = null) { 25 | return usersStore.get(`list.${id}`, defaultUser); 26 | } 27 | 28 | function getByName(name) { 29 | return Object.values(getAll()).find( 30 | (user) => user.name.toLowerCase() === name.toLowerCase() 31 | ); 32 | } 33 | 34 | function update(newUser) { 35 | const user = { ...get(newUser.id), ...newUser }; 36 | set(user.id, user); 37 | return user; 38 | } 39 | 40 | function del(id) { 41 | usersStore.delete(`list.${id}`); 42 | } 43 | 44 | function getAll() { 45 | return usersStore.get("list", {}); 46 | } 47 | 48 | module.exports = { 49 | create, 50 | set, 51 | get, 52 | getAll, 53 | update, 54 | getByName, 55 | delete: del, 56 | }; 57 | -------------------------------------------------------------------------------- /server/libs/viewers.js: -------------------------------------------------------------------------------- 1 | const Viewer = require("../db/models/Viewer"); 2 | const { Op } = require("sequelize"); 3 | 4 | async function addViewer(viewer) { 5 | return await Viewer.create(viewer); 6 | } 7 | 8 | async function updateOrAddViewer(viewer) { 9 | return await Viewer.upsert(viewer); 10 | } 11 | 12 | async function getViewerById(id) { 13 | return await Viewer.findOne({ where: { id } }); 14 | } 15 | 16 | async function getViewerByName(name) { 17 | return await Viewer.findOne({ where: { name } }); 18 | } 19 | 20 | async function updateViewer(viewer) { 21 | await Viewer.update(viewer, { where: { id: viewer.id } }); 22 | return await getViewerById(viewer.id); 23 | } 24 | 25 | async function getFamouseViewers({ limit = 100 } = {}) { 26 | const viewers = await Viewer.findAll({ 27 | limit, 28 | where: { avatarURL: { [Op.ne]: null } }, 29 | order: [["updatedAt", "DESC"]], 30 | }); 31 | return viewers.map((viewer) => viewer.toJSON()); 32 | } 33 | 34 | module.exports = { 35 | addViewer, 36 | updateViewer, 37 | getViewerById, 38 | updateOrAddViewer, 39 | getFamouseViewers, 40 | getViewerByName, 41 | }; 42 | -------------------------------------------------------------------------------- /server/store/alias.js: -------------------------------------------------------------------------------- 1 | const create = require("./create"); 2 | 3 | const defaults = { 4 | list: {}, 5 | }; 6 | 7 | module.exports = create({ name: "alias", defaults }); 8 | -------------------------------------------------------------------------------- /server/store/clips.js: -------------------------------------------------------------------------------- 1 | const create = require("./create"); 2 | 3 | const defaults = { 4 | list: {}, 5 | }; 6 | 7 | module.exports = create({ name: "clips", defaults }); 8 | -------------------------------------------------------------------------------- /server/store/create.js: -------------------------------------------------------------------------------- 1 | const Conf = require("conf"); 2 | 3 | module.exports = function create({ name, ...options }) { 4 | return new Conf({ ...options, configName: name }); 5 | }; 6 | -------------------------------------------------------------------------------- /server/store/logs.js: -------------------------------------------------------------------------------- 1 | const create = require("./create"); 2 | 3 | const defaults = { 4 | logs: [], 5 | }; 6 | 7 | module.exports = create({ name: "logs", defaults }); 8 | -------------------------------------------------------------------------------- /server/store/pause-channels.js: -------------------------------------------------------------------------------- 1 | const create = require("./create"); 2 | 3 | const defaults = { 4 | channels: [], 5 | }; 6 | 7 | module.exports = create({ name: "pause-channels", defaults }); 8 | -------------------------------------------------------------------------------- /server/store/poll.js: -------------------------------------------------------------------------------- 1 | const create = require("./create"); 2 | 3 | const defaults = { 4 | watching: false, 5 | started: false, 6 | items: {}, 7 | logs: {}, 8 | }; 9 | 10 | module.exports = create({ name: "poll", defaults }); 11 | -------------------------------------------------------------------------------- /server/store/teams.js: -------------------------------------------------------------------------------- 1 | const create = require("./create"); 2 | 3 | const defaults = { 4 | list: {}, 5 | }; 6 | 7 | module.exports = create({ name: "teams", defaults }); 8 | -------------------------------------------------------------------------------- /server/store/treasure-chest.js: -------------------------------------------------------------------------------- 1 | const create = require("./create"); 2 | 3 | const defaults = { 4 | position: { x: 0, y: 0 }, 5 | points: 0, 6 | owner: null, 7 | }; 8 | 9 | module.exports = create({ name: "treasure-chest", defaults }); 10 | -------------------------------------------------------------------------------- /server/store/twitch.js: -------------------------------------------------------------------------------- 1 | const create = require("./create"); 2 | 3 | const defaults = { 4 | accessToken: null, 5 | }; 6 | 7 | module.exports = create({ name: "twitch", defaults }); 8 | -------------------------------------------------------------------------------- /server/store/users.js: -------------------------------------------------------------------------------- 1 | const create = require("./create"); 2 | 3 | const defaults = { 4 | list: {}, 5 | }; 6 | 7 | module.exports = create({ name: "users", defaults }); 8 | -------------------------------------------------------------------------------- /server/store/wall-frame.js: -------------------------------------------------------------------------------- 1 | const create = require("./create"); 2 | 3 | const defaults = { 4 | coords: {}, 5 | }; 6 | 7 | module.exports = create({ name: "wall-frame", defaults }); 8 | -------------------------------------------------------------------------------- /server/twitch/index.js: -------------------------------------------------------------------------------- 1 | const TwitchClient = require("./Client"); 2 | const config = require("../config"); 3 | 4 | const twitchClient = new TwitchClient(config.twitch); 5 | const twitchAuth = twitchClient.auth.bind(twitchClient); 6 | 7 | module.exports = { 8 | twitchClient, 9 | twitchAuth, 10 | }; 11 | -------------------------------------------------------------------------------- /server/twitch/plugins/cooldown.js: -------------------------------------------------------------------------------- 1 | const cooldownIds = {}; 2 | 3 | module.exports = function cooldown(client, message, id, seconds = 1) { 4 | const viewer = message.data.viewer; 5 | 6 | if (viewer.badges.broadcaster || viewer.badges.moderator) { 7 | return false; 8 | } 9 | 10 | const now = Date.now(); 11 | const timeout = seconds * 1000; 12 | const elapsed = now - (cooldownIds[id] || 0); 13 | 14 | if (elapsed < timeout) { 15 | let cmd = id; 16 | const rest = parseInt((timeout - elapsed) / 1000); 17 | if (cmd.startsWith("cmd.")) cmd = cmd.slice(4); 18 | client.chat.say(message.channel, `!${cmd} cooldown (~${rest}s)`); 19 | return true; 20 | } 21 | 22 | cooldownIds[id] = now; 23 | return false; 24 | }; 25 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/channel-points.js: -------------------------------------------------------------------------------- 1 | function addPoints(message, client, points) { 2 | const viewer = message.data.viewer; 3 | viewer.points += points; 4 | points = Math.floor(viewer.points); 5 | client.chat.say(message.channel, `${viewer.name} tu as ${points}pts.`); 6 | } 7 | 8 | const actions = { 9 | "25cbf6d5-f4cc-4db3-aa75-5938348c757c": (message, client) => { 10 | addPoints(message, client, 100); 11 | }, 12 | "83f0b8a2-3337-40e1-964b-577b4870cd93": (message, client) => { 13 | addPoints(message, client, 1000); 14 | }, 15 | }; 16 | 17 | module.exports = ({ message, client }, next) => { 18 | const rewardId = message.msg._tags["custom-reward-id"]; 19 | //console.log(rewardId); 20 | 21 | if (!rewardId) { 22 | return next(); 23 | } 24 | 25 | const action = actions[rewardId]; 26 | 27 | if (!action) { 28 | return next(); 29 | } 30 | 31 | action(message, client); 32 | 33 | next(); 34 | }; 35 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/chat.js: -------------------------------------------------------------------------------- 1 | const { addMessage, computeMessage } = require("../../../libs/chat"); 2 | const { 3 | addTeam, 4 | getTeamByName, 5 | getRanking, 6 | } = require("../../../libs/teamRanking"); 7 | 8 | module.exports = async ({ message, client }, next) => { 9 | if (["!", "+", "-"].includes(message.message[0])) return next(); 10 | 11 | const messageModel = await addMessage({ 12 | viewerId: message.data.viewer.id, 13 | time: new Date(message.data.timestamp), 14 | message: computeMessage(message.emotes), 15 | }); 16 | 17 | const team = message.data.team; 18 | 19 | if (team) { 20 | let teamRanking = await getTeamByName(team.name); 21 | 22 | if (!teamRanking) { 23 | teamRanking = await addTeam({ 24 | team: team.name, 25 | messageCount: 1, 26 | totalMessageCount: 1, 27 | }); 28 | client.io.emit("team.newRanking", teamRanking); 29 | } else { 30 | teamRanking.messageCount++; 31 | teamRanking.totalMessageCount++; 32 | await teamRanking.save(); 33 | } 34 | 35 | client.io.emit("team.ranking", await getRanking()); 36 | } 37 | 38 | client.io.emit("chat.new-message", { 39 | ...messageModel.get({ plain: true }), 40 | team, 41 | }); 42 | 43 | next(); 44 | }; 45 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands.js: -------------------------------------------------------------------------------- 1 | const commands = require("./commands/index"); 2 | 3 | module.exports = ({ message, client, ...helpers }, next) => { 4 | const text = message.message; 5 | 6 | if (text[0] !== "!" || text[1] === "!") { 7 | return next(); 8 | } 9 | 10 | const args = text.slice(1).split(" "); 11 | const name = args.shift(); 12 | const command = { name, args }; 13 | const commandFn = commands[name]; 14 | 15 | if (typeof commandFn === "function") { 16 | commandFn({ command, message, client, ...helpers }); 17 | } else { 18 | command.name = "alias"; 19 | command.args.unshift(name); 20 | commands.alias({ command, message, client, ...helpers }); 21 | } 22 | 23 | next(); 24 | }; 25 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/_say.js: -------------------------------------------------------------------------------- 1 | const removeDiacritics = require("../../../../libs/removeDiacritics"); 2 | const say = require("say"); 3 | 4 | const cooldownTimeout = 30; 5 | 6 | const queue = []; 7 | let saying = false; 8 | 9 | const speed = 1.3; 10 | const voice = 0; 11 | const voices = [ 12 | "Microsoft Hortense Desktop", 13 | "Microsoft Zira Desktop", 14 | "Microsoft David Desktop", 15 | "Microsoft Haruka Desktop", 16 | "Microsoft Irina Desktop", 17 | ]; 18 | 19 | // say.getInstalledVoices((...args) => { 20 | // console.log(args); 21 | // }); 22 | 23 | function sayText(text) { 24 | text = text.replace(/[_-]/g, " "); 25 | text = removeDiacritics(text); 26 | 27 | say.speak(text, voices[voice], speed, () => { 28 | saying = false; 29 | if (queue.length) { 30 | sayText(queue.shift()); 31 | } 32 | }); 33 | } 34 | 35 | module.exports = ({ command, message, client, cooldown }) => { 36 | if (cooldown("cmd.say", cooldownTimeout)) return; 37 | 38 | let text = command.args.join(" "); 39 | 40 | if (!text) { 41 | client.chat.say(message.channel, `Usage: !say `); 42 | return; 43 | } 44 | 45 | if (saying) { 46 | return queue.push(text); 47 | } 48 | 49 | saying = true; 50 | 51 | sayText(text); 52 | }; 53 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/_shazam.js: -------------------------------------------------------------------------------- 1 | const envPaths = require("env-paths"); 2 | const path = require("path"); 3 | const fs = require("fs"); 4 | 5 | const userDir = envPaths("marv").data; 6 | const songFile = path.join(userDir, "upload/files/shazam.txt"); 7 | 8 | module.exports = ({ message, client }) => { 9 | const title = fs.readFileSync(songFile); 10 | 11 | client.chat.say( 12 | message.channel, 13 | `Vous écoutez "${title}" disponible sur YouTube.` 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/add-points.js: -------------------------------------------------------------------------------- 1 | const { getViewerByName } = require("../../../../libs/viewers"); 2 | 3 | module.exports = async ({ command, message, client, isModo }) => { 4 | let [nick, points] = command.args; 5 | 6 | if (!isModo()) return; 7 | 8 | points = parseInt(points); 9 | 10 | if (!nick || !points || isNaN(points)) { 11 | client.chat.say(message.channel, `Usage: !add-points `); 12 | return; 13 | } 14 | 15 | let targetViewer = await getViewerByName(nick); 16 | 17 | if (!targetViewer) { 18 | client.chat.say(message.channel, `L'utilisateur ${nick} est introuvable!`); 19 | return; 20 | } 21 | 22 | targetViewer.points += points; 23 | await targetViewer.save(); 24 | 25 | const cleanPoints = Math.floor(targetViewer.points); 26 | client.chat.say(message.channel, `${nick} tu as ${cleanPoints}pts.`); 27 | }; 28 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/alias.js: -------------------------------------------------------------------------------- 1 | const alias = require("../../../../store/alias"); 2 | 3 | const ignores = ["theme", "line", "highlight"]; 4 | 5 | module.exports = ({ command, message, client, isModo }) => { 6 | let [name, ...args] = command.args; 7 | let value = args.join(" ").trim(); 8 | 9 | if (!name) { 10 | client.chat.say(message.channel, `Usage: !alias `); 11 | return; 12 | } 13 | 14 | if (name[0] === "+" && value) { 15 | isModo() && alias.set(`list.${name.slice(1)}`, value); 16 | } else if (name[0] === "-") { 17 | isModo() && alias.delete(`list.${name.slice(1)}`); 18 | } else if (!ignores.includes(name)) { 19 | value = alias.get(`list.${name}`); 20 | value && client.chat.say(message.channel, value); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/blink.js: -------------------------------------------------------------------------------- 1 | const { getViewerByName } = require("../../../../libs/viewers"); 2 | 3 | const maxCount = 42; 4 | const costRatio = 10; 5 | 6 | module.exports = async ({ command, message, client }) => { 7 | const viewer = message.data.viewer; 8 | let [count, target] = command.args; 9 | 10 | count = parseInt(count); 11 | 12 | if (count > maxCount) { 13 | count = maxCount; 14 | client.chat.say( 15 | message.channel, 16 | `Désolé ${viewer.name} tu vas blinker que ${maxCount}x.` 17 | ); 18 | } 19 | 20 | if (isNaN(count) || count <= 0) { 21 | client.chat.say(message.channel, `Usage: !blink (max:${maxCount})`); 22 | return; 23 | } 24 | 25 | const cost = Math.abs(count * costRatio); 26 | 27 | if (viewer.points < cost) { 28 | client.chat.say( 29 | message.channel, 30 | `Désolé ${viewer.name} tu n'as pas assez de points pour blinker (cost: ${cost}).` 31 | ); 32 | return; 33 | } 34 | 35 | let targetUser = null; 36 | 37 | if (target) { 38 | targetUser = await getViewerByName(target); 39 | 40 | if (!targetUser || !targetUser.avatarURL) { 41 | client.chat.say( 42 | message.channel, 43 | `Désolé ${viewer.name} "${target}" est introuvable sur le mur.` 44 | ); 45 | return; 46 | } 47 | } 48 | 49 | viewer.points -= cost; 50 | 51 | client.io.emit("wof.blink", { user: targetUser || viewer, count }); 52 | }; 53 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/dl.js: -------------------------------------------------------------------------------- 1 | const { downloadDir, allowedExtensions } = require("../config/download"); 2 | const fs = require("fs-extra"); 3 | const axios = require("axios"); 4 | const path = require("path"); 5 | 6 | module.exports = ({ command, message, client, isModo }) => { 7 | let [name, url] = command.args; 8 | 9 | if (!isModo()) return; 10 | 11 | const ext = (url || "").split(".").pop(); 12 | 13 | if (!allowedExtensions.includes(ext)) { 14 | const ae = allowedExtensions.join(","); 15 | client.chat.say(message.channel, `Usage: !dl (ext.: ${ae})`); 16 | return; 17 | } 18 | 19 | axios({ 20 | url, 21 | timeout: 0, 22 | method: "GET", 23 | responseType: "arraybuffer", 24 | }) 25 | .then((response) => { 26 | fs.writeFileSync(path.join(downloadDir, `${name}.${ext}`), response.data); 27 | client.chat.say(message.channel, `Download done! cmd: !play ${name}`); 28 | }) 29 | .catch((error) => { 30 | client.chat.say(message.channel, `Error: ${error.message}`); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/error.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | 3 | const cost = 10; 4 | const colors = new chalk.Instance({ level: 3 }); 5 | 6 | module.exports = ({ command, message, client }) => { 7 | const viewer = message.data.viewer; 8 | let errorMessage = command.args.join(" ").trim(); 9 | 10 | if (viewer.points < cost) { 11 | return client.chat.say( 12 | message.channel, 13 | `Désolé ${viewer.name} tu n'as pas assez de points pour troller (cost: ${cost}).` 14 | ); 15 | } 16 | 17 | if (!errorMessage.length) { 18 | return client.chat.say(message.channel, `Usage: !error `); 19 | } 20 | 21 | viewer.points -= cost; 22 | 23 | // eslint-disable-next-line no-console 24 | console.log(colors.red.bold(errorMessage)); 25 | }; 26 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/frame.js: -------------------------------------------------------------------------------- 1 | const { getRandomVideoByUserName, getStreamByUserName } = require("../utils"); 2 | const urlParser = require("js-video-url-parser/lib/base"); 3 | require("js-video-url-parser/lib/provider/youtube"); 4 | require("js-video-url-parser/lib/provider/twitch"); 5 | 6 | const cooldownTimeout = 15; 7 | 8 | let currentChannel = null; 9 | 10 | module.exports = async ({ command, message, client, cooldown, isVip }) => { 11 | let [url] = command.args; 12 | 13 | if (!url && currentChannel) { 14 | return client.chat.say( 15 | message.channel, 16 | `Vous regardez ${currentChannel} !` 17 | ); 18 | } 19 | 20 | if ((url || "").match(/^[a-z0-9_]+$/i)) { 21 | url = `https://twitch.tv/${url}`; 22 | } 23 | 24 | let target = urlParser.parse(url); 25 | 26 | if (!target) { 27 | return client.chat.say( 28 | message.channel, 29 | `Usage: !frame (youtube|twitch)` 30 | ); 31 | } 32 | 33 | if (!isVip({ silent: true }) && target.provider !== "twitch") return; 34 | if (cooldown("cmd.frame", cooldownTimeout)) return; 35 | 36 | currentChannel = 37 | target.provider === "twitch" ? `twitch.tv/${target.channel} !` : url; 38 | 39 | if (target.provider === "twitch" && target.mediaType === "stream") { 40 | const stream = await getStreamByUserName({ client, name: target.channel }); 41 | 42 | if (!stream) { 43 | const video = await getRandomVideoByUserName({ 44 | client, 45 | mature: false, 46 | name: target.channel, 47 | channel: message.channel, 48 | }); 49 | if (!video) { 50 | return client.chat.say( 51 | message.channel, 52 | `Aucune vidéo "publique" trouvé chez ${target.channel}` 53 | ); 54 | } 55 | target = { ...target, ...video, mediaType: "video" }; 56 | } 57 | } 58 | 59 | client.io.emit("frame.push", target); 60 | }; 61 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/fv.js: -------------------------------------------------------------------------------- 1 | const clipsStore = require("../../../../store/clips"); 2 | const { random } = require("../utils"); 3 | 4 | module.exports = async ({ command, message, client, isModo }) => { 5 | let [channel, id, duration] = command.args; 6 | 7 | channel = (channel || "faudrey_voir").toLowerCase(); 8 | 9 | const clips = clipsStore.get(`list.${channel}`, []); 10 | 11 | if (id) { 12 | if (!isModo()) return; 13 | 14 | if (!duration) { 15 | client.chat.say(message.channel, `Usage: !fv `); 16 | return; 17 | } 18 | 19 | clips.push({ id, channel, duration }); 20 | clipsStore.set(`list.${channel}`, clips); 21 | 22 | return; 23 | } 24 | 25 | if (clips.length) { 26 | const clip = clips[random(0, clips.length)]; 27 | client.io.emit("faudrey_voir", { name: channel, clip }); 28 | client.chat.say(message.channel, `INTRUSION DE twitch.tv/${channel}`); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/gps.js: -------------------------------------------------------------------------------- 1 | const treasureChestStore = require("../../../../store/treasure-chest"); 2 | const { distance } = require("../utils"); 3 | 4 | const commandCost = 5; 5 | const cooldownTimeout = 60; 6 | 7 | module.exports = ({ message, client, cooldown }) => { 8 | const viewer = message.data.viewer; 9 | 10 | if (viewer.points < commandCost) { 11 | return client.chat.say( 12 | message.channel, 13 | `Désolé ${viewer.name} tu n'as pas assez de points pour te déplacer (cost: ${commandCost}).` 14 | ); 15 | } 16 | 17 | if (cooldown(`cmd.gps:${viewer.name}`, cooldownTimeout)) return; 18 | 19 | let chestPoints = treasureChestStore.get("points"); 20 | const chestPosition = treasureChestStore.get("position"); 21 | let diff = distance(chestPosition, viewer.position); 22 | 23 | chestPoints += commandCost * 2; 24 | viewer.points -= commandCost; 25 | 26 | treasureChestStore.set("points", chestPoints); 27 | 28 | client.chat.say( 29 | message.channel, 30 | `${viewer.name} tu es à ${diff} px du trésor (${chestPoints} pts).` 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/index.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | 4 | const commands = {}; 5 | 6 | fs.readdirSync(__dirname).forEach((file) => { 7 | if (file === "index.js" || !file.endsWith(".js") || file.startsWith("_")) 8 | return; 9 | const command = path.basename(file, ".js"); 10 | commands[command] = require(`./${command}`); 11 | }); 12 | 13 | module.exports = commands; 14 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/move.js: -------------------------------------------------------------------------------- 1 | const treasureChestStore = require("../../../../store/treasure-chest"); 2 | const { screenLimit } = require("../config/wall-of-fame"); 3 | const { random, distance, minMax } = require("../utils"); 4 | 5 | const costRatio = 0.1; 6 | 7 | function setRandomPosition() { 8 | treasureChestStore.set("position", { 9 | x: random(0, screenLimit.x), 10 | y: random(0, screenLimit.y), 11 | }); 12 | } 13 | 14 | module.exports = ({ command, message, client }) => { 15 | const viewer = message.data.viewer; 16 | const offsets = { x: 0, y: 0 }; 17 | 18 | command.args.forEach((arg) => { 19 | const key = arg[0]; 20 | const val = arg.slice(1); 21 | offsets[key] = parseInt(val); 22 | }); 23 | 24 | let newPosition = { 25 | x: minMax(0, screenLimit.x, viewer.position.x + offsets.x), 26 | y: minMax(0, screenLimit.y, viewer.position.y + offsets.y), 27 | }; 28 | 29 | const cost = Math.ceil(distance(newPosition, viewer.position) * costRatio); 30 | 31 | if (viewer.points < cost) { 32 | client.chat.say( 33 | message.channel, 34 | `Désolé ${viewer.name} tu n'as pas assez de points pour te déplacer (cost: ${cost}).` 35 | ); 36 | } else if (isNaN(offsets.x) || isNaN(offsets.y)) { 37 | client.chat.say(message.channel, `Usage: !move `); 38 | } else { 39 | viewer.points -= cost; 40 | viewer.position = newPosition; 41 | 42 | const chestPosition = treasureChestStore.get("position"); 43 | const diff = distance(chestPosition, viewer.position); 44 | 45 | // client.chat.say( 46 | // message.channel, 47 | // `${viewer.name} [${viewer.position.x},${viewer.position.y}]` 48 | // ); 49 | 50 | if (diff === 0) { 51 | let chestPoints = treasureChestStore.get("points"); 52 | client.chat.say( 53 | message.channel, 54 | `${viewer.name} a trouvé le trésor (${chestPoints} pts) !!!` 55 | ); 56 | client.emit("treasureChest.newOwner", viewer); 57 | treasureChestStore.set("owner", viewer.id); 58 | treasureChestStore.set("points", 0); 59 | viewer.points += chestPoints; 60 | setRandomPosition(); 61 | } 62 | 63 | client.emit("wof.move", message); 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/paillettes.js: -------------------------------------------------------------------------------- 1 | const cooldownTimeout = 30; 2 | 3 | module.exports = async ({ client, cooldown }) => { 4 | if (cooldown("cmd.paillettes", cooldownTimeout)) return; 5 | 6 | client.io.emit("paillettes"); 7 | }; 8 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/pause.js: -------------------------------------------------------------------------------- 1 | const { shuffle, getRandomVideoByUserName } = require("../utils"); 2 | const store = require("../../../../store/pause-channels"); 3 | 4 | // DONE: command !pause 5 | // DONE: pick random start location 6 | // DONE: increase countound if < 0 -> +42s 7 | // DONE: command !pause stop 8 | // DONE: loop videos 9 | // DONE: command !pause [+-] 10 | // DONE: check if mature chanel 11 | // DONE: random playlist 12 | 13 | // TODO: show live stream first ??? 14 | 15 | let channels = store.get("channels"); 16 | 17 | module.exports = ({ command, message, client, isModo }) => { 18 | let input = command.args.join(" "); 19 | 20 | if (!isModo()) return; 21 | 22 | if (input === "stop") { 23 | return client.emit("pause.stop"); 24 | } 25 | 26 | if (input && input.length > 1 && ["+", "-"].includes(input[0])) { 27 | const char = input[0]; 28 | const users = input 29 | .slice(1) 30 | .split(/[ ,]+/g) 31 | .filter((w) => w); 32 | 33 | users.forEach((user) => { 34 | const exists = channels.indexOf(user); 35 | 36 | if (char === "+" && exists == -1) { 37 | channels.push(user); 38 | } else if (char === "-" && exists > -1) { 39 | channels.splice(exists, 1); 40 | } 41 | }); 42 | 43 | store.set("channels", channels); 44 | 45 | return; 46 | } 47 | 48 | const minutes = parseFloat(input); 49 | 50 | if (!minutes || isNaN(minutes)) { 51 | client.chat.say(message.channel, `Usage: !pause `); 52 | return; 53 | } 54 | 55 | const promises = channels.map((name) => 56 | getRandomVideoByUserName({ client, name, channel: message.channel }) 57 | ); 58 | 59 | Promise.all(promises) 60 | .then((videos) => { 61 | videos = videos.filter((video) => video); 62 | client.emit("pause.start", { minutes, videos: shuffle(videos) }); 63 | }) 64 | .catch((error) => { 65 | // eslint-disable-next-line no-console 66 | console.log("ERROR >>>", error); 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/play.js: -------------------------------------------------------------------------------- 1 | const { downloadDir } = require("../config/download"); 2 | const fs = require("fs"); 3 | 4 | const cooldownTimeout = 15; 5 | 6 | module.exports = ({ command, message, client, cooldown }) => { 7 | let [name] = command.args; 8 | 9 | if (!name) { 10 | client.chat.say(message.channel, `Usage: !play | !dl `); 11 | return; 12 | } 13 | 14 | const fileList = fs.readdirSync(downloadDir); 15 | 16 | if (name === "-h") { 17 | const playlist = fileList.map((f) => f.slice(0, -4)).join(" | "); 18 | client.chat.say(message.channel, `Playlist: ${playlist}`); 19 | return; 20 | } 21 | 22 | const matchFile = fileList.find((file) => file.slice(0, -4) === name); 23 | 24 | if (!matchFile) { 25 | client.chat.say(message.channel, `Aucun fichier trouvé pour ${name}...`); 26 | return; 27 | } 28 | 29 | if (cooldown("cmd.play", cooldownTimeout)) return; 30 | 31 | client.io.emit("play.sound", { file: matchFile }); 32 | }; 33 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/points.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ message, client }) => { 2 | const viewer = message.data.viewer; 3 | const points = Math.floor(viewer.points); 4 | client.chat.say(message.channel, `${viewer.name} tu as ${points}pts.`); 5 | }; 6 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/poll.js: -------------------------------------------------------------------------------- 1 | const poll = require("../../../../store/poll"); 2 | const { twitchClient } = require("../../../index"); 3 | 4 | poll.set(`started`, false); 5 | 6 | const actions = { 7 | start(duration = 42) { 8 | if (poll.get(`started`)) return; 9 | duration = parseInt(duration); 10 | poll.set(`logs`, {}); 11 | poll.set(`started`, true); 12 | setTimeout(this.stop, duration * 1000); 13 | twitchClient.io.emit("poll.start", { duration }); 14 | }, 15 | stop() { 16 | if (!poll.get(`started`)) return; 17 | poll.set(`started`, false); 18 | twitchClient.io.emit("poll.stop"); 19 | }, 20 | reset() { 21 | poll.set(`watching`, false); 22 | poll.set(`started`, false); 23 | poll.set(`items`, {}); 24 | poll.set(`logs`, {}); 25 | twitchClient.io.emit("poll.reset"); 26 | }, 27 | show() { 28 | twitchClient.io.emit("poll.show"); 29 | }, 30 | hide() { 31 | twitchClient.io.emit("poll.hide"); 32 | }, 33 | }; 34 | 35 | module.exports = async ({ command, message, client, isModo }) => { 36 | if (!isModo()) return; 37 | 38 | let [action, ...args] = command.args; 39 | const actionNames = Object.keys(actions); 40 | 41 | if (!action || !actionNames.includes(action)) { 42 | return client.chat.say( 43 | message.channel, 44 | `Usage: !poll <${actionNames.join("|")}>` 45 | ); 46 | } 47 | 48 | actions[action](...args); 49 | }; 50 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/reset-ranking.js: -------------------------------------------------------------------------------- 1 | const { getRanking, resetRanking } = require("../../../../libs/teamRanking"); 2 | 3 | module.exports = async ({ client, isModo }) => { 4 | if (!isModo()) return; 5 | 6 | await resetRanking(); 7 | 8 | client.io.emit("team.ranking", await getRanking()); 9 | }; 10 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/say.js: -------------------------------------------------------------------------------- 1 | const tts = require("./tts"); 2 | 3 | const cooldownTimeout = 30; 4 | 5 | module.exports = ({ command, message, client, cooldown }) => { 6 | if (cooldown("cmd.say", cooldownTimeout)) return; 7 | 8 | const text = command.args.join(" "); 9 | 10 | if (!text) { 11 | client.chat.say(message.channel, `Usage: !say `); 12 | return; 13 | } 14 | 15 | tts(message.data.viewer.name, text); 16 | }; 17 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/tts-output/output.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skarab42/skarabot/ff9872b4f1e3124ec63951e942d9a55053d612f3/server/twitch/plugins/on-message/commands/tts-output/output.mp3 -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/tts.js: -------------------------------------------------------------------------------- 1 | const textToSpeech = require("@google-cloud/text-to-speech"); 2 | const { play } = require("sound-play"); 3 | const util = require("util"); 4 | const path = require("path"); 5 | const fs = require("fs"); 6 | 7 | process.env.GOOGLE_APPLICATION_CREDENTIALS = path.join( 8 | __dirname, 9 | "tts-keys/skara-bot-c57fd9129493.json" 10 | ); 11 | 12 | const volume = 5; 13 | const messages = []; 14 | let isPlaying = false; 15 | const client = new textToSpeech.TextToSpeechClient(); 16 | const mp3File = path.join(__dirname, "tts-output/output.mp3"); 17 | 18 | async function say() { 19 | if (isPlaying || messages.length === 0) return; 20 | 21 | isPlaying = true; 22 | 23 | const { user, text } = messages.shift(); 24 | 25 | const request = { 26 | input: { text: `${user} dit "${text}"` }, 27 | voice: { languageCode: "fr-FR", ssmlGender: "FEMALE" }, 28 | audioConfig: { 29 | volumeGainDb: 16, 30 | audioEncoding: "MP3", 31 | effectsProfileId: ["large-home-entertainment-class-device"], 32 | }, 33 | }; 34 | 35 | const [response] = await client.synthesizeSpeech(request); 36 | const writeFile = util.promisify(fs.writeFile); 37 | 38 | await writeFile(mp3File, response.audioContent, "binary"); 39 | await play(mp3File, volume); 40 | 41 | isPlaying = false; 42 | say(); 43 | } 44 | 45 | module.exports = (user, text) => { 46 | messages.push({ user, text }); 47 | 48 | say(); 49 | }; 50 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/commands/waf.js: -------------------------------------------------------------------------------- 1 | module.exports = async ({ command, client }) => { 2 | let [action] = command.args; 3 | 4 | action = action === "hide" ? "hide" : "show"; 5 | 6 | client.io.emit(`wof.${action}`); 7 | }; 8 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/config/download.js: -------------------------------------------------------------------------------- 1 | const { name } = require("../../../../../package"); 2 | const envPaths = require("env-paths"); 3 | const fs = require("fs-extra"); 4 | const path = require("path"); 5 | 6 | const userDir = envPaths(name).data; 7 | const downloadDir = path.join(userDir, "download"); 8 | 9 | fs.ensureDirSync(downloadDir); 10 | 11 | module.exports = { 12 | userDir, 13 | downloadDir, 14 | allowedExtensions: ["mp3", "wav", "ogg"], 15 | }; 16 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/config/wall-of-fame.js: -------------------------------------------------------------------------------- 1 | const screenSize = { width: 2620, height: 1440 }; 2 | const imgSize = { width: 100, height: 100 }; 3 | const screenLimit = { 4 | x: screenSize.width - imgSize.width, 5 | y: screenSize.height - imgSize.height, 6 | }; 7 | 8 | module.exports = { 9 | screenSize, 10 | imgSize, 11 | screenLimit, 12 | }; 13 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/config/welcomeSentences.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | "bonjour", 3 | "hello", 4 | "salut", 5 | "hi", 6 | "hellut", 7 | "cc", 8 | "yo", 9 | "hey", 10 | "boop", 11 | "plop", 12 | "pouet", 13 | "coucou", 14 | "tagazok", 15 | "zblhu", 16 | "salutations", 17 | "bonsoir", 18 | "heyguys", 19 | "luvpeekr", 20 | "redalihey", 21 | "Iti63hey", 22 | "morning", 23 | "dag", 24 | ]; 25 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/faudrey_voir.js: -------------------------------------------------------------------------------- 1 | const clipsStore = require("../../../store/clips"); 2 | const { random } = require("./utils"); 3 | 4 | module.exports = async ({ message, client }, next) => { 5 | const viewer = message.data.viewer; 6 | 7 | if (!message.data.isFirstMessage) { 8 | return next(); 9 | } 10 | 11 | const name = viewer.name.toLowerCase(); 12 | const clips = clipsStore.get(`list.${name}`, []); 13 | 14 | if (clips.length) { 15 | const clip = clips[random(0, clips.length)]; 16 | client.io.emit("faudrey_voir", { name, clip }); 17 | client.chat.say(message.channel, `INTRUSION DE twitch.tv/${name}`); 18 | } 19 | 20 | next(); 21 | }; 22 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/poll.js: -------------------------------------------------------------------------------- 1 | const poll = require("../../../store/poll"); 2 | 3 | module.exports = ({ message, client }, next) => { 4 | const viewer = message.data.viewer; 5 | const text = message.message; 6 | const prefix = text[0]; 7 | 8 | if (!poll.get(`started`) || !["+", "-"].includes(prefix) || text.length < 2) { 9 | return next(); 10 | } 11 | 12 | let [name] = text.slice(1).split(/[ .]/); 13 | name = name.trim().toUpperCase().slice(0, 42); 14 | 15 | if (!name) return next(); 16 | 17 | const lastItem = poll.get(`logs.${viewer.id}`); 18 | 19 | if (lastItem === name) return next(); 20 | 21 | poll.set(`logs.${viewer.id}`, name); 22 | 23 | let points = poll.get(`items.${name}`) || 0; 24 | points += prefix === "+" ? 1 : -1; 25 | 26 | poll.set(`items.${name}`, points); 27 | 28 | client.emit("poll.update", { ...poll.store, currentItem: { name, points } }); 29 | 30 | next(); 31 | }; 32 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/start-time.js: -------------------------------------------------------------------------------- 1 | const { getStartTime } = require("./../../../libs/stream"); 2 | 3 | module.exports = async ({ message }, next) => { 4 | message.data.startTime = await getStartTime(); 5 | return next(); 6 | }; 7 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/streamer-highlight.js: -------------------------------------------------------------------------------- 1 | const frameCommand = require("./commands/frame"); 2 | 3 | const delayH = 2; 4 | const delayMs = delayH * 3600000; 5 | const minViewCount = 1000; 6 | 7 | const sayMessage = "C'est un avion ? une fusée ? non c'est un streamer Twitch"; 8 | 9 | module.exports = async ({ message, client, ...helpers }, next) => { 10 | const viewer = message.data.viewer; 11 | const now = message.data.timestamp; 12 | const elapsed = message.data.timestamp - viewer.lastHighlight; 13 | 14 | if (viewer.badges.broadcaster) { 15 | return next(); 16 | } 17 | 18 | if (viewer.viewCount < minViewCount || elapsed < delayMs) { 19 | return next(); 20 | } 21 | 22 | // const channel = await client.api.kraken.channels.getChannel(user.id); 23 | const banner = `${sayMessage} -> ${viewer.name} | http://twitch.tv/${viewer.name}`; 24 | 25 | client.api.helix.videos 26 | .getVideosByUser(viewer.id) 27 | .then(({ data }) => { 28 | viewer.lastHighlight = now; 29 | 30 | if (!data.length) { 31 | client.chat.say(message.channel, banner); 32 | return next(); 33 | } 34 | 35 | let { url } = data[0]._data; 36 | 37 | client.chat.say(message.channel, `${banner} | ${url} |`); 38 | 39 | // if (!channel._data.mature) { 40 | const command = { name: "frame", args: [viewer.name] }; 41 | frameCommand({ command, message, client, ...helpers }); 42 | // } 43 | 44 | next(); 45 | }) 46 | .catch((error) => { 47 | // eslint-disable-next-line no-console 48 | console.log("ERROR:", error); // TODO handle error... 49 | next(); 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/terminal-chat.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | const colors = new chalk.Instance({ level: 3 }); 3 | 4 | module.exports = ({ message }, next) => { 5 | const { user, msg } = message; 6 | const text = message.message; 7 | const { color } = msg._tags; 8 | const nick = colors.hex(color)(`[${user}]`); 9 | 10 | if (text.startsWith("!error")) { 11 | return next(); 12 | } 13 | 14 | // eslint-disable-next-line no-console 15 | console.log(`${nick} ${text}`); 16 | 17 | next(); 18 | }; 19 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/timestamp.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ message }, next) => { 2 | message.data.timestamp = Date.now(); 3 | next(); 4 | }; 5 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/user-rewards.js: -------------------------------------------------------------------------------- 1 | const soundex = require("soundex"); 2 | 3 | const messageLengthRatio = 0.01; 4 | const welcomePoints = 10; 5 | const replyPoints = 1; 6 | 7 | const welcomeSentences = require("./config/welcomeSentences").map((s) => 8 | soundex(s) 9 | ); 10 | 11 | module.exports = ({ message }, next) => { 12 | let viewer = message.data.viewer; 13 | let points = message.message.length * messageLengthRatio; 14 | 15 | if (message.msg._tags["reply-parent-user-id"]) { 16 | points += replyPoints; 17 | } 18 | 19 | const firstWord = soundex(message.message.split(" ")[0].toLowerCase()); 20 | 21 | if (welcomeSentences.includes(firstWord)) { 22 | if (message.data.isFirstMessage) { 23 | points += welcomePoints; 24 | } else { 25 | points += welcomePoints * 0.1; 26 | } 27 | } 28 | 29 | viewer.points = Math.ceil(viewer.points + points); 30 | 31 | next(); 32 | }; 33 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/viewer-badges.js: -------------------------------------------------------------------------------- 1 | module.exports = async ({ message }, next) => { 2 | const badges = message.msg._tags.badges || null; 3 | const viewer = message.data.viewer; 4 | 5 | if (!badges) return next(); 6 | 7 | const newBadges = {}; 8 | 9 | badges.split(",").forEach((badge) => { 10 | const [key, val] = badge.split("/"); 11 | newBadges[key] = parseInt(val); 12 | }); 13 | 14 | viewer.badges = newBadges; 15 | 16 | next(); 17 | }; 18 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/viewer-log.js: -------------------------------------------------------------------------------- 1 | const { addViewer, getViewerById } = require("../../../libs/viewers"); 2 | const { fetchViewer } = require("./../../../libs/firebase"); 3 | 4 | module.exports = async ({ message }, next) => { 5 | const { msg, data } = message; 6 | const id = parseInt(msg._tags["user-id"]); 7 | 8 | data.viewer = await getViewerById(id); 9 | data.color = null; 10 | data.team = null; 11 | 12 | if (!data.viewer) { 13 | data.viewer = await addViewer({ id, name: msg._tags["display-name"] }); 14 | } 15 | 16 | let viewer = await fetchViewer(id); 17 | 18 | if (viewer) { 19 | data.team = { 20 | name: viewer.team, 21 | color: viewer.color, 22 | }; 23 | } 24 | 25 | data.viewer.messageCount++; 26 | 27 | next(); 28 | }; 29 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/viewer-save.js: -------------------------------------------------------------------------------- 1 | module.exports = async ({ message }, next) => { 2 | await message.data.viewer.save(); 3 | 4 | next(); 5 | }; 6 | -------------------------------------------------------------------------------- /server/twitch/plugins/on-message/wall-of-fame.js: -------------------------------------------------------------------------------- 1 | const { screenLimit } = require("./config/wall-of-fame"); 2 | const { random } = require("./utils"); 3 | 4 | function setRandomPosition(viewer) { 5 | viewer.position = { 6 | x: random(0, screenLimit.x), 7 | y: random(0, screenLimit.y), 8 | }; 9 | } 10 | 11 | module.exports = ({ message, client }, next) => { 12 | const viewer = message.data.viewer; 13 | const isFirstMessage = viewer.messageCount === 1; 14 | 15 | if (isFirstMessage || message.data.isFirstMessage) { 16 | isFirstMessage && setRandomPosition(viewer); 17 | client.emit("wof.add-viewer", viewer); 18 | } 19 | 20 | next(); 21 | }; 22 | -------------------------------------------------------------------------------- /server/twitch/plugins/roles.js: -------------------------------------------------------------------------------- 1 | const defaultOptions = { 2 | silent: false, 3 | }; 4 | 5 | function is(roles, client, message, options = {}) { 6 | options = { ...defaultOptions, ...options }; 7 | const viewer = message.data.viewer; 8 | const found = roles.filter((role) => viewer.badges[role]); 9 | 10 | if (found.length) { 11 | return true; 12 | } 13 | 14 | if (!options.silent) { 15 | client.chat.say( 16 | message.channel, 17 | `Usage: pas pour toi ${viewer.name} Kappa` 18 | ); 19 | } 20 | 21 | return false; 22 | } 23 | 24 | module.exports = { 25 | isVip: is.bind(null, ["broadcaster", "moderator", "vip"]), 26 | isModo: is.bind(null, ["broadcaster", "moderator"]), 27 | isBroadcaster: is.bind(null, ["broadcaster"]), 28 | }; 29 | -------------------------------------------------------------------------------- /spawnServer.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { spawn } from "child_process"; 3 | import chokidar from "chokidar"; 4 | 5 | export default function spawnServer(bin) { 6 | let server = null; 7 | 8 | const kill = () => { 9 | server.stdin.pause(); 10 | server.kill(); 11 | }; 12 | 13 | const start = () => { 14 | server = spawn("node", [bin]); 15 | 16 | server.stdout.on("data", (data) => { 17 | console.log(`[server] ${data.toString().trim()}`); 18 | }); 19 | 20 | server.stderr.on("data", (data) => { 21 | console.error(`[server:error] ${data}`); 22 | }); 23 | 24 | server.on("close", (code) => { 25 | console.log(`[server] exited with code ${code || 0}`); 26 | }); 27 | }; 28 | 29 | const watcher = chokidar.watch("server/**/*"); 30 | 31 | watcher.on("ready", () => { 32 | watcher.on("change", (path) => { 33 | console.log(`[server] file changed: ${path}`); 34 | if (server) kill(); 35 | start(); 36 | }); 37 | }); 38 | 39 | return { 40 | writeBundle() { 41 | if (server) return; 42 | start(); 43 | }, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /tailwind/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: { 3 | enabled: true, 4 | content: ["./client/**/*.{js,svelte}", "./public/**/*.{html,svg}"], 5 | }, 6 | darkMode: false, // or 'media' or 'class' 7 | theme: { 8 | extend: {}, 9 | }, 10 | variants: { 11 | extend: {}, 12 | }, 13 | plugins: [], 14 | }; 15 | -------------------------------------------------------------------------------- /tailwind/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | --------------------------------------------------------------------------------