diff --git a/driftbase/api/clients.py b/driftbase/api/clients.py index 4e15a233..094afc95 100644 --- a/driftbase/api/clients.py +++ b/driftbase/api/clients.py @@ -226,11 +226,6 @@ def post(self, args): "jwt": jwt, } - current_app.extensions['messagebus'].publish_message( - 'clients', - {'event': 'created', 'payload': payload, 'url': resource_url} - ) - return ret @@ -330,6 +325,14 @@ def delete(self, client_id): log.info("Client %s from player %s has been unregistered", client_id, current_user["player_id"]) + message_data = { + "event": "deleted", + "player_id": current_user["player_id"], + "client_id": client_id, + } + + current_app.extensions["messagebus"].publish_message("client", message_data) + return json_response("Client has been closed. Please terminate the client.", http_client.OK) diff --git a/driftbase/api/friendships.py b/driftbase/api/friendships.py index 63554a5e..5fbc4143 100644 --- a/driftbase/api/friendships.py +++ b/driftbase/api/friendships.py @@ -12,9 +12,11 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import aliased -from driftbase.models.db import Friendship, FriendInvite, CorePlayer +from driftbase.models.db import Friendship, FriendInvite, CorePlayer, Client from driftbase.schemas.friendships import InviteSchema, FriendRequestSchema from driftbase.messages import post_message +from driftbase.players import get_playergroup +from driftbase.config import get_client_heartbeat_config DEFAULT_INVITE_EXPIRATION_TIME_SECONDS = 60 * 60 * 1 @@ -23,16 +25,75 @@ bp = Blueprint("friendships", __name__, url_prefix="/friendships", description="Player to player relationships") endpoints = Endpoints() +def process_playergroup_message(queue_name, message): + log.debug(f"friendships::process_playergroup_message() received event in queue '{queue_name}': '{message}'") -def on_message(queue_name, message): - if queue_name == 'clients' and message['event'] == 'created': - log.info("Friendship is forevur! This one just connected: %s", message['payload']) + if message["event"] == "set" and message["group_name"] == "friends": + """ + Notify the player's friends that the player has come online. + Ideally, we would use the player's client 'created' event, but the friends playergroup for the player hasn't been created yet when that happens. + """ + + player_id = message["player_id"] + + log.info(f"Friends group updated for player '{player_id}'. Updating friends presence to 'online'") + + friend_ids = [player["player_id"] for player in message["payload"]["players"] if player_id != player["player_id"]] + + # Get all online friends + online_friends = _fetch_online_friends(friend_ids) + + # query = g.db.query(CorePlayer).filter(CorePlayer.player_id.in_(friend_ids), CorePlayer.is_online.is_(True)) + + import pprint + pprint.pprint(online_friends) + + # Notify online friends that the player has come online + # TODO: Figure out which online friends we need to notify, i.e. who thinks we're offline? For now, handle client side by comparing locally cached state to this event + payload = {"event": "friend_presence_changed", "friend_id": player_id, "is_online": True} + for player in online_friends: + print(f"Sending online message to player '{player.player_id}'") + post_message("players", player.player_id, "friendevent", payload) + +def process_client_message(queue_name, message): + log.debug(f"friendships::process_client_message() received event in queue '{queue_name}': '{message}'") + + if message["event"] == "deleted": + """ + Notify player's friends that the player has gone offline. + """ + + player_id = message["player_id"] + + log.info(f"Client deleted for player '{player_id}'. Updating friends presence to 'offline'") + + pg = get_playergroup("friends", player_id, handle_not_found=False) + + if not pg: + log.warning(f"Unable to update friend presence to 'offline' for player '{player_id}'. Player has no 'friends' playergroup") + return + + friend_ids = [player["player_id"] for player in pg["players"] if player_id != player["player_id"]] + + # Get all online friends + online_friends = _fetch_online_friends(friend_ids) + + import pprint + pprint.pprint(online_friends) + + # Notify online friends that the player has gone offline + # TODO: Figure out which online friends we need to notify, i.e. who thinks we're online? For now, handle client side by comparing locally cached state to this event + payload = {"event": "friend_presence_changed", "friend_id": player_id, "is_online": False} + for player in online_friends: + print(f"Sending offline message to player '{player.player_id}'") + post_message("players", player.player_id, "friendevent", payload) def drift_init_extension(app, api, **kwargs): api.register_blueprint(bp) endpoints.init_app(app) - app.messagebus.register_consumer(on_message, 'clients') + app.messagebus.register_consumer(process_playergroup_message, "playergroup") + app.messagebus.register_consumer(process_client_message, "client") def get_player(player_id): @@ -307,3 +368,17 @@ def endpoint_info(*args): if current_user: ret["my_friends"] = url_for("friendships.list", player_id=current_user["player_id"], _external=True) return ret + +def _fetch_online_friends(friend_ids): + """ + Fetch players via player id who have a client that is active and within the heartbeat timeout. + Mimics the logic of the 'is_online' hybrid property. + """ + _, heartbeat_timeout = get_client_heartbeat_config() + query = g.db.query(CorePlayer).join(Client).filter( + CorePlayer.player_id.in_(friend_ids), + Client.status == "active", + Client.heartbeat + datetime.timedelta(seconds=heartbeat_timeout) >= datetime.datetime.utcnow(), + ) + + return query.all() diff --git a/driftbase/players.py b/driftbase/players.py index 88e0f78a..2db9cbf9 100644 --- a/driftbase/players.py +++ b/driftbase/players.py @@ -4,7 +4,7 @@ import http.client as http_client import logging -from flask import g, request +from flask import g, request, current_app from flask_smorest import abort from drift.core.extensions.jwt import current_user @@ -51,7 +51,7 @@ def log_event(player_id, event_type_name, details=None, db_session=None): db_session.commit() -def get_playergroup(group_name, player_id=None): +def get_playergroup(group_name, player_id=None, handle_not_found=True): """Utility function to return player group. Can be used freely within a Flask request context. Raises 404 if group is not found. @@ -61,10 +61,12 @@ def get_playergroup(group_name, player_id=None): pg = g.redis.get(key) if pg: return json.loads(pg) - else: + elif handle_not_found: abort(http_client.NOT_FOUND, message="No player group named '%s' exists for player %s." % (group_name, player_id)) + return None + def get_playergroup_ids(group_name, player_id=None, caress_in_predicate=True): """Utility function to return a list of player id's for a given player group. @@ -84,6 +86,15 @@ def set_playergroup(group_name, player_id, payload): key = _get_playergroup_key(group_name, player_id) g.redis.set(key, json.dumps(payload), expire=RETENTION_IN_SEC) + message_data = { + "event": "set", + "group_name": group_name, + "player_id": player_id, + "payload": payload, + } + + current_app.extensions["messagebus"].publish_message("playergroup", message_data) + def _get_playergroup_key(group_name, player_id): """Returns redis key for player group. Throw exception if group name is invalid."""