diff --git a/services/database/database.proto b/services/database/database.proto index 613f4d2a..13cc1771 100644 --- a/services/database/database.proto +++ b/services/database/database.proto @@ -90,10 +90,23 @@ message Follow { message DbFollowRequest { enum RequestType { INSERT = 0; + FIND = 1; } RequestType request_type = 1; + Follow entry = 2; + + /* If request_type is FIND: + * - If match.followed is set, then the service will return all followers + * for this user's ID. + * - If match.follower is set, the service will return all users this + * person follows. + * - If both match.followed and match.follower are set, the service will + * return an entry if this follow exists, and none otherwise. + * - If neither are set, all follows in the database will be returned. + */ + Follow match = 3; } message DbFollowResponse { @@ -104,6 +117,8 @@ message DbFollowResponse { ResultType result_type = 1; string error = 2; + + repeated Follow results = 3; } service Database { diff --git a/services/database/follow_servicer.py b/services/database/follow_servicer.py index 8ad47621..65168c9e 100644 --- a/services/database/follow_servicer.py +++ b/services/database/follow_servicer.py @@ -1,5 +1,7 @@ import sqlite3 +import util + import database_pb2 @@ -10,8 +12,26 @@ def __init__(self, db, logger): self._logger = logger self._follow_type_handlers = { database_pb2.DbFollowRequest.INSERT: self._follow_handle_insert, + database_pb2.DbFollowRequest.FIND: self._follow_handle_find, } + def _db_tuple_to_entry(self, tup, entry): + if len(tup) != 2: + self._logger.warning( + "Error converting tuple to Follow: " + + "Wrong number of elements " + str(tup)) + return False + try: + # You'd think there'd be a better way. + entry.follower = tup[0] + entry.followed = tup[1] + except Exception as e: + self._logger.warning( + "Error converting tuple to Follow: " + + str(e)) + return False + return True + def Follow(self, request, context): response = database_pb2.DbFollowResponse() self._follow_type_handlers[request.request_type](request, response) @@ -32,3 +52,28 @@ def _follow_handle_insert(self, req, resp): resp.error = str(e) return resp.result_type = database_pb2.DbFollowResponse.OK + + def _follow_handle_find(self, req, resp): + filter_clause, values = util.entry_to_filter(req.match) + try: + if not filter_clause: + query = 'SELECT * FROM follows' + self._logger.debug('Running query "%s"', query) + res = self._db.execute(query) + else: + query = 'SELECT * FROM follows WHERE ' + filter_clause + valstr = ', '.join(str(v) for v in values) + self._logger.debug('Running query "%s" with values (%s)', + query, valstr) + res = self._db.execute(query, *values) + except sqlite3.Error as e: + self._logger.warning('Got error reading DB: ' + str(e)) + resp.result_type = database_pb2.DbFollowResponse.ERROR + resp.error = str(e) + return + resp.result_type = database_pb2.DbFollowResponse.OK + for tup in res: + if not self._db_tuple_to_entry(tup, resp.results.add()): + del resp.results[-1] + + self._logger.debug('%d results of follower query.', len(resp.results)) diff --git a/services/follows/follows.proto b/services/follows/follows.proto index 932fe953..3d2fd030 100644 --- a/services/follows/follows.proto +++ b/services/follows/follows.proto @@ -45,7 +45,36 @@ message FollowResponse { string error = 2; } +message FollowUser { + string handle = 1; + string host = 2; + string display_name = 3; +} + +message GetFollowsRequest { + /* Eg. "admin", "weeura@samsoniuk.ua", "@jose" */ + string username = 1; +} + +message GetFollowsResponse { + enum ResultType { + OK = 0; + ERROR = 1; + // More types can be added here. + } + + ResultType result_type = 1; + + /* Should only be set if result_type is not OK. */ + string error = 2; + + /* Should only be set if result_type is OK. */ + repeated FollowUser results = 3; +} + service Follows { rpc SendFollowRequest(LocalToAnyFollow) returns (FollowResponse); rpc ReceiveFollowRequest(ForeignToLocalFollow) returns (FollowResponse); + rpc GetFollowers(GetFollowsRequest) returns (GetFollowsResponse); + rpc GetFollowing(GetFollowsRequest) returns (GetFollowsResponse); } diff --git a/services/follows/get_followers.py b/services/follows/get_followers.py new file mode 100644 index 00000000..a7a15a8a --- /dev/null +++ b/services/follows/get_followers.py @@ -0,0 +1,76 @@ +from enum import Enum + +import database_pb2 +import follows_pb2 + + +class GetFollowsReceiver: + + def __init__(self, logger, util, database_stub): + self._logger = logger + self._util = util + self._database_stub = database_stub + self.RequestType = Enum('RequestType', 'FOLLOWING FOLLOWERS') + + def _get_follows(self, request, context, request_type): + if request_type == self.RequestType.FOLLOWERS: + self._logger.debug('List of followers of %s requested', + request.username) + else: + self._logger.debug('List of users %s is following requested', + request.username) + resp = follows_pb2.GetFollowsResponse() + + # Parse input username + handle, host = self._util.parse_username( + request.username) + if handle is None and host is None: + resp.result_type = follows_pb2.GetFollowsResponse.ERROR + resp.error = 'Could not parse queried username' + return resp + + # Get user obj associated with given user handle & host from database + user_entry = self._util.get_user_from_db(handle, host) + if user_entry is None: + error = 'Could not find or create user {}@{}'.format(from_handle, + from_instance) + self._logger.error(error) + resp.result_type = follows_pb2.GetFollowersResponse.ERROR + resp.error = error + return resp + user_id = user_entry.global_id + + # Get followers/followings for this user. + following_ids = None + if request_type == self.RequestType.FOLLOWERS: + following_ids = self._util.get_follows(followed_id=user_id).results + else: + following_ids = self._util.get_follows(follower_id=user_id).results + + # Convert other following users and add to output proto. + for following_id in following_ids: + _id = following_id.followed + if request_type == self.RequestType.FOLLOWERS: + _id = following_id.follower + user = self._util.get_user_from_db(global_id=_id) + if user is None: + self._logger.warning('Could not find user for id %d', + _id) + continue + + ok = self._util.convert_db_user_to_follow_user(user, + resp.results.add()) + if not ok: + self._logger.warning('Could not convert user %s@%s to ' + + 'FollowUser', user.handle, user.host) + + resp.result_type = follows_pb2.GetFollowsResponse.OK + return resp + + def GetFollowing(self, request, context): + self._logger.debug('GetFollowing, username = %s', request.username) + return self._get_follows(request, context, self.RequestType.FOLLOWING) + + def GetFollowers(self, request, context): + self._logger.debug('GetFollowers, username = %s', request.username) + return self._get_follows(request, context, self.RequestType.FOLLOWERS) diff --git a/services/follows/send_follow.py b/services/follows/send_follow.py index 76839bcb..6dc601ae 100644 --- a/services/follows/send_follow.py +++ b/services/follows/send_follow.py @@ -28,7 +28,7 @@ def SendFollowRequest(self, request, context): # Get user IDs for follow. follower_entry = self._util.get_user_from_db( - from_handle, from_instance) + handle=from_handle, host=from_instance) if follower_entry is None: error = 'Could not find or create user {}@{}'.format(from_handle, from_instance) @@ -36,7 +36,8 @@ def SendFollowRequest(self, request, context): resp.result_type = follows_pb2.FollowResponse.ERROR resp.error = error return resp - followed_entry = self._util.get_user_from_db(to_handle, to_instance) + followed_entry = self._util.get_user_from_db(handle=to_handle, + host=to_instance) if followed_entry is None: error = 'Could not find or create user {}@{}'.format(to_handle, to_instance) diff --git a/services/follows/servicer.py b/services/follows/servicer.py index 0f72d6a8..4b8fcc94 100644 --- a/services/follows/servicer.py +++ b/services/follows/servicer.py @@ -1,3 +1,4 @@ +from get_followers import GetFollowsReceiver from receive_follow import ReceiveFollowServicer from send_follow import SendFollowServicer @@ -14,3 +15,7 @@ def __init__(self, logger, util, database_stub): self.SendFollowRequest = send_servicer.SendFollowRequest rec_servicer = ReceiveFollowServicer(logger, util, database_stub) self.ReceiveFollowRequest = rec_servicer.ReceiveFollowRequest + + get_follows_receiver = GetFollowsReceiver(logger, util, database_stub) + self.GetFollowers = get_follows_receiver.GetFollowers + self.GetFollowing = get_follows_receiver.GetFollowing diff --git a/services/follows/util.py b/services/follows/util.py index 197c5ba3..5702e03b 100644 --- a/services/follows/util.py +++ b/services/follows/util.py @@ -1,3 +1,4 @@ +import follows_pb2 import database_pb2 MAX_FIND_RETRIES = 3 @@ -33,14 +34,20 @@ def _create_user_in_db(self, entry): # TODO(iandioch): Respond to errors. return insert_resp - def get_user_from_db(self, handle, host, attempt_number=0): + def get_user_from_db(self, + handle=None, + host=None, + global_id=None, + attempt_number=0): if attempt_number > MAX_FIND_RETRIES: self._logger.error('Retried query too many times.') return None - self._logger.debug('User %s@%s requested from database', handle, host) + self._logger.debug('User %s@%s (id %s) requested from database', + handle, host, global_id) user_entry = database_pb2.UsersEntry( handle=handle, - host=host + host=host, + global_id=global_id ) find_req = database_pb2.UsersRequest( request_type=database_pb2.UsersRequest.FIND, @@ -48,15 +55,22 @@ def get_user_from_db(self, handle, host, attempt_number=0): ) find_resp = self._db.Users(find_req) if len(find_resp.results) == 0: - self._logger.warning('No user %s@%s found', handle, host) + self._logger.warning('No user %s@%s (id %s) found', + handle, host, global_id) + if global_id is not None: + # Should not try to create a user and hope it has this ID. + return None self._create_user_in_db(user_entry) - return self.get_user_from_db(handle, host, attempt_number=attempt_number+1) + return self.get_user_from_db(handle, host, + attempt_number=attempt_number + 1) elif len(find_resp.results) == 1: - self._logger.debug('Found user %s@%s from database', handle, host) + self._logger.debug('Found user %s@%s (id %s) from database', + handle, host, global_id) return find_resp.results[0] else: - self._logger.error('> 1 user found in database for %s@%s' + - ', returning first one.', handle, host) + self._logger.error('> 1 user found in database for %s@%s (id %s)' + + ', returning first one.', + handle, host, global_id) return find_resp.results[0] def create_follow_in_db(self, follower_id, followed_id): @@ -72,6 +86,37 @@ def create_follow_in_db(self, follower_id, followed_id): ) follow_resp = self._db.Follow(follow_req) if follow_resp.result_type == database_pb2.DbFollowResponse.ERROR: - self._logger.error('Could not add follow to database: %s', follow_resp.error) + self._logger.error('Could not add follow to database: %s', + follow_resp.error) return follow_resp + def get_follows(self, follower_id=None, followed_id=None): + self._logger.debug('Finding follows ', + ('*' if (follower_id is None) else str(follower_id)), + ('*' if (followed_id is None) else str(followed_id))) + follow_entry = database_pb2.Follow( + follower=follower_id, + followed=followed_id + ) + follow_req = database_pb2.DbFollowRequest( + request_type=database_pb2.DbFollowRequest.FIND, + match=follow_entry + ) + follow_resp = self._db.Follow(follow_req) + if follow_resp.result_type == database_pb2.DbFollowResponse.ERROR: + self._logger.error('Could not add follow to database: %s', + follow_resp.error) + return follow_resp + + def convert_db_user_to_follow_user(self, db_user, follow_user): + self._logger.warning('Converting db user %s@%s to follow user.', + db_user.handle, db_user.host) + try: + follow_user.handle = db_user.handle + follow_user.host = db_user.host + follow_user.display_name = db_user.display_name + except Exception as e: + self._logger.warning('Error converting db user to follow user: ' + + str(e)) + return False + return True