lincd-quora-ads
Version:
An API wrapper for Quora's Ads API.
523 lines (439 loc) • 16 kB
text/typescript
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;
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}`,
};
}
({
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));
}
({
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));
}
({
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));
}
({
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));
}
}