├── .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 |
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 |
})
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 |
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 |
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 |
--------------------------------------------------------------------------------