noblox.js-server
Advanced tools
Comparing version 1.3.1 to 2.0.1
{ | ||
"name": "noblox.js-server", | ||
"version": "1.3.1", | ||
"description": "An example server that uses the noblox.js module.", | ||
"version": "2.0.1", | ||
"description": "A RESTful API for using noblox.js", | ||
"main": "server.js", | ||
"author": "suufi <suufi@mit.edu>", | ||
"license": "MIT", | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1", | ||
"lint": "standard .", | ||
"start": "node server.js" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/suufi/noblox.js-server.git" | ||
}, | ||
"author": "Suufi <sd.atrillion@gmail.com>", | ||
"license": "MIT", | ||
"dependencies": { | ||
"bluebird": "^3.4.6", | ||
"body-parser": "^1.15.2", | ||
"express": "^4.14.0", | ||
"noblox.js": "^4.1.2", | ||
"validator": "^6.1.0" | ||
"@hapi/boom": "^10.0.1", | ||
"@koa/router": "^13.0.0", | ||
"dotenv": "^16.4.5", | ||
"koa": "^2.13.1", | ||
"koa-404-handler": "^0.1.0", | ||
"koa-better-error-handler": "^11.0.4", | ||
"koa-bodyparser": "^4.3.0", | ||
"koa-joi-router": "^8.0.0", | ||
"noblox.js": "^6.0.2" | ||
}, | ||
"devDependencies": { | ||
"standard": "^17.1.0" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/suufi/noblox.js-server/issues" | ||
"url": "https://github.com/noblox/noblox.js/issues" | ||
}, | ||
"homepage": "https://github.com/suufi/noblox.js-server" | ||
"homepage": "https://github.com/noblox/noblox.js-server", | ||
"packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447" | ||
} |
232
README.md
@@ -1,201 +0,57 @@ | ||
# noblox.js-server | ||
<h1 align="center"> | ||
<img src="https://raw.githubusercontent.com/noblox/noblox.js-server/master/img/noblox.js-server-logo.png" alt="noblox.js-server" width="400"/> | ||
<br> | ||
</h1> | ||
[![ROBLOX API Discord](https://img.shields.io/badge/discord-roblox%20api%20chat-blue.svg?style=flat-square)](https://discord.gg/EDXNdAT) | ||
<h4 align="center">A RESTful API using <a href="https://github.com/suufi/noblox.js">noblox.js</a> and <a href="https://koajs.com/">Koa</a>.</h4> | ||
This is a primitive example server that uses my [noblox.js](https://github.com/suufi/noblox.js) library, allowing users to execute site actions from in-game via HttpService. | ||
<p align="center"> | ||
<a href="https://standardjs.com"><img src="https://img.shields.io/badge/code_style-standard-blue.svg?style=flat-square" alt="JavaScript Style Guide"/></a> | ||
<a href="https://discord.gg/R5GVSyTVGv"><img src="https://img.shields.io/badge/discord-noblox.js-blue.svg?style=flat-square" alt="noblox.js Discord"/></a> | ||
<a href="https://travis-ci.org/noblox/noblox.js-server"><img src="https://img.shields.io/travis/noblox/noblox.js-server/master.svg?style=flat-square" alt="Travis Build Status"/></a></a> | ||
</p> | ||
## Instructions | ||
<p align="center"> | ||
<a href="#about">About</a> • | ||
<a href="#prerequisites">Prerequisites</a> • | ||
<a href="#configuration">Configuration</a> • | ||
<a href="https://github.com/suufi/noblox.js/tree/master/examples">Examples</a> • | ||
<a href="https://www.youtube.com/playlist?list=PLEW4K4VqMUb_VMA3Yp9LI4gReRyVWGTnU">YouTube Series</a> • | ||
<a href="#credits">Credits</a> • | ||
<a href="#license">License</a> | ||
</p> | ||
Go to settings.json and set `cookie` to the .ROBLOSECURITY cookie of the ROBLOX account you want to use. The `key` field is essentially a password for the site (to prevent strangers from accessing account functions). There is also an optional setting `maximumRank` which can be used to prevent attacks. User's above this rank are immune from having their rank changed and attempts to change a user's rank to something above this will be rejected. I recommend generating a random string or just smashing your keyboard since this will typically be accessed by another script that doesn't have to memorize said key. | ||
## About | ||
This repository hosts the code for a working [RESTful API](https://restfulapi.net/) that utilizes Koa.js to expose [noblox.js](https://github.com/suufi/noblox.js) functions on the internet. Essentially, with this project, you can host it on your own server and interact with the Roblox API through your own Roblox game. | ||
## Free Host Tutorial | ||
1. Go to [heroku.com](https://heroku.com/) and sign up for an account. | ||
2. Install [heroku command line tools](https://devcenter.heroku.com/articles/heroku-command-line#download-and-install). | ||
3. [Download](https://github.com/sentanos/roblox-js-server/archive/master.zip) this repository and unzip. | ||
4. Open the settings.json file and fill in the fields "username", "password", and "key" in the quotes after each. | ||
5. Open a terminal or command prompt and type `cd `, then drag the folder into the window and release. It should fill in the path name afterwards. Hit enter. | ||
6. Type in `heroku login` and type in the username and password of the account you made in step 1. | ||
7. Type in `git init` [Enter] followed by `git add --all` [Enter] and then `git commit -am "Initial"` [Enter] | ||
8. Type in `heroku create` [Enter]; you can put in a custom name after the command as well. eg. `heroku create roblox-js-server` | ||
9. Finally type `git push heroku master` [Enter] and let it go through. If all goes well it will deploy after a minute or two and will tell you the url of your server around the end of the process. | ||
## Prerequisites | ||
## Updating | ||
To update the server files on heroku (esp. server.js): | ||
- [**node.js**](https://nodejs.org/en/download/current/) | ||
- a virtual private server (VPS) | ||
- To have your code running on a 24/7 basis, you need to use a VPS. We recommend using DigitalOcean for its ease of use and. [This referral link](https://m.do.co/c/14822e4e2d63) provides you with a $100 credit which can be used over 60 days. Other options include Amazon Web Services, Microsoft Azure, and Google Compute Engine. | ||
## Configuration | ||
### server.js | ||
After installing this repository on your server, start by creating an `.env` file to house your configuration settings. You can duplicate the `.env.sample` file and fill in the missing details. | ||
- Unless you know what you are doing, leave the `PORT` number the same. | ||
- `MAX_RANK` refers to the highest rank (1-254) the logged in account is allowed to promote users to. | ||
- `API_TOKEN` refers to a secret key to secure your RESTful API to avoid your API being accessed by unauthorized users. It is best to generate a key that isn't easy to guess. You can use [this](https://randomkeygen.com/) website to use an automatically generated key. You need not memorize this key. | ||
- `COOKIE` refers to the cookie of the logged-in user account the API will execute functions from. To find your cookie, please read [this](https://github.com/suufi/noblox.js#getting-your-cookie-chrome). | ||
1. Go to your original roblox-js-server folder and delete all files EXCEPT settings.json (unless you want to reenter info) | ||
2. [Redownload](https://github.com/sentanos/roblox-js-server/archive/master.zip) the repository and unzip | ||
3. Drag all the files in the new folder you downloaded into the old one EXCEPT for settings.json | ||
4. Open a terminal or command prompt and type `cd `, then drag the folder into the window and release. Hit enter. | ||
5. Type in `git add --all` [Enter] | ||
6. Type in `git commit -am "Update"` [Enter] | ||
7. Type in `git push heroku master` [Enter] and let it run. | ||
After your file is configured, use a process manager like [pm2](https://pm2.keymetrics.io/) to have your script run 24/7. We do not provide support for VPS, network, and domain configuration. | ||
Sometimes you also have to update dependency files like roblox-js which the module requires: | ||
1. Open a terminal or command prompt and type `cd `, then drag the folder into the window and release. Hit enter. | ||
2. Type in `heroku config:set NODE_MODULES_CACHE=false` [Enter] | ||
3. Type in `git commit --allow-empty -m "Rebuild"` [Enter] | ||
4. Type in `git push heroku master` [Enter] and let it run | ||
5. Type in `heroku config:unset NODE_MODULES_CACHE` [Enter] | ||
### noblox.lua | ||
If you plan on using the provided Lua module (ModuleScript) in this project, please do the following: | ||
- Place the script only in `ServerScriptService`. | ||
- Update the `DOMAIN` value in `CONFIGURATION` to reflect your server's IP address/domain & port. (e.g. if your domain name is noblox.io and this is running on port 3000, your value here would be `https://noblox.io:3000`) | ||
- Update the `API_TOKEN` value in `CONFIGURATION` so that it matches what you put earlier in `server.js`. | ||
- Optional: provide a `DEFAULT_GROUP_ID` to default to having noblox.js functions run on a single group when not specified. | ||
## Lua Example | ||
## Credits | ||
A module script is available in [lua/server.mod.lua](./lua/server.mod.lua) that allows you to use functions to send commands to the server. An initializer function is returned by the module and requires the arguments `domain` and `key`. If the third `group` argument is provided, the returned table will automatically call group-related functions with that groupId, otherwise it has to be the first argument. | ||
* [suufi](https://github.com/suufi) - Lead maintainer | ||
The commands `promote`, `demote`, `setRank`, `shout`, `post`, `handleJoinRequest`, `forumPostNew`, `forumPostReply`, and `message` are available, all arguments are in the same order as they are in the documentation, with parameters first and then each body argument in order (excluding key). The return value of the function is the decoded table that the API returns. | ||
## License | ||
Example usage, assuming ModuleScript is named "Server" and is in the same directory as the script (eg. both ServerScriptService): | ||
```lua | ||
local server = require(script.Parent.Server) | ||
local domain = 'rbx-js.herokuapp.com' -- Make sure there is no http:// in here! | ||
local key = '/UAO9lTOYapr8ecV8cs/t3cP9c7na6rKHfRn7M6GDct+PdJyQJ40Jebe+CKZDgKV8TRLtbBqfhJc/eHNC7RHA8BCKkrFOkaIKC9/ripy34QzLq3m2qqy/GdyCg/5KHFUPbsuRNetr52ZP+6E2puKWrR9XvuAMG9bq+X02luwmID6aU7YBpq7sALl21Pv0OB4wy43VhuI3esN8w/Rl0ZC3LiJWwMv8PnwCKqgmq9L9UXLVBEPNJ9Plcv73+QqArHqiZ/qtrJO88=' | ||
local groupId = 18 | ||
local userId = game:GetService'Players':GetUserIdFromNameAsync'Shedletsky' | ||
local api = server(domain, key, groupId) | ||
print(api.promote(userId).message) | ||
print(api.message(userId, 'Subject', 'Body').message) | ||
print(api.getBlurb(1).data.blurb) | ||
``` | ||
## Documentation | ||
### METHOD /example/{argument1: type}/{argument2: type}/[optional argument: type]?[optional_argument: type]&[optional_argument2: type] | ||
```http | ||
Example usage | ||
``` | ||
{argument3: type} | ||
Description | ||
Post data is under the header, in this example is starts with argument 3. This should be sent in json format or it will not be read correctly. | ||
All functions respond with a json table containing the fields `error`, `message`, and `data`. The `error` field is always visible but may be null, the `message` field contains a human-readable message summarizing the pages action, and `data` contains response data from the API is applicable. | ||
[response1: type, reponse2: type] | ||
### POST /demote/{group: number}/{target: number} | ||
```http | ||
/demote/18/2470023 | ||
{"key": "hunter2"} | ||
``` | ||
{key: string} | ||
Sets the role of the player to the adjacent lower-rank role. | ||
[newRankName: string, newRank: number, newRoleSetId: number] | ||
### GET /getBlurb/{userId: number} | ||
```http | ||
/getBlurb/2470023 | ||
``` | ||
Gets the blurb of the user with `userId`. | ||
[blurb: string] | ||
### POST /getPlayers/delete/{uid: string} | ||
```http | ||
/getPlayers/delete/2f08e9796a | ||
{"key": "hunter2"} | ||
``` | ||
{key: string} | ||
Deletes the getPlayers job with id `uid` from the filesystem if complete or the list if not. Note that if it is not complete it will still be running on the server though it cannot be accessed. | ||
### POST /getPlayers/make/{group: number}/[rank: number]?[limit: number]&[online: boolean] | ||
```http | ||
/getPlayers/make/147864?limit=1&online=false | ||
{"key": "hunter2"} | ||
``` | ||
{key: string} | ||
Gets the players in group with group ID `group`. If `rank` is not specified it gets all players from all ranks, otherwise it gets all players from the role with that rank. If `online` is true only online users will be collected. If `limit` is enabled users will be returned in the same order they are visible in the group pages to the set number and forever if the number is -1. Note that this slows down the function considerably. The function returns a `uid` that can eventually be used to get the resulting player list (if the page just waited it could time out since this action can take a while). The file containing players is stored on the file system in the folder `players` and is not cleared by this script, therefore the result will be usable across restarts. See below for retrieving. | ||
[uid: number] | ||
### GET /getPlayers/retrieve/{uid: string} | ||
```http | ||
/getPlayers/retrieve/2f08e9796a | ||
``` | ||
Gets the result of the getPlayers job, returning `progress` in percent when not complete while `complete` denotes whether or not it is. The players are in json object `players`. | ||
[progress: number, | ||
complete: boolean, | ||
players (object): {username (string): userId (number)}] | ||
### POST /handleJoinRequest/{group: number}/{username: string}/{accept: boolean} | ||
```http | ||
/handleJoinRequest/18/Froast/true | ||
{"key": "hunter2"} | ||
``` | ||
{key: string} | ||
Searches for the join request of user with username `username` in the group with group ID `group` and accepts them if `accept` is true and denies them if it is false (note that for either case you still need the parameter in the url) | ||
### POST /message/{recipient: number} | ||
```http | ||
/message/2470023 | ||
{"subject": "Test", "body": "Test", "key": "hunter2"} | ||
``` | ||
{subject: string, | ||
body: string, | ||
key: string} | ||
Messages user with ID `recipient` with a message that has subject `subject` and body `body`. | ||
### POST /promote/{group: number}/{target: number} | ||
```http | ||
/promote/18/2470023 | ||
{"key": "hunter2"} | ||
``` | ||
{key: string} | ||
Sets the role of the player to the adjacent higher-rank role. | ||
[newRankName: string, newRank: number, newRoleSetId: number] | ||
### POST /setRank/{group: number}/{target: number}/{rank: number} | ||
```http | ||
/setRank/18/2470023/2 | ||
{"key": "hunter2"} | ||
``` | ||
{key: string} | ||
Sets rank of player with user ID `target` to rank with rank number `rank` in group with group ID `group`. Responds with the role set ID of the user's updated rank, `newRoleSetId`. | ||
[newRoleSetId: number] | ||
### POST /shout/{group: number} | ||
```http | ||
/shout/18 | ||
{"message": "Test", "key": "hunter2"} | ||
``` | ||
{message: string, | ||
key: string} | ||
Shouts in group with group ID `group` and the message `message`. | ||
### POST /post/{group: number} | ||
```http | ||
/post/18 | ||
{"message": "Test", "key": "hunter2"} | ||
``` | ||
{message: string, | ||
key: string} | ||
Posts a message to the wall in group with group ID `group` and the message `message`. | ||
## Credits | ||
Credits to sentanos for the original roblox-js-server which this project is based off of. | ||
MIT |
631
server.js
@@ -1,462 +0,269 @@ | ||
var express = require('express') | ||
var rbx = require('noblox.js') | ||
var fs = require('fs') | ||
var crypto = require('crypto') | ||
var validator = require('validator') | ||
var bodyParser = require('body-parser') | ||
var Promise = require('bluebird') | ||
require('dotenv').config() | ||
var app = express() | ||
var port = process.env.PORT || 8080 | ||
var settings = require('./settings.json') | ||
var key = settings.key | ||
var maximumRank = settings.maximumRank || 255 | ||
const COOKIE = settings.cookie | ||
const Koa = require('koa') | ||
const Router = require('koa-joi-router') | ||
const Joi = Router.Joi | ||
app.set('env', 'production') | ||
const errorHandler = require('koa-better-error-handler') | ||
const koa404Handler = require('koa-404-handler') | ||
const Boom = require('@hapi/boom') | ||
const bodyParser = require('koa-bodyparser') | ||
var _setRank = rbx.setRank | ||
const nbx = require('noblox.js') | ||
rbx.setRank = function (opt) { | ||
var rank = opt.rank | ||
if (rank > maximumRank) { | ||
return Promise.reject(new Error('New rank ' + rank + ' is above rank limit ' + maximumRank)) | ||
} else { | ||
return _setRank(opt) | ||
} | ||
} | ||
const app = new Koa() | ||
const router = Router() | ||
var inProgress = {} | ||
var completed = {} | ||
const { COOKIE, API_TOKEN, MAX_RANK, PORT } = process.env | ||
var dir = './players' | ||
app.context.onerror = errorHandler() | ||
app.context.api = true | ||
app.use(koa404Handler) | ||
app.use(bodyParser()) | ||
if (!fs.existsSync(dir)) { | ||
fs.mkdirSync(dir) | ||
} | ||
let loggedIn = false | ||
fs.readdirSync('./players').forEach(function (file) { // This is considered a part of server startup and following functions could error anyways if it isn't complete, so using synchronous instead of asynchronous is very much intended. | ||
completed[file] = true | ||
}) | ||
function sendErr (res, json, status) { | ||
res.json(json) | ||
async function authenticate (ctx) { | ||
if (ctx.request.headers.authorization !== API_TOKEN) { | ||
ctx.throw(Boom.unauthorized('Authorization header does not match API_TOKEN.')) | ||
} | ||
} | ||
function validatorType (type) { | ||
switch (type) { | ||
case 'int': | ||
return validator.isInt | ||
case 'safe_string': | ||
return validator.isAlphanumeric | ||
case 'boolean': | ||
return validator.isBoolean | ||
case 'string': | ||
return function (value) { | ||
return typeof value === 'string' | ||
} | ||
default: | ||
return function () { | ||
return true | ||
} | ||
async function nbxAuthenticate (ctx) { | ||
if (!loggedIn) { | ||
ctx.throw(Boom.unauthorized('You are not logged into a Roblox account, please update COOKIE.')) | ||
} | ||
} | ||
function processType (type, value) { | ||
switch (type) { | ||
case 'int': | ||
return parseInt(value, 10) | ||
case 'boolean': | ||
return (value === 'true') | ||
default: | ||
return value | ||
} | ||
async function throwError (ctx, error) { | ||
const boomResponse = error | ||
ctx.status = boomResponse.output.statusCode | ||
ctx.body = boomResponse.output.payload | ||
} | ||
function verifyParameters (res, validate, requiredFields, optionalFields) { | ||
var result = {} | ||
if (requiredFields) { | ||
for (var index in requiredFields) { | ||
var type = requiredFields[index] | ||
var use = validatorType(type) | ||
router.get('/', async (ctx) => { | ||
ctx.status = 200 | ||
}) | ||
var found = false | ||
for (var i = 0; i < validate.length; i++) { | ||
var value = validate[i][index] | ||
if (value) { | ||
if (use(value)) { | ||
result[index] = processType(type, value) | ||
found = true | ||
} else { | ||
sendErr(res, {error: 'Parameter "' + index + '" is not the correct data type.', id: null}) | ||
return false | ||
} | ||
break | ||
} | ||
// GET username from ID route | ||
router.route({ | ||
method: 'get', | ||
path: '/get-username-from-id/:id', | ||
handler: async (ctx) => { | ||
await nbx.getUsernameFromId(ctx.params.id).then(function (username) { | ||
ctx.body = { | ||
success: true, | ||
data: username | ||
} | ||
if (!found) { | ||
sendErr(res, {error: 'Parameter "' + index + '" is required.', id: null}) | ||
return false | ||
} | ||
} | ||
}).catch(function (err) { | ||
ctx.throw(Boom.notFound(err)) | ||
}) | ||
} | ||
if (optionalFields) { | ||
for (index in optionalFields) { | ||
type = optionalFields[index] | ||
use = validatorType(type) | ||
for (i = 0; i < validate.length; i++) { | ||
value = validate[i][index] | ||
if (value) { | ||
if (use(value)) { | ||
result[index] = processType(type, value) | ||
} else { | ||
sendErr(res, {error: 'Parameter "' + index + '" is not the correct data type.', id: null}) | ||
return false | ||
} | ||
break | ||
} | ||
} | ||
} | ||
} | ||
return result | ||
} | ||
}) | ||
function authenticate (req, res, next) { | ||
if (req.body.key === key) { | ||
next() | ||
} else { | ||
sendErr(res, {error: 'Incorrect authentication key', id: null}, 401) | ||
} | ||
} | ||
function checkRank (opt) { | ||
var group = opt.group | ||
var target = opt.target | ||
return rbx.getRankInGroup(group, target) | ||
.then(function (rank) { | ||
if (rank === 0) { | ||
throw new Error('Target user ' + target + ' is not in group ' + group) | ||
// GET ID from username route | ||
router.route({ | ||
method: 'get', | ||
path: '/get-id-from-username/:username', | ||
handler: async (ctx) => { | ||
await nbx.getIdFromUsername(ctx.params.username).then(function (username) { | ||
ctx.body = { | ||
success: true, | ||
data: username | ||
} | ||
if (rank > maximumRank) { | ||
throw new Error('Original rank ' + rank + ' is above rank limit ' + maximumRank) | ||
} | ||
return rank | ||
}).catch(function (err) { | ||
ctx.throw(Boom.notFound(err)) | ||
}) | ||
} | ||
} | ||
}) | ||
function changeRank (amount) { | ||
return function (req, res, next) { | ||
var requiredFields = { | ||
'group': 'int', | ||
'target': 'int' | ||
// GET a player's information | ||
router.route({ | ||
method: 'get', | ||
path: '/user/:id', | ||
pre: async (ctx, next) => { | ||
await authenticate(ctx, next) | ||
return next() | ||
}, | ||
handler: async (ctx) => { | ||
if (ctx.invalid) { | ||
await throwError(ctx, Boom.badRequest(ctx.invalid.body)) | ||
return | ||
} | ||
var validate = [req.params] | ||
return nbx.getPlayerInfo(ctx.params.id).then((playerInfo) => { | ||
ctx.status = 200 | ||
ctx.body = playerInfo | ||
}).catch((err) => { | ||
return throwError(ctx, Boom.badRequest(err.message)) | ||
}) | ||
} | ||
}) | ||
var opt = verifyParameters(res, validate, requiredFields) | ||
if (!opt) { | ||
// POST a response to a user join request | ||
router.route({ | ||
method: 'post', | ||
path: '/group/:group/handle-join-request', | ||
validate: { | ||
type: 'json', | ||
body: Joi.object({ | ||
target: Joi.number().required(), | ||
accept: Joi.boolean().required() | ||
}), | ||
continueOnError: true | ||
}, | ||
pre: async (ctx, next) => { | ||
await authenticate(ctx, next) | ||
await nbxAuthenticate(ctx, next) | ||
return next() | ||
}, | ||
handler: async (ctx) => { | ||
if (ctx.invalid) { | ||
await throwError(ctx, Boom.badRequest(ctx.invalid.body)) | ||
return | ||
} | ||
var group = opt.group | ||
checkRank(opt) | ||
.then(function (rank) { | ||
return rbx.getRoles(group) | ||
.then(function (roles) { | ||
var found | ||
var foundRank | ||
// Roles is actually sorted on ROBLOX's side and returned the same way | ||
for (var i = 0; i < roles.length; i++) { | ||
var role = roles[i] | ||
var thisRank = role.Rank | ||
if (thisRank === rank) { | ||
var change = i + amount | ||
found = roles[change] | ||
if (!found) { | ||
sendErr(res, {error: 'Rank change is out of range'}) | ||
return | ||
} | ||
foundRank = found.Rank | ||
var up = roles[change + 1] | ||
var down = roles[change - 1] | ||
if ((up && up.Rank === foundRank) || (down && down.Rank === foundRank)) { | ||
sendErr(res, {error: 'There are two or more roles with the same rank number, please change or commit manually.'}) | ||
return | ||
} | ||
var name = found.Name | ||
opt.rank = foundRank | ||
return rbx.setRank(opt) | ||
.then(function (roleset) { | ||
res.json({error: null, data: {newRoleSetId: roleset, newRankName: name, newRank: foundRank}, message: 'Successfully changed rank of user ' + opt.target + ' to rank "' + name + '" in group ' + opt.group}) | ||
}) | ||
} | ||
} | ||
}) | ||
}) | ||
.catch(function (err) { | ||
sendErr(res, {error: 'Change rank failed: ' + err.message}) | ||
}) | ||
return nbx.handleJoinRequest(ctx.request.params.group, ctx.request.body.target, ctx.request.body.accept).then(() => { | ||
ctx.status = 200 | ||
ctx.body = { | ||
success: true, | ||
message: `User's join request was ${ctx.request.body.accept ? 'accepted' : 'declined'} successfully.` | ||
} | ||
}).catch((err) => { | ||
console.log(err) | ||
return throwError(ctx, Boom.unauthorized(err.message)) | ||
}) | ||
} | ||
} | ||
}) | ||
function getPlayersWithOpt (req, res, next) { | ||
var uid = crypto.randomBytes(5).toString('hex') | ||
var requiredFields = { | ||
'group': 'int' | ||
} | ||
var optionalFields = { | ||
'rank': 'int', | ||
'limit': 'int', | ||
'online': 'boolean' | ||
} | ||
var validate = [req.params, req.query] | ||
// POST a group shout | ||
router.route({ | ||
method: 'post', | ||
path: '/group/:group/shout', | ||
validate: { | ||
type: 'json', | ||
body: Joi.object({ | ||
message: Joi.string().required() | ||
}), | ||
continueOnError: true | ||
}, | ||
pre: async (ctx, next) => { | ||
await authenticate(ctx, next) | ||
await nbxAuthenticate(ctx, next) | ||
return next() | ||
}, | ||
handler: async (ctx) => { | ||
if (ctx.invalid) { | ||
await throwError(ctx, Boom.badRequest(ctx.invalid.body)) | ||
return | ||
} | ||
var opt = verifyParameters(res, validate, requiredFields, optionalFields) | ||
if (!opt) { | ||
return | ||
return nbx.shout(ctx.request.params.group, ctx.request.body.message).then((res) => { | ||
ctx.status = 200 | ||
ctx.body = res | ||
}).catch((err) => { | ||
return throwError(ctx, Boom.unauthorized(err.message)) | ||
}) | ||
} | ||
}) | ||
inProgress[uid] = 0 | ||
var players = rbx.getPlayers(opt) | ||
inProgress[uid] = players.getStatus | ||
players.promise.then(function (info) { | ||
if (inProgress[uid]) { // Check if job was deleted | ||
completed[uid] = true | ||
var file = fs.createWriteStream('./players/' + uid) | ||
file.write(JSON.stringify(info, null, ' ')) | ||
// DELETE a user (exile) | ||
router.route({ | ||
method: 'delete', | ||
path: '/group/:group/member/:target', | ||
pre: async (ctx, next) => { | ||
await authenticate(ctx, next) | ||
await nbxAuthenticate(ctx, next) | ||
return next() | ||
}, | ||
handler: async (ctx) => { | ||
if (ctx.invalid) { | ||
await throwError(ctx, Boom.badRequest(ctx.invalid.body)) | ||
return | ||
} | ||
info = null // Bye, bye | ||
}) | ||
res.json({error: null, data: {uid: uid}}) | ||
} | ||
app.use(bodyParser.json()) | ||
app.post('/setRank/:group/:target/:rank', authenticate, function (req, res, next) { | ||
var requiredFields = { | ||
'group': 'int', | ||
'rank': 'int', | ||
'target': 'int' | ||
} | ||
var validate = [req.params] | ||
var opt = verifyParameters(res, validate, requiredFields) | ||
if (!opt) { | ||
return | ||
} | ||
// This gets the rank manually instead of letting setRank do it because it needs the role's name. | ||
var rank = opt.rank | ||
checkRank(opt) | ||
.then(function () { | ||
return rbx.getRoles(opt.group) | ||
.then(function (roles) { | ||
var role = rbx.getRole(roles, rank) | ||
if (!role) { | ||
sendErr(res, {error: 'Role does not exist'}) | ||
return | ||
} | ||
var name = role.Name | ||
return rbx.setRank(opt) | ||
.then(function (roleset) { | ||
res.json({error: null, data: {newRoleSetId: roleset, newRankName: name, newRank: rank}, message: 'Successfully changed rank of user ' + opt.target + ' to rank "' + name + '" in group ' + opt.group}) | ||
}) | ||
}) | ||
return nbx.exile(ctx.params.group, ctx.params.target).then(() => { | ||
ctx.status = 200 | ||
ctx.body = { | ||
success: true | ||
} | ||
}).catch((err) => { | ||
return throwError(ctx, Boom.badRequest(err.message)) | ||
}) | ||
.catch(function (err) { | ||
sendErr(res, {error: 'Set rank failed: ' + err.message}) | ||
}) | ||
}) | ||
app.post('/handleJoinRequest/:group/:username/:accept', authenticate, function (req, res, next) { | ||
var requiredFields = { | ||
'group': 'int', | ||
'username': 'string', | ||
'accept': 'boolean' | ||
} | ||
var validate = [req.params] | ||
var opt = verifyParameters(res, validate, requiredFields) | ||
if (!opt) { | ||
return | ||
} | ||
rbx.handleJoinRequest(opt) | ||
.then(function () { | ||
res.json({error: null, message: 'Successfully ' + (opt.accept ? 'accepted' : 'declined') + ' ' + opt.username}) | ||
}) | ||
.catch(function (err) { | ||
sendErr(res, {error: 'Handle join request failed: ' + err.message}) | ||
}) | ||
}) | ||
app.post('/message/:recipient/', authenticate, function (req, res, next) { | ||
var requiredFields = { | ||
'recipient': 'int', | ||
'subject': 'string', | ||
'body': 'string' | ||
} | ||
var validate = [req.params, req.body] | ||
var opt = verifyParameters(res, validate, requiredFields) | ||
if (!opt) { | ||
return | ||
} | ||
rbx.message(opt) | ||
.then(function () { | ||
res.json({error: null, message: 'Messaged user ' + opt.recipient + ' with subject "' + opt.subject + '"'}) | ||
}) | ||
.catch(function (err) { | ||
sendErr(res, {error: 'Message failed: ' + err.message}) | ||
}) | ||
}) | ||
// POST to the rank of a user | ||
router.route({ | ||
method: 'post', | ||
path: '/group/:group/member/:target/rank', | ||
validate: { | ||
type: 'json', | ||
body: Joi.object({ | ||
rank: Joi.number().min(1).max(Number(MAX_RANK) || 254), | ||
role: Joi.string() | ||
}).xor('rank', 'role'), | ||
continueOnError: true | ||
}, | ||
pre: async (ctx, next) => { | ||
await authenticate(ctx, next) | ||
await nbxAuthenticate(ctx, next) | ||
return next() | ||
}, | ||
handler: async (ctx) => { | ||
if (ctx.invalid) { | ||
await throwError(ctx, Boom.badRequest(ctx.invalid.body)) | ||
return | ||
} | ||
app.post('/shout/:group', authenticate, function (req, res, next) { | ||
var requiredFields = { | ||
'group': 'int' | ||
} | ||
var optionalFields = { | ||
'message': 'string' | ||
} | ||
var validate = [req.params, req.body] | ||
var opt = verifyParameters(res, validate, requiredFields, optionalFields) | ||
if (!opt) { | ||
return | ||
} | ||
rbx.shout(opt) | ||
.then(function () { | ||
res.json({error: null, message: 'Shouted in group ' + opt.group}) | ||
return nbx.setRank(ctx.request.params.group, ctx.request.params.target, ctx.request.body.rank || ctx.request.body.role).then((res) => { | ||
ctx.status = 200 | ||
ctx.body = res | ||
}).catch((err) => { | ||
return throwError(ctx, Boom.badRequest(err.message)) | ||
}) | ||
.catch(function (err) { | ||
sendErr(res, {error: 'Error: ' + err.message}) | ||
}) | ||
}) | ||
app.post('/post/:group', authenticate, function (req, res, next) { | ||
var requiredFields = { | ||
'group': 'int', | ||
'message': 'string' | ||
} | ||
var validate = [req.params, req.body] | ||
var opt = verifyParameters(res, validate, requiredFields) | ||
if (!opt) { | ||
return | ||
} | ||
rbx.post(opt) | ||
.then(function () { | ||
res.json({error: null, message: 'Posted in group ' + opt.group}) | ||
}) | ||
.catch(function (err) { | ||
sendErr(res, {error: 'Error: ' + err.message}) | ||
}) | ||
}) | ||
app.get('/getBlurb/:userId', function (req, res, next) { | ||
var requiredFields = { | ||
'userId': 'int' | ||
} | ||
var validate = [req.params] | ||
var opt = verifyParameters(res, validate, requiredFields) | ||
if (!opt) { | ||
return | ||
} | ||
rbx.getBlurb(opt) | ||
.then(function (blurb) { | ||
res.json({error: null, data: {blurb: blurb}}) | ||
}) | ||
.catch(function (err) { | ||
sendErr(res, {error: 'Error: ' + err.message}) | ||
}) | ||
}) | ||
app.post('/promote/:group/:target', authenticate, changeRank(1)) | ||
app.post('/demote/:group/:target', authenticate, changeRank(-1)) | ||
app.post('/getPlayers/make/:group/:rank', getPlayersWithOpt) | ||
app.post('/getPlayers/make/:group', getPlayersWithOpt) | ||
app.post('/getPlayers/delete/:uid', authenticate, function (req, res, next) { | ||
var uid = req.params.uid | ||
function fail () { | ||
sendErr(res, {error: 'Invalid ID or the job is not complete'}) | ||
} | ||
if (uid.length === 10 && validator.isHexadecimal(uid)) { | ||
var path = './players/' + uid | ||
if (completed[uid]) { | ||
completed[uid] = false | ||
inProgress[uid] = null | ||
fs.unlink(path, function (err) { // Since the uid was verified to be hex this shouldn't be a security issue | ||
if (err) { | ||
next(err) | ||
} else { | ||
res.json({error: null, message: 'File deleted'}) | ||
} | ||
}) | ||
// POST to the rank of a user by providing a value to change it by | ||
router.route({ | ||
method: 'post', | ||
path: '/group/:group/member/:target/rank-change', | ||
validate: { | ||
type: 'json', | ||
body: Joi.object({ | ||
change: Joi.number().required() | ||
}), | ||
continueOnError: true | ||
}, | ||
pre: async (ctx, next) => { | ||
await authenticate(ctx, next) | ||
await nbxAuthenticate(ctx, next) | ||
return next() | ||
}, | ||
handler: async (ctx) => { | ||
if (ctx.invalid) { | ||
await throwError(ctx, Boom.badRequest(ctx.invalid.body)) | ||
return | ||
} | ||
} else if (inProgress[uid]) { | ||
inProgress[uid] = null | ||
res.json({error: null, message: 'Removed from list, the job itself has not been stopped'}) | ||
} else { | ||
fail() | ||
} | ||
}) | ||
app.get('/getPlayers/retrieve/:uid', function (req, res, next) { | ||
var uid = req.params.uid | ||
function fail () { | ||
sendErr(res, {error: 'Invalid ID'}) | ||
return nbx.changeRank(ctx.request.params.group, ctx.request.params.target, ctx.request.body.change).then((res) => { | ||
ctx.status = 200 | ||
ctx.body = res | ||
}).catch((err) => { | ||
return throwError(ctx, Boom.badRequest(err.message)) | ||
}) | ||
} | ||
if (uid.length === 10 && validator.isHexadecimal(uid)) { | ||
var path = './players/' + uid | ||
var complete = completed[uid] | ||
var progress = inProgress[uid] | ||
if (complete) { | ||
fs.stat(path, function (err) { | ||
if (err) { | ||
next(err) | ||
} else { | ||
res.append('Content-Type', 'application/json') | ||
res.write('{"error":null,"data":{"progress":100,"complete":true,') | ||
var stream = fs.createReadStream(path) | ||
var first = true | ||
stream.on('data', function (data) { | ||
if (first) { | ||
first = false | ||
res.write(data.toString().substring(1)) | ||
} else { | ||
res.write(data) | ||
} | ||
}) | ||
stream.on('end', function () { | ||
res.end('}') | ||
}) | ||
} | ||
}) | ||
} else if (progress) { | ||
sendErr(res, {error: 'Job is still processing', data: {complete: false, progress: progress()}}, 200) | ||
} else { | ||
fail() | ||
} | ||
} else { | ||
fail() | ||
} | ||
}) | ||
app.use(function (err, req, res, next) { | ||
console.error(err.stack) | ||
sendErr(res, {error: 'Internal server error'}) | ||
}) | ||
app.use(router.middleware()) | ||
function login () { | ||
return rbx.cookieLogin(COOKIE) | ||
if (COOKIE) { | ||
nbx.setCookie(COOKIE).then((currentUser) => { | ||
loggedIn = true | ||
app.listen(PORT) | ||
console.log(`Listening on port ${PORT},`, `logged into ${currentUser.name}#${currentUser.id}`) | ||
}) | ||
} else { | ||
app.listen(PORT) | ||
console.log(`Listening on port ${PORT},`, 'not logged in.') | ||
} | ||
login().then(function () { | ||
app.listen(port, function () { | ||
console.log('Listening on port ' + port) | ||
}) | ||
}) | ||
.catch(function (err) { | ||
var errorApp = express() | ||
errorApp.get('/*', function (req, res, next) { | ||
res.json({error: 'Server configuration error: ' + err.message}) | ||
}) | ||
errorApp.listen(port, function () { | ||
console.log('Configuration error page listening') | ||
}) | ||
}) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
68555
11
0
9
1
258
57
2
+ Added@hapi/boom@^10.0.1
+ Added@koa/router@^13.0.0
+ Addeddotenv@^16.4.5
+ Addedkoa@^2.13.1
+ Addedkoa-404-handler@^0.1.0
+ Addedkoa-bodyparser@^4.3.0
+ Addedkoa-joi-router@^8.0.0
+ Added@hapi/boom@10.0.1(transitive)
+ Added@hapi/bourne@3.0.0(transitive)
+ Added@hapi/hoek@11.0.49.3.0(transitive)
+ Added@hapi/topo@5.1.0(transitive)
+ Added@koa/router@10.1.113.1.0(transitive)
+ Added@microsoft/signalr@8.0.7(transitive)
+ Added@selderee/plugin-htmlparser2@0.11.0(transitive)
+ Added@sideway/address@4.1.5(transitive)
+ Added@sideway/formula@3.0.1(transitive)
+ Added@sideway/pinpoint@2.0.0(transitive)
+ Addedabort-controller@3.0.0(transitive)
+ Addedawait-busboy@1.0.3(transitive)
+ Addedblack-hole-stream@0.0.1(transitive)
+ Addedbluebird@2.11.0(transitive)
+ Addedbusboy@0.3.0(transitive)
+ Addedcache-content-type@1.0.1(transitive)
+ Addedcamelcase@6.3.0(transitive)
+ Addedcapitalize@2.0.4(transitive)
+ Addedchalk@5.3.0(transitive)
+ Addedclone@2.1.2(transitive)
+ Addedco@4.6.0(transitive)
+ Addedco-body@6.0.06.2.0(transitive)
+ Addedcookies@0.9.1(transitive)
+ Addedcopy-to@2.0.1(transitive)
+ Addeddebug@4.1.14.3.7(transitive)
+ Addeddecamelize@2.0.0(transitive)
+ Addeddeep-equal@1.0.1(transitive)
+ Addeddeepmerge@4.3.1(transitive)
+ Addeddelegates@1.0.0(transitive)
+ Addeddepd@1.1.2(transitive)
+ Addeddicer@0.3.0(transitive)
+ Addeddotenv@16.4.5(transitive)
+ Addedentities@5.0.0(transitive)
+ Addedevent-target-shim@5.0.1(transitive)
+ Addedeventsource@2.0.2(transitive)
+ Addedfast-safe-stringify@2.1.1(transitive)
+ Addedfetch-cookie@2.2.0(transitive)
+ Addedflatten@1.0.3(transitive)
+ Addedhas-tostringtag@1.0.2(transitive)
+ Addedhtml-to-text@9.0.5(transitive)
+ Addedhtmlparser2@8.0.2(transitive)
+ Addedhttp-assert@1.5.0(transitive)
+ Addedhttp-errors@1.8.1(transitive)
+ Addedhumanize-string@2.1.0(transitive)
+ Addedinflation@2.1.0(transitive)
+ Addedis-gen-fn@0.0.1(transitive)
+ Addedis-generator-function@1.0.10(transitive)
+ Addedjoi@17.13.3(transitive)
+ Addedkeygrip@1.1.0(transitive)
+ Addedkoa@2.15.3(transitive)
+ Addedkoa-404-handler@0.1.0(transitive)
+ Addedkoa-better-error-handler@11.0.4(transitive)
+ Addedkoa-bodyparser@4.4.1(transitive)
+ Addedkoa-compose@4.2.0(transitive)
+ Addedkoa-convert@2.0.0(transitive)
+ Addedkoa-joi-router@8.0.0(transitive)
+ Addedleac@0.6.0(transitive)
+ Addedlodash.iserror@3.1.1(transitive)
+ Addedlodash.isfunction@3.0.9(transitive)
+ Addedlodash.isnumber@3.0.3(transitive)
+ Addedlodash.isobject@3.0.2(transitive)
+ Addedlodash.isstring@4.0.1(transitive)
+ Addedlodash.map@4.6.0(transitive)
+ Addedlodash.values@4.3.0(transitive)
+ Addednoblox.js@6.0.2(transitive)
+ Addednode-fetch@2.7.0(transitive)
+ Addedonly@0.0.2(transitive)
+ Addedparseley@0.12.1(transitive)
+ Addedpath-to-regexp@6.3.0(transitive)
+ Addedpeberminta@0.9.0(transitive)
+ Addedpostman-request@2.88.1-postman.8-beta.1(transitive)
+ Addedpostman-url-encoder@1.0.1(transitive)
+ Addedquerystringify@2.2.0(transitive)
+ Addedrequires-port@1.0.0(transitive)
+ Addedselderee@0.11.0(transitive)
+ Addedset-cookie-parser@2.7.0(transitive)
+ Addedsliced@1.0.1(transitive)
+ Addedstatuses@1.5.0(transitive)
+ Addedstream-length@1.0.2(transitive)
+ Addedstreamsearch@0.1.2(transitive)
+ Addedtough-cookie@4.1.4(transitive)
+ Addedtr46@0.0.3(transitive)
+ Addedtsscmp@1.0.6(transitive)
+ Addeduniversalify@0.2.0(transitive)
+ Addedurl-parse@1.5.10(transitive)
+ Addedwebidl-conversions@3.0.1(transitive)
+ Addedwhatwg-url@5.0.0(transitive)
+ Addedws@7.5.10(transitive)
+ Addedxregexp@4.0.0(transitive)
+ Addedylru@1.4.0(transitive)
- Removedbluebird@^3.4.6
- Removedbody-parser@^1.15.2
- Removedexpress@^4.14.0
- Removedvalidator@^6.1.0
- Removedansi-styles@4.3.0(transitive)
- Removedarray-flatten@1.1.1(transitive)
- Removedbluebird@3.7.2(transitive)
- Removedbody-parser@1.20.3(transitive)
- Removedbufferutil@4.0.8(transitive)
- Removedchalk@4.1.2(transitive)
- Removedcolor-convert@2.0.1(transitive)
- Removedcolor-name@1.1.4(transitive)
- Removedcookie@0.6.0(transitive)
- Removedcookie-signature@1.0.6(transitive)
- Removedd@1.0.2(transitive)
- Removeddebug@2.6.9(transitive)
- Removedencodeurl@2.0.0(transitive)
- Removedes5-ext@0.10.64(transitive)
- Removedes6-iterator@2.0.3(transitive)
- Removedes6-symbol@3.1.4(transitive)
- Removedesniff@2.0.1(transitive)
- Removedetag@1.8.1(transitive)
- Removedevent-emitter@0.3.5(transitive)
- Removedexpress@4.21.0(transitive)
- Removedext@1.7.0(transitive)
- Removedfinalhandler@1.3.1(transitive)
- Removedforwarded@0.2.0(transitive)
- Removedhas-flag@4.0.0(transitive)
- Removedipaddr.js@1.9.1(transitive)
- Removedlodash@4.17.21(transitive)
- Removedmerge-descriptors@1.0.3(transitive)
- Removedmime@1.6.0(transitive)
- Removedms@2.0.0(transitive)
- Removednext-tick@1.1.0(transitive)
- Removednoblox.js@4.15.1(transitive)
- Removednode-gyp-build@4.8.2(transitive)
- Removedpath-to-regexp@0.1.10(transitive)
- Removedproxy-addr@2.0.7(transitive)
- Removedrange-parser@1.2.1(transitive)
- Removedrequest@2.88.2(transitive)
- Removedrequest-promise@4.2.6(transitive)
- Removedrequest-promise-core@1.1.4(transitive)
- Removedsend@0.19.0(transitive)
- Removedserve-static@1.16.2(transitive)
- Removedsignalr-client@0.0.20(transitive)
- Removedstealthy-require@1.1.1(transitive)
- Removedsupports-color@7.2.0(transitive)
- Removedtype@2.7.3(transitive)
- Removedtypedarray-to-buffer@3.1.5(transitive)
- Removedutf-8-validate@5.0.10(transitive)
- Removedutils-merge@1.0.1(transitive)
- Removedvalidator@6.3.0(transitive)
- Removedwebsocket@1.0.35(transitive)
- Removedyaeti@0.0.6(transitive)
Updatednoblox.js@^6.0.2