import datetime as dt from contextlib import suppress import pymongo from funcy import compose, take, first from pymongo.errors import DuplicateKeyError, WriteError from steem.account import Account from steem.post import Post from steem.utils import keep_in_dict from steembase.exceptions import PostDoesNotExist from steemdata.utils import typify, json_expand, remove_body from toolz import pipe from utils import strip_dot_from_keys, safe_json_metadata def upsert_comment_chain(mongo, identifier, recursive=False): """ Upsert given comments and its parent(s). Args: mongo: mongodb instance identifier: Post identifier recursive: (Defaults to False). Recursively update all parent comments. """ with suppress(PostDoesNotExist): p = Post(identifier) upsert_comment(mongo, p.identifier) if recursive and p.is_comment(): parent_identifier = '@%s/%s' % (p.parent_author, p.parent_permlink) upsert_comment_chain(mongo, parent_identifier, recursive) def get_comment(identifier): with suppress(PostDoesNotExist): return pipe( Post(identifier).export(), strip_dot_from_keys, safe_json_metadata ) def upsert_comment(mongo, identifier): """ Upsert root post or comment. """ with suppress(PostDoesNotExist, DuplicateKeyError): c = get_comment(identifier) update = {'$set': {**c, 'updatedAt': dt.datetime.utcnow()}} if c['depth'] > 0: return mongo.Comments.update( {'identifier': c['identifier']}, update, upsert=True) return mongo.Posts.update({'identifier': c['identifier']}, update, upsert=True) def update_account(mongo, username, load_extras=True): """ Update Account. If load_extras is True, update: - followers, followings - curation stats - withdrawal routers, conversion requests """ a = Account(username) account = { **typify(a.export(load_extras=load_extras)), 'account': username, 'updatedAt': dt.datetime.utcnow(), } if type(account['json_metadata']) is dict: account['json_metadata'] = \ strip_dot_from_keys(account['json_metadata']) if not load_extras: account = {'$set': account} try: mongo.Accounts.update({'name': a.name}, account, upsert=True) except WriteError: # likely an invalid profile account['json_metadata'] = {} mongo.Accounts.update({'name': a.name}, account, upsert=True) print("Invalidated json_metadata on %s" % a.name) def update_account_ops(mongo, username): """ This method will fetch entire account history, and back-fill any missing ops. """ for event in Account(username).history(): with suppress(DuplicateKeyError): transform = compose(strip_dot_from_keys, remove_body, json_expand, typify) mongo.AccountOperations.insert_one(transform(event)) def account_operations_index(mongo, username): """ Lookup AccountOperations for latest synced index. """ start_index = 0 # use projection to ensure covered query highest_index = list( mongo.AccountOperations.find({'account': username}, {'_id': 0, 'index': 1}). sort("index", pymongo.DESCENDING).limit(1) ) if highest_index: start_index = highest_index[0].get('index', 0) return start_index def update_account_ops_quick(mongo, username, batch_size=200, steemd_instance=None): """ Only update the latest history, limited to 1 batch of defined batch_size. """ start_index = account_operations_index(mongo, username) # fetch latest records and update the db history = \ Account(username, steemd_instance=steemd_instance).history_reverse(batch_size=batch_size) for event in take(batch_size, history): if event['index'] < start_index: return with suppress(DuplicateKeyError): mongo.AccountOperations.insert_one(json_expand(typify(event))) def find_latest_item(mongo, collection_name, field_name): last_op = mongo.db[collection_name].find_one( filter={}, projection={field_name: 1, '_id': 0}, sort=[(field_name, pymongo.DESCENDING)], ) return last_op[field_name] def parse_operation(op): """ Update all relevant collections that this op impacts. """ op_type = op['type'] update_accounts_light = set() update_accounts_full = set() update_comments = set() def construct_identifier(): return '@%s/%s' % ( op.get('author', op.get('comment_author')), op.get('permlink', op.get('comment_permlink')), ) def account_from_auths(): return first(op.get('required_auths', op.get('required_posting_auths'))) if op_type in ['account_create', 'account_create_with_delegation']: update_accounts_light.add(op['creator']) update_accounts_full.add(op['new_account_name']) elif op_type in ['account_update', 'withdraw_vesting', 'claim_reward_balance', 'return_vesting_delegation', 'account_witness_vote']: update_accounts_light.add(op['account']) elif op_type == 'account_witness_proxy': update_accounts_light.add(op['account']) update_accounts_light.add(op['proxy']) elif op_type in ['author_reward', 'comment']: update_accounts_light.add(op['author']) update_comments.add(construct_identifier()) elif op_type == 'vote': update_accounts_light.add(op['voter']) update_comments.add(construct_identifier()) elif op_type == 'cancel_transfer_from_savings': update_accounts_light.add(op['from']) elif op_type == 'change_recovery_account': update_accounts_light.add(op['account_to_recover']) elif op_type == 'comment_benefactor_reward': update_accounts_light.add(op['benefactor']) elif op_type == ['convert', 'fill_convert_request', 'interest', 'limit_order_cancel', 'limit_order_create', 'shutdown_witness', 'witness_update']: update_accounts_light.add(op['owner']) elif op_type == 'curation_reward': update_accounts_light.add(op['curator']) elif op_type in ['custom', 'custom_json']: update_accounts_light.add(account_from_auths()) elif op_type == 'delegate_vesting_shares': update_accounts_light.add(op['delegator']) update_accounts_light.add(op['delegatee']) elif op_type == 'delete_comment': update_accounts_light.add(op['author']) elif op_type in ['escrow_approve', 'escrow_dispute', 'escrow_release', 'escrow_transfer']: accs = keep_in_dict(op, ['agent', 'from', 'to', 'who', 'receiver']).values() update_accounts_light.update(accs) elif op_type == 'feed_publish': update_accounts_light.add(op['publisher']) elif op_type in ['fill_order']: update_accounts_light.add(op['open_owner']) update_accounts_light.add(op['current_owner']) elif op_type in ['fill_vesting_withdraw']: update_accounts_light.add(op['to_account']) update_accounts_light.add(op['from_account']) elif op_type == 'pow2': acc = op['work'][1]['input']['worker_account'] update_accounts_light.add(acc) elif op_type in ['recover_account', 'request_account_recovery']: update_accounts_light.add(op['account_to_recover']) elif op_type == 'set_withdraw_vesting_route': update_accounts_light.add(op['from_account']) update_accounts_light.add(op['to_account']) elif op_type in ['transfer', 'transfer_from_savings', 'transfer_to_savings', 'transfer_to_vesting']: accs = keep_in_dict(op, ['agent', 'from', 'to', 'who', 'receiver']).values() update_accounts_light.update(accs) # # handle followers # if op_type == 'custom_json': # with suppress(ValueError): # cmd, op_json = json.loads(op['json']) # ['follow', {data...}] # if cmd == 'follow': # accs = keep_in_dict(op_json, ['follower', 'following']).values() # update_accounts_light.discard(first(accs)) # update_accounts_light.discard(second(accs)) # update_accounts_full.update(accs) return { 'accounts': list(update_accounts_full), 'accounts_light': list(update_accounts_light), 'comments': list(update_comments), }