/* Unofficial brainlet interface functions Copyright (C) 2020 hiimgoodpack This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ const validator = require("validator"); const urllib = require("urllib"); const WebSocket = require("ws"); const reportBug = ` You've also discovered something that shouldn't occur. You can report it at https://git.hippoz.xyz/hiimgoodpack/brainlet-lib/issues`; const rateLimited = "You are being rate limited"; module.exports = {}; // I didn't make the rules. This is what the server requires module.exports.valid = { username: (name) => { if (validator.isEmpty(name)) return "Empty username"; if (!validator.isLength(name.trim(), { min: 3, max: 32 })) return "Username length must be between 3 and 32 characters"; if (!validator.isAlphanumeric(name)) return "Username must use alphanumeric characters"; }, email: (email) => { if (validator.isEmpty(email)) return "Empty email"; if (!validator.isEmail(email)) return "Invalid email"; }, password: (password) => { if (validator.isEmpty(password)) return "Empty password"; if (!validator.isLength(password, { min: 8, max: 128 })) return "Password length must be between 8 and 128 characters"; }, category: (category) => { if (validator.isEmpty(category)) return "Empty category name"; if (!validator.isLength(category.trim(), { min: 3, max: 32 })) return "Category name must be between 3 and 32 characters"; } }; /* User format: { name; email; password; token; permission; role; color; _id; } */ // WARNING: You should trust the server being used! module.exports.users = { create: (server, {name = "", email = "", password = ""}) => { return new Promise((res, rej) => { const validationError = module.exports.valid.username(name) || module.exports.valid.email(email) || module.exports.valid.password(password); if (validationError) { rej(validationError); return; } const api = `http://${server}/api/v1/users/account/create`; urllib.request(api, { method: "POST", data: { username: name, email: email, password: password } }).then((result) => { if (result.res.statusCode === 200) res(); else { const rawData = result.res.data.toString(); if (rawData === rateLimited) { rej(`Error when creating account: ${rateLimited}`); return; } const error = JSON.parse(rawData); rej(`Error when creating account: ${error.errors[0].msg} (${error.message})`); } }).catch((error) => { rej(`Error when sending request to ${api}: ${error.toString()}`); }); }); }, login: (server, {name = "", password = ""}) => { return new Promise((res, rej) => { const validationError = module.exports.valid.username(name) || module.exports.valid.password(password); if (validationError) { rej(validationError); return; } const api = `http://${server}/api/v1/users/token/create`; urllib.request(api, { method: "POST", data: { username: name, password: password } }).then((result) => { let newUser = {name: name, password: password}; const rawData = result.res.data.toString(); if (rawData === rateLimited) { rej(`Error when logging into account: ${rateLimited}`); return; } const data = JSON.parse(rawData); if (result.res.statusCode !== 200) { rej(`Error when logging into account: ${data.message}`); return; } newUser.token = data.token; newUser.permission = data.user.permissionLevel; newUser.role = data.user.role; newUser.color = data.user.color; newUser._id = data.user._id; res(newUser); }).catch((error) => { console.log(error) rej(`Error when sending request to ${api}: ${error.toString()}`); }); }); } }; module.exports.categories = { create: (server, token, name) => { return new Promise((res, rej) => { const validationError = module.exports.valid.category(name); const api = `http://${server}/api/v1/content/category/create`; urllib.request(api, { method: "POST", headers: { Cookie: `token=${token}` }, data: { title: name } }).then((result) => { if (result.res.statusCode === 200) res(); else { const rawData = result.res.data.toString(); if (rawData === rateLimited) { rej(`Error when creating category: ${rateLimited}`); return; } const data = JSON.parse(rawData); rej(`Error when creating category: ${data.message}`); } }).catch((error) => { rej(`Error when sending request to ${api}: ${error.toString()}`); }); }); }, list: (server, token, limit) => { return new Promise((res, rej) => { const api = `http://${server}/api/v1/content/category/list?count=${limit}` urllib.request(api, { method: "GET", headers: { Cookie: `token=${token}` } }).then((result) => { const rawData = result.res.data.toString(); if (rawData === rateLimited) { rej(`Error while listing categories: ${rateLimited}`); return; } const data = JSON.parse(rawData); if (result.res.statusCode !== 200) { rej(`Error while listing categories: ${data.message}`); return; } res(data.categories); }).catch((error) => { rej(`Error while sending request to ${api}: ${error.toString()}`); }); }); }, client: (server, token) => { const socketURL = `ws://${server}/socket.io/?token=${token}&transport=websocket`; return new Promise((res, rej) => { const socket = new WebSocket(socketURL); let categories = {}; let messageCallbacks = []; let authenticated = false; let sharedData = { connect: (categoriesID) => { return new Promise((res, rej) => { for (const categoryID of categoriesID) { categories[categoryID] = { updateCallback: res } } const data = [ "subscribe", [categoriesID] ]; socket.send(`42/gateway,${JSON.stringify(data)}`); }); }, send: (categoryID, message) => { const data = [ "message", { category: { _id: categoryID }, content: message } ]; socket.send(`42/gateway,${JSON.stringify(data)}`); }, onMessage: (callback) => { messageCallbacks[messageCallbacks.length] = callback; } }; socket.on("message", (rawMessage) => { const type = parseInt(rawMessage); const contents = rawMessage.substr(type.toString().length); switch (type) { // Authentication case 0: { socket.send("40/gateway,"); if (authenticated) console.error(`The server unexpectedly sent message type 0 multiple times${reportBug}`); break; } case 42: { const data = JSON.parse(contents.substr(("/gateway,").length)); const dataType = data[0]; switch (dataType) { case "hello": { socket.send("42/gateway,[\"yoo\"]"); if (authenticated) console.error(`The server unexpectedly sent hello through message type 42 multiple times${reportBug}`); else { authenticated = true; res(sharedData); } break; } case "clientListUpdate": { const categoryID = data[1].category._id; if (!categories[categoryID]) categories[categoryID] = {}; const callback = categories[categoryID].updateCallback; if (callback) { categories[categoryID].updateCallback = null; callback(categoryID); } break; } case "message": { for (const callback in messageCallbacks) { messageCallbacks[callback](data[1]); } } } break; } // Ping case 2: { socket.send("3"); break; } // Things I don't care about // More authentication messages case 40: break; default: { console.error(`Got sent unknown type ${type}\nMessage contents: ${rawMessage}`); break; } } }); }); } };