Initial commit

This commit is contained in:
Akinwale Ariwodola 2017-09-23 17:05:00 +01:00
commit a408b4e9d3
18 changed files with 1676 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
config/config.js
node_modules
token

0
README.md Normal file
View file

809
app.js Normal file
View file

@ -0,0 +1,809 @@
const async = require('async');
const base58 = require('bs58check');
const config = require('./config/config');
const fs = require('fs');
const moment = require('moment');
const mysql = require('mysql');
const request = require('request');
const util = require('util');
if (config.debug) {
require('request-debug')(request);
}
// URLS
const howToUseUrl = 'https://reddit.com/r/lbry';
const baseUrl = 'https://oauth.reddit.com';
const rateUrl = 'https://api.lbry.io/lbc/exchange_rate';
const tokenUrlFormat = 'https://%s:%s@www.reddit.com/api/v1/access_token';
const txBaseUrl = 'https://explorer.lbry.io/tx';
// Other globals
const userAgent = 'lbryian/1.0.0 Node.js (by /u/lbryian)';
const commentKind = 't1';
const privateMessageKind = 't4';
let globalAccessToken;
let accessTokenTime;
// Load message templates
const messageTemplates = {};
const templateNames = [
'onbalance',
'ondeposit',
'onsendtip',
'onsendtip.insufficientfunds',
'onsendtip.invalidamount',
'onwithdraw',
'onwithdraw.amountltefee',
'onwithdraw.insufficientfunds',
'onwithdraw.invalidaddress',
'onwithdraw.invalidamount'
];
for (let i = 0; i < templateNames.length; i++) {
const name = templateNames[i];
messageTemplates[name] = fs.readFileSync(`templates/${name}.txt`, { encoding: 'utf8' });
}
// Connect to the database
let db;
const initSqlConnection = () => {
const _db = mysql.createConnection({
host: config.mariadb.host,
user: config.mariadb.username,
password: config.mariadb.password,
database: config.mariadb.database,
charset: 'utf8mb4',
timezone: 'Z'
});
_db.on('error', (err) => {
if (err.code === 2006 || ['PROTOCOL_CONNECTION_LOST', 'PROTOCOL_PACKETS_OUT_OF_ORDER', 'PROTOCOL_ENQUEUE_AFTER_FATAL_ERROR'].indexOf(err.code) > -1) {
_db.destroy();
db = initSqlConnection();
}
});
return _db;
};
db = initSqlConnection();
const loadAccessToken = (callback) => {
if (fs.existsSync(config.accessTokenPath)) {
const token = fs.readFileSync(config.accessTokenPath, { encoding: 'utf8' });
return callback(null, String(token));
}
return callback(null, null);
};
const oauth = (callback) => {
const url = util.format(tokenUrlFormat, config.clientId, config.clientSecret);
request.post(url, { form: { grant_type: 'password', username: config.username, password: config.password} }, (err, res, body) => {
if (err) {
return callback(err, null);
}
let accessToken = null;
try {
const response = JSON.parse(body);
accessToken = response.access_token;
accessTokenTime = moment();
if (accessToken && accessToken.trim().length > 0) {
fs.writeFileSync(config.accessTokenPath, accessToken);
}
} catch (e) {
return callback(e, null);
}
return callback(null, accessToken);
});
};
const retrieveUnreadMessages = (accessToken, callback) => {
const url = util.format('%s/message/unread?limit=100', baseUrl);
request.get({ url: url, headers: { 'User-Agent': 'lbryian/1.0.0 Node.js (by /u/lbryian)', 'Authorization': 'Bearer ' + accessToken } }, (err, res, body) => {
if (err) {
console.log(err);
return callback(err);
}
let response;
try {
response = JSON.parse(body);
} catch (e) {
return callback(e, null);
}
return callback(null, response.data.children);
});
};
const createOrGetUserId = (username, callback) => {
async.waterfall([
(cb) => {
db.query('SELECT Id FROM Users WHERE LOWER(Username) = ?', [username.toLowerCase()], cb);
},
(res, fields, cb) => {
if (res.length === 0) {
// user does not exist, create the user
return cb(null, 0);
}
return cb(null, res[0].Id);
},
(userId, cb) => {
if (userId === 0) {
return db.query('INSERT INTO Users (Username, Created) VALUES (?, UTC_TIMESTAMP())', [username], (err, res) => {
if (err) {
console.log(err);
return cb(err, null);
}
return cb(null, res.insertId);
});
}
return cb(null, userId);
}
], callback);
};
const getBalance = (userId, callback) => {
db.query('SELECT Balance FROM Users WHERE Id = ?', [userId], (err, res) => {
if (err) {
return callback(err, null);
}
return callback(0, res.length === 0 ? 0 : res[0].Balance);
});
};
const generateDepositAddress = (callback) => {
request.post({ url: config.lbrycrd.rpcurl, json: { method: 'getnewaddress', params: [config.lbrycrd.account] } }, (err, resp, body) => {
if (err || body.error) {
return callback(err || body.error, null);
}
return callback(null, body.result);
});
};
const getDepositAddress = (userId, callback) => {
let newAddress = false;
async.waterfall([
(cb) => {
db.query('SELECT DepositAddress FROM Users WHERE Id = ?', [userId], cb);
},
(res, fields, cb) => {
const address = res.length > 0 ? res[0].DepositAddress : null;
if (!address || address.trim().length === 0) {
newAddress = true;
return generateDepositAddress(cb);
}
return cb(null, address);
},
(address, cb) => {
if (newAddress) {
return db.query('UPDATE Users SET DepositAddress = ? WHERE Id = ?', [address, userId], (err) => {
if (err) {
return cb(err, null);
}
return cb(null, address);
});
}
return cb(null, address);
}
], callback);
};
const sendTip = (sender, recipient, amount, tipdata, callback) => {
console.log(`sending ${amount} LBC from ${sender} to ${recipient}`);
const data = {};
async.waterfall([
(cb) => {
// Start DB transaction
db.beginTransaction((err) => {
if (err) {
return cb(err, null);
}
return cb(null, true);
});
},
(started, cb) => {
// start a transaction
// check the sender's balance
createOrGetUserId(sender, cb);
},
(senderId, cb) => {
data.senderId = senderId;
getBalance(senderId, cb);
},
(senderBalance, cb) => {
// balance is less than amount to tip, or the difference after sending the tip is negative
if (senderBalance < amount || (senderBalance - amount) < 0) {
return sendPMUsingTemplate('onsendtip.insufficientfunds', { how_to_use_url: howToUseUrl }, message.data.author, () => {
cb(new Error('Insufficient funds'), null);
});
}
return db.query('UPDATE Users SET Balance = Balance - ? WHERE Id = ?', [amount, data.senderId], cb);
},
(res, fields, cb) => {
// Update the recipient's balance
createOrGetUserId(recipient, cb);
},
(recipientId, cb) => {
data.recipientId = recipientId;
db.query('UPDATE Users SET Balance = Balance + ? WHERE Id = ?', [amount, recipientId], cb);
},
(res, fields, cb) => {
// save the message
const msgdata = tipdata.message.data;
db.query( ['INSERT INTO Messages (AuthorId, Type, FullId, RedditId, ParentRedditId, Subreddit, Body, Context, RedditCreated, Created) ',
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP())'].join(''),
[data.senderId,
tipdata.message.kind === privateMessageKind ? 1 : 2,
msgdata.name,
msgdata.id,
msgdata.parent_id,
msgdata.subreddit,
msgdata.body,
msgdata.context,
moment.utc(msgdata.created_utc * 1000).format('YYYY-MM-DD HH:mm:ss')
], cb);
},
(res, fields, cb) => {
console.log('Inserting tip.');
// save the tip information
db.query( ['INSERT INTO Tips (MessageId, SenderId, RecipientId, Amount, AmountUsd, ParsedAmount, Created) ',
'VALUES (?, ?, ?, ?, ?, ?, UTC_TIMESTAMP())'].join(''),
[res.insertId,
data.senderId,
data.recipientId,
amount,
tipdata.amountUsd,
tipdata.parsedAmount,
], cb);
},
(res, fields, cb) => {
// reply to the source message with message template after successful commit
replyMessageUsingTemplate('onsendtip', { recipient: `u/${recipient}`, tip: `${amount} LBC ($${tipdata.amountUsd})`, how_to_use_url: howToUseUrl},
tipdata.message.data.name, cb);
},
(success, cb) => {
// Mark the message as read
markMessageRead(tipdata.message.data.name, cb);
},
(success, cb) => {
// commit the transaction
db.commit((err) => {
if (err) {
return cb(err, null);
}
return cb(null, true);
});
}
], (err) => {
if (err) {
console.log(err);
return db.rollback(() => {
callback(err, null);
});
}
// success
return callback(null, true);
});
};
const convertUsdToLbc = (amount, callback) => {
request.get({ url: rateUrl }, (err, res, body) => {
let response;
try {
response = JSON.parse(body);
} catch (e) {
return callback(e, null);
}
if (!response.data || !response.data.lbc_usd) {
return callback(new Error('Could not retrieve the LBC/USD conversion rate.'));
}
const rateUsd = parseFloat(response.data.lbc_usd);
if (isNaN(rateUsd) || rateUsd === 0) {
return callback(new Error('Invalid LBC/USD rate retrieved.'));
}
const amountLbc = (amount / rateUsd).toFixed(8);
return callback(null, amountLbc);
});
};
const convertLbcToUsd = (amount, callback) => {
request.get({ url: rateUrl }, (err, res, body) => {
let response;
try {
response = JSON.parse(body);
} catch (e) {
return callback(e, null);
}
if (!response.data || !response.data.lbc_usd) {
return callback(new Error('Could not retrieve the LBC/USD conversion rate.'));
}
const rateUsd = parseFloat(response.data.lbc_usd);
if (isNaN(rateUsd) || rateUsd === 0) {
return callback(new Error('Invalid LBC/USD rate retrieved.'));
}
const amountLbc = (amount * rateUsd).toFixed(2);
return callback(null, amountLbc);
});
};
const markMessageRead = (messageFullId, callback) => {
const url = `${baseUrl}/api/read_message`;
request.post({ url, form: { id: messageFullId }, headers: { 'User-Agent': userAgent, 'Authorization': 'Bearer ' + globalAccessToken } }, (err, res, body) => {
if (err) {
return callback(err, null);
}
let response;
try {
response = JSON.parse(body);
} catch (e) {
return callback(e, null);
}
// success
return callback(null, true);
});
};
const sendPMUsingTemplate = (template, substitions, subject, recipient, callback) => {
if (!messageTemplates[template]) {
return callback(new Error(`Message template ${template} not found.`));
}
let messageText = messageTemplates[template];
console.log(messageText);
for (let variable in substitutions) {
if (substitutions.hasOwnProperty(variable)) {
const re = new RegExp(['{', variable, '}'].join(''), 'ig');
messageText = messageText.replace(re, substitutions[variable]);
}
}
// send the message
const url = `${baseUrl}/api/compose`;
request.post({
url,
form: { api_type: 'json', text: messageText, subject, to: recipient },
headers: { 'User-Agent': userAgent, 'Authorization': 'Bearer ' + globalAccessToken }
}, (err, res, body) => {
if (err) {
return callback(err, null);
}
let response;
try {
response = JSON.parse(body);
} catch (e) {
return callback(e, null);
}
if (response.json.ratelimit > 0 ||
response.json.errors.length > 0) {
return callback(new Error('Rate limited.'), null);
}
// success
return callback(null, true);
});
};
const replyMessageUsingTemplate = (template, substitutions, sourceMessageFullId, callback) => {
if (!messageTemplates[template]) {
return callback(new Error(`Message template ${template} not found.`));
}
let messageText = messageTemplates[template];
for (let variable in substitutions) {
if (substitutions.hasOwnProperty(variable)) {
const re = new RegExp(['{', variable, '}'].join(''), 'ig');
messageText = messageText.replace(re, substitutions[variable]);
}
}
// send the message
const url = `${baseUrl}/api/comment`;
request.post({
url,
form: { api_type: 'json', text: messageText, thing_id: sourceMessageFullId },
headers: { 'User-Agent': userAgent, 'Authorization': 'Bearer ' + globalAccessToken }
}, (err, res, body) => {
if (err) {
return callback(err, null);
}
let response;
try {
response = JSON.parse(body);
} catch (e) {
return callback(e, null);
}
if (response.json.ratelimit > 0 ||
response.json.errors.length > 0) {
return callback(new Error('Rate limited.'), null);
}
// success
return callback(null, true);
});
};
const getMessageAuthor = (thingId, accessToken, callback) => {
const url = util.format('%s/api/info?id=%s', baseUrl, thingId);
request.get({ url: url, headers: { 'User-Agent': userAgent, 'Authorization': 'Bearer ' + globalAccessToken } }, (err, res, body) => {
if (err) {
return callback(err, null);
}
let response;
try {
response = JSON.parse(body);
} catch (e) {
return callback(e, null);
}
return callback(null, (response.data.children.length > 0) ? response.data.children[0].data.author : null);
});
};
const doSendTip = function(body, message, callback) {
/**
* accepted formats:
* 1 usd u/lbryian OR u/lbryian 1 usd
* 1 lbc u/lbryian OR u/lbryian 1 lbc
* $1 u/lbryian OR u/lbryian $1
*/
const parts = body.split(' ', 3);
const parentId = message.data.parent_id ? message.data.parent_id.trim() : null;
if ((!parentId || parentId.length === 0) || (parts.length === 0) || (parts.length !== 3 && (parts.length === 2 && parts[0].substring(0,1) !== '$'))) {
// ignore the comment
return callback(null, null);
}
if (parts[0] && parts[0].substring(0, 1) === '/') {
parts[0] = parts[0].substring(1);
}
let amountUsd = 0;
let amountLbc = 0;
const nameFirst = parts[0] === config.redditName;
if (parts.length === 2) {
// get the amount
amountUsd = parseFloat(parts[nameFirst ? 1 : 0].substring(1));
if (isNaN(amountUsd) || amountUsd <= 0) {
return sendPMUsingTemplate('onsendtip.invalidamount', { how_to_use_url: howToUseUrl }, message.data.author, () => {
callback(null, null);
});
}
} else if (parts.length === 3) {
const amount = parseFloat(parts[nameFirst ? 1 : 0]);
const unit = parts[nameFirst ? 2 : 1].toLowerCase();
if (isNaN(amount) || amount <= 0 || ['usd', 'lbc'].indexOf(unit) === -1) {
// invalid amount or unit
return callback(null, null);
}
if (unit === 'lbc') {
amountLbc = amount;
} else {
amountUsd = amount;
}
}
if (amountLbc > 0 || amountUsd > 0) {
const parsedAmount = (parts.length === 2) ? parts[nameFirst ? 1 : 0] : [parts[nameFirst ? 1 : 0], parts[nameFirst ? 2 : 1]].join(' ');
// get the author of the parent message
async.waterfall([
(cb) => {
getMessageAuthor(message.data.parent_id, globalAccessToken, cb);
},
(recipient, cb) => {
const sender = message.data.author;
if (sender !== recipient) {
return cb(null, { amountLbc, amountUsd, message, recipient, sender, parsedAmount });
}
return cb(null, null);
},
(tipdata, cb) => {
if (tipdata) {
if (tipdata.amountUsd > 0) {
return convertUsdToLbc(tipdata.amountUsd, (err, convertedAmount) => {
if (err) {
return cb(err);
}
tipdata.amountLbc = convertedAmount;
return cb(null, tipdata);
});
} else if (tipdata.amountLbc > 0 && (!tipdata.amountUsd || tipdata.amountUsd === 0)) {
return convertLbcToUsd(tipdata.amountLbc, (err, convertedAmount) => {
if (err) {
return cb(err);
}
tipdata.amountUsd = convertedAmount;
return cb(null, tipdata);
});
}
}
return cb(null, null);
},
(data, cb) => {
if (data) {
return sendTip(data.sender, data.recipient, data.amountLbc, data, cb);
}
return cb(null, null);
}
], callback);
}
};
const doSendBalance = (message, callback) => {
async.waterfall([
(cb) => {
createOrGetUserId(message.data.author, cb);
},
(authorId, cb) => {
getBalance(authorId, cb);
},
(balance, cb) => {
// send message with balance
replyMessageUsingTemplate('onbalance', { how_to_use_url: howToUseUrl, amount: balance }, message.data.name, cb);
},
(success, cb) => {
// mark messge as read
markMessageRead(message.data.name, cb);
}
], (err) => {
if (err) {
console.log(err);
return callback(err, null);
}
// success
return callback(null, true);
});
};
const sendLbcToAddress = (address, amount, callback) => {
request.post({ url: config.lbrycrd.rpcurl, json: { method: 'sendtoaddress', params: [address, amount] } }, (err, resp, body) => {
if (err || body.error) {
return callback(err || body.error, null);
}
return callback(null, body.result);
});
};
const doWithdrawal = (amount, address, message, callback) => {
const data = {};
async.waterfall([
(cb) => {
// Start DB transaction
db.beginTransaction((err) => {
if (err) {
return cb(err, null);
}
return cb(null, true);
});
},
// prevent withdrawal to deposit address
(started, cb) => {
createOrGetUserId(message.data.author, cb);
},
(authorId, cb) => {
data.userId = authorId;
getDepositAddress(authorId, cb);
},
(depositAddress, cb) => {
if (address === depositAddress) {
return cb(new Error('Attempt to withdraw to deposit address.'), null);
}
return getBalance(data.userId, cb);
},
(balance, cb) => {
// check sufficient balance
if (balance < amount || balance - amount < 0) {
return sendPMUsingTemplate('onwithdraw.insufficientfunds', { how_to_use_url: howToUseUrl }, message.data.author, () => {
cb(new Error('Insufficient funds'), null);
});
}
// Update the balance
db.query('UPDATE Users SET Balance = Balance - ? WHERE Id = ?', [amount, data.userId], cb);
},
(res, fields, cb) => {
// Send the transaction on the blockchain
sendLbcToAddress(address, amount, cb);
},
(txhash, cb) => {
data.txhash = txhash;
// Insert the withdrawal entry
db.query('INSERT INTO Withdrawals (UserId, TxHash, Amount, Created) VALUES (?, ?, ?, UTC_TIMESTAMP())', [data.userId, txhash, amount], cb);
},
(res, fields, cb) => {
// commit the transaction
db.commit((err) => {
if (err) {
return cb(err, null);
}
return cb(null, true);
});
},
(success, cb) => {
// mark messge as read
markMessageRead(message.data.name, cb);
},
(success, cb) => {
// send a reply
replyMessageUsingTemplate('onwithdraw', { how_to_use_url: howToUseUrl, address: address, amount: amount, txid: data.txhash }, message.data.name, cb);
}
], (err) => {
if (err) {
console.log(err);
return db.rollback(() => {
callback(err, null);
});
}
// success
return callback(null, true);
});
};
const doSendDepositAddress = (message, callback) => {
async.waterfall([
(cb) => {
createOrGetUserId(message.data.author, cb);
},
(authorId, cb) => {
getDepositAddress(authorId, cb);
},
(address, cb) => {
// send message with balance
replyMessageUsingTemplate('ondeposit', { how_to_use_url: howToUseUrl, address: address }, message.data.name, cb);
},
(success, cb) => {
// mark messge as read
markMessageRead(message.data.name, cb);
}
], (err) => {
if (err) {
return callback(err, null);
}
// success
return callback(null, true);
});
};
// Commands
// balance (PM)
// deposit (PM)
// tip (Comment): <amount> <unit> u/lbryian
// withdraw (PM): withdraw <amount> <address>
const processMessage = function(message, callback) {
if (!message.kind || !message.data) {
return callback(new Error('Invalid message specified for processing.'));
}
const body = String(message.data.body).trim();
if (message.kind === privateMessageKind) {
// balance, deposit or withdraw
// Check the command
if ('balance' === body.toLowerCase()) {
// do balance check
return doSendBalance(message, callback);
} else if ('deposit' === body.toLowerCase()) {
// send deposit address
return doSendDepositAddress(message, callback);
} else {
// withdrawal
const parts = body.split(' ');
if (parts.length !== 3 ||
parts[0].toLowerCase() !== 'withdraw') {
// invalid message, ignore
return callback(null, null);
}
const amount = parseFloat(parts[1]);
if (isNaN(amount) || amount < 0) {
// TODO: send a message that the withdrawal amount is invalid
return sendPMUsingTemplate('onwithdraw.invalidamount', { how_to_use_url: howToUseUrl }, message.data.author, () => {
callback(null, null);
});
}
if (amount <= config.lbrycrd.txfee) {
return sendPMUsingTemplate('onwithdraw.amountltefee', { how_to_use_url: howToUseUrl, amount: amount, fee: config.lbrycrd.txfee }, message.data.author, () => {
callback(null, null);
});
}
// base58 check the address
const address = parts[2];
try {
base58.decode(address);
} catch(e) {
return sendPMUsingTemplate('onwithdraw.invalidaddress', { how_to_use_url: howToUseUrl }, message.data.author, () => {
callback(null, null);
});
}
return doWithdrawal(amount, address, message, callback);
}
return callback(null, null);
}
if (message.kind === commentKind) {
doSendTip(body, message, callback);
}
};
// Run the bot
const runBot = () => {
async.waterfall([
(cb) => {
if (!accessTokenTime || moment.duration(moment().diff(accessTokenTime)).asMinutes() >= 55) {
// remove old or expired tokens
// TODO: Implement refreshToken
if (fs.existsSync(config.accessTokenPath)) {
fs.unlinkSync(config.accessTokenPath);
}
}
return cb(null);
},
(cb) => {
loadAccessToken(cb);
},
(token, cb) => {
if (!token || token.trim().length === 0) {
return oauth(cb);
}
return cb(null, token);
},
(token, cb) => {
globalAccessToken = token;
retrieveUnreadMessages(token, cb);
},
(unread, cb) => {
async.eachSeries(unread, (message, ecb) => {
processMessage(message, ecb);
}, cb);
}
], (err) => {
if (err) {
console.log(err);
}
// Wait 1 minute for next iteration
console.log('Waiting 1 minute...');
setTimeout(runBot, 60000);
});
};
runBot();

24
config/default.js Normal file
View file

@ -0,0 +1,24 @@
module.exports = {
debug: true,
accessTokenPath: 'token',
clientId: '<CLIENT_ID>',
clientSecret: '<CLIENT_SECRET>',
username: '<USERNAME>',
password: '<PASSWORD>',
// for handling tip comments
redditName: 'u/lbryian',
mariadb: {
host: 'localhost',
username: '<DB_USERNAME>',
password: '<DB_PASSWORD>',
database: '<DB_NAME>'
},
lbrycrd: {
account: 'tips',
rpcurl: 'http://127.0.0.1:9245',
txfee: 0.00002000
}
};

136
deposits.js Normal file
View file

@ -0,0 +1,136 @@
// Background tx processor for handling deposits and withdrawals
const async = require('async');
const config = require('./config/config');
const mysql = require('mysql');
const request = require('request');
if (config.debug) {
require('request-debug')(request);
}
// Connect to the database
let db;
const initSqlConnection = () => {
const _db = mysql.createConnection({
host: config.mariadb.host,
user: config.mariadb.username,
password: config.mariadb.password,
database: config.mariadb.database,
charset: 'utf8mb4',
timezone: 'Z'
});
_db.on('error', (err) => {
if (err.code === 2006 || ['PROTOCOL_CONNECTION_LOST', 'PROTOCOL_PACKETS_OUT_OF_ORDER', 'PROTOCOL_ENQUEUE_AFTER_FATAL_ERROR'].indexOf(err.code) > -1) {
_db.destroy();
db = initSqlConnection();
}
});
return _db;
};
db = initSqlConnection();
const userIdForDepositAddress = (address, callback) => {
db.query('SELECT Id FROM Users WHERE DepositAddress = ?', [address], (err, res) => {
if (err) {
return callback(err, null);
}
if (res.length === 0) {
return callback(new Error(`User with deposit address ${address} not found.`));
}
return callback(null, res[0].Id);
});
};
const createDeposit = (address, txhash, amount, confirmations, callback) => {
async.waterfall([
(cb) => {
userIdForDepositAddress(address, cb);
},
(depositorId, cb) => {
db.query('INSERT INTO Deposits (UserId, TxHash, Amount, Confirmations, Created) VALUES (?, ?, ?, ?, UTC_TIMESTAMP()) ON DUPLICATE KEY UPDATE Confirmations = ?',
[depositorId, txhash, amount, confirmations, confirmations], cb);
}
], callback);
};
const confirmationsForTx = (txhash, callback) => {
request.post({ url: config.lbrycrd.rpcurl, json: { method: 'gettransaction', params: [txhash] } }, (err, res, body) => {
if (body.error) {
return callback(body.error, null);
}
return callback(null, body.result.confirmations);
});
};
const processNewDeposits = (callback) => {
async.waterfall([
(cb) => {
request.post({ url: config.lbrycrd.rpcurl, json: { method: 'listtransactions', params: [config.lbrycrd.account, 1000] } }, cb);
},
(res, body, cb) => {
if (body.error) {
return cb(body.error, null);
}
// simply insert the deposits
return async.each(body.result, (tx, ecb) => {
if (tx.amount <= 0) {
return ecb(null, null);
}
return createDeposit(tx.address, tx.txid, tx.amount, tx.confirmations, ecb);
}, cb);
}
], callback);
};
// deposits with confirmations < 3
const processPendingDeposits = (callback) => {
async.waterfall([
(cb) => {
db.query('SELECT Id, TxHash FROM Deposits WHERE Confirmations < 3', cb);
},
(res, fields, cb) => {
if (res.length === 0) {
return cb(null, []);
}
return async.each(res, (deposit, ecb) => {
confirmationsForTx(deposit.TxHash, (err, confirmations) => {
if (err) {
return ecb(err, null);
}
db.query('UPDATE Deposits SET Confirmations = ? WHERE Id = ?', [confirmations, deposit.Id], ecb);
});
}, cb);
}
], callback);
};
const runProcess = () => {
async.waterfall([
(cb) => {
console.log('Processing new deposits.');
processNewDeposits(cb);
},
(cb) => {
console.log('Processing pending deposits.');
processPendingDeposits(cb);
}
], (err) => {
if (err) {
console.log('Error occurred.');
console.log(err);
}
// run again in 1 minute
console.log('Waiting 1 minute...');
setTimeout(runProcess, 60000);
});
};
runProcess();

553
package-lock.json generated Normal file
View file

@ -0,0 +1,553 @@
{
"name": "lbry-social-tipbot",
"version": "0.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"ajv": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.2.tgz",
"integrity": "sha1-R8aNaehvXZUxA7AHSpQw3GPaXjk=",
"requires": {
"co": "4.6.0",
"fast-deep-equal": "1.0.0",
"json-schema-traverse": "0.3.1",
"json-stable-stringify": "1.0.1"
}
},
"asn1": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
"integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y="
},
"assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
},
"async": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz",
"integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==",
"requires": {
"lodash": "4.17.4"
}
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
},
"aws4": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz",
"integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4="
},
"base-x": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.2.tgz",
"integrity": "sha1-v4c4YbdRQnm3lp80CSnquHwR0TA=",
"requires": {
"safe-buffer": "5.1.1"
}
},
"bcrypt-pbkdf": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz",
"integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=",
"optional": true,
"requires": {
"tweetnacl": "0.14.5"
}
},
"bignumber.js": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-4.0.2.tgz",
"integrity": "sha1-LR3DfuWWiGfs6pC22k0W5oYI0h0="
},
"boom": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz",
"integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=",
"requires": {
"hoek": "4.2.0"
}
},
"bs58": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
"integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=",
"requires": {
"base-x": "3.0.2"
}
},
"bs58check": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.0.2.tgz",
"integrity": "sha1-BvY7AcL6YXMDPJDrh/H+PS4T2Jo=",
"requires": {
"bs58": "4.0.1",
"create-hash": "1.1.3"
}
},
"caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
},
"cipher-base": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
"integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
"requires": {
"inherits": "2.0.3",
"safe-buffer": "5.1.1"
}
},
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
},
"combined-stream": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz",
"integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=",
"requires": {
"delayed-stream": "1.0.0"
}
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"create-hash": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz",
"integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=",
"requires": {
"cipher-base": "1.0.4",
"inherits": "2.0.3",
"ripemd160": "2.0.1",
"sha.js": "2.4.8"
}
},
"cryptiles": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz",
"integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=",
"requires": {
"boom": "5.2.0"
},
"dependencies": {
"boom": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz",
"integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==",
"requires": {
"hoek": "4.2.0"
}
}
}
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
"requires": {
"assert-plus": "1.0.0"
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
"ecc-jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
"integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=",
"optional": true,
"requires": {
"jsbn": "0.1.1"
}
},
"extend": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
"integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ="
},
"extsprintf": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
},
"fast-deep-equal": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz",
"integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8="
},
"forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
},
"form-data": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz",
"integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=",
"requires": {
"asynckit": "0.4.0",
"combined-stream": "1.0.5",
"mime-types": "2.1.17"
}
},
"getpass": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
"integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
"requires": {
"assert-plus": "1.0.0"
}
},
"har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
},
"har-validator": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
"integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
"requires": {
"ajv": "5.2.2",
"har-schema": "2.0.0"
}
},
"hash-base": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz",
"integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=",
"requires": {
"inherits": "2.0.3"
}
},
"hawk": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz",
"integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==",
"requires": {
"boom": "4.3.1",
"cryptiles": "3.1.2",
"hoek": "4.2.0",
"sntp": "2.0.2"
}
},
"hoek": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz",
"integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ=="
},
"http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
"requires": {
"assert-plus": "1.0.0",
"jsprim": "1.4.1",
"sshpk": "1.13.1"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
},
"jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
"optional": true
},
"json-schema": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
},
"json-schema-traverse": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
"integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A="
},
"json-stable-stringify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
"integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=",
"requires": {
"jsonify": "0.0.0"
}
},
"json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
},
"jsonify": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM="
},
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
"integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
"requires": {
"assert-plus": "1.0.0",
"extsprintf": "1.3.0",
"json-schema": "0.2.3",
"verror": "1.10.0"
}
},
"lodash": {
"version": "4.17.4",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
},
"mime-db": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz",
"integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE="
},
"mime-types": {
"version": "2.1.17",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz",
"integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=",
"requires": {
"mime-db": "1.30.0"
}
},
"moment": {
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz",
"integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8="
},
"mysql": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/mysql/-/mysql-2.14.1.tgz",
"integrity": "sha512-ZPXqQeYH7L1QPDyC77Rcp32cNCQnNjz8Y4BbF17tOjm5yhSfjFa3xS4PvuxWJtEEmwVc4ccI7sSntj4eyYRq0A==",
"requires": {
"bignumber.js": "4.0.2",
"readable-stream": "2.3.3",
"safe-buffer": "5.1.1",
"sqlstring": "2.2.0"
}
},
"oauth-sign": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
"integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM="
},
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
},
"process-nextick-args": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
},
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
},
"qs": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
"integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A=="
},
"readable-stream": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
"integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==",
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "1.0.0",
"process-nextick-args": "1.0.7",
"safe-buffer": "5.1.1",
"string_decoder": "1.0.3",
"util-deprecate": "1.0.2"
}
},
"request": {
"version": "2.82.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.82.0.tgz",
"integrity": "sha512-/QWqfmyTfQ4OYs6EhB1h2wQsX9ZxbuNePCvCm0Mdz/mxw73mjdg0D4QdIl0TQBFs35CZmMXLjk0iCGK395CUDg==",
"requires": {
"aws-sign2": "0.7.0",
"aws4": "1.6.0",
"caseless": "0.12.0",
"combined-stream": "1.0.5",
"extend": "3.0.1",
"forever-agent": "0.6.1",
"form-data": "2.3.1",
"har-validator": "5.0.3",
"hawk": "6.0.2",
"http-signature": "1.2.0",
"is-typedarray": "1.0.0",
"isstream": "0.1.2",
"json-stringify-safe": "5.0.1",
"mime-types": "2.1.17",
"oauth-sign": "0.8.2",
"performance-now": "2.1.0",
"qs": "6.5.1",
"safe-buffer": "5.1.1",
"stringstream": "0.0.5",
"tough-cookie": "2.3.3",
"tunnel-agent": "0.6.0",
"uuid": "3.1.0"
}
},
"request-debug": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/request-debug/-/request-debug-0.2.0.tgz",
"integrity": "sha1-/AVOyBcYGwTKQaBSwTb2HEirr3g=",
"requires": {
"stringify-clone": "1.1.1"
}
},
"ripemd160": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz",
"integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=",
"requires": {
"hash-base": "2.0.2",
"inherits": "2.0.3"
}
},
"safe-buffer": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
},
"sha.js": {
"version": "2.4.8",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.8.tgz",
"integrity": "sha1-NwaMLEdra69ALRSknGf1l5IfY08=",
"requires": {
"inherits": "2.0.3"
}
},
"sntp": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/sntp/-/sntp-2.0.2.tgz",
"integrity": "sha1-UGQRDwr4X3z9t9a2ekACjOUrSys=",
"requires": {
"hoek": "4.2.0"
}
},
"sqlstring": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.2.0.tgz",
"integrity": "sha1-wxNcTqirzX5+50GklmqJHYak8ZE="
},
"sshpk": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz",
"integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=",
"requires": {
"asn1": "0.2.3",
"assert-plus": "1.0.0",
"bcrypt-pbkdf": "1.0.1",
"dashdash": "1.14.1",
"ecc-jsbn": "0.1.1",
"getpass": "0.1.7",
"jsbn": "0.1.1",
"tweetnacl": "0.14.5"
}
},
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"requires": {
"safe-buffer": "5.1.1"
}
},
"stringify-clone": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/stringify-clone/-/stringify-clone-1.1.1.tgz",
"integrity": "sha1-MJojX7Ts/M19OI2+GLqQT6yvQzs="
},
"stringstream": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
"integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg="
},
"tough-cookie": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz",
"integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=",
"requires": {
"punycode": "1.4.1"
}
},
"tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
"requires": {
"safe-buffer": "5.1.1"
}
},
"tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
"optional": true
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"uuid": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
"integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g=="
},
"verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
"requires": {
"assert-plus": "1.0.0",
"core-util-is": "1.0.2",
"extsprintf": "1.3.0"
}
}
}
}

18
package.json Normal file
View file

@ -0,0 +1,18 @@
{
"name": "lbry-social-tipbot",
"description": "LBC reddit tip bot",
"version": "0.0.1",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://gitlab.com/aureolin/lbry-social-tipbot.git"
},
"dependencies": {
"async": "^2.5.0",
"bs58check": "^2.0.2",
"moment": "^2.18.1",
"mysql": "^2.14.1",
"request": "^2.82.0",
"request-debug": "^0.2.0"
}
}

93
sql/ddl.sql Normal file
View file

@ -0,0 +1,93 @@
CREATE TABLE Users
(
`Id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`Username` VARCHAR(20) NOT NULL,
`Balance` DECIMAL(18,8) UNSIGNED DEFAULT 0 NOT NULL,
`DepositAddress` VARCHAR(34) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL,
`Created` DATETIME NOT NULL,
PRIMARY KEY `PK_UserId` (`Id`),
UNIQUE KEY `Idx_RedditUsername` (`Username`),
UNIQUE KEY `Idx_UserDepositAddress` (`DepositAddress`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4;
CREATE TABLE Messages
(
`Id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`Type` SMALLINT NOT NULL COMMENT '1 - Private Message, 2 - Comment',
`FullId` VARCHAR(15) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL,
`RedditId` VARCHAR(15) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL,
`ParentRedditId` VARCHAR(20) CHARACTER SET latin1 COLLATE latin1_general_ci,
`Subreddit` VARCHAR(50),
`AuthorId` BIGINT UNSIGNED NOT NULL,
`Body` TEXT,
`Context` TEXT,
`RedditCreated` DATETIME NOT NULL,
`Created` DATETIME NOT NULL,
PRIMARY KEY `PK_MessageId` (`Id`),
FOREIGN KEY `FK_MessageAuthor` (`AuthorId`) REFERENCES `Users` (`Id`),
UNIQUE KEY `Idx_MessageRedditId` (`RedditId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4;
CREATE TABLE Tips
(
`Id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`MessageId` BIGINT UNSIGNED NOT NULL,
`SenderId` BIGINT UNSIGNED NOT NULL,
`RecipientId` BIGINT UNSIGNED NOT NULL,
`ParsedAmount` VARCHAR(20) NOT NULL COMMENT 'user amount string, $0.x, 0.x usd or 0.x lbc',
`AmountUsd` DECIMAL(18,2) UNSIGNED,
`Amount` DECIMAL(18,8) UNSIGNED NOT NULL,
`Created` DATETIME NOT NULL,
PRIMARY KEY `PK_TipId` (`Id`),
FOREIGN KEY `FK_TipSender` (`SenderId`) REFERENCES `Users` (`Id`),
FOREIGN KEY `FK_TipRecipient` (`RecipientId`) REFERENCES `Users` (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4;
CREATE TABLE Deposits
(
`Id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`UserId` BIGINT UNSIGNED NOT NULL,
`TxHash` VARCHAR(70) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL,
`Amount` DECIMAL(18,8) UNSIGNED NOT NULL,
`Confirmations` INTEGER UNSIGNED DEFAULT 0 NOT NULL COMMENT 'at least 3 confirmations required',
`Created` DATETIME NOT NULL,
PRIMARY KEY `PK_DepositId` (`Id`),
FOREIGN KEY `FK_Depositor` (`UserId`) REFERENCES `Users` (`Id`),
UNIQUE KEY `Idx_UserDepositTx` (`UserId`, `TxHash`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4;
CREATE TABLE Withdrawals
(
`Id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`UserId` BIGINT UNSIGNED NOT NULL,
`TxHash` VARCHAR(70) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL,
`Amount` DECIMAL(18,8) UNSIGNED NOT NULL,
`Created` DATETIME NOT NULL,
PRIMARY KEY `PK_DepositId` (`Id`),
FOREIGN KEY `FK_Withdrawer` (`UserId`) REFERENCES `Users` (`Id`),
UNIQUE KEY `Idx_WithdrawalTxHash` (`TxHash`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4;
DELIMITER //
CREATE TRIGGER `Trg_OnDepositCreated`
AFTER INSERT ON `Deposits`
FOR EACH ROW
BEGIN
IF NEW.Confirmations >= 3 THEN
UPDATE Users U SET U.Balance = U.Balance + NEW.Amount WHERE U.Id = NEW.UserId;
END IF;
END;
CREATE TRIGGER `Trg_OnDepositUpdated`
AFTER UPDATE ON `Deposits`
FOR EACH ROW
BEGIN
IF OLD.Confirmations < 3 AND NEW.Confirmations >= 3 THEN
UPDATE Users U SET U.Balance = U.Balance + NEW.Amount WHERE U.Id = NEW.UserId;
END IF;
END;
//
DELIMITER ;

4
templates/onbalance.txt Normal file
View file

@ -0,0 +1,4 @@
Your balance is **{amount} LBC**.
----
[^How ^to ^use]({how_to_use_url}) ^&bull; [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^&bull; ^r/lbry

4
templates/ondeposit.txt Normal file
View file

@ -0,0 +1,4 @@
Send any amount of LBC you'd like to deposit to `{address}`.
----
[^How ^to ^use]({how_to_use_url}) ^&bull; [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^&bull; ^r/lbry

View file

@ -0,0 +1,4 @@
You do not have sufficient funds to send a tip to {recipient}. You tried to send **{tip} LBC** but your balance is **{amount} LBC**.
----
[^How ^to ^use]({how_to_use_url}) ^&bull; [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^&bull; ^r/lbry

View file

@ -0,0 +1,4 @@
I'm sorry, I do not understand the amount that you specified for the tip.
----
[^How ^to ^use]({how_to_use_url}) ^&bull; [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^&bull; ^r/lbry

4
templates/onsendtip.txt Normal file
View file

@ -0,0 +1,4 @@
{recipient}, you've received `{tip}`!
----
[^How ^to ^use]({how_to_use_url}) ^&bull; [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^&bull; ^r/lbry

View file

@ -0,0 +1,4 @@
You cannot withdraw an amount that is less than or equal to the transaction fee. You tried to withdraw **{amount} LBC** but the transaction fee is **{fee} LBC**.
----
[^How ^to ^use]({how_to_use_url}) ^&bull; [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^&bull; ^r/lbry

View file

@ -0,0 +1,4 @@
You do not have sufficient funds for your withdrawal request. You tried to withdraw **{amount} LBC** but your balance is **{balance} LBC**.
----
[^How ^to ^use]({how_to_use_url}) ^&bull; [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^&bull; ^r/lbry

View file

@ -0,0 +1,4 @@
I'm sorry, I cannot send any LBC to `{address}` because it is invalid. Also, you cannot withdraw to your deposit address.
----
[^How ^to ^use]({how_to_use_url}) ^&bull; [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^&bull; ^r/lbry

View file

@ -0,0 +1,4 @@
I'm sorry, I do not understand the amount that you specified for the withdrawal.
----
[^How ^to ^use]({how_to_use_url}) ^&bull; [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^&bull; ^r/lbry

4
templates/onwithdraw.txt Normal file
View file

@ -0,0 +1,4 @@
You have successfully withdrawn **{amount} LBC** to `{address}`! ([tx](https://explorer.lbry.io/tx/{txid})).
----
[^How ^to ^use]({how_to_use_url}) ^&bull; [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^&bull; ^r/lbry