diff --git a/.gitlab-ci.yaml b/.gitlab-ci.yaml index 2427c20a7685aa7336cd62bf8b0df4e3c1361a82..3408310a7e98c5f38b6119369507f66b7dad118f 100644 --- a/.gitlab-ci.yaml +++ b/.gitlab-ci.yaml @@ -73,7 +73,8 @@ hivemind_sync: script: - pip3 install --user --upgrade pip setuptools - - scripts/ci_sync.sh "$HIVEMIND_DB_NAME" "$HIVEMIND_POSTGRESQL_CONNECTION_STRING" "$HIVEMIND_SOURCE_HIVED_URL" $HIVEMIND_MAX_BLOCK $HIVEMIND_HTTP_PORT + # WARNING: hardcoded 5000017 for max block + - scripts/ci_sync.sh "$HIVEMIND_DB_NAME" "$HIVEMIND_POSTGRESQL_CONNECTION_STRING" "$HIVEMIND_SOURCE_HIVED_URL" 5000017 $HIVEMIND_HTTP_PORT artifacts: paths: diff --git a/hive/db/db_state.py b/hive/db/db_state.py index b6af6fa0596bcb83d9d8348f62228383eccf0eb3..3772593ecf3d3fbd2f8b9d80354b1a76bd318233 100644 --- a/hive/db/db_state.py +++ b/hive/db/db_state.py @@ -358,6 +358,14 @@ class DbState: time_end = perf_counter() log.info("[INIT] update_notification_cache executed in %.4fs", time_end - time_start) + time_start = perf_counter() + sql = """ + SELECT update_follow_count({}, {}); + """.format(last_imported_block, current_imported_block) + DbState.db().query_no_return(sql) + time_end = perf_counter() + log.info("[INIT] update_follow_count executed in %.4fs", time_end - time_start) + # Update a block num immediately DbState.db().query_no_return("UPDATE hive_state SET block_num = :block_num", block_num = current_imported_block) @@ -458,8 +466,8 @@ class DbState: cls._set_ver(9) if cls._ver == 9: - from hive.indexer.follow import Follow - Follow.force_recount() + #from hive.indexer.follow import Follow + #Follow.force_recount() cls._set_ver(10) if cls._ver == 10: diff --git a/hive/db/schema.py b/hive/db/schema.py index a1fc3ec4c3ae6a2e191b52fc8f53a89820311c37..56501b1579ad9292e3d4809e5e3234346cc96fab 100644 --- a/hive/db/schema.py +++ b/hive/db/schema.py @@ -606,8 +606,8 @@ def setup(db): "bridge_get_account_posts_by_blog.sql", "condenser_get_names_by_reblogged.sql", "condenser_get_discussions_by_comments.sql", - "condenser_get_account_reputations.sql" - + "condenser_get_account_reputations.sql", + "update_follow_count.sql" ] from os.path import dirname, realpath dir_path = dirname(realpath(__file__)) diff --git a/hive/db/sql_scripts/db_upgrade.sh b/hive/db/sql_scripts/db_upgrade.sh index 7a70e0d068ffd13faac77bf6e6b14b25f11c7aef..8879597185b51f7539692f0be8148dea6c2a65af 100755 --- a/hive/db/sql_scripts/db_upgrade.sh +++ b/hive/db/sql_scripts/db_upgrade.sh @@ -59,7 +59,8 @@ for sql in postgres_handle_view_changes.sql \ bridge_get_account_posts_by_blog.sql \ condenser_get_names_by_reblogged.sql \ condenser_get_discussions_by_comments.sql \ - condenser_get_account_reputations.sql + condenser_get_account_reputations.sql \ + update_follow_count.sql do echo Executing psql -U $1 -d $2 -f $sql diff --git a/hive/db/sql_scripts/update_follow_count.sql b/hive/db/sql_scripts/update_follow_count.sql new file mode 100644 index 0000000000000000000000000000000000000000..90376dda9d8e8dda680e110b7b6bcd96828e311f --- /dev/null +++ b/hive/db/sql_scripts/update_follow_count.sql @@ -0,0 +1,33 @@ +DROP FUNCTION IF EXISTS update_follow_count(hive_blocks.num%TYPE, hive_blocks.num%TYPE); +CREATE OR REPLACE FUNCTION update_follow_count( + in _first_block hive_blocks.num%TYPE, + in _last_block hive_blocks.num%TYPE +) +RETURNS VOID +LANGUAGE 'plpgsql' +AS +$BODY$ +BEGIN +UPDATE hive_accounts ha +SET + followers = data_set.followers_count, + following = data_set.following_count +FROM + ( + WITH data_cfe(user_id) AS ( + SELECT DISTINCT following FROM hive_follows WHERE block_num BETWEEN _first_block AND _last_block + UNION + SELECT DISTINCT follower FROM hive_follows WHERE block_num BETWEEN _first_block AND _last_block + ) + SELECT + data_cfe.user_id AS user_id, + (SELECT COUNT(1) FROM hive_follows hf1 WHERE hf1.following = data_cfe.user_id AND hf1.state = 1) AS followers_count, + (SELECT COUNT(1) FROM hive_follows hf2 WHERE hf2.follower = data_cfe.user_id AND hf2.state = 1) AS following_count + FROM + data_cfe + ) AS data_set(user_id, followers_count, following_count) +WHERE + ha.id = data_set.user_id; +END +$BODY$ +; \ No newline at end of file diff --git a/hive/indexer/accounts.py b/hive/indexer/accounts.py index 7af31b4c4fae3926a06af6ec130b322cb4470962..98c4f862de9f12ecacf74019158b73caaf81d077 100644 --- a/hive/indexer/accounts.py +++ b/hive/indexer/accounts.py @@ -83,8 +83,6 @@ class Accounts(DbAdapterHolder): """Check if an account name exists.""" if isinstance(names, str): return names in cls._ids - if isinstance(names, list): - return all(name in cls._ids for name in names) return False @classmethod diff --git a/hive/indexer/blocks.py b/hive/indexer/blocks.py index 7a94abcac2d8a5e55d30473bfee592f46123192a..f814e85549f8b8aea0575d28d9f7b5c194c4b80b 100644 --- a/hive/indexer/blocks.py +++ b/hive/indexer/blocks.py @@ -441,7 +441,8 @@ class Blocks: "SELECT update_hive_posts_api_helper({},{})".format(first_block, last_block), "SELECT update_feed_cache({}, {})".format(first_block, last_block), "SELECT update_hive_posts_mentions({}, {})".format(first_block, last_block), - "SELECT update_notification_cache({}, {}, {})".format(first_block, last_block, is_hour_action) + "SELECT update_notification_cache({}, {}, {})".format(first_block, last_block, is_hour_action), + "SELECT update_follow_count({}, {})".format(first_block, last_block), #,"SELECT update_account_reputations({}, {})".format(first_block, last_block) ] diff --git a/hive/indexer/follow.py b/hive/indexer/follow.py index 1dcc90cf6ab82e38770b39095d1eabb11866f096..dfbe742584a7961aae2750b4037a4d9dd5a84461 100644 --- a/hive/indexer/follow.py +++ b/hive/indexer/follow.py @@ -1,34 +1,21 @@ """Handles follow operations.""" import logging -from time import perf_counter as perf from funcy.seqs import first from hive.db.adapter import Db -from hive.db.db_state import DbState from hive.utils.misc import chunks from hive.indexer.accounts import Accounts from hive.indexer.db_adapter_holder import DbAdapterHolder from hive.utils.normalize import escape_characters + log = logging.getLogger(__name__) -FOLLOWERS = 'followers' -FOLLOWING = 'following' DB = Db.instance() -def _flip_dict(dict_to_flip): - """Swap keys/values. Returned dict values are array of keys.""" - flipped = {} - for key, value in dict_to_flip.items(): - if value in flipped: - flipped[value].append(key) - else: - flipped[value] = [key] - return flipped - class Follow(DbAdapterHolder): """Handles processing of incoming follow ups and flushing to db.""" @@ -72,38 +59,40 @@ class Follow(DbAdapterHolder): block_num=block_num) cls.idx += 1 - if not DbState.is_initial_sync(): - for following in op['flg']: - new_state = state - old_state = cls._get_follow_db_state(op['flr'], following) - if new_state == 1: - Follow.follow(op['flr'], following) - if old_state == 1: - Follow.unfollow(op['flr'], following) - if state > 8: # check if given state exists in dict # if exists add follower to a list for a given state # if not exists create list and set that list for given state if state in cls.follow_update_items_to_flush: - cls.follow_update_items_to_flush[state].append(op['flr']) + cls.follow_update_items_to_flush[state].append((op['flr'], block_num)) else: - cls.follow_update_items_to_flush[state] = [op['flr']] + cls.follow_update_items_to_flush[state] = [(op['flr'], block_num)] @classmethod def _validated_op(cls, account, op, date): """Validate and normalize the operation.""" - if ( not 'what' in op + if (not 'what' in op or not isinstance(op['what'], list) or not 'follower' in op or not 'following' in op): return None + # follower/following is empty + if not op['follower'] or not op['following']: + return None + op['following'] = op['following'] if isinstance(op['following'], list) else [op['following']] + # mimic original behaviour + # if following name does not exist do not process it: basically equal to drop op for single following entry - # follower/following is empty - if not op['follower'] or not op['following']: + op['following'] = [op for op in op['following'] if Accounts.exists(op)] + + # if follower name does not exist drop op + if not Accounts.exists(op['follower']): + return None + + if op['follower'] in op['following'] or op['follower'] != account: return None what = first(op['what']) or '' @@ -115,59 +104,13 @@ class Follow(DbAdapterHolder): if what not in defs: return None - all_accounts = list(op['following']) - all_accounts.append(op['follower']) - if (op['follower'] in op['following'] - or op['follower'] != account): - return None - - non_existent_names = Accounts.check_names(all_accounts) - if non_existent_names: - log.warning("Follow op validation, following names does not exists in database: {}".format(non_existent_names)) - return dict(flr=escape_characters(op['follower']), flg=[escape_characters(following) for following in op['following']], state=defs[what], at=date) @classmethod - def _get_follow_db_state(cls, follower, following): - """Retrieve current follow state of an account pair.""" - sql = """ - SELECT - state - FROM - hive_follows hf - INNER JOIN hive_accounts ha_flr ON ha_flr.id = hf.follower AND ha_flr.name = :follower - INNER JOIN hive_accounts ha_flg ON ha_flg.id = hf.following AND ha_flg.name = :following - """ - return cls.db.query_one(sql, follower=follower, following=following) - - # -- stat tracking -- - - _delta = {FOLLOWERS: {}, FOLLOWING: {}} - - @classmethod - def follow(cls, follower, following): - """Applies follow count change the next flush.""" - cls._apply_delta(follower, FOLLOWING, 1) - cls._apply_delta(following, FOLLOWERS, 1) - - @classmethod - def unfollow(cls, follower, following): - """Applies follow count change the next flush.""" - cls._apply_delta(follower, FOLLOWING, -1) - cls._apply_delta(following, FOLLOWERS, -1) - - @classmethod - def _apply_delta(cls, account, role, direction): - """Modify an account's follow delta in specified direction.""" - if not account in cls._delta[role]: - cls._delta[role][account] = 0 - cls._delta[role][account] += direction - - @classmethod - def _flush_follow_items(cls): + def flush(cls): n = 0 if cls.follow_items_to_flush: sql_prefix = """ @@ -217,7 +160,8 @@ class Follow(DbAdapterHolder): WHEN 7 THEN TRUE WHEN 8 THEN FALSE ELSE EXCLUDED.follow_muted - END) + END), + block_num = EXCLUDED.block_num WHERE hf.following = EXCLUDED.following AND hf.follower = EXCLUDED.follower """ values = [] @@ -270,7 +214,7 @@ class Follow(DbAdapterHolder): for state, update_flush_items in cls.follow_update_items_to_flush.items(): for chunk in chunks(update_flush_items, 1000): sql = None - query_values = ','.join(["({})".format(account) for account in chunk]) + query_values = ','.join(["({}, {})".format(account[0], account[1]) for account in chunk]) # [DK] probaly not a bad idea to move that logic to SQL function if state == 9: #reset blacklists for follower @@ -278,18 +222,20 @@ class Follow(DbAdapterHolder): UPDATE hive_follows hf SET - blacklisted = false + blacklisted = false, + block_num = ds.block_num FROM ( SELECT - ha.id as follower_id + ha.id as follower_id, + block_num FROM ( VALUES {} - ) AS T(name) + ) AS T(name, block_num) INNER JOIN hive_accounts ha ON ha.name = T.name - ) AS ds (follower_id) + ) AS ds (follower_id, block_num) WHERE hf.follower = ds.follower_id """.format(query_values) @@ -299,18 +245,20 @@ class Follow(DbAdapterHolder): UPDATE hive_follows hf SET - state = 0 + state = 0, + block_num = ds.block_num FROM ( SELECT - ha.id as follower_id + ha.id as follower_id, + block_num FROM ( VALUES {} - ) AS T(name) + ) AS T(name, block_num) INNER JOIN hive_accounts ha ON ha.name = T.name - ) AS ds (follower_id) + ) AS ds (follower_id, block_num) WHERE hf.follower = ds.follower_id AND hf.state = 1 @@ -321,18 +269,20 @@ class Follow(DbAdapterHolder): UPDATE hive_follows hf SET - state = 0 + state = 0, + block_num = ds.block_num FROM ( SELECT - ha.id as follower_id + ha.id as follower_id, + block_num FROM ( VALUES {} - ) AS T(name) + ) AS T(name, block_num) INNER JOIN hive_accounts ha ON ha.name = T.name - ) AS ds (follower_id) + ) AS ds (follower_id, block_num) WHERE hf.follower = ds.follower_id AND hf.state = 2 @@ -343,36 +293,40 @@ class Follow(DbAdapterHolder): UPDATE hive_follows hf SET - follow_blacklists = false + follow_blacklists = false, + block_num = ds.block_num FROM ( SELECT - ha.id as follower_id + ha.id as follower_id, + block_num FROM ( VALUES {0} - ) AS T(name) + ) AS T(name, block_num) INNER JOIN hive_accounts ha ON ha.name = T.name - ) AS ds (follower_id) + ) AS ds (follower_id, block_num) WHERE hf.follower = ds.follower_id; UPDATE hive_follows hf SET - follow_blacklists = true + follow_blacklists = true, + block_num = ds.block_num FROM ( SELECT - ha.id as follower_id + ha.id as follower_id, + block_num FROM ( VALUES {0} - ) AS T(name) + ) AS T(name, block_num) INNER JOIN hive_accounts ha ON ha.name = T.name - ) AS ds (follower_id) + ) AS ds (follower_id, block_num) WHERE hf.follower = ds.follower_id AND following = (SELECT id FROM hive_accounts WHERE name = 'null') @@ -384,36 +338,40 @@ class Follow(DbAdapterHolder): UPDATE hive_follows hf SET - follow_muted = false + follow_muted = false, + block_num = ds.block_num FROM ( SELECT - ha.id as follower_id + ha.id as follower_id, + block_num FROM ( VALUES {0} - ) AS T(name) + ) AS T(name, block_num) INNER JOIN hive_accounts ha ON ha.name = T.name - ) AS ds (follower_id) + ) AS ds (follower_id, block_num) WHERE hf.follower = ds.follower_id; UPDATE hive_follows hf SET - follow_muted = true + follow_muted = true, + block_num = ds.block_num FROM ( SELECT - ha.id as follower_id + ha.id as follower_id, + block_num FROM ( VALUES {0} - ) AS T(name) + ) AS T(name, block_num) INNER JOIN hive_accounts ha ON ha.name = T.name - ) AS ds (follower_id) + ) AS ds (follower_id, block_num) WHERE hf.follower = ds.follower_id AND following = (SELECT id FROM hive_accounts WHERE name = 'null') @@ -427,18 +385,20 @@ class Follow(DbAdapterHolder): blacklisted = false, follow_blacklists = false, follow_muted = false, - state = 0 + state = 0, + block_num = ds.block_num FROM ( SELECT - ha.id as follower_id + ha.id as follower_id, + block_num FROM ( VALUES {0} - ) AS T(name) + ) AS T(name, block_num) INNER JOIN hive_accounts ha ON ha.name = T.name - ) AS ds (follower_id) + ) AS ds (follower_id, block_num) WHERE hf.follower = ds.follower_id; @@ -446,18 +406,20 @@ class Follow(DbAdapterHolder): hive_follows hf SET follow_blacklists = true, - follow_muted = true + follow_muted = true, + block_num = ds.block_num FROM ( SELECT - ha.id as follower_id + ha.id as follower_id, + block_num FROM ( VALUES {0} - ) AS T(name) + ) AS T(name, block_num) INNER JOIN hive_accounts ha ON ha.name = T.name - ) AS ds (follower_id) + ) AS ds (follower_id, block_num) WHERE hf.follower = ds.follower_id AND following = (SELECT id FROM hive_accounts WHERE name = 'null') @@ -470,95 +432,3 @@ class Follow(DbAdapterHolder): cls.follow_update_items_to_flush.clear() cls.idx = 0 return n - - @classmethod - def flush(cls, trx=False): - """Flushes pending follow count deltas.""" - - n = cls._flush_follow_items() - - updated = 0 - sqls = [] - for col, deltas in cls._delta.items(): - for delta, names in _flip_dict(deltas).items(): - updated += len(names) - query_values = ','.join(["({})".format(account) for account in names]) - sql = """ - UPDATE - hive_accounts ha - SET - %s = %s + :mag - FROM - ( - VALUES - %s - ) AS T(name) - WHERE ha.name = T.name - """ - sqls.append((sql % (col, col, query_values), dict(mag=delta))) - - if not updated: - return n - - start = perf() - cls.db.batch_queries(sqls, trx=trx) - if trx: - log.info("[SYNC] flushed %d follow deltas in %ds", - updated, perf() - start) - - cls._delta = {FOLLOWERS: {}, FOLLOWING: {}} - return updated + n - - @classmethod - def flush_recount(cls): - """Recounts follows/following counts for all queued accounts. - - This is currently not used; this approach was shown to be too - expensive, but it's useful in case follow counts manage to get - out of sync. - """ - names = set([*cls._delta[FOLLOWERS].keys(), - *cls._delta[FOLLOWING].keys()]) - query_values = ','.join(["({})".format(account) for account in names]) - sql = """ - UPDATE - hive_accounts ha - SET - followers = (SELECT COUNT(*) FROM hive_follows WHERE state = 1 AND following = ha.id), - following = (SELECT COUNT(*) FROM hive_follows WHERE state = 1 AND follower = ha.id) - FROM - ( - VALUES - {} - ) AS T(name) - WHERE ha.name = T.name - """.format(query_values) - cls.db.query(sql) - - @classmethod - def force_recount(cls): - """Recounts all follows after init sync.""" - log.info("[SYNC] query follower counts") - sql = """ - CREATE TEMPORARY TABLE following_counts AS ( - SELECT ha.id account_id, COUNT(state) num - FROM hive_accounts ha - LEFT JOIN hive_follows hf ON ha.id = hf.follower AND state = 1 - GROUP BY ha.id); - CREATE TEMPORARY TABLE follower_counts AS ( - SELECT ha.id account_id, COUNT(state) num - FROM hive_accounts ha - LEFT JOIN hive_follows hf ON ha.id = hf.following AND state = 1 - GROUP BY ha.id); - """ - cls.db.query(sql) - - log.info("[SYNC] update follower counts") - sql = """ - UPDATE hive_accounts SET followers = num FROM follower_counts - WHERE id = account_id AND followers != num; - - UPDATE hive_accounts SET following = num FROM following_counts - WHERE id = account_id AND following != num; - """ - cls.db.query(sql) diff --git a/hive/indexer/sync.py b/hive/indexer/sync.py index 2f6813c23b84ea64a124f9987c090e56661d5d9c..05c63d4d3c4f8956e6fd4ea14291ba70d91dce80 100644 --- a/hive/indexer/sync.py +++ b/hive/indexer/sync.py @@ -297,9 +297,6 @@ class Sync: if not CONTINUE_PROCESSING: return - log.info("[INIT] *** Initial cache build ***") - Follow.force_recount() - def from_steemd(self, is_initial_sync=False, chunk_size=1000): """Fast sync strategy: read/process blocks in batches.""" steemd = self._steem diff --git a/hive/utils/normalize.py b/hive/utils/normalize.py index ee7b220167553f5e3d1d786021c0a2c89c96a189..f0a2f61421d6a3c1d09ff0e3dc1e5fe340b2398d 100644 --- a/hive/utils/normalize.py +++ b/hive/utils/normalize.py @@ -68,6 +68,7 @@ def to_nai(value): def escape_characters(text): """ Escape special charactes """ + assert isinstance(text, str), "Expected string got: {}".format(type(text)) if len(text.strip()) == 0: return "'" + text + "'" diff --git a/mock_data/block_data/follow_op/mock_block_data_follow.json b/mock_data/block_data/follow_op/mock_block_data_follow.json index fd6cb77fc7d31c7cd55775cc7467550dd6429ca8..d2a7532821ebb878460e8c5a283e6d1dde318702 100644 --- a/mock_data/block_data/follow_op/mock_block_data_follow.json +++ b/mock_data/block_data/follow_op/mock_block_data_follow.json @@ -269,6 +269,17 @@ "json": "[\"follow\",{\"follower\":\"tester1\",\"following\":[\"t'es'ter3\", \"<html><body><p>PPPPP</p></body></html>\"],\"what\":[\"blog\"]}]" } }, + { + "type": "custom_json_operation", + "value": { + "required_auths": [], + "required_posting_auths": [ + "tester1" + ], + "id": "follow", + "json": "[\"follow\",{\"follower\":\"tester1\",\"following\":[\"tester7\", \"<script>alert('hello world');</script>\"],\"what\":[\"blog\"]}]" + } + }, { "type": "custom_json_operation", "value": { @@ -352,7 +363,7 @@ "tester1" ], "id": "follow", - "json": "[\"follow\",{\"follower\":\"tester1\",\"following\":[\"tester3\", \"tester4\"],\"what\":[\"blogo-doggo\"]}]" + "json": "[\"follow\",{\"follower\":\"tester1\",\"following\":[\"tester3\", \"gtg\"],\"what\":[\"blogo-doggo\"]}]" } }, { @@ -363,7 +374,84 @@ "te'%@ter1" ], "id": "follow", - "json": "[\"follow\",{\"follower\":\"te'%@ter1\",\"following\":[\"tester3\", \"tester4\"],\"what\":[\"blog\"]}]" + "json": "[\"follow\",{\"follower\":\"te'%@ter1\",\"following\":[\"gtg\", \"tester4\"],\"what\":[\"blog\"]}]" + } + }, + { + "type": "custom_json_operation", + "value": { + "required_auths": [], + "required_posting_auths": [ + "{\"tester1\":\"tester1\"}" + ], + "id": "follow", + "json": "[\"follow\",{\"follower\":{\"tester1\":\"tester1\"},\"following\":{\"gtg\":\"gtg\"},\"what\":[\"blog\"]}]" + } + }, + { + "type": "custom_json_operation", + "value": { + "required_auths": [], + "required_posting_auths": [ + "tester1" + ], + "id": "follow", + "json": "[\"follow\",{\"follower\":\"tester1\",\"following\":{\"gtg\":\"gtg\"},\"what\":[\"blog\"]}]" + } + }, + { + "type": "custom_json_operation", + "value": { + "required_auths": [], + "required_posting_auths": [ + "tester1" + ], + "id": "follow", + "json": "[\"follow\",{\"follower\":\"tester1\",\"following\":[\"tester3\", [\"gtg\"]],\"what\":[\"blog\"]}]" + } + }, + { + "type": "custom_json_operation", + "value": { + "required_auths": [], + "required_posting_auths": [ + "tester1" + ], + "id": "follow", + "json": "[\"follow\",{\"follower\":[\"tester1\"],\"following\":[\"tester3\", [\"gtg\"]],\"what\":[\"blog\"]}]" + } + }, + { + "type": "custom_json_operation", + "value": { + "required_auths": [], + "required_posting_auths": [ + "[\"tester1\"]" + ], + "id": "follow", + "json": "[\"follow\",{\"follower\":[\"tester1\"],\"following\":[\"tester3\", {\"gtg\":\"gtg\"}],\"what\":[\"blog\"]}]" + } + }, + { + "type": "custom_json_operation", + "value": { + "required_auths": [], + "required_posting_auths": [ + "tester1" + ], + "id": "follow", + "json": "[\"follow\",{\"follower\":\"tester1\",\"following\":[\"tester3\", {\"gtg\":\"gtg\"}],\"what\":[\"blog\"]}]" + } + }, + { + "type": "custom_json_operation", + "value": { + "required_auths": [], + "required_posting_auths": [ + "tester1" + ], + "id": "follow", + "json": "[\"follow\",{\"follower\":\"tester1\",\"following\":[\"tester7\", \"<script>alert('hello world');</script>\"],\"what\":[\"blog\"]}]" } } ] diff --git a/tests/tests_api b/tests/tests_api index 97e34ea617ccb9c50e46e68cb2302108e5264e84..f8ac3e6776ef31f23d14a746b6ba6c281382e9cc 160000 --- a/tests/tests_api +++ b/tests/tests_api @@ -1 +1 @@ -Subproject commit 97e34ea617ccb9c50e46e68cb2302108e5264e84 +Subproject commit f8ac3e6776ef31f23d14a746b6ba6c281382e9cc