From a408b4e9d31fb87df7d05e80be493df97dbf0447 Mon Sep 17 00:00:00 2001 From: Akinwale Ariwodola Date: Sat, 23 Sep 2017 17:05:00 +0100 Subject: [PATCH] Initial commit --- .gitignore | 3 + README.md | 0 app.js | 809 +++++++++++++++++++++ config/default.js | 24 + deposits.js | 136 ++++ package-lock.json | 553 ++++++++++++++ package.json | 18 + sql/ddl.sql | 93 +++ templates/onbalance.txt | 4 + templates/ondeposit.txt | 4 + templates/onsendtip.insufficientfunds.txt | 4 + templates/onsendtip.invalidamount.txt | 4 + templates/onsendtip.txt | 4 + templates/onwithdraw.amountltefee.txt | 4 + templates/onwithdraw.insufficientfunds.txt | 4 + templates/onwithdraw.invalidaddress.txt | 4 + templates/onwithdraw.invalidamount.txt | 4 + templates/onwithdraw.txt | 4 + 18 files changed, 1676 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app.js create mode 100644 config/default.js create mode 100644 deposits.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 sql/ddl.sql create mode 100644 templates/onbalance.txt create mode 100644 templates/ondeposit.txt create mode 100644 templates/onsendtip.insufficientfunds.txt create mode 100644 templates/onsendtip.invalidamount.txt create mode 100644 templates/onsendtip.txt create mode 100644 templates/onwithdraw.amountltefee.txt create mode 100644 templates/onwithdraw.insufficientfunds.txt create mode 100644 templates/onwithdraw.invalidaddress.txt create mode 100644 templates/onwithdraw.invalidamount.txt create mode 100644 templates/onwithdraw.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dbfa1ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +config/config.js +node_modules +token \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/app.js b/app.js new file mode 100644 index 0000000..e10a272 --- /dev/null +++ b/app.js @@ -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): u/lbryian +// withdraw (PM): withdraw
+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(); \ No newline at end of file diff --git a/config/default.js b/config/default.js new file mode 100644 index 0000000..764e1c6 --- /dev/null +++ b/config/default.js @@ -0,0 +1,24 @@ +module.exports = { + debug: true, + accessTokenPath: 'token', + clientId: '', + clientSecret: '', + username: '', + password: '', + + // for handling tip comments + redditName: 'u/lbryian', + + mariadb: { + host: 'localhost', + username: '', + password: '', + database: '' + }, + + lbrycrd: { + account: 'tips', + rpcurl: 'http://127.0.0.1:9245', + txfee: 0.00002000 + } +}; diff --git a/deposits.js b/deposits.js new file mode 100644 index 0000000..55b1f82 --- /dev/null +++ b/deposits.js @@ -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(); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..40f6d18 --- /dev/null +++ b/package-lock.json @@ -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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..17c7d46 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/sql/ddl.sql b/sql/ddl.sql new file mode 100644 index 0000000..726d718 --- /dev/null +++ b/sql/ddl.sql @@ -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 ; \ No newline at end of file diff --git a/templates/onbalance.txt b/templates/onbalance.txt new file mode 100644 index 0000000..fda8791 --- /dev/null +++ b/templates/onbalance.txt @@ -0,0 +1,4 @@ +Your balance is **{amount} LBC**. + +---- +[^How ^to ^use]({how_to_use_url}) ^• [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^• ^r/lbry \ No newline at end of file diff --git a/templates/ondeposit.txt b/templates/ondeposit.txt new file mode 100644 index 0000000..9450d10 --- /dev/null +++ b/templates/ondeposit.txt @@ -0,0 +1,4 @@ +Send any amount of LBC you'd like to deposit to `{address}`. + +---- +[^How ^to ^use]({how_to_use_url}) ^• [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^• ^r/lbry \ No newline at end of file diff --git a/templates/onsendtip.insufficientfunds.txt b/templates/onsendtip.insufficientfunds.txt new file mode 100644 index 0000000..3ffa325 --- /dev/null +++ b/templates/onsendtip.insufficientfunds.txt @@ -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}) ^• [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^• ^r/lbry \ No newline at end of file diff --git a/templates/onsendtip.invalidamount.txt b/templates/onsendtip.invalidamount.txt new file mode 100644 index 0000000..d01b408 --- /dev/null +++ b/templates/onsendtip.invalidamount.txt @@ -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}) ^• [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^• ^r/lbry \ No newline at end of file diff --git a/templates/onsendtip.txt b/templates/onsendtip.txt new file mode 100644 index 0000000..7ea3a4f --- /dev/null +++ b/templates/onsendtip.txt @@ -0,0 +1,4 @@ +{recipient}, you've received `{tip}`! + +---- +[^How ^to ^use]({how_to_use_url}) ^• [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^• ^r/lbry \ No newline at end of file diff --git a/templates/onwithdraw.amountltefee.txt b/templates/onwithdraw.amountltefee.txt new file mode 100644 index 0000000..5d98c58 --- /dev/null +++ b/templates/onwithdraw.amountltefee.txt @@ -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}) ^• [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^• ^r/lbry \ No newline at end of file diff --git a/templates/onwithdraw.insufficientfunds.txt b/templates/onwithdraw.insufficientfunds.txt new file mode 100644 index 0000000..d4c246e --- /dev/null +++ b/templates/onwithdraw.insufficientfunds.txt @@ -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}) ^• [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^• ^r/lbry \ No newline at end of file diff --git a/templates/onwithdraw.invalidaddress.txt b/templates/onwithdraw.invalidaddress.txt new file mode 100644 index 0000000..d4c71b8 --- /dev/null +++ b/templates/onwithdraw.invalidaddress.txt @@ -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}) ^• [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^• ^r/lbry \ No newline at end of file diff --git a/templates/onwithdraw.invalidamount.txt b/templates/onwithdraw.invalidamount.txt new file mode 100644 index 0000000..6682644 --- /dev/null +++ b/templates/onwithdraw.invalidamount.txt @@ -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}) ^• [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^• ^r/lbry \ No newline at end of file diff --git a/templates/onwithdraw.txt b/templates/onwithdraw.txt new file mode 100644 index 0000000..bfc7e69 --- /dev/null +++ b/templates/onwithdraw.txt @@ -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}) ^• [^What ^is ^LBRY?](https://lbry.io/faq/what-is-lbry) ^• ^r/lbry \ No newline at end of file