Skip to content

Commit

Permalink
Refactor premium content access check to take a list and process batc…
Browse files Browse the repository at this point in the history
…h content
  • Loading branch information
Saliou Diallo committed Aug 27, 2022
1 parent ed0ef45 commit a146466
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 106 deletions.
58 changes: 41 additions & 17 deletions discovery-provider/integration_tests/premium_content/test_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ def test_track_access(app):
with app.app_context():
db = get_db_read_replica()

user_entities = [{"user_id": 1}, {"user_id": 2}]
user_entity_1 = {"user_id": 1}
user_entity_2 = {"user_id": 2}
user_entities = [user_entity_1, user_entity_2]

non_premium_track_entity = {
"track_id": 1,
"is_premium": False,
Expand All @@ -21,36 +24,57 @@ def test_track_access(app):
"premium_conditions": {"nft-collection": "some-nft-collection"},
}
track_entities = [non_premium_track_entity, premium_track_entity]

entities = {"users": user_entities, "tracks": track_entities}

populate_mock_db(db, entities)

premium_content_access_checker = PremiumContentAccessChecker()

# test non-existent track
non_exisent_track_id = 3
result = premium_content_access_checker.check_access(
user_id=user_entities[0]["user_id"],
premium_content_id=non_exisent_track_id,
premium_content_type="track",

result = premium_content_access_checker.check_access_for_batch(
[
{
"user_id": user_entity_1["user_id"],
"premium_content_id": non_exisent_track_id,
"premium_content_type": "track",
},
{
"user_id": user_entity_2["user_id"],
"premium_content_id": non_premium_track_entity["track_id"],
"premium_content_type": "track",
},
{
"user_id": user_entity_2["user_id"],
"premium_content_id": premium_track_entity["track_id"],
"premium_content_type": "track",
},
]
)
assert not result["is_premium"] and result["does_user_have_access"]

track_access_result = result["track"]

# test non-existent track
assert user_entity_1["user_id"] not in track_access_result

# test non-premium track
result = premium_content_access_checker.check_access(
user_id=user_entities[1]["user_id"],
premium_content_id=non_premium_track_entity["track_id"],
premium_content_type="track",
user_2_non_premium_track_access_result = track_access_result[
user_entity_2["user_id"]
][non_premium_track_entity["track_id"]]
assert (
not user_2_non_premium_track_access_result["is_premium"]
and user_2_non_premium_track_access_result["does_user_have_access"]
)
assert not result["is_premium"] and result["does_user_have_access"]

# test premium track with user who has access
result = premium_content_access_checker.check_access(
user_id=user_entities[1]["user_id"],
premium_content_id=premium_track_entity["track_id"],
premium_content_type="track",
user_2_premium_track_access_result = track_access_result[
user_entity_2["user_id"]
][premium_track_entity["track_id"]]
assert (
user_2_premium_track_access_result["is_premium"]
and user_2_premium_track_access_result["does_user_have_access"]
)
assert result["is_premium"] and result["does_user_have_access"]

# todo: test premium track with user who has no access
# after we implement nft infexing
167 changes: 102 additions & 65 deletions discovery-provider/src/premium_content/premium_content_access_checker.py
Original file line number Diff line number Diff line change
@@ -1,97 +1,134 @@
import json
import logging
from typing import Dict, Optional, Tuple, TypedDict, cast
from typing import Dict, List, TypedDict, cast

from src.models.tracks.track import Track
from src.premium_content.helpers import does_user_have_nft_collection
from src.premium_content.types import PremiumContentType
from src.utils import db_session
from src.utils import db_session, helpers

logger = logging.getLogger(__name__)


class PremiumContentAccessResponse(TypedDict):
class PremiumContentAccessArgs(TypedDict):
user_id: int
premium_content_id: int
premium_content_type: PremiumContentType


class PremiumContentAccess(TypedDict):
is_premium: bool
does_user_have_access: bool


PremiumTrackAccessResult = Dict[int, Dict[int, PremiumContentAccess]]


class PremiumContentAccessBatchResponse(TypedDict):
# track : user id -> track id -> access
track: PremiumTrackAccessResult


class PremiumContentAccessChecker:
# check if content is premium
# - if not, then user has access
# - if so, check whether user fulfills the conditions
#
# Returns a dictionary: { track is premium, user has access }
# Given a list of objects, each with a user id, premium content id, and premium content type,
# this method checks for access to the premium contents by the users.
#
# Note: premium content id for type should exist, but just in case it does not,
# we return true for user access so that (non-existent) track does not change
# existing flow of the function caller.
def check_access(
self,
user_id: int,
premium_content_id: int,
premium_content_type: PremiumContentType,
) -> PremiumContentAccessResponse:
premium_content_data = self._get_premium_content_data(premium_content_id)
is_premium = premium_content_data["is_premium"]
premium_conditions = premium_content_data["premium_conditions"]
content_owner_id = premium_content_data["content_owner_id"]

if not is_premium:
# premium_conditions should always be null here as it makes
# no sense to have a non-premium track with conditions
if premium_conditions:
logger.warn(
f"premium_content_access_checker.py | _aggregate_conditions | non-premium content with id {premium_content_id} and type {premium_content_type} has premium conditions."
)
return {"is_premium": False, "does_user_have_access": True}

# premium_conditions should always be true here because we know
# that is_premium is true if we get here and it makes no sense
# to have a premium track with no conditions
if not premium_conditions:
logger.warn(
f"premium_content_access_checker.py | _aggregate_conditions | premium content with id {premium_content_id} and type {premium_content_type} has no premium conditions."
)
return {"is_premium": is_premium, "does_user_have_access": True}

does_user_have_access = self._evaluate_conditions(
user_id=user_id,
premium_content_owner_id=cast(int, content_owner_id),
premium_conditions=premium_conditions,
# Returns a dictionary in the following format:
# {
# <premium-content-type>: {
# <user-id>: {
# <track-id>: {
# "is_premium": bool,
# "does_user_have_access": bool
# }
# }
# }
# }
def check_access_for_batch(
self, args: List[PremiumContentAccessArgs]
) -> PremiumContentAccessBatchResponse:
# for now, we only allow tracks to be premium; premium playlists will come later
valid_args = list(
filter(lambda arg: arg["premium_content_type"] == "track", args)
)
return {"is_premium": True, "does_user_have_access": does_user_have_access}

# Returns a dictionary { content is premium, premium content conditions, content owner id }
def _get_premium_content_data(
self, premium_content_id: int, premium_content_type: PremiumContentType
):
# for now, we only allow tracks to be premium; premium playlists will come later
if premium_content_type != "track":
return {
"is_premium": False,
"premium_conditions": None,
"owner_id": None,
}
if not valid_args:
return {"track": {}}

track_access_users = {
arg["premium_content_id"]: arg["user_id"] for arg in valid_args
}
premium_track_data = self._get_premium_track_data_for_batch(
list(track_access_users.keys())
)
track_access_result: PremiumTrackAccessResult = {}

for track_id, data in premium_track_data.items():
user_id = track_access_users[track_id]
if user_id not in track_access_result:
track_access_result[user_id] = {}

is_premium = data["is_premium"]
premium_conditions = data["premium_conditions"]
content_owner_id = data["content_owner_id"]

if not is_premium:
# premium_conditions should always be null here as it makes
# no sense to have a non-premium track with conditions
if premium_conditions:
logger.warn(
f"premium_content_access_checker.py | _aggregate_conditions | non-premium content with id {track_id} and type 'track' has premium conditions."
)
track_access_result[user_id][track_id] = {
"is_premium": False,
"does_user_have_access": True,
}

# premium_conditions should always be true here because we know
# that is_premium is true if we get here and it makes no sense
# to have a premium track with no conditions
elif not premium_conditions:
logger.warn(
f"premium_content_access_checker.py | _aggregate_conditions | premium content with id {track_id} and type 'track' has no premium conditions."
)
track_access_result[user_id][track_id] = {
"is_premium": True,
"does_user_have_access": True,
}

else:
track_access_result[user_id][track_id] = {
"is_premium": True,
"does_user_have_access": self._evaluate_conditions(
user_id=user_id,
premium_content_owner_id=cast(int, content_owner_id),
premium_conditions=premium_conditions,
),
}

return {"track": track_access_result}

def _get_premium_track_data_for_batch(self, track_ids: List[int]):
db = db_session.get_db_read_replica()
with db.scoped_session() as session:
track = (
tracks = (
session.query(Track)
.filter(
Track.track_id == premium_content_id,
Track.track_id.in_(track_ids),
Track.is_current == True,
Track.is_delete == False,
)
.first()
.all()
)

if not track:
return False, None, None
tracks = list(map(helpers.model_to_dictionary, tracks))

return {
"is_premium": track.is_premium,
"premium_conditions": track.premium_conditions,
"owner_id": track.owner_id,
track["track_id"]: {
"is_premium": track["is_premium"],
"premium_conditions": track["premium_conditions"],
"content_owner_id": track["owner_id"],
}
for track in tracks
}

# There will eventually be another step prior to this one where
Expand Down
42 changes: 30 additions & 12 deletions discovery-provider/src/tasks/social_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,17 +207,26 @@ def add_track_repost(
tx_receipt
)
)

premium_content_access_args = []
for event in new_track_repost_events:
premium_content_access_args.append(
{
"user_id": event["args"]._userId,
"premium_content_id": event["args"]._trackId,
"premium_content_type": "track",
}
)
premium_content_access = premium_content_access_checker.check_access_for_batch(
premium_content_access_args
)

for event in new_track_repost_events:
event_args = event["args"]
repost_user_id = event_args._userId
repost_track_id = event_args._trackId

premium_content_access = premium_content_access_checker.check_access(
user_id=repost_user_id,
premium_content_id=repost_track_id,
premium_content_type="track",
)
if not premium_content_access["does_user_have_access"]:
if not premium_content_access["track"][repost_user_id][repost_track_id]:
continue

if (repost_user_id in track_repost_state_changes) and (
Expand Down Expand Up @@ -260,17 +269,26 @@ def delete_track_repost(
tx_receipt
)
)

premium_content_access_args = []
for event in new_repost_events:
premium_content_access_args.append(
{
"user_id": event["args"]._userId,
"premium_content_id": event["args"]._trackId,
"premium_content_type": "track",
}
)
premium_content_access = premium_content_access_checker.check_access_for_batch(
premium_content_access_args
)

for event in new_repost_events:
event_args = event["args"]
repost_user_id = event_args._userId
repost_track_id = event_args._trackId

premium_content_access = premium_content_access_checker.check_access(
user_id=repost_user_id,
premium_content_id=repost_track_id,
premium_content_type="track",
)
if not premium_content_access["does_user_have_access"]:
if not premium_content_access["track"][repost_user_id][repost_track_id]:
continue

if (repost_user_id in track_repost_state_changes) and (
Expand Down
Loading

0 comments on commit a146466

Please sign in to comment.