diff --git a/itests/tests/conftest.py b/itests/tests/conftest.py index aa8feb7f..c5e56adb 100644 --- a/itests/tests/conftest.py +++ b/itests/tests/conftest.py @@ -1,3 +1,4 @@ +import os import uuid import grpc @@ -7,6 +8,7 @@ from squeak.params import SelectParams from proto import squeak_admin_pb2 from proto import squeak_admin_pb2_grpc from proto import squeak_server_pb2_grpc +from tests.util import bytes_to_base64_string from tests.util import generate_signing_key from tests.util import get_address from tests.util import load_lightning_client @@ -143,3 +145,13 @@ def peer_id(server_stub, admin_stub): @pytest.fixture def random_name(): yield "random_name_{}".format(uuid.uuid1()) + + +@pytest.fixture +def random_image(): + yield os.urandom(567) + + +@pytest.fixture +def random_image_base64_string(random_image): + yield bytes_to_base64_string(random_image) diff --git a/itests/tests/test_squeak_node.py b/itests/tests/test_squeak_node.py index f97cb314..9256b660 100644 --- a/itests/tests/test_squeak_node.py +++ b/itests/tests/test_squeak_node.py @@ -475,14 +475,6 @@ def test_make_contact_profile(server_stub, admin_stub): def test_set_profile_following(server_stub, admin_stub, contact_profile_id): - # Get the existing profile - get_squeak_profile_response = admin_stub.GetSqueakProfile( - squeak_admin_pb2.GetSqueakProfileRequest( - profile_id=contact_profile_id, - ) - ) - assert not get_squeak_profile_response.squeak_profile.following - # Set the profile to be following admin_stub.SetSqueakProfileFollowing( squeak_admin_pb2.SetSqueakProfileFollowingRequest( @@ -499,16 +491,24 @@ def test_set_profile_following(server_stub, admin_stub, contact_profile_id): ) assert get_squeak_profile_response.squeak_profile.following + # Set the profile to be not following + admin_stub.SetSqueakProfileFollowing( + squeak_admin_pb2.SetSqueakProfileFollowingRequest( + profile_id=contact_profile_id, + following=False, + ) + ) -def test_set_profile_sharing(server_stub, admin_stub, contact_profile_id): - # Get the existing profile + # Get the squeak profile again get_squeak_profile_response = admin_stub.GetSqueakProfile( squeak_admin_pb2.GetSqueakProfileRequest( profile_id=contact_profile_id, ) ) - assert not get_squeak_profile_response.squeak_profile.sharing + assert not get_squeak_profile_response.squeak_profile.following + +def test_set_profile_sharing(server_stub, admin_stub, contact_profile_id): # Set the profile to be sharing admin_stub.SetSqueakProfileSharing( squeak_admin_pb2.SetSqueakProfileSharingRequest( @@ -525,6 +525,22 @@ def test_set_profile_sharing(server_stub, admin_stub, contact_profile_id): ) assert get_squeak_profile_response.squeak_profile.sharing + # Set the profile to be not sharing + admin_stub.SetSqueakProfileSharing( + squeak_admin_pb2.SetSqueakProfileSharingRequest( + profile_id=contact_profile_id, + sharing=False, + ) + ) + + # Get the squeak profile again + get_squeak_profile_response = admin_stub.GetSqueakProfile( + squeak_admin_pb2.GetSqueakProfileRequest( + profile_id=contact_profile_id, + ) + ) + assert not get_squeak_profile_response.squeak_profile.sharing + def test_rename_profile(server_stub, admin_stub, contact_profile_id, random_name): # Rename the profile to something new @@ -544,6 +560,49 @@ def test_rename_profile(server_stub, admin_stub, contact_profile_id, random_name assert get_squeak_profile_response.squeak_profile.profile_name == random_name +def test_set_profile_image(server_stub, admin_stub, contact_profile_id, random_image, random_image_base64_string): + print("random_image: {}".format(random_image)) + print("random_image_base64_string: {}".format(random_image_base64_string)) + # Set the profile image to something new + admin_stub.SetSqueakProfileImage( + squeak_admin_pb2.SetSqueakProfileImageRequest( + profile_id=contact_profile_id, + profile_image=random_image, + ) + ) + + # Get the squeak profile + get_squeak_profile_response = admin_stub.GetSqueakProfile( + squeak_admin_pb2.GetSqueakProfileRequest( + profile_id=contact_profile_id, + ) + ) + print("get_squeak_profile_response.squeak_profile.profile_image: {}".format( + get_squeak_profile_response.squeak_profile.profile_image, + )) + assert get_squeak_profile_response.squeak_profile.profile_image == random_image_base64_string + assert get_squeak_profile_response.squeak_profile.has_custom_profile_image + + # Clear the profile image + admin_stub.ClearSqueakProfileImage( + squeak_admin_pb2.ClearSqueakProfileImageRequest( + profile_id=contact_profile_id, + ) + ) + + # Get the squeak profile + get_squeak_profile_response = admin_stub.GetSqueakProfile( + squeak_admin_pb2.GetSqueakProfileRequest( + profile_id=contact_profile_id, + ) + ) + print("get_squeak_profile_response.squeak_profile.profile_image: {}".format( + get_squeak_profile_response.squeak_profile.profile_image, + )) + assert get_squeak_profile_response.squeak_profile.profile_image != random_image_base64_string + assert not get_squeak_profile_response.squeak_profile.has_custom_profile_image + + def test_delete_profile(server_stub, admin_stub, contact_profile_id): # Delete the profile admin_stub.DeleteSqueakProfile( diff --git a/itests/tests/util.py b/itests/tests/util.py index 2576dc33..7ec140d6 100644 --- a/itests/tests/util.py +++ b/itests/tests/util.py @@ -1,5 +1,6 @@ from __future__ import print_function +import base64 import time from contextlib import contextmanager @@ -100,6 +101,11 @@ def subtract_tweak(n, tweak): return scalar_to_bytes(sum_int) +def bytes_to_base64_string(data: bytes) -> str: + encoded_string = base64.b64encode(data) + return encoded_string.decode('utf-8') + + @contextmanager def connect_peer(lightning_client, lightning_host, remote_pubkey): # Connect the peer diff --git a/proto/squeak_admin.proto b/proto/squeak_admin.proto index fded4242..c39de303 100644 --- a/proto/squeak_admin.proto +++ b/proto/squeak_admin.proto @@ -116,6 +116,14 @@ service SqueakAdmin { */ rpc DeleteSqueakProfile (DeleteSqueakProfileRequest) returns (DeleteSqueakProfileReply) {} + /** sqkadmin: `setsqueakprofileimage` + */ + rpc SetSqueakProfileImage (SetSqueakProfileImageRequest) returns (SetSqueakProfileImageReply) {} + + /** sqkadmin: `clearsqueakprofileimage` + */ + rpc ClearSqueakProfileImage (ClearSqueakProfileImageRequest) returns (ClearSqueakProfileImageReply) {} + /** sqkadmin: `makesqueak` */ rpc MakeSqueak (MakeSqueakRequest) returns (MakeSqueakReply) {} @@ -359,6 +367,28 @@ message DeleteSqueakProfileRequest { message DeleteSqueakProfileReply { } +message SetSqueakProfileImageRequest { + /// The profile id + int32 profile_id = 1; + + /// The profile image + bytes profile_image = 2; +} + +message SetSqueakProfileImageReply { +} + +message ClearSqueakProfileImageRequest { + /// The profile id + int32 profile_id = 1; + + /// The profile image + bytes profile_image = 2; +} + +message ClearSqueakProfileImageReply { +} + message SqueakProfile { /// The profile id int32 profile_id = 1; @@ -377,6 +407,12 @@ message SqueakProfile { /// Following bool following = 6; + + /// The profile image + string profile_image = 7; + + /// Has custom profile image + bool has_custom_profile_image = 8; } message MakeSqueakRequest { diff --git a/squeaknode/admin/squeak_admin_server_handler.py b/squeaknode/admin/squeak_admin_server_handler.py index 07fbddfc..3488565e 100644 --- a/squeaknode/admin/squeak_admin_server_handler.py +++ b/squeaknode/admin/squeak_admin_server_handler.py @@ -145,7 +145,11 @@ class SqueakAdminServerHandler(object): squeak_profile = self.squeak_controller.get_squeak_profile(profile_id) if squeak_profile is None: return None + logger.info("Got squeak profile with image: {}".format( + squeak_profile.profile_image)) squeak_profile_msg = squeak_profile_to_message(squeak_profile) + logger.info("Got squeak profile msg with image: {}".format( + squeak_profile_msg.profile_image)) return squeak_admin_pb2.GetSqueakProfileReply( squeak_profile=squeak_profile_msg, ) @@ -215,6 +219,35 @@ class SqueakAdminServerHandler(object): self.squeak_controller.delete_squeak_profile(profile_id) return squeak_admin_pb2.DeleteSqueakProfileReply() + def handle_set_squeak_profile_image(self, request): + profile_id = request.profile_id + profile_image = request.profile_image + logger.info( + "Handle set squeak profile image with profile id: {}".format( + profile_id, + ) + ) + logger.info( + "Handle set squeak profile image with image: {}".format( + profile_image, + ) + ) + self.squeak_controller.set_squeak_profile_image( + profile_id, profile_image) + return squeak_admin_pb2.SetSqueakProfileImageReply() + + def handle_clear_squeak_profile_image(self, request): + profile_id = request.profile_id + logger.info( + "Handle clear squeak profile image with profile id: {}".format( + profile_id, + ) + ) + self.squeak_controller.clear_squeak_profile_image( + profile_id, + ) + return squeak_admin_pb2.ClearSqueakProfileImageReply() + def handle_get_squeak_profile_private_key(self, request): profile_id = request.profile_id logger.info( diff --git a/squeaknode/admin/squeak_admin_server_servicer.py b/squeaknode/admin/squeak_admin_server_servicer.py index 09d82bbf..670444b1 100644 --- a/squeaknode/admin/squeak_admin_server_servicer.py +++ b/squeaknode/admin/squeak_admin_server_servicer.py @@ -97,6 +97,12 @@ class SqueakAdminServerServicer(squeak_admin_pb2_grpc.SqueakAdminServicer): def DeleteSqueakProfile(self, request, context): return self.handler.handle_delete_squeak_profile(request) + def SetSqueakProfileImage(self, request, context): + return self.handler.handle_set_squeak_profile_image(request) + + def ClearSqueakProfileImage(self, request, context): + return self.handler.handle_clear_squeak_profile_image(request) + def GetSqueakProfilePrivateKey(self, request, context): return self.handler.handle_get_squeak_profile_private_key(request) diff --git a/squeaknode/admin/util.py b/squeaknode/admin/util.py index 75b392e4..987a91ed 100644 --- a/squeaknode/admin/util.py +++ b/squeaknode/admin/util.py @@ -54,6 +54,9 @@ def squeak_profile_to_message(squeak_profile): if squeak_profile is None: return None has_private_key = squeak_profile.private_key is not None + profile_image = squeak_profile.profile_image or DEFAULT_PROFILE_IMAGE + has_custom_profile_image = squeak_profile.profile_image is not None + image_base64_str = bytes_to_base64_string(profile_image) return squeak_admin_pb2.SqueakProfile( profile_id=squeak_profile.profile_id, profile_name=squeak_profile.profile_name, @@ -61,6 +64,8 @@ def squeak_profile_to_message(squeak_profile): address=squeak_profile.address, sharing=squeak_profile.sharing, following=squeak_profile.following, + profile_image=image_base64_str, + has_custom_profile_image=has_custom_profile_image, ) diff --git a/squeaknode/core/squeak_controller.py b/squeaknode/core/squeak_controller.py index b7595095..eee29f0f 100644 --- a/squeaknode/core/squeak_controller.py +++ b/squeaknode/core/squeak_controller.py @@ -130,8 +130,9 @@ class SqueakController: profile_name=profile_name, private_key=signing_key_bytes, address=str(address), - sharing=False, - following=False, + sharing=True, + following=True, + profile_image=None, ) return self.squeak_db.insert_profile(squeak_profile) @@ -148,6 +149,7 @@ class SqueakController: address=str(address), sharing=False, following=False, + profile_image=None, ) return self.squeak_db.insert_profile(squeak_profile) @@ -161,7 +163,8 @@ class SqueakController: private_key=None, address=squeak_address, sharing=False, - following=False, + following=True, + profile_image=None, ) return self.squeak_db.insert_profile(squeak_profile) @@ -193,6 +196,12 @@ class SqueakController: def delete_squeak_profile(self, profile_id: int): self.squeak_db.delete_profile(profile_id) + def set_squeak_profile_image(self, profile_id: int, profile_image: bytes): + self.squeak_db.set_profile_image(profile_id, profile_image) + + def clear_squeak_profile_image(self, profile_id: int): + self.squeak_db.set_profile_image(profile_id, None) + def get_squeak_profile_private_key(self, profile_id: int): profile = self.get_squeak_profile(profile_id) if profile.private_key is None: diff --git a/squeaknode/core/squeak_profile.py b/squeaknode/core/squeak_profile.py index 7f87532d..012a5edb 100644 --- a/squeaknode/core/squeak_profile.py +++ b/squeaknode/core/squeak_profile.py @@ -10,3 +10,4 @@ class SqueakProfile(NamedTuple): address: str sharing: bool following: bool + profile_image: Optional[bytes] diff --git a/squeaknode/db/alembic/versions/6851569c46e3_add_profile_image_column_to_profile_.py b/squeaknode/db/alembic/versions/6851569c46e3_add_profile_image_column_to_profile_.py new file mode 100644 index 00000000..282d8fcb --- /dev/null +++ b/squeaknode/db/alembic/versions/6851569c46e3_add_profile_image_column_to_profile_.py @@ -0,0 +1,33 @@ +"""Add profile image column to profile table + +Revision ID: 6851569c46e3 +Revises: d5a06570ed3e +Create Date: 2021-01-28 23:25:31.015184 + +""" +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision = '6851569c46e3' +down_revision = 'd5a06570ed3e' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('profile', schema=None) as batch_op: + batch_op.add_column( + sa.Column('profile_image', sa.Binary(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('profile', schema=None) as batch_op: + batch_op.drop_column('profile_image') + + # ### end Alembic commands ### diff --git a/squeaknode/db/models.py b/squeaknode/db/models.py index 8accf645..1ab49f93 100644 --- a/squeaknode/db/models.py +++ b/squeaknode/db/models.py @@ -66,6 +66,7 @@ class Models: Column("address", String(35), unique=True, nullable=False), Column("sharing", Boolean, nullable=False), Column("following", Boolean, nullable=False), + Column("profile_image", Binary, nullable=True), ) self.peers = Table( diff --git a/squeaknode/db/squeak_db.py b/squeaknode/db/squeak_db.py index d84cfbc7..e6548db9 100644 --- a/squeaknode/db/squeak_db.py +++ b/squeaknode/db/squeak_db.py @@ -652,12 +652,15 @@ class SqueakDb: with self.get_connection() as connection: connection.execute(delete_profile_stmt) - # sql = """ - # DELETE FROM profile - # WHERE profile_id=%s; - # """ - # with self.get_cursor() as curs: - # curs.execute(sql, (profile_id,)) + def set_profile_image(self, profile_id: int, profile_image: bytes): + """ Set a profile image. """ + stmt = ( + self.profiles.update() + .where(self.profiles.c.profile_id == profile_id) + .values(profile_image=profile_image) + ) + with self.get_connection() as connection: + connection.execute(stmt) def get_unverified_block_squeaks(self) -> List[bytes]: """ Get all squeaks without block header. """ @@ -1157,6 +1160,7 @@ class SqueakDb: address=row["address"], sharing=row["sharing"], following=row["following"], + profile_image=row["profile_image"], ) def _parse_squeak_entry_with_profile(self, row) -> SqueakEntryWithProfile: diff --git a/tests/core/test_squeak_core.py b/tests/core/test_squeak_core.py index 04d9890d..ed2a6ee6 100644 --- a/tests/core/test_squeak_core.py +++ b/tests/core/test_squeak_core.py @@ -75,6 +75,7 @@ def signing_profile(): address=str(address), sharing=False, following=False, + profile_image=None, )