diff --git a/init.sql b/init.sql index 5bcf72e8..a0dc89ac 100644 --- a/init.sql +++ b/init.sql @@ -14,13 +14,13 @@ CREATE TABLE IF NOT EXISTS squeak ( n_nonce BIGINT NOT NULL, enc_content CHAR(2272) NOT NULL, -- Encrypted content length is always 1136 bytes (2272 hex characters). vch_script_sig bytea NOT NULL, - address VARCHAR(35) NOT NULL, -- Maximum length of a bitcoin address is 35. + author_address VARCHAR(35) NOT NULL, -- Maximum length of a bitcoin address is 35. vch_decryption_key bytea NOT NULL, block_header bytea ); CREATE INDEX IF NOT EXISTS idx_squeak_address - ON squeak(address); + ON squeak(author_address); CREATE TABLE IF NOT EXISTS profile ( profile_id SERIAL PRIMARY KEY, diff --git a/itests/test.py b/itests/test.py index 754c29fe..f55577cf 100644 --- a/itests/test.py +++ b/itests/test.py @@ -403,6 +403,53 @@ def run(): get_profile_by_address_response.squeak_profile.profile_name == "bob" ) + # Make another squeak as a reply + reply_1_squeak_response = admin_stub.MakeSqueak( + squeak_admin_pb2.MakeSqueakRequest( + profile_id=profile_id, + content="Reply #1", + replyto=make_squeak_hash, + ) + ) + reply_1_squeak_hash = reply_1_squeak_response.squeak_hash + print("Get reply #1 squeak hash: " + str(reply_1_squeak_hash)) + + # Make a second squeak as a reply + reply_2_squeak_response = admin_stub.MakeSqueak( + squeak_admin_pb2.MakeSqueakRequest( + profile_id=profile_id, + content="Reply #2", + replyto=reply_1_squeak_hash, + ) + ) + reply_2_squeak_hash = reply_2_squeak_response.squeak_hash + print("Get make reply squeak response: " + str(reply_2_squeak_response)) + + # Get the squeak and check that the reply field is correct + get_reply_squeak_display_response = admin_stub.GetSqueakDisplay( + squeak_admin_pb2.GetSqueakDisplayRequest( + squeak_hash=reply_2_squeak_hash, + ) + ) + print("Get reply squeak display entry response: " + str(get_reply_squeak_display_response)) + assert ( + get_reply_squeak_display_response.squeak_display_entry.squeak_hash == reply_2_squeak_hash + ) + assert ( + get_reply_squeak_display_response.squeak_display_entry.reply_to == reply_1_squeak_hash + ) + + # Get the ancestors of the latest reply squeak + get_ancestors_response = admin_stub.GetAncestorSqueakDisplays( + squeak_admin_pb2.GetAncestorSqueakDisplaysRequest( + squeak_hash=reply_2_squeak_hash, + ) + ) + print("Get ancestor squeak display entries response: " + str(get_ancestors_response)) + assert ( + len(get_ancestors_response.squeak_display_entries) == 3 + ) + if __name__ == "__main__": logging.basicConfig() diff --git a/proto/squeak_admin.proto b/proto/squeak_admin.proto index 48663535..60d44423 100644 --- a/proto/squeak_admin.proto +++ b/proto/squeak_admin.proto @@ -62,6 +62,10 @@ service SqueakAdmin { */ rpc GetAddressSqueakDisplays (GetAddressSqueakDisplaysRequest) returns (GetAddressSqueakDisplaysReply) {} + /** sqkadmin: `getaddresssqueakdisplays` + */ + rpc GetAncestorSqueakDisplays (GetAncestorSqueakDisplaysRequest) returns (GetAncestorSqueakDisplaysReply) {} + } message HelloRequest { @@ -226,3 +230,13 @@ message GetAddressSqueakDisplaysReply { /// Multiple squeak display entries repeated SqueakDisplayEntry squeak_display_entries = 1; } + +message GetAncestorSqueakDisplaysRequest { + /// Hash of the squeak. + string squeak_hash = 1; +} + +message GetAncestorSqueakDisplaysReply { + /// Multiple squeak display entries + repeated SqueakDisplayEntry squeak_display_entries = 1; +} diff --git a/squeakserver/admin/squeak_admin_server_handler.py b/squeakserver/admin/squeak_admin_server_handler.py index f3c51d9a..83b2a1c2 100644 --- a/squeakserver/admin/squeak_admin_server_handler.py +++ b/squeakserver/admin/squeak_admin_server_handler.py @@ -96,3 +96,14 @@ class SqueakAdminServerHandler(object): max_block, ) return squeak_entries_with_profile + + def handle_get_ancestor_squeak_display_entries(self, squeak_hash_str): + logger.info("Handle get ancestor squeak display entries for squeak hash: {}".format(squeak_hash_str)) + squeak_entries_with_profile = self.squeak_node.get_ancestor_squeak_entries_with_profile( + squeak_hash_str, + ) + logger.info("Got number of ancestor squeak entries: {}".format(len(squeak_entries_with_profile))) + logger.info("Got ancestor squeak display entries:") + for entry in squeak_entries_with_profile: + logger.info("Entry: {}".format(entry)) + return squeak_entries_with_profile diff --git a/squeakserver/admin/squeak_admin_server_servicer.py b/squeakserver/admin/squeak_admin_server_servicer.py index f72ed37c..d8790886 100644 --- a/squeakserver/admin/squeak_admin_server_servicer.py +++ b/squeakserver/admin/squeak_admin_server_servicer.py @@ -7,7 +7,7 @@ import grpc from proto import squeak_admin_pb2, squeak_admin_pb2_grpc -from squeakserver.server.util import get_hash +from squeakserver.server.util import get_hash, get_replyto logger = logging.getLogger(__name__) @@ -132,6 +132,20 @@ class SqueakAdminServerServicer(squeak_admin_pb2_grpc.SqueakAdminServicer): squeak_display_entries=squeak_display_msgs ) + def GetAncestorSqueakDisplays(self, request, context): + squeak_hash_str = request.squeak_hash + squeak_entries_with_profile = self.handler.handle_get_ancestor_squeak_display_entries( + squeak_hash_str, + ) + squeak_display_msgs = [ + self._squeak_entry_to_message(entry) + for entry in + squeak_entries_with_profile + ] + return squeak_admin_pb2.GetFollowedSqueakDisplaysReply( + squeak_display_entries=squeak_display_msgs + ) + def _squeak_entry_to_message(self, squeak_entry_with_profile): if squeak_entry_with_profile is None: return None @@ -144,6 +158,8 @@ class SqueakAdminServerServicer(squeak_admin_pb2_grpc.SqueakAdminServicer): is_author_known = squeak_profile is not None author_name = squeak_profile.profile_name if squeak_profile else None author_address = str(squeak.GetAddress()) + is_reply = squeak.is_reply + reply_to = get_replyto(squeak).hex() if is_reply else None return squeak_admin_pb2.SqueakDisplayEntry( squeak_hash=get_hash(squeak).hex(), is_unlocked=squeak.HasDecryptionKey(), @@ -153,6 +169,8 @@ class SqueakAdminServerServicer(squeak_admin_pb2_grpc.SqueakAdminServicer): is_author_known=is_author_known, author_name=author_name, author_address=author_address, + is_reply=is_reply, + reply_to=reply_to, ) def _squeak_profile_to_message(self, squeak_profile): diff --git a/squeakserver/node/squeak_node.py b/squeakserver/node/squeak_node.py index 8b5d7390..4ea5a4a8 100644 --- a/squeakserver/node/squeak_node.py +++ b/squeakserver/node/squeak_node.py @@ -161,3 +161,8 @@ class SqueakNode: min_block, max_block, ) + + def get_ancestor_squeak_entries_with_profile(self, squeak_hash_str): + return self.postgres_db.get_thread_ancestor_squeak_entries_with_profile( + squeak_hash_str, + ) diff --git a/squeakserver/server/postgres_db.py b/squeakserver/server/postgres_db.py index 46715bb8..4f85f574 100644 --- a/squeakserver/server/postgres_db.py +++ b/squeakserver/server/postgres_db.py @@ -50,7 +50,7 @@ class PostgresDb: def insert_squeak(self, squeak): """ Insert a new squeak. """ sql = """ - INSERT INTO squeak(hash, n_version, hash_enc_content, hash_reply_sqk, hash_block, n_block_height, vch_script_pub_key, vch_encryption_key, enc_data_key, iv, n_time, n_nonce, enc_content, vch_script_sig, address, vch_decryption_key) + INSERT INTO squeak(hash, n_version, hash_enc_content, hash_reply_sqk, hash_block, n_block_height, vch_script_pub_key, vch_encryption_key, enc_data_key, iv, n_time, n_nonce, enc_content, vch_script_sig, author_address, vch_decryption_key) VALUES(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING hash;""" @@ -98,7 +98,7 @@ class PostgresDb: sql = """ SELECT * FROM squeak LEFT JOIN profile - ON squeak.address=profile.address + ON squeak.author_address=profile.address WHERE squeak.hash=%s """ @@ -114,7 +114,7 @@ class PostgresDb: sql = """ SELECT * FROM squeak JOIN profile - ON squeak.address=profile.address + ON squeak.author_address=profile.address WHERE squeak.block_header IS NOT NULL AND profile.following=False ORDER BY n_block_height DESC, n_time DESC; @@ -129,9 +129,9 @@ class PostgresDb: sql = """ SELECT * FROM squeak JOIN profile - ON squeak.address=profile.address + ON squeak.author_address=profile.address WHERE squeak.block_header IS NOT NULL - AND squeak.address=%s + AND squeak.author_address=%s AND n_block_height >= %s AND n_block_height <= %s ORDER BY n_block_height DESC, n_time DESC; @@ -141,11 +141,32 @@ class PostgresDb: rows = curs.fetchall() return [self._parse_squeak_entry_with_profile(row) for row in rows] + def get_thread_ancestor_squeak_entries_with_profile(self, squeak_hash_str): + """ Get all reply ancestors of squeak hash. """ + sql = """ + WITH RECURSIVE is_thread_ancestor(n) AS ( + VALUES(%s) + UNION\n + SELECT hash_reply_sqk FROM squeak, is_thread_ancestor + WHERE squeak.hash=is_thread_ancestor.n + ) + SELECT * FROM squeak + JOIN is_thread_ancestor + ON squeak.hash=is_thread_ancestor.n + LEFT JOIN profile + ON squeak.author_address=profile.address + WHERE squeak.block_header IS NOT NULL; + """ + with self.get_cursor() as curs: + curs.execute(sql, (squeak_hash_str,)) + rows = curs.fetchall() + return [self._parse_squeak_entry_with_profile(row) for row in rows] + def lookup_squeaks(self, addresses, min_block, max_block): """ Lookup squeaks. """ sql = """ SELECT hash FROM squeak - WHERE address IN %s + WHERE author_address IN %s AND n_block_height >= %s AND n_block_height <= %s AND vch_decryption_key IS NOT NULL diff --git a/squeakserver/server/util.py b/squeakserver/server/util.py index 89cafa66..288bbfe1 100644 --- a/squeakserver/server/util.py +++ b/squeakserver/server/util.py @@ -7,6 +7,10 @@ def get_hash(squeak): return squeak.GetHash()[::-1] +def get_replyto(squeak): + return squeak.hashReplySqk + + def generate_offer_nonce(): return os.urandom(DATA_KEY_LENGTH)