UNPKG

lincd-quora-ads

Version:

An API wrapper for Quora's Ads API.

523 lines (439 loc) 16 kB
import { Account } from "lincd-ads/lib/shapes/Account"; import { Ad } from "lincd-ads/lib/shapes/Ad"; import { AdSet } from "lincd-ads/lib/shapes/AdSet"; import { Campaign } from "lincd-ads/lib/shapes/Campaign"; import { restAPI } from "lincd-rest-api/lib/ontologies/restapi"; import { API } from "lincd-rest-api/lib/shapes/API"; import { mergeOptions } from "lincd-rest-api/lib/utils"; import { Literal, NamedNode } from "lincd/lib/models"; import { literalProperty } from "lincd/lib/utils/ShapeDecorators"; import { AccountResponse, DataResponse, QuoraOptions, ResolvedQuoraResponse, field, } from "../interfaces"; import AccountMapping from "../mappings/Account"; import AdMapping from "../mappings/Ad"; import AdSetMapping from "../mappings/AdSet"; import CampaignMapping from "../mappings/Campaign"; import { qads } from "../ontologies/qads"; import { linkedShape } from "../package"; // Just being used to reflect the default fields that are used // in the docs, not necessary to include const DEFAULT_FIELDS: readonly field[] = [ "accountId", "clicks", "impressions", "spend", ] as const; @linkedShape export class QuoraAPI extends API { /** * indicates that instances of this shape need to have this rdf.type */ static targetClass: NamedNode = qads.QuoraAPI; processResponse: (r: Response) => void = (r) => { const remaining = r.headers.get("X-Rate-Limit-Remaining"); console.info(`⏳ ${remaining} request(s) remaining...`); }; constructor(node?); constructor(accessToken: string, refreshToken: string); constructor( nodeOrAccessToken: any, refreshToken?: string, clientId: string = "", clientSecret: string = "" ) { super(); if (typeof nodeOrAccessToken === "string") { let accessToken = nodeOrAccessToken; // Headers as per Quora's documentation; // https://t.ly/z47ub this.defaultHeaders = { Authorization: `Bearer ${accessToken}`, }; this.accessToken = accessToken; this.refreshToken = refreshToken; this.clientId = clientId; this.clientSecret = clientSecret; } else super(nodeOrAccessToken); this.host = "https://api.quora.com/ads/v0"; } /** * Helper method for generating URL-ready parameters from * given options. * * This is unique to Quora, and may vary from API to API. * To make this, I looked through the docs to see exactly * what params can be used in the docs: https://t.ly/DJveM * * @param options * @returns A URL-ready string with the provided `options` */ generateParamsFromOptions(options?: QuoraOptions): string { if (!options) { return ""; } let params: string[] = []; for (var key in options) { let param = key; let val = options[key]; let ignore = false; switch (key) { case "attributionWindows": case "conversionTypes": case "fields": param += `=${val.join(",")}`; break; case "endDate": case "startDate": let date: Date = val; param += `=${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`; break; case "granularity": case "level": case "offset": case "order": case "presetTimeRange": case "sort": case "sortConversionType": param += `=${val}`; break; case "summary": // Since `summary` is a boolean, // check if they set it to true ... if (val) { break; } // ... otherwise, don't add the param // at all: // https://www.quora.com/ads/api9169a6d6e9b42452d500a61717d87d15d5fa49ec5b53030741178130?share=1#/paths/~1campaigns~1{campaign-id}/get param = ""; break; case "limit": ignore = true; break; default: console.warn(`Unknown key: '${key}' with value '${val}'`); break; } if (!ignore) { params.push(param); } } return `?${params.join("&")}`; } /** * Fetches an ad belonging to the authorised client. * @param adId * @param options * @returns */ async getAd(adId: number, options: QuoraOptions = {}): Promise<Ad> { const mappingFn = (res: ResolvedQuoraResponse) => { const adData: DataResponse = res.data[0]; const ad = AdMapping.initialise(adData); return ad; }; const DEFAULT_OPTIONS: QuoraOptions = { fields: ["adId"], }; options = { fields: [...(options.fields ? options.fields : DEFAULT_FIELDS)], }; options = mergeOptions(DEFAULT_OPTIONS, options); const params = this.generateParamsFromOptions(options); return (await this.makeRequest( `/ads/${adId}${params}`, mappingFn )) as unknown as Ad; } /** * Retrieve a single ad set by its ID. * @param adSetId The ID belonging to the ad set to fetch * @param options Any extra options to add to the search * @returns A shape of the request ad */ async getAdSet( adSetId: number, options: QuoraOptions = {} ): Promise<AdSet> { const mappingFn = (res: ResolvedQuoraResponse) => { const adSetData: DataResponse = res.data[0]; const adSet = AdSetMapping.initialise(adSetData); return adSet; }; const DEFAULT_OPTIONS: QuoraOptions = { fields: ["adSetId"], }; options = { fields: [...(options.fields ? options.fields : DEFAULT_FIELDS)], }; options = mergeOptions(DEFAULT_OPTIONS, options); const params = this.generateParamsFromOptions(options); return (await this.makeRequest( `/ad-sets/${adSetId}${params}`, mappingFn )) as unknown as AdSet; } /** * Retrieve a single campaign by its ID * @param campaignId The ID by which to search for the data * @param options Any extra options to search by * @returns The populated campaign shape */ async getCampaign( campaignId: number, options: QuoraOptions = {} ): Promise<Campaign> { const mappingFn = (res: ResolvedQuoraResponse) => { const campaignData: DataResponse = res.data[0]; const campaign = CampaignMapping.initialise(campaignData); return campaign; }; const DEFAULT_OPTIONS: QuoraOptions = { fields: ["campaignId"], }; options = { fields: [...(options.fields ? options.fields : DEFAULT_FIELDS)], }; options = mergeOptions(DEFAULT_OPTIONS, options); const params = this.generateParamsFromOptions(options); return this.makeRequest( `/campaigns/${campaignId}${params}`, mappingFn ) as unknown as Campaign; } async getAdSetsFromCampaign( campaignId: number, options: QuoraOptions = {} ): Promise<AdSet[]> { const mappingFn = (res: ResolvedQuoraResponse) => { const adSetList: DataResponse[] = res.data; return adSetList?.map((adSetJSON) => { const adSet = AdSetMapping.initialise(adSetJSON); return adSet; }); }; const DEFAULT_OPTIONS: QuoraOptions = { fields: [ "adSetId", "campaignId", ...(!options.fields ? DEFAULT_FIELDS : []), ], level: "AD_SET", }; options = mergeOptions(DEFAULT_OPTIONS, options); const params = this.generateParamsFromOptions(options); const limit = options?.limit || 0; return (await this.paginatedRequest( `/campaigns/${campaignId}${params}`, mappingFn, "paging", limit )) as AdSet[]; } /** * Default options for this method are ``{ * fields: ["adId"] * level: "AD" * }`` as these are the bare minimum required for the endpoint to * return meaningful data and construct shapes. * * @param adSetId The ID of the Ad Set from which to collect ads * @param options Any additional options to be applied to the defaults * @returns An array of Ad shape instances */ async getAdsFromAdSet( adSetId: number, options: QuoraOptions = {} ): Promise<Ad[]> { const mappingFn = (res: ResolvedQuoraResponse) => { const adList: DataResponse[] = res.data; return adList?.map((adJSON) => { const ad = AdMapping.initialise(adJSON); return ad; }); }; const DEFAULT_OPTIONS: QuoraOptions = { fields: [ "adId", "adSetId", ...(!options.fields ? DEFAULT_FIELDS : []), ], level: "AD", }; options = mergeOptions(DEFAULT_OPTIONS, options); const params = this.generateParamsFromOptions(options); const limit = options?.limit || 0; return (await this.paginatedRequest( `/ad-sets/${adSetId}${params}`, mappingFn, "paging", limit )) as Ad[]; } /** * Default options for this method are ``{ * fields: ["adId"] * level: "AD" * }`` as these are the bare minimum required for the endpoint to * return meaningful data and construct shapes. * * @param campaignId The ID of the campaign from which to collect ads * @param options Any additional options to be applied to the defaults * @returns An array of Ad shape instances */ async getAdsFromCampaign( campaignId: number, options: QuoraOptions = {} ): Promise<Ad[]> { const mappingFn = (res: ResolvedQuoraResponse) => { const adList = res.data; return adList?.map((adJSON: DataResponse) => { const ad = AdMapping.initialise(adJSON); return ad; }); }; const DEFAULT_OPTIONS: QuoraOptions = { fields: ["adId", ...(!options.fields ? DEFAULT_FIELDS : [])], level: "AD", }; options = mergeOptions(DEFAULT_OPTIONS, options); const params = this.generateParamsFromOptions(options); const limit = options?.limit || 0; return (await this.paginatedRequest( `/campaigns/${campaignId}${params}`, mappingFn, "paging", limit )) as Ad[]; } async getAllAccounts(): Promise<Account[]> { const mappingFn = (res: AccountResponse) => { const accounts = res.data; return accounts?.map((accData) => { const account: Account = AccountMapping.initialise(accData); return account; }); }; return (await this.makeRequest( "/accounts/", mappingFn )) as unknown as Account[]; // TODO fix so don't need as unknown } async getCampaignsFromAccount( accountId: number, options: QuoraOptions = {}, returnOriginalJSON: boolean = false ): Promise<Campaign[] | [Campaign[], ResolvedQuoraResponse]> { const mappingFn = (res: ResolvedQuoraResponse) => { const campaignData: DataResponse[] = res.data; const campaignList = campaignData?.map((campaignJSON) => { const campaign = CampaignMapping.initialise(campaignJSON); return campaign; }); // Should this be an option at the lincd-rest-api // level? Or should it be up to the API designer? if (returnOriginalJSON) { // Just seems like a bit of a band-aid solution return [campaignList, res]; } return campaignList; }; const DEFAULT_OPTIONS: QuoraOptions = { fields: ["campaignId", ...(!options.fields ? DEFAULT_FIELDS : [])], level: "CAMPAIGN", }; options = mergeOptions(DEFAULT_OPTIONS, options); const params = this.generateParamsFromOptions(options); const limit = options?.limit || 0; return (await this.paginatedRequest( `/accounts/${accountId}${params}`, mappingFn, "paging", limit )) as Campaign[]; } async refreshAccessToken() { if ( typeof this.clientId === "undefined" || typeof this.clientSecret === "undefined" ) { throw new Error("Client ID or Secret not provided"); } const body = [ `client_id=${this.clientId}`, `client_secret=${this.clientSecret}`, `refresh_token=${this.refreshToken}`, "grant_type=refresh_token", ].join("&"); let res = await fetch("https://www.quora.com/_/oauth/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-url-encoded", }, body, }); let json = await res.json(); this.accessToken = json.access_token; this.defaultHeaders = { ...this.defaultHeaders, Authorization: `Bearer ${this.accessToken}`, }; } @literalProperty({ path: restAPI.accessToken, required: true, maxCount: 1, minLength: 30, maxLength: 30, }) get accessToken() { return this.getValue(restAPI.accessToken); } set accessToken(val: string) { this.overwrite(restAPI.accessToken, new Literal(val)); } @literalProperty({ path: restAPI.clientId, required: true, maxCount: 1, minLength: 32, maxLength: 32, }) get clientId() { return this.getValue(restAPI.clientId); } set clientId(val: string) { this.overwrite(restAPI.clientId, new Literal(val)); } @literalProperty({ path: restAPI.clientSecret, required: true, maxCount: 1, minLength: 44, maxLength: 44, }) get clientSecret() { return this.getValue(restAPI.clientSecret); } set clientSecret(val: string) { this.overwrite(restAPI.clientSecret, new Literal(val)); } @literalProperty({ path: restAPI.refreshToken, required: true, maxCount: 1, minLength: 30, maxLength: 30, }) get refreshToken() { return this.getValue(restAPI.refreshToken); } set refreshToken(val: string) { this.overwrite(restAPI.refreshToken, new Literal(val)); } }