2017-09-23 18:05:00 +02:00
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 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' ;
2017-09-24 14:55:24 +02:00
const tipRegex = /(\$[\d\.]+|[\d\.]+( usd| lbc))/ig ;
2017-09-24 14:50:05 +02:00
const gildRegex = new RegExp ( 'gild (u|\/u)\/lbryian|(u|\/u)\/lbryian gild' , 'ig' ) ;
2017-09-23 18:05:00 +02:00
// Other globals
const commentKind = 't1' ;
const privateMessageKind = 't4' ;
let globalAccessToken ;
let accessTokenTime ;
// Load message templates
const messageTemplates = { } ;
const templateNames = [
'onbalance' ,
'ondeposit' ,
2017-09-25 18:10:34 +02:00
'ondeposit.completed' ,
2017-09-24 09:03:46 +02:00
'ongild' ,
'ongild.insufficientfunds' ,
2017-09-23 18:05:00 +02:00
'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 ) {
return callback ( err ) ;
}
let response ;
try {
response = JSON . parse ( body ) ;
} catch ( e ) {
return callback ( e , null ) ;
}
2018-01-23 12:32:50 +01:00
if ( response . error ) {
return callback ( new Error ( response . message ) ) ;
}
2019-04-12 18:01:07 +02:00
console . log ( ` Got ${ response . data . children . length } unread messages. ` ) ;
2018-01-23 12:32:50 +01:00
2017-09-23 18:05:00 +02:00
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 ) {
return cb ( err , null ) ;
}
return cb ( null , res . insertId ) ;
} ) ;
}
return cb ( null , userId ) ;
}
] , callback ) ;
} ;
2017-09-25 18:10:34 +02:00
const processCompletedDeposits = ( callback ) => {
2017-09-25 18:38:34 +02:00
const delay = 2000 ;
2017-09-25 18:10:34 +02:00
async . waterfall ( [
( cb ) => {
db . query ( 'SELECT C.DepositId, D.Amount, U.Username, U.Balance FROM CompletedDepositConfirmations C JOIN Deposits D ON D.Id = C.DepositId JOIN Users U ON U.Id = C.UserId' , cb ) ;
} ,
( res , fields , cb ) => {
if ( res . length > 0 ) {
return async . eachSeries ( res , ( completedDeposit , ecb ) => {
2017-09-25 18:38:34 +02:00
sendPMUsingTemplate ( 'ondeposit.completed' , { how _to _use _url : config . howToUseUrl , amount : completedDeposit . Amount , balance : completedDeposit . Balance } ,
'Deposit completed!' , completedDeposit . Username , ( err ) => {
if ( err ) {
return setTimeout ( ecb , delay , err ) ;
}
// remove the entry from the DB
return db . query ( 'DELETE FROM CompletedDepositConfirmations WHERE DepositId = ?' , [ completedDeposit . DepositId ] , ( ierr ) => {
if ( ierr ) {
return setTimeout ( ecb , delay , ierr ) ;
2017-09-25 18:10:34 +02:00
}
2017-09-25 18:38:34 +02:00
// success
return setTimeout ( ecb , delay , null , true ) ;
} ) ;
} ) ;
2017-09-25 18:10:34 +02:00
// TODO: Implement inserting messages into a pending message queue instead
2017-09-25 18:40:05 +02:00
} , ( err ) => {
if ( err ) {
return cb ( err , null ) ;
}
return cb ( null , true ) ;
} ) ;
2017-09-25 18:10:34 +02:00
}
2017-09-25 18:24:04 +02:00
return cb ( null , true ) ;
2017-09-25 18:10:34 +02:00
}
] , callback ) ;
} ;
2017-09-23 18:05:00 +02:00
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 ) {
2017-09-24 09:03:46 +02:00
return sendPMUsingTemplate ( 'onsendtip.insufficientfunds' ,
2017-09-24 09:35:19 +02:00
{
how _to _use _url : config . howToUseUrl ,
recipient : ` u/ ${ recipient } ` ,
amount : amount ,
2017-09-24 10:52:13 +02:00
amount _usd : [ '$' , parseFloat ( tipdata . amountUsd ) . toFixed ( 2 ) ] . join ( '' ) ,
2017-09-24 09:35:19 +02:00
balance : senderBalance
} , 'Insufficient funds to send tip' , tipdata . message . data . author , ( ) => {
2017-09-24 09:28:08 +02:00
markMessageRead ( tipdata . message . data . name , ( ) => {
cb ( new Error ( 'Insufficient funds' ) , null ) ;
} ) ;
2017-09-23 18:05:00 +02:00
} ) ;
}
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 ) => {
// 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
2017-09-24 10:52:13 +02:00
const amountUsdStr = parseFloat ( tipdata . amountUsd ) . toFixed ( 2 ) ;
2017-09-24 09:41:00 +02:00
replyMessageUsingTemplate ( 'onsendtip' , { recipient : ` u/ ${ recipient } ` , tip : ` ${ amount } LBC ( $ ${ amountUsdStr } ) ` , how _to _use _url : config . howToUseUrl } ,
2017-09-23 18:05:00 +02:00
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 ) {
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 ) ;
} ) ;
} ;
2017-09-24 09:03:46 +02:00
const gildThing = ( thingFullId , callback ) => {
const url = ` ${ baseUrl } /api/v1/gold/gild/ ${ thingFullId } ` ;
request . post ( { url , headers : { 'User-Agent' : config . userAgent , 'Authorization' : 'Bearer ' + globalAccessToken } } , ( err , res , body ) => {
if ( err ) {
return callback ( err , null ) ;
}
let response ;
try {
response = JSON . parse ( body ) ;
} catch ( e ) {
2017-09-24 10:13:36 +02:00
//return callback(e, null);
2017-09-24 09:03:46 +02:00
}
2017-09-24 10:13:36 +02:00
if ( response && ( response . json . ratelimit > 0 || response . json . errors . length > 0 ) ) {
2017-09-24 09:03:46 +02:00
return callback ( new Error ( 'Rate limited.' ) , null ) ;
}
// success
return callback ( null , true ) ;
} ) ;
} ;
2017-09-23 18:05:00 +02:00
const markMessageRead = ( messageFullId , callback ) => {
const url = ` ${ baseUrl } /api/read_message ` ;
2017-09-24 09:03:46 +02:00
request . post ( { url , form : { id : messageFullId } , headers : { 'User-Agent' : config . userAgent , 'Authorization' : 'Bearer ' + globalAccessToken } } , ( err , res , body ) => {
2017-09-23 18:05:00 +02:00
if ( err ) {
2019-04-12 18:01:07 +02:00
console . log ( err ) ;
2017-09-23 18:05:00 +02:00
return callback ( err , null ) ;
}
let response ;
try {
response = JSON . parse ( body ) ;
} catch ( e ) {
return callback ( e , null ) ;
}
2019-04-12 18:01:07 +02:00
2017-09-23 18:05:00 +02:00
// success
return callback ( null , true ) ;
} ) ;
} ;
2017-09-24 09:28:08 +02:00
const sendPMUsingTemplate = ( template , substitutions , subject , recipient , callback ) => {
2017-09-23 18:05:00 +02:00
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/compose ` ;
request . post ( {
url ,
form : { api _type : 'json' , text : messageText , subject , to : recipient } ,
2017-09-24 09:03:46 +02:00
headers : { 'User-Agent' : config . userAgent , 'Authorization' : 'Bearer ' + globalAccessToken }
2017-09-23 18:05:00 +02:00
} , ( 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 } ,
2017-09-24 09:03:46 +02:00
headers : { 'User-Agent' : config . userAgent , 'Authorization' : 'Bearer ' + globalAccessToken }
2017-09-23 18:05:00 +02:00
} , ( err , res , body ) => {
if ( err ) {
return callback ( err , null ) ;
}
let response ;
try {
response = JSON . parse ( body ) ;
} catch ( e ) {
return callback ( e , null ) ;
}
2018-05-04 03:17:37 +02:00
if ( ! response . json ) {
return callback ( new Error ( 'Invalid response.' ) , null ) ;
}
2017-09-23 18:05:00 +02:00
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 ) ;
2017-09-24 09:03:46 +02:00
request . get ( { url : url , headers : { 'User-Agent' : config . userAgent , 'Authorization' : 'Bearer ' + globalAccessToken } } , ( err , res , body ) => {
2017-09-23 18:05:00 +02:00
if ( err ) {
return callback ( err , null ) ;
}
let response ;
try {
response = JSON . parse ( body ) ;
} catch ( e ) {
return callback ( e , null ) ;
}
2017-12-07 09:43:08 +01:00
// possible 500 error
if ( ! response || ! response . data || ! response . data . children ) {
return callback ( new Error ( 'Could not retrieve the message author info.' ) , null ) ;
}
2017-09-23 18:05:00 +02:00
return callback ( null , ( response . data . children . length > 0 ) ? response . data . children [ 0 ] . data . author : null ) ;
} ) ;
} ;
2017-09-24 09:03:46 +02:00
const sendGild = ( sender , recipient , amount , gilddata , callback ) => {
console . log ( ` gilding ${ recipient } with ${ amount } LBC worth ${ gilddata . amountUsd } from ${ sender } ` ) ;
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 required for gilding, or the difference after sending the tip is negative
if ( senderBalance < amount || ( senderBalance - amount ) < 0 ) {
return sendPMUsingTemplate ( 'ongild.insufficientfunds' ,
2017-09-24 09:35:19 +02:00
{
how _to _use _url : config . howToUseUrl ,
recipient : ` u/ ${ recipient } ` ,
amount : amount ,
2017-09-24 10:52:13 +02:00
amount _usd : [ '$' , parseFloat ( gilddata . amountUsd ) . toFixed ( 2 ) ] . join ( '' ) ,
2017-09-24 09:35:19 +02:00
balance : senderBalance
} , 'Insufficient funds' , gilddata . message . data . author , ( ) => {
2017-09-24 09:28:08 +02:00
markMessageRead ( gilddata . message . data . name , ( ) => {
cb ( new Error ( 'Insufficient funds' ) , null ) ;
} ) ;
2017-09-24 09:03:46 +02:00
} ) ;
}
return db . query ( 'UPDATE Users SET Balance = Balance - ? WHERE Id = ?' , [ amount , data . senderId ] , cb ) ;
} ,
( res , fields , cb ) => {
2017-09-24 10:10:07 +02:00
createOrGetUserId ( recipient , cb ) ;
} ,
( recipientId , cb ) => {
data . recipientId = recipientId ;
2017-09-24 09:03:46 +02:00
// save the message
const msgdata = gilddata . message . data ;
db . query ( [ 'INSERT INTO Messages (AuthorId, Type, FullId, RedditId, ParentRedditId, Subreddit, Body, Context, RedditCreated, Created) ' ,
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP())' ] . join ( '' ) ,
[ data . senderId ,
gilddata . 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 ) => {
// save the tip information
db . query ( [ 'INSERT INTO Tips (MessageId, SenderId, RecipientId, Amount, AmountUsd, ParsedAmount, IsGild, Created) ' ,
'VALUES (?, ?, ?, ?, ?, ?, 1, UTC_TIMESTAMP())' ] . join ( '' ) ,
[ res . insertId ,
data . senderId ,
data . recipientId ,
amount ,
gilddata . amountUsd ,
2017-09-24 10:52:13 +02:00
[ '$' , config . gildPrice . toFixed ( 2 ) ] . join ( '' ) ,
2017-09-24 09:03:46 +02:00
] , cb ) ;
} ,
( res , fields , cb ) => {
// send the gild
2017-09-24 09:56:34 +02:00
gildThing ( gilddata . message . data . parent _id , cb ) ;
2017-09-24 09:03:46 +02:00
} ,
( success , cb ) => {
// reply to the source message with message template after successful commit
2017-09-24 10:52:13 +02:00
const amountUsdStr = parseFloat ( gilddata . amountUsd ) . toFixed ( 2 ) ;
2017-09-24 09:41:00 +02:00
replyMessageUsingTemplate ( 'ongild' , { sender : ` u/ ${ sender } ` , recipient : ` u/ ${ recipient } ` , gild _amount : ` ${ amount } LBC ( $ ${ amountUsdStr } ) ` , how _to _use _url : config . howToUseUrl } ,
2017-09-24 09:03:46 +02:00
gilddata . message . data . name , cb ) ;
} ,
( success , cb ) => {
// Mark the message as read
markMessageRead ( gilddata . 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 ) {
return db . rollback ( ( ) => {
callback ( err , null ) ;
} ) ;
}
// success
return callback ( null , true ) ;
} ) ;
} ;
const doGild = function ( message , callback ) {
async . waterfall ( [
( cb ) => {
getMessageAuthor ( message . data . parent _id , globalAccessToken , cb ) ;
} ,
( recipient , cb ) => {
const sender = message . data . author ;
if ( sender !== recipient ) {
return cb ( null , { message , recipient , sender , amountUsd : config . gildPrice } ) ;
}
return cb ( null , null ) ;
} ,
( gilddata , cb ) => {
if ( gilddata && gilddata . amountUsd > 0 ) {
return convertUsdToLbc ( gilddata . amountUsd , ( err , convertedAmount ) => {
if ( err ) {
return cb ( err ) ;
}
gilddata . amountLbc = convertedAmount ;
return cb ( null , gilddata ) ;
} ) ;
}
return cb ( null , null ) ;
} ,
( data , cb ) => {
2017-09-24 09:05:47 +02:00
if ( data ) {
2017-09-24 09:03:46 +02:00
return sendGild ( data . sender , data . recipient , data . amountLbc , data , cb ) ;
}
return cb ( null , null ) ;
}
2019-04-12 18:01:07 +02:00
] , ( err ) => {
if ( err ) {
callback ( err , null ) ;
}
markMessageRead ( message . data . name , callback ) ;
} ) ;
2017-09-24 09:03:46 +02:00
} ;
2017-09-24 14:50:05 +02:00
const doSendTip = ( body , message , callback ) => {
2017-09-23 18:05:00 +02:00
/ * *
2017-09-24 14:50:05 +02:00
* accepted matched strings :
* "1 usd" or "1 lbc" or "$1"
2017-09-23 18:05:00 +02:00
* /
2017-09-24 14:50:05 +02:00
// Use regex matching
2017-09-23 18:05:00 +02:00
let amountUsd = 0 ;
let amountLbc = 0 ;
2017-09-24 14:50:05 +02:00
let matchedString = '' ;
const match = String ( message . data . body ) . match ( tipRegex ) ;
2017-09-24 15:02:42 +02:00
if ( match && match . length > 0 ) {
2017-09-24 14:50:05 +02:00
matchedString = match [ 0 ] ;
if ( matchedString . indexOf ( ' ' ) > - 1 ) {
const parts = matchedString . split ( ' ' , 2 ) ;
const amount = parseFloat ( parts [ 0 ] ) ;
const unit = parts [ 1 ] . toLowerCase ( ) ;
if ( isNaN ( amount ) || amount <= 0 || [ 'usd' , 'lbc' ] . indexOf ( unit ) === - 1 ) {
// invalid amount or unit
return sendPMUsingTemplate ( 'onsendtip.invalidamount' , { how _to _use _url : config . howToUseUrl } , 'Invalid amount for send tip' , message . data . author , ( ) => {
markMessageRead ( message . data . name , callback ) ;
} ) ;
}
2019-04-12 18:01:07 +02:00
2017-09-24 14:50:05 +02:00
if ( unit === 'lbc' ) {
amountLbc = amount ;
} else {
amountUsd = amount ;
}
2017-09-23 18:05:00 +02:00
} else {
2017-09-24 14:50:05 +02:00
amountUsd = parseFloat ( matchedString . substring ( 1 ) ) ;
if ( isNaN ( amountUsd ) || amountUsd <= 0 ) {
return sendPMUsingTemplate ( 'onsendtip.invalidamount' , { how _to _use _url : config . howToUseUrl } , 'Invalid amount for send tip' , message . data . author , ( ) => {
markMessageRead ( message . data . name , callback ) ;
} ) ;
}
2017-09-23 18:05:00 +02:00
}
}
2019-04-12 18:01:07 +02:00
2017-09-23 18:05:00 +02:00
if ( amountLbc > 0 || amountUsd > 0 ) {
2017-09-24 14:50:05 +02:00
const parsedAmount = matchedString ;
2017-09-23 18:05:00 +02:00
// get the author of the parent message
2017-09-24 14:50:05 +02:00
return async . waterfall ( [
2017-09-23 18:05:00 +02:00
( 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 ) ;
}
2019-04-12 18:01:07 +02:00
2017-09-23 18:05:00 +02:00
tipdata . amountUsd = convertedAmount ;
return cb ( null , tipdata ) ;
} ) ;
}
}
2019-04-12 18:01:07 +02:00
return cb ( null , null ) ;
2017-09-23 18:05:00 +02:00
} ,
( data , cb ) => {
if ( data ) {
return sendTip ( data . sender , data . recipient , data . amountLbc , data , cb ) ;
}
}
2019-04-12 18:01:07 +02:00
] , ( err ) => {
if ( err ) { callback ( err , null ) ; }
markMessageRead ( message . data . name , callback ) ;
} ) ;
2017-09-23 18:05:00 +02:00
}
2019-04-12 18:01:07 +02:00
2017-09-24 14:50:05 +02:00
// not a valid or recognised message, simply mark the message as read
return markMessageRead ( message . data . name , callback ) ;
2017-09-23 18:05:00 +02:00
} ;
const doSendBalance = ( message , callback ) => {
async . waterfall ( [
( cb ) => {
createOrGetUserId ( message . data . author , cb ) ;
} ,
( authorId , cb ) => {
getBalance ( authorId , cb ) ;
} ,
( balance , cb ) => {
// send message with balance
2017-09-24 09:03:46 +02:00
replyMessageUsingTemplate ( 'onbalance' , { how _to _use _url : config . howToUseUrl , amount : balance } , message . data . name , cb ) ;
2017-09-23 18:05:00 +02:00
} ,
( success , cb ) => {
// mark messge as read
markMessageRead ( message . data . name , cb ) ;
}
] , ( err ) => {
if ( err ) {
return callback ( err , null ) ;
}
// success
return callback ( null , true ) ;
} ) ;
} ;
const sendLbcToAddress = ( address , amount , callback ) => {
2017-09-23 21:31:45 +02:00
request . post ( { url : config . lbrycrd . rpcurl , json : { method : 'sendfrom' , params : [ config . lbrycrd . account , address , amount ] } } , ( err , resp , body ) => {
2017-09-23 18:05:00 +02:00
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 ) {
2017-09-24 09:28:08 +02:00
return sendPMUsingTemplate ( 'onwithdraw.insufficientfunds' , { how _to _use _url : config . howToUseUrl , amount : amount , balance : balance } ,
'Insufficient funds for withdrawal' , message . data . author , ( ) => {
markMessageRead ( message . data . name , ( ) => {
cb ( new Error ( 'Insufficient funds' ) , null ) ;
} ) ;
2017-09-23 18:05:00 +02:00
} ) ;
}
// 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
2017-09-24 09:03:46 +02:00
replyMessageUsingTemplate ( 'onwithdraw' , { how _to _use _url : config . howToUseUrl , address : address , amount : amount , txid : data . txhash } , message . data . name , cb ) ;
2017-09-23 18:05:00 +02:00
}
] , ( err ) => {
if ( 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
2017-09-24 09:03:46 +02:00
replyMessageUsingTemplate ( 'ondeposit' , { how _to _use _url : config . howToUseUrl , address : address } , message . data . name , cb ) ;
2017-09-23 18:05:00 +02:00
} ,
( 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 ) {
2019-04-12 18:01:07 +02:00
//console.log('Invalid message encountered.');
2017-09-23 18:05:00 +02:00
return callback ( new Error ( 'Invalid message specified for processing.' ) ) ;
}
2019-04-12 18:01:07 +02:00
2017-09-23 18:05:00 +02:00
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
2019-04-12 18:01:07 +02:00
//console.log("Invalid Message=" + body);
return markMessageRead ( message . data . name , callback ) ;
2017-09-23 18:05:00 +02:00
}
2019-04-12 18:01:07 +02:00
2017-09-23 18:05:00 +02:00
const amount = parseFloat ( parts [ 1 ] ) ;
if ( isNaN ( amount ) || amount < 0 ) {
// TODO: send a message that the withdrawal amount is invalid
2017-09-24 09:28:08 +02:00
return sendPMUsingTemplate ( 'onwithdraw.invalidamount' , { how _to _use _url : config . howToUseUrl } , 'Invalid amount for withdrawal' , message . data . author , ( ) => {
markMessageRead ( message . data . name , callback ) ;
2017-09-23 18:05:00 +02:00
} ) ;
}
if ( amount <= config . lbrycrd . txfee ) {
2017-09-24 09:28:08 +02:00
return sendPMUsingTemplate ( 'onwithdraw.amountltefee' , { how _to _use _url : config . howToUseUrl , amount : amount , fee : config . lbrycrd . txfee } ,
'Withdrawal amount less than minimum fee' , message . data . author , ( ) => {
markMessageRead ( message . data . name , callback ) ;
2017-09-23 18:05:00 +02:00
} ) ;
}
// base58 check the address
const address = parts [ 2 ] ;
try {
base58 . decode ( address ) ;
} catch ( e ) {
2017-09-24 09:28:08 +02:00
return sendPMUsingTemplate ( 'onwithdraw.invalidaddress' , { how _to _use _url : config . howToUseUrl } , 'Invalid address for withdrawal' , message . data . author , ( ) => {
markMessageRead ( message . data . name , callback ) ;
2017-09-23 18:05:00 +02:00
} ) ;
}
2019-04-12 18:01:07 +02:00
2017-09-23 18:05:00 +02:00
return doWithdrawal ( amount , address , message , callback ) ;
}
2019-04-12 18:01:07 +02:00
return markMessageRead ( message . data . name , callback ) ;
2017-09-23 18:05:00 +02:00
}
2019-04-12 18:01:07 +02:00
2017-09-23 18:05:00 +02:00
if ( message . kind === commentKind ) {
2017-09-24 15:02:42 +02:00
const gildMatch = body . match ( gildRegex ) ;
if ( gildMatch && gildMatch . length > 0 ) {
2017-09-24 09:03:46 +02:00
doGild ( message , callback ) ;
} else {
doSendTip ( body , message , callback ) ;
}
2017-09-23 18:05:00 +02:00
}
} ;
// Run the bot
const runBot = ( ) => {
async . waterfall ( [
( cb ) => {
2017-09-25 18:10:34 +02:00
if ( ! accessTokenTime || moment . duration ( moment ( ) . diff ( accessTokenTime ) ) . asMinutes ( ) >= 59 ) {
2017-09-23 18:05:00 +02:00
// 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 ;
2017-09-25 18:10:34 +02:00
processCompletedDeposits ( cb ) ;
} ,
( success , cb ) => {
retrieveUnreadMessages ( globalAccessToken , cb ) ;
2017-09-23 18:05:00 +02:00
} ,
( 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 ) ;
} ) ;
} ;
2019-04-12 18:01:07 +02:00
runBot ( ) ;