diff --git a/bot.ts b/bot.ts index 4f9025d..72f9739 100644 --- a/bot.ts +++ b/bot.ts @@ -16,7 +16,6 @@ import { } from '@discordjs/voice'; import { ChannelType, Collection, GuildBasedChannel, Snowflake, VoiceChannel, VoiceState } from 'discord.js'; import * as dotenv from 'dotenv'; -import { startServer as startWebServer } from './client/webserver'; import * as schedule from 'node-schedule'; import { loadAvoidList } from './helpers/load-avoid-list'; import { LoggerColors } from './helpers/logger-colors'; @@ -25,10 +24,11 @@ import { logger } from './helpers/logger'; import { SetupDiscordCLient } from './helpers/setup-discord-client'; import { convertHoursToMinutes, dateToString } from './helpers/converters'; import { AvoidList } from './models/avoid-list'; +import { runServer } from './client/router'; dotenv.config(); -export var nextPlayBackTime: string = ''; // Export so it can be used in the webserver module aswell. +export var nextPlayBackTime: string = 'Never played'; // Export so it can be used in the webserver module aswell. const minTimeInterval = parseInt(process.env.INTERVALMIN_MINUTES!, 10); // Minimum interval in minutes. const maxTimeInterval = convertHoursToMinutes(parseFloat(process.env.INTERVALMAX_HOURS!)); // Maximum interval in minutes. const voiceChannelRetries = parseInt(process.env.VOICECHANNELRETRIES!, 10); // Number of retries to find a voice channel with members in it. @@ -39,7 +39,7 @@ discordClient.on('ready', async () => { console.log(`Logged in as ${discordClient.user?.tag}!`); joinRandomChannel(voiceChannelRetries); - startWebServer(); + runServer(); }); /** diff --git a/client-v2/client/src/app/components/login/login.service.ts b/client-v2/client/src/app/components/login/login.service.ts new file mode 100644 index 0000000..59e9ec0 --- /dev/null +++ b/client-v2/client/src/app/components/login/login.service.ts @@ -0,0 +1,9 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class DiscordService { + constructor(private http: HttpClient) {} +} \ No newline at end of file diff --git a/client/controllers/authentication.ts b/client/controllers/authentication.ts new file mode 100644 index 0000000..50587e6 --- /dev/null +++ b/client/controllers/authentication.ts @@ -0,0 +1,100 @@ +import express from 'express'; +import ip from 'ip'; +import DiscordOauth2 from 'discord-oauth2'; +import { ssl } from '../server'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +const router = express.Router(); +const oauth = new DiscordOauth2(); + +var clientId = process.env.OAUTH_CLIENT_ID; +var clientSecret = process.env.OAUTH_CLIENT_SECRET; +var customRedirectUrl = process.env.OAUTH_REDIRECT_URI; +var scopes = "identify email guilds"; + +router.get('/login', (req, res) => { + if(!variablesIsSet(res)) + return; + + var redirect = `${ssl}://${ip.address()}/oauth` + + if(customRedirectUrl && customRedirectUrl != "default") + redirect = `${customRedirectUrl}/oauth` + + res.redirect(`https://discordapp.com/api/oauth2/authorize?client_id=${clientId}&scope=${scopes.replace(' ', '+')}&response_type=code&redirect_uri=${redirect}`); +}); + +router.get('/oauth', async (_req: any, res: express.Response) => { + if(!variablesIsSet(res)) + return; + + var options = { + url: 'https://discord.com/api/oauth2/token', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + 'client_id': clientId!, + 'client_secret': clientSecret!, + 'grant_type': 'client_credentials', + 'code': _req.query.code, + 'redirect_uri': `${ssl}://${ip.address()}`, + 'scope': scopes + }) + } + + var response = await fetch('https://discord.com/api/oauth2/token', options) + .then((response) => { + return response.json(); + }); + + res.send(response); +}); + +router.get('/userinfo', async (req, res) => { + const token = req.headers.authorization; + + if(token == null || token == undefined) + return res.sendStatus(401); + + var cleanToken = token.replace('Bearer ', ''); + + const user = await oauth.getUser(cleanToken); + + res.send(user); +}); + +router.get('/guilds', async (req, res) => { + const token = req.headers.authorization; + + if(token == null || token == undefined) + return res.sendStatus(401); + + var cleanToken = token.replace('Bearer ', ''); + + const guilds = await oauth.getUserGuilds(cleanToken); + + res.send(guilds); +}); + +function variablesIsSet(response: any): boolean { + let errorText = "Invalid configuration. Please check your environment variables."; + + if(clientId == undefined || clientSecret == undefined) { + response.send(errorText); + + return false; + } + + if(clientId.length < 5 && clientSecret.length < 5) { + response.send(errorText); + return false; + } + + return true; +} + +export default router; diff --git a/client/controllers/index.ts b/client/controllers/index.ts new file mode 100644 index 0000000..7b1109a --- /dev/null +++ b/client/controllers/index.ts @@ -0,0 +1,44 @@ +import express from 'express'; +import path from 'path'; +import { Handlers } from "../handlers/index"; +import { nextPlayBackTime } from '../../bot'; +import { loadAvoidList } from '../../helpers/load-avoid-list'; + +const router = express.Router(); + +/** + * Returns the next playback time. + * @returns string - The next playback time. + */ +router.get('/nextplaybacktime', (_req, res) => { + res.send(nextPlayBackTime); +}); + +/** + * Returns the index.html file. + * @returns index.html - The index.html file. + */ +router.get('/', (_req, res) => { + res.sendFile(path.join(__dirname, '../web/index.html')); +}); + +router.get('/avoidlist', (_req, res) => { + res.send(loadAvoidList()); +}); + +router.post('/avoidlist', (req, res) => { + Handlers.AddUserToAvoidList(res, req); +}); + +router.delete('/avoidlist/:user', (req, res) => { + Handlers.DeleteUserFromAvoidList(res, req); +}); + +router.get('/join', (_req, res) => { + Handlers.JoinChannel(res); +}); + +router.use(express.static(path.join(__dirname, "../web"))); + + +export default router; \ No newline at end of file diff --git a/client/controllers/sounds.ts b/client/controllers/sounds.ts new file mode 100644 index 0000000..388d62c --- /dev/null +++ b/client/controllers/sounds.ts @@ -0,0 +1,22 @@ +import express from 'express'; +import path from 'path'; +import { Handlers } from "../handlers/index"; + +const router = express.Router(); + +router.delete('/sounds/:filename', (_req, res) => { + Handlers.DeleteSoundFile(res, _req); +}); + +/** + * Returns a file from the sounds folder by filename + * @param filename - The name of the file to return. + * @returns mp3 - The requested file. + */ +router.use('/sounds', express.static(path.join(__dirname, '../../sounds'))); + +router.get('/sounds', (_req, res: express.Response) => { + return Handlers.GetSoundFiles(res); +}); + +export default router; \ No newline at end of file diff --git a/client/controllers/upload.ts b/client/controllers/upload.ts new file mode 100644 index 0000000..4da89ad --- /dev/null +++ b/client/controllers/upload.ts @@ -0,0 +1,44 @@ +import express from 'express'; +import multer, { diskStorage } from 'multer'; +import path from 'path'; +import { Handlers } from "../handlers/index"; +import { generateFileName } from '../../helpers/generate-file-name'; + +const router = express.Router(); + +const storage = diskStorage({ + destination: 'sounds/', + filename: function (_req, file, cb) { + cb(null, generateFileName(file.originalname)); + } +}); + +const upload = multer({ + storage: storage, + limits: { fileSize: 1 * 1024 * 1024 }, + fileFilter: function (_req, file, cb) { + if (path.extname(file.originalname) !== '.mp3') { + return cb(new Error('Only .mp3 files are allowed')); + } + + cb(null, true); + } +}); + +/** + * Uploads a file to the sounds folder. + * @Body myFile - The file to upload. + */ +router.post('/upload', upload.single('myFile'), async (req, res) => { + res.send('File uploaded successfully.'); +}); + +router.post('/youtube', async (req, res) => { + await Handlers.UploadYouTubeFile(res, req); +}); + +router.post('/upload-youtube', async (req, res) => { + await Handlers.UploadYouTubeFile(res, req); +}); + +export default router; \ No newline at end of file diff --git a/client/router.ts b/client/router.ts new file mode 100644 index 0000000..0b31b66 --- /dev/null +++ b/client/router.ts @@ -0,0 +1,23 @@ +import express from 'express'; +import bodyParser from 'body-parser'; +import { startServer } from './server'; + +const app = express(); +app.use(bodyParser.json()); + +// Import routes +import indexRoutes from './controllers/index'; +import uploadRoutes from './controllers/upload'; +import soundsRoutes from './controllers/sounds'; +import authRoutes from './controllers/authentication'; + +// Use routes +export function runServer() { + app.use('/', indexRoutes); + app.use('/', uploadRoutes); + app.use('/', soundsRoutes); + app.use('/', authRoutes); + app.listen(3040); + + startServer(app); +} diff --git a/client/server.ts b/client/server.ts new file mode 100644 index 0000000..b4fe320 --- /dev/null +++ b/client/server.ts @@ -0,0 +1,33 @@ +import https from 'https'; +import fs from 'fs'; +import path from 'path'; +import { LoggerColors } from '../helpers/logger-colors'; +import ip from 'ip'; +import { Express } from 'express'; + +export var ssl: "https" | "http" = "http"; + +export function startServer(app: Express) { + let port: 80 | 443 = 80; + let server; + + try { + const options = { + requestCert: true, + rejectUnauthorized: false, + key: fs.readFileSync(path.join(__dirname, '/certs/key.pem')), + cert: fs.readFileSync(path.join(__dirname, '/certs/cert.pem')), + }; + server = https.createServer(options, app); + ssl = "https"; + port = 443; + } catch (error) { + console.log(LoggerColors.Yellow, 'Could not find SSL certificates, falling back to http.'); + server = app; + ssl = "http"; + } + + server.listen(port, () => { + console.log(`Server started at ${ssl}://${ip.address()}:${port}`); + }); +} diff --git a/client/webserver.ts b/client/webserver.ts deleted file mode 100644 index 8077b03..0000000 --- a/client/webserver.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { nextPlayBackTime } from './../bot'; -import express from 'express'; -import multer, { diskStorage } from 'multer'; -import path from 'path'; -import fs from 'fs'; -import bodyParser from 'body-parser'; -import https from 'https'; -import ip from 'ip'; - -import { Handlers } from "./handlers/index" -import { loadAvoidList } from '../helpers/load-avoid-list'; -import { LoggerColors } from '../helpers/logger-colors'; -import { generateFileName } from '../helpers/generate-file-name'; - -const app = express(); -const storage = diskStorage({ - destination: 'sounds/', - filename: function (_req, file, cb) { - cb(null, generateFileName(file.originalname)); - } -}); - -app.use(bodyParser.json()); - -const upload = multer({ - storage: storage, - limits: { fileSize: 1 * 1024 * 1024 }, - fileFilter: function (_req, file, cb) { - if (path.extname(file.originalname) !== '.mp3') { - return cb(new Error('Only .mp3 files are allowed')); - } - - cb(null, true); - } -}); - -/** - * Returns the index.html file. - * @returns index.html - The index.html file. - */ -app.get('/', (_req, res) => { - res.sendFile(path.join(__dirname, 'web/index.html')); -}); - -/** - * Uploads a file to the sounds folder. - * @Body myFile - The file to upload. - */ -app.post('/upload', upload.single('myFile'), async (req, res) => { - res.send('File uploaded successfully.'); -}); - -app.post('/upload-youtube', async (req, res) => { - await Handlers.UploadYouTubeFile(res, req); -}); - -/** - * Returns a file from the sounds folder by filename - * @param filename - The name of the file to return. - * @returns mp3 - The requested file. - */ -app.use('/sounds', express.static(path.join(__dirname, '../sounds'))); - -app.get('/sounds', (_req, res: express.Response) => { - return Handlers.GetSoundFiles(res); -}); - -/** - * Returns the next playback time. - * @returns string - The next playback time. - */ -app.get('/nextplaybacktime', (_req, res) => { - res.send(nextPlayBackTime); -}); - -app.delete('/sounds/:filename', (_req, res) => { - Handlers.DeleteSoundFile(res, _req); -}); - -app.use(express.static(path.join(__dirname, "web"))); - -app.get('/join', (_req, res) => { - Handlers.JoinChannel(res); -}); - -app.post('/avoidlist', (req, res) => { - Handlers.AddUserToAvoidList(res, req); -}); - -app.delete('/avoidlist/:user', (req, res) => { - Handlers.DeleteUserFromAvoidList(res, req); -}); - -app.get('/avoidlist', (_req, res) => { - res.send(loadAvoidList()); -}); - -/** - * Starts the web server on either http or https protocol based on the availability of SSL certificates. - * @returns void - */ -export function startServer() { - let port: 80 | 443 = 80; - let server; - let ssl: "https" | "http" = "http"; - - try { - const options = { - requestCert: true, - rejectUnauthorized: false, - key: fs.readFileSync(path.join(__dirname, '/certs/key.pem')), - cert: fs.readFileSync(path.join(__dirname, '/certs/cert.pem')), - }; - server = https.createServer(options, app); - ssl = "https"; - port = 443; - } catch (error) { - console.log(LoggerColors.Yellow, 'Could not find SSL certificates, falling back to http.'); - server = app; - ssl = "http"; - } - - server.listen(port, () => { - console.log(`Server started at ${ssl}://${ip.address()}:${port}`); - }); -} \ No newline at end of file diff --git a/package.json b/package.json index 020a722..ab2bf8c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Discord bot randomly plays sounds", "scripts": { "start": "ts-node bot.ts", - "create": "node -e \"require('fs').writeFileSync('.env', 'TOKEN=MY-API-TOKEN\\nINTERVALMIN_MINUTES=10\\nINTERVALMAX_HOURS=6\\nVOICECHANNELRETRIES=12')\"", + "create": "node -e \"require('fs').writeFileSync('.env', 'TOKEN=MY-API-TOKEN\\nINTERVALMIN_MINUTES=10\\nINTERVALMAX_HOURS=6\\nVOICECHANNELRETRIES=12\\nOAUTH_CLIENT_ID=\\nOAUTH_CLIENT_SECRET=\\nOAUTH_REDIRECT_URI=\"\"default\"\"')\"", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "SocksOnHead", @@ -15,6 +15,7 @@ "@types/multer": "~1.4.8", "@types/node": "~20.8.2", "body-parser": "^1.20.2", + "discord-oauth2": "^2.12.1", "discord.js": "~14.13.0", "dotenv": "~16.3.1", "express": "~4.18.2",