Repository navigation
Expand file tree
/
Copy pathpostpromoter.js
More file actions
342 lines (277 loc) · 13 KB
/
Copy pathpostpromoter.js
File metadata and controls
342 lines (277 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
var fs = require("fs");
const steem = require('steem');
var utils = require('./utils');
var account = null;
var last_trans = 0;
var outstanding_bids = [];
var last_round = [];
var config = null;
var first_load = true;
var isVoting = false;
var last_withdrawal = null;
steem.api.setOptions({ url: 'https://api.steemit.com' });
utils.log("*START*");
// Load the settings from the config file
config = JSON.parse(fs.readFileSync("config.json"));
// If the API is enabled, start the web server
if(config.api && config.api.enabled) {
var express = require('express');
var app = express();
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
app.get('/api/bids', (req, res) => res.json({ current_round: outstanding_bids, last_round: last_round }));
app.listen(config.api.port, () => utils.log('API running on port ' + config.api.port))
}
// Check if bot state has been saved to disk, in which case load it
if (fs.existsSync('state.json')) {
var state = JSON.parse(fs.readFileSync("state.json"));
if (state.last_trans)
last_trans = state.last_trans;
if (state.outstanding_bids)
outstanding_bids = state.outstanding_bids;
if (state.last_round)
last_round = state.last_round;
if(state.last_withdrawal)
last_withdrawal = state.last_withdrawal;
utils.log('Restored saved bot state: ' + JSON.stringify(state));
}
// Schedule to run every 10 seconds
setInterval(startProcess, 10000);
function startProcess() {
// Load the settings from the config file each time so we can pick up any changes
config = JSON.parse(fs.readFileSync("config.json"));
// Load the bot account info
steem.api.getAccounts([config.account], function (err, result) {
account = result[0];
// Check if there are any rewards to claim.
claimRewards();
// Check if it is time to withdraw funds.
checkAutoWithdraw();
});
if (account && !isVoting) {
getTransactions();
// Load the current voting power of the account
var vp = utils.getVotingPower(account);
// We are at 100% voting power - time to vote!
if (vp >= 10000 && outstanding_bids.length > 0) {
// Don't process any bids while we are voting due to race condition (they will be processed when voting is done).
isVoting = true;
// Make a copy of the list of outstanding bids and vote on them
startVoting(outstanding_bids.slice().reverse());
// Save the last round of bids for use in API call
last_round = outstanding_bids.slice();
// Reset the list of outstanding bids for the next round
outstanding_bids = [];
}
// Save the state of the bot to disk.
saveState();
}
}
function startVoting(bids) {
// Sum the amounts of all of the bids
var total = bids.reduce(function(total, bid) { return total + bid.amount; }, 0);
utils.log('=======================================================');
utils.log('Bidding Round End! Starting to vote! Total bids: $' + total);
utils.log('=======================================================');
for(var i = 0; i < bids.length; i++) {
// Calculate the vote weight to be used for each bid based on the amount bid as a percentage of the total bids
bids[i].weight = Math.round(config.batch_vote_weight * 100 * (bids[i].amount / total));
}
vote(bids);
}
function vote(bids) {
// Get the first bid in the list
var bid = bids.pop();
utils.log('Bid Weight: ' + bid.weight);
steem.broadcast.vote(config.posting_key, account.name, bid.author, bid.permlink, bid.weight, function(err, result) {
if (!err && result) {
utils.log(utils.format(bid.weight / 100) + '% vote cast for: @' + bid.author + '/' + bid.permlink);
// If promotion content is specified in the config then use it to comment on the upvoted post
if (config.promotion_content && config.promotion_content != '') {
// Generate the comment permlink via steemit standard convention
var permlink = 're-' + bid.author.replace(/\./g, '') + '-' + bid.permlink + '-' + new Date().toISOString().replace(/-|:|\./g, '').toLowerCase();
// Replace variables in the promotion content
var content = config.promotion_content.replace(/\{weight\}/g, utils.format(bid.weight / 100)).replace(/\{botname\}/g, config.account).replace(/\{sender\}/g, bid.sender);
// Broadcast the comment
steem.broadcast.comment(config.posting_key, bid.author, bid.permlink, account.name, permlink, permlink, content, '{"app":"postpromoter/1.2.0"}', function (err, result) {
if (err)
utils.log(err, result);
});
}
} else
utils.log(err, result);
});
// If there are more bids, vote on the next one after 20 seconds
if(bids.length > 0) {
setTimeout(function() { vote(bids); }, 30000);
} else {
utils.log('=======================================================');
utils.log('Voting Complete!');
utils.log('=======================================================');
isVoting = false;
}
}
function getTransactions() {
var num_trans = 50;
// If this is the first time the bot is ever being run, start with just the most recent transaction
if (first_load && last_trans == 0) {
utils.log('First run - starting with last transaction on account.');
num_trans = 1;
}
// If this is the first time the bot is run after a restart get a larger list of transactions to make sure none are missed
if (first_load && last_trans > 0) {
utils.log('First run - loading all transactions since bot was stopped.');
num_trans = 1000;
}
steem.api.getAccountHistory(account.name, -1, num_trans, function (err, result) {
first_load = false;
result.forEach(function(trans) {
var op = trans[1].op;
// Check that this is a new transaction that we haven't processed already
if(trans[0] > last_trans) {
// We only care about SBD transfers to the bot
if (op[0] == 'transfer' && op[1].to == account.name) {
var amount = parseFloat(op[1].amount);
var currency = utils.getCurrency(op[1].amount);
utils.log("Incoming Bid! From: " + op[1].from + ", Amount: " + op[1].amount + ", memo: " + op[1].memo);
// Check for min and max bid values in configuration settings
var min_bid = config.min_bid ? parseFloat(config.min_bid) : 0;
var max_bid = config.max_bid ? parseFloat(config.max_bid) : 9999;
if(config.disabled_mode) {
// Bot is disabled, refund all Bids
refund(op[1].from, amount, currency, 'Bot is disabled!');
} else if(amount < min_bid) {
// Bid amount is too low
refund(op[1].from, amount, currency, 'Min bid amount is ' + config.min_bid);
} else if (amount > max_bid) {
// Bid amount is too high
refund(op[1].from, amount, currency, 'Max bid amount is ' + config.max_bid);
} else if(config.currencies_accepted && config.currencies_accepted.indexOf(currency) < 0) {
// Sent an unsupported currency
refund(op[1].from, amount, currency, 'Bids in ' + currency + ' are not accepted.');
} else {
// Bid amount is just right!
checkPost(op[1].memo, amount, currency, op[1].from);
}
}
// Save the ID of the last transaction that was processed.
last_trans = trans[0];
}
});
});
}
function checkPost(memo, amount, currency, sender) {
// Parse the author and permlink from the memo URL
var permLink = memo.substr(memo.lastIndexOf('/') + 1);
var author = memo.substring(memo.lastIndexOf('@') + 1, memo.lastIndexOf('/'));
// Make sure the author isn't on the blacklist!
if(config.blacklist && config.blacklist.indexOf(author) >= 0)
{
utils.log('Invalid Bid - @' + author + ' is on the blacklist!');
return;
}
steem.api.getContent(author, permLink, function (err, result) {
if (!err && result && result.id > 0) {
// If comments are not allowed then we need to first check if the post is a comment
if(!config.allow_comments && (result.parent_author != null && result.parent_author != '')) {
refund(sender, amount, currency, 'Bids not allowed on comments.');
return;
}
var created = new Date(result.created + 'Z');
// Get the list of votes on this post to make sure the bot didn't already vote on it (you'd be surprised how often people double-submit!)
var votes = result.active_votes.filter(function(vote) { return vote.voter == account.name; });
if (votes.length > 0 || (new Date() - created) >= (config.max_post_age * 60 * 60 * 1000)) {
// This post is already voted on by this bot or the post is too old to be voted on
refund(sender, amount, currency, ((votes.length > 0) ? 'Already Voted' : 'Post older than max age'));
return;
}
} else {
// Invalid memo
refund(sender, amount, currency, 'Invalid Memo');
return;
}
// Check if there is already a bid for this post in the current round
var existing_bid = outstanding_bids.find(bid => bid.url == result.url);
if(existing_bid) {
// There is already a bid for this post in the current round
utils.log('Existing Bid Found - New Amount: ' + amount + ', Total Amount: ' + (existing_bid.amount + amount));
existing_bid.amount += amount;
} else {
// All good - push to the array of valid bids for this round
utils.log('Valid Bid - Amount: ' + amount + ', Title: ' + result.title);
outstanding_bids.push({ amount: amount, currency: currency, sender: sender, author: result.author, permlink: result.permlink, url: result.url });
}
});
}
function saveState() {
var state = {
outstanding_bids: outstanding_bids,
last_round: last_round,
last_trans: last_trans,
last_withdrawal: last_withdrawal
};
// Save the state of the bot to disk
fs.writeFile('state.json', JSON.stringify(state), function (err) {
if (err)
utils.log(err);
});
}
function refund(sender, amount, currency, reason) {
// Make sure refunds are enabled and the sender isn't on the no-refund list (for exchanges and things like that).
if(!config.refunds_enabled || (config.no_refund && config.no_refund.indexOf(sender) >= 0)) {
utils.log("Invalid bid - " + reason + ' NO REFUND');
return;
}
// Issue the refund.
steem.broadcast.transfer(config.active_key, config.account, sender, utils.format(amount, 3) + ' ' + currency, 'Refund for invalid bid - ' + reason, function(err, response) {
if(err)
utils.log(err, response);
else {
utils.log('Refund of ' + amount + ' ' + currency + ' sent to @' + sender + ' for reason: ' + reason);
}
});
}
function claimRewards() {
if (!config.auto_claim_rewards)
return;
// Make api call only if you have actual reward
if (parseFloat(account.reward_steem_balance) > 0 || parseFloat(account.reward_sbd_balance) > 0 || parseFloat(account.reward_vesting_balance) > 0) {
steem.broadcast.claimRewardBalance(config.posting_key, config.account, account.reward_steem_balance, account.reward_sbd_balance, account.reward_vesting_balance, function (err, result) {
if (err) {
utils.log(err);
}
if (result) {
utils.log('$$$ Rewards Claim SBD:' + account.reward_sbd_balance + ', STEEM:' + account.reward_steem_balance + ', Vesting:' + account.reward_vesting_balance);
}
});
}
}
function checkAutoWithdraw() {
// Check if auto-withdraw is active
if (!config.auto_withdrawal.active)
return;
// If it's past the withdrawal time and we haven't made a withdrawal today and there is a positive SBD balance, then process the withdrawal
if (new Date(new Date().toDateString()) > new Date(last_withdrawal) && new Date().getHours() >= config.auto_withdrawal.execute_time && parseFloat(account.sbd_balance) > 0) {
// Save the date of the last withdrawal
last_withdrawal = new Date().toDateString();
// Load account details of the account we are sending the withdrawal to (this is needed for encrypted memos)
steem.api.getAccounts([config.auto_withdrawal.to_account], function(err, result) {
var to_account = result[0];
var memo = config.auto_withdrawal.memo.replace(/\{balance\}/g, account.sbd_balance);
// Encrypt memo
if(memo.startsWith('#'))
memo = steem.memo.encode(config.memo_key, to_account.memo_key, memo);
// Withdraw all available SBD to the specified account
steem.broadcast.transfer(config.active_key, config.account, config.auto_withdrawal.to_account, account.sbd_balance, memo, function (err, response) {
if (err)
utils.log(err, response);
else {
utils.log('$$$ Auto withdrawal: ' + account.sbd_balance + ' sent to @' + config.auto_withdrawal.to_account);
}
});
});
}
}