-
Notifications
You must be signed in to change notification settings - Fork 4
/
pizza-bot.py
472 lines (331 loc) · 16.6 KB
/
pizza-bot.py
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
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
#!/usr/bin/env python3
'''A script to findm and react to PIZZA commands in comments'''
from beem import Hive
from beem.account import Account
from beem.blockchain import Blockchain
from beem.comment import Comment
import beem.instance
import os
import jinja2
import configparser
import time
import requests
import sqlite3
from datetime import date
from hiveengine.wallet import Wallet
### Global configuration
BLOCK_STATE_FILE_NAME = 'lastblock.txt'
config = configparser.ConfigParser()
config.read('pizzabot.config')
ENABLE_COMMENTS = config['Global']['ENABLE_COMMENTS'] == 'True'
ENABLE_TRANSFERS = config['HiveEngine']['ENABLE_TRANSFERS'] == 'True'
ENABLE_DISCORD = config['Global']['ENABLE_DISCORD'] == 'True'
ACCOUNT_NAME = config['Global']['ACCOUNT_NAME']
ACCOUNT_POSTING_KEY = config['Global']['ACCOUNT_POSTING_KEY']
HIVE_API_NODE = config['Global']['HIVE_API_NODE']
HIVE = Hive(node=[HIVE_API_NODE], keys=[config['Global']['ACCOUNT_ACTIVE_KEY']])
HIVE.chain_params['chain_id'] = 'beeab0de00000000000000000000000000000000000000000000000000000000'
beem.instance.set_shared_blockchain_instance(HIVE)
ACCOUNT = Account(ACCOUNT_NAME)
TOKEN_NAME = config['HiveEngine']['TOKEN_NAME']
BOT_COMMAND_STR = config['Global']['BOT_COMMAND_STR']
ESP_BOT_COMMAND_STR = config['Global']['ESP_BOT_COMMAND_STR']
WEBHOOK_URL = config['Global']['DISCORD_WEBHOOK_URL']
SQLITE_DATABASE_FILE = 'pizzabot.db'
SQLITE_GIFTS_TABLE = 'pizza_bot_gifts'
### END Global configuration
print('Loaded configs:')
for section in config.keys():
for key in config[section].keys():
if '_key' in key: continue # don't log posting/active keys
print('%s : %s = %s' % (section, key, config[section][key]))
# Markdown templates for comments
comment_fail_template = jinja2.Template(open(os.path.join('templates','comment_fail.template'),'r').read())
comment_outofstock_template = jinja2.Template(open(os.path.join('templates','comment_outofstock.template'),'r').read())
comment_success_template = jinja2.Template(open(os.path.join('templates','comment_success.template'),'r').read())
comment_daily_limit_template = jinja2.Template(open(os.path.join('templates','comment_daily_limit.template'),'r').read())
comment_curation_template = jinja2.Template(open(os.path.join('templates','comment_curation.template'),'r').read())
# Spanish language templates
esp_comment_fail_template = jinja2.Template(open(os.path.join('templates','esp_comment_fail.template'),'r').read())
esp_comment_outofstock_template = jinja2.Template(open(os.path.join('templates','esp_comment_outofstock.template'),'r').read())
esp_comment_success_template = jinja2.Template(open(os.path.join('templates','esp_comment_success.template'),'r').read())
esp_comment_daily_limit_template = jinja2.Template(open(os.path.join('templates','esp_comment_daily_limit.template'),'r').read())
### sqlite3 database helpers
def db_create_tables():
db_conn = sqlite3.connect(SQLITE_DATABASE_FILE)
c = db_conn.cursor()
c.execute("CREATE TABLE IF NOT EXISTS %s(date TEXT NOT NULL, invoker TEXT NOT NULL, recipient TEXT NOT NULL, block_num INTEGER NOT NULL);" % SQLITE_GIFTS_TABLE)
db_conn.commit()
db_conn.close()
def db_save_gift(date, invoker, recipient, block_num):
db_conn = sqlite3.connect(SQLITE_DATABASE_FILE)
c = db_conn.cursor()
c.execute('INSERT INTO %s VALUES (?,?,?,?);' % SQLITE_GIFTS_TABLE, [
date,
invoker,
recipient,
block_num
])
db_conn.commit()
db_conn.close()
def db_count_gifts(date, invoker):
db_conn = sqlite3.connect(SQLITE_DATABASE_FILE)
c = db_conn.cursor()
c.execute("SELECT count(*) FROM %s WHERE date = '%s' AND invoker = '%s';" % (SQLITE_GIFTS_TABLE,date,invoker))
row = c.fetchone()
db_conn.commit()
db_conn.close()
return row[0]
def db_count_gifts_unique(date, invoker, recipient):
db_conn = sqlite3.connect(SQLITE_DATABASE_FILE)
c = db_conn.cursor()
c.execute("SELECT count(*) FROM %s WHERE date = '%s' AND invoker = '%s' AND recipient = '%s';" % (SQLITE_GIFTS_TABLE,date,invoker,recipient))
row = c.fetchone()
db_conn.commit()
db_conn.close()
return row[0]
def get_account_posts(account):
acc = Account(account)
account_history = acc.get_account_history(-1, 5000)
account_history = [x for x in account_history if x['type'] == 'comment' and not x['parent_author']]
return account_history
def get_account_details(account):
acc = Account(account)
return acc.json()
def get_block_number():
if not os.path.exists(BLOCK_STATE_FILE_NAME):
return None
with open(BLOCK_STATE_FILE_NAME, 'r') as infile:
block_num = infile.read()
block_num = int(block_num)
return block_num
def set_block_number(block_num):
with open(BLOCK_STATE_FILE_NAME, 'w') as outfile:
outfile.write('%d' % block_num)
def has_already_replied(post):
for reply in post.get_replies():
if reply.author == ACCOUNT_NAME:
return True
return False
def post_comment(parent_post, author, comment_body):
if ENABLE_COMMENTS:
print('Commenting!')
parent_post.reply(body=comment_body, author=author)
# sleep 3s before continuing
time.sleep(3)
else:
print('Debug mode comment:')
print(comment_body)
def post_discord_message(username, message_body):
if not ENABLE_DISCORD:
return
payload = {
"username": username,
"content": message_body
}
try:
requests.post(WEBHOOK_URL, data=payload)
except:
print('Error while sending discord message. Check configs.')
def daily_limit_reached(invoker_name, level=1):
today = str(date.today())
today_gift_count = db_count_gifts(today, invoker_name)
access_level = 'AccessLevel%d' % level
if today_gift_count >= int(config[access_level]['MAX_DAILY_GIFTS']):
return True
return False
def daily_limit_unique_reached(invoker_name, recipient_name, level=1):
today = str(date.today())
today_gift_count_unique = db_count_gifts_unique(today, invoker_name, recipient_name)
access_level = 'AccessLevel%d' % level
if today_gift_count_unique >= int(config[access_level]['MAX_DAILY_GIFTS_UNIQUE']):
return True
return False
def get_invoker_level(invoker_name):
# check how much TOKEN the invoker has
wallet_token_info = Wallet(invoker_name).get_token(TOKEN_NAME)
if not wallet_token_info:
invoker_balance = 0
invoker_stake = 0
else:
invoker_balance = float(wallet_token_info['balance'])
invoker_stake = float(wallet_token_info['stake'])
# does invoker meet level 2 requirements?
min_balance = float(config['AccessLevel2']['MIN_TOKEN_BALANCE'])
min_staked = float(config['AccessLevel2']['MIN_TOKEN_STAKED'])
if invoker_balance + invoker_stake >= min_balance and invoker_stake >= min_staked:
return 2
# does invoker meet level 1 requirements?
min_balance = float(config['AccessLevel1']['MIN_TOKEN_BALANCE'])
min_staked = float(config['AccessLevel1']['MIN_TOKEN_STAKED'])
if invoker_balance + invoker_stake >= min_balance and invoker_stake >= min_staked:
return 1
return 0
def is_block_listed(name):
return name in config['HiveEngine']['GIFT_BLOCK_LIST'].split(',')
def can_gift(invoker_name, recipient_name):
if invoker_name in config['HiveEngine']['GIFT_ALLOW_LIST']:
return True
if is_block_listed(invoker_name):
return False
if is_block_listed(recipient_name):
return False
level = get_invoker_level(invoker_name)
if level == 0:
return False
if daily_limit_reached(invoker_name, level):
return False
if daily_limit_unique_reached(invoker_name, recipient_name, level):
return False
return True
def hive_posts_stream():
db_create_tables()
blockchain = Blockchain(node=[HIVE_API_NODE])
start_block = get_block_number()
for op in blockchain.stream(opNames=['comment','vote'], start=start_block, threading=False, thread_num=1):
set_block_number(op['block_num'])
if 'type' in op.keys() and op['type'] == 'vote':
if 'voter' in op.keys() and op['voter'] == config['VoteWatcher']['FOLLOW_ACCOUNT']:
print('%s -> %s' % (op['voter'], op['author']))
# skip downvotes
if op['weight'] < 0:
print('Downvote')
continue
author_account = op['voter']
parent_author = op['author']
# we reply to post instead of a comment
reply_identifier = '@%s/%s' % (parent_author,op['permlink'])
print('Found curation vote in block # %s - https://peakd.com/%s' % (op['block_num'],reply_identifier))
else:
# skip votes from other accounts
continue
else:
# it's a comment or post
# how are there posts with no author?
if 'author' not in op.keys():
continue
author_account = op['author']
parent_author = op['parent_author']
reply_identifier = '@%s/%s' % (author_account,op['permlink'])
if parent_author == ACCOUNT_NAME:
message_body = '%s replied with: %s' % (author_account,op['body'])
post_discord_message(ACCOUNT_NAME, message_body)
# skip comments that don't include the bot's command prefix
if BOT_COMMAND_STR not in op['body'] and ESP_BOT_COMMAND_STR not in op['body']:
continue
else:
debug_message = 'Found %s command: https://peakd.com/%s in block %s' % (BOT_COMMAND_STR, reply_identifier, op['block_num'])
print(debug_message)
# no self-tipping
if author_account == parent_author:
continue
# bail out if the parent_author (recipient) is missing
if not parent_author:
continue
# skip tips sent to the bot itself
if parent_author == ACCOUNT_NAME:
continue
# check if spanish language comment templates should be used
use_spanish_templates = 'body' in op.keys() and ESP_BOT_COMMAND_STR in op['body']
message_body = '%s asked to send a slice to %s' % (author_account, parent_author)
post_discord_message(ACCOUNT_NAME, message_body)
try:
post = Comment(reply_identifier)
except beem.exceptions.ContentDoesNotExistsException:
print('post not found!')
continue
# if we already commented on this post, skip
if has_already_replied(post):
print("We already replied!")
continue
invoker_level = get_invoker_level(author_account)
if is_block_listed(author_account) or is_block_listed(parent_author):
post_discord_message(ACCOUNT_NAME, 'Nope')
print('Invoker or recipient is on the block list')
continue
# Check if the invoker meets requirements to use the bot
if not can_gift(author_account, parent_author):
print('Invoker doesnt meet minimum requirements')
min_balance = float(config['AccessLevel1']['MIN_TOKEN_BALANCE'])
min_staked = float(config['AccessLevel1']['MIN_TOKEN_STAKED'])
if invoker_level > 0 and daily_limit_reached(author_account,invoker_level):
# Check if invoker has reached daily limits
max_daily_gifts = config['AccessLevel%s' % invoker_level]['MAX_DAILY_GIFTS']
if use_spanish_templates:
comment_body = esp_comment_daily_limit_template.render(token_name=TOKEN_NAME,
target_account=author_account,
max_daily_gifts=max_daily_gifts)
else:
comment_body = comment_daily_limit_template.render(token_name=TOKEN_NAME,
target_account=author_account,
max_daily_gifts=max_daily_gifts)
message_body = '%s tried to send PIZZA but reached the daily limit.' % (author_account)
# disabled comments for this path to save RCs
print(message_body)
post_discord_message(ACCOUNT_NAME, message_body)
elif invoker_level > 0 and daily_limit_unique_reached(author_account, parent_author,invoker_level):
# Check if daily limit for unique tips has been reached
message_body = '%s tried to send PIZZA but reached the daily limit.' % (author_account)
print(message_body)
post_discord_message(ACCOUNT_NAME, message_body)
else:
# Tell the invoker how to gain access to the bot
if use_spanish_templates:
comment_body = esp_comment_fail_template.render(token_name=TOKEN_NAME,
target_account=author_account,
min_balance=min_balance,
min_staked=min_staked)
else:
comment_body = comment_fail_template.render(token_name=TOKEN_NAME,
target_account=author_account,
min_balance=min_balance,
min_staked=min_staked)
message_body = '%s tried to send PIZZA but didnt meet requirements.' % (author_account)
post_comment(post, ACCOUNT_NAME, comment_body)
print(message_body)
post_discord_message(ACCOUNT_NAME, message_body)
continue
# check how much TOKEN the bot has
TOKEN_GIFT_AMOUNT = float(config['HiveEngine']['TOKEN_GIFT_AMOUNT'])
bot_balance = float(Wallet(ACCOUNT_NAME).get_token(TOKEN_NAME)['balance'])
if bot_balance < TOKEN_GIFT_AMOUNT:
message_body = 'Bot wallet has run out of %s' % TOKEN_NAME
print(message_body)
post_discord_message(ACCOUNT_NAME, message_body)
if use_spanish_templates:
comment_body = esp_comment_outofstock_template.render(token_name=TOKEN_NAME)
else:
comment_body = comment_outofstock_template.render(token_name=TOKEN_NAME)
post_comment(post, ACCOUNT_NAME, comment_body)
continue
# transfer
if ENABLE_TRANSFERS:
print('[*] Transfering %f %s from %s to %s' % (TOKEN_GIFT_AMOUNT, TOKEN_NAME, ACCOUNT_NAME, parent_author))
wallet = Wallet(ACCOUNT_NAME, steem_instance=HIVE)
wallet.transfer(parent_author, TOKEN_GIFT_AMOUNT, TOKEN_NAME, memo=config['HiveEngine']['TRANSFER_MEMO'])
today = str(date.today())
db_save_gift(today, author_account, parent_author, op['block_num'])
message_body = 'I sent %f %s to %s' % (TOKEN_GIFT_AMOUNT, TOKEN_NAME, parent_author)
print(message_body)
post_discord_message(ACCOUNT_NAME, message_body)
else:
print('[*] Skipping transfer of %f %s from %s to %s' % (TOKEN_GIFT_AMOUNT, TOKEN_NAME, ACCOUNT_NAME, parent_author))
# Leave a comment to nofify about the transfer
if 'type' in op.keys() and op['type'] == 'vote':
comment_body = comment_curation_template.render(token_name=TOKEN_NAME, target_account=parent_author, token_amount=TOKEN_GIFT_AMOUNT, author_account=author_account)
else:
today = str(date.today())
today_gift_count = db_count_gifts(today, author_account)
if invoker_level > 0:
max_daily_gifts = config['AccessLevel%s' % invoker_level]['MAX_DAILY_GIFTS']
else:
max_daily_gifts = 0
if use_spanish_templates:
# use Spanish template
comment_body = esp_comment_success_template.render(token_name=TOKEN_NAME, target_account=parent_author, token_amount=TOKEN_GIFT_AMOUNT, author_account=author_account, today_gift_count=today_gift_count, max_daily_gifts=max_daily_gifts)
else:
comment_body = comment_success_template.render(token_name=TOKEN_NAME, target_account=parent_author, token_amount=TOKEN_GIFT_AMOUNT, author_account=author_account, today_gift_count=today_gift_count, max_daily_gifts=max_daily_gifts)
post_comment(post, ACCOUNT_NAME, comment_body)
#break
if __name__ == '__main__':
hive_posts_stream()