this repo has no description
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 381 lines 17 kB view raw
1/** 2 * Created by joel on 11/4/2018. 3 */ 4import { isNothing, isSome } from "@jet/environment/types/optional"; 5import * as serverData from "../json-parsing/server-data"; 6import * as urls from "../network/urls"; 7import * as attributes from "./attributes"; 8/// this is exposed for compatibility. If you find yourself needing to use this outside of the media api module you 9/// probably have code smell. DO NOT USE. 10export function buildURLFromRequest(objectGraph, request) { 11 var _a, _b; 12 const baseURL = request.href && request.href.length > 0 13 ? baseURLForHref(request.href) 14 : baseURLForResourceType(objectGraph, request.isMixedMediaRequest, request.resourceType, request.countryCodeOverride); 15 const mediaApiURL = new urls.URL(baseURL); 16 if (serverData.isDefinedNonNullNonEmpty(request.resourceType)) { 17 for (const pathComponent of pathComponentsForRequest(request.resourceType, request.targetResourceType)) { 18 mediaApiURL.append("pathname", pathComponent); 19 } 20 } 21 if (request.isMixedMediaRequest) { 22 for (const [resourceType, ids] of request.idsByResourceType.entries()) { 23 mediaApiURL.param(`ids[${resourceType}]`, Array.from(ids).sort().join(",")); 24 } 25 } 26 else if (request.ids.size > 1 || request.useIdsAsQueryParam) { 27 mediaApiURL.param("ids", Array.from(request.ids).sort().join(",")); 28 } 29 else if (request.ids.size === 1) { 30 const id = request.ids.values().next().value; 31 mediaApiURL.append("pathname", id); 32 } 33 if (request.resourceType !== undefined) { 34 const trailingPathComponent = trailingPathComponentForResourceType(request.resourceType); 35 if (serverData.isDefinedNonNullNonEmpty(trailingPathComponent)) { 36 mediaApiURL.append("pathname", trailingPathComponent); 37 } 38 } 39 mediaApiURL.param("platform", (_a = request.platform) !== null && _a !== void 0 ? _a : undefined); 40 if (request.additionalPlatforms.size > 0) { 41 mediaApiURL.param("additionalPlatforms", Array.from(request.additionalPlatforms).sort().join(",")); 42 } 43 /** 44 * Add `extend` attributes. 45 * Note that when `useCustomAttributes` is true, there is `customArtwork` param even when `attributeIncludes` is initially empty. 46 * This due MAPI auto-extend for `artwork`, and lack of auto-extend for `customArtwork` 47 */ 48 if (request.attributeIncludes.size > 0 || request.useCustomAttributes) { 49 let extendAttributes = Array.from(request.attributeIncludes); 50 if (request.useCustomAttributes) { 51 extendAttributes = convertRequestAttributesToCustomAttributes(objectGraph, extendAttributes); 52 } 53 extendAttributes.sort(); 54 mediaApiURL.param("extend", extendAttributes.join(",")); 55 } 56 // Add age restriction if present. 57 if (serverData.isDefinedNonNull(request.ageRestriction) && objectGraph.bag.enableAgeRatingFilter) { 58 mediaApiURL.param("restrict[ageRestriction]", request.ageRestriction.toString()); 59 } 60 // Automatically extend iOS catalog requests for apps to include appBinaryTraits. 61 if (request.includeAppBinaryTraitsAttribute) { 62 request.includingScopedAttributes("apps", ["appBinaryTraits"]); 63 } 64 if (serverData.isDefinedNonNull(request.scopedAttributeIncludes)) { 65 for (const [dataType, scopedIncludes] of request.scopedAttributeIncludes.entries()) { 66 mediaApiURL.param(`extend[${dataType}]`, Array.from(scopedIncludes).sort().join(",")); 67 } 68 } 69 if (request.relationshipIncludes.size > 0) { 70 mediaApiURL.param("include", Array.from(request.relationshipIncludes).sort().join(",")); 71 } 72 if (serverData.isDefinedNonNull(request.scopedRelationshipIncludes)) { 73 for (const [dataType, scopedIncludes] of request.scopedRelationshipIncludes.entries()) { 74 mediaApiURL.param(`include[${dataType}]`, Array.from(scopedIncludes).sort().join(",")); 75 } 76 } 77 if (serverData.isDefinedNonNull(request.metaIncludes)) { 78 for (const [dataType, scopedMeta] of request.metaIncludes.entries()) { 79 mediaApiURL.param(`meta[${dataType}]`, Array.from(scopedMeta).sort().join(",")); 80 } 81 } 82 if (serverData.isSetDefinedNonNullNonEmpty(request.viewsIncludes)) { 83 mediaApiURL.param("views", Array.from(request.viewsIncludes).sort().join(",")); 84 } 85 if (serverData.isDefinedNonNull(request.kindIncludes)) { 86 for (const [dataType, scopedMeta] of request.kindIncludes.entries()) { 87 mediaApiURL.param(`kinds[${dataType}]`, Array.from(scopedMeta).sort().join(",")); 88 } 89 } 90 if (serverData.isDefinedNonNull(request.associateIncludes)) { 91 for (const [dataType, scopedAssociate] of request.associateIncludes.entries()) { 92 mediaApiURL.param(`associate[${dataType}]`, Array.from(scopedAssociate).sort().join(",")); 93 } 94 } 95 if (serverData.isDefinedNonNull(request.scopedAvailableInIncludes)) { 96 for (const [dataType, scopedAvailableIn] of request.scopedAvailableInIncludes.entries()) { 97 mediaApiURL.param(`availableIn[${dataType}]`, Array.from(scopedAvailableIn).sort().join(",")); 98 } 99 } 100 if (serverData.isDefinedNonNullNonEmpty(request.fields)) { 101 let extendedFields = Array.from(request.fields); 102 if (request.useCustomAttributes) { 103 extendedFields = convertRequestFieldsToCustomFields(extendedFields); 104 } 105 request.fields.sort(); 106 mediaApiURL.param("fields", extendedFields.join(",")); 107 } 108 if (serverData.isDefinedNonNull(request.limit) && request.limit > 0) { 109 mediaApiURL.param(`limit`, `${request.limit}`); 110 } 111 if (serverData.isDefinedNonNull(request.sparseLimit)) { 112 mediaApiURL.param(`sparseLimit`, `${request.sparseLimit}`); 113 } 114 if (serverData.isDefinedNonNull(request.scopedSparseLimit)) { 115 for (const [dataType, scopedLimit] of request.scopedSparseLimit.entries()) { 116 mediaApiURL.param(`sparseLimit[${dataType}]`, String(scopedLimit)); 117 } 118 } 119 if (serverData.isDefinedNonNull(request.sparseCount)) { 120 mediaApiURL.param(`sparseCount`, `${request.sparseCount}`); 121 } 122 for (const relationshipID of Object.keys(request.relationshipLimits).sort()) { 123 const limit = request.relationshipLimits[relationshipID]; 124 mediaApiURL.param(`limit[${relationshipID}]`, `${limit}`); 125 } 126 if (serverData.isDefinedNonNullNonEmpty(request.additionalQuery)) { 127 mediaApiURL.append("query", request.additionalQuery); 128 } 129 if (serverData.isDefinedNonNullNonEmpty(request.searchTerm)) { 130 // Search hints shouldn't add `search` to the end of the path name as the correct final path 131 // is `v1/catalog/us/search/suggestions`, which is handled by `trailingPathComponentForResourceType()` 132 // Search hints also shouldn't have the bubble param 133 if (isNothing(request.resourceType) || request.resourceType !== "search-hints") { 134 mediaApiURL.append("pathname", "search"); 135 mediaApiURL.param("bubble[search]", request.searchTypes.join(",")); 136 } 137 mediaApiURL.param("term", request.searchTerm); 138 } 139 if (serverData.isDefinedNonNullNonEmpty(request.enabledFeatures)) { 140 mediaApiURL.param("with", request.enabledFeatures.join(",")); 141 } 142 if (serverData.isDefinedNonNullNonEmpty(request.context)) { 143 mediaApiURL.param("contexts", request.context); 144 } 145 if (serverData.isDefinedNonNullNonEmpty(request.filterType) && 146 serverData.isDefinedNonNullNonEmpty(request.filterValue)) { 147 mediaApiURL.param(`filter[${request.filterType}]`, request.filterValue); 148 } 149 const language = objectGraph.bag.mediaApiLanguage; 150 // Only attach the language query param if: 151 // - there is a language available in the bag, and 152 // - a language has not been manually attached to the request. This is used for special situations to override the language for particular content, 153 // so it should take precedence over the default. 154 if (serverData.isDefinedNonNull(language) && serverData.isNull(request.additionalQuery["l"])) { 155 mediaApiURL.param("l", language); 156 } 157 mediaApiURL.host = (_b = hostForUrl(objectGraph, mediaApiURL, request)) !== null && _b !== void 0 ? _b : undefined; 158 mediaApiURL.protocol = "https"; 159 return mediaApiURL; 160} 161/** 162 * Get the media api base url for all requests. 163 * @param objectGraph Current object graph 164 * @param isMixedCatalogRequest Whether the request intends to use mixed catalog 165 * @param type The request resource type 166 * @param overrideCountryCode A country code to override the bag value. 167 * @returns A built base URL string 168 */ 169function baseURLForResourceType(objectGraph, isMixedCatalogRequest, type, overrideCountryCode) { 170 switch (type) { 171 case "personalization-data": 172 case "reviews": 173 case "app-distribution": 174 return `/v1/${endpointTypeForResourceType(type)}/`; 175 default: 176 const countryCode = isSome(overrideCountryCode) && overrideCountryCode.length > 0 177 ? overrideCountryCode 178 : objectGraph.bag.mediaCountryCode; 179 const baseURL = `/v1/${endpointTypeForResourceType(type)}/${countryCode}`; 180 return isMixedCatalogRequest ? baseURL : `${baseURL}/`; 181 } 182} 183/** 184 * Get the media api base url for all requests that already have an href. 185 * @return {string} 186 */ 187function baseURLForHref(href) { 188 return href; 189} 190function endpointTypeForResourceType(type) { 191 switch (type) { 192 case "apps": 193 case "app-events": 194 case "arcade-apps": 195 case "app-bundles": 196 case "charts": 197 case "contents": 198 case "developers": 199 case "eula": 200 case "in-apps": 201 case "multiple-system-operators": 202 case "user-reviews": 203 case "customers-also-bought-apps-with-download-intent": 204 return "catalog"; 205 case "categories": 206 case "editorial-pages": 207 case "editorial-items": 208 case "editorial-item-groups": 209 case "editorial-elements": 210 case "groupings": 211 case "multiplex": 212 case "multirooms": 213 case "rooms": 214 case "today": 215 case "collections": 216 return "editorial"; 217 case "ratings": 218 return "ratings"; 219 case "personalization-data": 220 case "reviews": 221 return "me"; 222 case "upsellMarketingItem": 223 case "landing": 224 return "engagement"; 225 case "landing:new-protocol": 226 return "recommendations"; 227 case "personal-recommendations": 228 return "recommendations"; 229 case "engagement-data": 230 return "engagement"; 231 case "app-distribution": 232 return "listing"; 233 default: 234 return "catalog"; 235 } 236} 237/** 238 * The path component to add for the given resource 239 */ 240function pathComponentsForRequest(resourceType, targetResourceType) { 241 switch (resourceType) { 242 case "eula": 243 if (targetResourceType === undefined) { 244 return [resourceType]; // Might be modelled better as an error. 245 } 246 else { 247 return [resourceType, targetResourceType]; 248 } 249 case "landing:new-protocol": 250 return []; 251 case "landing": 252 if (targetResourceType === undefined) { 253 return ["search", resourceType]; // Might be modelled better as an error. 254 } 255 else { 256 return ["search", resourceType, targetResourceType]; 257 } 258 case "user-reviews": 259 return ["apps"]; 260 case "reviews": 261 return ["reviews", "apps"]; 262 case "multiplex": 263 return ["multiplex"]; 264 case "upsellMarketingItem": 265 return ["upsell", "marketing-items"]; 266 case "trending-contents": 267 return ["search", resourceType]; 268 case "customers-also-bought-apps-with-download-intent": 269 return ["apps"]; 270 case "searchLanding:see-all": 271 return []; 272 case "search-hints": 273 return []; 274 case "app-distribution": 275 return ["apps"]; 276 default: 277 return [resourceType]; 278 } 279} 280/** 281 * Add a component to the end of the path for the given resource 282 */ 283function trailingPathComponentForResourceType(type) { 284 switch (type) { 285 case "user-reviews": 286 return "reviews"; 287 case "customers-also-bought-apps-with-download-intent": 288 return "view/customers-also-bought-apps-with-download-intent"; 289 case "collections": 290 return "contents"; 291 case "searchLanding:see-all": 292 return "view/see-all"; 293 case "search-hints": 294 return "search/suggestions"; 295 default: 296 return null; 297 } 298} 299function hostForUrl(objectGraph, url, request) { 300 var _a; 301 const path = (_a = url.pathname) !== null && _a !== void 0 ? _a : ""; 302 let host = null; 303 if (request.isStorePreviewRequest) { 304 host = objectGraph.bag.mediaPreviewHost; 305 } 306 else if (request.isMediaRealmRequest) { 307 host = objectGraph.bag.mediaRealmHost; 308 } 309 else if (path.includes("search/landing")) { 310 // Special case <rdar://problem/50185140> RFW3: Use bag key "apps-media-api-edge-end-points" for "search/landing" end-point 311 // until we figure out a better way to test the paths 312 const useEdgeForSearchLanding = objectGraph.bag.edgeEndpoints.indexOf("landing") !== -1; 313 host = useEdgeForSearchLanding ? objectGraph.bag.mediaEdgeHost(objectGraph) : objectGraph.bag.mediaHost; 314 } 315 else if (request.resourceType === "app-distribution" && isSome(objectGraph.bag.appDistributionMediaAPIHost)) { 316 host = objectGraph.bag.appDistributionMediaAPIHost; 317 } 318 else if (request.isMixedMediaRequest && objectGraph.bag.mediaAPICatalogMixedShouldUseEdge) { 319 // CatalogMixed endpoint should be routed to edge when the bag is enabled. 320 host = objectGraph.bag.mediaEdgeHost(objectGraph); 321 } 322 else if (objectGraph.bag.edgeEndpoints.map((endpoint) => path.includes(endpoint)).reduce(truthReducer, false)) { 323 if (path.includes("search") && !path.includes("view/see-all")) { 324 host = objectGraph.bag.mediaEdgeSearchHost; 325 } 326 else { 327 host = objectGraph.bag.mediaEdgeHost(objectGraph); 328 } 329 } 330 else { 331 host = objectGraph.bag.mediaHost; 332 } 333 if (serverData.isNull(host)) { 334 host = "api.apps.apple.com"; 335 } 336 return host; 337} 338const truthReducer = (accumulator, current) => accumulator || current; 339/** 340 * Performs a conversion for given attribute to fetch the customAttribute variant of it. 341 * @param objectGraph The object graph 342 * @param attribute Attribute to convert if needed, e.g. `artwork` 343 * @returns `string` attribute that is custom equivalent of `attribute`, or `attribute` unmodified. 344 */ 345function convertRequestAttributesToCustomAttributes(objectGraph, requestAttributes) { 346 const convertedAttributes = requestAttributes.map((attribute) => { 347 var _a; 348 return (_a = attributes.attributeKeyAsCustomAttributeKey(attribute)) !== null && _a !== void 0 ? _a : attribute; 349 }); 350 /** 351 * `artwork` is an autoincluded resources, so `attributes` usually doesn't contain this explicitly :( 352 * Per MAPI contract, we "autoinclude" `customArtwork` explicitly for requests with custon attributes. 353 */ 354 convertedAttributes.push("customArtwork"); 355 /** 356 * `iconArtwork` is not autoincluded, but we need to ensure it is always requested even alongside its 357 * custom counterpart, `customIconArtwork`. This is because we might be viewing a macOS only app on iOS, 358 * where custom attributes are supported, but not available for macOS apps. 359 */ 360 if (requestAttributes.includes("iconArtwork")) { 361 convertedAttributes.push("iconArtwork"); 362 } 363 /** 364 * `customDeepLink` is always desired as an included resource in case an app decides to use a custom tap destination. 365 * Per MAPI contract, we "autoinclude" `customDeepLink` explicitly for all requests with custom attributes. 366 */ 367 convertedAttributes.push("customDeepLink"); 368 return convertedAttributes; 369} 370/** 371 * Performs the conversion for given field value (which may specify `attributes` keys) to customAttribute variant of it. 372 */ 373function convertRequestFieldsToCustomFields(requestFields) { 374 const convertedFields = requestFields.map((fieldName) => { 375 var _a; 376 return (_a = attributes.attributeKeyAsCustomAttributeKey(fieldName)) !== null && _a !== void 0 ? _a : fieldName; 377 }); 378 // DON'T include `customArtwork` for request `field` conversion. Only specify if `artwork` was initially in `requestFields`. 379 return convertedFields; 380} 381//# sourceMappingURL=url-builder.js.map