diff --git a/bot/bot.js b/bot/bot.js index 5b1ebcc..42f0768 100644 --- a/bot/bot.js +++ b/bot/bot.js @@ -6,6 +6,9 @@ const Discord = require("discord.js"); let config = require("config"); config = config.get("bot"); +//load modules +const claimbot = require('./modules/claimbot.js'); + var aliases; try { aliases = require("./alias.json"); @@ -44,6 +47,9 @@ bot.on("ready", function() { require("./plugins.js").init(); console.log("type " + config.prefix + "help in Discord for a commands list."); bot.user.setGame(config.prefix + "help"); + + //initialize the claimbot (content bot) + claimbot.init(bot); }); bot.on("disconnected", function() { @@ -193,5 +199,4 @@ exports.addCustomFunc = function(customFunc) { exports.commandCount = function() { return Object.keys(commands).length; }; - -bot.login(config.token); +bot.login(config.token); \ No newline at end of file diff --git a/bot/modules/claimbot.js b/bot/modules/claimbot.js new file mode 100644 index 0000000..70ba6f6 --- /dev/null +++ b/bot/modules/claimbot.js @@ -0,0 +1,405 @@ +"use strict"; + +let lbry; +let mongo; +let discordBot; +let moment = require("moment"); +let request = require("request"); +let sleep = require("sleep"); +let config = require("config"); +let channels = config.get("claimbot").channels; +const Discord = require("discord.js"); + +module.exports = { + init: init +}; + +function init(discordBot_, mongodburl) { + if (lbry) { + throw new Error("init was already called once"); + } + + discordBot = discordBot_; + + const MongoClient = require("mongodb").MongoClient; + MongoClient.connect(config.get("mongodb").url, function(err, db) { + if (err) { + throw err; + } + mongo = db; + + const bitcoin = require("bitcoin"); + lbry = new bitcoin.Client(config.get("lbrycrd")); + + console.log("Activating claimbot"); + discordBot.channels.get("377938982111019010").send("activating claimbot"); + + setInterval(function() { + announceNewClaims(); + }, 60 * 1000); + announceNewClaims(); + }); +} + +function announceNewClaims() { + if (!mongo) { + discordPost("Failed to connect to mongo", {}); + return; + } + + if (!lbry) { + discordPost("Failed to connect to lbrycrd", {}); + return; + } + + Promise.all([getLastBlock(), lbryCall("getinfo")]) + .then(function([lastProcessedBlock, currentBlockInfo]) { + const currentHeight = currentBlockInfo["blocks"]; + + // console.log('Checking for new blocks'); + + if (lastProcessedBlock === null) { + console.log( + "First run. Setting last processed block to " + + currentHeight + + " and exiting." + ); + return setLastBlock(currentHeight); + } + + const testBlock = false; + + if (testBlock || lastProcessedBlock < currentHeight) { + const firstBlockToProcess = testBlock || lastProcessedBlock + 1, + lastBlockToProcess = testBlock || currentHeight; + + // console.log('Doing blocks ' + firstBlockToProcess + ' to ' + lastBlockToProcess); + return announceClaimsLoop( + firstBlockToProcess, + lastBlockToProcess, + currentHeight + ); + } + }) + .catch(function(err) { + discordPost(err.stack, {}); + }); +} + +function announceClaimsLoop(block, lastBlock, currentHeight) { + // console.log('Doing block ' + block) + let claimsFound = 0; + return lbryCall("getblockhash", block) + .then(function(blockHash) { + return lbryCall("getblock", blockHash); + }) + .then(function(blockData) { + return Promise.all(blockData["tx"].map(getClaimsForTxid)); + }) + .then(function(arrayOfClaimArrays) { + const claims = Array.prototype + .concat(...arrayOfClaimArrays) + .filter(function(c) { + return !!c; + }); + console.log("Found " + claims.length + " claims in " + block); + claimsFound = claims.length; + return Promise.all( + claims.map(function(claim) { + //slack has a rate limit. to avoid hitting it we must have a small delay between each message + //if claims were found in this block, then we wait, otherwise we don't + if (claimsFound > 0) sleep.msleep(300); + return announceClaim(claim, block, currentHeight); + }) + ); + }) + .then(function() { + return setLastBlock(block); + }) + .then(function() { + const nextBlock = block + 1; + if (nextBlock <= lastBlock) { + return announceClaimsLoop(nextBlock, lastBlock, currentHeight); + } + }); +} + +function announceClaim(claim, claimBlockHeight, currentHeight) { + console.log("" + claimBlockHeight + ": New claim for " + claim["name"]); + console.log(claim); + let options = { + method: "GET", + //url: 'https://explorer.lbry.io/api/getclaimbyid/' + claim['claimId'] + url: "http://127.0.0.1:5000/claim_decode/" + claim["name"] + }; + + request(options, function(error, response, body) { + if (error) throw new Error(error); + try { + console.log(body); + let claimData = null; + let channelName = null; + try { + body = JSON.parse(body); + if ( + body.hasOwnProperty("stream") && + body.stream.hasOwnProperty("metadata") + ) { + claimData = body.stream.metadata; + channelName = body.hasOwnProperty("channel_name") + ? body["channel_name"] + : null; + } + } catch (e) { + console.error(e); + return; + } + + return Promise.all([ + lbryCall("getvalueforname", claim["name"]), + lbryCall("getclaimsforname", claim["name"]) + ]).then(function([currentWinningClaim, claimsForName]) { + //console.log(JSON.stringify(claimData)); + let value = null; + if (claimData !== null) value = claimData; + else { + try { + value = JSON.parse(claim["value"]); + } catch (e) {} + } + + const text = []; + + if (value) { + /*if (channelName) { + text.push("Channel: lbry://" + channelName); + } + else*/ + console.log(value); + if (value["author"]) { + text.push("author: " + value["author"]); + } + if (value["description"]) { + text.push(value["description"]); + } + // if (value['content_type']) + // { + // text.push("*Content Type:* " + value['content_type']); + // } + if (value["nsfw"]) { + text.push("*Warning: Adult Content*"); + } + + //"fee":{"currency":"LBC","amount":186,"version":"_0_0_1","address":"bTGoFCakvQXvBrJg1b7FJzombFUu6iRJsk"} + if (value["fee"]) { + const fees = []; + text.push( + "Price: " + + value["fee"].amount + + " *" + + value["fee"].currency + + "*" + ); + /*for (var key in value['fee']) { + fees.push(value['fee'][key]['amount'] + ' ' + key); + } + text.push(fees.join(', '));*/ + } + + if (!claim["is controlling"]) { + // the following is based on https://lbry.io/faq/claimtrie-implementation + const lastTakeoverHeight = claimsForName["nLastTakeoverHeight"], + maxDelay = 4032, // 7 days of blocks at 2.5min per block + activationDelay = Math.min( + maxDelay, + Math.floor((claimBlockHeight - lastTakeoverHeight) / 32) + ), + takeoverHeight = claimBlockHeight + activationDelay, + secondsPerBlock = 161, // in theory this should be 150, but in practice its closer to 161 + takeoverTime = + Date.now() + + (takeoverHeight - currentHeight) * secondsPerBlock * 1000; + + text.push( + "Takes effect on approx. **" + + moment(takeoverTime, "x").format("MMMM Do [at] HH:mm [UTC]") + + "** (block " + + takeoverHeight + + ")" + ); + } + + const richEmbeded = { + author: { + name: value["author"] || "Anonymous", + url: "http://open.lbry.io/" + claim["name"], + icon_url: + "http://barkpost-assets.s3.amazonaws.com/wp-content/uploads/2013/11/3dDoge.gif" + }, + title: (channelName ? channelName + "/" : "") + claim["name"], + color: 1399626, + description: escapeSlackHtml(text.join("\n")), + footer: { + text: + "Block " + claimBlockHeight + " • Claim ID " + claim["claimId"] + }, + image: { url: !value["nsfw"] ? value["thumbnail"] || "" : "" }, + url: "http://open.lbry.io/" + claim["name"] + }; + + discordPost(text, richEmbeded); + } + /* + if (!claim['is controlling']) { + // the following is based on https://lbry.io/faq/claimtrie-implementation + const lastTakeoverHeight = claimsForName['nLastTakeoverHeight'], + maxDelay = 4032, // 7 days of blocks at 2.5min per block + activationDelay = Math.min(maxDelay, Math.floor((claimBlockHeight - lastTakeoverHeight) / 32)), + takeoverHeight = claimBlockHeight + activationDelay, + secondsPerBlock = 161, // in theory this should be 150, but in practice its closer to 161 + takeoverTime = Date.now() + ((takeoverHeight - currentHeight) * secondsPerBlock * 1000); + + text.push('Takes effect on approx. *' + moment(takeoverTime, 'x').format('MMMM Do [at] HH:mm [UTC]') + '* (block ' + takeoverHeight + ')'); + } + + const richEmbeded = { + author: { + name: value['author'] | 'Anonymous', + url: 'http://open.lbry.io/' + claim['name'], + icon_url: 'http://barkpost-assets.s3.amazonaws.com/wp-content/uploads/2013/11/3dDoge.gif' + }, + title: "[lbry://" + (channelName ? channelName + '/' : '') + claim['name'] + '](http://open.lbry.io/' + claim['name'] + ')', + color: "#155b4a", + description: escapeSlackHtml(text.join("\n")), + footer: "Block " + claimBlockHeight + " • Claim ID " + claim['claimId'], + image: (!value['nsfw']) ? (value['thumbnail'] | '') : '', + thumbnail: (!value['nsfw']) ? (value['thumbnail'] | '') : '', + url: 'http://open.lbry.io/' + claim['name'], + } + const attachment = { + "fallback": "New claim for lbry://" + claim['name'], + "color": "#155b4a", + // "pretext": "New claim in block " + claimBlockHeight, + // "author_name": 'lbry://' + claim['name'], + // "author_link": 'lbry://' + claim['name'], + // "author_icon": "http://flickr.com/icons/bobby.jpg", + "title": "lbry://" + (channelName ? channelName + '/' : '') + claim['name'], //escapeSlackHtml(value['title']), + "title_link": "lbry://" + (channelName ? channelName + '/' : '') + claim['name'], + "text": escapeSlackHtml(text.join("\n")), + // "fields": [], + // "image_url": value['nsfw'] ? null : value['thumbnail'], + // "thumb_url": (!value || value['nsfw']) ? null : value['thumbnail'], + "unfurl_links": false, + "unfurl_media": false, + "link_names": false, + "parse": "none", + "footer": "Block " + claimBlockHeight + " • Claim ID " + claim['claimId'], + "mrkdwn_in": ['text'], + }; + + if (value) { + attachment['fallback'] += (': "' + value['title'] + '" by ' + value['author']); + attachment['author_name'] = 'lbry://' + (channelName ? channelName + '/' : '') + claim['name']; + attachment['author_link'] = 'lbry://' + (channelName ? channelName + '/' : '') + claim['name']; + attachment['title'] = escapeSlackHtml(value['title']); + if (!value['nsfw']) { + attachment['thumb_url'] = value['thumbnail']; + } + } + discordPost(text, richEmbeded);*/ + //discordPost('', { icon_emoji: ':bellhop_bell:', attachments: [attachment] }); + }); + } catch (e) { + console.error(e); + } + }); +} + +function escapeSlackHtml(txt) { + return txt + .replace("&", "&") + .replace("<", "<") + .replace(">", ">"); +} + +function getClaimsForTxid(txid) { + return lbryCall("getclaimsfortx", txid).catch(function(err) { + // an error here most likely means the transaction is spent, + // which also means there are no claims worth looking at + return []; + }); +} + +function getLastBlock() { + return new Promise(function(resolve, reject) { + mongo.collection("claimbot").findOne({}, function(err, obj) { + if (err) { + reject(err); + } else if (!obj) { + mongo + .collection("claimbot") + .createIndex({ last_block: 1 }, { unique: true }); + resolve(null); + } else { + resolve(obj.last_block); + } + }); + }); +} + +function setLastBlock(block) { + return new Promise(function(resolve, reject) { + mongo + .collection("claimbot") + .findOneAndUpdate( + { last_block: { $exists: true } }, + { last_block: block }, + { upsert: true, returnOriginal: false }, + function(err, obj) { + if (!err && obj && obj.value.last_block != block) { + reject( + "Last value should be " + + block + + ", but it is " + + obj.value.last_block + ); + } else { + resolve(); + } + } + ); + }); +} + +function discordPost(text, params) { + //tmpdata = params.attachments[0]; + let richEmbeded = new Discord.RichEmbed(params); + /*richEmbeded.setAuthor(tmpdata.author_name, tmpdata.thumb_url, 'http://open.lbry.io/' + tmpdata.title); + richEmbeded.setColor(tmpdata.color); + richEmbeded.setDescription(tmpdata.text); + richEmbeded.setTitle(tmpdata.title); + richEmbeded.setURL('http://open.lbry.io/' + tmpdata.title); + richEmbeded.setFooter(tmpdata.footer);*/ + + //console.log(params); + channels.forEach(channel => { + discordBot.channels.get(channel).send("", richEmbeded); + }); + /*discordBot.postMessage(channel, text, params).fail(function (value) { + console.log('FAILED TO SLACK to ' + channel + '. Text: "' + text + '". Params: ' + JSON.stringify(params) + "\nResponse: " + JSON.stringify(value)); + });*/ +} + +function lbryCall(...args) { + return new Promise(function(resolve, reject) { + lbry.cmd(...args, function(err, ...response) { + if (err) { + reject( + new Error("JSONRPC call failed. Args: [" + args.join(", ") + "]") + ); + } else { + resolve(...response); + } + }); + }); +} diff --git a/package.json b/package.json index dc76459..cf6b8aa 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "node-config": "^0.0.2", "numeral": "^2.0.6", "request": "^2.83.0", + "sleep": "^5.1.1", "wget": "^0.0.1" }, "scripts": { diff --git a/yarn.lock b/yarn.lock index e8da305..f082175 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18,13 +18,13 @@ ajv@^4.9.1: json-stable-stringify "^1.0.1" ajv@^5.1.0: - version "5.2.3" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.3.tgz#c06f598778c44c6b161abafe3466b81ad1814ed2" + version "5.3.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.3.0.tgz#4414ff74a50879c208ee5fdc826e32c303549eda" dependencies: co "^4.6.0" fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" - json-stable-stringify "^1.0.1" ansi-regex@^2.0.0: version "2.1.1" @@ -535,8 +535,8 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" config@^1.27.0: - version "1.27.0" - resolved "https://registry.yarnpkg.com/config/-/config-1.27.0.tgz#3ab30d0080ff76f407c2f47ac1326adfd908af5f" + version "1.28.0" + resolved "https://registry.yarnpkg.com/config/-/config-1.28.0.tgz#666740053a81489905f26087c8656394c193507d" dependencies: json5 "0.4.0" os-homedir "1.0.2" @@ -599,6 +599,10 @@ detect-indent@^4.0.0: dependencies: repeating "^2.0.0" +detect-libc@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.2.tgz#71ad5d204bf17a6a6ca8f450c61454066ef461e1" + discord.js@^11.2.1: version "11.2.1" resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-11.2.1.tgz#bfc0f5a8b6398dc372d026e503592646456053fc" @@ -699,6 +703,10 @@ fast-deep-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + filename-regex@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" @@ -891,9 +899,9 @@ home-or-tmp@^2.0.0: os-homedir "^1.0.0" os-tmpdir "^1.0.1" -hooks-fixed@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/hooks-fixed/-/hooks-fixed-2.0.0.tgz#a01d894d52ac7f6599bbb1f63dfc9c411df70cba" +hooks-fixed@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hooks-fixed/-/hooks-fixed-2.0.2.tgz#20076daa07e77d8a6106883ce3f1722e051140b0" http-signature@~1.1.0: version "1.1.1" @@ -951,8 +959,8 @@ is-binary-path@^1.0.0: binary-extensions "^1.0.0" is-buffer@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc" + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" is-ci@^1.0.10: version "1.0.10" @@ -1134,6 +1142,10 @@ lex-parser@0.1.x, lex-parser@~0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/lex-parser/-/lex-parser-0.1.4.tgz#64c4f025f17fd53bfb45763faeb16f015a747550" +lodash.get@4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + lodash.some@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" @@ -1220,13 +1232,14 @@ mongodb@2.2.33: readable-stream "2.2.7" mongoose@^4.12.3: - version "4.12.3" - resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-4.12.3.tgz#7099bf8ce4945150001f4c2462e56c9e958ddcb9" + version "4.13.0" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-4.13.0.tgz#81bb266e045d66ac8dfdd84fc6749c873d7a6ac4" dependencies: async "2.1.4" bson "~1.0.4" - hooks-fixed "2.0.0" + hooks-fixed "2.0.2" kareem "1.5.0" + lodash.get "4.4.2" mongodb "2.2.33" mpath "0.3.0" mpromise "0.5.5" @@ -1261,7 +1274,7 @@ muri@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/muri/-/muri-1.3.0.tgz#aeccf3db64c56aa7c5b34e00f95b7878527a4721" -nan@^2.3.0: +nan@>=2.5.1, nan@^2.3.0: version "2.7.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" @@ -1277,9 +1290,10 @@ node-config@^0.0.2: resolved "https://registry.yarnpkg.com/node-config/-/node-config-0.0.2.tgz#46b40dcfbcb0e66d46a15f81b54eac2130fb150d" node-pre-gyp@^0.6.36: - version "0.6.38" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.38.tgz#e92a20f83416415bb4086f6d1fb78b3da73d113d" + version "0.6.39" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649" dependencies: + detect-libc "^1.0.2" hawk "3.1.3" mkdirp "^0.5.1" nopt "^4.0.1" @@ -1609,6 +1623,12 @@ slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" +sleep@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/sleep/-/sleep-5.1.1.tgz#878fa1d44d08eeb0f26fb2018ef8629eb1a3ab94" + dependencies: + nan ">=2.5.1" + sliced@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/sliced/-/sliced-0.0.5.tgz#5edc044ca4eb6f7816d50ba2fc63e25d8fe4707f" @@ -1618,8 +1638,8 @@ sliced@1.0.1: resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" snekfetch@^3.3.0: - version "3.5.2" - resolved "https://registry.yarnpkg.com/snekfetch/-/snekfetch-3.5.2.tgz#aec6f2a7d2c43b9ed942653d1074070a2c1cae50" + version "3.5.8" + resolved "https://registry.yarnpkg.com/snekfetch/-/snekfetch-3.5.8.tgz#4d4e539f8435352105e74c392f62f66740a27d6c" sntp@1.x.x: version "1.0.9" @@ -1628,8 +1648,8 @@ sntp@1.x.x: hoek "2.x.x" sntp@2.x.x: - version "2.0.2" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.0.2.tgz#5064110f0af85f7cfdb7d6b67a40028ce52b4b2b" + version "2.1.0" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" dependencies: hoek "4.x.x" @@ -1704,8 +1724,8 @@ supports-color@^2.0.0: resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" tar-pack@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984" + version "3.4.1" + resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f" dependencies: debug "^2.2.0" fstream "^1.0.10" @@ -1815,8 +1835,8 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" ws@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-3.2.0.tgz#d5d3d6b11aff71e73f808f40cc69d52bb6d4a185" + version "3.3.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.1.tgz#d97e34dee06a1190c61ac1e95f43cb60b78cf939" dependencies: async-limiter "~1.0.0" safe-buffer "~5.1.0"