Browse Source

initial commit

rekjn 5 years ago
commit
3659bdc1fb
20 changed files with 3387 additions and 0 deletions
  1. 108 0
      .gitignore
  2. 41 0
      fileutil.js
  3. BIN
      frontend/background.jpg
  4. BIN
      frontend/background.png
  5. BIN
      frontend/closeicon.png
  6. 30 0
      frontend/index.html
  7. BIN
      frontend/loading.gif
  8. 129 0
      frontend/modal.css
  9. 125 0
      frontend/modal.js
  10. BIN
      frontend/rubik.ttf
  11. BIN
      frontend/rubikbold.ttf
  12. 147 0
      frontend/style.css
  13. 181 0
      handlers.js
  14. 234 0
      index.js
  15. 45 0
      logger.js
  16. 171 0
      musicbot.js
  17. 1994 0
      package-lock.json
  18. 19 0
      package.json
  19. 20 0
      run.js
  20. 143 0
      templates/uploadform.html

+ 108 - 0
.gitignore

@@ -0,0 +1,108 @@
+
+# Created by https://www.gitignore.io/api/node
+# Edit at https://www.gitignore.io/?templates=node
+
+### Node ###
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+.env.test
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+
+# next.js build output
+.next
+
+# nuxt.js build output
+.nuxt
+
+# react / gatsby 
+public/
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# custom - bot directories and token configuration
+announcer.mp3
+reexported/
+playlist/
+ssl/
+uploads/
+v1.zip
+config.json
+logs/
+
+# End of https://www.gitignore.io/api/node

+ 41 - 0
fileutil.js

@@ -0,0 +1,41 @@
+const ffmpeg = require ( 'ffmpeg' );
+const fs = require ( 'fs' );
+const crypto = require ( 'crypto' );
+const { exec } = require ( 'child_process' );
+
+const Config = require ( './config.json' );
+const logger = require ( './logger.js' );
+
+function handleUploadedFile ( filename, callback, error )
+{
+    exec ( Config.probe_command + ' ' + filename, function ( err, stdout, stderr )
+    {
+        if ( err ) return error ( err );
+        
+        try
+        {
+            let response = JSON.parse ( stdout );
+
+            if ( response.streams.length == 1 && response.streams[0].codec_name == 'mp3' && response.format.format_name == 'mp3' ) 
+            {
+                let name = crypto.randomBytes ( 12 ).toString ( 'hex' );
+
+                fs.renameSync ( filename, './playlist/' + name + '.mp3' );
+                logger.log ( '[Info/WebServer] Added new music ' + name );
+                return callback ( );
+            }
+            else 
+            {
+                return error ( );
+            }
+        } catch ( exception )
+        {
+            fs.unlinkSync ( filename );
+
+            logger.error ( exception );
+            error ( exception );
+        }
+    } );
+}
+
+module.exports.handleUploadedFile = handleUploadedFile;

BIN
frontend/background.jpg


BIN
frontend/background.png


BIN
frontend/closeicon.png


+ 30 - 0
frontend/index.html

@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="utf-8" />
+        <link rel="stylesheet" href="style.css" type="text/css" />
+        <title>radio patapedia</title>
+    </head>
+    <body>
+        <div class="backgroundCover"></div>
+        <div class="bodyWrapper">
+            <div class="siteHeader">
+                <div class="siteHeaderWordmark">
+                    radio patapedia
+                </div>
+            </div>
+            <div class="siteMain">
+                <h1>Dodaj Piosenkę</h1>
+                <p>Wymagane będzie potwiedzenie tożsamożci.</p>
+                <br />
+                <a class="button" href="auth">Nowa Piosenka</a>
+                <br />
+                <br />
+                <!--<h1>Aktualne piosenki</h1>-->
+                <div class="footer">
+                    <p>Strona i aplikacja stworzona przez rekjna. Tło ukradłem z deviantarta owoca.</p>
+                </div>
+            </div>
+        </div>
+    </body>
+</html>

BIN
frontend/loading.gif


+ 129 - 0
frontend/modal.css

@@ -0,0 +1,129 @@
+div.modalBlackout {
+    position: fixed;
+    top: 0; left: 0; right: 0; bottom: 0;
+    background: rgba(0, 0, 0, 0.6);
+    z-index: 10000;
+}
+
+div.modalBlackout > div.modalObject {
+    background: #fff;
+    width: 650px;
+    position: absolute;
+    top: 130px;
+    left: calc(50% - 325px);
+    font-family: sans-serif;
+    color: #3a3a3a;
+}
+
+div.modalBlackout > div.modalObject > div.modalObjectTitle {
+    background: #d8d8d8;
+    padding: 15px;
+    font-weight: bold;
+    font-size: 110%;
+}
+
+div.modalBlackout > div.modalObject > div.modalObjectContent {
+    margin-left: 20px;
+    margin-top: 15px;
+    margin-bottom: 15px;
+    margin-right: 20px;
+}
+
+div.modalBlackout > div.modalObject > div.modalObjectOptions {
+    padding: 15px;
+    padding-top: 9px;
+    padding-bottom: 10px;
+    background: #d8d8d8;
+    display: flex;
+    flex-direction: row-reverse;
+}
+
+div.modalBlackout > div.modalObject > div.modalObjectOptions > a.modalOption {
+    display: block;
+    background: #3a3a3a;
+    color: #dedede;
+    padding: 10px 15px;
+    cursor: pointer;
+    border-radius: 2px;
+    transition: background .25s, border-color .25s;
+    border: 2px #3a3a3a solid;
+    margin-left: 5px;
+    font-weight: bold;
+    font-size: 95%;
+    height: 16px;
+}
+
+div.modalBlackout > div.modalObject > div.modalObjectOptions > a.modalOption:hover {
+    background: #333333;
+    border-color: #333333;
+}
+
+div.modalBlackout > div.modalObject > div.modalObjectOptions > a.modalOption.alternate {
+    background: transparent;
+    color: #3a3a3a;
+    transition: background .25s, border-color .25s, color .25s;
+}
+
+div.modalBlackout > div.modalObject > div.modalObjectOptions > a.modalOption.alternate:hover {
+    color: #000000;
+    border-color: #000000;
+}
+
+div.modalBlackout > div.modalObject > div.modalObjectTitle {
+    display: flex;
+    padding-bottom: 14px;
+    line-height: 20px;
+}
+
+div.modalBlackout > div.modalObject > div.modalObjectTitle > a.modalObjectClose {
+    display: block;
+    background-image: url('closeicon.png');
+    width: 16px;
+    height: 16px;
+    margin-top: 1px;
+    background-size: 16px 16px;
+    cursor: pointer;
+    margin-left: auto;
+}
+
+input[type=text].paintInput {
+    font-size: 105%;
+    border: 2px #ccc solid;
+    border-radius: 2px;
+    transition: border-color .25s;
+    display: block;
+    width: 80%;
+    margin: auto;
+    margin-top: 5px;
+    padding: 5px 10px;
+}
+
+input[type=text].paintInput:focus {
+    border-color: #bbb;
+}
+
+div.saveDialogBlackout {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    top: 0px;
+    left: 0px;
+    background: rgba(255, 255, 255, 0.4);
+    /* display: flex; */
+    align-items: center;
+    justify-content: center;
+    display: none;
+}
+
+div.saveDialogBlackout > img {
+    width: 50px;
+}
+
+div.error {
+    color: #800000;
+    background: #ff8080;
+    border: #800000 2px solid;
+    border-radius: 3px;
+    padding: 10px 15px;
+    margin-bottom: 12px;
+}

+ 125 - 0
frontend/modal.js

@@ -0,0 +1,125 @@
+/*
+ * script: modalbox
+ * author: rekjn
+ * description: frontend utility modal boxes
+ */
+
+let modalbox = 
+{
+    __blackout: null,
+    __active: { },
+
+    displayModal: function ( id, title, content, options )
+    {
+        if ( modalbox.__active.hasOwnProperty ( id ) )
+            return modalbox.__active [id];
+
+        let modal = 
+        {
+            title: title,
+            content: content,
+            options: options,
+            element: modalbox.generateModal ( id, title, content, options )
+        };
+
+        modalbox.__active [id] = modal;
+        return modal;
+    },
+
+    generateModal: function ( id, title, content, options )
+    {
+        // construct dom tree
+        let modalObject = document.createElement ( 'div' );
+        modalObject.classList.add ( 'modalObject' );
+        modalObject.setAttribute ( 'id', id );
+
+        let modalObjectTitle = document.createElement ( 'div' );
+        modalObjectTitle.classList.add ( 'modalObjectTitle' );
+        modalObjectTitle.innerHTML = title;
+
+        let modalObjectCloseButton = document.createElement ( 'a' );
+        modalObjectCloseButton.classList.add ( 'modalObjectClose' );
+        modalObjectTitle.appendChild ( modalObjectCloseButton );
+
+        modalObjectCloseButton.addEventListener ( 'click', function ( ev ) 
+        {
+            modalbox.closeModal ( this );
+        } );
+
+        let modalObjectContent = document.createElement ( 'div' );
+        modalObjectContent.classList.add ( 'modalObjectContent' );
+        modalObjectContent.innerHTML = content;
+
+        let modalObjectOptions = document.createElement ( 'div' );
+        modalObjectOptions.classList.add ( 'modalObjectOptions' );
+
+        modalbox.generateModalOptions ( id, options, modalObjectOptions );
+
+        modalObject.appendChild ( modalObjectTitle );
+        modalObject.appendChild ( modalObjectContent );
+        modalObject.appendChild ( modalObjectOptions );
+
+        let blackout = modalbox.getBlackout ( );
+        blackout.appendChild ( modalObject );
+
+        return modalObject;
+    },
+
+    generateModalOptions: function ( modal_id, options, wrapper )
+    {
+        if ( !Array.isArray ( options ) )  
+            throw new Error ( 'invalid type passed as option array' );
+
+        options.forEach ( function ( value, index )
+        {
+            if ( typeof value !== 'object' ) 
+                throw new Error ( 'invalid option format' );
+
+            let id = modal_id + '__' + index;
+            if ( value.id ) id = value.id;
+
+            let styleClasses = 'modalOption';
+            if ( value.additionalClasses ) styleClasses += ' ' + value.additionalClasses;
+
+            let optionObject = document.createElement ( 'a' );
+            optionObject.setAttribute ( 'id', id );
+            optionObject.setAttribute ( 'class', styleClasses );
+            optionObject.innerHTML = value.message;
+
+            if ( typeof value.handler !== 'function' ) 
+                throw new Error ( 'invalid option handler format' );
+
+            optionObject.addEventListener ( 'click', value.handler );
+            wrapper.appendChild ( optionObject );
+        } );
+    },
+
+    getBlackout: function ( )
+    {
+        if ( modalbox.__blackout ) return modalbox.__blackout;
+
+        let blackout = document.createElement ( 'div' );
+        blackout.setAttribute ( 'id', 'modalBlackout' );
+        blackout.classList.add ( 'modalBlackout' );
+
+        document.querySelector ( 'body' ).appendChild ( blackout );
+
+        modalbox.__blackout = blackout;
+        return modalbox.__blackout;
+    },
+
+    closeModal: function ( object )
+    {
+        let modalObject = object.closest ( '.modalObject' );
+        let id = modalObject.getAttribute ( 'id' );
+
+        modalObject.remove ( );
+        delete modalbox.__active [id];
+
+        if ( Object.keys ( modalbox.__active ).length === 0 )
+        {
+            modalbox.__blackout.remove ( );
+            modalbox.__blackout = null;
+        }
+    }
+};

BIN
frontend/rubik.ttf


BIN
frontend/rubikbold.ttf


+ 147 - 0
frontend/style.css

@@ -0,0 +1,147 @@
+@font-face {
+    font-family: rubik;
+    src: url(rubik.ttf);
+}
+
+@font-face {
+    font-family: rubikbold;
+    src: url(rubikbold.ttf);
+}
+
+body, html {
+    padding: 0; margin: 0;
+    font-family: rubik;
+    color: #efefef;
+}
+
+div.bodyWrapper {
+    width: 75%;
+    margin: auto;
+}
+
+div.bodyWrapper > div.siteHeader {
+    height: 200px;
+    font-size: 80px;
+    font-weight: 100;
+    line-height: 160px;
+    text-align: center;
+    font-family: rubikbold;
+    color: #523c7b;
+}
+
+div.bodyWrapper > div.siteMain {
+    background: #efefef;
+    color: #3a3a3a;
+    padding: 20px 30px;
+}
+
+div.backgroundCover {
+    width: 100vw;
+    height: 100vh;
+    position: fixed;
+    background: url('background.png') no-repeat fixed center;
+    background-size: cover;
+    filter: blur(8px);
+  }
+  
+  body {
+    background: #523c7b;
+  }
+  
+  div.bodyWrapper {
+    z-index: 99;
+    position: relative;
+  }
+
+  div.siteHeaderWordmark {
+    text-shadow:
+     -1px -1px 0 #d7caf8,  
+      1px -1px 0 #d7caf8,
+      -1px 1px 0 #d7caf8,
+       1px 1px 0 #d7caf8;
+  }
+
+a.button {
+    background: #523c7b;
+    text-decoration: none;
+    padding: 10px 15px;
+    color: #efefef;
+}
+
+.footer {
+    color: #686868;
+}
+
+div.userinfo {
+    background: #36393f;
+    color: rgb(220, 221, 222);
+    overflow: hidden;
+    padding: 20px;
+    border-radius: 70px;
+  }
+  
+  div.userinfo img {
+    float: left;
+    border-radius: 50%;
+    width: 90px;
+    margin-right: 30px;
+  }
+  
+  div.userinfo p.username {
+    font-size: 110%;
+    font-weight: bold;
+  }
+  
+  div.userinfo span.discriminator {
+    color: #a6a7a4;
+  }
+
+  .inputfile {
+	width: 0.1px;
+	height: 0.1px;
+	opacity: 0;
+	overflow: hidden;
+	position: absolute;
+	z-index: -1;
+}
+
+.inputfile + label, input[type=submit] {
+    background: #523c7b;
+    text-decoration: none;
+    padding: 10px 15px;
+    color: #efefef;
+    display: inline-block;
+    cursor: pointer;
+    border: 0px;
+    font-size: inherit;
+    font-family: inherit;
+}
+
+div.audiofile {
+    margin-top: 20px;
+    border: 5px solid #d7caf8;
+    padding: 20px 25px;
+}
+
+div.audiofile span {
+    margin-right: 50px;
+}
+
+div.uploadform {
+  position: relative;
+}
+
+div.uploadBlackout {
+  position: absolute;
+  width: 100%;
+  background:rgba(239,239,239,0.7);
+  z-index: 1000;
+  height: 100%;
+  overflow: hidden;
+  text-align: center;
+}
+
+div.uploadBlackout > img {
+  width: 65px;
+  margin-top: 10px;
+}

+ 181 - 0
handlers.js

@@ -0,0 +1,181 @@
+const requestlib = require ( 'request' );
+const formidable = require ( 'formidable' );
+const util = require ( 'util' );
+
+const Config = require ( './config.json' );
+const WebServer = require ( './index.js' );
+
+const fileutil = require ( './fileutil.js' );
+const logger = require ( './logger.js' );
+
+function verifyUserCredentials ( access_token, token_type, success, failure )
+{
+    requestlib.get ( {
+        url: 'https://discordapp.com/api/users/@me',
+        headers: { authorization: token_type + ' ' + access_token }
+    }, function ( error, res, body )
+    {
+        if ( !error ) 
+        {
+            success ( JSON.parse ( body ) );
+        }
+        else failure ( error );
+    } );
+}
+
+function displayApiError ( request, response, code, message )
+{
+    response.writeHead ( code, { 'Content-Type': 'application/json' } );
+    response.write ( JSON.stringify ( { success: false, message: message } ) );
+    response.end ( );
+}
+
+WebServer.registerRequestHandler ( '/process', function ( request, response, requestData, cookies, session )
+{
+    if ( request.method.toLowerCase ( ) == 'post' )
+    {
+        if ( !session.variables.discordAuth && Config.auth_required )
+        {
+            displayApiError ( request, response, 403, 'authentication required' );
+            return;
+        }
+
+        let form = new formidable.IncomingForm ( );
+        form.uploadDir = './uploads';
+        form.maxFileSize = 10 * 1024 * 1024;
+
+        form.parse ( request, function ( error, fields, files )
+        {
+            if ( error ) return displayApiError ( request, response, 400, 'invalid request' );
+
+            if ( !files.song )
+            {
+                response.writeHead ( 400, { 'Content-Type': 'application/json' } );
+                response.write ( JSON.stringify ( { success: false, message: 'no files uploaded' } ) );
+                response.end ( );
+
+                return;
+            }
+
+            let path = files.song.path;
+            fileutil.handleUploadedFile ( path, function ( )
+            {
+                // success
+                response.writeHead ( 200, { 'Content-Type': 'application/json' } );
+                response.write ( JSON.stringify ( { success: true, message: 'upload complete' } ) );
+                response.end ( );
+            }, function ( )
+            {
+                return displayApiError ( request, response, 400, 'invalid file format' );
+            } );
+        } );
+    }
+    else displayApiError ( request, response, 405, 'method not allowed' );
+} );
+
+WebServer.registerRequestHandler ( '/upload', function ( request, response, requestData, cookies, session ) 
+{
+    if ( session.variables.discordAuth )
+    {
+        response.writeHead ( 200, { 'Content-Type': 'text/html' } );
+        let userdata = session.variables.discordAuth.userdata;
+
+        WebServer.renderTemplate ( 'uploadform', request, response, 
+        {  
+            username: userdata.username,
+            useravatar: 'https://cdn.discordapp.com/avatars/' + userdata.id + '/' + userdata.avatar + '.png',
+            discriminator: userdata.discriminator
+        } );
+
+        response.end ( );
+    }
+    else
+    {
+        return WebServer.redirect ( request, response, '/' );
+    }
+} );
+
+WebServer.registerRequestHandler ( '/auth', function ( request, response, requestData, cookies, session )
+{
+    if ( session.variables.discordAuth ) return WebServer.redirect ( request, response, '/upload' );
+
+    if ( requestData.query && requestData.query.code && !session.variables.discordAuth )
+    {
+        let code = requestData.query.code;
+
+        requestlib.post ( { 
+            url: 'https://discordapp.com/api/oauth2/token',
+            form: 
+            {  
+                client_id: Config.client_id,
+                client_secret: Config.client_secret,
+                grant_type: 'authorization_code',
+                code: code,
+                redirect_uri: Config.discord_auth.redirect_uri,
+                scope: 'identify'
+            }
+        }, function ( error, res, body )
+        {
+            if ( !error )
+            {
+                let json = JSON.parse ( body );
+
+                if ( json.access_token )
+                {
+                    // return WebServer.redirect ( request, response, 'https://localhost:3000/upload' );
+                    verifyUserCredentials ( json.access_token, json.token_type, function ( userdata )
+                    {
+                        logger.log ( '[Info/WebServer] Authenticated discord user ' + userdata.id );
+
+                        session.variables.discordAuth = { };
+                        session.variables.discordAuth.data = json;
+                        session.variables.discordAuth.userdata = userdata;
+
+                        return WebServer.redirect ( request, response, 'upload' );
+                    }, function ( )
+                    {
+                        response.writeHead ( 200, { 'Content-Type': 'text/html' } );
+                        response.write ( '<h1>Auth Failed</h1>' );
+                        response.end ( );
+                    } );
+                }
+                else return WebServer.redirect ( request, response, '/' );             
+            }
+            else throw new Error ( error );
+        } );
+    }
+    else
+    {
+        logger.log ( '[Info/WebServer] Invalid Auth, no code provided. Redirecting...' );
+        return WebServer.redirect ( request, response, Config.discord_auth.redirect );
+    }
+} );
+
+WebServer.registerRequestHandler ( '/template_test', function ( request, response, requestData, cookies, session )
+{
+    response.writeHead ( 200, { 'Content-Type': 'text/html' } );
+
+    WebServer.renderTemplate ( 'uploadform', request, response, 
+    {  
+        username: 'huj',
+        useravatar: 'https://cdn.discordapp.com/avatars/276791868141076480/4a3736a3aa445bec61dde599040d0ec7.png',
+        discriminator: '6969'
+    } );
+
+    response.end ( );
+} );
+
+WebServer.registerRequestHandler ( '/session_test', function ( request, response, requestData, cookies, session )
+{
+    if ( !session.variables.testRandomNumber ) session.variables.testRandomNumber = Math.floor ( Math.random ( ) * 2000 );
+
+    response.writeHead ( 200, { 'Content-Type': 'text/html' } );
+    response.write ( '<h1>Session Data</h1>' );
+    response.write ( '<p>Session ID: ' + session.id + '</p>' );
+    response.write ( '<p>Session Started: ' + session.started + '</p>' );
+    response.write ( '<p>Session Expires: ' + session.expires + '</p>' );
+    response.write ( '<p>Lifetime Remaining: ' + ( session.expires - Date.now ( ) ) + '</p>' );
+    response.write ( '<p>Secret Number: ' + session.variables.testRandomNumber + '</p>' );
+    response.write ( '<p>Variables: ' + JSON.stringify ( session.variables ) + '</p>' )
+	response.end ( );
+} );

+ 234 - 0
index.js

@@ -0,0 +1,234 @@
+const MusicBot = require ( './musicbot.js' );
+const Config = require ( './config.json' );
+const logger = require ( './logger.js' );
+
+const https = require ( 'https' );
+const fs = require ( 'fs' );
+const url = require ( 'url' );
+const path = require ( 'path' );
+const crypto = require ( 'crypto' );
+const Cookies = require ( 'cookies' );
+
+logger.initialize ( );
+
+const mimeTypes =
+{
+	'js': 		'application/javascript',
+	'json': 	'application/json',
+	'ogg': 		'application/ogg',
+	'mp3': 		'audio/mpeg',
+	'wav': 		'audio/x-wav',
+	'gif': 		'image/gif',
+	'jpg': 		'image/jpeg',
+	'jpeg': 	'image/jpeg',
+	'png': 		'image/png',
+	'css': 		'text/css',
+	'html': 	'text/html',
+	'xml': 		'text/xml',
+	'txt':		'text/plain'
+};
+
+const options =
+{
+	key: fs.readFileSync ( Config.https.key ),
+	cert: fs.readFileSync ( Config.https.cert )
+};
+
+let requestHandlers = { };
+
+function renderTemplate ( template, request, response, data )
+{
+	let filepath = './templates/' + template + '.html';
+
+	if ( fs.existsSync ( filepath ) )
+	{
+		let buffer = fs.readFileSync ( filepath ).toString ( );
+
+		for ( let key in data )
+			buffer = buffer.replace ( new RegExp ( '{{' + key + '}}', 'g' ), data [key] );
+
+		response.write ( buffer );
+	}
+}
+
+function redirectRequest ( request, response, url )
+{
+	response.writeHead ( 302, { 
+		'Location': url
+	} );
+
+	response.end ( );
+}
+
+function registerRequestHandler ( path, handler )
+{
+	if ( typeof path !== "string" ) return false;
+	if ( typeof handler !== "function" ) return false;
+
+	if ( !requestHandlers [path] )
+	{
+		logger.log ( '[Info/WebServer] Registered request handler for ' + path );
+
+		requestHandlers [path] = handler;
+		return true;
+	}
+	
+	return false;
+}
+
+function getRequestData ( request )
+{
+	let requestData = { };
+	let urlObject = url.parse ( request.url, true );
+
+	requestData.path = urlObject.pathname;
+	requestData.query = urlObject.query;
+	requestData.ip = request.connection.remoteAddress;
+
+	return requestData;
+}
+
+function getMimeType ( filepath )
+{
+	let defaultMimeType = 'application/octet-stream';
+	let extension = path.extname ( filepath ).substring ( 1, this.length );
+
+	if ( mimeTypes [extension] ) return mimeTypes [extension];
+	else return defaultMimeType;
+}
+
+function createDefaultResponse ( request, response, requestData )
+{
+	let fullPath = './frontend' + requestData.path;
+	fullPath.replace ( /\.\./g, '' );
+	
+	if ( fs.existsSync ( fullPath ) && fs.lstatSync ( fullPath ).isDirectory ( ) )
+		fullPath = fullPath + 'index.html';
+
+	if ( fs.existsSync ( fullPath ) )
+	{
+		let mimeType = getMimeType ( fullPath );
+		let stat = fs.statSync ( fullPath );
+		
+		response.writeHead ( 200, 
+		{ 
+			'Content-Type': mimeType,
+			'Content-Length': stat.size
+		} );
+
+		let readStream = fs.createReadStream ( fullPath )
+		readStream.pipe ( response );
+	}
+	else
+	{
+		logger.log ( '[Info/WebServer] Outgoing Rsponse 404. File: ' + fullPath );
+			
+		// display error page
+		response.writeHead ( 404, { 'Content-Type': 'text/html' } );
+		response.write ( '<h1>Not Found</h1>' );
+		response.end ( );
+	}
+}
+
+let sessions = { };
+
+function terminateSession ( sessionID )
+{
+	logger.log ( '[Info/WebServer] Terminating session ' + sessionID );
+	delete sessions [sessionID];
+}
+
+function renewSession ( sessionID, cookies )
+{
+	logger.log ( '[Info/WebServer] Renewing session ' + sessionID );
+
+	clearTimeout ( sessions [sessionID].timeout );
+
+	sessions [sessionID].timeout = setTimeout ( function ( ) { terminateSession ( sessionID ); }, Config.session_duration );
+	sessions [sessionID].expires = Date.now ( ) + Config.session_duration;
+
+	cookies.set ( 'session_id', sessionID, { expires: new Date ( sessions [sessionID].expires ) } );
+}
+
+function getRequestSession ( request, requestData, cookies )
+{
+	let sessionID = cookies.get ( 'session_id' );
+
+	if ( sessionID )
+	{
+		if ( sessions [sessionID] )
+		{
+			renewSession ( sessionID, cookies );
+			return sessions [sessionID];
+		}
+	}
+	
+	sessionID = crypto.randomBytes ( 12 ).toString ( 'base64' );
+	let newSession = { };
+
+	newSession.id = sessionID;
+	newSession.started = Date.now ( );
+	newSession.expires = newSession.started + Config.session_duration;
+	newSession.timeout = setTimeout ( function ( ) { terminateSession ( sessionID ); }, Config.session_duration );
+
+	newSession.variables = { };
+	cookies.set ( 'session_id', sessionID, { expires: new Date ( newSession.expires ) } );
+
+	sessions [sessionID] = newSession;
+
+	logger.log ( '[Info/WebServer] Starting new session ' + sessionID );
+	return sessions [sessionID];
+}
+
+const server = https.createServer ( options, function ( request, response )
+{
+	let requestData = getRequestData ( request );
+	let cookies = Cookies ( request, response );
+	let session = getRequestSession ( request, response, cookies );
+
+	logger.log ( '[Info/WebServer] Incomming Request ' + requestData.path + ' from ' + requestData.ip );
+
+	if ( requestHandlers [requestData.path] ) 
+	{
+		try
+		{
+			requestHandlers [requestData.path] ( request, response, requestData, cookies, session );
+		} 
+		catch ( error )
+		{
+			logger.log ( '[Info/WebServer] Outgoing Rsponse 500' );
+			logger.error ( error );
+			
+			// display error page
+			response.writeHead ( 500, { 'Content-Type': 'text/html' } );
+			response.write ( '<h1>Internal Server Error</h1>' );
+			response.end ( );
+		}
+	}
+	else
+	{
+		createDefaultResponse ( request, response, requestData );
+	}
+} );
+
+module.exports.registerRequestHandler = registerRequestHandler;
+module.exports.redirect = redirectRequest;
+module.exports.renderTemplate = renderTemplate;
+
+require ( './handlers.js' );
+
+function runMusicBot ( )
+{
+	try 
+	{
+		MusicBot.run ( );
+	} 
+	catch ( error )
+	{
+		logger.error ( error );
+		setTimeout ( function ( ) { runMusicBot ( ); }, 500 );
+	}
+}
+
+server.listen ( 3000 );
+runMusicBot ( );

+ 45 - 0
logger.js

@@ -0,0 +1,45 @@
+const fs = require ( 'fs' );
+const util = require ( 'util' );
+
+let logFile = null;
+
+function initialize ( )
+{
+    let date = new Date ( );
+    let name = date.getFullYear ( ) + "_" + date.getMonth ( ) + "_" + date.getDay ( ) + "_" + date.getHours ( ) + "_" + date.getMinutes ( ) + "_r" + Math.floor ( Math.random ( ) * 1000 );
+
+    name = './logs/' + name + '.log.txt';
+
+    logFile = fs.createWriteStream ( name, { flags: 'w' } );
+}
+
+function logInternal ( message, prefix )
+{
+    let date = new Date ( );
+    let timestamp = date.getHours ( ) + ':' + date.getMinutes ( ) + ':' + date.getSeconds ( ) + ' ' + date.getDay ( ) + '/' + date.getMonth ( );
+
+    message = '[' + timestamp + '][' + prefix + ']' + message;
+
+    console.log ( message );
+    logFile.write ( message + '\n' );
+}
+
+function log ( message )
+{
+    logInternal ( message, 'INFO' );
+}
+
+function warn ( message )
+{
+    logInternal ( message, 'WARN' );
+}
+
+function error ( message )
+{
+    logInternal ( message, 'ERROR' );
+}
+
+module.exports.initialize = initialize;
+module.exports.log = log;
+module.exports.warn = warn;
+module.exports.error = error;

+ 171 - 0
musicbot.js

@@ -0,0 +1,171 @@
+const Discord = require ( 'discord.js' );
+const Client = new Discord.Client ( );
+
+const logger = require ( './logger.js' );
+
+const Config = require ( './config.json' );
+const fs = require ( 'fs' );
+
+let isVoiceChannel = false;
+let dispatcher = undefined;
+let voteSkips = 0;
+let voteSkipped = { };
+let globalVoiceChannel = null;
+
+let songs = [];
+
+function playMusic ( )
+{
+	generateSongList ( );
+	let song = getRandomSong ( );
+
+	logger.log ( '[Sound/automusic] now playing: ' + song );
+
+	let voiceChannel = Client.channels.get ( Config.channel_id );
+	globalVoiceChannel = voiceChannel;
+	
+	if ( voiceChannel instanceof Discord.VoiceChannel )
+	{
+		voiceChannel.join ( ).then ( ( connection ) =>
+		{
+			connection.on ( 'disconnect', function ( )
+			{
+				logger.log ( '[Info/automusic] disconnected from channel, will reconnect soon' );
+
+				setTimeout ( function ( )
+				{
+					logger.log ( '[Info/automusic] reconnecting' );
+					playMusic ( );
+				}, 3000 );
+			} );
+
+			globalConnection = connection;
+			
+			dispatcher = connection.play ( './playlist/' + song, { passes: 3 } );
+			dispatcher.on ( 'error', function ( m ) { logger.error ( m ); } );
+			
+			dispatcher.on ( 'end', ( ) => 
+			{
+				setTimeout ( function ( )
+				{
+					let announcer = connection.play ( './announcer.mp3', { passes: 3 } );
+					announcer.on ( 'error', function ( m ) { logger.error ( m ); } );
+
+					logger.log ( '[Info/automusic] playing announcer' );
+
+					announcer.on ( 'end', ( ) => 
+					{
+						voteSkips = 0;
+						voteSkipped = { };
+
+						logger.log ( '[Info/automusic] song concluded, playing another random song' );
+						setTimeout ( function ( ) { playMusic ( ); }, 500 );
+					} );
+				}, 1000 );
+			} );
+		} )
+		.catch ( ( error ) => {
+			logger.log ( '[Error/automusic] cannot join voice channel' );
+			logger.log ( error );
+		} );
+	}
+	else
+	{
+		logger.log ( '[Info/automusic] WARNING! ' + Config.channel_id + ' is not a valid voice channel id!' );
+	}
+}
+
+function getRandomSong ( )
+{
+	let rng = Math.floor ( Math.random ( ) * songs.length );
+	return songs [rng];
+}
+
+function generateSongList ( )
+{
+	fs.readdirSync ( './playlist' ).forEach ( function ( file )
+	{
+		songs.push ( file );
+	} );
+}
+
+Client.on ( 'ready', ( ) => 
+{
+	// client is ready
+	generateSongList ( );
+
+	logger.log ( '[Info/automusic] logged in!' );
+	Client.user.setActivity ( 'Minecraft' );
+	
+	playMusic ( );
+} );
+
+Client.on ( 'warning', function ( m ) { logger.warn ( m ); } )
+    .on ( 'error', function ( m ) { logger.error ( m ); } )
+
+    .on ( 'disconnect', ( ) => {
+        logger.warn ( '[Info/automusic] disconnected!' );
+    } );
+
+function formatResponse ( text )
+{
+    if ( typeof text === 'string' )
+        return text.replace( /`/g, "`" + String.fromCharCode ( 8203 ) ).replace ( /@/g, "@" + String.fromCharCode ( 8203 ) );
+    else
+        return text;
+}
+
+Client.on ( 'message', ( message ) => 
+{
+	// maintenance
+	if ( message.content.startsWith ( '$voteskip' ) )
+	{
+		let userid = message.author.id;
+		let onlineusers = globalVoiceChannel.members.size;
+		let required = Math.floor ( ( onlineusers - 1 ) / 2 );
+
+		if ( !voteSkipped [userid] )
+		{
+			voteSkips++;
+			message.channel.send ( "Zarejestrowano twój glos **" + voteSkips + "/" + ( required + 1 ) + "**" );
+
+			voteSkipped [userid] = true;
+
+			if ( voteSkips > Math.floor ( ( onlineusers - 1 ) / 2 ) )
+			{
+				message.channel.send ( "Głosem większości dostępnych piosenka została pominięta." );
+				dispatcher.end ( );
+			}
+		}
+	}
+
+	if ( message.content.startsWith ( '$skip' ) && message.author.id === '276791868141076480' )
+	{
+		dispatcher.end ( );
+		return;
+	}
+
+	if ( message.content.startsWith ( '$eval ' ) && message.author.id === '276791868141076480' )
+	{
+		try
+		{
+			let payload = message.content.substring ( 6, message.content.length );
+			let output = true;
+			let result = eval ( payload );
+			
+			if ( output )
+				message.channel.send ( formatResponse ( result ), { code: 'xl' } );
+		} 
+		catch ( error )
+		{
+			message.channel.send ( '```' + formatResponse ( error ) + '```' );
+		}
+		
+		return;
+	}
+} );
+
+module.exports.run = function ( )
+{
+	Client.login ( Config.token );
+}

File diff suppressed because it is too large
+ 1994 - 0
package-lock.json


+ 19 - 0
package.json

@@ -0,0 +1,19 @@
+{
+  "name": "automusic",
+  "version": "1.0.0",
+  "description": "automatic music bot",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "rekjn",
+  "license": "ISC",
+  "dependencies": {
+    "cookies": "^0.8.0",
+    "discord.js": "github:discordjs/discord.js",
+    "ffmpeg": "0.0.4",
+    "formidable": "^1.2.1",
+    "opusscript": "0.0.7",
+    "request": "^2.88.0"
+  }
+}

+ 20 - 0
run.js

@@ -0,0 +1,20 @@
+const cluster = require ( 'cluster' );
+
+if ( cluster.isMaster )
+{
+    cluster.fork ( );
+
+    cluster.on ( 'exit', function ( worker, code, signal ) 
+    {
+        console.log ( '[Info/cluster] restarting process after unhandled exception (in 8.5 seconds)' );
+        setTimeout ( function ( ) 
+        { 
+            console.log ( '[Info/cluster] process restarted' );
+            cluster.fork ( ); 
+        }, 8500 );
+    } );
+}
+else
+{
+    require ( './index.js' );
+}

+ 143 - 0
templates/uploadform.html

@@ -0,0 +1,143 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="utf-8" />
+        <link rel="stylesheet" href="style.css" type="text/css" />
+        <link rel="stylesheet" href="modal.css" type="text/css" />
+        <script src="modal.js"></script>
+        <title>radio patapedia</title>
+        <script>
+            // frontend
+            let isUploading = false;
+
+            window.addEventListener ( 'load', function ( )
+            {
+                document.querySelector ( '#uploadForm' ).addEventListener ( 'change', function ( ev )
+                {
+                    let upload = document.querySelector ( '#file' );
+                    let label = document.querySelector ( '#desc' );
+
+                    if ( upload.files.length > 0 )
+                    {
+                        let name = upload.files [0].name;
+                        label.innerHTML = name;
+                    }
+                } );
+
+                document.querySelector ( '#uploadForm' ).addEventListener ( 'submit', function ( ev )
+                {
+                    ev.preventDefault ( );
+
+                    let formData = new FormData ( this );
+                    let blackout = document.querySelector ( '#uploadBlackout' );
+                    
+                    if ( !isUploading )
+                    {
+                        let request = new XMLHttpRequest ( );
+                        request.withCredentials = true;
+
+                        blackout.style.display = 'block';
+
+                        isUploading = true;
+                        request.open ( 'POST', 'process' );
+
+                        request.addEventListener ( 'readystatechange', function ( reqEvent )
+                        {
+                            if ( request.readyState == XMLHttpRequest.DONE )
+                            {
+                                if ( request.status == 200 )
+                                {
+                                    modalbox.displayModal ( 'progressDialog', 'Przesyłanie zakończone', '<p>Twoja piosenka została dodana do puli. Niedługo powinna pojawić się na czacie głosowym ♪</p>', 
+                                        [ 
+                                            { id: 'progressDialogOk', message: 'Ok', handler: function ( ev ) { 
+                                                window.location.href = '/';
+                                            } }
+                                        ] 
+                                    );
+                                }
+                                else
+                                {
+                                    try 
+                                    { 
+                                        let jsonResult = JSON.parse ( request.responseText );
+
+                                        if ( jsonResult.message ) 
+                                        {
+                                            modalbox.displayModal ( 'progressDialog', 'Wystąpił błąd', '<p>Podczas przesyłania twojej piosenki wystąpił błąd!</p><p>Serwer pozostawił komentarz: ' + jsonResult.message + '</p>', 
+                                                [ 
+                                                    { id: 'progressDialogOk', message: 'Ok', handler: function ( ev ) { 
+                                                        modalbox.closeModal ( this );
+                                                    } }
+                                                ] 
+                                            );
+                                        }
+                                        else 
+                                        {
+                                            modalbox.displayModal ( 'progressDialog', 'Wystąpił błąd', '<p>Podczas przesyłania twojej piosenki wystąpił błąd!</p><p>Serwer nie pozostawił żadnego komentarza. Prosimy spróbować ponownie później.</p>', 
+                                                [ 
+                                                    { id: 'progressDialogOk', message: 'Ok', handler: function ( ev ) { 
+                                                        modalbox.closeModal ( this );
+                                                    } }
+                                                ] 
+                                            );
+                                        }
+                                    }
+                                    catch ( error )
+                                    {
+                                        modalbox.displayModal ( 'progressDialog', 'Wystąpił błąd', '<p>Podczas przesyłania twojej piosenki wystąpił błąd!</p><p>Wystąpił nieznany błąd po stronie klienta</p>', 
+                                            [ 
+                                                { id: 'progressDialogOk', message: 'Ok', handler: function ( ev ) { 
+                                                    modalbox.closeModal ( this );
+                                                } }
+                                            ] 
+                                        );
+                                    }
+
+                                    blackout.style.display = 'none';
+                                    isUploading = false;
+                                }
+                            }
+                        } );
+
+                        request.send ( formData );
+                    }
+                } );
+            } );
+        </script>
+    </head>
+    <body>
+        <div class="backgroundCover"></div>
+        <div class="bodyWrapper">
+            <div class="siteHeader">
+                <div class="siteHeaderWordmark">
+                    radio patapedia
+                </div>
+            </div>
+            <div class="siteMain">
+                <h1>Przesyłanie Pliku</h1>
+                <p>Plik musi być w formacie MP3. Inne formaty audio nie są obsługiwane. Wielokrotne przesyłanie niepoprawnych plików zakończy się automatyczną blokadą konta.</p>
+                <div class="userinfo">
+                    <img src="{{useravatar}}" alt="Avatar">
+                    <p>Przesyłasz plik jako</p>
+                    <p class="username"><span class="name">{{username}}</span><span class="discriminator">#{{discriminator}}</span></p>
+                </div>
+                <div class="uploadform">
+                    <div id="uploadBlackout" class="uploadBlackout" style="display:none">
+                        <img src="loading.gif" alt="Loading..." />
+                    </div>
+                    <form method="POST" action="process" id="uploadForm">
+                        <div class="audiofile">
+                            <span id="desc">Nie wybrano pliku</span>
+                            <input type="file" name="song" id="file" class="inputfile" accept="audio/mpeg" />
+                            <label for="file" id="uploadLabel">Wybierz plik</label>
+                            <input type="submit" value="Rozpocznij Przesyłanie" />
+                        </div>
+                    </form>
+                </div>
+                <div class="footer">
+                    <p>Strona i aplikacja stworzona przez rekjna. Tło ukradłem z deviantarta owoca.</p>
+                </div>
+            </div>
+        </div>
+    </body>
+</html>