diff --git a/README.ko.md b/README.ko.md index 9b69757..6bcdb71 100644 --- a/README.ko.md +++ b/README.ko.md @@ -16,6 +16,11 @@ Language : [English](README.md), Korean(한국어) [Patreon](https://www.patreon.com/dapucita)을 통해 이 프로젝트에 기부하고 지원해주세요! ## 특징 +![Node.js](https://img.shields.io/badge/-Node.js-339933?style=for-the-badge&logo=node%2ejs&logoColor=fff) +![Typescript](https://img.shields.io/badge/-Typescript-007acc?style=for-the-badge&logo=typescript&logoColor=fff) +![React](https://img.shields.io/badge/-React-61dafb?style=for-the-badge&logo=react&logoColor=fff) +![SQLite](https://img.shields.io/badge/-SQLite-003b57?style=for-the-badge&logo=sqlite&logoColor=fff) +![Chrome](https://img.shields.io/badge/-Chrome-4285f4?style=for-the-badge&logo=google%20chrome&logoColor=fff) - Windows, Linux, OS X 등 멀티 플랫폼 지원 - 웹 기반 관리 시스템 - 게임 자동 운영 @@ -30,7 +35,7 @@ Language : [English](README.md), Korean(한국어) - [Game Playing](https://github.com/dapucita/haxbotron/wiki/Game-Playing) - [Core Server](https://github.com/dapucita/haxbotron/wiki/%5BKorean%5D-Core-Server) - [DB Server](https://github.com/dapucita/haxbotron/wiki/DB-Server) -- ... [위키](https://github.com/dapucita/haxbotron/wiki)와 `docs` 폴더에 있는 문서도 참고하세요 +- ... [위키](https://github.com/dapucita/haxbotron/wiki)도 참고하세요 ## 사용하기 [처음 시작하기](https://github.com/dapucita/haxbotron/wiki/%5BKorean%5D-%EC%B2%98%EC%9D%8C-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0) 문서에 따라 과정을 진행하면 쉽게 `Haxbotron`을 시작할 수 있습니다. diff --git a/README.md b/README.md index 3ab85cc..a68a947 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Please donate and support this project by [Patreon](https://www.patreon.com/dapu - [Game Playing](https://github.com/dapucita/haxbotron/wiki/Game-Playing) - [Core Server](https://github.com/dapucita/haxbotron/wiki/Core-Server) - [DB Server](https://github.com/dapucita/haxbotron/wiki/DB-Server) -- ...and check also our [wiki](https://github.com/dapucita/haxbotron/wiki) and documents in `docs`. +- ...and check also our [wiki](https://github.com/dapucita/haxbotron/wiki). ## How to Use You can easily start by following the document [Getting Started](https://github.com/dapucita/haxbotron/wiki/Getting-Started). diff --git a/core/app.ts b/core/app.ts index d59042b..8734452 100644 --- a/core/app.ts +++ b/core/app.ts @@ -34,16 +34,6 @@ const coreServerSettings = { const buildOutputDirectory = path.resolve(__dirname, "../public"); const whiteListIPs: string[] = process.env.SERVER_WHITELIST_IP?.split(",") || ['127.0.0.1']; -/* -if (process.env.TWEAKS_GEOLOCATIONOVERRIDE && JSON.parse(process.env.TWEAKS_GEOLOCATIONOVERRIDE.toLowerCase()) === true) { - hostRoomConfig.geo = { - code: process.env.TWEAKS_GEOLOCATIONOVERRIDE_CODE || "KR" - , lat: parseFloat(process.env.TWEAKS_GEOLOCATIONOVERRIDE_LAT || "37.5665") - , lon: parseFloat(process.env.TWEAKS_GEOLOCATIONOVERRIDE_LON || "126.978") - } -} -*/ - nodeStorage.init(); browser.attachSIOserver(sio); diff --git a/core/game/bot.ts b/core/game/bot.ts index 12c9f91..80ad44c 100644 --- a/core/game/bot.ts +++ b/core/game/bot.ts @@ -23,10 +23,18 @@ window.gameRoom = { _room: window.HBInit(loadedConfig._config) ,config: loadedConfig ,link: '' - ,stadiumData:{ + ,stadiumData: { default: localStorage.getItem('_defaultMap')! ,training: localStorage.getItem('_readyMap')! } + ,bannedWordsPool: { + nickname: [] + ,chat: [] + } + ,teamColours: { + red: { angle: 0, textColour: 0xffffff, teamColour1: 0xe66e55, teamColour2: 0xe66e55, teamColour3: 0xe66e55 } + ,blue: { angle: 0, textColour: 0xffffff, teamColour1: 0x5a89e5, teamColour2: 0x5a89e5, teamColour3: 0x5a89e5 } + } ,logger: Logger.getInstance() ,isStatRecord: false ,isGamingNow: false @@ -87,9 +95,10 @@ var scheduledTimer = setInterval(() => { placeholderScheduler.targetName = player.name; // check muted player and unmute when it's time to unmute - if (player.permissions.mute === true && nowTimeStamp > player.permissions.muteExpire) { + if (player.permissions.mute === true && player.permissions.muteExpire !== -1 && nowTimeStamp > player.permissions.muteExpire) { player.permissions.mute = false; //unmute window.gameRoom._room.sendAnnouncement(Tst.maketext(LangRes.scheduler.autoUnmute, placeholderScheduler), null, 0x479947, "normal", 0); //notify it + window._emitSIOPlayerStatusChangeEvent(player.id); } // when afk too long kick option is enabled, then check sleeping with afk command and kick if afk too long diff --git a/core/game/controller/TextFilter.ts b/core/game/controller/TextFilter.ts new file mode 100644 index 0000000..3d95b34 --- /dev/null +++ b/core/game/controller/TextFilter.ts @@ -0,0 +1,32 @@ +import { AhoCorasick } from '../model/TextFilter/filter' + +/** + * Check if given string is already used in the game room (check duplicated nickname) + * @param compare plain text to be compared + * @returns return `true` when already in use + */ +export function isExistNickname(compare: string): boolean { + for (let eachPlayer of window.gameRoom.playerList.values()) { + if(eachPlayer.name.trim() === compare.trim()) { + return true; + } + } + return false; +} + +/** + * Check if given string includes banned words from nickname filter + * @param pool banned words list + * @param compare plain text to be compared + * @returns return `true` when includes banned word(s) + */ +export function isIncludeBannedWords(pool: string[], compare: string): boolean { + const ac = new AhoCorasick(pool); + const results = ac.search(compare); + + if(Array.isArray(results) && results.length) { + return true; + } else { + return false; + } +} diff --git a/core/game/controller/commands/afk.ts b/core/game/controller/commands/afk.ts index bd35750..df57093 100644 --- a/core/game/controller/commands/afk.ts +++ b/core/game/controller/commands/afk.ts @@ -64,4 +64,6 @@ export function cmdAfk(byPlayer: PlayerObject, message?: string): void { window.gameRoom.isStatRecord = false; } } + + window._emitSIOPlayerStatusChangeEvent(byPlayer.id); } diff --git a/core/game/controller/commands/freeze.ts b/core/game/controller/commands/freeze.ts index 748031b..87f30a9 100644 --- a/core/game/controller/commands/freeze.ts +++ b/core/game/controller/commands/freeze.ts @@ -10,6 +10,8 @@ export function cmdFreeze(byPlayer: PlayerObject): void { window.gameRoom.isMuteAll = true; //on window.gameRoom._room.sendAnnouncement(LangRes.command.freeze.onFreeze, null, 0x479947, "normal", 1); } + + window._emitSIOPlayerStatusChangeEvent(byPlayer.id); } else { window.gameRoom._room.sendAnnouncement(LangRes.command.freeze._ErrorNoPermission, byPlayer.id, 0xFF7777, "normal", 2); } diff --git a/core/game/controller/commands/mute.ts b/core/game/controller/commands/mute.ts index 6d72e82..5420b47 100644 --- a/core/game/controller/commands/mute.ts +++ b/core/game/controller/commands/mute.ts @@ -32,6 +32,8 @@ export function cmdMute(byPlayer: PlayerObject, message?: string): void { window.gameRoom._room.sendAnnouncement(Tst.maketext(LangRes.command.mute.successMute, placeholder), null, 0x479947, "normal", 1); } } + + window._emitSIOPlayerStatusChangeEvent(byPlayer.id); } else { window.gameRoom._room.sendAnnouncement(LangRes.command.mute._ErrorNoPlayer, byPlayer.id, 0xFF7777, "normal", 2); } diff --git a/core/game/controller/commands/super.ts b/core/game/controller/commands/super.ts index cfcd60a..6e72e73 100644 --- a/core/game/controller/commands/super.ts +++ b/core/game/controller/commands/super.ts @@ -13,6 +13,8 @@ export async function cmdSuper(byPlayer: PlayerObject, message?: string, submess //setPlayerData(playerList.get(playerID)); // update window.gameRoom._room.sendAnnouncement(LangRes.command.super.loginSuccess, byPlayer.id, 0x479947, "normal", 2); window.gameRoom.logger.i('super', `${byPlayer.name}#${byPlayer.id} did successfully login to super admin with the key. (KEY ${submessage})`); + + window._emitSIOPlayerStatusChangeEvent(byPlayer.id); } else { window.gameRoom.playerList.get(byPlayer.id)!.permissions.malActCount++; // add malicious behaviour count window.gameRoom._room.sendAnnouncement(LangRes.command.super.loginFail, byPlayer.id, 0xFF7777, "normal", 2); @@ -39,6 +41,8 @@ export async function cmdSuper(byPlayer: PlayerObject, message?: string, submess //setPlayerData(playerList.get(playerID)); // update window.gameRoom._room.sendAnnouncement(LangRes.command.super.logoutSuccess, byPlayer.id, 0x479947, "normal", 2); window.gameRoom.logger.i('super', `${byPlayer.name}#${byPlayer.id} did logout from super admin.`); + + window._emitSIOPlayerStatusChangeEvent(byPlayer.id); } else { window.gameRoom._room.sendAnnouncement(LangRes.command.super._ErrorNoPermission, byPlayer.id, 0xFF7777, "normal", 2); } diff --git a/core/game/controller/events/onPlayerAdminChange.ts b/core/game/controller/events/onPlayerAdminChange.ts index 2c50549..a83de73 100644 --- a/core/game/controller/events/onPlayerAdminChange.ts +++ b/core/game/controller/events/onPlayerAdminChange.ts @@ -32,4 +32,6 @@ export function onPlayerAdminChangeListener(changedPlayer: PlayerObject, byPlaye if(window.gameRoom.config.rules.autoAdmin === true) { // if auto admin option is enabled updateAdmins(); // check when the last admin player disqulified by self } + + window._emitSIOPlayerStatusChangeEvent(changedPlayer.id); } diff --git a/core/game/controller/events/onPlayerChat.ts b/core/game/controller/events/onPlayerChat.ts index 819d75b..60c62d8 100644 --- a/core/game/controller/events/onPlayerChat.ts +++ b/core/game/controller/events/onPlayerChat.ts @@ -5,6 +5,7 @@ import { PlayerObject } from "../../model/GameObject/PlayerObject"; import { isCommandString, parseCommand } from "../Parser"; import { getUnixTimestamp } from "../Statistics"; import { convertTeamID2Name, TeamID } from "../../model/GameObject/TeamID"; +import { isIncludeBannedWords } from "../TextFilter"; export function onPlayerChatListener(player: PlayerObject, message: string): boolean { // Event called when a player sends a chat message. @@ -60,6 +61,8 @@ export function onPlayerChatListener(player: PlayerObject, message: string): boo window.gameRoom.playerList.get(player.id)!.permissions['mute'] = true; // mute this player window.gameRoom.playerList.get(player.id)!.permissions.muteExpire = nowTimeStamp + window.gameRoom.config.settings.muteDefaultMillisecs; //record mute expiration date by unix timestamp window.gameRoom._room.sendAnnouncement(Tst.maketext(LangRes.antitrolling.chatFlood.muteReason, placeholderChat), null, 0xFF0000, "normal", 1); // notify that fact + + window._emitSIOPlayerStatusChangeEvent(player.id); return false; } } @@ -68,6 +71,16 @@ export function onPlayerChatListener(player: PlayerObject, message: string): boo window.gameRoom._room.sendAnnouncement(Tst.maketext(LangRes.onChat.tooLongChat, placeholderChat), player.id, 0xFF0000, "bold", 2); // notify that fact return false; } + // if this player use seperator (|,|) in chat message + if(message.includes('|,|')) { + window.gameRoom._room.sendAnnouncement(Tst.maketext(LangRes.onChat.includeSeperator, placeholderChat), player.id, 0xFF0000, "bold", 2); // notify that fact + return false; + } + // Check if includes banned words + if(window.gameRoom.config.settings.chatTextFilter === true && isIncludeBannedWords(window.gameRoom.bannedWordsPool.chat, message)) { + window.gameRoom._room.sendAnnouncement(Tst.maketext(LangRes.onChat.bannedWords, placeholderChat), player.id, 0xFF0000, "bold", 2); // notify that fact + return false; + } // otherwise, send to room return true; } diff --git a/core/game/controller/events/onPlayerJoin.ts b/core/game/controller/events/onPlayerJoin.ts index 56e03e3..e3f0a1e 100644 --- a/core/game/controller/events/onPlayerJoin.ts +++ b/core/game/controller/events/onPlayerJoin.ts @@ -8,6 +8,7 @@ import { setDefaultStadiums, updateAdmins } from "../RoomTools"; import { convertTeamID2Name, TeamID } from "../../model/GameObject/TeamID"; import { putTeamNewPlayerConditional, roomActivePlayersNumberCheck } from "../../model/OperateHelper/Quorum"; import { decideTier, getAvatarByTier, Tier } from "../../model/Statistics/Tier"; +import { isExistNickname, isIncludeBannedWords } from "../TextFilter"; export async function onPlayerJoinListener(player: PlayerObject): Promise { const joinTimeStamp: number = getUnixTimestamp(); @@ -61,6 +62,13 @@ export async function onPlayerJoinListener(player: PlayerObject): Promise // window.room.clearBan(player.id); //useless cuz banned player in haxball couldn't make join-event. } } + + // if this player use seperator (|,|) in nickname, then kick + if (player.name.includes('|,|')) { + window.gameRoom.logger.i('onPlayerJoin', `${player.name}#${player.id} was joined but kicked for including seperator word. (|,|)`); + window.gameRoom._room.kickPlayer(player.id, Tst.maketext(LangRes.onJoin.includeSeperator, placeholderJoin), false); // kick + return; + } // if this player has already joinned by other connection for (let eachPlayer of window.gameRoom.playerList.values()) { @@ -72,13 +80,27 @@ export async function onPlayerJoinListener(player: PlayerObject): Promise } } - // if player's nickname is logger than limitation + // if player's nickname is longer than limitation if (player.name.length > window.gameRoom.config.settings.nicknameLengthLimit) { window.gameRoom.logger.i('onPlayerJoin', `${player.name}#${player.id} was joined but kicked for too long nickname.`); window.gameRoom._room.kickPlayer(player.id, Tst.maketext(LangRes.onJoin.tooLongNickname, placeholderJoin), false); // kick return; } + // if player's nickname is already used (duplicated nickname) + if (window.gameRoom.config.settings.forbidDuplicatedNickname === true && isExistNickname(player.name) === true) { + window.gameRoom.logger.i('onPlayerJoin', `${player.name}#${player.id} was joined but kicked for duplicated nickname.`); + window.gameRoom._room.kickPlayer(player.id, Tst.maketext(LangRes.onJoin.duplicatedNickname, placeholderJoin), false); // kick + return; + } + + // if player's nickname includes some banned words + if (window.gameRoom.config.settings.nicknameTextFilter === true && isIncludeBannedWords(window.gameRoom.bannedWordsPool.nickname, player.name) === true) { + window.gameRoom.logger.i('onPlayerJoin', `${player.name}#${player.id} was joined but kicked for including banned word(s).`); + window.gameRoom._room.kickPlayer(player.id, Tst.maketext(LangRes.onJoin.bannedNickname, placeholderJoin), false); // kick + return; + } + // add the player who joined into playerList by creating class instance let existPlayerData = await getPlayerDataFromDB(player.auth); if (existPlayerData !== undefined) { diff --git a/core/game/controller/events/onPlayerTeamChange.ts b/core/game/controller/events/onPlayerTeamChange.ts index 3ce7a48..9972c82 100644 --- a/core/game/controller/events/onPlayerTeamChange.ts +++ b/core/game/controller/events/onPlayerTeamChange.ts @@ -39,4 +39,6 @@ export function onPlayerTeamChangeListener(changedPlayer: PlayerObject, byPlayer window.gameRoom.playerList.get(changedPlayer.id)!.team = changedPlayer.team; window.gameRoom.logger.i('onPlayerTeamChange', `${changedPlayer.name}#${changedPlayer.id} is moved team to ${convertTeamID2Name(changedPlayer.team)}.`); } + + window._emitSIOPlayerStatusChangeEvent(changedPlayer.id); } diff --git a/core/game/model/Configuration/GameRoomSettings.ts b/core/game/model/Configuration/GameRoomSettings.ts index 3fdfb8d..80fb14e 100644 --- a/core/game/model/Configuration/GameRoomSettings.ts +++ b/core/game/model/Configuration/GameRoomSettings.ts @@ -58,4 +58,8 @@ export interface GameRoomSettings { nicknameLengthLimit : number chatLengthLimit : number + + forbidDuplicatedNickname: boolean + nicknameTextFilter: boolean + chatTextFilter: boolean } diff --git a/core/game/model/TextFilter/filter.js b/core/game/model/TextFilter/filter.js new file mode 100644 index 0000000..ef94d3e --- /dev/null +++ b/core/game/model/TextFilter/filter.js @@ -0,0 +1,107 @@ +/* +Text filter for Haxbotron by dapucita + +============================ +Implementation of the Aho-Corasick string searching algorithm +Source code originally from https://github.com/BrunoRB/ahocorasick + +var AhoCorasick = require('ahocorasick'); +var ac = new AhoCorasick(['keyword1', 'keyword2', 'etc']); +var results = ac.search('should find keyword1 at position 19 and keyword2 at position 47.'); +// [ [ 19, [ 'keyword1' ] ], [ 47, [ 'keyword2' ] ] ] +============================ + +MIT License +*/ + +export class AhoCorasick { + constructor(keywords) { + this._buildTables(keywords); + } + _buildTables(keywords) { + var gotoFn = { + 0: {} + }; + var output = {}; + + var state = 0; + keywords.forEach(function (word) { + var curr = 0; + for (var i = 0; i < word.length; i++) { + var l = word[i]; + if (gotoFn[curr] && l in gotoFn[curr]) { + curr = gotoFn[curr][l]; + } + else { + state++; + gotoFn[curr][l] = state; + gotoFn[state] = {}; + curr = state; + output[state] = []; + } + } + + output[curr].push(word); + }); + + var failure = {}; + var xs = []; + + // f(s) = 0 for all states of depth 1 (the ones from which the 0 state can transition to) + for (var l in gotoFn[0]) { + var state = gotoFn[0][l]; + failure[state] = 0; + xs.push(state); + } + + while (xs.length) { + var r = xs.shift(); + // for each symbol a such that g(r, a) = s + for (var l in gotoFn[r]) { + var s = gotoFn[r][l]; + xs.push(s); + + // set state = f(r) + var state = failure[r]; + while (state > 0 && !(l in gotoFn[state])) { + state = failure[state]; + } + + if (l in gotoFn[state]) { + var fs = gotoFn[state][l]; + failure[s] = fs; + output[s] = output[s].concat(output[fs]); + } + else { + failure[s] = 0; + } + } + } + + this.gotoFn = gotoFn; + this.output = output; + this.failure = failure; + } + search(string) { + var state = 0; + var results = []; + for (var i = 0; i < string.length; i++) { + var l = string[i]; + while (state > 0 && !(l in this.gotoFn[state])) { + state = this.failure[state]; + } + if (!(l in this.gotoFn[state])) { + continue; + } + + state = this.gotoFn[state][l]; + + if (this.output[state].length) { + var foundStrs = this.output[state]; + results.push([i, foundStrs]); + } + } + + return results; + } +} diff --git a/core/game/resource/strings.sample.en.ts b/core/game/resource/strings.sample.en.ts index 211eb6e..59cc202 100644 --- a/core/game/resource/strings.sample.en.ts +++ b/core/game/resource/strings.sample.en.ts @@ -2,7 +2,7 @@ // THE TYPES OF PLACEHOLDER ARE LIMITED BY STRING SET. export const scheduler = { - advertise: '📢 Haxbotron🤖 - Open Source Bot Project\n💬 Discord https://discord.gg/qfg45B2 Donate https://www.patreon.com/dapucita' + advertise: '📢 Haxbotron🤖 (https://dapucita.github.io/haxbotron/)\n💬 Discord https://discord.gg/qfg45B2 Donate https://www.patreon.com/dapucita' ,shutdown: '📢 This room will be shutdown soon. Thanks for joinning our game!' ,afkKick: '📢 kicked: AFK' ,afkCommandTooLongKick: '📢 AFK over 2mins' @@ -73,7 +73,7 @@ export const command = { ,tier: '📑 !tier shows you information of tier and rating system.' ,notice: '📑 !notice shows you notice message.' } - ,about: '📄 {RoomName} ({_LaunchTime})\n💬 This room is powered by Haxbotron🤖 bot. https://github.com/dapucita/haxbotron\n💬 Discord https://discord.gg/qfg45B2 Donate https://www.patreon.com/dapucita' + ,about: '📄 {RoomName} ({_LaunchTime})\n💬 This room is powered by Haxbotron🤖 bot. (https://dapucita.github.io/haxbotron/)\n💬 Discord https://discord.gg/qfg45B2 Donate https://www.patreon.com/dapucita' ,stats: { _ErrorNoPlayer: '❌ Wrong player ID. You can only target numeric ID.(eg: !stats #12)\n📑 You can check IDs by command !list red,blue,spec' ,statsMsg: '📊 {targetName}#{ticketTarget} (Rating {targetStatsRatingAvatar}{targetStatsRating}) Total {targetStatsTotal} games(winrate {targetStatsWinRate}%), Disconnected {targetStatsDisconns} games\n📊 Goal {targetStatsGoals}, Assist {targetStatsAssists}, OG {targetStatsOgs}, Lose goal {targetStatsLosepoints}, Pass Success Rate {targetStatsPassSuccess}%\n📊 and Per Game : {targetStatsGoalsPerGame}goals, {targetStatsAssistsPerGame}assists, {targetStatsOgsPerGame}ogs, {targetStatsLostGoalsPerGame}lose goals.' @@ -171,6 +171,9 @@ export const onJoin = { ,doubleJoinningMsg: '🚫 {playerName}#{playerID} has already joined.' ,doubleJoinningKick: '🚫 You did double joinning.' ,tooLongNickname: '🚫 Too long nickname.' + ,duplicatedNickname: '🚫 Duplicated nickname.' + ,bannedNickname: '🚫 Banned nickname.' + ,includeSeperator: '🚫 Chat message includes banned word. (|,|)' ,banList: { permanentBan: '{banListReason}' ,fixedTermBan: '{banListReason}' @@ -185,6 +188,8 @@ export const onLeft = { export const onChat = { mutedChat: '🔇 You are muted. You can\'t send message to others, and only can command by chat.' ,tooLongChat: '🔇 Chat message is too long.' + ,bannedWords: '🚫 Chat message includes banned words.' + ,includeSeperator: '🚫 Chat message includes banned word. (|,|)' } export const onTeamChange = { diff --git a/core/game/resource/strings.ts b/core/game/resource/strings.ts index 2cde3e5..f5c8dac 100644 --- a/core/game/resource/strings.ts +++ b/core/game/resource/strings.ts @@ -2,7 +2,7 @@ // THE TYPES OF PLACEHOLDER ARE LIMITED BY STRING SET. export const scheduler = { - advertise: '📢 Haxbotron🤖 - Open Source Bot Project\n💬 [디스코드] https://discord.gg/qfg45B2 [후원하기] https://www.patreon.com/dapucita' + advertise: '📢 Haxbotron🤖 (https://dapucita.github.io/haxbotron/)\n💬 [디스코드] https://discord.gg/qfg45B2 [후원하기] https://www.patreon.com/dapucita' ,shutdown: '📢 방이 곧 닫힙니다. 이용해주셔서 감사합니다.' ,afkKick: '📢 잠수로 인한 퇴장' ,afkCommandTooLongKick: '📢 2분 이상 잠수로 퇴장' @@ -73,7 +73,7 @@ export const command = { ,tier: '📑 !tier : 티어와 레이팅 시스템에 대한 정보를 보여줍니다.' ,notice: '📑 !notice : 공지사항을 보여줍니다.' } - ,about: '📄 방 이름 : {RoomName} ({_LaunchTime})\n💬 이 방은 Haxbotron🤖 봇에 의해 운영됩니다. https://github.com/dapucita/haxbotron\n💬 [디스코드] https://discord.gg/qfg45B2 [후원하기] https://www.patreon.com/dapucita' + ,about: '📄 방 이름 : {RoomName} ({_LaunchTime})\n💬 이 방은 Haxbotron🤖 봇에 의해 운영됩니다. (https://dapucita.github.io/haxbotron/)\n💬 [디스코드] https://discord.gg/qfg45B2 [후원하기] https://www.patreon.com/dapucita' ,stats: { _ErrorNoPlayer: '❌ 접속중이지 않습니다. #숫자아이디 의 형식으로 지정해야 합니다. (예: !stats #12)\n📑 !list red,blue,spec 명령어로 숫자아이디를 확인할 수 있습니다.' ,statsMsg: '📊 {targetName}#{ticketTarget}님의 전적 (레이팅 {targetStatsRatingAvatar}{targetStatsRating}) 총 {targetStatsTotal}판(승률 {targetStatsWinRate}%), 연결끊김 {targetStatsDisconns}회 \n📊 (이어서) 골 {targetStatsGoals}, 어시 {targetStatsAssists}, 자책 {targetStatsOgs}, 실점 {targetStatsLosepoints}, 패스성공률 {targetStatsPassSuccess}%\n📊 (이어서) 경기당 {targetStatsGoalsPerGame}골, {targetStatsAssistsPerGame}도움과 {targetStatsOgsPerGame}자책, {targetStatsLostGoalsPerGame}실점을 기록중입니다.' @@ -171,6 +171,9 @@ export const onJoin = { ,doubleJoinningMsg: '🚫 {playerName}#{playerID}님이 중복 접속하였습니다.' ,doubleJoinningKick: '🚫 중복 접속으로 퇴장' ,tooLongNickname: '🚫 너무 긴 닉네임' + ,duplicatedNickname: '🚫 중복 닉네임' + ,bannedNickname: '🚫 금지된 닉네임' + ,includeSeperator: '🚫 금지된 닉네임 (|,|)' ,banList: { permanentBan: '{banListReason}' ,fixedTermBan: '{banListReason}' @@ -185,6 +188,8 @@ export const onLeft = { export const onChat = { mutedChat: '🔇 음소거되어 채팅을 할 수 없습니다. 명령어는 사용할 수 있습니다.' ,tooLongChat: '🔇 채팅 메시지가 너무 깁니다.' + ,bannedWords: '🚫 채팅에 금칙어가 포함되어 있습니다.' + ,includeSeperator: '🚫 채팅에 금칙어(|,|)가 포함되어 있습니다.' } export const onTeamChange = { diff --git a/core/lib/browser.hostconfig.ts b/core/lib/browser.hostconfig.ts index 852ee05..1688e07 100644 --- a/core/lib/browser.hostconfig.ts +++ b/core/lib/browser.hostconfig.ts @@ -110,6 +110,10 @@ export interface BrowserHostRoomSettings { nicknameLengthLimit : number chatLengthLimit : number + + forbidDuplicatedNickname: boolean + nicknameTextFilter: boolean + chatTextFilter: boolean } export interface BrowserHostRoomHEloConfig { diff --git a/core/lib/browser.ts b/core/lib/browser.ts index 6da7008..31aa9ea 100644 --- a/core/lib/browser.ts +++ b/core/lib/browser.ts @@ -5,6 +5,7 @@ import { BrowserHostRoomInitConfig } from "./browser.hostconfig"; import * as dbUtilityInject from "./db.injection"; import { loadStadiumData } from "./stadiumLoader"; import { Server as SIOserver, Socket as SIOsocket } from "socket.io"; +import { TeamID } from "../game/model/GameObject/TeamID"; /** * Use this class for control Headless Browser. @@ -56,7 +57,7 @@ export class HeadlessBrowser { } //winstonLogger.info("[core] The browser is opened."); - + this._BrowserContainer = await puppeteer.launch({ headless: browserSettings.openHeadless, args: browserSettings.customArgs }); this._BrowserContainer.on('disconnected', () => { @@ -98,13 +99,13 @@ export class HeadlessBrowser { } } - if(!this._BrowserContainer) await this.initBrowser(); // open if browser isn't exist. + if (!this._BrowserContainer) await this.initBrowser(); // open if browser isn't exist. const page: puppeteer.Page = await this._BrowserContainer!.newPage(); // create new page(tab) const existPages = await this._BrowserContainer?.pages(); // get exist pages for check if first blank page is exist - if(existPages?.length == 2 && this._PageContainer.size == 0) existPages[0].close(); // close useless blank page - + if (existPages?.length == 2 && this._PageContainer.size == 0) existPages[0].close(); // close useless blank page + page.on('console', (msg: any) => { @@ -152,7 +153,7 @@ export class HeadlessBrowser { localStorage.setItem('_defaultMap', defaultMap); localStorage.setItem('_readyMap', readyMap); }, JSON.stringify(initConfig), loadStadiumData(initConfig.rules.defaultMapName), loadStadiumData(initConfig.rules.readyMapName)); - + // add event listeners ============================================================ page.addListener('_SIO.Log', (event: any) => { this._SIOserver?.sockets.emit('log', { ruid: ruid, origin: event.origin, type: event.type, message: event.message }); @@ -160,17 +161,23 @@ export class HeadlessBrowser { page.addListener('_SIO.InOut', (event: any) => { this._SIOserver?.sockets.emit('joinleft', { ruid: ruid, playerID: event.playerID }); }); + page.addListener('_SIO.StatusChange', (event: any) => { + this._SIOserver?.sockets.emit('statuschange', { ruid: ruid, playerID: event.playerID }); + }); // ================================================================================ // ================================================================================ // inject some functions ========================================================== await page.exposeFunction('_emitSIOLogEvent', (origin: string, type: string, message: string) => { - page.emit('_SIO.Log', {origin: origin, type: type, message: message}); + page.emit('_SIO.Log', { origin: origin, type: type, message: message }); }) await page.exposeFunction('_emitSIOPlayerInOutEvent', (playerID: number) => { page.emit('_SIO.InOut', { playerID: playerID }); }) - + await page.exposeFunction('_emitSIOPlayerStatusChangeEvent', (playerID: number) => { + page.emit('_SIO.StatusChange', { playerID: playerID }); + }) + // inject functions for CRUD with DB Server ==================================== await page.exposeFunction('_createSuperadminDB', dbUtilityInject.createSuperadminDB); await page.exposeFunction('_readSuperadminDB', dbUtilityInject.readSuperadminDB); @@ -204,7 +211,7 @@ export class HeadlessBrowser { */ private async fetchRoomURILink(ruid: string): Promise { await this._PageContainer.get(ruid)!.waitForFunction(() => window.gameRoom.link !== undefined && window.gameRoom.link.length > 0); // wait for 30secs(default) until room link is created - + let link: string = await this._PageContainer.get(ruid)!.evaluate(() => { return window.gameRoom.link; }); @@ -351,7 +358,7 @@ export class HeadlessBrowser { public async getPlayerInfo(ruid: string, id: number): Promise { return await this._PageContainer.get(ruid)!.evaluate((id: number) => { //let idNum: number = parseInt(id); - if(window.gameRoom.playerList.has(id)) { + if (window.gameRoom.playerList.has(id)) { return window.gameRoom.playerList.get(id); } else { return undefined; @@ -368,14 +375,14 @@ export class HeadlessBrowser { */ public async banPlayerFixedTerm(ruid: string, id: number, ban: boolean, message: string, seconds: number): Promise { await this._PageContainer.get(ruid)?.evaluate(async (id: number, ban: boolean, message: string, seconds: number) => { - if(window.gameRoom.playerList.has(id)) { + if (window.gameRoom.playerList.has(id)) { const banItem = { conn: window.gameRoom.playerList.get(id)!.conn, reason: message, register: Math.floor(Date.now()), - expire: Math.floor(Date.now()) + (seconds*1000) + expire: Math.floor(Date.now()) + (seconds * 1000) } - if(await window._readBanlistDB(window.gameRoom.config._RUID, window.gameRoom.playerList.get(id)!.conn) !== undefined) { + if (await window._readBanlistDB(window.gameRoom.config._RUID, window.gameRoom.playerList.get(id)!.conn) !== undefined) { //if already exist then update it await window._updateBanlistDB(window.gameRoom.config._RUID, banItem); } else { @@ -383,7 +390,7 @@ export class HeadlessBrowser { await window._createBanlistDB(window.gameRoom.config._RUID, banItem); } window.gameRoom._room.kickPlayer(id, message, ban); - window.gameRoom.logger.i('system', `[Kick] #${id} has been ${ban?'banned':'kicked'} by operator. (duration: ${seconds}secs, reason: ${message})`); + window.gameRoom.logger.i('system', `[Kick] #${id} has been ${ban ? 'banned' : 'kicked'} by operator. (duration: ${seconds}secs, reason: ${message})`); } }, id, ban, message, seconds); } @@ -398,18 +405,28 @@ export class HeadlessBrowser { }, message); } + /** + * Send whisper text message + */ + public async whisper(ruid: string, id: number, message: string): Promise { + await this._PageContainer.get(ruid)?.evaluate((id: number, message: string) => { + window.gameRoom._room.sendAnnouncement(message, id, 0xFFFF00, "bold", 2); + window.gameRoom.logger.i('system', `[Whisper][to ${window.gameRoom.playerList.get(id)?.name}#${id}] ${message}`); + }, id, message); + } + /** * Get notice message. * @param ruid Game room's UID */ - public async getNotice(ruid: string): Promise { + public async getNotice(ruid: string): Promise { return await this._PageContainer.get(ruid)!.evaluate(() => { - if(window.gameRoom.notice) { + if (window.gameRoom.notice) { return window.gameRoom.notice; } else { return undefined; } - }) + }); } /** @@ -420,6 +437,183 @@ export class HeadlessBrowser { public async setNotice(ruid: string, message: string): Promise { await this._PageContainer.get(ruid)!.evaluate((message: string) => { window.gameRoom.notice = message; - },message) + }, message); + } + + /** + * Set password of game room. + * @param ruid Game room's UID + * @param password Password (null string for disable password) + */ + public async setPassword(ruid: string, password: string) { + await this._PageContainer.get(ruid)!.evaluate((password: string) => { + const convertedPassword: string | null = (password == "") ? null : password; + window.gameRoom._room.setPassword(convertedPassword); + window.gameRoom.config._config.password = password; + }, password); + } + + /** + * Get banned words pool for nickname filter + * @param ruid Game room's UID + */ + public async getNicknameTextFilteringPool(ruid: string): Promise { + return await this._PageContainer.get(ruid)!.evaluate(() => { + return window.gameRoom.bannedWordsPool.nickname; + }); + } + + /** + * Get banned words pool for chat message filter + * @param ruid Game room's UID + */ + public async getChatTextFilteringPool(ruid: string): Promise { + return await this._PageContainer.get(ruid)!.evaluate(() => { + return window.gameRoom.bannedWordsPool.chat; + }); + } + + /** + * Set banned words pool for nickname filter + * @param ruid Game room's UID + * @param pool banned words pool + */ + public async setNicknameTextFilter(ruid: string, pool: string[]) { + await this._PageContainer.get(ruid)!.evaluate((pool: string[]) => { + window.gameRoom.bannedWordsPool.nickname = pool; + }, pool); + } + + /** + * Set banned words pool for chat message filter + * @param ruid Game room's UID + * @param pool banned words pool + */ + public async setChatTextFilter(ruid: string, pool: string[]) { + await this._PageContainer.get(ruid)!.evaluate((pool: string[]) => { + window.gameRoom.bannedWordsPool.chat = pool; + }, pool); + } + + /** + * Clear banned words pool for nickname filter + * @param ruid Game room's UID + */ + public async clearNicknameTextFilter(ruid: string) { + await this._PageContainer.get(ruid)!.evaluate(() => { + window.gameRoom.bannedWordsPool.nickname = []; + }); + } + + /** + * Clear banned words pool for chat message filter + * @param ruid Game room's UID + */ + public async clearChatTextFilter(ruid: string) { + await this._PageContainer.get(ruid)!.evaluate(() => { + window.gameRoom.bannedWordsPool.chat = []; + }); + } + + /** + * Check if the game room's chat is muted + * @param ruid Game room's UID + * @returns + */ + public async getChatFreeze(ruid: string): Promise { + return await this._PageContainer.get(ruid)!.evaluate(() => { + return window.gameRoom.isMuteAll; + }); + } + + /** + * Mute or not game room's whole chat + * @param ruid Game room's UID + * @param freeze mute or unmute whole chat + */ + public async setChatFreeze(ruid: string, freeze: boolean) { + await this._PageContainer.get(ruid)!.evaluate((freeze: boolean) => { + window.gameRoom.isMuteAll = freeze; + window.gameRoom.logger.i('system', `[Freeze] Whole chat is ${freeze ? 'muted' : 'unmuted'} by Operator.`); + window._emitSIOPlayerStatusChangeEvent(0); + }, freeze); + } + + /** + * Mute the player + * @param ruid ruid Game room's UID + * @param id player's numeric ID + * @param muteExpireTime mute expiration time + */ + public async setPlayerMute(ruid: string, id: number, muteExpireTime: number) { + await this._PageContainer.get(ruid)!.evaluate((id: number, muteExpireTime: number) => { + window.gameRoom.playerList.get(id)!.permissions.mute = true; + window.gameRoom.playerList.get(id)!.permissions.muteExpire = muteExpireTime; + + window.gameRoom.logger.i('system', `[Mute] ${window.gameRoom.playerList.get(id)!.name}#${id} is muted by Operator.`); + window._emitSIOPlayerStatusChangeEvent(id); + }, id, muteExpireTime); + } + + /** + * Unmute the player + * @param ruid ruid Game room's UID + * @param id player's numeric ID + */ + public async setPlayerUnmute(ruid: string, id: number) { + await this._PageContainer.get(ruid)!.evaluate((id: number) => { + window.gameRoom.playerList.get(id)!.permissions.mute = false; + + window.gameRoom.logger.i('system', `[Mute] ${window.gameRoom.playerList.get(id)!.name}#${id} is unmuted by Operator.`); + window._emitSIOPlayerStatusChangeEvent(id); + }, id); + } + + /** + * Get team colours + * @param ruid ruid Game room's UID + * @param team team ID (red 1, blue 2) + * @returns `angle`, `textColour`, `teamColour1`, `teamColour2`, `teamColour3` as Number + */ + public async getTeamColours(ruid: string, team: TeamID) { + return await this._PageContainer.get(ruid)!.evaluate((team: number) => { + return window.gameRoom.teamColours[team === 1 ? 'red' : 'blue']; + }, team); + } + + /** + * Set team colours + * @param ruid ruid Game room's UID + * @param team team ID (red 1, blue 2) + * @param angle angle for the team color stripes (in degrees) + * @param textColour color of the player avatars + * @param teamColour1 first color for the team + * @param teamColour2 second color for the team + * @param teamColour3 third color for the team + */ + public async setTeamColours(ruid: string, team: TeamID, angle: number, textColour: number, teamColour1: number, teamColour2: number, teamColour3: number) { + await this._PageContainer.get(ruid)!.evaluate((team: number, angle: number, textColour: number, teamColour1: number, teamColour2: number, teamColour3: number) => { + window.gameRoom._room.setTeamColors(team, angle, textColour, [teamColour1, teamColour2, teamColour3]); + + if (team === 2) { + window.gameRoom.teamColours.blue = { + angle: angle, + textColour: textColour, + teamColour1: teamColour1, + teamColour2: teamColour2, + teamColour3: teamColour3, + } + } else { + window.gameRoom.teamColours.red = { + angle: angle, + textColour: textColour, + teamColour1: teamColour1, + teamColour2: teamColour2, + teamColour3: teamColour3, + } + } + + window.gameRoom.logger.i('system', `[TeamColour] New team colour is set for Team ${team}.`); + }, team, angle, textColour, teamColour1, teamColour2, teamColour3); } } diff --git a/core/package-lock.json b/core/package-lock.json index d6c9f80..8c04c03 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -1,6 +1,6 @@ { "name": "haxbotron-core", - "version": "0.4.4", + "version": "0.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -66,6 +66,11 @@ "@hapi/hoek": "^9.0.0" } }, + "@icons/material": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", + "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==" + }, "@koa/cors": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@koa/cors/-/cors-3.1.0.tgz", @@ -477,6 +482,15 @@ "csstype": "^3.0.2" } }, + "@types/react-color": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.4.tgz", + "integrity": "sha512-EswbYJDF1kkrx93/YU+BbBtb46CCtDMvTiGmcOa/c5PETnwTiSWoseJ1oSWeRl/4rUXkhME9bVURvvPg0W5YQw==", + "requires": { + "@types/react": "*", + "@types/reactcss": "*" + } + }, "@types/react-dom": { "version": "17.0.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.0.tgz", @@ -512,6 +526,14 @@ "@types/react": "*" } }, + "@types/reactcss": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.3.tgz", + "integrity": "sha512-d2gQQ0IL6hXLnoRfVYZukQNWHuVsE75DzFTLPUuyyEhJS8G2VvlE+qfQQ91SJjaMqlURRCNIsX7Jcsw6cEuJlA==", + "requires": { + "@types/react": "*" + } + }, "@types/serve-static": { "version": "1.13.9", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", @@ -804,26 +826,6 @@ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", "dev": true }, - "ansi-escapes": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", - "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", - "requires": { - "type-fest": "^0.11.0" - }, - "dependencies": { - "type-fest": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", - "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==" - } - } - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" - }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -1084,11 +1086,6 @@ } } }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" - }, "chokidar": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", @@ -1119,19 +1116,6 @@ "tslib": "^1.9.0" } }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==" - }, "clsx": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", @@ -1415,11 +1399,6 @@ "integrity": "sha512-0zr+ZfytnLeJZxGgmEpPTcItu5Mm4A5zHPZXLfHcGp0mdsk95rmD7ePNewYtK1yIdLbk8Z1U2oTRRfOtR4gbYg==", "dev": true }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, "emojis-list": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", @@ -1642,16 +1621,6 @@ } } }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - } - }, "extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -1703,21 +1672,6 @@ "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.0.tgz", "integrity": "sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg==" }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "requires": { - "escape-string-regexp": "^1.0.5" - }, - "dependencies": { - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - } - } - }, "file-stream-rotator": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.5.7.tgz", @@ -1981,7 +1935,8 @@ "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true }, "has-unicode": { "version": "2.0.1", @@ -2174,58 +2129,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - } - } - }, "interpret": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", @@ -2261,11 +2164,6 @@ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, "is-generator-function": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.8.tgz", @@ -2705,6 +2603,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -2777,6 +2680,11 @@ } } }, + "material-colors": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -2870,7 +2778,8 @@ "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true }, "mini-create-react-context": { "version": "0.4.1", @@ -2934,11 +2843,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" - }, "needle": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/needle/-/needle-2.6.0.tgz", @@ -3124,6 +3028,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, "requires": { "mimic-fn": "^2.1.0" } @@ -3450,6 +3355,20 @@ "object-assign": "^4.1.1" } }, + "react-color": { + "version": "2.19.3", + "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", + "integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==", + "requires": { + "@icons/material": "^0.2.4", + "lodash": "^4.17.15", + "lodash-es": "^4.17.15", + "material-colors": "^1.2.1", + "prop-types": "^15.5.10", + "reactcss": "^1.2.0", + "tinycolor2": "^1.4.1" + } + }, "react-dom": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz", @@ -3527,6 +3446,14 @@ "prop-types": "^15.6.2" } }, + "reactcss": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", + "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", + "requires": { + "lodash": "^4.0.1" + } + }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -3641,15 +3568,6 @@ "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -3658,19 +3576,6 @@ "glob": "^7.1.3" } }, - "run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==" - }, - "rxjs": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", - "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", - "requires": { - "tslib": "^1.9.0" - } - }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -3874,16 +3779,6 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -3892,14 +3787,6 @@ "safe-buffer": "~5.2.0" } }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "requires": { - "ansi-regex": "^5.0.0" - } - }, "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -3915,6 +3802,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -4043,13 +3931,10 @@ "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "requires": { - "os-tmpdir": "~1.0.2" - } + "tinycolor2": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz", + "integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==" }, "to-regex-range": { "version": "5.0.1", @@ -4139,7 +4024,8 @@ "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true }, "tsscmp": { "version": "1.0.6", diff --git a/core/package.json b/core/package.json index 94f17af..9cb87a6 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "haxbotron-core", - "version": "0.4.5", + "version": "0.5.0", "description": "Haxbotron is a headless host server application for Haxball.", "main": "out/app.js", "scripts": { @@ -31,6 +31,7 @@ "@types/koa-static": "^4.0.1", "@types/koa__cors": "^3.0.2", "@types/react": "^17.0.0", + "@types/react-color": "^3.0.4", "@types/react-dom": "^17.0.0", "@types/react-router-dom": "^5.1.7", "@types/socket.io": "^2.1.13", @@ -39,7 +40,6 @@ "bcrypt": "^5.0.0", "cookie": "^0.4.1", "dotenv": "^8.2.0", - "inquirer": "^7.3.3", "joi": "^17.3.0", "jsonwebtoken": "^8.5.1", "koa": "^2.13.1", @@ -51,6 +51,7 @@ "node-persist": "^3.1.0", "puppeteer": "^5.4.1", "react": "^17.0.1", + "react-color": "^2.19.3", "react-dom": "^17.0.1", "react-router-dom": "^5.2.0", "socket.io": "^3.1.0", diff --git a/core/react/component/Admin/Dashboard.tsx b/core/react/component/Admin/Dashboard.tsx index bbf9509..a83b2dd 100644 --- a/core/react/component/Admin/Dashboard.tsx +++ b/core/react/component/Admin/Dashboard.tsx @@ -33,6 +33,8 @@ import RoomInfo from './RoomInfo'; import RoomBanList from './RoomBanList'; import RoomPlayerList from './RoomPlayerList'; import RoomSocial from './RoomSocial'; +import RoomTextFilter from './RoomTextFilter'; +import RoomAssets from './RoomAssets'; const drawerWidth = 240; @@ -232,6 +234,8 @@ function Dashboard({ match }: RouteComponentProps) { + + diff --git a/core/react/component/Admin/RoomAssets.tsx b/core/react/component/Admin/RoomAssets.tsx new file mode 100644 index 0000000..427ff5d --- /dev/null +++ b/core/react/component/Admin/RoomAssets.tsx @@ -0,0 +1,373 @@ +import React, { useEffect, useRef, useState } from 'react'; +import clsx from 'clsx'; +import Box from '@material-ui/core/Box'; +import Container from '@material-ui/core/Container'; +import Grid from '@material-ui/core/Grid'; +import Paper from '@material-ui/core/Paper'; +import Copyright from '../common/Footer.Copyright'; +import Title from './common/Widget.Title'; +import { useParams } from 'react-router-dom'; +import Alert, { AlertColor } from '../common/Alert'; +import { ChromePicker, ColorResult } from 'react-color'; +import { + Button, ButtonGroup, Divider, FormControl, FormControlLabel, + FormLabel, InputAdornment, Radio, RadioGroup, + Table, TableBody, TableCell, TableHead, TableRow, TextField +} from '@material-ui/core'; +import client from '../../lib/client'; +import { isNumber } from '../../lib/numcheck'; + +interface styleClass { + styleClass: any +} + +interface matchParams { + ruid: string +} + +interface TeamColours { + angle: number + textColour: number + teamColour1: number + teamColour2: number + teamColour3: number +} + +type ModeSelect = 'avatar' | 'team1' | 'team2' | 'team3'; + +const convertHexToInt = (rrggbb: string) => { + //const bbggrr = rrggbb.substr(4, 2) + rrggbb.substr(2, 2) + rrggbb.substr(0, 2); + return parseInt(rrggbb, 16); +} + +export default function RoomAssets({ styleClass }: styleClass) { + + const classes = styleClass; + + const fixedHeightPaper = clsx(classes.paper, classes.fullHeight); + + const matchParams: matchParams = useParams(); + + const canvasRef = useRef(null); + + const [flashMessage, setFlashMessage] = useState(''); + const [alertStatus, setAlertStatus] = useState("success" as AlertColor); + + // Red Team #e66e55, Blue team #5a89e5 + const [colourPick, setColourPick] = useState('#ffffff'); + + const [redTeamColours, setRedTeamColours] = useState({ + angle: 0, textColour: 0xffffff, teamColour1: 0xe66e55, teamColour2: 0xe66e55, teamColour3: 0xe66e55 + } as TeamColours) + const [blueTeamColours, setBlueTeamColours] = useState({ + angle: 0, textColour: 0xffffff, teamColour1: 0x5a89e5, teamColour2: 0x5a89e5, teamColour3: 0x5a89e5 + } as TeamColours) + + const [teamSelectValue, setTeamSelectValue] = useState('red'); + const [newModeSelectValue, setNewModeSelectValue] = useState('avatar' as ModeSelect); + const [newAngle, setNewAngle] = useState('0'); + const [newTextColour, setNewTextColour] = useState('#ffffff'); + const [newTeamColour1, setNewTeamColour1] = useState('#ffffff'); + const [newTeamColour2, setNewTeamColour2] = useState('#ffffff'); + const [newTeamColour3, setNewTeamColour3] = useState('#ffffff'); + + const onChangeNewAngle = (e: React.ChangeEvent) => { + setNewAngle(e.target.value); + } + + const handleColourChange = (colour: ColorResult, event: React.ChangeEvent) => { + setColourPick(colour.hex); + } + + const handleTeamSelectChange = (e: React.ChangeEvent) => { + setTeamSelectValue(e.target.value); + }; + + const handleTeamColoursLoad = async (event: React.MouseEvent) => { + event.preventDefault(); + + if (localStorage.getItem(`_${teamSelectValue}TeamColours`) !== null) { + const colours = JSON.parse(localStorage.getItem(`_${teamSelectValue}TeamColours`)!); + + setNewAngle(colours.angle.toString()); + setNewTextColour(colours.textColour.toString(16)); + setNewTeamColour1(colours.teamColour1.toString(16)); + setNewTeamColour2(colours.teamColour2.toString(16)); + setNewTeamColour3(colours.teamColour3.toString(16)); + } + } + + const handleColoursApply = async (event: React.FormEvent) => { + event.preventDefault(); + + localStorage.setItem(`_${teamSelectValue}TeamColours`, JSON.stringify({ + angle: newAngle, textColour: newTextColour, teamColour1: newTeamColour1, teamColour2: newTeamColour2, teamColour3: newTeamColour3 + })); + + try { + const result = await client.post(`/api/v1/room/${matchParams.ruid}/asset/team/colour`, { + team: teamSelectValue === 'blue' ? 2 : 1 + , angle: isNumber(parseInt(newAngle)) ? parseInt(newAngle) : 0 + , textColour: convertHexToInt(newTextColour.substr(1)) + , teamColour1: convertHexToInt(newTeamColour1.substr(1)) + , teamColour2: convertHexToInt(newTeamColour2.substr(1)) + , teamColour3: convertHexToInt(newTeamColour3.substr(1)) + }); + if (result.status === 201) { + setFlashMessage('Successfully set.'); + setAlertStatus('success'); + setTimeout(() => { + setFlashMessage(''); + }, 3000); + + getTeamColours(); + } + } catch (error) { + setAlertStatus('error'); + if (error.response.status === 404) { + setFlashMessage('Failed to set team colours.'); + } else { + setFlashMessage('Unexpected error is caused. Please try again.'); + } + } + } + + const getTeamColours = async () => { + try { + const result = await client.get(`/api/v1/room/${matchParams.ruid}/asset/team/colour`); + if (result.status === 200) { + const colours = { + red: result.data.red as TeamColours, + blue: result.data.blue as TeamColours + } + setRedTeamColours(colours.red); + setBlueTeamColours(colours.blue); + } + } catch (error) { + setAlertStatus('error'); + if (error.response.status === 404) { + setFlashMessage('Failed to load team colours.'); + } else { + setFlashMessage('Unexpected error is caused. Please try again.'); + } + } + } + + const drawBackground = () => { + if (!canvasRef.current) { + return; + } + const canvas: HTMLCanvasElement = canvasRef.current; + const context = canvas.getContext('2d'); + + if (context) { + // background + context.fillStyle = "#718C5A"; + context.fillRect(0,0,200,200); + } + } + + const drawCircle = () => { + if (!canvasRef.current) { + return; + } + const canvas: HTMLCanvasElement = canvasRef.current; + const context = canvas.getContext('2d'); + + if (context) { + // circle + context.beginPath(); + context.arc(100, 100, 80, 0, Math.PI * 2); + context.stroke(); + } + } + + useEffect(() => { + if (teamSelectValue === 'blue') { + setNewAngle(blueTeamColours.angle.toString()); + setNewTextColour('#' + blueTeamColours.textColour.toString(16)); + setNewTeamColour1('#' + blueTeamColours.teamColour1.toString(16)); + setNewTeamColour2('#' + blueTeamColours.teamColour2.toString(16)); + setNewTeamColour3('#' + blueTeamColours.teamColour3.toString(16)); + } else { + setNewAngle(redTeamColours.angle.toString()); + setNewTextColour('#' + redTeamColours.textColour.toString(16)); + setNewTeamColour1('#' + redTeamColours.teamColour1.toString(16)); + setNewTeamColour2('#' + redTeamColours.teamColour2.toString(16)); + setNewTeamColour3('#' + redTeamColours.teamColour3.toString(16)); + } + }, [teamSelectValue, redTeamColours, blueTeamColours]); + + useEffect(() => { + switch (newModeSelectValue) { + case 'avatar': { + setNewTextColour(colourPick); + break; + } + case 'team1': { + setNewTeamColour1(colourPick); + break; + } + case 'team2': { + setNewTeamColour2(colourPick); + break; + } + case 'team3': { + setNewTeamColour3(colourPick); + break; + } + } + }, [colourPick]); + + useEffect(() => { + switch (newModeSelectValue) { + case 'avatar': { + setColourPick(newTextColour); + break; + } + case 'team1': { + setColourPick(newTeamColour1); + break; + } + case 'team2': { + setColourPick(newTeamColour2); + break; + } + case 'team3': { + setColourPick(newTeamColour3); + break; + } + } + }, [newModeSelectValue]); + + useEffect(() => { + getTeamColours(); + + //TODO: SUPPORT OTHER COLOUR PATTERNS LIKE ONE OR TWO COLOURS + //TODO: MAKE PREVIEW OF COLOURS + //drawBackground(); + //drawCircle(); + }, []); + + return ( + + + + + + {flashMessage && {flashMessage}} + New Team Colours + +
+ + + + Select Team + + } label="Red" /> + } label="Blue" /> + + + + + °, + }} + /> + + + + + + + + + + + + + + Angle + Avatar + First + Second + Third + + + + + + {newAngle}° + {newTextColour} + {newTeamColour1} + {newTeamColour2} + {newTeamColour3} + + + + + + +
+
+
+ + + + + + + + +
+ + + Ingame Team Colours + + + + + + Team + Angle + Avatar + First + Second + Third + + + + + Red + {redTeamColours.angle}° + #{redTeamColours.textColour.toString(16)} + #{redTeamColours.teamColour1.toString(16)} + #{redTeamColours.teamColour2.toString(16)} + #{redTeamColours.teamColour3.toString(16)} + + + Blue + {blueTeamColours.angle}° + #{blueTeamColours.textColour.toString(16)} + #{blueTeamColours.teamColour1.toString(16)} + #{blueTeamColours.teamColour2.toString(16)} + #{blueTeamColours.teamColour3.toString(16)} + + +
+
+
+
+
+
+
+ + + +
+ ); +} diff --git a/core/react/component/Admin/RoomInfo.tsx b/core/react/component/Admin/RoomInfo.tsx index e306c59..fffb163 100644 --- a/core/react/component/Admin/RoomInfo.tsx +++ b/core/react/component/Admin/RoomInfo.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import clsx from 'clsx'; import Box from '@material-ui/core/Box'; import Container from '@material-ui/core/Container'; @@ -9,8 +9,9 @@ import Title from './common/Widget.Title'; import client from '../../lib/client'; import { useParams } from 'react-router-dom'; import Alert from '../common/Alert'; -import { TextField } from '@material-ui/core'; +import { Button, Divider, TextField } from '@material-ui/core'; import { BrowserHostRoomCommands, BrowserHostRoomConfig, BrowserHostRoomGameRule, BrowserHostRoomHEloConfig, BrowserHostRoomSettings } from '../../../lib/browser.hostconfig'; +import { WSocketContext } from '../../context/ws'; interface styleClass { styleClass: any @@ -40,17 +41,40 @@ export default function RoomInfo({ styleClass }: styleClass) { const matchParams: matchParams = useParams(); + const ws = useContext(WSocketContext); + const [flashMessage, setFlashMessage] = useState(''); const [alertStatus, setAlertStatus] = useState("success" as AlertColor); const [roomInfoJSON, setRoomInfoJSON] = useState({} as roomInfo); const [roomInfoJSONText, setRoomInfoJSONText] = useState(''); + const [plainPassword, setPlainPassword] = useState(''); + const [freezeStatus, setFreezeStatus] = useState(false); + + const getFreezeStatus = async () => { + try { + const result = await client.get(`/api/v1/room/${matchParams.ruid}/info/freeze`); + if (result.status === 200) { + setFreezeStatus(result.data.freezed); + } + } catch (error) { + if (error.response.status === 404) { + setFlashMessage('Failed to load status of chat'); + setAlertStatus("error"); + } else { + setFlashMessage('Unexpected error is caused. Please try again.'); + setAlertStatus("error"); + } + } + } + const getRoomInfo = async () => { try { const result = await client.get(`/api/v1/room/${matchParams.ruid}/info`); if (result.status === 200) { setRoomInfoJSON(result.data); + setPlainPassword(result.data._roomConfig.password || ''); } } catch (error) { if (error.response.status === 404) { @@ -63,19 +87,123 @@ export default function RoomInfo({ styleClass }: styleClass) { } } + const onChangePassword = (e: React.ChangeEvent) => { + setPlainPassword(e.target.value); + } + + const handleSetPassword = async (event: React.FormEvent) => { + event.preventDefault(); + try { + const result = await client.post(`/api/v1/room/${matchParams.ruid}/info/password`, { + password: plainPassword + }); + if (result.status === 201) { + setFlashMessage('Successfully set password.'); + setAlertStatus('success'); + setPlainPassword(''); + setTimeout(() => { + setFlashMessage(''); + }, 3000); + + getRoomInfo(); + } + } catch (error) { + //error.response.status + setFlashMessage('Failed to set password.'); + setAlertStatus('error'); + setTimeout(() => { + setFlashMessage(''); + }, 3000); + } + } + + const handleFreezeChat = async (event: React.MouseEvent) => { + event.preventDefault(); + try { + if (freezeStatus) { + const result = await client.delete(`/api/v1/room/${matchParams.ruid}/info/freeze`); + if (result.status === 204) { + setFlashMessage('Successfully unfreezed whole chat.'); + setAlertStatus('success'); + setTimeout(() => { + setFlashMessage(''); + }, 3000); + + getFreezeStatus(); + } + } else { + const result = await client.post(`/api/v1/room/${matchParams.ruid}/info/freeze`); + if (result.status === 204) { + setFlashMessage('Successfully freezed whole chat.'); + setAlertStatus('success'); + setTimeout(() => { + setFlashMessage(''); + }, 3000); + + getFreezeStatus(); + } + } + + } catch (error) { + //error.response.status + setFlashMessage('Failed to freeze whole chat.'); + setAlertStatus('error'); + setTimeout(() => { + setFlashMessage(''); + }, 3000); + } + } + + const handleClearPassword = async (event: React.MouseEvent) => { + event.preventDefault(); + try { + const result = await client.delete(`/api/v1/room/${matchParams.ruid}/info/password`); + if (result.status === 204) { + setFlashMessage('Successfully clear password.'); + setAlertStatus('success'); + setPlainPassword(''); + setTimeout(() => { + setFlashMessage(''); + }, 3000); + + getRoomInfo(); + } + } catch (error) { + //error.response.status + setFlashMessage('Failed to clear password.'); + setAlertStatus('error'); + setTimeout(() => { + setFlashMessage(''); + }, 3000); + } + } + useEffect(() => { getRoomInfo(); + getFreezeStatus(); }, []); useEffect(() => { try { setRoomInfoJSONText(JSON.stringify(roomInfoJSON, null, 4)); - } catch (errore) { + } catch (error) { setFlashMessage('Failed to load text.'); setAlertStatus("error"); } }, [roomInfoJSON]); + useEffect(() => { // websocket with socket.io + ws.on('statuschange', (content: any) => { + if (content.ruid === matchParams.ruid) { + getFreezeStatus(); + } + }); + return () => { + // before the component is destroyed + // unbind all event handlers used in this component + } + }, [ws]); + return ( @@ -84,12 +212,31 @@ export default function RoomInfo({ styleClass }: styleClass) { {flashMessage && {flashMessage}} Room Information + + + + + +
+ + + + +
+
+ + diff --git a/core/react/component/Admin/RoomList.tsx b/core/react/component/Admin/RoomList.tsx index 31e4c30..40dcc90 100644 --- a/core/react/component/Admin/RoomList.tsx +++ b/core/react/component/Admin/RoomList.tsx @@ -5,7 +5,7 @@ import Container from '@material-ui/core/Container'; import Grid from '@material-ui/core/Grid'; import Paper from '@material-ui/core/Paper'; import Copyright from '../common/Footer.Copyright'; -import { Table, TableBody, TableCell, TableHead, TableRow, TextField } from '@material-ui/core'; +import { Table, TableBody, TableCell, TableHead, TableRow } from '@material-ui/core'; import Title from './common/Widget.Title'; import client from '../../lib/client'; import { WSocketContext } from '../../context/ws'; diff --git a/core/react/component/Admin/RoomLog.tsx b/core/react/component/Admin/RoomLog.tsx index 3cef2f5..14deccb 100644 --- a/core/react/component/Admin/RoomLog.tsx +++ b/core/react/component/Admin/RoomLog.tsx @@ -8,7 +8,7 @@ import Copyright from '../common/Footer.Copyright'; import Title from './common/Widget.Title'; import { WSocketContext } from '../../context/ws'; import { useParams } from 'react-router-dom'; -import { Button, TextField } from '@material-ui/core'; +import { Button, Divider, TextField } from '@material-ui/core'; import client from '../../lib/client'; import Alert, { AlertColor } from '../common/Alert'; @@ -125,7 +125,8 @@ export default function RoomLog({ styleClass }: styleClass) {
- + + Log Messages
    diff --git a/core/react/component/Admin/RoomPlayerList.tsx b/core/react/component/Admin/RoomPlayerList.tsx index 9dd7f98..74a356f 100644 --- a/core/react/component/Admin/RoomPlayerList.tsx +++ b/core/react/component/Admin/RoomPlayerList.tsx @@ -77,6 +77,8 @@ function OnlinePlayerRow(props: { ruid: string, row: Player }) { const [newBan, setNewBan] = useState({ reason: '', seconds: 0 } as newBanFields); + const [whisperMessage, setWhisperMessage] = useState(''); + const [flashMessage, setFlashMessage] = useState(''); const [alertStatus, setAlertStatus] = useState("success" as AlertColor); @@ -108,7 +110,49 @@ function OnlinePlayerRow(props: { ruid: string, row: Player }) { } } - const handleAdd = async (event: React.FormEvent) => { + const onChangeWhisperMessage = (e: React.ChangeEvent) => { + setWhisperMessage(e.target.value); + } + + const handleWhisper = async (event: React.FormEvent) => { + event.preventDefault(); + try { + const result = await client.post(`/api/v1/room/${ruid}/chat/${row.id}`, { message: whisperMessage }); + if (result.status === 201) { + setFlashMessage('Successfully sent.'); + setAlertStatus('success'); + setWhisperMessage(''); + setTimeout(() => { + setFlashMessage(''); + }, 3000); + } + } catch (error) { + setAlertStatus('error'); + switch (error.response.status) { + case 400: { + setFlashMessage('No message.'); + break; + } + case 401: { + setFlashMessage('No permission.'); + break; + } + case 404: { + setFlashMessage('No exists room or player.'); + break; + } + default: { + setFlashMessage('Unexpected error is caused. Please try again.'); + break; + } + } + setTimeout(() => { + setFlashMessage(''); + }, 3000); + } + } + + const handleKick = async (event: React.FormEvent) => { event.preventDefault(); try { const result = await client.delete(`/api/v1/room/${ruid}/player/${row.id}`, { @@ -136,8 +180,41 @@ function OnlinePlayerRow(props: { ruid: string, row: Player }) { } } + const handleOnlinePlayerMute = async (event: React.MouseEvent) => { + event.preventDefault(); + try { + if(row.permissions.mute) { + const result = await client.delete(`/api/v1/room/${ruid}/player/${row.id}/permission/mute`); + if (result.status === 204) { + setFlashMessage('Successfully unmuted.'); + setAlertStatus('success'); + setTimeout(() => { + setFlashMessage(''); + }, 3000); + } + } else { + const result = await client.post(`/api/v1/room/${ruid}/player/${row.id}/permission/mute`, { muteExpire: -1 }); // Permanent + if (result.status === 204) { + setFlashMessage('Successfully muted.'); + setAlertStatus('success'); + setTimeout(() => { + setFlashMessage(''); + }, 3000); + } + } + } catch (error) { + //error.response.status + setFlashMessage('Failed to mute/unmute.'); + setAlertStatus('error'); + setTimeout(() => { + setFlashMessage(''); + }, 3000); + } + } + return ( + {flashMessage && {flashMessage}} setOpen(!open)}> @@ -148,14 +225,27 @@ function OnlinePlayerRow(props: { ruid: string, row: Player }) { {row.auth} {row.conn} {convertTeamID(row.team)} + + + - - {flashMessage && {flashMessage}} -
    + + + + + + + +
    { + if (content.ruid === matchParams.ruid) { + getOnlinePlayersID(); + } + }); return () => { // before the component is destroyed // unbind all event handlers used in this component @@ -433,6 +528,7 @@ export default function RoomPlayerList({ styleClass }: styleClass) { AUTH CONN Team + Chat diff --git a/core/react/component/Admin/RoomTextFilter.tsx b/core/react/component/Admin/RoomTextFilter.tsx new file mode 100644 index 0000000..9866feb --- /dev/null +++ b/core/react/component/Admin/RoomTextFilter.tsx @@ -0,0 +1,208 @@ +import React, { useEffect, useState } from 'react'; +import clsx from 'clsx'; +import Box from '@material-ui/core/Box'; +import Container from '@material-ui/core/Container'; +import Grid from '@material-ui/core/Grid'; +import Paper from '@material-ui/core/Paper'; +import Copyright from '../common/Footer.Copyright'; +import Title from './common/Widget.Title'; +import { useParams } from 'react-router-dom'; +import { Button, Divider, TextField, Typography } from '@material-ui/core'; +import client from '../../lib/client'; +import Alert, { AlertColor } from '../common/Alert'; + +interface styleClass { + styleClass: any +} + +interface matchParams { + ruid: string +} + +export default function RoomTextFilter({ styleClass }: styleClass) { + + const classes = styleClass; + + const fixedHeightPaper = clsx(classes.paper, classes.fullHeight); + + const matchParams: matchParams = useParams(); + + const [nicknameFilteringPool, setNicknameFilteringPool] = useState(''); + const [chatFilteringPool, setChatFilteringPool] = useState(''); + + const [flashMessage, setFlashMessage] = useState(''); + const [alertStatus, setAlertStatus] = useState("success" as AlertColor); + + const onChangeNicknameFilteringPool = (e: React.ChangeEvent) => { + setNicknameFilteringPool(e.target.value); + } + + const onChangeChatFilteringPool = (e: React.ChangeEvent) => { + setChatFilteringPool(e.target.value); + } + + const handleNicknameFilteringPoolClear = async (event: React.MouseEvent) => { + event.preventDefault(); + clearFilteringPool('nickname'); + setNicknameFilteringPool(''); + } + + const handleChatFilteringPoolClear = async (event: React.MouseEvent) => { + event.preventDefault(); + clearFilteringPool('chat'); + setChatFilteringPool(''); + } + + const clearFilteringPool = async (endpoint: string) => { + try { + const result = await client.delete(`/api/v1/room/${matchParams.ruid}/filter/${endpoint}`); + if (result.status === 204) { + setFlashMessage('Successfully clear.'); + setAlertStatus('success'); + setTimeout(() => { + setFlashMessage(''); + }, 3000); + } + } catch (error) { + //error.response.status + setFlashMessage('Failed to clear.'); + setAlertStatus('error'); + setTimeout(() => { + setFlashMessage(''); + }, 3000); + } + } + + const handleNicknameFilteringPoolSet = async (event: React.FormEvent) => { + event.preventDefault(); + setFilteringPool('nickname', nicknameFilteringPool); + } + + const handleChatFilteringPoolSet = async (event: React.FormEvent) => { + event.preventDefault(); + setFilteringPool('chat', chatFilteringPool); + } + + const setFilteringPool = async (endpoint: string, pool: string) => { + localStorage.setItem(`_${endpoint}FilteringPool`, pool); + try { + const result = await client.post(`/api/v1/room/${matchParams.ruid}/filter/${endpoint}`, { pool: pool }); + if (result.status === 201) { + setFlashMessage('Successfully set.'); + setAlertStatus('success'); + setTimeout(() => { + setFlashMessage(''); + }, 3000); + } + } catch (error) { + setAlertStatus('error'); + switch (error.response.status) { + case 400: { + setFlashMessage('No words in text pool.'); + break; + } + case 401: { + setFlashMessage('No permission.'); + break; + } + case 404: { + setFlashMessage('No exists room.'); + break; + } + default: { + setFlashMessage('Unexpected error is caused. Please try again.'); + break; + } + } + setTimeout(() => { + setFlashMessage(''); + }, 3000); + } + } + + const getNicknameFilteringPool = async () => { + getFilteringPool('nickname', setNicknameFilteringPool) + } + + const getChatFilteringPool = async () => { + getFilteringPool('chat', setChatFilteringPool) + } + + const getFilteringPool = async (endpoint: string, setterFunction: Function) => { + try { + const result = await client.get(`/api/v1/room/${matchParams.ruid}/filter/${endpoint}`); + if (result.status === 200) { + const textPool: string = result.data.pool; + setterFunction(textPool); + } + } catch (error) { + setAlertStatus('error'); + if (error.response.status === 404) { + setFlashMessage('Failed to load filtering pool.'); + setNicknameFilteringPool(''); + } else { + setFlashMessage('Unexpected error is caused. Please try again.'); + } + } + } + + const handleNicknameFilteringPoolLoad = async (event: React.MouseEvent) => { + event.preventDefault(); + + setNicknameFilteringPool(localStorage.getItem('_nicknameFilteringPool') || ''); + } + + const handleChatFilteringPoolLoad = async (event: React.MouseEvent) => { + event.preventDefault(); + + setChatFilteringPool(localStorage.getItem('_chatFilteringPool') || ''); + } + + useEffect(() => { + getNicknameFilteringPool(); + getChatFilteringPool(); + }, []); + + return ( + + + + + + {flashMessage && {flashMessage}} + Nickname Filtering Pool + Seperate by |,| and click Apply button. + + + + + + + + + Chat Filtering Pool + Seperate by |,| and click Apply button. +
    + + + + + +
    +
    +
    +
    + + + +
    + ); +} diff --git a/core/react/component/Admin/SideMenu/RoomInfo.SideMenu.tsx b/core/react/component/Admin/SideMenu/RoomInfo.SideMenu.tsx index 863dd8b..dc36f50 100644 --- a/core/react/component/Admin/SideMenu/RoomInfo.SideMenu.tsx +++ b/core/react/component/Admin/SideMenu/RoomInfo.SideMenu.tsx @@ -15,6 +15,8 @@ import VpnKeyIcon from '@material-ui/icons/VpnKey'; import ListAltIcon from '@material-ui/icons/ListAlt'; import DnsIcon from '@material-ui/icons/Dns'; import SendIcon from '@material-ui/icons/Send'; +import FilterListIcon from '@material-ui/icons/FilterList'; +import AttachmentIcon from '@material-ui/icons/Attachment'; interface matchParams { ruid: string @@ -65,6 +67,12 @@ export default function RoomInfoSideMenu() { + + + + + + @@ -83,6 +91,12 @@ export default function RoomInfoSideMenu() { + + + + + + diff --git a/core/react/lib/defaultroomconfig.json b/core/react/lib/defaultroomconfig.json index 5ba54e9..83ff2fc 100644 --- a/core/react/lib/defaultroomconfig.json +++ b/core/react/lib/defaultroomconfig.json @@ -51,7 +51,10 @@ "guaranteedPlayingTimeSeconds": 20, "avatarOverridingByTier": true, "nicknameLengthLimit": 12, - "chatLengthLimit": 80 + "chatLengthLimit": 80, + "forbidDuplicatedNickname": true, + "nicknameTextFilter": true, + "chatTextFilter": true }, "rules": { "ruleName": "default-rule", @@ -73,8 +76,8 @@ "factor": { "placement_match_chances": 10, "factor_k_placement": 50, - "factor_k_normal": 30, - "factor_k_replace": 10 + "factor_k_normal": 40, + "factor_k_replace": 20 }, "tier": { "class_tier_1": 500, @@ -82,10 +85,10 @@ "class_tier_3": 800, "class_tier_4": 900, "class_tier_5": 1000, - "class_tier_6": 1125, - "class_tier_7": 1250, - "class_tier_8": 1375, - "class_tier_9": 1500 + "class_tier_6": 1100, + "class_tier_7": 1200, + "class_tier_8": 1300, + "class_tier_9": 1400 }, "avatar": { "avatar_unknown": "❔", @@ -131,8 +134,8 @@ "_superSubkick": "kick", "_superSubban": "ban", "_disabledCommandList": [ - "sampledisabledcommandname1", - "sampledisabledcommandname2" + "SampleDisabledCommandName1", + "SampleDisabledCommandName2" ], "about": "about", "afk": "afk", diff --git a/core/typings/global.d.ts b/core/typings/global.d.ts index e3df7b8..a57d380 100644 --- a/core/typings/global.d.ts +++ b/core/typings/global.d.ts @@ -25,7 +25,29 @@ declare global { training: string } - logger: Logger; // logger for whole bot application + bannedWordsPool: { + nickname: string[] + chat: string[] + } + + teamColours: { + red: { + angle: number + textColour: number + teamColour1: number + teamColour2: number + teamColour3: number + } + blue: { + angle: number + textColour: number + teamColour1: number + teamColour2: number + teamColour3: number + } + } + + logger: Logger // logger for whole bot application isStatRecord: boolean // TRUE means that recording stats now isGamingNow: boolean // is playing now? @@ -66,6 +88,7 @@ declare global { // Injected functions _emitSIOLogEvent(origin: string, type: string, message: string): void _emitSIOPlayerInOutEvent(playerID: number): void + _emitSIOPlayerStatusChangeEvent(playerID: number): void // CRUD with DB Server via REST API async _createPlayerDB(ruid: string, player: PlayerStorage): Promise async _readPlayerDB(ruid: string, playerAuth: string): Promise diff --git a/core/web/controller/api/v1/room.ts b/core/web/controller/api/v1/room.ts index 16572cf..bff28cd 100644 --- a/core/web/controller/api/v1/room.ts +++ b/core/web/controller/api/v1/room.ts @@ -1,8 +1,10 @@ import { Context } from "koa"; import { Player } from "../../../../game/model/GameObject/Player"; +import { TeamID } from "../../../../game/model/GameObject/TeamID"; import { HeadlessBrowser } from "../../../../lib/browser"; import { BrowserHostRoomInitConfig } from '../../../../lib/browser.hostconfig'; import { nestedHostRoomConfigSchema } from "../../../schema/hostroomconfig.validation"; +import { teamColourSchema } from "../../../schema/teamcolour.validation"; const browser = HeadlessBrowser.getInstance(); @@ -20,15 +22,15 @@ export async function createRoom(ctx: Context) { const newRoomConfig: BrowserHostRoomInitConfig = { _LaunchDate: new Date() - ,_RUID: ctx.request.body.ruid - ,_config: ctx.request.body._config - ,settings: ctx.request.body.settings - ,rules: ctx.request.body.rules - ,HElo: ctx.request.body.helo - ,commands: ctx.request.body.commands + , _RUID: ctx.request.body.ruid + , _config: ctx.request.body._config + , settings: ctx.request.body.settings + , rules: ctx.request.body.rules + , HElo: ctx.request.body.helo + , commands: ctx.request.body.commands } - if(newRoomConfig._config.password == "") { + if (newRoomConfig._config.password == "") { newRoomConfig._config.password = undefined; } @@ -104,7 +106,7 @@ export async function getPlayersList(ctx: Context) { } else { ctx.status = 404; } - + } /** @@ -115,7 +117,7 @@ export async function getPlayerInfo(ctx: Context) { ctx.status = 404; if (browser.checkExistRoom(ruid)) { const player: Player | undefined = await browser.getPlayerInfo(ruid, parseInt(id)); - if(player !== undefined) { + if (player !== undefined) { ctx.body = player; ctx.status = 200; } @@ -128,7 +130,7 @@ export async function getPlayerInfo(ctx: Context) { export async function kickOnlinePlayer(ctx: Context) { const { ruid, id } = ctx.params; const { ban, seconds, message } = ctx.request.body; - if(ban === undefined || !seconds || !message) { + if (ban === undefined || !seconds || !message) { ctx.status = 400; // Unfulfilled error return; } @@ -148,7 +150,7 @@ export function broadcast(ctx: Context) { const message: string | undefined = ctx.request.body.message; ctx.status = 404; if (browser.checkExistRoom(ruid)) { - if(message) { + if (message) { browser.broadcast(ruid, message); ctx.status = 201; } else { @@ -157,6 +159,24 @@ export function broadcast(ctx: Context) { } } +/** + * send whisper message + */ +export async function whisper(ctx: Context) { + const { ruid, id } = ctx.params; + const message: string | undefined = ctx.request.body.message; + const playerID: number = parseInt(id) + ctx.status = 404; + if (browser.checkExistRoom(ruid) && await browser.checkOnlinePlayer(ruid, playerID)) { + if (message) { + browser.whisper(ruid, playerID, message); + ctx.status = 201; + } else { + ctx.status = 400; + } + } +} + /** * set notice message */ @@ -165,7 +185,7 @@ export async function setNotice(ctx: Context) { const message: string | undefined = ctx.request.body.message; ctx.status = 404; if (browser.checkExistRoom(ruid)) { - if(message) { + if (message) { browser.setNotice(ruid, message); ctx.status = 201; } else { @@ -181,11 +201,11 @@ export async function getNotice(ctx: Context) { const { ruid } = ctx.params; ctx.status = 404; if (browser.checkExistRoom(ruid)) { - const message: string|undefined = await browser.getNotice(ruid); - if(typeof message === 'string') { + const message: string | undefined = await browser.getNotice(ruid); + if (typeof message === 'string') { ctx.body = { message: message }; ctx.status = 200; - } + } } } @@ -200,3 +220,259 @@ export async function deleteNotice(ctx: Context) { ctx.status = 204; } } + +/** + * set room's password + */ +export async function setPassword(ctx: Context) { + const { ruid } = ctx.params; + const password: string = ctx.request.body.password; + ctx.status = 404; + + if (!password) { + ctx.status = 400; // Unfulfilled error + return; + } + + if (browser.checkExistRoom(ruid)) { + browser.setPassword(ruid, password); + ctx.status = 201; + } +} + +/** + * Clear room's password + */ +export async function clearPassword(ctx: Context) { + const { ruid } = ctx.params; + ctx.status = 404; + + if (browser.checkExistRoom(ruid)) { + browser.setPassword(ruid, ''); + ctx.status = 204; + } +} + +/** + * Get text filtering pool for nickname + */ +export async function getNicknameTextFilteringPool(ctx: Context) { + const { ruid } = ctx.params; + ctx.status = 404; + if (browser.checkExistRoom(ruid)) { + const pool: string[] = await browser.getNicknameTextFilteringPool(ruid); + + ctx.body = { pool: pool.join('|,|') }; + ctx.status = 200; + } +} + +/** + * Get text filtering pool for chat messages + */ +export async function getChatTextFilteringPool(ctx: Context) { + const { ruid } = ctx.params; + ctx.status = 404; + if (browser.checkExistRoom(ruid)) { + const pool: string[] = await browser.getChatTextFilteringPool(ruid); + + ctx.body = { pool: pool.join('|,|') }; + ctx.status = 200; + } +} + +/** + * Set text filtering pool for nickname + */ +export async function setNicknameTextFilter(ctx: Context) { + const { ruid } = ctx.params; + const pool: string = ctx.request.body.pool; + ctx.status = 404; + + if (!pool) { + ctx.status = 400; // Unfulfilled error + return; + } + + if (browser.checkExistRoom(ruid)) { + browser.setNicknameTextFilter(ruid, pool.split('|,|')); + ctx.status = 201; + } +} + +/** + * Set text filtering pool for chat messages + */ +export async function setChatTextFilter(ctx: Context) { + const { ruid } = ctx.params; + const pool: string = ctx.request.body.pool; + ctx.status = 404; + + if (!pool) { + ctx.status = 400; // Unfulfilled error + return; + } + + if (browser.checkExistRoom(ruid)) { + browser.setChatTextFilter(ruid, pool.split('|,|')); + ctx.status = 201; + } +} + +/** + * Clear text filtering pool for nickname + */ +export async function clearNicknameTextFilter(ctx: Context) { + const { ruid } = ctx.params; + ctx.status = 404; + + if (browser.checkExistRoom(ruid)) { + browser.clearNicknameTextFilter(ruid); + ctx.status = 204; + } +} + +/** + * Clear text filtering pool for chat messages + */ +export async function clearChatTextFilter(ctx: Context) { + const { ruid } = ctx.params; + ctx.status = 404; + + if (browser.checkExistRoom(ruid)) { + browser.clearChatTextFilter(ruid); + ctx.status = 204; + } +} + +/** + * Check player's mute status + */ +export async function checkPlayerMuted(ctx: Context) { + const { ruid, id } = ctx.params; + ctx.status = 404; + + if (browser.checkExistRoom(ruid)) { + const player: Player | undefined = await browser.getPlayerInfo(ruid, parseInt(id)); + if (player !== undefined) { + ctx.body = { + mute: player.permissions.mute + ,muteExpire: player.permissions.muteExpire + }; + ctx.status = 200; + } + } +} + +/** + * Mute player + */ +export async function mutePlayer(ctx: Context) { + const { ruid, id } = ctx.params; + const { muteExpire } = ctx.request.body; + ctx.status = 404; + + if (!muteExpire) { + ctx.status = 400; // Unfulfilled error + return; + } + + if (browser.checkExistRoom(ruid)) { + browser.setPlayerMute(ruid, parseInt(id), muteExpire); + ctx.status = 201; + } +} + +/** + * Unmute player + */ +export async function unmutePlayer(ctx: Context) { + const { ruid, id } = ctx.params; + ctx.status = 404; + + if (browser.checkExistRoom(ruid)) { + browser.setPlayerUnmute(ruid, parseInt(id)); + ctx.status = 201; + } +} + +/** + * Check whether the game room's chat is freezed + */ +export async function checkChatFreezed(ctx: Context) { + const { ruid } = ctx.params; + ctx.status = 404; + + if (browser.checkExistRoom(ruid)) { + ctx.body = { + freezed: await browser.getChatFreeze(ruid) + } + ctx.status = 200; + } +} + +/** + * Freeze whole chat + */ +export async function freezeChat(ctx: Context) { + const { ruid } = ctx.params; + ctx.status = 404; + + if (browser.checkExistRoom(ruid)) { + browser.setChatFreeze(ruid, true); + ctx.status = 204; + } +} + +/** + * Unfreeze whole chat + */ +export async function unfreezeChat(ctx: Context) { + const { ruid } = ctx.params; + ctx.status = 404; + + if (browser.checkExistRoom(ruid)) { + browser.setChatFreeze(ruid, false); + ctx.status = 204; + } +} + +/** + * Get team colours + */ +export async function getTeamColours(ctx: Context) { + const { ruid } = ctx.params; + ctx.status = 404; + + if (browser.checkExistRoom(ruid)) { + ctx.body = { + red: await browser.getTeamColours(ruid, TeamID.Red) + ,blue: await browser.getTeamColours(ruid, TeamID.Blue) + } + ctx.status = 200; + } +} + +/** + * Set team colours + */ +export async function setTeamColours(ctx: Context) { + const { ruid } = ctx.params; + const { team, angle, textColour, teamColour1, teamColour2, teamColour3 } = ctx.request.body; + ctx.status = 404; + + const validationResult = teamColourSchema.validate(ctx.request.body); + + if (validationResult.error) { + ctx.status = 400; + ctx.body = validationResult.error; + return; + } + + if(team !== TeamID.Red && team !== TeamID.Blue) return; // 404 Error + + if (browser.checkExistRoom(ruid)) { + browser.setTeamColours(ruid, team, angle, textColour, teamColour1, teamColour2, teamColour3); + ctx.status = 201; + } +} diff --git a/core/web/router/api/v1/room.ts b/core/web/router/api/v1/room.ts index 2024fbb..a8d34ff 100644 --- a/core/web/router/api/v1/room.ts +++ b/core/web/router/api/v1/room.ts @@ -15,10 +15,32 @@ roomRouter.get('/:ruid/player', checkLoginMiddleware, roomController.getPlayersL roomRouter.get('/:ruid/player/:id', checkLoginMiddleware, roomController.getPlayerInfo); // get player info roomRouter.delete('/:ruid/player/:id', checkLoginMiddleware, roomController.kickOnlinePlayer); // kick/ban player info -roomRouter.post('/:ruid/chat', checkLoginMiddleware, roomController.broadcast); // send message +roomRouter.get('/:ruid/player/:id/permission/mute', checkLoginMiddleware, roomController.checkPlayerMuted); // check the player is muted +roomRouter.post('/:ruid/player/:id/permission/mute', checkLoginMiddleware, roomController.mutePlayer); // mute player +roomRouter.delete('/:ruid/player/:id/permission/mute', checkLoginMiddleware, roomController.unmutePlayer); // unmute player + +roomRouter.post('/:ruid/chat', checkLoginMiddleware, roomController.broadcast); // send message to game room +roomRouter.post('/:ruid/chat/:id', checkLoginMiddleware, roomController.whisper); // send message to specific player roomRouter.get('/:ruid/info', checkLoginMiddleware, roomController.getRoomDetailInfo); // get detail room info +roomRouter.post('/:ruid/info/password', checkLoginMiddleware, roomController.setPassword); // set password +roomRouter.delete('/:ruid/info/password', checkLoginMiddleware, roomController.clearPassword); // clear password + +roomRouter.get('/:ruid/info/freeze', checkLoginMiddleware, roomController.checkChatFreezed); // check whether chat is freezed +roomRouter.post('/:ruid/info/freeze', checkLoginMiddleware, roomController.freezeChat); // freeze whole chat +roomRouter.delete('/:ruid/info/freeze', checkLoginMiddleware, roomController.unfreezeChat); // unfreeze whole chat + roomRouter.get('/:ruid/social/notice', checkLoginMiddleware, roomController.getNotice); // get notice message roomRouter.post('/:ruid/social/notice', checkLoginMiddleware, roomController.setNotice); // set notice message roomRouter.delete('/:ruid/social/notice', checkLoginMiddleware, roomController.deleteNotice); // delete notice message + +roomRouter.get('/:ruid/filter/nickname', checkLoginMiddleware, roomController.getNicknameTextFilteringPool); // get banned words pool for chat filter +roomRouter.get('/:ruid/filter/chat', checkLoginMiddleware, roomController.getChatTextFilteringPool); // get banned words pool for nickname filter +roomRouter.post('/:ruid/filter/nickname', checkLoginMiddleware, roomController.setNicknameTextFilter); // set banned words pool for chat filter +roomRouter.post('/:ruid/filter/chat', checkLoginMiddleware, roomController.setChatTextFilter); // set banned words pool for nickname filter +roomRouter.delete('/:ruid/filter/nickname', checkLoginMiddleware, roomController.clearNicknameTextFilter); // clear banned words pool for chat filter +roomRouter.delete('/:ruid/filter/chat', checkLoginMiddleware, roomController.clearChatTextFilter); // clear banned words pool for nickname filter + +roomRouter.get('/:ruid/asset/team/colour', checkLoginMiddleware, roomController.getTeamColours); // get team colours +roomRouter.post('/:ruid/asset/team/colour', checkLoginMiddleware, roomController.setTeamColours); // set team colours diff --git a/core/web/schema/hostroomconfig.validation.ts b/core/web/schema/hostroomconfig.validation.ts index 78cd113..9a5cbfe 100644 --- a/core/web/schema/hostroomconfig.validation.ts +++ b/core/web/schema/hostroomconfig.validation.ts @@ -93,6 +93,10 @@ const roomSettingSchema = Joi.object().keys({ ,nicknameLengthLimit : Joi.number().required() ,chatLengthLimit : Joi.number().required() + + ,forbidDuplicatedNickname: Joi.boolean().required() + ,nicknameTextFilter: Joi.boolean().required() + ,chatTextFilter: Joi.boolean().required() }); const commandsSchema = Joi.object().keys({ diff --git a/core/web/schema/teamcolour.validation.ts b/core/web/schema/teamcolour.validation.ts new file mode 100644 index 0000000..5024fa6 --- /dev/null +++ b/core/web/schema/teamcolour.validation.ts @@ -0,0 +1,10 @@ +import Joi from 'joi'; + +export const teamColourSchema = Joi.object().keys({ + team: Joi.number().required() + ,angle: Joi.number().required() + ,textColour: Joi.number().required() + ,teamColour1: Joi.number().required() + ,teamColour2: Joi.number().required() + ,teamColour3: Joi.number().required() +}); diff --git a/db/app.ts b/db/app.ts index 880cca8..daa5f7a 100644 --- a/db/app.ts +++ b/db/app.ts @@ -1,88 +1,64 @@ import "reflect-metadata"; +// ======================================================== +// Haxbotron Headless Host Server for Haxball by dapucita +// https://github.com/dapucita/haxbotron +// ======================================================== import "dotenv/config"; - import * as path from "path"; -import express, { Request, Response, NextFunction } from "express"; -import helmet from "helmet"; -import bodyParser from "body-parser"; -import morgan from "morgan"; -import { IpDeniedError, IpFilter } from "express-ipfilter"; - -import { createConnection } from "typeorm" +import Koa from "koa"; +import Router from "koa-router"; +import bodyParser from "koa-bodyparser"; +import ip from "koa-ip"; +import logger from "koa-logger"; +import { createConnection } from "typeorm"; import { winstonLogger } from "./utility/winstonLoggerSystem"; -import { ResponseError } from "./model/interface/ResponseError"; import { Player } from "./entity/player.entity"; import { BanList } from "./entity/banlist.entity"; import { SuperAdmin } from "./entity/superadmin.entity"; import { apiRouterV1 } from "./router/v1.api.router"; - -// START +// ======================================================== +//const _GitHublastestRelease = await axios.get('https://api.github.com/repos/dapucita/haxbotron/releases/latest'); +console.log("_| _| _| _| " + "\n" + + "_| _| _|_|_| _| _| _|_|_| _|_| _|_|_|_| _| _|_| _|_| _|_|_| " + "\n" + + "_|_|_|_| _| _| _|_| _| _| _| _| _| _|_| _| _| _| _|" + "\n" + + "_| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _|" + "\n" + + "_| _| _|_|_| _| _| _|_|_| _|_| _|_| _| _|_| _| _|"); +//console.log(`Lastest Version : ${_GitHublastestRelease.data.tag_name} | Current Version : v${process.env.npm_package_version}`); +console.log(`Haxbotron by dapucita (Visit our GitHub : https://github.com/dapucita/haxbotron)`); winstonLogger.info(`haxbotron-db server is launched at ${new Date().toLocaleString()}`); - -const app: express.Application = express(); - -const whiteListIPs: string[] = process.env.SERVER_WHITELIST_IP?.split(",") || ['127.0.0.1','::ffff:127.0.0.1']; - +// ======================================================== +const dbServerSettings = { + port: (process.env.SERVER_PORT ? parseInt(JSON.parse(process.env.SERVER_PORT)) : 13001) + , level: (process.env.SERVER_LEVEL || 'common') +} +const whiteListIPs: string[] = process.env.SERVER_WHITELIST_IP?.split(",") || ['127.0.0.1']; +// ======================================================== // DB CONNECTION createConnection({ type: 'sqlite', database: path.join(__dirname, '..', process.env.DB_HOST || 'haxbotron.sqlite.db'), - entities: [ Player, BanList, SuperAdmin ], + entities: [Player, BanList, SuperAdmin], logging: true, synchronize: true }).then(conn => { - winstonLogger.info(`haxbotron-db server is connected with database.`); -}).catch(err => console.log(err)); - -// Set -app.set('views', path.join(__dirname, '../view')); -app.set('view engine', 'pug'); -app.set('port', process.env.SERVER_PORT ? parseInt(JSON.parse(process.env.SERVER_PORT)) : 13001); - -// Middlewares -app.use(IpFilter(whiteListIPs, { mode: 'allow' })); -app.use(morgan(process.env.SERVER_LEVEL || 'common')); -app.use(helmet()); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({extended: false})); - -// Routerss -app.use("/api/v1", apiRouterV1); - -// Error Handler -app.use((req: Request, res: Response, next: NextFunction) => { - //const err: ResponseError = new Error('Not Found') as ResponseError; - const err: ResponseError = new Error('Not Found') as ResponseError; - err.status = 404; - next(err); -}); - -app.use((err: ResponseError, req: Request, res: Response) => { - res.locals.message = err.message; - res.locals.error = req.app.get('env') === 'development' ? err : {}; - - if (err instanceof IpDeniedError) { - res.status(401); - } else { - res.status(err.status || 500); - } - - res.render('error'); -}); - -// LISTENING -app.listen(app.get('port'), () => { - //const _GitHublastestRelease = await axios.get('https://api.github.com/repos/dapucita/haxbotron/releases/latest'); - console.log("_| _| _| _| "+"\n"+ - "_| _| _|_|_| _| _| _|_|_| _|_| _|_|_|_| _| _|_| _|_| _|_|_| "+"\n"+ - "_|_|_|_| _| _| _|_| _| _| _| _| _| _|_| _| _| _| _|"+"\n"+ - "_| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _|"+"\n"+ - "_| _| _|_|_| _| _| _|_|_| _|_| _|_| _| _|_| _| _|"); - //console.log(`Lastest Version : ${_GitHublastestRelease.data.tag_name} | Current Version : v${process.env.npm_package_version}`); - console.log(`Haxbotron by dapucita (Visit our GitHub : https://github.com/dapucita/haxbotron)`); - winstonLogger.info(`[db] Haxbotron DB server is opened at ${app.get('port')} port.`); - winstonLogger.info(`[db] IP Whitelist : ${whiteListIPs}`); + const app = new Koa(); // koa server + const router = new Router(); + + router.use('/api/v1', apiRouterV1.routes()); + + app + .use(ip(whiteListIPs)) + .use(logger()) + .use(bodyParser()) + .use(router.routes()) + .use(router.allowedMethods()); + + // LISTENING + app.listen(dbServerSettings.port, () => { + winstonLogger.info(`[db] Haxbotron DB server is opened at ${dbServerSettings.port} port.`); + winstonLogger.info(`[db] IP Whitelist : ${whiteListIPs}`); + }); +}).catch(err => { + winstonLogger.error(`[db] Failed to start server. Error: ${err}`); }); - -export default app; diff --git a/db/controller/banlist.controller.ts b/db/controller/banlist.controller.ts index dfb8f58..06d40da 100644 --- a/db/controller/banlist.controller.ts +++ b/db/controller/banlist.controller.ts @@ -1,7 +1,8 @@ -import { Request, Response, NextFunction } from 'express'; +import { Context } from "koa"; import { IRepository } from '../repository/repository.interface'; import { BanList } from '../entity/banlist.entity'; import { BanListModel } from '../model/BanListModel'; +import { banListModelSchema } from "../model/Validator"; export class BanListController { private readonly _repository: IRepository; @@ -10,49 +11,107 @@ export class BanListController { this._repository = repository; } - public async getAllBannedPlayers(request: Request, response: Response, next: NextFunction): Promise { - if(request.query?.start && request.query?.count) { + public async getAllBannedPlayers(ctx: Context) { + const { ruid } = ctx.params; + const { start, count } = ctx.request.query; + + if (start && count) { return this._repository - .findAll(request.params.ruid, {start: parseInt(request.query.start), count: parseInt(request.query.count)}) - .then((players) => response.status(200).send(players)) - .catch((error) => response.status(404).send({ error: error.message })); + .findAll(ruid, { start: parseInt(start), count: parseInt(count) }) + .then((players) => { + ctx.status = 200; + ctx.body = players; + }) + .catch((error) => { + ctx.status = 404; + ctx.body = { error: error.message }; + }); } else { return this._repository - .findAll(request.params.ruid) - .then((players) => response.status(200).send(players)) - .catch((error) => response.status(404).send({ error: error.message })); + .findAll(ruid) + .then((players) => { + ctx.status = 200; + ctx.body = players; + }) + .catch((error) => { + ctx.status = 404; + ctx.body = { error: error.message }; + }); } } - public async getBannedPlayer(request: Request, response: Response, next: NextFunction): Promise { + public async getBannedPlayer(ctx: Context) { + const { ruid, conn } = ctx.params; + return this._repository - .findSingle(request.params.ruid, request.params.conn) - .then((player) => response.status(200).send(player)) - .catch((error) => response.status(404).send({ error: error.message })); + .findSingle(ruid, conn) + .then((player) => { + ctx.status = 200; + ctx.body = player; + }) + .catch((error) => { + ctx.status = 404; + ctx.body = { error: error.message }; + }); } - public async addBanPlayer(request: Request, response: Response, next: NextFunction): Promise { - let banlistModel: BanListModel = request.body; + public async addBanPlayer(ctx: Context) { + const validationResult = banListModelSchema.validate(ctx.request.body); + + if (validationResult.error) { + ctx.status = 400; + ctx.body = validationResult.error; + return; + } + + const { ruid } = ctx.params; + const banlistModel: BanListModel = ctx.request.body; return this._repository - .addSingle(request.params.ruid, banlistModel) - .then(() => response.status(204).send()) - .catch((error) => response.status(400).send({ error: error.message })); + .addSingle(ruid, banlistModel) + .then(() => { + ctx.status = 204; + }) + .catch((error) => { + ctx.status = 400; + ctx.body = { error: error.message }; + }); } - public async updateBannedPlayer(request: Request, response: Response, next: NextFunction): Promise { - let banlistModel: BanListModel = request.body; + public async updateBannedPlayer(ctx: Context) { + const validationResult = banListModelSchema.validate(ctx.request.body); + + if (validationResult.error) { + ctx.status = 400; + ctx.body = validationResult.error; + return; + } + + const { ruid, conn } = ctx.params; + const banlistModel: BanListModel = ctx.request.body; return this._repository - .updateSingle(request.params.ruid, request.params.conn, banlistModel) - .then(() => response.status(204).send()) - .catch((error) => response.status(404).send({ error: error.message })); + .updateSingle(ruid, conn, banlistModel) + .then(() => { + ctx.status = 204; + }) + .catch((error) => { + ctx.status = 404; + ctx.body = { error: error.message }; + }); } - public async deleteBannedPlayer(request: Request, response: Response, next: NextFunction): Promise { + public async deleteBannedPlayer(ctx: Context) { + const { ruid, conn } = ctx.params; + return this._repository - .deleteSingle(request.params.ruid, request.params.conn) - .then(() => response.status(204).send()) - .catch((error) => response.status(404).send({ error: error.message })); + .deleteSingle(ruid, conn) + .then(() => { + ctx.status = 204; + }) + .catch((error) => { + ctx.status = 404; + ctx.body = { error: error.message }; + }); } -} \ No newline at end of file +} diff --git a/db/controller/player.controller.ts b/db/controller/player.controller.ts index 39bc817..8b1e7cd 100644 --- a/db/controller/player.controller.ts +++ b/db/controller/player.controller.ts @@ -1,7 +1,8 @@ -import { Request, Response, NextFunction } from 'express'; +import { Context } from "koa"; import { IRepository } from '../repository/repository.interface'; import { Player } from '../entity/player.entity'; import { PlayerModel } from '../model/PlayerModel'; +import { playerModelSchema } from "../model/Validator"; export class PlayerController { private readonly _repository: IRepository; @@ -10,49 +11,107 @@ export class PlayerController { this._repository = repository; } - public async getAllPlayers(request: Request, response: Response, next: NextFunction): Promise { - if(request.query?.start && request.query?.count) { + public async getAllPlayers(ctx: Context) { + const { ruid } = ctx.params; + const { start, count } = ctx.request.query; + + if (start && count) { return this._repository - .findAll(request.params.ruid, {start: parseInt(request.query.start), count: parseInt(request.query.count)}) - .then((players) => response.status(200).send(players)) - .catch((error) => response.status(404).send({ error: error.message })); + .findAll(ruid, { start: parseInt(start), count: parseInt(count) }) + .then((players) => { + ctx.status = 200; + ctx.body = players; + }) + .catch((error) => { + ctx.status = 404; + ctx.body = { error: error.message }; + }); } else { return this._repository - .findAll(request.params.ruid) - .then((players) => response.status(200).send(players)) - .catch((error) => response.status(404).send({ error: error.message })); + .findAll(ruid) + .then((players) => { + ctx.status = 200; + ctx.body = players; + }) + .catch((error) => { + ctx.status = 404; + ctx.body = { error: error.message }; + }); } } - public async getPlayer(request: Request, response: Response, next: NextFunction): Promise { + public async getPlayer(ctx: Context) { + const { ruid, auth } = ctx.params; + return this._repository - .findSingle(request.params.ruid, request.params.auth) - .then((player) => response.status(200).send(player)) - .catch((error) => response.status(404).send({ error: error.message })); + .findSingle(ruid, auth) + .then((player) => { + ctx.status = 200; + ctx.body = player; + }) + .catch((error) => { + ctx.status = 404; + ctx.body = { error: error.message }; + }); } - public async addPlayer(request: Request, response: Response, next: NextFunction): Promise { - let playerModel: PlayerModel = request.body; + public async addPlayer(ctx: Context) { + const validationResult = playerModelSchema.validate(ctx.request.body); + + if (validationResult.error) { + ctx.status = 400; + ctx.body = validationResult.error; + return; + } + + const { ruid } = ctx.params; + const playerModel: PlayerModel = ctx.request.body; return this._repository - .addSingle(request.params.ruid, playerModel) - .then(() => response.status(204).send()) - .catch((error) => response.status(400).send({ error: error.message })); + .addSingle(ruid, playerModel) + .then(() => { + ctx.status = 204; + }) + .catch((error) => { + ctx.status = 400; + ctx.body = { error: error.message }; + }); } - public async updatePlayer(request: Request, response: Response, next: NextFunction): Promise { - let playerModel: PlayerModel = request.body; + public async updatePlayer(ctx: Context) { + const validationResult = playerModelSchema.validate(ctx.request.body); + + if (validationResult.error) { + ctx.status = 400; + ctx.body = validationResult.error; + return; + } + + const { ruid, auth } = ctx.params; + const playerModel: PlayerModel = ctx.request.body; return this._repository - .updateSingle(request.params.ruid, request.params.auth, playerModel) - .then(() => response.status(204).send()) - .catch((error) => response.status(404).send({ error: error.message })); + .updateSingle(ruid, auth, playerModel) + .then(() => { + ctx.status = 204; + }) + .catch((error) => { + ctx.status = 404; + ctx.body = { error: error.message }; + }); } - public async deletePlayer(request: Request, response: Response, next: NextFunction): Promise { + public async deletePlayer(ctx: Context) { + const { ruid, auth } = ctx.params; + return this._repository - .deleteSingle(request.params.ruid, request.params.auth) - .then(() => response.status(204).send()) - .catch((error) => response.status(404).send({ error: error.message })); + .deleteSingle(ruid, auth) + .then(() => { + ctx.status = 204; + }) + .catch((error) => { + ctx.status = 404; + ctx.body = { error: error.message }; + }); } } \ No newline at end of file diff --git a/db/controller/superadmin.controller.ts b/db/controller/superadmin.controller.ts index d5cca04..4d07302 100644 --- a/db/controller/superadmin.controller.ts +++ b/db/controller/superadmin.controller.ts @@ -1,6 +1,7 @@ -import { Request, Response, NextFunction } from 'express'; +import { Context } from "koa"; import { SuperAdmin } from '../entity/superadmin.entity'; import { SuperAdminModel } from '../model/SuperAdminModel'; +import { superAdminModelSchema } from "../model/Validator"; import { IRepository } from '../repository/repository.interface'; export class SuperAdminController { @@ -10,49 +11,107 @@ export class SuperAdminController { this._repository = repository; } - public async getAllSuperAdmins(request: Request, response: Response, next: NextFunction): Promise { - if(request.query?.start && request.query?.count) { + public async getAllSuperAdmins(ctx: Context) { + const { ruid } = ctx.params; + const { start, count } = ctx.request.query; + + if(start && count) { return this._repository - .findAll(request.params.ruid, {start: parseInt(request.query.start), count: parseInt(request.query.count)}) - .then((superadmins) => response.status(200).send(superadmins)) - .catch((error) => response.status(404).send({ error: error.message })); + .findAll(ruid, {start: parseInt(start), count: parseInt(count)}) + .then((superadmins) => { + ctx.status = 200; + ctx.body = superadmins; + }) + .catch((error) => { + ctx.status = 404; + ctx.body = { error: error.message }; + }); } else { return this._repository - .findAll(request.params.ruid) - .then((superadmins) => response.status(200).send(superadmins)) - .catch((error) => response.status(404).send({ error: error.message })); + .findAll(ruid) + .then((superadmins) => { + ctx.status = 200; + ctx.body = superadmins; + }) + .catch((error) => { + ctx.status = 404; + ctx.body = { error: error.message }; + }); } } - public async getSuperAdmin(request: Request, response: Response, next: NextFunction): Promise { + public async getSuperAdmin(ctx: Context) { + const { ruid, key } = ctx.params; + return this._repository - .findSingle(request.params.ruid, request.params.key) - .then((superadmin) => response.status(200).send(superadmin)) - .catch((error) => response.status(404).send({ error: error.message })); + .findSingle(ruid, key) + .then((superadmins) => { + ctx.status = 200; + ctx.body = superadmins; + }) + .catch((error) => { + ctx.status = 404; + ctx.body = { error: error.message }; + }); } - public async addSuperAdmin(request: Request, response: Response, next: NextFunction): Promise { - let superAdminModel: SuperAdminModel = request.body; + public async addSuperAdmin(ctx: Context) { + const validationResult = superAdminModelSchema.validate(ctx.request.body); + + if (validationResult.error) { + ctx.status = 400; + ctx.body = validationResult.error; + return; + } + + const { ruid } = ctx.params; + const superAdminModel: SuperAdminModel = ctx.request.body; return this._repository - .addSingle(request.params.ruid, superAdminModel) - .then(() => response.status(204).send()) - .catch((error) => response.status(400).send({ error: error.message })); + .addSingle(ruid, superAdminModel) + .then(() => { + ctx.status = 204; + }) + .catch((error) => { + ctx.status = 400; + ctx.body = { error: error.message }; + }); } - public async updateSuperAdmin(request: Request, response: Response, next: NextFunction): Promise { - let superAdminModel: SuperAdminModel = request.body; + public async updateSuperAdmin(ctx: Context) { + const validationResult = superAdminModelSchema.validate(ctx.request.body); + + if (validationResult.error) { + ctx.status = 400; + ctx.body = validationResult.error; + return; + } + + const { ruid, key } = ctx.params; + const superAdminModel: SuperAdminModel = ctx.request.body; return this._repository - .updateSingle(request.params.ruid, request.params.key, superAdminModel) - .then(() => response.status(204).send()) - .catch((error) => response.status(404).send({ error: error.message })); + .updateSingle(ruid, key, superAdminModel) + .then(() => { + ctx.status = 204; + }) + .catch((error) => { + ctx.status = 404; + ctx.body = { error: error.message }; + }); } - public async deleteSuperAdmin(request: Request, response: Response, next: NextFunction): Promise { + public async deleteSuperAdmin(ctx: Context) { + const { ruid, key } = ctx.params; + return this._repository - .deleteSingle(request.params.ruid, request.params.key) - .then(() => response.status(204).send()) - .catch((error) => response.status(404).send({ error: error.message })); + .deleteSingle(ruid, key) + .then(() => { + ctx.status = 204; + }) + .catch((error) => { + ctx.status = 404; + ctx.body = { error: error.message }; + }); } } \ No newline at end of file diff --git a/db/model/Validator.ts b/db/model/Validator.ts index e609fc3..772a58c 100644 --- a/db/model/Validator.ts +++ b/db/model/Validator.ts @@ -1,42 +1,35 @@ -import { Request, Response, NextFunction } from 'express'; -import { body, validationResult } from "express-validator" +import Joi from 'joi'; -export const validatePlayerModelRules = [ - body('auth').notEmpty().bail().isString(), - body('conn').notEmpty().bail().isString(), - body('name').notEmpty().bail().isString(), - body('rating').notEmpty().bail().isInt(), - body('totals').notEmpty().bail().isInt(), - body('wins').notEmpty().bail().isInt(), - body('goals').notEmpty().bail().isInt(), - body('assists').notEmpty().bail().isInt(), - body('ogs').notEmpty().bail().isInt(), - body('losePoints').notEmpty().bail().isInt(), - body('passed').notEmpty().bail().isInt(), - body('mute').notEmpty().bail().isBoolean(), - body('muteExpire').notEmpty().bail().isInt(), - body('rejoinCount').notEmpty().bail().isInt(), - body('joinDate').notEmpty().bail().isInt(), - body('leftDate').notEmpty().bail().isInt(), - body('malActCount').notEmpty().bail().isInt() -]; +export const playerModelSchema = Joi.object().keys({ + auth: Joi.string().required() + ,conn: Joi.string().required() + ,name: Joi.string().required() + ,rating: Joi.number().required() + ,totals: Joi.number().required() + ,disconns: Joi.number().required() + ,wins: Joi.number().required() + ,goals: Joi.number().required() + ,assists: Joi.number().required() + ,ogs: Joi.number().required() + ,losePoints: Joi.number().required() + ,balltouch: Joi.number().required() + ,passed: Joi.number().required() + ,mute: Joi.boolean().required() + ,muteExpire: Joi.number().required() + ,rejoinCount: Joi.number().required() + ,joinDate: Joi.number().required() + ,leftDate: Joi.number().required() + ,malActCount: Joi.number().required() +}); -export const validateBanListModelRules = [ - body('conn').notEmpty().bail().isString(), - body('reason').notEmpty().bail().isString(), - body('register').notEmpty().bail().isInt(), - body('expire').notEmpty().bail().isInt(), -]; +export const banListModelSchema = Joi.object().keys({ + conn: Joi.string().required() + ,reason: Joi.string().required() + ,register: Joi.number().required() + ,expire: Joi.number().required() +}); -export const validateSuperAdminModelRules = [ - body('key').notEmpty().bail().isString(), - body('description').notEmpty().bail().isString() -]; - -export const checkValidationRules = (request: Request, response: Response, next: NextFunction) => { - const errors = validationResult(request); - if (!errors.isEmpty()) { - return response.status(400).json({ errors: errors.array() }); - } - next(); -}; \ No newline at end of file +export const superAdminModelSchema = Joi.object().keys({ + key: Joi.string().required() + ,description: Joi.string().required() +}); diff --git a/db/model/interface/ResponseError.ts b/db/model/interface/ResponseError.ts deleted file mode 100644 index b6c97bd..0000000 --- a/db/model/interface/ResponseError.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ResponseError extends Error { - status?: number - data?: any -} \ No newline at end of file diff --git a/db/package-lock.json b/db/package-lock.json index 0b1a073..cc4c43e 100644 --- a/db/package-lock.json +++ b/db/package-lock.json @@ -1,29 +1,9 @@ { "name": "haxbotron-db", - "version": "0.4.4", + "version": "0.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { - "@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" - }, - "@babel/parser": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", - "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==" - }, - "@babel/types": { - "version": "7.12.12", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", - "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", - "requires": { - "@babel/helper-validator-identifier": "^7.12.11", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, "@dabh/diagnostics": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz", @@ -34,11 +14,58 @@ "kuler": "^2.0.0" } }, + "@hapi/hoek": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.1.1.tgz", + "integrity": "sha512-CAEbWH7OIur6jEOzaai83jq3FmKmv4PmX1JYfs9IrYcGEVI/lyL1EXJGCj7eFVJ0bg5QR8LMxBlEtA+xKiLpFw==" + }, + "@hapi/topo": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.0.0.tgz", + "integrity": "sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@koa/cors": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@koa/cors/-/cors-3.1.0.tgz", + "integrity": "sha512-7ulRC1da/rBa6kj6P4g2aJfnET3z8Uf3SWu60cjbtxTA5g8lxRdX/Bd2P92EagGwwAhANeNw8T8if99rJliR6Q==", + "requires": { + "vary": "^1.1.2" + } + }, + "@sideway/address": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.1.tgz", + "integrity": "sha512-+I5aaQr3m0OAmMr7RQ3fR9zx55sejEYR2BFJaxL+zT3VM2611X0SHvPWIbAUBZVTn/YzYKbV8gJ2oT/QELknfQ==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@sideway/formula": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", + "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==" + }, + "@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, "@sqltools/formatter": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.2.tgz", "integrity": "sha512-/5O7Fq6Vnv8L6ucmPjaWbVG1XkP4FO+w5glqfkIsq3Xw4oyNAdJddbnYodNDAfjVUvo/rrSCTom4kAND7T1o5Q==" }, + "@types/accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", + "requires": { + "@types/node": "*" + } + }, "@types/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", @@ -56,10 +83,26 @@ "@types/node": "*" } }, - "@types/cors": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.9.tgz", - "integrity": "sha512-zurD1ibz21BRlAOIKP8yhrxlqKx6L9VCwkB5kMiP6nZAhoF5MvC7qS1qPA7nRcr1GJolfkQC7/EAL4hdYejLtg==" + "@types/content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-P1bffQfhD3O4LW0ioENXUhZ9OIa0Zn+P7M+pWgkCKaT53wVLSq0mrKksCID/FGHpFhRSxRGhgrQmfhRuzwtKdg==" + }, + "@types/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg==" + }, + "@types/cookies": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.6.tgz", + "integrity": "sha512-FK4U5Qyn7/Sc5ih233OuHO0qAkOpEcD/eG6584yEiLKizTFRny86qHLe/rej3HFQrkBuUjF4whFliAdODbVN/w==", + "requires": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } }, "@types/express": { "version": "4.17.9", @@ -82,19 +125,83 @@ "@types/range-parser": "*" } }, + "@types/http-assert": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.1.tgz", + "integrity": "sha512-PGAK759pxyfXE78NbKxyfRcWYA/KwW17X290cNev/qAsn9eQIxkH4shoNBafH37wewhDG/0p1cHPbK6+SzZjWQ==" + }, + "@types/http-errors": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.0.tgz", + "integrity": "sha512-2aoSC4UUbHDj2uCsCxcG/vRMXey/m17bC7UwitVm5hn22nI8O8Y9iDpA76Orc+DWkQ4zZrOKEshCqR/jSuXAHA==" + }, + "@types/keygrip": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz", + "integrity": "sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==" + }, + "@types/koa": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.13.1.tgz", + "integrity": "sha512-Qbno7FWom9nNqu0yHZ6A0+RWt4mrYBhw3wpBAQ3+IuzGcLlfeYkzZrnMq5wsxulN2np8M4KKeUpTodsOsSad5Q==", + "requires": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "@types/koa-bodyparser": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@types/koa-bodyparser/-/koa-bodyparser-4.3.0.tgz", + "integrity": "sha512-aB/vwwq4G9FAtKzqZ2p8UHTscXxZvICFKVjuckqxCtkX1Ro7F5KHkTCUqTRZFBgDoEkmeca+bFLI1bIsdPPZTA==", + "dev": true, + "requires": { + "@types/koa": "*" + } + }, + "@types/koa-compose": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.5.tgz", + "integrity": "sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==", + "requires": { + "@types/koa": "*" + } + }, + "@types/koa-logger": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/koa-logger/-/koa-logger-3.1.1.tgz", + "integrity": "sha512-wp2HaskkPugfwgXgNnc+idnReuJZSTTYQbkcxXjsMhp1kTc342PxDzTL9FXDgBfEvgt9NX1CCGjkwPKX2dlEKQ==", + "requires": { + "@types/koa": "*" + } + }, + "@types/koa-router": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/koa-router/-/koa-router-7.4.1.tgz", + "integrity": "sha512-Hg78TXz78QYfEgdq3nTeRmQFEwJKZljsXb/DhtexmyrpRDRnl59oMglh9uPj3/WgKor0woANrYTnxA8gaWGK2A==", + "dev": true, + "requires": { + "@types/koa": "*" + } + }, + "@types/koa__cors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/koa__cors/-/koa__cors-3.0.2.tgz", + "integrity": "sha512-gBetQR0DJ9JTG1YQoW33BADHCrDPJGiJUKUUcEPJwW1A2unzpIMhorEpXB6eMaaXTaqHLemcGnq3RmH9XaryRQ==", + "requires": { + "@types/koa": "*" + } + }, "@types/mime": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==" }, - "@types/morgan": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.2.tgz", - "integrity": "sha512-edtGMEdit146JwwIeyQeHHg9yID4WSolQPxpEorHmN3KuytuCHyn2ELNr5Uxy8SerniFbbkmgKMrGM933am5BQ==", - "requires": { - "@types/node": "*" - } - }, "@types/node": { "version": "14.14.20", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.20.tgz", @@ -133,11 +240,6 @@ "negotiator": "0.6.2" } }, - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" - }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -210,16 +312,6 @@ "sprintf-js": "~1.0.2" } }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" - }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -229,11 +321,6 @@ "safer-buffer": "~2.1.0" } }, - "assert-never": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz", - "integrity": "sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==" - }, "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -263,14 +350,6 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "optional": true }, - "babel-walk": { - "version": "3.0.0-canary-5", - "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", - "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", - "requires": { - "@babel/types": "^7.9.6" - } - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -281,14 +360,6 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, - "basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "requires": { - "safe-buffer": "5.1.2" - } - }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -307,23 +378,6 @@ "inherits": "~2.0.0" } }, - "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", - "requires": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - } - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -347,6 +401,15 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, + "cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "requires": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + } + }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -367,14 +430,6 @@ "supports-color": "^7.1.0" } }, - "character-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", - "integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=", - "requires": { - "is-regex": "^1.0.3" - } - }, "chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -481,6 +536,22 @@ } } }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "co-body": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.1.0.tgz", + "integrity": "sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==", + "requires": { + "inflation": "^2.0.0", + "qs": "^6.5.2", + "raw-body": "^2.3.3", + "type-is": "^1.6.16" + } + }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -550,15 +621,6 @@ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, - "constantinople": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", - "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", - "requires": { - "@babel/parser": "^7.6.0", - "@babel/types": "^7.6.1" - } - }, "content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -573,29 +635,36 @@ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + "cookies": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", + "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", + "requires": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + } + } + }, + "copy-to": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz", + "integrity": "sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU=" }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, - "cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "requires": { - "object-assign": "^4", - "vary": "^1" - } - }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -605,19 +674,16 @@ "assert-plus": "^1.0.0" } }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -649,11 +715,6 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, - "doctypes": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", - "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=" - }, "dotenv": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", @@ -709,68 +770,6 @@ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "express": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", - "requires": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - } - }, - "express-ipfilter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/express-ipfilter/-/express-ipfilter-1.1.2.tgz", - "integrity": "sha512-dm1G3sVxlSbcOWSxfUTCo20ySyNQXJ4hJD5fuQJFoZlhkQvpbuDGBlh8AbFm1GwX85EWvfyhekOkvcydaXkBkg==", - "requires": { - "ip": "~1.1.0", - "lodash": "^4.17.11", - "proxy-addr": "^2.0.4", - "range_check": "^1.2.0" - } - }, - "express-validator": { - "version": "6.9.2", - "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.9.2.tgz", - "integrity": "sha512-Yqlsw2/uBobtBVkP+gnds8OMmVAEb3uTI4uXC93l0Ym5JGHgr8Vd4ws7oSo7GGYpWn5YCq4UePMEppKchURXrw==", - "requires": { - "lodash": "^4.17.20", - "validator": "^13.5.2" - } - }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -818,20 +817,6 @@ "moment": "^2.11.2" } }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - } - }, "find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -863,11 +848,6 @@ "mime-types": "^2.1.12" } }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" - }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -898,11 +878,6 @@ "rimraf": "2" } }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", @@ -967,14 +942,6 @@ "har-schema": "^2.0.0" } }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", @@ -988,26 +955,25 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, - "has-symbols": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" - }, "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, - "helmet": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-4.3.1.tgz", - "integrity": "sha512-WsafDyKsIexB0+pUNkq3rL1rB5GVAghR68TP8ssM9DPEMzfBiluEQlVzJ/FEj6Vq2Ag3CNuxf7aYMjXrN0X49Q==" - }, "highlight.js": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.5.0.tgz", "integrity": "sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw==" }, + "http-assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.1.tgz", + "integrity": "sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw==", + "requires": { + "deep-equal": "~1.0.1", + "http-errors": "~1.7.2" + } + }, "http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -1031,6 +997,11 @@ "sshpk": "^1.7.0" } }, + "humanize-number": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/humanize-number/-/humanize-number-0.0.2.tgz", + "integrity": "sha1-EcCvakcWQ2M1iFiASPF5lUFInBg=" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1052,6 +1023,11 @@ "minimatch": "^3.0.4" } }, + "inflation": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", + "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1071,43 +1047,11 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" - }, - "ip6": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/ip6/-/ip6-0.0.4.tgz", - "integrity": "sha1-RMWp23njnUBSAbTXjROzhw5I2zE=" - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - }, "is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, - "is-core-module": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", - "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", - "requires": { - "has": "^1.0.3" - } - }, - "is-expression": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", - "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", - "requires": { - "acorn": "^7.1.1", - "object-assign": "^4.1.1" - } - }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", @@ -1116,18 +1060,10 @@ "number-is-nan": "^1.0.0" } }, - "is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" - }, - "is-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", - "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", - "requires": { - "has-symbols": "^1.0.1" - } + "is-generator-function": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.8.tgz", + "integrity": "sha512-2Omr/twNtufVZFr1GhxjOMFPAj2sjc/dKaIqBhvo4qciXfJmITGH6ZGd8eZYNHza8t1y0e01AuqRhJwfWp26WQ==" }, "is-stream": { "version": "2.0.0", @@ -1140,6 +1076,11 @@ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "optional": true }, + "is_js": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/is_js/-/is_js-0.9.0.tgz", + "integrity": "sha1-CrlFQFArp6+iTIVqqYVWFmnpxS0=" + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -1157,10 +1098,17 @@ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", "optional": true }, - "js-stringify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", - "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" + "joi": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.4.0.tgz", + "integrity": "sha512-F4WiW2xaV6wc1jxete70Rw4V/VuMd6IN+a5ilZsxG4uYtUXWu2kq9W5P2dz30e7Gmw8RCbY/u/uk+dMPma9tAg==", + "requires": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.0", + "@sideway/formula": "^3.0.0", + "@sideway/pinpoint": "^2.0.0" + } }, "js-yaml": { "version": "3.14.1", @@ -1207,13 +1155,213 @@ "verror": "1.10.0" } }, - "jstransformer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", - "integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=", + "keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "requires": { + "tsscmp": "1.0.6" + } + }, + "koa": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.13.1.tgz", + "integrity": "sha512-Lb2Dloc72auj5vK4X4qqL7B5jyDPQaZucc9sR/71byg7ryoD1NCaCm63CShk9ID9quQvDEi1bGR/iGjCG7As3w==", + "requires": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.8.0", + "debug": "~3.1.0", + "delegates": "^1.0.0", + "depd": "^2.0.0", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^1.2.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + } + } + }, + "koa-bodyparser": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/koa-bodyparser/-/koa-bodyparser-4.3.0.tgz", + "integrity": "sha512-uyV8G29KAGwZc4q/0WUAjH+Tsmuv9ImfBUF2oZVyZtaeo0husInagyn/JH85xMSxM0hEk/mbCII5ubLDuqW/Rw==", "requires": { - "is-promise": "^2.0.0", - "promise": "^7.0.1" + "co-body": "^6.0.0", + "copy-to": "^2.0.1" + } + }, + "koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==" + }, + "koa-convert": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", + "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", + "requires": { + "co": "^4.6.0", + "koa-compose": "^3.0.0" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "^1.1.0" + } + } + } + }, + "koa-ip": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/koa-ip/-/koa-ip-2.1.0.tgz", + "integrity": "sha512-3gpcu8i2YFR0jf8j98Mw2yUAglu3powVxFiYQGMe89n6JGGVIg6dv7zRMN+l8HF3wdiLseAfj2C97h+wlJAw4Q==", + "requires": { + "debug": "4.1.1", + "lodash.isplainobject": "4.0.6", + "request-ip": "2.1.3" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "koa-logger": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-logger/-/koa-logger-3.2.1.tgz", + "integrity": "sha512-MjlznhLLKy9+kG8nAXKJLM0/ClsQp/Or2vI3a5rbSQmgl8IJBQO0KI5FA70BvW+hqjtxjp49SpH2E7okS6NmHg==", + "requires": { + "bytes": "^3.1.0", + "chalk": "^2.4.2", + "humanize-number": "0.0.2", + "passthrough-counter": "^1.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "koa-router": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/koa-router/-/koa-router-10.0.0.tgz", + "integrity": "sha512-gAE5J1gBQTvfR8rMMtMUkE26+1MbO3DGpGmvfmM2pR9Z7w2VIb2Ecqeal98yVO7+4ltffby7gWOzpCmdNOQe0w==", + "requires": { + "debug": "^4.1.1", + "http-errors": "^1.7.3", + "koa-compose": "^4.1.0", + "methods": "^1.1.2", + "path-to-regexp": "^6.1.0" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "http-errors": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", + "integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "path-to-regexp": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.0.tgz", + "integrity": "sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg==" + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + } } }, "kuler": { @@ -1229,10 +1377,10 @@ "p-locate": "^4.1.0" } }, - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" }, "logform": { "version": "2.2.0", @@ -1258,21 +1406,11 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, "mime-db": { "version": "1.45.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", @@ -1336,25 +1474,6 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" }, - "morgan": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", - "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", - "requires": { - "basic-auth": "~2.0.1", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-finished": "~2.3.0", - "on-headers": "~1.0.2" - }, - "dependencies": { - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - } - } - }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -1557,11 +1676,6 @@ "ee-first": "1.1.1" } }, - "on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" - }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1578,6 +1692,11 @@ "fn.name": "1.x.x" } }, + "only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" + }, "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", @@ -1648,6 +1767,11 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "passthrough-counter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passthrough-counter/-/passthrough-counter-1.0.0.tgz", + "integrity": "sha1-GWfZ5m2lcrXAI8eH2xEqOHqxZvo=" + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -1658,16 +1782,6 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -1679,141 +1793,12 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, - "promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "requires": { - "asap": "~2.0.3" - } - }, - "proxy-addr": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", - "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.1" - } - }, "psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", "optional": true }, - "pug": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.0.tgz", - "integrity": "sha512-inmsJyFBSHZaiGLaguoFgJGViX0If6AcfcElimvwj9perqjDpUpw79UIEDZbWFmoGVidh08aoE+e8tVkjVJPCw==", - "requires": { - "pug-code-gen": "^3.0.0", - "pug-filters": "^4.0.0", - "pug-lexer": "^5.0.0", - "pug-linker": "^4.0.0", - "pug-load": "^3.0.0", - "pug-parser": "^6.0.0", - "pug-runtime": "^3.0.0", - "pug-strip-comments": "^2.0.0" - } - }, - "pug-attrs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", - "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", - "requires": { - "constantinople": "^4.0.1", - "js-stringify": "^1.0.2", - "pug-runtime": "^3.0.0" - } - }, - "pug-code-gen": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.1.tgz", - "integrity": "sha512-xJIGvmXTQlkJllq6hqxxjRWcay2F9CU69TuAuiVZgHK0afOhG5txrQOcZyaPHBvSWCU/QQOqEp5XCH94rRZpBQ==", - "requires": { - "constantinople": "^4.0.1", - "doctypes": "^1.1.0", - "js-stringify": "^1.0.2", - "pug-attrs": "^3.0.0", - "pug-error": "^2.0.0", - "pug-runtime": "^3.0.0", - "void-elements": "^3.1.0", - "with": "^7.0.0" - } - }, - "pug-error": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.0.0.tgz", - "integrity": "sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==" - }, - "pug-filters": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", - "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", - "requires": { - "constantinople": "^4.0.1", - "jstransformer": "1.0.0", - "pug-error": "^2.0.0", - "pug-walk": "^2.0.0", - "resolve": "^1.15.1" - } - }, - "pug-lexer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.0.tgz", - "integrity": "sha512-52xMk8nNpuyQ/M2wjZBN5gXQLIylaGkAoTk5Y1pBhVqaopaoj8Z0iVzpbFZAqitL4RHNVDZRnJDsqEYe99Ti0A==", - "requires": { - "character-parser": "^2.2.0", - "is-expression": "^4.0.0", - "pug-error": "^2.0.0" - } - }, - "pug-linker": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", - "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", - "requires": { - "pug-error": "^2.0.0", - "pug-walk": "^2.0.0" - } - }, - "pug-load": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", - "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", - "requires": { - "object-assign": "^4.1.1", - "pug-walk": "^2.0.0" - } - }, - "pug-parser": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", - "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", - "requires": { - "pug-error": "^2.0.0", - "token-stream": "1.0.0" - } - }, - "pug-runtime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.0.tgz", - "integrity": "sha512-GoEPcmQNnaTsePEdVA05bDpY+Op5VLHKayg08AQiqJBWU/yIaywEYv7TetC5dEQS3fzBBoyb2InDcZEg3mPTIA==" - }, - "pug-strip-comments": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", - "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", - "requires": { - "pug-error": "^2.0.0" - } - }, - "pug-walk": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", - "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==" - }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -1825,27 +1810,6 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "range_check": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/range_check/-/range_check-1.4.0.tgz", - "integrity": "sha1-zYfHrGLEC6nfabhwPGBPYMN0hjU=", - "requires": { - "ip6": "0.0.4", - "ipaddr.js": "1.2" - }, - "dependencies": { - "ipaddr.js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.2.0.tgz", - "integrity": "sha1-irpJyRknmVhb3WQ+DMtQ6K53e6Q=" - } - } - }, "raw-body": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", @@ -1929,6 +1893,14 @@ } } }, + "request-ip": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/request-ip/-/request-ip-2.1.3.tgz", + "integrity": "sha512-J3qdE/IhVM3BXkwMIVO4yFrvhJlU3H7JH16+6yHucadT4fePnR8dyh+vEs6FIx0S2x5TCt2ptiPfHcn0sqhbYQ==", + "requires": { + "is_js": "^0.9.0" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -1939,15 +1911,6 @@ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, - "resolve": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", - "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", - "requires": { - "is-core-module": "^2.1.0", - "path-parse": "^1.0.6" - } - }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -1971,44 +1934,6 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, - "send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "dependencies": { - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - } - } - }, - "serve-static": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" - } - }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -2154,21 +2079,11 @@ "thenify": ">= 3.1.0 < 4" } }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" - }, "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, - "token-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", - "integrity": "sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=" - }, "tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -2189,6 +2104,11 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2281,16 +2201,6 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "validator": { - "version": "13.5.2", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.5.2.tgz", - "integrity": "sha512-mD45p0rvHVBlY2Zuy3F3ESIe1h5X58GPfAtslBjY7EtTqGquZTj+VX/J4RnHWN8FKq0C9WRVt1oWAcytWRuYLQ==" - }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -2307,11 +2217,6 @@ "extsprintf": "^1.2.0" } }, - "void-elements": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=" - }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -2382,17 +2287,6 @@ "triple-beam": "^1.2.0" } }, - "with": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", - "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", - "requires": { - "@babel/parser": "^7.9.6", - "@babel/types": "^7.9.6", - "assert-never": "^1.2.1", - "babel-walk": "3.0.0-canary-5" - } - }, "wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -2573,6 +2467,11 @@ "camelcase": "^5.0.0", "decamelize": "^1.2.0" } + }, + "ylru": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", + "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==" } } } diff --git a/db/package.json b/db/package.json index 34b0cc2..6ea6f76 100644 --- a/db/package.json +++ b/db/package.json @@ -1,6 +1,6 @@ { "name": "haxbotron-db", - "version": "0.4.5", + "version": "0.5.0", "description": "Haxbotron is a headless host server application for Haxball.", "main": "out/app.js", "author": "dapucita", @@ -18,20 +18,19 @@ "build": "tsc" }, "dependencies": { - "@types/body-parser": "^1.19.0", - "@types/cors": "^2.8.9", - "@types/express": "^4.17.9", - "@types/morgan": "^1.9.2", + "@koa/cors": "^3.1.0", + "@types/cookie": "^0.4.0", + "@types/koa-logger": "^3.1.1", + "@types/koa__cors": "^3.0.2", "@types/node": "^14.14.20", - "body-parser": "^1.19.0", - "cors": "^2.8.5", + "cookie": "^0.4.1", "dotenv": "^8.2.0", - "express": "^4.17.1", - "express-ipfilter": "^1.1.2", - "express-validator": "^6.9.2", - "helmet": "^4.3.1", - "morgan": "^1.10.0", - "pug": "^3.0.0", + "joi": "^17.3.0", + "koa": "^2.13.1", + "koa-bodyparser": "^4.3.0", + "koa-ip": "^2.1.0", + "koa-logger": "^3.2.1", + "koa-router": "^10.0.0", "reflect-metadata": "^0.1.13", "sqlite3": "^5.0.0", "typeorm": "^0.2.29", @@ -39,6 +38,9 @@ "winston-daily-rotate-file": "^4.5.0" }, "devDependencies": { - "typescript": "^4.1.3" + "typescript": "^4.1.3", + "@types/koa": "^2.11.6", + "@types/koa-bodyparser": "^4.3.0", + "@types/koa-router": "^7.4.1" } } diff --git a/db/router/v1.api.router.ts b/db/router/v1.api.router.ts index a48babf..c23fbc5 100644 --- a/db/router/v1.api.router.ts +++ b/db/router/v1.api.router.ts @@ -1,19 +1,18 @@ -import express, { Router } from "express"; -import cors from "cors"; +import Router from "koa-router"; +import cors from "@koa/cors"; import { playerRouter } from "./v1.player.router"; import { banlistRouter } from "./v1.banlist.router"; import { superadminRouter } from "./v1.superadmin.router"; import { ruidlistRouter } from "./v1.ruidlist.router"; -export const apiRouterV1: Router = express.Router({ mergeParams: true }); +export const apiRouterV1 = new Router(); -apiRouterV1.use(cors({ - origin: true, // Access-Control-Allow-Origin - credentials: true // Access-Control-Allow-Credentials -})); - -apiRouterV1.use('/ruidlist', ruidlistRouter); - -apiRouterV1.use('/room/:ruid/player', playerRouter); -apiRouterV1.use('/room/:ruid/banlist', banlistRouter); -apiRouterV1.use('/room/:ruid/superadmin', superadminRouter); +apiRouterV1 + .use(cors({ + origin: process.env.CLIENT_HOST, // Access-Control-Allow-Origin + credentials: true, // Access-Control-Allow-Credentials + })) + .use('/ruidlist', ruidlistRouter.routes()) + .use('/room/:ruid/player', playerRouter.routes()) + .use('/room/:ruid/banlist', banlistRouter.routes()) + .use('/room/:ruid/superadmin', superadminRouter.routes()); diff --git a/db/router/v1.banlist.router.ts b/db/router/v1.banlist.router.ts index 7c49bc2..52a07f6 100644 --- a/db/router/v1.banlist.router.ts +++ b/db/router/v1.banlist.router.ts @@ -1,41 +1,41 @@ -import express, { Request, Response, Router, NextFunction } from "express"; +import Router from "koa-router"; +import { Context } from "koa"; import { BanListController } from "../controller/banlist.controller"; import { BanList } from "../entity/banlist.entity"; -import { checkValidationRules, validateBanListModelRules } from "../model/Validator"; import { BanListRepository } from "../repository/banlist.repository"; import { IRepository } from "../repository/repository.interface"; -export const banlistRouter: Router = express.Router({ mergeParams: true }); +export const banlistRouter = new Router(); const banlistRepository: IRepository = new BanListRepository(); const controller: BanListController = new BanListController(banlistRepository); // /v1/banlist GET // /v1/banlist?start&end GET // get all banned players list and data -banlistRouter.get('/', async (request: Request, response: Response, next: NextFunction) => { - await controller.getAllBannedPlayers(request, response, next); +banlistRouter.get('/', async (ctx: Context) => { + await controller.getAllBannedPlayers(ctx) }); // /v1/banlist POST // register new player -banlistRouter.post('/', validateBanListModelRules, checkValidationRules, async (request: Request, response: Response, next: NextFunction) => { - await controller.addBanPlayer(request, response, next); +banlistRouter.post('/', async (ctx: Context) => { + await controller.addBanPlayer(ctx) }); // /v1/banlist/:conn GET // get the banned player data -banlistRouter.get('/:conn', async (request: Request, response: Response, next: NextFunction) => { - await controller.getBannedPlayer(request, response, next); +banlistRouter.get('/:conn', async (ctx: Context) => { + await controller.getBannedPlayer(ctx) }); // /v1/banlist/:conn PUT // update whole data of the banned player -banlistRouter.put('/:conn', validateBanListModelRules, checkValidationRules, async (request: Request, response: Response, next: NextFunction) => { - await controller.updateBannedPlayer(request, response, next); +banlistRouter.put('/:conn', async (ctx: Context) => { + await controller.updateBannedPlayer(ctx) }); // /v1/banlist/:conn DELETE // delete the player from ban list -banlistRouter.delete('/:conn', async (request: Request, response: Response, next: NextFunction) => { - await controller.deleteBannedPlayer(request, response, next); +banlistRouter.delete('/:conn', async (ctx: Context) => { + await controller.deleteBannedPlayer(ctx) }); diff --git a/db/router/v1.player.router.ts b/db/router/v1.player.router.ts index 2f33d0b..2022886 100644 --- a/db/router/v1.player.router.ts +++ b/db/router/v1.player.router.ts @@ -1,40 +1,40 @@ -import express, { Request, Response, Router, NextFunction } from "express"; +import Router from "koa-router"; +import { Context } from "koa"; import { PlayerController } from '../controller/player.controller'; import { IRepository } from '../repository/repository.interface'; import { PlayerRepository } from '../repository/player.repository'; import { Player } from '../entity/player.entity'; -import { checkValidationRules, validatePlayerModelRules } from "../model/Validator"; -export const playerRouter: Router = express.Router({ mergeParams: true }); +export const playerRouter = new Router(); const playersRepository: IRepository = new PlayerRepository(); const controller: PlayerController = new PlayerController(playersRepository); // /v1/player GET // get all players list and data -playerRouter.get('/', async (request: Request, response: Response, next: NextFunction) => { - await controller.getAllPlayers(request, response, next); +playerRouter.get('/', async (ctx: Context) => { + await controller.getAllPlayers(ctx); }); // /v1/player POST // register new player -playerRouter.post('/', validatePlayerModelRules, checkValidationRules, async (request: Request, response: Response, next: NextFunction) => { - await controller.addPlayer(request, response, next); +playerRouter.post('/', async (ctx: Context) => { + await controller.addPlayer(ctx) }); // /v1/player/:auth GET // get the player data -playerRouter.get('/:auth', async (request: Request, response: Response, next: NextFunction) => { - await controller.getPlayer(request, response, next); +playerRouter.get('/:auth', async (ctx: Context) => { + await controller.getPlayer(ctx) }); // /v1/player/:auth PUT // update whole data of the player -playerRouter.put('/:auth', validatePlayerModelRules, checkValidationRules, async (request: Request, response: Response, next: NextFunction) => { - await controller.updatePlayer(request, response, next); +playerRouter.put('/:auth', async (ctx: Context) => { + await controller.updatePlayer(ctx) }); // /v1/player/:auth DELETE // delete the player -playerRouter.delete('/:auth', async (request: Request, response: Response, next: NextFunction) => { - await controller.deletePlayer(request, response, next); +playerRouter.delete('/:auth', async (ctx: Context) => { + await controller.deletePlayer(ctx) }); diff --git a/db/router/v1.ruidlist.router.ts b/db/router/v1.ruidlist.router.ts index f7ea628..2a90288 100644 --- a/db/router/v1.ruidlist.router.ts +++ b/db/router/v1.ruidlist.router.ts @@ -1,4 +1,5 @@ -import express, { Request, Response, Router, NextFunction } from "express"; +import { Context } from "koa"; +import Router from "koa-router"; import { Player } from '../entity/player.entity'; import { getRepository, Repository } from "typeorm"; @@ -6,17 +7,22 @@ interface ruidListItem { ruid: string } -export const ruidlistRouter: Router = express.Router({ mergeParams: true }); +export const ruidlistRouter = new Router(); // Get All exist RUIDs list on DB -ruidlistRouter.get('/', async (request: Request, response: Response, next: NextFunction) => { - //await controller.getAllPlayers(request, response, next); +ruidlistRouter.get('/', async (ctx: Context) => { const repository: Repository = getRepository(Player); await repository .createQueryBuilder('Player') .select('ruid') .distinct(true) .getRawMany() - .then((ruidList: ruidListItem[]) => response.status(200).send(ruidList)) - .catch((error) => response.status(404).send({ error: error.message })); + .then((ruidList: ruidListItem[]) => { + ctx.status = 200; + ctx.body = ruidList; + }) + .catch((error) => { + ctx.status = 404; + ctx.body = { error: error.message }; + }); }); diff --git a/db/router/v1.superadmin.router.ts b/db/router/v1.superadmin.router.ts index 382847b..0666aa3 100644 --- a/db/router/v1.superadmin.router.ts +++ b/db/router/v1.superadmin.router.ts @@ -1,34 +1,34 @@ -import express, { Request, Response, Router, NextFunction } from "express"; +import Router from "koa-router"; +import { Context } from "koa"; import { SuperAdminController } from "../controller/superadmin.controller"; import { SuperAdmin } from "../entity/superadmin.entity"; -import { checkValidationRules, validateSuperAdminModelRules } from "../model/Validator"; import { IRepository } from "../repository/repository.interface"; import { SuperAdminRepository } from "../repository/superadmin.repository"; -export const superadminRouter: Router = express.Router({ mergeParams: true }); +export const superadminRouter = new Router(); const superadminRepository: IRepository = new SuperAdminRepository(); const controller: SuperAdminController = new SuperAdminController(superadminRepository); // /v1/superadmin POST // register new superadmin key -superadminRouter.post('/', validateSuperAdminModelRules, checkValidationRules, async (request: Request, response: Response, next: NextFunction) => { - await controller.addSuperAdmin(request, response, next); +superadminRouter.post('/', async (ctx: Context) => { + await controller.addSuperAdmin(ctx) }); // /v1/superadmin GET // get all superadmin data -superadminRouter.get('/', async (request: Request, response: Response, next: NextFunction) => { - await controller.getAllSuperAdmins(request, response, next); +superadminRouter.get('/', async (ctx: Context) => { + await controller.getAllSuperAdmins(ctx) }); // /v1/superadmin/:key GET // get the superadmin data (key and description) -superadminRouter.get('/:key', async (request: Request, response: Response, next: NextFunction) => { - await controller.getSuperAdmin(request, response, next); +superadminRouter.get('/:key', async (ctx: Context) => { + await controller.getSuperAdmin(ctx) }); // /v1/superadmin/:key DELETE // delete superadmin key -superadminRouter.delete('/:key', async (request: Request, response: Response, next: NextFunction) => { - await controller.deleteSuperAdmin(request, response, next); +superadminRouter.delete('/:key', async (ctx: Context) => { + await controller.deleteSuperAdmin(ctx) }); diff --git a/db/tsconfig.json b/db/tsconfig.json index 6b50f8f..a12cedf 100644 --- a/db/tsconfig.json +++ b/db/tsconfig.json @@ -3,9 +3,8 @@ "experimentalDecorators": true, "emitDecoratorMetadata": true, - "target": "es6", + "target": "ES2018", "module": "commonjs", - "lib": ["es6"], "allowJs": true, "resolveJsonModule": true, diff --git a/db/view/error.pug b/db/view/error.pug deleted file mode 100644 index eb1d7fc..0000000 --- a/db/view/error.pug +++ /dev/null @@ -1,3 +0,0 @@ -h1= message -h2= error.status -pre #{error.stack} diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index c419263..0000000 --- a/docs/_config.yml +++ /dev/null @@ -1 +0,0 @@ -theme: jekyll-theme-cayman \ No newline at end of file diff --git a/docs/haxbotron-img.png b/docs/haxbotron-img.png deleted file mode 100644 index c87cda0..0000000 Binary files a/docs/haxbotron-img.png and /dev/null differ diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index f0f5093..0000000 --- a/docs/index.md +++ /dev/null @@ -1,39 +0,0 @@ -# Haxbotron 🤖 -![haxbotron-image](haxbotron-img.png) - -Haxbotron is a host bot application for Haxball game. - -This project is maintained by `dapucita`. - -## Donate -Please donate and support this project by [Patreon](https://www.patreon.com/dapucita) ! - -위 페이지에서 기부하여 이 프로젝트를 지원해주세요! - -## How to Use -- `README.md` -- [Getting Started](https://github.com/dapucita/haxbotron/wiki/Getting-Started) -- [Game Playing](https://github.com/dapucita/haxbotron/wiki/Game-Playing) -- [Core Server](https://github.com/dapucita/haxbotron/wiki/Core-Server) -- [DB Server](https://github.com/dapucita/haxbotron/wiki/DB-Server) -- ...and check also our [wiki](https://github.com/dapucita/haxbotron/wiki) and documents in `docs`. - -## Features -- launch on Multi platforms: Windows, Linux, OS X and so on. -- Automatic game operating system -- Detailed player data and statistics -- Sophisticated tier and rating system -- Powerful anti-trolling system -- Practical tools for actual gaming -- Built-in maps, especially popular in Korea. - -## Language Localisations -- English -- Korean(한국어) - -## Contacts -- [Github](https://github.com/dapucita/haxbotron) -- [Discord](https://discord.gg/qfg45B2) - -## Copyrights -By `dapucita`, MIT License. diff --git a/package.json b/package.json index 8beb23e..3047430 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "haxbotron", - "version": "0.4.5", + "version": "0.5.0", "description": "Haxbotron is a headless host server application for Haxball.", "main": "out/index.js", "author": "dapucita",