From c3b9349e81e18418ea8f6c3d7a077f04c9bf89f1 Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Wed, 27 Sep 2023 17:05:24 -0700 Subject: [PATCH 1/2] [PAY-1933] Gate purchaseable content in search --- discovery-provider/es-indexer/src/indexNames.ts | 2 +- .../es-indexer/src/indexers/TrackIndexer.ts | 5 +++++ discovery-provider/src/api/v1/helpers.py | 6 ++++++ discovery-provider/src/api/v1/search.py | 2 ++ discovery-provider/src/queries/search_es.py | 7 +++++++ .../services/audius-api-client/AudiusAPIClient.ts | 13 +++++++++---- .../src/common/store/pages/search-page/sagas.js | 14 +++++++++++--- packages/web/src/common/store/search-bar/sagas.ts | 12 +++++------- 8 files changed, 46 insertions(+), 15 deletions(-) diff --git a/discovery-provider/es-indexer/src/indexNames.ts b/discovery-provider/es-indexer/src/indexNames.ts index 949ecb18122..17f33742f08 100644 --- a/discovery-provider/es-indexer/src/indexNames.ts +++ b/discovery-provider/es-indexer/src/indexNames.ts @@ -2,6 +2,6 @@ export const indexNames = { playlists: 'playlists18', reposts: 'reposts13', saves: 'saves13', - tracks: 'tracks15', + tracks: 'tracks16', users: 'users16', } diff --git a/discovery-provider/es-indexer/src/indexers/TrackIndexer.ts b/discovery-provider/es-indexer/src/indexers/TrackIndexer.ts index 6e37a18e5e7..687ec94db56 100644 --- a/discovery-provider/es-indexer/src/indexers/TrackIndexer.ts +++ b/discovery-provider/es-indexer/src/indexers/TrackIndexer.ts @@ -44,6 +44,7 @@ export class TrackIndexer extends BaseIndexer { is_delete: { type: 'boolean' }, is_unlisted: { type: 'boolean' }, downloadable: { type: 'boolean' }, + purchaseable: { type: 'boolean' }, // saves saved_by: { type: 'keyword' }, @@ -94,6 +95,10 @@ export class TrackIndexer extends BaseIndexer { -- etl tracks select tracks.*, + case when "premium_conditions"->>'usdc_purchase' + is not null then true + else false + end as purchaseable, (tracks.download->>'is_downloadable')::boolean as downloadable, coalesce(aggregate_plays.count, 0) as play_count, diff --git a/discovery-provider/src/api/v1/helpers.py b/discovery-provider/src/api/v1/helpers.py index 3f0cfe221c6..4c704cb59de 100644 --- a/discovery-provider/src/api/v1/helpers.py +++ b/discovery-provider/src/api/v1/helpers.py @@ -813,6 +813,12 @@ def add_auth_headers_to_parser(parser): choices=("all", "users", "tracks", "playlists", "albums"), description="The type of response, one of: all, users, tracks, playlists, or albums", ) +full_search_parser.add_argument( + "includePurchaseable", + required=False, + type=bool, + description="Whether or not to include purchaseable content", +) verify_token_parser = reqparse.RequestParser(argument_class=DescriptiveArgument) verify_token_parser.add_argument("token", required=True, description="JWT to verify") diff --git a/discovery-provider/src/api/v1/search.py b/discovery-provider/src/api/v1/search.py index b6f96c405be..9b2eaec32aa 100644 --- a/discovery-provider/src/api/v1/search.py +++ b/discovery-provider/src/api/v1/search.py @@ -53,6 +53,7 @@ def get(self): "limit": limit, "offset": offset, "only_downloadable": False, + "include_purchaseable": args.get("includePurchaseable", False), } resp = search(search_args) return success_response(resp) @@ -93,6 +94,7 @@ def get(self): "limit": limit, "offset": offset, "only_downloadable": False, + "include_purchaseable": args.get("includePurchaseable", False), } resp = search(search_args) return success_response(resp) diff --git a/discovery-provider/src/queries/search_es.py b/discovery-provider/src/queries/search_es.py index 3cbc69cd1f4..72d3b337261 100644 --- a/discovery-provider/src/queries/search_es.py +++ b/discovery-provider/src/queries/search_es.py @@ -28,6 +28,7 @@ def search_es_full(args: dict): search_type = args.get("kind", "all") only_downloadable = args.get("only_downloadable") is_auto_complete = args.get("is_auto_complete") + include_purchaseable = args.get("include_purchaseable") do_tracks = search_type == "all" or search_type == "tracks" do_users = search_type == "all" or search_type == "users" do_playlists = search_type == "all" or search_type == "playlists" @@ -50,6 +51,7 @@ def search_es_full(args: dict): current_user_id=current_user_id, must_saved=False, only_downloadable=only_downloadable, + include_purchaseable=include_purchaseable, ), ] ) @@ -64,6 +66,7 @@ def search_es_full(args: dict): current_user_id=current_user_id, must_saved=True, only_downloadable=only_downloadable, + include_purchaseable=include_purchaseable, ), ] ) @@ -369,6 +372,7 @@ def track_dsl( current_user_id, must_saved=False, only_downloadable=False, + include_purchaseable=False, ): dsl = { "must": [ @@ -395,6 +399,9 @@ def track_dsl( if only_downloadable: dsl["must"].append({"term": {"downloadable": {"value": True}}}) + if not include_purchaseable: + dsl["must_not"].append({"term": {"purchaseable": {"value": True}}}) + personalize_dsl(dsl, current_user_id, must_saved) return default_function_score(dsl, "repost_count") diff --git a/packages/common/src/services/audius-api-client/AudiusAPIClient.ts b/packages/common/src/services/audius-api-client/AudiusAPIClient.ts index 2bdd39d6deb..7031b0626d9 100644 --- a/packages/common/src/services/audius-api-client/AudiusAPIClient.ts +++ b/packages/common/src/services/audius-api-client/AudiusAPIClient.ts @@ -339,6 +339,7 @@ type GetSearchArgs = { kind?: SearchKind limit?: number offset?: number + includePurchaseable?: boolean } type TrendingIdsResponse = { @@ -1350,7 +1351,8 @@ export class AudiusAPIClient { query, kind, offset, - limit + limit, + includePurchaseable }: GetSearchArgs) { this._assertInitialized() const encodedUserId = encodeHashId(currentUserId) @@ -1359,7 +1361,8 @@ export class AudiusAPIClient { query, kind, offset, - limit + limit, + includePurchaseable } const searchResponse = @@ -1377,7 +1380,8 @@ export class AudiusAPIClient { query, kind, offset, - limit + limit, + includePurchaseable }: GetSearchArgs) { this._assertInitialized() const encodedUserId = encodeHashId(currentUserId) @@ -1386,7 +1390,8 @@ export class AudiusAPIClient { query, kind, offset, - limit + limit, + includePurchaseable } const searchResponse = diff --git a/packages/web/src/common/store/pages/search-page/sagas.js b/packages/web/src/common/store/pages/search-page/sagas.js index c80bd99f7f5..537e53e68e3 100644 --- a/packages/web/src/common/store/pages/search-page/sagas.js +++ b/packages/web/src/common/store/pages/search-page/sagas.js @@ -5,7 +5,8 @@ import { searchResultsPageTracksLineupActions as tracksLineupActions, SearchKind, processAndCacheUsers, - removeNullable + removeNullable, + FeatureFlags } from '@audius/common' import { flatMap, zip } from 'lodash' import { @@ -103,6 +104,11 @@ const searchMultiMap = { export function* getSearchResults(searchText, kind, limit, offset) { yield waitForRead() + const getFeatureEnabled = yield getContext('getFeatureEnabled') + const isUSDCEnabled = yield call( + getFeatureEnabled, + FeatureFlags.USDC_PURCHASES + ) const apiClient = yield getContext('apiClient') const userId = yield select(getUserId) @@ -114,7 +120,8 @@ export function* getSearchResults(searchText, kind, limit, offset) { query, kind, limit, - offset + offset, + includePurchaseable: isUSDCEnabled }) ) const allSearchResults = yield all(searches) @@ -136,7 +143,8 @@ export function* getSearchResults(searchText, kind, limit, offset) { query: searchText, kind, limit, - offset + offset, + includePurchaseable: isUSDCEnabled }) } const { tracks, albums, playlists, users } = results diff --git a/packages/web/src/common/store/search-bar/sagas.ts b/packages/web/src/common/store/search-bar/sagas.ts index dabe6db9b90..4dc594bbbd0 100644 --- a/packages/web/src/common/store/search-bar/sagas.ts +++ b/packages/web/src/common/store/search-bar/sagas.ts @@ -51,7 +51,8 @@ export function* getSearchResults(searchText: string) { currentUserId: userId, query, limit: 3, - offset: 0 + offset: 0, + includePurchaseable: isUSDCEnabled }) ) const allSearchResults = yield* all(searches) @@ -72,18 +73,15 @@ export function* getSearchResults(searchText: string) { currentUserId: userId, query: searchText, limit: 3, - offset: 0 + offset: 0, + includePurchaseable: isUSDCEnabled }) } const { tracks, albums, playlists, users } = results const checkedUsers = users.filter((u) => !u.is_deactivated) const checkedTracks = tracks.filter((t) => { - return ( - !t.is_delete && - !t.user.is_deactivated && - (isUSDCEnabled || !('usdc_purchase' in (t.premium_conditions || {}))) - ) + return !t.is_delete && !t.user.is_deactivated }) const checkedPlaylists = playlists.filter((p) => !p.user?.is_deactivated) const checkedAlbums = albums.filter((a) => !a.user?.is_deactivated) From c0977a94bf507c954729982feee37b97f58b05d3 Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Wed, 27 Sep 2023 17:49:06 -0700 Subject: [PATCH 2/2] Add test --- .../es-indexer/src/indexers/TrackIndexer.ts | 2 +- .../es-indexer/src/types/docs.ts | 1 + .../integration_tests/queries/test_search.py | 48 +++++++++++++++++++ discovery-provider/src/queries/search_es.py | 2 +- 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/discovery-provider/es-indexer/src/indexers/TrackIndexer.ts b/discovery-provider/es-indexer/src/indexers/TrackIndexer.ts index 687ec94db56..6d1fc9274c3 100644 --- a/discovery-provider/es-indexer/src/indexers/TrackIndexer.ts +++ b/discovery-provider/es-indexer/src/indexers/TrackIndexer.ts @@ -95,7 +95,7 @@ export class TrackIndexer extends BaseIndexer { -- etl tracks select tracks.*, - case when "premium_conditions"->>'usdc_purchase' + case when tracks.premium_conditions->>'usdc_purchase' is not null then true else false end as purchaseable, diff --git a/discovery-provider/es-indexer/src/types/docs.ts b/discovery-provider/es-indexer/src/types/docs.ts index 1fcc99d3bd1..24f99f8d163 100644 --- a/discovery-provider/es-indexer/src/types/docs.ts +++ b/discovery-provider/es-indexer/src/types/docs.ts @@ -50,6 +50,7 @@ export type TrackDoc = TrackRow & { favorite_count: number play_count: any // todo: is it a string or number? pg returns string downloadable: boolean + purchaseable: boolean user: EntityUserDoc duration: number } diff --git a/discovery-provider/integration_tests/queries/test_search.py b/discovery-provider/integration_tests/queries/test_search.py index 89270c81871..afff5a581ca 100644 --- a/discovery-provider/integration_tests/queries/test_search.py +++ b/discovery-provider/integration_tests/queries/test_search.py @@ -42,6 +42,12 @@ def setup_search(app_module): blockhash=hex(3), number=3, parenthash="0x03", + is_current=False, + ), + Block( + blockhash=hex(4), + number=4, + parenthash="0x04", is_current=True, ), ] @@ -94,6 +100,28 @@ def setup_search(app_module): title="xyz", download={"cid": None, "is_downloadable": True, "requires_follow": False}, ), + Track( + blockhash=hex(4), + blocknumber=4, + track_id=4, + is_current=True, + is_delete=False, + owner_id=1, + route_id="", + track_segments=[], + genre="", + updated_at=now, + created_at=now, + is_unlisted=False, + title="the track 4", + download={"cid": None, "is_downloadable": True, "requires_follow": False}, + premium_conditions={ + "usdc_purchase": { + "price": 100, + "splits": {"4hbyJjqpWAbarjCQhY8YSeptZz1WYSS88DGqG4BteE3v": 1000000}, + } + }, + ), ] users = [ @@ -328,6 +356,26 @@ def test_get_downloadable_tracks(app_module): assert len(es_res["saved_tracks"]) == 0 +def test_get_tracks_with_purchases(app_module): + """Tests we get results with purchaseable tracks""" + + search_args = { + "is_auto_complete": True, + "kind": "tracks", + "query": "the track", + "current_user_id": None, + "with_users": True, + "limit": 10, + "offset": 0, + "only_downloadable": False, + "include_purchaseable": True, + } + es_res = search_es_full(search_args) + + assert len(es_res["tracks"]) == 3 + assert es_res["tracks"][2]["track_id"] == 4 + + def test_get_external_users(app_module): """Tests we get all users""" diff --git a/discovery-provider/src/queries/search_es.py b/discovery-provider/src/queries/search_es.py index 72d3b337261..4b95f41509c 100644 --- a/discovery-provider/src/queries/search_es.py +++ b/discovery-provider/src/queries/search_es.py @@ -28,7 +28,7 @@ def search_es_full(args: dict): search_type = args.get("kind", "all") only_downloadable = args.get("only_downloadable") is_auto_complete = args.get("is_auto_complete") - include_purchaseable = args.get("include_purchaseable") + include_purchaseable = args.get("include_purchaseable", False) do_tracks = search_type == "all" or search_type == "tracks" do_users = search_type == "all" or search_type == "users" do_playlists = search_type == "all" or search_type == "playlists"