async def get_post_header(context, author, permlink): """Fetch basic post data""" db = context['db'] valid_account(author) valid_permlink(permlink) sql = """ SELECT hp.id, ha_a.name as author, hpd_p.permlink as permlink, hcd.category as category, depth FROM hive_posts hp INNER JOIN hive_accounts ha_a ON ha_a.id = hp.author_id INNER JOIN hive_permlink_data hpd_p ON hpd_p.id = hp.permlink_id LEFT JOIN hive_category_data hcd ON hcd.id = hp.category_id WHERE ha_a.name = :author AND hpd_p.permlink = :permlink AND counter_deleted = 0 """ row = await db.query_row(sql, author=author, permlink=permlink) assert row, 'Post {}/{} does not exist'.format(author, permlink) return dict(author=row['author'], permlink=row['permlink'], category=row['category'], depth=row['depth'])
async def get_active_votes(context, author: str, permlink: str): """ Returns all votes for the given post. """ valid_account(author) valid_permlink(permlink) db = context['db'] return await find_votes_impl(db, author, permlink, VotesPresentation.ActiveVotes)
async def find_votes(context, author: str, permlink: str): """ Returns all votes for the given post """ valid_account(author) valid_permlink(permlink) return { 'votes': await find_votes_impl(context['db'], author, permlink, VotesPresentation.DatabaseApi) }
async def get_content_replies(context, author: str, permlink: str): """Get a list of post objects based on parent.""" db = context['db'] valid_account(author) valid_permlink(permlink) parent_id = await cursor.get_post_id(db, author, permlink) if parent_id: child_ids = await cursor.get_child_ids(db, parent_id) if child_ids: return await load_posts(db, child_ids) return []
async def get_content(context, author: str, permlink: str): """Get a single post object.""" db = context['db'] valid_account(author) valid_permlink(permlink) post_id = await cursor.get_post_id(db, author, permlink) if not post_id: return {'id': 0, 'author': '', 'permlink': ''} posts = await load_posts(db, [post_id]) assert posts, 'post was not found in cache' return posts[0]
async def get_discussions_by_comments(context, start_author: str, start_permlink: str = '', limit: int = 20, truncate_body: int = 0, filter_tags: list = None): """Get comments by made by author.""" assert not filter_tags, 'filter_tags not supported' start_author = valid_account(start_author) start_permlink = valid_permlink(start_permlink, allow_empty=True) limit = valid_limit(limit, 100, 20) truncate_body = valid_truncate(truncate_body) posts = [] db = context['db'] sql = "SELECT * FROM bridge_get_account_posts_by_comments( (:account)::VARCHAR, (:author)::VARCHAR, (:permlink)::VARCHAR, (:limit)::SMALLINT )" result = await db.query_all(sql, account=start_author, author=start_author if start_permlink else '', permlink=start_permlink, limit=limit) for row in result: row = dict(row) post = _condenser_post_object(row, truncate_body=truncate_body) post['active_votes'] = await find_votes_impl( db, post['author'], post['permlink'], VotesPresentation.CondenserApi) posts.append(post) return posts
async def get_post(context, author, permlink, observer=None): """Fetch a single post""" # pylint: disable=unused-variable #TODO: `observer` logic for user-post state db = context['db'] valid_account(author) valid_account(observer, allow_empty=True) valid_permlink(permlink) sql = "SELECT * FROM bridge_get_post( (:author)::VARCHAR, (:permlink)::VARCHAR )" result = await db.query_all(sql, author=author, permlink=permlink) post = _bridge_post_object(result[0]) post['active_votes'] = await find_votes_impl(db, author, permlink, VotesPresentation.BridgeApi) post = append_statistics_to_post(post, result[0], False) return post
async def get_discussions_by_blog(context, tag: str, start_author: str = '', start_permlink: str = '', limit: int = 20, truncate_body: int = 0, filter_tags: list = None): """Retrieve account's blog posts, including reblogs.""" assert not filter_tags, 'filter_tags not supported' tag = valid_account(tag) start_author = valid_account(start_author, allow_empty=True) start_permlink = valid_permlink(start_permlink, allow_empty=True) limit = valid_limit(limit, 100, 20) truncate_body = valid_truncate(truncate_body) sql = "SELECT * FROM bridge_get_account_posts_by_blog( (:account)::VARCHAR, (:author)::VARCHAR, (:permlink)::VARCHAR, (:limit)::INTEGER, False )" db = context['db'] result = await db.query_all(sql, account=tag, author=start_author, permlink=start_permlink, limit=limit) posts_by_id = [] for row in result: row = dict(row) post = _condenser_post_object(row, truncate_body=truncate_body) post['active_votes'] = await find_votes_impl( db, post['author'], post['permlink'], VotesPresentation.CondenserApi) posts_by_id.append(post) return posts_by_id
async def list_votes(context, start: list, limit: int = 1000, order: str = None): """ Returns all votes, starting with the specified voter and/or author and permlink. """ supported_order_list = ["by_comment_voter", "by_voter_comment"] assert not order is None, "missing a required argument: 'order'" assert order in supported_order_list, "Unsupported order, valid orders: {}".format( ", ".join(supported_order_list)) limit = valid_limit(limit, 1000, 1000) db = context['db'] if order == "by_voter_comment": assert len( start ) == 3, "Expecting 3 arguments in 'start' array: voter, optional page start author and permlink" voter = start[0] valid_account(voter) start_post_author = start[1] valid_account(start_post_author, allow_empty=True) start_post_permlink = start[2] valid_permlink(start_post_permlink, allow_empty=True) sql = "SELECT * FROM list_votes_by_voter_comment(:voter,:author,:permlink,:limit)" rows = await db.query_all(sql, voter=voter, author=start_post_author, permlink=start_post_permlink, limit=limit) else: assert len( start ) == 3, "Expecting 3 arguments in 'start' array: post author and permlink, optional page start voter" author = start[0] valid_account(author) permlink = start[1] valid_permlink(permlink) start_voter = start[2] valid_account(start_voter, allow_empty=True) sql = "SELECT * FROM list_votes_by_comment_voter(:voter,:author,:permlink,:limit)" rows = await db.query_all(sql, voter=start_voter, author=author, permlink=permlink, limit=limit) return {'votes': api_vote_info(rows, VotesPresentation.DatabaseApi)}
async def get_replies_by_last_update(context, start_author: str = None, start_permlink: str = '', limit: int = 20, truncate_body: int = 0): """Get all replies made to any of author's posts.""" assert start_author, '`start_author` cannot be blank' ids = await cursor.pids_by_replies_to_account( context['db'], valid_account(start_author), valid_permlink(start_permlink, allow_empty=True), valid_limit(limit, 100)) return await load_posts(context['db'], ids, truncate_body=truncate_body)
async def get_discussion(context, author: str, permlink: str, observer: str = ''): """Modified `get_state` thread implementation.""" db = context['db'] author = valid_account(author) permlink = valid_permlink(permlink) observer = valid_account(observer, allow_empty=True) sql = "SELECT * FROM bridge_get_discussion(:author,:permlink,:observer)" rows = await db.query_all(sql, author=author, permlink=permlink, observer=observer) if not rows or len(rows) == 0: return {} root_id = rows[0]['id'] all_posts = {} root_post = _bridge_post_object(rows[0]) root_post['active_votes'] = await find_votes_impl( db, rows[0]['author'], rows[0]['permlink'], VotesPresentation.BridgeApi) root_post = append_statistics_to_post(root_post, rows[0], False) root_post['replies'] = [] all_posts[root_id] = root_post parent_to_children_id_map = {} for index in range(1, len(rows)): parent_id = rows[index]['parent_id'] if parent_id not in parent_to_children_id_map: parent_to_children_id_map[parent_id] = [] parent_to_children_id_map[parent_id].append(rows[index]['id']) post = _bridge_post_object(rows[index]) post['active_votes'] = await find_votes_impl( db, rows[index]['author'], rows[index]['permlink'], VotesPresentation.BridgeApi) post = append_statistics_to_post(post, rows[index], False) post['replies'] = [] all_posts[post['post_id']] = post for key in parent_to_children_id_map: children = parent_to_children_id_map[key] post = all_posts[key] for child_id in children: post['replies'].append(_ref(all_posts[child_id])) #result has to be in form of dictionary of dictionaries {post_ref: post} results = {} for key in all_posts: post_ref = _ref(all_posts[key]) results[post_ref] = all_posts[key] return results
async def get_post(context, author, permlink, observer=None): """Fetch a single post""" # pylint: disable=unused-variable #TODO: `observer` logic db = context['db'] observer_id = await get_account_id(db, observer) if observer else None pid = await _get_post_id(db, valid_account(author), valid_permlink(permlink)) posts = await load_posts(db, [pid]) assert len(posts) == 1, 'cache post not found' return posts[0]
async def _get_content_replies_impl(db, fat_node_style, author: str, permlink: str): """Get a list of post objects based on parent.""" valid_account(author) valid_permlink(permlink) sql = "SELECT * FROM condenser_get_content_replies(:author, :permlink)" result = await db.query_all(sql, author=author, permlink=permlink) posts = [] for row in result: row = dict(row) post = _condenser_post_object(row, get_content_additions=fat_node_style) post['active_votes'] = await find_votes_impl( db, row['author'], row['permlink'], VotesPresentation.ActiveVotes if fat_node_style else VotesPresentation.CondenserApi) posts.append(post) return posts
async def get_discussion(context, author, permlink): """Modified `get_state` thread implementation.""" db = context['db'] author = valid_account(author) permlink = valid_permlink(permlink) root_id = await _get_post_id(db, author, permlink) if not root_id: return {} return await _load_discussion(db, root_id)
async def get_post_discussions_by_payout(context, start_author: str = '', start_permlink: str = '', limit: int = 20, tag: str = None, truncate_body: int = 0): """Query top-level posts, sorted by payout.""" ids = await cursor.pids_by_query( context['db'], 'payout', valid_account(start_author, allow_empty=True), valid_permlink(start_permlink, allow_empty=True), valid_limit(limit, 100), valid_tag(tag, allow_empty=True)) return await load_posts(context['db'], ids, truncate_body=truncate_body)
async def get_discussions_by_comments(context, start_author: str = None, start_permlink: str = '', limit: int = 20, truncate_body: int = 0, filter_tags: list = None): """Get comments by made by author.""" assert start_author, '`start_author` cannot be blank' assert not filter_tags, 'filter_tags not supported' ids = await cursor.pids_by_account_comments( context['db'], valid_account(start_author), valid_permlink(start_permlink, allow_empty=True), valid_limit(limit, 100)) return await load_posts(context['db'], ids, truncate_body=truncate_body)
async def get_account_posts(context, sort, account, start_author='', start_permlink='', limit=20, observer=None): """Get posts for an account -- blog, feed, comments, or replies.""" valid_sorts = ['blog', 'feed', 'posts', 'comments', 'replies', 'payout'] assert sort in valid_sorts, 'invalid account sort' assert account, 'account is required' db = context['db'] account = valid_account(account) start_author = valid_account(start_author, allow_empty=True) start_permlink = valid_permlink(start_permlink, allow_empty=True) start = (start_author, start_permlink) limit = valid_limit(limit, 100) # pylint: disable=unused-variable observer_id = await get_account_id(db, observer) if observer else None # TODO if sort == 'blog': ids = await cursor.pids_by_blog(db, account, *start, limit) posts = await load_posts(context['db'], ids) for post in posts: if post['author'] != account: post['reblogged_by'] = [account] return posts elif sort == 'feed': res = await cursor.pids_by_feed_with_reblog(db, account, *start, limit) return await load_posts_reblogs(context['db'], res) elif sort == 'posts': start = start if start_permlink else (account, None) assert account == start[ 0], 'comments - account must match start author' ids = await cursor.pids_by_posts(db, *start, limit) return await load_posts(context['db'], ids) elif sort == 'comments': start = start if start_permlink else (account, None) assert account == start[ 0], 'comments - account must match start author' ids = await cursor.pids_by_comments(db, *start, limit) return await load_posts(context['db'], ids) elif sort == 'replies': start = start if start_permlink else (account, None) ids = await cursor.pids_by_replies(db, *start, limit) return await load_posts(context['db'], ids) elif sort == 'payout': start = start if start_permlink else (account, None) ids = await cursor.pids_by_payout(db, account, *start, limit) return await load_posts(context['db'], ids)
async def _get_content_impl(db, fat_node_style, author: str, permlink: str, observer=None): """Get a single post object.""" valid_account(author) valid_permlink(permlink) sql = "SELECT * FROM condenser_get_content(:author, :permlink)" post = None result = await db.query_all(sql, author=author, permlink=permlink) if result: result = dict(result[0]) post = _condenser_post_object(result, 0, fat_node_style) post['active_votes'] = await find_votes_impl( db, author, permlink, VotesPresentation.ActiveVotes if fat_node_style else VotesPresentation.CondenserApi) return post
def split_url(url, allow_empty=False): """Validate and split a post url into author/permlink.""" if not url: assert allow_empty, 'url must be specified' return None assert isinstance(url, str), 'url must be a string' parts = url.split('/') assert len(parts) == 2, 'invalid url parts' author = valid_account(parts[0]) permlink = valid_permlink(parts[1]) return (author, permlink)
async def get_discussions_by_created(context, start_author: str = '', start_permlink: str = '', limit: int = 20, tag: str = None, truncate_body: int = 0, filter_tags: list = None): """Query posts, sorted by creation date.""" assert not filter_tags, 'filter_tags not supported' ids = await cursor.pids_by_query( context['db'], 'created', valid_account(start_author, allow_empty=True), valid_permlink(start_permlink, allow_empty=True), valid_limit(limit, 100), valid_tag(tag, allow_empty=True)) return await load_posts(context['db'], ids, truncate_body=truncate_body)
async def get_discussions_by_feed(context, tag: str = None, start_author: str = '', start_permlink: str = '', limit: int = 20, truncate_body: int = 0, filter_tags: list = None): """Retrieve account's personalized feed.""" assert tag, '`tag` cannot be blank' assert not filter_tags, 'filter_tags not supported' res = await cursor.pids_by_feed_with_reblog( context['db'], valid_account(tag), valid_account(start_author, allow_empty=True), valid_permlink(start_permlink, allow_empty=True), valid_limit(limit, 100)) return await load_posts_reblogs(context['db'], res, truncate_body=truncate_body)
async def get_account_posts(context, sort:str, account:str, start_author:str='', start_permlink:str='', limit:int=20, observer:str=None): """Get posts for an account -- blog, feed, comments, or replies.""" supported_sort_list = ['blog', 'feed', 'posts', 'comments', 'replies', 'payout'] assert sort in supported_sort_list, "Unsupported sort, valid sorts: {}".format(", ".join(supported_sort_list)) db = context['db'] account = valid_account(account) start_author = valid_account(start_author, allow_empty=True) start_permlink = valid_permlink(start_permlink, allow_empty=True) observer = valid_account(observer, allow_empty=True) limit = valid_limit(limit, 100, 20) sql = None account_posts = True # set when only posts (or reblogs) of given account are supposed to be in results if sort == 'blog': sql = "SELECT * FROM bridge_get_account_posts_by_blog( (:account)::VARCHAR, (:author)::VARCHAR, (:permlink)::VARCHAR, (:limit)::INTEGER, True )" elif sort == 'feed': sql = "SELECT * FROM bridge_get_by_feed_with_reblog((:account)::VARCHAR, (:author)::VARCHAR, (:permlink)::VARCHAR, (:limit)::INTEGER)" elif sort == 'posts': sql = "SELECT * FROM bridge_get_account_posts_by_posts( (:account)::VARCHAR, (:author)::VARCHAR, (:permlink)::VARCHAR, (:limit)::SMALLINT )" elif sort == 'comments': sql = "SELECT * FROM bridge_get_account_posts_by_comments( (:account)::VARCHAR, (:author)::VARCHAR, (:permlink)::VARCHAR, (:limit)::SMALLINT )" elif sort == 'replies': account_posts = False sql = "SELECT * FROM bridge_get_account_posts_by_replies( (:account)::VARCHAR, (:author)::VARCHAR, (:permlink)::VARCHAR, (:limit)::SMALLINT, True )" elif sort == 'payout': sql = "SELECT * FROM bridge_get_account_posts_by_payout( (:account)::VARCHAR, (:author)::VARCHAR, (:permlink)::VARCHAR, (:limit)::SMALLINT )" sql_result = await db.query_all(sql, account=account, author=start_author, permlink=start_permlink, limit=limit ) posts = [] for row in sql_result: post = _bridge_post_object(row) post['active_votes'] = await find_votes_impl(db, row['author'], row['permlink'], VotesPresentation.BridgeApi) if sort == 'blog': if post['author'] != account: post['reblogged_by'] = [account] elif sort == 'feed': reblogged_by = set(row['reblogged_by']) reblogged_by.discard(row['author']) # Eliminate original author of reblogged post if reblogged_by: reblogged_by_list = list(reblogged_by) reblogged_by_list.sort() post['reblogged_by'] = reblogged_by_list post = append_statistics_to_post(post, row, False if account_posts else row['is_pinned']) posts.append(post) return posts
async def get_discussions_by_feed(context, tag: str, start_author: str = '', start_permlink: str = '', limit: int = 20, truncate_body: int = 0, filter_tags: list = None, observer: str = None): """Retrieve account's personalized feed.""" assert not filter_tags, 'filter_tags not supported' return await get_discussions_by_feed_impl( context['db'], valid_account(tag), valid_account(start_author, allow_empty=True), valid_permlink(start_permlink, allow_empty=True), valid_limit(limit, 100, 20), valid_truncate(truncate_body), observer)
async def post_notifications(context, author: str, permlink: str, min_score: int = 25, last_id: int = None, limit: int = 100): """Load notifications for a specific post.""" # pylint: disable=too-many-arguments db = context['db'] valid_account(author) valid_permlink(permlink) min_score = valid_score(min_score, 100, 25) last_id = valid_number(last_id, 0, "last_id") limit = valid_limit(limit, 100, 100) sql_query = "SELECT * FROM post_notifications( (:author)::VARCHAR, (:permlink)::VARCHAR, (:min_score)::SMALLINT, (:last_id)::BIGINT, (:limit)::SMALLINT )" rows = await db.query_all(sql_query, author=author, permlink=permlink, min_score=min_score, last_id=last_id, limit=limit) return [_render(row) for row in rows]
async def get_replies_by_last_update(context, start_author: str, start_permlink: str = '', limit: int = 20, truncate_body: int = 0): """Get all replies made to any of author's posts.""" # despite the name time of last edit is not used, posts ranked by creation time (that is, their id) # note that in this call start_author has dual meaning: # - when there is only start_author it means account that authored posts that we seek replies to # - when there is also start_permlink it points to one of replies (last post of previous page) and # we'll be getting account like above in form of author of parent post to the post pointed by # given start_author+start_permlink return await cursor.get_by_replies_to_account( context['db'], valid_account(start_author), valid_permlink(start_permlink, allow_empty=True), valid_limit(limit, 100, 20), valid_truncate(truncate_body))
async def get_discussions_by_author_before_date(context, author: str = None, start_permlink: str = '', before_date: str = '', limit: int = 10): """Retrieve account's blog posts, without reblogs. NOTE: before_date is completely ignored, and it appears to be broken and/or completely ignored in steemd as well. This call is similar to get_discussions_by_blog but does NOT serve reblogs. """ # pylint: disable=invalid-name,unused-argument assert author, '`author` cannot be blank' ids = await cursor.pids_by_blog_without_reblog( context['db'], valid_account(author), valid_permlink(start_permlink, allow_empty=True), valid_limit(limit, 100)) return await load_posts(context['db'], ids)
async def get_ranked_posts(context, sort, start_author='', start_permlink='', limit=20, tag=None, observer=None): """Query posts, sorted by given method.""" db = context['db'] observer_id = await get_account_id(db, observer) if observer else None assert sort in [ 'trending', 'hot', 'created', 'promoted', 'payout', 'payout_comments', 'muted' ], 'invalid sort' ids = await cursor.pids_by_ranked( context['db'], sort, valid_account(start_author, allow_empty=True), valid_permlink(start_permlink, allow_empty=True), valid_limit(limit, 100), valid_tag(tag, allow_empty=True), observer_id) return await load_posts(context['db'], ids)
async def get_ranked_posts(context, sort:str, start_author:str='', start_permlink:str='', limit:int=20, tag:str='', observer:str=''): """Query posts, sorted by given method.""" supported_sort_list = ['trending', 'hot', 'created', 'promoted', 'payout', 'payout_comments', 'muted'] assert sort in supported_sort_list, "Unsupported sort, valid sorts: {}".format(", ".join(supported_sort_list)) db = context['db'] async def process_query_results( sql_result ): posts = [] for row in sql_result: post = _bridge_post_object(row) post['active_votes'] = await find_votes_impl(db, row['author'], row['permlink'], VotesPresentation.BridgeApi) post = append_statistics_to_post(post, row, row['is_pinned']) posts.append(post) return posts start_author = valid_account(start_author, allow_empty=True) start_permlink = valid_permlink(start_permlink, allow_empty=True) limit = valid_limit(limit, 100, 20) tag = valid_tag(tag, allow_empty=True) observer = valid_account(observer, allow_empty=(tag != "my")) if tag == "my": result = await _get_ranked_posts_for_observer_communities(db, sort, start_author, start_permlink, limit, observer) return await process_query_results(result) if tag and check_community(tag): result = await _get_ranked_posts_for_communities(db, sort, tag, start_author, start_permlink, limit, observer) return await process_query_results(result) if ( tag and tag != "all" ): result = await _get_ranked_posts_for_tag(db, sort, tag, start_author, start_permlink, limit, observer) return await process_query_results(result) result = await _get_ranked_posts_for_all(db, sort, start_author, start_permlink, limit, observer) return await process_query_results(result)
async def get_reblogged_by(context, author: str, permlink: str): """Get all rebloggers of a post.""" return await cursor.get_reblogged_by(context['db'], valid_account(author), valid_permlink(permlink))
async def get_state(context, path: str): """`get_state` reimplementation. See: https://github.com/steemit/steem/blob/06e67bd4aea73391123eca99e1a22a8612b0c47e/libraries/app/database_api.cpp#L1937 """ (path, part) = _normalize_path(path) db = context['db'] state = { 'feed_price': await _get_feed_price(db), 'props': await _get_props_lite(db), 'tags': {}, 'accounts': {}, 'content': {}, 'tag_idx': { 'trending': [] }, 'discussion_idx': { "": {} } } # account - `/@account/tab` (feed, blog, comments, replies) if part[0] and part[0][0] == '@': assert not part[1] == 'transfers', 'transfers API not served here' assert not part[2], 'unexpected account path[2] %s' % path if part[1] == '': part[1] = 'blog' account = valid_account(part[0][1:]) state['accounts'][account] = await _load_account(db, account) if part[1] in ACCOUNT_TAB_KEYS: key = ACCOUNT_TAB_KEYS[part[1]] posts = await _get_account_discussion_by_key(db, account, key) state['content'] = _keyed_posts(posts) state['accounts'][account][key] = list(state['content'].keys()) elif part[1] in ACCOUNT_TAB_IGNORE: pass # condenser no-op URLs else: # invalid/undefined case; probably requesting `@user/permlink`, # but condenser still relies on a valid response for redirect. state['error'] = 'invalid get_state account path %s' % path # discussion - `/category/@account/permlink` elif part[1] and part[1][0] == '@': author = valid_account(part[1][1:]) permlink = valid_permlink(part[2]) state['content'] = await _load_discussion(db, author, permlink) state['accounts'] = await _load_content_accounts( db, state['content'], True) # ranked posts - `/sort/category` elif part[0] in POST_LIST_SORTS: assert not part[2], "unexpected discussion path part[2] %s" % path sort = valid_sort(part[0]) tag = valid_tag(part[1].lower(), allow_empty=True) pids = await get_posts_by_given_sort(context, sort, '', '', 20, tag) state['content'] = _keyed_posts(pids) state['discussion_idx'] = {tag: {sort: list(state['content'].keys())}} state['tag_idx'] = { 'trending': await get_top_trending_tags_summary(context) } # tag "explorer" - `/tags` elif part[0] == "tags": assert not part[1] and not part[2], 'invalid /tags request' for tag in await get_trending_tags(context): state['tag_idx']['trending'].append(tag['name']) state['tags'][tag['name']] = tag elif part[0] in CONDENSER_NOOP_URLS: assert not part[1] and not part[2] else: raise ApiError('unhandled path: /%s' % path) return state