mubot-server
Version:
A server for mubot
556 lines (553 loc) • 26.5 kB
JavaScript
// 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
//
;
// 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);