// Copyright 2019-2022 Campbell Crowley. All rights reserved. // Author: Campbell Crowley (web@campbellcrowley.com) const http = require('http'); const socketIo = require('socket.io'); const auth = require('../../auth.js'); const crypto = require('crypto'); const HungryGames = require('../hg/HungryGames.js'); const MessageMaker = require('../lib/MessageMaker.js'); require('../subModule.js').extend(HGWeb); // Extends the SubModule class. /** * @classdesc Creates a web interface for managing the Hungry Games. Expects * ../hungryGames.js is loaded or will be loaded. * @class */ function HGWeb() { const self = this; this.myName = 'HGWeb'; let hg_ = null; let ioClient; /** * Buffer storing all current image uploads and their associated meta-data. * * @private * @type {object} */ const imageBuffer = {}; let app; let io; if (!this.common.isSlave) { app = http.createServer(handler); app.on('error', (err) => { if (io) io.close(); if (app) app.close(); if (err.code === 'EADDRINUSE') { this.warn( 'HGWeb failed to bind to port because it is in use. (' + err.port + ')'); if (!this.common.isMaster) { this.log( 'Restarting into client mode due to server already bound ' + 'to port.'); startClient(); } } else { this.error('HGWeb failed to bind to port for unknown reason.', err); } }); } /** * Start a socketio client connection to the primary running server. * * @private */ function startClient() { const client = require('socket.io-client'); if (self.common.isSlave) { const host = self.common.masterHost; const port = host.host === 'localhost' ? (self.common.isRelease ? 8011 : 8013) : host.port; ioClient = client(`${host.protocol}//${host.host}:${port}`, { path: `${host.path}child/hg/`, }); } else { ioClient = client( self.common.isRelease ? 'http://localhost:8011' : 'http://localhost:8013', {path: '/socket.io/hg/', reconnectionDelay: 0}); } clientSocketConnection(ioClient); } /** * Update the reference to HungryGames. * * @private * @returns {HG} Reference to the currently loaded HungryGames subModule. */ function hg() { const prev = hg_; hg_ = self.bot.getSubmodule('./hungryGames.js'); if (!hg_) return; if (prev !== hg_) { unlinkHG(); hg_.on('dayStateChange', dayStateChange); hg_.on('toggleOption', handleOptionChange); hg_.on('create', broadcastGame); hg_.on('refresh', broadcastGame); hg_.on('memberAdd', handleMemberAdd); hg_.on('memberRemove', handleMemberRemove); hg_.on('actionInsert', handleActionUpdate); hg_.on('actionRemove', handleActionUpdate); hg_.on('actionUpdate', handleActionUpdate); hg_.on('eventToggled', handleEventToggled); hg_.on('eventAdded', handleEventAdded); hg_.on('eventRemoved', handleEventRemoved); hg_.on('gameStarted', broadcastGame); hg_.on('reset', broadcastGame); hg_.on('shutdown', unlinkHG); hg_.client.on('guildMemberAdd', handleGuildMemberAdd); hg_.client.on('guildMemberRemove', handleGuildMemberRemove); } return hg_; } /** * Unregister all event handlers from `hg_`. * * @private */ function unlinkHG() { if (!hg_) return; hg_.removeListener('dayStateChange', dayStateChange); hg_.removeListener('toggleOption', handleOptionChange); hg_.removeListener('create', broadcastGame); hg_.removeListener('refresh', broadcastGame); hg_.removeListener('memberAdd', handleMemberAdd); hg_.removeListener('memberRemove', handleMemberRemove); hg_.removeListener('actionInsert', handleActionUpdate); hg_.removeListener('actionRemove', handleActionUpdate); hg_.removeListener('actionUpdate', handleActionUpdate); hg_.removeListener('eventToggled', handleEventToggled); hg_.removeListener('eventAdded', handleEventAdded); hg_.removeListener('eventRemoved', handleEventRemoved); hg_.removeListener('gameStarted', broadcastGame); hg_.removeListener('reset', broadcastGame); hg_.removeListener('shutdown', unlinkHG); hg_.client.removeListener('guildMemberAdd', handleGuildMemberAdd); hg_.client.removeListener('guildMemberRemove', handleGuildMemberRemove); } /** @inheritdoc */ this.initialize = function() { if (self.common.isSlave) { startClient(); } else { io = socketIo(app, {path: '/socket.io/'}); app.listen(self.common.isRelease ? 8011 : 8013, '127.0.0.1'); io.on('connection', socketConnection); } }; /** * Causes a full shutdown of all servers. * * @public */ this.shutdown = function() { if (io) io.close(); if (ioClient) { ioClient.close(); ioClient = null; } if (app) app.close(); unlinkHG(); }; /** * Handler for all http requests. Should never be called. * * @private * @param {http.IncomingMessage} req The client's request. * @param {http.ServerResponse} res Our response to the client. */ function handler(req, res) { res.writeHead(418); res.end('TEAPOT'); } /** * Map of all currently connected sockets. * * @private * @type {object.<Socket>} */ const sockets = {}; /** * Returns the number of connected clients that are not siblings. * * @public * @returns {number} Number of sockets. */ this.getNumClients = function() { return Object.keys(sockets).length - Object.keys(siblingSockets).length; }; /** * Map of all sockets connected that are siblings. * * @private * @type {object.<Socket>} */ const siblingSockets = {}; /** * Handler for a new socket connecting. * * @private * @param {socketIo~Socket} socket The socket.io socket that connected. */ function socketConnection(socket) { // x-forwarded-for is trusted because the last process this jumps through is // our local proxy. const ipName = self.common.getIPName( socket.handshake.headers['x-forwarded-for'] || socket.handshake.address); self.common.log( 'Socket connected HG (' + Object.keys(sockets).length + '): ' + ipName, socket.id); sockets[socket.id] = socket; // @TODO: Replace this authentication with asymmetric key signatures. socket.on('vaderIAmYourSon', (verification, cb) => { if (verification === auth.hgWebSiblingVerification) { siblingSockets[socket.id] = socket; cb(auth.hgWebSiblingVerificationResponse); socket.on('_guildBroadcast', (gId, ...args) => { for (const i in sockets) { if (sockets[i] && sockets[i].cachedGuilds && sockets[i].cachedGuilds.includes(gId)) { sockets[i].emit(...args); } } }); } else { self.common.error('Client failed to authenticate as child.', socket.id); } }); // Unrestricted Access // socket.on('fetchDefaultOptions', () => { socket.emit('defaultOptions', hg().defaultOptions.entries); }); socket.on('fetchDefaultEvents', () => { socket.emit('defaultEvents', hg().getDefaultEvents().serializable); }); socket.on('fetchActionList', () => { const Action = HungryGames.Action; if (!Action) { socket.emit('actionList', null, null); } else { socket.emit('actionList', Action.actionList, Action.triggerMeta); } }); // End Unrestricted Access \\ // Restricted Access // socket.on('fetchGuilds', (...args) => handle(fetchGuilds, args, false)); socket.on('fetchGuild', (...args) => handle(self.fetchGuild, args)); socket.on('fetchMember', (...args) => handle(self.fetchMember, args)); socket.on('fetchRoles', (...args) => handle(self.fetchRoles, args)); socket.on('fetchChannel', (...args) => handle(self.fetchChannel, args)); socket.on('fetchGames', (...args) => handle(self.fetchGames, args)); socket.on('fetchDay', (...args) => handle(self.fetchDay, args)); socket.on('fetchNextDay', (...args) => handle(self.fetchNextDay, args)); socket.on('fetchActions', (...args) => handle(self.fetchActions, args)); socket.on('insertAction', (...args) => handle(self.insertAction, args)); socket.on('removeAction', (...args) => handle(self.removeAction, args)); socket.on('updateAction', (...args) => handle(self.updateAction, args)); socket.on('excludeMember', (...args) => handle(self.excludeMember, args)); socket.on('includeMember', (...args) => handle(self.includeMember, args)); socket.on('toggleOption', (...args) => handle(self.toggleOption, args)); socket.on('createGame', (...args) => handle(self.createGame, args)); socket.on('resetGame', (...args) => handle(self.resetGame, args)); socket.on('startGame', (...args) => handle(self.startGame, args)); socket.on('startAutoplay', (...args) => handle(self.startAutoplay, args)); socket.on('nextDay', (...args) => handle(self.nextDay, args)); socket.on('gameStep', (...args) => handle(self.gameStep, args)); socket.on('endGame', (...args) => handle(self.endGame, args)); socket.on('pauseAutoplay', (...args) => handle(self.pauseAutoplay, args)); socket.on('pauseGame', (...args) => handle(self.pauseGame, args)); socket.on('editTeam', (...args) => handle(self.editTeam, args)); socket.on( 'createEvent', (...args) => handle(self.createEvent, args, false)); socket.on('addEvent', (...args) => handle(self.addEvent, args)); socket.on('removeEvent', (...args) => handle(self.removeEvent, args)); socket.on( 'deleteEvent', (...args) => handle(self.deleteEvent, args, false)); socket.on('toggleEvent', (...args) => handle(self.toggleEvent, args)); socket.on( 'replaceEvent', (...args) => handle(self.replaceEvent, args, false)); socket.on('fetchEvent', (...args) => handle(self.fetchEvent, args, false)); socket.on( 'fetchUserEvents', (...args) => handle(self.fetchUserEvents, args, false)); socket.on( 'claimLegacyEvents', (...args) => handle(self.claimLegacyEvents, args)); socket.on( 'forcePlayerState', (...args) => handle(self.forcePlayerState, args)); socket.on('renameGame', (...args) => handle(self.renameGame, args)); socket.on('renameNPC', (...args) => handle(self.renameNPC, args)); socket.on('removeNPC', (...args) => handle(self.removeNPC, args)); socket.on( 'fetchStatGroupList', (...args) => handle(self.fetchStatGroupList, args)); socket.on( 'fetchStatGroupMetadata', (...args) => handle(self.fetchStatGroupMetadata, args)); socket.on('fetchStats', (...args) => handle(self.fetchStats, args)); socket.on( 'fetchLeaderboard', (...args) => handle(self.fetchLeaderboard, args)); socket.on( 'modifyInventory', (...args) => handle(self.modifyInventory, args)); socket.on( 'selectStatGroup', (...args) => handle(self.selectStatGroup, args)); socket.on( 'createStatGroup', (...args) => handle(self.createStatGroup, args)); socket.on( 'deleteStatGroup', (...args) => handle(self.deleteStatGroup, args)); socket.on('imageChunk', (...args) => handle(self.imageChunk, args)); socket.on('imageInfo', (...args) => handle(self.imageInfo, args)); // End Restricted Access \\ /** * Calls the functions with added arguments, and copies the request to all * sibling clients. * * @private * @param {Function} func The function to call. * @param {Array.<*>} args Array of arguments to send to function. * @param {boolean} [forward=true] Forward this request directly to all * siblings. */ function handle(func, args, forward = true) { const noLog = ['fetchMember', 'fetchChannel', 'fetchEvent', 'imageChunk']; if (!noLog.includes(func.name.toString())) { const logArgs = args.map((el) => { if (typeof el === 'function') { return (el.name || 'cb') + '()'; } else { return el; } }); self.common.logDebug(`${func.name}(${logArgs.join(',')})`, socket.id); } let cb; if (typeof args[args.length - 1] === 'function') { const origCB = args[args.length - 1]; let fired = false; cb = function(...args) { if (fired) { self.warn( 'Attempting to fire callback a second time! (' + func.name + ')'); } origCB(...args); fired = true; }; args[args.length - 1] = cb; } try { func.apply(func, [args[0], socket].concat(args.slice(1))); } catch (err) { console.error(err); if (typeof cb === 'function') { cb('INTERNAL_SERVER_ERROR'); } return; } if (typeof cb === 'function') { args[args.length - 1] = {_function: true}; } if (forward) { Object.values(siblingSockets).forEach((s) => { s.emit( 'forwardedRequest', args[0], socket.id, func.name, args.slice(1), (res) => { if (res._forward) socket.emit(...res.data); if (res._callback && typeof cb === 'function') { cb(...res.data); } }); }); } } socket.on('disconnect', (reason) => { self.common.log( 'Socket disconnected HG (' + (Object.keys(sockets).length - 1) + ')(' + reason + '): ' + ipName, socket.id); if (siblingSockets[socket.id]) delete siblingSockets[socket.id]; delete sockets[socket.id]; }); } /** * Handler for connecting as a client to the server. * * @private * @param {socketIo~Socket} socket The socket.io socket that connected. */ function clientSocketConnection(socket) { let authenticated = false; socket.on('connect', () => { socket.emit('vaderIAmYourSon', auth.hgWebSiblingVerification, (res) => { self.common.log('Sibling authenticated successfully.', socket.id); authenticated = res === auth.hgWebSiblingVerificationResponse; }); }); socket.on('fetchGuilds', (userData, id, cb) => { fetchGuilds(userData, {id: id}, cb); }); socket.on('forwardedRequest', (userData, sId, func, args, cb) => { if (!authenticated) return; const fakeSocket = { fake: true, emit: function(...args) { if (typeof cb == 'function') cb({_forward: true, data: args}); }, id: sId, }; if (args[args.length - 1] && args[args.length - 1]._function) { args[args.length - 1] = function(...a) { if (typeof cb === 'function') cb({_callback: true, data: a}); }; } if (!self[func]) { self.common.error(func + ': is not a function.', socket.id); } else { self[func].apply(self[func], [userData, fakeSocket].concat(args)); } }); const error = function(...args) { console.error('HG WebSocket Error:', ...args); }; socket.on('connect_error', error); socket.on('connect_timeout', error); socket.on('reconnect_error', error); socket.on('reconnect_failed', error); socket.on('error', error); } /** * This gets fired whenever the day state of any game changes in the hungry * games. This then notifies all clients that the state changed, if they care * about the guild. * * @private * @param {HungryGames} hg HG object firing the event. * @param {string} gId Guild id of the state change. * @listens HG#dayStateChange */ function dayStateChange(hg, gId) { const game = hg.getHG().getGame(gId); let eventState = null; if (!game) return; if (game.currentGame.day.events[game.currentGame.day.state - 2] && game.currentGame.day.events[game.currentGame.day.state - 2].battle) { eventState = game.currentGame.day.events[game.currentGame.day.state - 2].state; } guildBroadcast( gId, 'dayState', game.currentGame.day.num, game.currentGame.day.state, eventState); } /** * Broadcast a message to all relevant clients. * * @private * @param {string} gId Guild ID to broadcast message for. * @param {string} event The name of the event to broadcast. * @param {*} args Data to send in broadcast. */ function guildBroadcast(gId, event, ...args) { const keys = Object.keys(sockets); for (const i in keys) { if (!sockets[keys[i]].cachedGuilds) continue; if (sockets[keys[i]].cachedGuilds.find((g) => g === gId)) { sockets[keys[i]].emit(event, gId, ...args); } } if (ioClient) { ioClient.emit('_guildBroadcast', gId, event, gId, ...args); } } /** * Handles an option being changed and broadcasting the update to clients. * * @private * @listens HG#toggleOption * @param {HungryGames} hg HG object firing the event. * @param {string} gId Guild ID of the option change. * @param {string} opt1 Option key. * @param {string} opt2 Option second key or value. * @param {string} [opt3] Option value if object option. */ function handleOptionChange(hg, gId, opt1, opt2, opt3) { if (opt1 === 'teamSize') { broadcastGame(hg, gId); } else { guildBroadcast(gId, 'option', opt1, opt2, opt3); } } /** * Handle a member being added to a guild. * * @private * @listens Discord~Client#guildMemberAdd * @param {Discord~GuildMember} member The member added. */ function handleGuildMemberAdd(member) { handleMemberAdd(hg(), member.guild.id, member.id); } /** * Handle a member being removed from a guild. * * @private * @listens Discord~Client#guildMemberRemove * @param {Discord~GuildMember} member The member removed. */ function handleGuildMemberRemove(member) { handleMemberRemove(hg(), member.guild.id, member.id); } /** * Handle a member being added to a guild. * * @private * @listens HG#memberAdd * @param {HungryGames} hg HG object firing the event. * @param {string} gId Guild ID of the member added. * @param {string} mId Member ID that was added. */ function handleMemberAdd(hg, gId, mId) { guildBroadcast(gId, 'memberAdd', mId); } /** * Handle a member being removed from a guild. * * @private * @listens HG#memberRemove * @param {HungryGames} hg HG object firing the event. * @param {string} gId Guild ID of the member removed. * @param {string} mId Member ID that was removed. */ function handleMemberRemove(hg, gId, mId) { guildBroadcast(gId, 'memberRemove', mId); } /** * Handle actions being modified in a server. * * @private * @listens HG#actionInsert * @listens HG#actionRemove * @listens HG#actionUpdate * @param {HungryGames} hg HG object firing event. * @param {string} gId The guild ID that was updated. */ function handleActionUpdate(hg, gId) { const game = hg.getHG().getGame(gId); guildBroadcast( gId, 'actions', game && game.actions && game.actions.serializable); } /** * Handle events being toggled in a server. * * @private * @listens HG#eventToggled * @param {HungryGames} hg HG object firing event. * @param {string} gId The guild ID that was updated. * @param {string} type The category the event was toggled in. * @param {string} eId The ID of the event that was toggled. * @param {boolean} value The if event is now enabled. */ function handleEventToggled(hg, gId, type, eId, value) { guildBroadcast(gId, 'eventToggled', type, eId, value); } /** * Handle events being added to a server. * * @private * @listens HG#eventAdded * @param {HungryGames} hg HG object firing event. * @param {string} gId The guild ID that was updated. * @param {string} type The event category. * @param {string} eId The ID of the event that was added. */ function handleEventAdded(hg, gId, type, eId) { guildBroadcast(gId, 'eventAdded', type, eId); } /** * Handle events being removed from a server. * * @private * @listens HG#eventRemoved * @param {HungryGames} hg HG object firing event. * @param {string} gId The guild ID that was updated. * @param {string} type The event category. * @param {string} eId The ID of the event that was removed. */ function handleEventRemoved(hg, gId, type, eId) { guildBroadcast(gId, 'eventRemoved', type, eId); } /** * Handles broadcasting the game data to all relevant clients. * * @private * @listens HG#create * @listens HG#refresh * @param {HungryGames} hg HG object firing event. * @param {string} gId The guild ID to data for. */ function broadcastGame(hg, gId) { const game = hg.getHG().getGame(gId); guildBroadcast(gId, 'game', game && game.serializable); } /** * Send a message to the given socket informing the client that the command * they attempted failed due to insufficient permission. * * @private * @param {Socket} socket The socket.io socket to reply on. * @param {string} cmd The command the client attempted. * @param {string} [gId] The guild ID for locale info. */ function replyNoPerm(socket, cmd, gId) { const output = hg().getString('genericNoPerm', gId, cmd); self.common.logDebug(`Attempted ${cmd} without permission.`, socket.id); socket.emit('message', output); } /** * Checks if the current shard is responsible for the requested guild. * * @private * @param {number|string} gId The guild id to check. * @returns {boolean} True if this shard has this guild. */ function checkMyGuild(gId) { const g = self.client && self.client.guilds.resolve(gId); return (g && true) || false; } /** * Check that the given user has permission to manage the games in the given * guild. * * @private * @param {UserData} userData The user to check. * @param {string} gId The guild id to check against. * @param {?string} cId The channel id to check against. * @param {string} cmd The command being attempted. * @returns {boolean} Whther the user has permission or not to manage the * hungry games in the given guild. */ function checkPerm(userData, gId, cId, cmd) { if (!userData) return false; const msg = makeMessage(userData.id, gId, cId, 'hg ' + cmd); if (!msg || !msg.author) return false; if (userData.id == self.common.spikeyId) return true; return !self.command.validate(null, msg); } /** * Check that the given user has permission to see and send messages in the * given channel, as well as manage the games in the given guild. * * @private * @param {UserData} userData The user to check. * @param {string} gId The guild id of the guild that contains the channel. * @param {string} cId The channel id to check against. * @param {string} cmd The command being attempted to check permisisons for. * @returns {boolean} Whther the user has permission or not to manage the * hungry games in the given guild and has permission to send messages in the * given channel. */ function checkChannelPerm(userData, gId, cId, cmd) { if (!checkPerm(userData, gId, cId, cmd)) return false; if (userData.id == self.common.spikeyId) return true; const g = self.client.guilds.resolve(gId); const channel = g.channels.resolve(cId); if (!channel) return false; const m = g.members.resolve(userData.id); const perms = channel.permissionsFor(m); if (!perms.has(self.Discord.PermissionsBitField.Flags.ViewChannel)) { return false; } if (!perms.has(self.Discord.PermissionsBitField.Flags.SendMessages)) { return false; } return true; } /** * Forms a Discord~Message similar object from given IDs. * * @private * @param {string} uId The id of the user who wrote this message. * @param {string} gId The id of the guild this message is in. * @param {?string} cId The id of the channel this message was 'sent' in. * @param {?string} msg The message content. * @returns {?MessageMaker} The created message-like object, or null if * invalid channel. */ function makeMessage(uId, gId, cId, msg) { const message = new MessageMaker(self, uId, gId, cId, msg); return message.guild ? message : null; } /** * Strips a Discord~GuildMember to only the necessary data that a client will * need. * * @private * @param {Discord~GuildMember} m The guild member to strip the data from. * @returns {object} The minimal member. */ function makeMember(m) { const m_ = m; if (typeof m !== 'object') { m = { roles: { cache: [], }, guild: {}, permissions: {bitfield: 0}, user: self.client.users.resolve(m), }; } if (!m.user) { self.error(`Failed to make member: ${m_}`); return null; } return { nickname: m.nickname, roles: m.roles.cache.map && m.roles.cache.map((el) => el.id), color: m.displayColor, guild: {id: m.guild.id}, permissions: m.permissions.bitfield, premiumSinceTimestamp: m.premiumSinceTimestamp, user: { username: m.user.username, avatarURL: m.user.displayAvatarURL(), id: m.user.id, bot: m.user.bot, // m.user.descriminator seems to be broken and always returns // `undefined`. descriminator: m.user.tag.match(/#(\d{4})$/)[1], }, joinedTimestamp: m.joinedTimestamp, }; } /** * Cancel and clean up a current image upload. * * @private * @param {string} iId Image upload ID to purge and abort. */ function cancelImageUpload(iId) { if (!imageBuffer[iId]) return; clearTimeout(imageBuffer[iId].timeout); delete imageBuffer[iId]; } /** * Create an upload ID and buffer for a client to send to. Automatically * cancelled after 60 seconds. * * @private * @param {string} uId The user ID that started this upload. * @returns {object} The metadata storing object. */ function beginImageUpload(uId) { let id; do { id = `${crypto.randomBytes(8).toString('hex').toUpperCase()}`; } while (imageBuffer[id]); imageBuffer[id] = {receivedBytes: 0, buffer: [], startTime: Date.now(), id: id, uId: uId}; imageBuffer[id].timeout = setTimeout(function() { cancelImageUpload(id); }, 60000); return imageBuffer[id]; } /** * @description Function calls handlers for requested commands. * @typedef HGWeb~SocketFunction * @type {Function} * * @param {WebUserData} userData The user data of the user performing the * request. * @param {socketIo~Socket} socket The socket connection firing the command. * Not necessarily the socket that will reply to the end client. * @param {...*} args Additional function-specific arguments. * @param {HGWeb~basicCB} [cb] Callback that fires once requested action is * complete or has failed. Client may not pass a callback. */ /** * Basic callback with single argument. The argument is null if there is no * error, or a string if there was an error. * * @callback HGWeb~basicCB * * @param {?string} err The error response. * @param {*} res Response data if no error. */ /** * Fetch all relevant data for all mutual guilds with the user and send it to * the user. * * @private * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {Function} [cb] Callback that fires once the requested action is * complete, or has failed. */ function fetchGuilds(userData, socket, cb) { if (!userData) { self.common.error('Fetch Guilds without userData', socket.id); if (typeof cb === 'function') cb('SIGNED_OUT'); return; } else if (userData.apiRequest) { // Disabled for API requests due to the possible issue with performance // fetching list of guilds. return; } const numReplies = (Object.entries(siblingSockets).length || 0); let replied = 0; const guildBuffer = {}; let done; if (typeof cb === 'function') { done = cb; } else { /** * The callback for each response with the requested data. Replies to the * user once all requests have replied. * * @private * @param {string|object} guilds Either the guild data to send to the * user, or 'guilds' if this is a reply from a sibling client. * @param {?string} [err] The error that occurred, or null if no error. * @param {object} [response] The guild data if `guilds` equals 'guilds'. */ done = function(guilds, err, response) { if (guilds === 'guilds') { if (err) { guilds = null; } else { guilds = response; } } for (let i = 0; guilds && i < guilds.length; i++) { guildBuffer[guilds[i].id] = guilds[i]; } replied++; if (replied > numReplies) { if (typeof cb === 'function') cb(guildBuffer); socket.emit('guilds', null, guildBuffer); socket.cachedGuilds = Object.keys(guildBuffer || {}); } }; } Object.entries(siblingSockets).forEach((obj) => { obj[1].emit('fetchGuilds', userData, socket.id, done); }); if (self.common.isMaster) { done([]); return; } try { let guilds = []; if (userData.guilds && userData.guilds.length > 0) { userData.guilds.forEach((el) => { const g = self.client && self.client.guilds.resolve(el.id); if (!g) return; guilds.push(g); }); } else { guilds = self.client && [...self.client.guilds.cache .filter((obj) => obj.members.resolve(userData.id)) .values()]; } const strippedGuilds = stripGuilds(guilds, userData); done(strippedGuilds); } catch (err) { self.common.error( 'Error while fetching guilds (Cached: ' + (userData.guilds && true || false) + ')', socket.id); console.error(err); done(); } } /** * Strip a Discord~Guild to the basic information the client will need. * * @private * @param {Discord~Guild[]} guilds The array of guilds to strip. * @param {object} userData The current user's session data. * @returns {Array<object>} The stripped guilds. */ function stripGuilds(guilds, userData) { return guilds.map((g) => { let dOpts = self.command.getDefaultSettings() || {}; dOpts = Object.entries(dOpts) .filter((el) => el[1].getFullName().startsWith('hg')) .reduce((p, c) => { p[c[0]] = c[1]; return p; }, {}); let uOpts = self.command.getUserSettings(g.id) || {}; uOpts = Object.entries(uOpts) .filter((el) => el[0].startsWith('hg')) .reduce( (p, c) => { p[c[0]] = c[1]; return p; }, {}); const member = g.members.resolve(userData.id); const newG = {}; newG.iconURL = g.iconURL(); newG.name = g.name; newG.id = g.id; newG.bot = self.client.user.id; newG.ownerId = g.ownerId; newG.members = g.members.cache.map((m) => m.id); newG.defaultSettings = dOpts; newG.userSettings = uOpts; newG.channels = g.channels.cache .filter( (c) => member && (userData.id == self.common.spikeyId || c.permissionsFor(member).has( self.Discord.PermissionsBitField.Flags.ViewChannel))) .map((c) => { return { id: c.id, permissions: userData.id == self.common.spikeyId ? self.Discord.PermissionsBitField.ALL : c.permissionsFor(member).bitfield, }; }); newG.myself = makeMember(member || userData.id); return newG; }); } /** * Fetch a single guild. * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {string|number} gId The ID of the guild that was requested. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.fetchGuild = function fetchGuild(userData, socket, gId, cb) { if (!userData) { self.common.error('Fetch Guild without userData', socket.id); if (typeof cb === 'function') cb('SIGNED_OUT'); return; } if (typeof cb !== 'function') { self.common.logWarning( 'Fetch Guild attempted without callback', socket.id); return; } const guild = self.client && self.client.guilds.resolve(gId); if (!guild) return; if (userData.id != self.common.spikeyId && !guild.members.resolve(userData.id)) { cb(null); return; } cb(null, stripGuilds([guild], userData)[0]); }; /** * Fetch data about a member of a guild. * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {number|string} mId The member's id to lookup. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.fetchMember = function fetchMember(userData, socket, gId, mId, cb) { if (!checkPerm(userData, gId, null, 'players')) return; const g = self.client && self.client.guilds.resolve(gId); if (!g) return; const m = g.members.resolve(mId); if (!m) return; const finalMember = makeMember(m); if (typeof cb === 'function') { cb(null, finalMember); } else { socket.emit('member', gId, mId, finalMember); } }; /** * Fetch data about a role in a guild. * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.fetchRoles = function fetchRoles(userData, socket, gId, cb) { if (!checkPerm(userData, gId, null)) return; const g = self.client && self.client.guilds.resolve(gId); if (!g) return; const roles = [...g.roles.cache.values()]; if (typeof cb === 'function') { cb(null, roles); } else { socket.emit('member', gId, roles); } }; /** * Fetch data about a channel of a guild. * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {number|string} cId The channel's id to lookup. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.fetchChannel = function fetchChannel(userData, socket, gId, cId, cb) { if (!checkChannelPerm(userData, gId, cId, '')) return; const g = self.client && self.client.guilds.resolve(gId); if (!g) return; const m = g.members.resolve(userData.id); const channel = g.channels.resolve(cId); const perms = channel.permissionsFor(m) || {bitfield: 0}; const stripped = {}; stripped.id = channel.id; stripped.permissions = perms.bitfield; stripped.name = channel.name; stripped.position = channel.position; if (channel.parent) stripped.parent = {position: channel.parent.position}; stripped.type = channel.type; if (typeof cb === 'function') { cb(null, stripped); } else { socket.emit('channel', gId, cId, stripped); } }; /** * Fetch all game data within a guild. * * @see {@link HungryGames.getGame} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {basicCB} [cb] Callback that fires once the requested action is * complete, or has failed. */ this.fetchGames = function fetchGames(userData, socket, gId, cb) { if (!checkPerm(userData, gId, null, 'options') || !checkPerm(userData, gId, null, 'events') || !checkPerm(userData, gId, null, 'players')) { if (!checkMyGuild(gId)) return; replyNoPerm(socket, 'fetchGames', gId); return; } const game = hg().getHG().getGame(gId); if (typeof cb === 'function') { cb(null, game && game.serializable); } else { socket.emit('game', gId, game && game.serializable); } }; /** * Fetch the updated game's day information. * * @see {@link HungryGames.getGame} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {basicCB} [cb] Callback that fires once the requested action is * complete, or has failed. */ this.fetchDay = function fetchDay(userData, socket, gId, cb) { let g; let m; if (!userData) { return; } else { g = self.client && self.client.guilds.resolve(gId); if (!g) { // Request is probably fulfilled by another sibling. return; } else { m = g.members.resolve(userData.id); if (!m) { self.common.log( 'Attempted fetchDay, but unable to find member in guild' + gId + '@' + userData.id, socket.id); return; } } } const game = hg().getHG().getGame(gId); if (!game || !game.currentGame || !game.currentGame.day) { if (typeof cb === 'function') { cb('NO_GAME_IN_GUILD'); } else { socket.emit( 'message', 'There doesn\'t appear to be a game on this server yet.'); } return; } if (!g.channels.resolve(game.outputChannel) .permissionsFor(m) .has(self.Discord.PermissionsBitField.Flags.ViewChannel)) { replyNoPerm(socket, 'fetchDay', gId); return; } if (typeof cb === 'function') { cb(null, game.currentGame.day, game.currentGame.includedUsers); } else { socket.emit( 'day', gId, game.currentGame.day, game.currentGame.includedUsers); } }; /** * Fetch the game's next day information. * * @see {@link HungryGames.getGame} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.fetchNextDay = function fetchNextDay(userData, socket, gId, cb) { if (!checkPerm(userData, gId, null, 'kill')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'fetchNextDay', gId); return; } const g = self.client && self.client.guilds.resolve(gId); const m = g.members.resolve(userData.id); if (!m) { if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'fetchNextDay', gId); return; } const game = hg().getHG().getGame(gId); if (!game || !game.currentGame || !game.currentGame.nextDay) { if (typeof cb === 'function') { cb('NO_GAME_IN_GUILD'); } else { socket.emit( 'message', 'There doesn\'t appear to be a game on this server yet.'); } return; } if (!g.channels.resolve(game.outputChannel) .permissionsFor(m) .has(self.Discord.PermissionsBitField.Flags.ViewChannel)) { replyNoPerm(socket, 'fetchNextDay', gId); return; } if (typeof cb === 'function') { cb(null, game.currentGame.nextDay); } else { socket.emit('nextDay', gId, game.currentGame.nextDay); } }; /** * Fetch the game's current action/trigger information. * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.fetchActions = function fetchActions(userData, socket, gId, cb) { if (!checkPerm(userData, gId, null, 'options')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'fetchActions', gId); return; } const game = hg().getHG().getGame(gId); if (!game || !game.actions) { if (typeof cb === 'function') cb('NO_GAME_IN_GUILD'); return; } if (typeof cb === 'function') { cb(null, game.actions.serializable); } else { socket.emit('actions', gId, game.actions.serializable); } }; /** * Add an action to a trigger in a guild. * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {string} trigger The name of the trigger. * @param {string} action The name of the action. * @param {object} args Optional arguments to pass for the action creation. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.insertAction = function insertAction( userData, socket, gId, trigger, action, args, cb) { if (!checkPerm(userData, gId, null, 'options')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'insertAction', gId); return; } hg().getHG().insertAction(gId, trigger, action, args, cb); }; /** * Remove an action from a trigger in a guild. * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {string} trigger The name of the trigger. * @param {string} id The id of the action to remove. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.removeAction = function removeAction( userData, socket, gId, trigger, id, cb) { if (!checkPerm(userData, gId, null, 'options')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'removeAction', gId); return; } hg().getHG().removeAction(gId, trigger, id, cb); }; /** * Update an action for a trigger in a guild. * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {string} trigger The name of the trigger. * @param {string} id The id of the action to remove. * @param {string} key The key of the value to change. * @param {number|string} value The value to set. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.updateAction = function updateAction( userData, socket, gId, trigger, id, key, value, cb) { if (!checkPerm(userData, gId, null, 'options')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'updateAction', gId); return; } hg().getHG().updateAction(gId, trigger, id, key, value, cb); }; /** * Exclude a member from the Games. * * @see {@link HungryGames.excludeUsers} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {number|string} mId The member id to exclude. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.excludeMember = function excludeMember(userData, socket, gId, mId, cb) { if (!checkPerm(userData, gId, null, 'exclude')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'excludeMember', gId); return; } if (mId === 'everyone' || mId === 'online' || mId == 'offline' || mId == 'dnd' || mId == 'idle') { hg().excludeUsers(mId, gId, (res) => { if (typeof cb === 'function') cb(res); }); } else { hg().excludeUsers([mId], gId, (res) => { if (typeof cb === 'function') cb(res); }); } }; /** * Include a member in the Games. * * @see {@link HungryGames.includeUsers} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {number|string} mId The member id to include. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.includeMember = function includeMember(userData, socket, gId, mId, cb) { if (!checkPerm(userData, gId, null, 'include')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'includeMember', gId); return; } if (mId === 'everyone' || mId === 'online' || mId == 'offline' || mId == 'dnd' || mId == 'idle') { hg().includeUsers(mId, gId, (res) => { if (typeof cb === 'function') cb(res); }); } else { hg().includeUsers([mId], gId, (res) => { if (typeof cb === 'function') cb(res); }); } }; /** * Toggle an option in the Games. * * @see {@link HungryGames.setOption} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {string} option The option to change. * @param {string|number} value The value to set option to. * @param {string} extra The extra text if the option is in an object. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.toggleOption = function toggleOption( userData, socket, gId, option, value, extra, cb) { if (!checkPerm(userData, gId, null, 'option')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'toggleOption', gId); return; } hg().getHG().fetchGame(gId, (game) => { const response = hg().setOption(gId, option, value, extra || undefined); if (typeof cb === 'function') { cb(null, response); } else if (!game) { socket.emit('message', response); } }); }; /** * Create a Game. * * @see {@link HungryGames.createGame} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.createGame = function createGame(userData, socket, gId, cb) { if (!checkPerm(userData, gId, null, 'create')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'createGame', gId); return; } hg().createGame(gId, (game) => { if (typeof cb === 'function') cb(game ? null : 'ATTEMPT_FAILED'); }); }; /** * Reset game data. * * @see {@link HungryGames.resetGame} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {string} cmd Command specifying what data to delete. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.resetGame = function resetGame(userData, socket, gId, cmd, cb) { if (!checkPerm(userData, gId, null, 'reset')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'resetGame', gId); return; } hg().getHG().fetchGame(gId, () => { const response = hg().getHG().resetGame(gId, cmd); if (typeof cb === 'function') cb(null, response); }); }; /** * Start the game. * * @see {@link HungryGames.startGame} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {number|string} cId Channel to start the game in. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.startGame = function startGame(userData, socket, gId, cId, cb) { if (!checkChannelPerm(userData, gId, cId, 'start')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'startGame', gId); return; } hg().startGame(userData.id, gId, cId); if (typeof cb === 'function') cb(null); }; /** * Enable autoplay. * * @see {@link HungryGames.startAutoplay} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {number|string} cId Channel to send the messages in. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.startAutoplay = function startAutoplay(userData, socket, gId, cId, cb) { if (!checkChannelPerm(userData, gId, cId, 'autoplay')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'startAutoplay', gId); return; } hg().startAutoplay(userData.id, gId, cId); if (typeof cb === 'function') cb(null); }; /** * Start the next day. * * @see {@link HungryGames.nextDay} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {number|string} cId Channel to send the messages in. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.nextDay = function nextDay(userData, socket, gId, cId, cb) { if (!checkChannelPerm(userData, gId, cId, 'next')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'nextDay', gId); return; } hg().nextDay(userData.id, gId, cId); if (typeof cb === 'function') cb(null); }; /** * Step the game once. * * @see {@link HungryGames.gameStep} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {number|string} cId Channel to send the messages in. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.gameStep = function gameStep(userData, socket, gId, cId, cb) { if (!checkChannelPerm(userData, gId, cId, 'step')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'gameStep', gId); return; } hg().gameStep(userData.id, gId, cId); if (typeof cb === 'function') cb(null); }; /** * End the game. * * @see {@link HungryGames.endGame} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.endGame = function endGame(userData, socket, gId, cb) { if (!checkPerm(userData, gId, null, 'end')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'endGame', gId); return; } hg().endGame(userData.id, gId); const game = hg().getHG().getGame(gId); if (typeof cb === 'function') { cb(null, game && game.serializable); } else { socket.emit('game', gId, game && game.serializable); } }; /** * Disable autoplay. * * @see {@link HungryGames.pauseAutoplay} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.pauseAutoplay = function pauseAutoplay(userData, socket, gId, cb) { if (!checkPerm(userData, gId, null, 'autoplay')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'pauseAutoplay', gId); return; } hg().pauseAutoplay(userData.id, gId); const game = hg().getHG().getGame(gId); if (typeof cb === 'function') { cb(null, game && game.serializable); } else { socket.emit('game', gId, game && game.serializable); } }; /** * Pause game. * * @see {@link HungryGames.pauseGame} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.pauseGame = function pauseGame(userData, socket, gId, cb) { if (!checkPerm(userData, gId, null, 'pause')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'pauseGame', gId); return; } const error = hg().pauseGame(gId); const game = hg().getHG().getGame(gId); if (typeof cb === 'function') { if (error !== 'Success') { cb(error); } else { cb(null, game && game.serializable); } } else { if (error !== 'Success') { socket.emit('message', error); } else { socket.emit('game', gId, game && game.serializable); } } }; /** * Edit the teams. * * @see {@link HungryGames.editTeam} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {string} cmd The command to run. * @param {string} one The first argument. * @param {string} two The second argument. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.editTeam = function editTeam(userData, socket, gId, cmd, one, two, cb) { if (!checkPerm(userData, gId, null, 'team')) { if (!checkMyGuild(gId)) return; replyNoPerm(socket, 'editTeam', gId); return; } const message = hg().editTeam(userData.id, gId, cmd, one, two); if (typeof cb === 'function') { cb(null, message); } else { if (message) socket.emit('message', message); } }; /** * Create a game event. * * @see {@link HungryGames~createEvent} * @see {@link HungryGames~EventContainer~fetch} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {HungryGames~Event} evt The event data of the event to create. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.createEvent = function createEvent(userData, socket, evt, cb) { if (!userData) return; if (typeof cb !== 'function') cb = function() {}; evt.creator = userData.id; evt.id = null; if (evt.type === 'normal') { const err = HungryGames.NormalEvent.validate(evt); if (err) { cb(err); return; } evt = HungryGames.NormalEvent.from(evt); } else if (evt.type === 'arena') { if (Array.isArray(evt.outcomes)) { evt.outcomes.forEach((el, i) => { el.creator = userData.id; evt.outcomes[i] = HungryGames.NormalEvent.from(el); }); } const err = HungryGames.ArenaEvent.validate(evt); if (err) { cb(err); return; } evt = HungryGames.ArenaEvent.from(evt); } else if (evt.type === 'weapon') { if (Array.isArray(evt.outcomes)) { evt.outcomes.forEach((el, i) => { el.creator = userData.id; evt.outcomes[i] = HungryGames.NormalEvent.from(el); }); } evt.message = 'Weapon Message'; const err = HungryGames.WeaponEvent.validate(evt); if (err) { cb(err); return; } evt = HungryGames.WeaponEvent.from(evt); } else { cb('BAD_TYPE'); return; } hg().getHG().createEvent(evt, (err, evtFinal) => { if (err) { if (typeof cb === 'function') { cb('ATTEMPT_FAILED', err); } else { socket.emit('message', 'Failed to create event: ' + err); } } else { const eId = evtFinal.id; self.debug(`Created HG Event: ${eId}`); cb(null, eId); } }); }; /** * @description Add an existing event to a guild's custom events. * @public * @see {@link HungryGames~EventContainer~fetch} * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {string} gId The guild ID of the guild to modify. * @param {string} type The event category to add the event to. * @param {string} eId The event ID to add. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.addEvent = function addEvent(userData, socket, gId, type, eId, cb) { if (!checkPerm(userData, gId, null, 'event')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, this.name, gId); return; } if (typeof cb !== 'function') cb = function() {}; if (!type) { cb('BAD_TYPE'); return; } hg().getHG().fetchGame(gId, (game) => { if (!game) { cb('NO_GAME'); return; } game.customEventStore.fetch(eId, type, (err) => { if (err) { cb('ATTEMPT_FAILED', err); } else { cb(null); if (type) guildBroadcast(gId, 'eventAdded', type, eId); } }); }); }; /** * @description Remove an event from a guild's custom events. * @public * @see {@link HungryGames~EventContainer~remove} * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {string} gId The guild ID of the guild to modify. * @param {string} type The event category to remove the event from. * @param {string} eId The event ID to remove. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.removeEvent = function removeEvent( userData, socket, gId, type, eId, cb) { if (!checkPerm(userData, gId, null, 'event')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, this.name, gId); return; } if (typeof cb !== 'function') cb = function() {}; hg().getHG().fetchGame(gId, (game) => { if (!game) { cb('NO_GAME'); return; } if (game.customEventStore.remove(eId, type)) { cb(null); if (type) guildBroadcast(gId, 'eventRemoved', type, eId); } else { cb('ATTEMPT_FAILED'); } }); }; /** * Delete a game event. * * @see {@link HungryGames.deleteEvent} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {string} eId The event ID to delete. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.deleteEvent = function deleteEvent(userData, socket, eId, cb) { if (!userData) return; hg().getHG().deleteEvent(userData.id, eId, (err) => { if (typeof cb !== 'function') return; if (err) { cb('ATTEMPT_FAILED', err); } else { cb(null); } }); }; /** * @description Enable or disable an event without deleting it. * @see {@link HungryGames.toggleEvent} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo-Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to run this command on. * @param {string} type The type of event that we are toggling. * @param {string} event The ID of the event to toggle. * @param {?boolean} value Set the enabled value instead of toggling. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete. */ this.toggleEvent = function toggleEvent( userData, socket, gId, type, event, value, cb) { if (!checkPerm(userData, gId, null, 'event')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'removeEvent', gId); return; } if (typeof cb !== 'function') cb = function() {}; const err = hg().toggleEvent(gId, type, event, value); if (err) { cb('ATTEMPT_FAILED', err); } else { cb(null); } }; /** * Replace a custom event with new data. * * @see {@link HungryGames~replaceEvent} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {HungryGames~Event} evt The event data to update the event to. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.replaceEvent = function replaceEvent(userData, socket, evt, cb) { if (!userData) return; if (typeof cb !== 'function') cb = function() {}; evt.creator = userData.id; if (evt.type === 'normal') { const err = HungryGames.NormalEvent.validate(evt); if (err) { cb(err); return; } evt = HungryGames.NormalEvent.from(evt); } else if (evt.type === 'arena') { const err = HungryGames.ArenaEvent.validate(evt); if (err) { cb(err); return; } evt = HungryGames.ArenaEvent.from(evt); } else if (evt.type === 'weapon') { const err = HungryGames.WeaponEvent.validate(evt); if (err) { cb(err); return; } evt = HungryGames.WeaponEvent.from(evt); } else { cb('BAD_TYPE'); return; } hg().getHG().replaceEvent(userData.id, evt, (err) => { if (err) { if (typeof cb === 'function') { cb('ATTEMPT_FAILED', err); } } else { cb(null); } }); }; /** * Fetch a single event data. * * @see {@link HungryGames~EventContainer~fetch} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo-Socket} socket The socket connection to reply on. * @param {string} eId The event ID to fetch. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete. */ this.fetchEvent = function fetchEvent(userData, socket, eId, cb) { if (!userData) return; hg().getDefaultEvents().fetch(eId, null, (err, evt) => { if (err) { cb('ATTEMPT_FAILED', err); } else { cb(null, evt); } }); }; /** * Fetch list of IDs of all events the user has created. * * @see {@link HungryGames~fetchUserEvents} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo-Socket} socket The socket connection to reply on. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete. */ this.fetchUserEvents = function fetchUserEvents(userData, socket, cb) { if (!userData) return; hg().getHG().fetchUserEvents(userData.id, cb); }; /** * Claim legacy events to the user. * * @see {@link HungryGames~claimLegacy} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo-Socket} socket The socket connection to reply on. * @param {string} gId The guild id to look at. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete. */ this.claimLegacyEvents = function claimLegacyEvents( userData, socket, gId, cb) { if (!checkPerm(userData, gId, null, 'claimlegacy')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, this.name, gId); return; } hg().getHG().fetchGame(gId, (game) => { if (typeof cb !== 'function') cb = function() {}; if (!game) { cb('NO_GAME'); } else { hg().claimLegacy(game, userData.id, cb); } }); }; /** * Force a player in the game to end a day in a certain state. * * @see {@link HungryGames.forcePlayerState} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo-Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to run this command on. * @param {string[]} list The list of user IDs of the players to effect. * @param {string} state The forced state. * @param {string} [text] The message to show in the games as a result of this * command. * @param {boolean} [persists] Will this state be forced until the game ends. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete. */ this.forcePlayerState = function forcePlayerState( userData, socket, gId, list, state, text, persists, cb) { let cmdToCheck = state; switch (state) { case 'living': case 'thriving': cmdToCheck = 'heal'; break; case 'dead': cmdToCheck = 'kill'; break; case 'wounded': cmdToCheck = 'hurt'; break; } if (!checkPerm(userData, gId, null, cmdToCheck)) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'forcePlayerState', gId); return; } const game = hg().getHG().getGame(gId); if (!game) return; if (typeof text != 'string') { text = hg().getHG()._defaultEventStore.getArray('player'); } const locale = self.bot.getLocale && self.bot.getLocale(gId); HungryGames.GuildGame.forcePlayerState( game, list, state, hg().getHG().messages, text, locale, (res) => { const output = hg().getString(res, gId); if (typeof cb === 'function') { cb(null, output, game.serializable); } else { socket.emit('message', output); } }); }; /** * Rename the guild's game. * * @see {@link HungryGames.renameGame} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo-Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to run this command on. * @param {string} name The name to change the game to. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete. */ this.renameGame = function renameGame(userData, socket, gId, name, cb) { if (!checkPerm(userData, gId, null, 'rename')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'renameGame', gId); return; } hg().renameGame(gId, name); if (typeof cb === 'function') { let name = null; let game = hg().getHG().getGame(gId); if (game) game = game.currentGame; if (game) name = game.name; cb(name); } }; /** * Rename an NPC in a game. * * @see {@link HungryGames.renameNPC} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo-Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to run this command on. * @param {string} npcId The ID of the NPC to rename. * @param {string} username The new username for the NPC. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete. */ this.renameNPC = function renameNPC( userData, socket, gId, npcId, username, cb) { if (!checkPerm(userData, gId, null, 'ai create')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'renameNPC', gId); return; } const error = hg().renameNPC(gId, npcId, username); if (typeof cb === 'function') { cb(typeof error === 'string' ? error : null); } }; /** * Remove an NPC from a game. * * @see {@link HungryGames.removeNPC} * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo-Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to run this command on. * @param {string} npcId The ID of the NPC to remove. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete. */ this.removeNPC = function removeNPC(userData, socket, gId, npcId, cb) { if (!checkPerm(userData, gId, null, 'ai remove')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'removeNPC', gId); return; } const error = hg().removeNPC(gId, npcId); if (typeof cb === 'function') { cb(typeof error === 'string' ? error : null); } }; /** * Respond with list of stat groups for the requested guild. * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.fetchStatGroupList = function fetchStatGroupList( userData, socket, gId, cb) { if (!checkPerm(userData, gId, null, 'groups')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'fetchStatGroupList', gId); return; } const game = hg().getHG().getGame(gId); if (!game) { if (typeof cb === 'function') cb('NO_GAME_IN_GUILD'); return; } game._stats.fetchGroupList((err, list) => { if (err) { if (err.code === 'ENOENT') { list = []; } else { self.error('Failed to get list of stat groups.'); console.error(err); if (typeof cb === 'function') cb('ATTEMPT_FAILED'); return; } } if (typeof cb === 'function') { cb(null, list); } else { socket.emit('statGroupList', gId, list); } }); }; /** * Respond with metadata for the requested stat group. * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} guildId The guild id to look at. * @param {string} groupId The ID of the group. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.fetchStatGroupMetadata = function fetchStatGroupMetadata( userData, socket, guildId, groupId, cb) { if (!checkPerm(userData, guildId, null, 'groups')) { if (!checkMyGuild(guildId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'fetchStatGroupMetadata', guildId); return; } const game = hg().getHG().getGame(guildId); if (!game) { if (typeof cb === 'function') cb('NO_GAME_IN_GUILD'); return; } game._stats.fetchGroup(groupId, (err, group) => { if (err) { if (typeof cb === 'function') cb('BAD_GROUP'); return; } group.fetchMetadata((err, meta) => { if (err) { self.error( 'Failed to fetch metadata for stat group: ' + guildId + '/' + group.id); console.error(err); if (typeof cb === 'function') cb('ATTEMPT_FAILED'); return; } if (typeof cb === 'function') { cb(null, meta); } else { socket.emit('statGroupMetadata', guildId, groupId, meta); } }); }); }; /** * Respond with stats for a specific user in a group. * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} guildId The guild id to look at. * @param {string} groupId The ID of the group. * @param {string} userId The ID of the user. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.fetchStats = function fetchStats( userData, socket, guildId, groupId, userId, cb) { if (!checkPerm(userData, guildId, null, 'stats')) { if (!checkMyGuild(guildId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'fetchStats', guildId); return; } const game = hg().getHG().getGame(guildId); if (!game) { if (typeof cb === 'function') cb('NO_GAME_IN_GUILD'); return; } game._stats.fetchGroup(groupId, (err, group) => { if (err) { if (typeof cb === 'function') cb('BAD_GROUP'); return; } group.fetchUser(userId, (err, data) => { if (err) { self.error( 'Failed to fetch user stats: ' + guildId + '@' + userId + '/' + group.id); console.error(err); if (typeof cb === 'function') cb('ATTEMPT_FAILED'); return; } if (typeof cb === 'function') { cb(null, data.serializable); } else { socket.emit('userStats', guildId, groupId, userId, data.serializable); } }); }); }; /** * Respond with leaderboard information. * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} guildId The guild id to look at. * @param {string} groupId The ID of the group. * @param {HGStatGroupUserSelectOptions} opt Data select options. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.fetchLeaderboard = function fetchLeaderboard( userData, socket, guildId, groupId, opt, cb) { if (!checkPerm(userData, guildId, null, 'stats')) { if (!checkMyGuild(guildId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'fetchLeaderboard', guildId); return; } const game = hg().getHG().getGame(guildId); if (!game) { if (typeof cb === 'function') cb('NO_GAME_IN_GUILD'); return; } game._stats.fetchGroup(groupId, (err, group) => { if (err) { if (typeof cb === 'function') cb('BAD_GROUP'); return; } group.fetchUsers(opt, (err, rows) => { if (err) { self.error( 'Failed to fetch leaderboard: ' + guildId + '/' + group.id); console.error(err); if (typeof cb === 'function') cb('ATTEMPT_FAILED'); return; } const serializable = rows.map((el) => { const out = {id: el.id}; Object.assign(out, el.serializable); return out; }); if (typeof cb === 'function') { cb(null, serializable); } else { socket.emit('userStats', guildId, groupId, opt, serializable); } }); }); }; /** * Give or take weapons from a player. * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {string} uId The ID of the user to give or take weapon from. * @param {string} weapon The ID of the weapon to give or take. * @param {number} count Number of weapons to give or take. Positive is give, * negative is take. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.modifyInventory = function modifyInventory( userData, socket, gId, uId, weapon, count, cb) { if (!checkPerm(userData, gId, null, 'give') || !checkPerm(userData, gId, null, 'take')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'modifyInventory', gId); return; } const game = hg().getHG().getGame(gId); if (!game) { if (typeof cb === 'function') cb('NO_GAME_IN_GUILD'); return; } game.modifyPlayerWeapon( uId, weapon, hg().getHG(), count, false, (err, ...params) => { const res = hg().getString(err, gId, ...params); if (typeof cb === 'function') { cb(null, res, game.serializable); } else { socket.emit('message', res); } }); }; /** * Set the currently selected stat group. * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {string} guildId The guild id to look at. * @param {?string} groupId The ID of the group to select, or null to set * none. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.selectStatGroup = function selectStatGroup( userData, socket, guildId, groupId, cb) { if (!checkPerm(userData, guildId, null, 'group select')) { if (!checkMyGuild(guildId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'selectStatGroup', guildId); return; } const game = hg().getHG().getGame(guildId); if (!game) { if (typeof cb === 'function') cb('NO_GAME_IN_GUILD'); return; } if (!groupId || groupId.length == 0) { game.statGroup = null; if (typeof cb === 'function') cb(null); return; } else if (typeof groupId !== 'string') { cb('BAD_GROUP'); return; } game._stats.fetchGroup(groupId, (err, group) => { if (err) { cb('BAD_GROUP'); return; } game.statGroup = group.id; if (typeof cb === 'function') cb(null); }); }; /** * Create a stat group. * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {string} guildId The guild id to look at. * @param {?string} name The name of the new group. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.createStatGroup = function createStatGroup( userData, socket, guildId, name, cb) { if (!checkPerm(userData, guildId, null, 'group create')) { if (!checkMyGuild(guildId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'createStatGroup', guildId); return; } const game = hg().getHG().getGame(guildId); if (!game) { if (typeof cb === 'function') cb('NO_GAME_IN_GUILD'); return; } if (name && typeof name === 'string') { if (name.length === 0 || name.length > 24) { cb('BAD_NAME'); return; } } else if (name) { cb('BAD_NAME'); return; } game._stats.createGroup({name: name}, (group) => { group.fetchMetadata((err, meta) => { if (err) { cb('ATTEMPT_FAILED'); return; } if (typeof cb === 'function') cb(null, group.id, meta); }); }); }; /** * Delete a stat group. * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {string} guildId The guild id to look at. * @param {string} groupId The group id to delete. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. */ this.deleteStatGroup = function deleteStatGroup( userData, socket, guildId, groupId, cb) { if (!checkPerm(userData, guildId, null, 'group delete')) { if (!checkMyGuild(guildId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'deleteStatGroup', guildId); return; } const game = hg().getHG().getGame(guildId); if (!game) { if (typeof cb === 'function') cb('NO_GAME_IN_GUILD'); return; } game._stats.fetchGroup(groupId, (err, group) => { if (err) { cb('BAD_GROUP'); return; } if (game.statGroup === group.id) { game.statGroup = null; } group.reset(); if (typeof cb === 'function') cb(null); }); }; /** * Handle receiving image data for avatar uploading. * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {string} iId The image ID that is being uploaded. * @param {string} chunkId Id of the chunk being received. * @param {?Buffer} chunk Chunk of data received, or null if all data has been * sent. * @param {Function} [cb] Callback that fires once the requested action is * complete, or has failed. */ this.imageChunk = function imageChunk( userData, socket, gId, iId, chunkId, chunk, cb) { const meta = imageBuffer[iId]; if (!meta) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'imageChunk', gId); return; } if (meta.uId != userData.id) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'imageChunk', gId); return; } if (meta.type == 'NPC') { if (!checkPerm(userData, gId, null, 'ai create')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'imageChunk', gId); cancelImageUpload(iId); return; } } else { self.common.logWarning( 'Unknown image type attempted to be uploaded: ' + meta.type, socket.id); cancelImageUpload(iId); } if (chunk) { chunk = Buffer.from(chunk); meta.receivedBytes += chunk.length; if (isNaN(chunkId * 1)) { cancelImageUpload(iId); if (typeof cb === 'function') cb('Malformed Data'); return; } else if (meta.receivedBytes > hg().maxBytes) { cancelImageUpload(iId); if (typeof cb === 'function') cb('Data Overflow'); return; } meta.buffer[chunkId] = chunk; if (typeof cb === 'function') cb(chunkId); return; } if (meta.type == 'NPC') { const npcId = hg().NPC.createID(); const p = hg().NPC.saveAvatar(Buffer.concat(meta.buffer), npcId); if (!p) { cancelImageUpload(iId); if (typeof cb === 'function') cb('Malformed Data'); return; } p.then((url) => { const error = hg().createNPC(gId, meta.username, url, npcId); const game = hg().getHG().getGame(gId); cancelImageUpload(iId); if (typeof cb === 'function') { cb(error, game && game.serializable); } else if (error) { socket.emit('message', error); } self.common.logDebug( 'NPC Created from upload with URL: ' + url, socket.id); }).catch(() => { cancelImageUpload(iId); if (typeof cb === 'function') cb('Malformed Data'); }); } else { self.common.logWarning( 'Unknown upload type completed. Data is being deleted. (' + meta.type + ')', socket.id); if (typeof cb === 'function') cb(); cancelImageUpload(iId); } }; /** * Handle client requesting to begin image upload. * * @public * @type {HGWeb~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {object} meta Metadata to associate with this upload. * @param {HGWeb~basicCB} [cb] Callback that fires once the requested action * is complete, or has failed. If succeeded, an upload ID will be passed as * the second parameter. Any error will be the first parameter. */ this.imageInfo = function imageInfo(userData, socket, gId, meta, cb) { if (!meta || typeof meta.type !== 'string' || isNaN(meta.contentLength * 1)) { if (typeof cb === 'function') cb('Malformed Data'); return; } if (meta.type === 'NPC') { if (meta.contentLength > hg().maxBytes) { if (typeof cb === 'function') cb('Excessive Payload'); return; } if (typeof meta.username !== 'string') { if (typeof cb === 'function') cb('Malformed Data'); return; } meta.username = hg().formatUsername(meta.username); if (meta.username.length < 2) { if (typeof cb === 'function') cb('Malformed Data'); return; } if (!checkPerm(userData, gId, null, 'ai create')) { if (!checkMyGuild(gId)) return; if (typeof cb === 'function') cb('NO_PERM'); replyNoPerm(socket, 'imageInfo', gId); return; } const buf = beginImageUpload(userData.id); buf.username = meta.username; buf.type = meta.type; if (typeof cb === 'function') cb(null, buf.id); } else { if (typeof cb === 'function') cb('NO_PERM'); } }; } module.exports = new HGWeb();