UNPKG

mubot-server

Version:
556 lines (553 loc) 26.5 kB
// Description: // An https server for hubot // // Dependencies: // This package is specific to Mubot, a custom Hubot implementation to manually install this package directly to Hubot // you must install express 4 and update bot.coffee accordingly (this is done automatically with Mubot). // // Commands: // none // // Author: // leathan // 'use strict'; // INIT (function(){ const log = process.env.SERVER_LOG_LEVEL; // Wether or not to display debugging info const path = require('path'); // Used for cross platform file system support. const querystring = require('querystring'); // Handy for parsing urls. const favicon = require('serve-favicon') ; // Favicon middlerware for express. // BITMARK SPECIFIC INIT const express = require('express'); // Used for setting up static directories. const randomstring = require('randomstring'); // Used for cookie generation. const exec = require('child_process').exec; // Used to access bitmark-cli. const fs = require('fs'); // Used to copy over the users default img. const ROOT = path.join(__dirname, '/../'); // Server root path. const LOGS_ROOT = ROOT + 'logs/'; // Bitmark root path. const BITMARK_ROOT = ROOT + 'bitmark/'; // Bitmark root path. const API_ROOT = ROOT + 'api/'; // API root path. const mongoose = require('mongoose'); // Our database. const multer = require('multer'); // Express file upload middleware. const request = require('request'); const USER_SOCKETS = {}; // // Will simplify later, sets up the uploaded files name and path multer middleware. const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, BITMARK_ROOT + 'uploads/'), filename: (req, file, cb) => { Users.findOne({'login-cookie': req.headers.cookie.split('login-cookie=')[1].split(';')[0] }, (err, user) => { // if(err) return next(err); // ---------------^-----------------------^-^------------- cb(null, user.username) }) } }); // // BITMARK DATABASE CONNECT mongoose.Promise = global.Promise; mongoose.connect('mongodb://localhost/bitmark-api', { useMongoClient: true }) .then(() => log && console.log('[MONGOOSE] Connection succesful.')) .catch(err => console.log('[MONGOOSE ERROR] ' + err)); // Set up database. const TransactionsSchema = new mongoose.Schema({ 'txid': String, 'amount': Number, 'address': String, 'type': String, 'date': { type: Date, default: Date.now } }); const PmsSchema = new mongoose.Schema({ 'sender': String, 'receiver': String, 'ismarked': Number, 'message': String, 'date': { type: Date, default: Date.now } }); const PostsSchema = new mongoose.Schema({ 'username': String, 'marks': Number, 'replyto': String, 'replyto_user': String, 'ismarking': Number, 'message': String, 'date': { type: Date, default: Date.now } }); const UsersSchema = new mongoose.Schema({ 'username': String, 'login-cookie': String, 'password': String, 'wallet': String, 'balance': Number, 'reputation': Number, 'notifications': Array, 'pms': Array, 'awaitingPms': Object, 'date': { type: Date, default: Date.now } }); // Beautiful hack to allow hotreloading when models already exists. const Trans = mongoose.models.Transactions || mongoose.model('Transactions', TransactionsSchema); const Users = mongoose.models.Users || mongoose.model('Users', UsersSchema); const Posts = mongoose.models.Posts || mongoose.model('Posts', PostsSchema); const Pms = mongoose.models.Pms || mongoose.model('Pms', PmsSchema); // // MAIN module.exports = bot => { const io = bot.io.of('/bitmark'); // Server wide FavIcon bot.router.use(favicon(ROOT + 'public/favicon.ico')); bot.router.use('/', express.static(ROOT + 'public')); bot.router.use('/profile', express.static(ROOT + 'profile')); bot.router.use('/logs', express.static(LOGS_ROOT + 'public')); bot.router.get('/logs', (req, res) => res.sendFile(LOGS_ROOT + 'logs.html')); bot.router.get('/logs/:server', (req, res) => res.sendFile(LOGS_ROOT + 'logs.html')); bot.router.get('/walletnotify/:coin/:txid', (req, res) => { exec(`bitmarkd getrawtransaction ${req.params.txid} 1`, (err, trans) => { try { trans = JSON.parse(trans.trim().replace(/(\n|\\)/g,'')); if(!trans.confirmations) return; // vout is a transaction output, we dont know which one is ours so we check them all. for(let i = 0, l = trans.vout.length; i < l; ++i) { // See if this output corresponds to a bitmark-api user. Users.findOne({wallet: trans.vout[i].scriptPubKey.addresses[0]}, (err, user) => { if(!user) return; Trans.findOne({txid: req.params.txid}, (err, transFound) => { if(err) return; if(!transFound) { let amount = parseInt(trans.vout[i].value * 1000); Trans.create({txid: req.params.txid, address: trans.vout[i].scriptPubKey.addresses[0], amount: amount, type: 'deposit'}, (err, newTran) => { if(!newTran) return; log && console.log('transaction update for addr ' + trans.vout[i].scriptPubKey.addresses[0]); Users.findOneAndUpdate({wallet: trans.vout[i].scriptPubKey.addresses[0]}, {$inc: { balance: amount }}, (err, user) => { if(!user) return; log && console.log('deposit update for ' + user.username); if(usersOnline[u.username]) { io.emit('deposit', {username: user.username, amount: amount, txid: req.params.txid}); usersOnline[user.username].balance += amount } }) }) } }) }) if (index = bot.brain.data.keys['_' + req.params.coin].indexOf(trans.vout[i].scriptPubKey.addresses[0]) >= 0) { user = bot.brain.data.keys[bot.brain.data.keys['_'+req.params.coin][index+1]]; if(user[req.params.coin].txids.includes(req.params.txid)) break; else { user[req.params.coin].txids.push(req.params.txid); user[req.params.coin].balance += parseInt(trans.vout[i].value * 1000); log && console.log('Wallet notify script has just updated a users balance.') } } } } catch(e) { res.end(1); console.log('Critical error parsing transaction ' + e) } }); res.end('Ok.') }); // Post to discord. bot.router.get('/discord', (req, res) => { res.sendFile(ROOT + 'public/discord.html') }); bot.router.post('/discord', (req, res) => { bot.messageRoom(req.body.channel, req.body.comment); res.end('Your message has been sent.') }); // Crypto API -> ATM Redirect main page here. bot.router.use('/api', express.static(API_ROOT + 'public')); bot.router.get(['/api/gambling', '/api/crypto'], (req, res) => { res.sendFile(API_ROOT + 'api.html') }); bot.router.get('/api', (req, res) => { var query = querystring.parse(req._parsedUrl.query); if (query) { bot.emit('CryptoRequest', query, res) } else { res.sendFile(API_ROOT + 'api.html') } }); bot.router.get('/ip', (req, res) => { res.writeHead(200, {'Content-Type': 'text/plain'}); var ip; if (req.headers['x-forwarded-for']) { ip = req.headers['x-forwarded-for'].split(',')[0] } else if (req.connection && req.connection.remoteAddress) { ip = req.connection.remoteAddress } else { ip = req.ip } if(ip.toString().includes('127.0.0.1')) res.end('Hi leathan'); else res.end(/(\d{1,3}\..*)/.exec(ip)[1] || 'Idk') }); // BITMARK bot.router.use('/bitmark', express.static(BITMARK_ROOT + 'public')); bot.router.use('/uploads', express.static(BITMARK_ROOT + 'uploads')); bot.router.post('/bitmark/uploads', multer({ storage: storage }).single('upl'), (req,res) => { res.sendFile(BITMARK_ROOT + 'bitmark.html') }); bot.router.get('/bitmark', (req, res, next) => { res.sendFile(BITMARK_ROOT + 'bitmark.html') }); /* GET /wallet */ bot.router.get('/bitmark/wallet', (req, res, next) => { res.cookie('state', 'wallet').sendFile(BITMARK_ROOT + 'bitmark.html') }); /* GET /profile */ bot.router.get('/bitmark/profile', (req, res, next) => { res.cookie('state', 'profile').sendFile(BITMARK_ROOT + 'bitmark.html') }); /* GET /users/:username */ bot.router.get('/bitmark/users/:username', (req, res, next) => { Users.findOne({username: new RegExp('^' + req.params.username + '$', 'i') }, (err, user) => { if(err) return next(err); if(user) res.send('This is ' + user.username + "'s profile. <br> They have " + user.balance + ' marks.'); else res.send('That user does not exist.'); }) }); /* POST /users/login */ bot.router.post('/bitmark/users/login', (req, res, next) => { var login_cookie = randomstring.generate(); Users.findOneAndUpdate({'username': new RegExp('^' + req.body.username + '$', 'i'), 'password': req.body.password }, {'login-cookie': login_cookie}, (err, user) => { if(err) return next(err); // Set their login cookie. if(user) res.cookie('login-cookie', login_cookie).end(0); else res.end('ERROR') }) }); /* POST /users/createAccount */ bot.router.post('/bitmark/users/createAccount', (req, res, next) => { if(/^_|[^a-zA-Z0-9_]/.test(req.body.username)) return res.end({error: 'Illegally formatted name'}); Users.findOne({username: req.body.username }, (err, user) => { if(err) return next(err); if(user) res.json({error: 'Username already exists.'}); else { exec('bitmarkd getnewaddress', (error, stdout) => { req.body.wallet = stdout.trim(); req.body.balance = 10; // This is a temporary balance that im funding. req.body.reputation = 0; // For future use. req.body.pms = []; req.body.awaitingPms = []; Users.create(req.body, (err, post) => { fs.createReadStream(BITMARK_ROOT + 'public/images/default-user-image.png').pipe(fs.createWriteStream(BITMARK_ROOT + 'uploads/' + req.body.username)); if(err) return next(err); res.json(post) }) }) } }) }); bot.router.get('/bitmark/post/:id', (req, res, next) => { res.cookie('state', req.params.id).sendFile(BITMARK_ROOT + '/bitmark.html') // to add expire date on the cookie use. // res.cookie("state", req.params.id, { expires: new Date(Date.now() + 9000000) }).sendFile(BITMARK_ROOT + '/bitmark.html') }); // API // To delete databases use. // bot.router.get('/bitmark/reset', (req, res, next) => { // Posts.deleteMany({},()=>{}); // Users.deleteMany({},()=>{}); // res.json({}) // }); // to delete users notifications // bot.router.get('/bitmark/reset', (req, res, next) => { // lets just hope no one reads this untill i remove it. // Users.findOneAndUpdate({ username: 'leathan' }, { $set: { notifications: [] } }, (err, user) => { // res.json({}) // }) // }); bot.router.get('/bitmark/api/posts', (req, res, next) => { Posts.find({message: {$ne: ""}}).sort({date: -1}).exec((err, posts) => res.json(posts || {})) }); bot.router.get('/bitmark/api/replies', (req, res, next) => { Posts.find({ 'replyto': { $exists: true } }, (err, replies) => res.json(replies || {})) }); bot.router.get('/bitmark/api/marks', (req, res, next) => { Posts.find({ 'ismarking': { $exists: true } }, (err, markings) => res.json(markings || {})) }); bot.router.get('/bitmark/api/post/:id', (req, res, next) => { Posts.findById(req.params.id, (err, post) => { res.json(post || {}) }) }); bot.router.get('/bitmark/api/marks_received/:username', (req, res, next) => { Post.find({ 'ismarking': { $exists: true }, 'replyto_user': req.params.username }, (err, markings) => res.json(markings || {})) }); bot.router.get('/bitmark/api/marks_given/:username', (req, res, next) => { Post.find({ 'username': req.params.username, 'ismarking': { $exists: true } }, (err, markings) => res.json(markings || {})) }); bot.router.get('/bitmark/api/info/:username', (req, res, next) => { Users.findOne({username: new RegExp('^' + req.params.username + '$', 'i') }, // Don't return the sensitive information via ajax response {'awaitingPms': false, 'password': false, 'login-cookie': false, 'notifications': false, '_id': false, 'pms': false}, (err, user) => { res.json(user || {error: "User doesn't exist"}) }) }); // API - Cookie needed bot.router.get('/bitmark/api/info', (req, res, next) => { // If they are not logged in send a stub. if(!req.cookies || req.cookies['login-cookie'] === 'logged-out' || !req.cookies['login-cookie']) { return res.json( {'username':'Guest','wallet':'','balance':0,'reputation':0,'notifications':[],'pms':[]}) } Users.findOne({'login-cookie': req.cookies['login-cookie']}, {'password': false, 'login-cookie':false}, (err, user) => { res.json(user || {'username':'Guest','wallet':'','balance':0,'reputation':0,'notifications':[],'pms':[]}) }) }); bot.router.post('/bitmark/api/mark', (req, res, next) => { if(!req.cookies || !req.cookies['login-cookie'] || req.cookies['login-cookie'] === 'logged-out') return res.json({}); Posts.findById(req.body._id, function (err, marked_post) { if(!marked_post) return res.json({}); // Find user if balance > 0 and subtract marking amount (1 always for now) Users.findOneAndUpdate({'login-cookie': req.cookies['login-cookie'], 'balance': {$gt: 0} }, { $inc: { 'balance': -1 } }, (err, marking_user) => { if(!marking_user) return res.json({error: 'finding marking user or decrementing their balance'}); createNotification(marked_post); // Increment the in memory value as well. usersOnline[marked_post.username] && ++usersOnline[marked_post.username].balance; usersOnline[marking_user.username] && --usersOnline[marking_user.username].balance; // Add balance to receiver Users.findOneAndUpdate({ 'username': marked_post.username }, { $inc: { 'balance': 1 } }, ()=> { // Increment the in memory value as well. usersOnline[marked_post.username] && ++usersOnline[marked_post.username].balance }); // MARK POST Posts.findByIdAndUpdate( marked_post._id , { $inc: { 'marks' : 1 } }, _=>0); Posts.create({'replyto_user': marked_post.username, 'replyto': marked_post._id, 'username': marking_user.username, 'ismarking': 1, 'message': req.body.marking_msg, 'marks': 0 }, (err, marking) => { if(!marking) return res.json({}); io.emit('new post', marking); res.json(marking) }) }) }) }); bot.router.get('/bitmark/api/delete_notification/:id', (req, res, next) => { if(!req.cookies || req.cookies['login-cookie'] === 'logged-out' || !req.cookies['login-cookie']) return res.json({}); Users.findOneAndUpdate({'login-cookie': req.cookies['login-cookie']}, { $pull: { 'notifications': {'id': req.params.id } } }, (err, user) => { res.json(user || {}) }) }); bot.router.post('/bitmark/api/create', (req, res, next) => { if(!req.cookies || req.cookies['login-cookie'] === 'logged-out' || !req.cookies['login-cookie']) return res.json({}); // Post text has nothing or just whitespace, dont continue. req.body.message = req.body.message.trim(); if(!req.body.message.length) return res.json({}); Users.findOne({'login-cookie': req.cookies['login-cookie']}, (err, creating_user) => { if(!creating_user) { return res.json({error: 'Not logged in.'}); return; } // NOT LOGGED IN req.body.marks = 0; req.body.username = creating_user.username; if(req.body.replyto) { Posts.findById(req.body.replyto, (err, post) => { req.body.replyto_user = post.username; createPost(req.body); // Dont create notification is user is replying to themselves if(post.username !== creating_user.username) createNotification(post) }) } else createPost(req.body); function createPost(post) { Posts.create(post, (err, created_post) => { if(!created_post) { console.log('Critical error creating post'); return res.end({}) } io.emit('new post', created_post); res.json(created_post) }) } }) }); function logOut(user) { Users.findOneAndUpdate({'username': user }, { 'login-cookie': 'logged-out' }, (err, user) => { delete logged[user['login-cookie']] }); USER_SOCKETS[user].broadcast.emit('user logged out', user); delete usersOnline[user]; delete USER_SOCKETS[user] } // Save every users socket, by user. const usersOnline = {}, logged = {}; function isLoggedIn(socket, cb) { if(socket.handshake.headers.cookie) { let cookie; if(cookie = /login-cookie=(\w{32})/.exec(socket.handshake.headers.cookie)) cookie = cookie[1]; else return cb(false); if(logged[cookie]) { USER_SOCKETS[logged[cookie]] = socket; return cb(logged[cookie]) } // If this is our first time seeing the user, query database and build shortcut hashes. Users.findOne({ 'login-cookie': cookie }, (err, user) => { if(err) throw new Error('Error @ Users.findOne({ "login-cookie": cookie }, (err, user) => {'); if(!user) return cb(false); // Bad cookie. let cleanedUser = { balance: user.balance, username: user.username, // make sure sensitive info isn't sent. reputation: user.reputation, status: 'online', address: user.wallet, updated_at: user.updated_at }; io.emit('user logged in', cleanedUser); logged[cookie] = user.username; usersOnline[user.username] = cleanedUser; // shortcut hashes USER_SOCKETS[user.username] = socket; cb(user.username) }) } else { // Guest logged in/out. cb(false) } } io.on('connection', socket => { isLoggedIn(socket, username => { if(!username) return; // Its a guest. // When creating a callback using socket.io you must pass in a data obj as first param. socket.on('transactions', (_, cb) => { Trans.find({address: usersOnline[username].address}, (err, trans) => cb(trans.reverse() || false)) }); socket.on('withdraw', data => { if(usersOnline[username].balance < data.amount) return; exec('bitmarkd sendtoaddress ' + data.toAddress + ' ' + (data.amount / 1000), (err, txid) => { if(err || !txid) return console.log('Critical error withdrawing to ' + username + ' addr: ' + usersOnline[username].address); Trans.create({txid, address: usersOnline[username].address, amount: data.amount, type: 'withdraw'}, (err, tran) => { console.log('Withdrawel for ' + username + ' created.') }) Users.findOneAndUpdate({username: username}, { $inc: { balance: -data.amount } }, (err, user) => { if(!user) return console.log('Critical error processing withdrawel for ' + username + ' addr: ' + usersOnline[username].address); console.log('withdrawel for ' + username + ' processed.'); io.emit('withdraw', {username, txid, amount: data.amount}); usersOnline[username].balance -= data.amount }) }) }); socket.on('status update', status => { // ignore duplicates. console.log(username) if(status === usersOnline[username].status) return; // This occures when the window gets unfocused. if(status === 'recently-away' && usersOnline[username].status === 'away') return; usersOnline[username].status = status; socket.broadcast.emit('status update', {username, status}) }); socket.on('log out', () => { logOut(username); socket.broadcast.emit('user logged out', username) }) socket.on('private message open', pm => { if(pm.maximized) pmRead(username, pm); // dont log history for now.. delete pm.history; delete pm.text; if(pm.exists) delete pm.exists; else Users.findOneAndUpdate({ 'username': username }, { $push: { 'pms': pm } }, () => { log && console.log(pm.username + "'s private message opened or loaded by " + username + '.') }); getPms(username, pm.username, pms => socket.emit('private message data', pms)) }) socket.on('mark private message', (pm, cb) => markPm(username, pm, cb)); socket.on('new private message', pm => { if(USER_SOCKETS[pm.username]) USER_SOCKETS[pm.username].emit('private_messages typing', { user: username, finished: true }); createPm(username, pm, processedPm => { socket.emit('new private message', processedPm); if(USER_SOCKETS[processedPm.receiver]) { USER_SOCKETS[processedPm.receiver].emit('new private message', processedPm) } }) }); socket.on('private message toggle', pm => { if(pm.maximized) pmRead(username, pm); delete pm.history; delete pm.text; Users.findOneAndUpdate({ 'username': username, 'pms.username': pm.username }, { $set: { 'pms.$': pm } }, (err, user) => { log && console.log(pm.username + "'s private message toggled by " + username + '. PM.MAXIMIZED: ' + pm.maximized) }) }); socket.on('private message close', pm => { Users.findOneAndUpdate({ 'username': username }, { $pull: { pms: { 'username': pm.username } } }, (err, user) => { log && console.log(pm.username + "'s private message closed by " + username + '.') }) }); socket.on('private_messages typing', data => { if(USER_SOCKETS[data.user]) USER_SOCKETS[data.user].emit('private_messages typing', {user: username}) }) socket.on('private message read', pm => pmRead(username, pm)) }); socket.on('users online', (_, cb) => cb(usersOnline)); }); function pmRead(username, pm) { Users.findOneAndUpdate({ 'username': username }, { $unset: {['awaitingPms.' + pm.username]: 1} }, (err, user) => { log && console.log(pm.username + "'s private message read by " + username + '.') }) } function getPms(user, withUser, cb, amount) { amount = amount || 100; Pms.find({ $or:[ {sender: user, receiver: withUser}, {sender: withUser, receiver: user} ]}).sort({'date': -1}).limit(amount).exec((err, pms) => { log && console.log(withUser + "'s private messages loaded by " + user + '.'); cb(pms.reverse() || {}) }) } function markPm(user, pm, cb) { if(pm.ismarked || user === pm.sender || usersOnline[user].balance < 1) return cb(false); Pms.findOneAndUpdate(pm, {ismarked: 1}, (err, pm) => { if(!pm) return cb(false); // Here we increment/decrement the database balances, plus the in memory values. Users.findOneAndUpdate({ 'username': user }, { $inc: { 'balance': -1 } }, ()=> usersOnline[user] && --usersOnline[user].balance); Users.findOneAndUpdate({ 'username': pm.sender }, { $inc: { 'balance': 1 } }, ()=> usersOnline[pm.sender] && ++usersOnline[pm.sender].balance); Posts.create({'replyto': pm._id.toString(), 'replyto_user': pm.sender, 'username': user, 'ismarking': 1, 'marks': 0 }, (err, marking) => io.emit('new post', marking || {})); cb(!!pm) }) } function createPm(sender, pmData, cb) { var pm = {sender: sender, receiver: pmData.username, message: pmData.message}; Pms.create(pm, (err, pm) => { if(!pm) return !console.log('Critical error creating pm.'); // Only add another notification if this pm isnt posted the same // minute as the pm before (thats what .sameMin indicates) if(!pmData.sameMin) { if(!usersOnline[pmData.username]) { Users.findOneAndUpdate({ username: pmData.username }, { $inc: {['awaitingPms.' + sender]: 1} }, (err, user) => { if(!user) console.log(pmData.username + ' ONE is not awaiting ' + sender + "'s pm because he has that window open."); else console.log(sender + "'s pm is awaited by " + pmData.username + '.') }) } else { Users.findOneAndUpdate({ username: pmData.username, $or: [ { 'pms.username': { $not: new RegExp('^'+sender+'$', 'i') } }, { $and: [ { 'pms.username': sender }, { 'pms.maximized': false } ] } ] }, { $inc: {['awaitingPms.' + sender]: 1} }, (err, user) => { if(!user) console.log(pmData.username + ' is not awaiting ' + sender + "'s pm because he has that window open."); else console.log(sender + "'s pm is awaited by " + pmData.username + '.') }) } } cb(pm || {}) }) } function createNotification(post) { notification = { id: post._id.toString(), // Amount of notifications per post, defaults to 1. amount: 1 }; Users.findOne({'username': post.username}, (err, user) => { // Iterator for notifications. var i = 0; for(let l = user.notifications.length; i < l; i++) { let n = user.notifications[i]; // This user already has notification for this post so increment the amount. if (n.id === notification.id) { notification.amount += n.amount; break } } // A post has receiced anther notification. if (notification.amount > 1) { // The dot notation here is mongoose specific meaning notifications[found]. Its NOT a hash obj. Users.findOneAndUpdate({'username': post.username}, {$set: {['notifications.' + i]: notification} }, _=>0) } // New post notification else { Users.findOneAndUpdate({'username': post.username}, { $push: { 'notifications': notification } }, _=>0) } }) } // catch 404 not found bot.router.use((req, res, next) => { res.end('<h1>Not Found Dawg</h1>'); }); // error handler bot.router.use((err, req, res, next) => { res.status(err.status || 500); res.end('<h1>Weird Error Yo</h1>') }) }; }).call(this);