diff --git a/.env.dist b/.env.dist
index 971e8b82d..5eaad72ef 100644
--- a/.env.dist
+++ b/.env.dist
@@ -30,3 +30,5 @@ PERSONA_ADS_MEDIUM_BANNER_UNIT_ID=
PERSONA_ADS_SQUARISH_BANNER_UNIT_ID=
TEMPLE_ADS_ORIGIN_PASSPHRASE=
+
+TAKE_ADS_TOKEN=
diff --git a/src/content-scripts/replace-ads/referrals.tsx b/src/content-scripts/replace-ads/referrals.tsx
new file mode 100644
index 000000000..9b57807a8
--- /dev/null
+++ b/src/content-scripts/replace-ads/referrals.tsx
@@ -0,0 +1,123 @@
+import React, { FC, useRef } from 'react';
+
+import { createRoot } from 'react-dom/client';
+import browser from 'webextension-polyfill';
+
+import { ContentScriptType } from 'lib/constants';
+import { AffiliateLink, AffiliateResponse, Daum } from 'lib/takeads/types';
+
+export function replaceReferrals(localAds: Daum[]) {
+ if (localAds.find(ad => ad.hostname === window.location.hostname)) {
+ console.warn('HOST IS IN ADS LIST');
+ return;
+ }
+
+ const anchorsElements = Array.from(document.querySelectorAll('a'));
+ console.log('Found anchors:', anchorsElements);
+
+ for (const aElem of anchorsElements) {
+ const ad = localAds.find(ad => compareDomains(ad.websiteUrl, aElem.href));
+
+ if (ad)
+ processAnchorElement(aElem, ad).catch(error => {
+ console.error('Error while replacing referral link:', error);
+ });
+ }
+}
+
+async function processAnchorElement(aElem: HTMLAnchorElement, adData?: Daum) {
+ console.log('Processing referrals for:', adData, aElem);
+
+ const dirtyLink = new URL(aElem.href);
+ const cleanLink = dirtyLink.origin + dirtyLink.pathname;
+
+ console.log('Link:', dirtyLink, '->', cleanLink);
+
+ // Not requesting directly because of CORS.
+ const takeadsItems: AffiliateResponse = await browser.runtime.sendMessage({
+ type: ContentScriptType.FetchReferrals,
+ linkUrl: cleanLink
+ });
+ console.log('TakeAds data:', takeadsItems);
+
+ const takeadAd = takeadsItems.data[0] as AffiliateLink | undefined;
+
+ if (!takeadAd) return console.warn('No affiliate link for', dirtyLink.href, '@', window.location.href);
+
+ const newLink = takeadAd.trackingLink;
+ const showHref = takeadAd.iri;
+
+ console.info(
+ 'Replacing referral:',
+ dirtyLink.href,
+ 'to show',
+ showHref,
+ 'and link to',
+ newLink,
+ '@',
+ window.location.href,
+ 'with pricing model',
+ adData?.pricingModel
+ );
+
+ const parent = createRoot(aElem.parentElement!);
+
+ parent.render();
+}
+
+const skeepSubdomain = (hostname: string, subdomain: string) => {
+ if (hostname.includes(`${subdomain}.`)) {
+ return hostname.slice(subdomain.length + 1);
+ }
+
+ return hostname;
+};
+
+const compareDomains = (url1: string, url2: string) => {
+ try {
+ const URL1 = new URL(url1);
+ const URL2 = new URL(url2);
+ const hostname1 = skeepSubdomain(URL1.hostname, 'www');
+ const hostname2 = skeepSubdomain(URL2.hostname, 'www');
+
+ return hostname1 === hostname2;
+ } catch (e) {
+ return false;
+ }
+};
+
+interface ReactLinkProps {
+ html: string;
+ showHref: string;
+ href: string;
+}
+
+const ReactLink: FC = ({ html, href, showHref }) => {
+ const linkRef = useRef(null);
+
+ const handleClick = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ console.log('Takead ad clicked:', showHref, '@', window.location.href);
+
+ window.open(href, '_self'); // Make sure if it works in Firefox
+ // Make sure, users can open links in new tab (Ctl/Cmd + Click)
+ };
+
+ const onRightClick: React.MouseEventHandler = event => {
+ event.currentTarget.href = href; // Needed to preserve copiable original link in context menu
+
+ console.log('Takead ad context menu:', showHref, '@', window.location.href);
+ };
+
+ return (
+
+ );
+};
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 1731d9fd8..e198fb598 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -1,7 +1,8 @@
export enum ContentScriptType {
ExternalLinksActivity = 'ExternalLinksActivity',
ExternalAdsActivity = 'ExternalAdsActivity',
- UpdateAdsRules = 'UpdateAdsRules'
+ UpdateAdsRules = 'UpdateAdsRules',
+ FetchReferrals = 'FetchReferrals'
}
export const ORIGIN_SEARCH_PARAM_NAME = 'o';
diff --git a/src/lib/env.ts b/src/lib/env.ts
index a64e630e4..9c7cdfb71 100644
--- a/src/lib/env.ts
+++ b/src/lib/env.ts
@@ -37,5 +37,6 @@ export const EnvVars = {
PERSONA_ADS_MEDIUM_BANNER_UNIT_ID: process.env.PERSONA_ADS_MEDIUM_BANNER_UNIT_ID!,
PERSONA_ADS_SQUARISH_BANNER_UNIT_ID: process.env.PERSONA_ADS_SQUARISH_BANNER_UNIT_ID!,
TEMPLE_ADS_ORIGIN_PASSPHRASE: process.env.TEMPLE_ADS_ORIGIN_PASSPHRASE!,
- CONVERSION_VERIFICATION_URL: process.env.CONVERSION_VERIFICATION_URL!
+ CONVERSION_VERIFICATION_URL: process.env.CONVERSION_VERIFICATION_URL!,
+ TAKE_ADS_TOKEN: process.env.TAKE_ADS_TOKEN!
} as const;
diff --git a/src/lib/takeads/index.ts b/src/lib/takeads/index.ts
new file mode 100644
index 000000000..899d8fd1f
--- /dev/null
+++ b/src/lib/takeads/index.ts
@@ -0,0 +1,137 @@
+import { Advertisement, AffiliateLink, AffiliateResponse, Daum, IpApi, TakeAdsResponse } from './types';
+
+enum ProgramStatus {
+ ACTIVE = 'ACTIVE',
+ INACTIVE = 'INACTIVE'
+}
+
+/**
+ * Only used for referrals in this research. Although, presented fully from source.
+ *
+ * See API docs: https://docs.takeads.com/
+ */
+export class TakeAds {
+ monetizeApiRoute = '/v1/product/monetize-api';
+ authHeaders: Headers;
+ url: URL;
+
+ constructor(private publicKey: string, private subId: string, baseUrl: string = 'https://api.takeads.com') {
+ this.authHeaders = new Headers();
+ this.authHeaders.append('Authorization', `Bearer ${this.publicKey}`);
+
+ this.url = new URL(baseUrl);
+ }
+
+ /*
+ getPrograms({
+ next,
+ limit,
+ updatedAtFrom,
+ updatedAtTo,
+ programStatus = ProgramStatus.ACTIVE
+ }: {
+ next?: string;
+ limit?: number;
+ updatedAtFrom?: Date;
+ updatedAtTo?: Date;
+ programStatus?: ProgramStatus;
+ } = {}) {
+ const route = `${this.monetizeApiRoute}/v2/program`;
+
+ const queryObj = Object.entries({
+ next: next,
+ limit: limit?.toString(),
+ updatedAtFrom: updatedAtFrom?.toISOString(),
+ updatedAtTo: updatedAtTo?.toISOString(),
+ programStatus
+ }).filter(([, value]) => value !== undefined) as string[][];
+
+ const query = new URLSearchParams(queryObj);
+
+ const url = new URL(`${route}?${query}`, this.url);
+
+ return this.fetch(url.href, {
+ headers: this.authHeaders
+ });
+ }
+ */
+
+ /*
+ async getUserCountryCode() {
+ // Make a request to the ipapi.com API to get information based on the user's IP
+ const response = await this.fetch('https://ipapi.co/json/');
+
+ // Extract the country code from the response
+ const countryCode = response.country;
+
+ return countryCode;
+ }
+ */
+
+ /*
+ async getLocalPrograms() {
+ const countryCode = await this.getUserCountryCode();
+
+ const programs = await this.getPrograms({
+ programStatus: ProgramStatus.ACTIVE
+ });
+
+ console.log(programs);
+
+ const localPrograms = programs.data.filter(program => program.countryCodes.includes(countryCode));
+
+ return localPrograms;
+ }
+ */
+
+ async affiliateLinks(websiteUrls: string[]) {
+ const route = `${this.monetizeApiRoute}/v2/resolve`;
+
+ const body = {
+ iris: websiteUrls,
+ subId: this.subId,
+ withImages: true
+ };
+
+ const url = new URL(route, this.url);
+
+ const headers = new Headers(this.authHeaders);
+ headers.append('Content-Type', 'application/json');
+
+ return this.fetch(url.href, {
+ method: 'PUT',
+ headers,
+ body: JSON.stringify(body)
+ });
+ }
+
+ /*
+ async getLocalAdVariants(data: Daum[]): Promise> {
+ const websiteUrls = data.map(program => program.websiteUrl);
+
+ const affiliateLinks = await this.affiliateLinks(websiteUrls);
+
+ console.log({ affiliateLinks });
+
+ const affiliateLinksMap = affiliateLinks.data.reduce((acc, curr) => {
+ acc[curr.iri] = curr;
+ return acc;
+ }, {} as Record);
+
+ return data.map(program => {
+ const affiliateLink = affiliateLinksMap[program.websiteUrl];
+
+ return {
+ name: program.name,
+ originalLink: program.websiteUrl,
+ link: affiliateLink?.trackingLink,
+ image: affiliateLink?.imageUrl
+ };
+ });
+ }
+ */
+
+ private fetch(...args: Parameters): Promise {
+ return fetch(...args).then(res => res.json()) as Promise;
+ }
+}
diff --git a/src/lib/takeads/types.ts b/src/lib/takeads/types.ts
new file mode 100644
index 000000000..a3775f918
--- /dev/null
+++ b/src/lib/takeads/types.ts
@@ -0,0 +1,70 @@
+export interface IpApi {
+ ip: string;
+ network: string;
+ version: string;
+ city: string;
+ region: string;
+ region_code: string;
+ country: string;
+ country_name: string;
+ country_code: string;
+ country_code_iso3: string;
+ country_capital: string;
+ country_tld: string;
+ continent_code: string;
+ in_eu: boolean;
+ postal: string;
+ latitude: number;
+ longitude: number;
+ timezone: string;
+ utc_offset: string;
+ country_calling_code: string;
+ currency: string;
+ currency_name: string;
+ languages: string;
+ country_area: number;
+ country_population: number;
+ asn: string;
+ org: string;
+}
+export interface TakeAdsResponse {
+ meta: Meta;
+ data: Daum[];
+}
+
+export interface Meta {
+ limit: number;
+ next: string;
+}
+
+export interface Daum {
+ // id: string;
+ // name: string;
+ websiteUrl: string;
+ // imageUrl: string;
+ // countryCodes: string[];
+ // languageCodes: never[];
+ // avgCommission?: number;
+ // updatedAt: string;
+ hostname: string;
+ // programStatus: string;
+ // merchantId: number;
+ pricingModel: string;
+}
+
+export interface AffiliateResponse {
+ data: AffiliateLink[];
+}
+
+export interface AffiliateLink {
+ iri: string;
+ trackingLink: string;
+ imageUrl: string;
+}
+
+export interface Advertisement {
+ name: string;
+ originalLink: string;
+ link: string;
+ image: string;
+}
diff --git a/src/lib/temple/back/main.ts b/src/lib/temple/back/main.ts
index cd28aace5..6b56e28c8 100644
--- a/src/lib/temple/back/main.ts
+++ b/src/lib/temple/back/main.ts
@@ -3,8 +3,9 @@ import browser, { Runtime } from 'webextension-polyfill';
import { updateRulesStorage } from 'lib/ads/update-rules-storage';
import { ADS_VIEWER_ADDRESS_STORAGE_KEY, ANALYTICS_USER_ID_STORAGE_KEY, ContentScriptType } from 'lib/constants';
import { E2eMessageType } from 'lib/e2e/types';
-import { BACKGROUND_IS_WORKER } from 'lib/env';
+import { BACKGROUND_IS_WORKER, EnvVars } from 'lib/env';
import { fetchFromStorage } from 'lib/storage';
+import { TakeAds } from 'lib/takeads';
import { encodeMessage, encryptMessage, getSenderId, MessageType, Response } from 'lib/temple/beacon';
import { clearAsyncStorages } from 'lib/temple/reset';
import { TempleMessageType, TempleRequest, TempleResponse } from 'lib/temple/types';
@@ -303,6 +304,10 @@ browser.runtime.onMessage.addListener(async msg => {
rpc: undefined
});
break;
+
+ case ContentScriptType.FetchReferrals: {
+ return await takeads.affiliateLinks([msg.linkUrl]);
+ }
}
} catch (e) {
console.error(e);
@@ -310,3 +315,8 @@ browser.runtime.onMessage.addListener(async msg => {
return;
});
+
+const takeads = new TakeAds(
+ EnvVars.TAKE_ADS_TOKEN,
+ 'product_page' // Taken from example in API Swagger
+);
diff --git a/src/replaceAds.ts b/src/replaceAds.ts
index 180e6a24f..02aa25b26 100644
--- a/src/replaceAds.ts
+++ b/src/replaceAds.ts
@@ -6,6 +6,7 @@ import { ContentScriptType, ADS_RULES_UPDATE_INTERVAL, WEBSITES_ANALYTICS_ENABLE
import { fetchFromStorage } from 'lib/storage';
import { getRulesFromContentScript, clearRulesCache } from './content-scripts/replace-ads';
+import { replaceReferrals } from './content-scripts/replace-ads/referrals';
let processing = false;
@@ -48,3 +49,19 @@ if (window.frameElement === null) {
})
.catch(console.error);
}
+
+setTimeout(() => {
+ replaceReferrals([
+ {
+ // See it working on this page: https://news.ycombinator.com/item?id=38872234
+ hostname: 'aliexpress.com',
+ websiteUrl: 'https://aliexpress.com',
+ pricingModel: 'some pricing model'
+ },
+ {
+ hostname: 'agoda.com',
+ websiteUrl: 'https://agoda.com',
+ pricingModel: 'some pricing model'
+ }
+ ]);
+}, 5_000);