diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index c68290c6c23d..6cbf9bb61cb4 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -347,6 +347,8 @@ jobs: --set-env-vars="ORIGIN_PLAY=mdnyalp.dev" \ --set-env-vars="SOURCE_CONTENT=https://storage.googleapis.com/${{ vars.GCP_BUCKET_NAME }}/main/" \ --set-env-vars="SOURCE_API=https://api.developer.allizom.org/" \ + --set-env-vars="BSA_ENABLED=true" \ + --set-env-vars="BSA_URL_PREFIX=https://developer.allizom.org/fr/" \ --set-env-vars="SENTRY_DSN=${{ secrets.SENTRY_DSN_CLOUD_FUNCTION }}" \ --set-env-vars="SENTRY_ENVIRONMENT=stage" \ --set-env-vars="SENTRY_TRACES_SAMPLE_RATE=${{ vars.SENTRY_TRACES_SAMPLE_RATE }}" \ @@ -354,8 +356,7 @@ jobs: --set-secrets="KEVEL_SITE_ID=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-kevel-site-id/versions/latest" \ --set-secrets="KEVEL_NETWORK_ID=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-kevel-network-id/versions/latest" \ --set-secrets="SIGN_SECRET=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-sign-secret/versions/latest" \ - --set-secrets="CARBON_ZONE_KEY=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-carbon-zone-key/versions/latest" \ - --set-secrets="CARBON_FALLBACK_ENABLED=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-fallback-enabled/versions/latest" \ + --set-secrets="BSA_ZONE_KEYS=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-bsa-zone-keys/versions/latest" \ 2>&1 | sed "s/^/[$region] /" & pids+=($!) done diff --git a/client/src/placement-context.tsx b/client/src/placement-context.tsx index 0b540771fc89..9d594d2d1ce1 100644 --- a/client/src/placement-context.tsx +++ b/client/src/placement-context.tsx @@ -39,6 +39,7 @@ export interface PlacementData { ctaTextColorDark?: string; ctaBackgroundColorDark?: string; }; + version?: number; } type PlacementType = "side" | "top" | "hpMain" | "hpFooter"; diff --git a/client/src/ui/organisms/placement/index.tsx b/client/src/ui/organisms/placement/index.tsx index 0f863b5e18ad..6ce37b5cd3cf 100644 --- a/client/src/ui/organisms/placement/index.tsx +++ b/client/src/ui/organisms/placement/index.tsx @@ -30,6 +30,7 @@ interface PlacementRenderArgs { cta?: string; user: User; style: object; + version?: number; } const INTERSECTION_OPTIONS = { @@ -42,9 +43,7 @@ function viewed(pong?: PlacementData) { pong?.view && navigator.sendBeacon?.( `/pong/viewed?code=${encodeURIComponent(pong?.view)}${ - pong?.fallback - ? `&fallback=${encodeURIComponent(pong?.fallback?.view)}` - : "" + pong?.version ? `&version=${pong.version}` : "" }` ); } @@ -262,7 +261,7 @@ export function PlacementInner({ }, [isVisible, isIntersecting, sendViewed]); const { image, copy } = pong?.fallback || pong || {}; - const { click } = pong || {}; + const { click, version } = pong || {}; return ( <> {!isServer && @@ -278,6 +277,7 @@ export function PlacementInner({ cta, user, style, + version, })} ); @@ -294,6 +294,7 @@ function RenderSideOrTopBanner({ cta, user, style, + version = 1, }: PlacementRenderArgs) { return (
diff --git a/cloud-function/README.md b/cloud-function/README.md index 479fbb1629a4..7eae98ab6fb7 100644 --- a/cloud-function/README.md +++ b/cloud-function/README.md @@ -46,9 +46,10 @@ The placement handler uses the following environment variables: - `KEVEL_SITE_ID` (default: `0`) - Required for serving placements via Kevel. - `KEVEL_NETWORK_ID` (default: `0`) - Required for serving placements via Kevel. - `SIGN_SECRET` (default: `""`) - Required for serving placements. -- `CARBON_ZONE_KEY` (default: `""`) - Required for serving placements via - Carbon. -- `CARBON_FALLBACK_ENABLED` (default: `"false"`) - Whether fallback placements - should be served via Carbon. +- `BSA_ZONE_KEYS` (default: `""`) - Required for serving placements via BSA. +- `BSA_URL_PREFIX`(default: "https://localhost") - Where to show BSA placements + if enabled. Formatted like : + "placementname1:zonekey1;placementkey2:zonekey2...". +- `BSA_ENABLED` (default: `"false"`) - Whether to use placements via BSA. You can override the defaults by adding a `.env` file with `KEY=value` lines. diff --git a/cloud-function/src/app.ts b/cloud-function/src/app.ts index e5d81d6ad619..04ed76e07541 100644 --- a/cloud-function/src/app.ts +++ b/cloud-function/src/app.ts @@ -5,7 +5,6 @@ import { ANY_ATTACHMENT_EXT } from "./internal/constants/index.js"; import { Origin } from "./env.js"; import { proxyContent } from "./handlers/proxy-content.js"; -import { proxyKevel } from "./handlers/proxy-kevel.js"; import { proxyApi } from "./handlers/proxy-api.js"; import { handleStripePlans } from "./handlers/handle-stripe-plans.js"; import { proxyTelemetry } from "./handlers/proxy-telemetry.js"; @@ -22,6 +21,7 @@ import { notFound } from "./middlewares/not-found.js"; import { resolveRunnerHtml } from "./middlewares/resolve-runner-html.js"; import { proxyRunner } from "./handlers/proxy-runner.js"; import { stripForwardedHostHeaders } from "./middlewares/stripForwardedHostHeaders.js"; +import { proxyPong } from "./handlers/proxy-pong.js"; const router = Router(); router.use(stripForwardedHostHeaders); @@ -37,8 +37,8 @@ router.all( proxyApi ); router.all("/submit/mdn-yari/*", requireOrigin(Origin.main), proxyTelemetry); -router.all("/pong/*", requireOrigin(Origin.main), express.json(), proxyKevel); -router.all("/pimg/*", requireOrigin(Origin.main), proxyKevel); +router.all("/pong/*", requireOrigin(Origin.main), express.json(), proxyPong); +router.all("/pimg/*", requireOrigin(Origin.main), proxyPong); router.get( ["/[^/]+/docs/*/runner.html", "/[^/]+/blog/*/runner.html", "/runner.html"], requireOrigin(Origin.play), diff --git a/cloud-function/src/env.ts b/cloud-function/src/env.ts index 04140d676012..adfbc6bc4a94 100644 --- a/cloud-function/src/env.ts +++ b/cloud-function/src/env.ts @@ -68,9 +68,13 @@ export function sourceUri(source: Source): string { export const KEVEL_SITE_ID = Number(process.env["KEVEL_SITE_ID"] ?? 0); export const KEVEL_NETWORK_ID = Number(process.env["KEVEL_NETWORK_ID"] ?? 0); export const SIGN_SECRET = process.env["SIGN_SECRET"] ?? ""; -export const CARBON_ZONE_KEY = process.env["CARBON_ZONE_KEY"] ?? ""; -export const CARBON_FALLBACK_ENABLED = Boolean( - JSON.parse(process.env["CARBON_FALLBACK_ENABLED"] || "false") +export const BSA_ZONE_KEYS = Object.fromEntries( + (process.env["BSA_ZONE_KEYS"] ?? "").split(";").map((k) => k.split(":")) +); +export const BSA_URL_PREFIX = + process.env["BSA_URL_PREFIX"] ?? "https://localhost"; +export const BSA_ENABLED = Boolean( + JSON.parse(process.env["BSA_ENABLED"] || "false") ); // HTTPS. diff --git a/cloud-function/src/handlers/proxy-bsa.ts b/cloud-function/src/handlers/proxy-bsa.ts new file mode 100644 index 000000000000..bfa752d511c7 --- /dev/null +++ b/cloud-function/src/handlers/proxy-bsa.ts @@ -0,0 +1,94 @@ +import * as url from "node:url"; + +import type { Request, Response } from "express"; + +import { Coder } from "../internal/pong/index.js"; +import { + createPong2GetHandler, + createPong2ClickHandler, + createPong2ViewedHandler, + fetchImage, +} from "../internal/pong/index.js"; + +import * as env from "../env.js"; + +import { getRequestCountry } from "../utils.js"; + +const { SIGN_SECRET, BSA_ZONE_KEYS } = env; + +const coder = new Coder(SIGN_SECRET); +const handleGet = createPong2GetHandler(BSA_ZONE_KEYS, coder, env); +const handleClick = createPong2ClickHandler(coder); +const handleViewed = createPong2ViewedHandler(coder); + +export async function proxyBSA(req: Request, res: Response) { + const countryCode = getRequestCountry(req); + + const userAgent = req.headers["user-agent"] ?? ""; + + const parsedUrl = url.parse(req.url); + const pathname = parsedUrl.pathname ?? ""; + const search = parsedUrl.search ?? ""; + + if (pathname === "/pong/get") { + if (req.method !== "POST") { + return res.sendStatus(405).end(); + } + + const { body } = req; + const { statusCode: status, payload } = await handleGet( + body, + countryCode, + userAgent + ); + + return res + .status(status) + .setHeader("cache-control", "no-store") + .setHeader("content-type", "application/json") + .end(JSON.stringify(payload)); + } else if (req.path === "/pong/click") { + if (req.method !== "GET") { + return res.sendStatus(405).end(); + } + const params = new URLSearchParams(search); + try { + const { status, location } = await handleClick(params); + if (location && (status === 301 || status === 302)) { + return res.redirect(location); + } else { + return res.sendStatus(502).end(); + } + } catch (e) { + console.error(e); + } + } else if (pathname === "/pong/viewed") { + if (req.method !== "POST") { + return res.sendStatus(405).end(); + } + const params = new URLSearchParams(search); + try { + await handleViewed(params); + return res.sendStatus(201).end(); + } catch (e) { + console.error(e); + } + } else if (pathname.startsWith("/pimg/")) { + const src = coder.decodeAndVerify( + decodeURIComponent(pathname.substring("/pimg/".length)) + ); + if (!src) { + return res.sendStatus(400).end(); + } + const { buf, contentType } = await fetchImage(src); + return res + .status(200) + .set({ + "cache-control": "max-age=86400", + "content-type": contentType, + }) + .end(Buffer.from(buf)); + } + + return res.status(204).end(); +} diff --git a/cloud-function/src/handlers/proxy-pong.ts b/cloud-function/src/handlers/proxy-pong.ts new file mode 100644 index 000000000000..cc6886dd1e7b --- /dev/null +++ b/cloud-function/src/handlers/proxy-pong.ts @@ -0,0 +1,16 @@ +import { BSA_ENABLED, BSA_URL_PREFIX } from "../env.js"; +import { proxyBSA } from "./proxy-bsa.js"; +import { proxyKevel } from "./proxy-kevel.js"; + +import type { Request, Response } from "express"; + +export async function proxyPong(req: Request, res: Response) { + if (BSA_ENABLED) { + const referrer = req.get("referrer") || ""; + const version = Number(req.query["version"]) || 1; + if (referrer.startsWith(BSA_URL_PREFIX) || version === 2) { + return proxyBSA(req, res); + } + } + return proxyKevel(req, res); +} diff --git a/libs/pong/cc2ip.js b/libs/pong/cc2ip.js index 8812990abf45..4797d3126b0a 100644 --- a/libs/pong/cc2ip.js +++ b/libs/pong/cc2ip.js @@ -1,177 +1,90 @@ const cc2ip = { - AD: "46.172.232.102", - AE: "94.200.156.82", - AF: "103.146.146.97", - AG: "66.249.150.51", - AL: "192.109.219.158", - AM: "37.186.119.247", - AO: "41.63.188.109", - AQ: "2a05:dfc7:5353::53", - AR: "45.65.225.220", - AS: "202.70.125.9", - AT: "195.234.100.25", - AU: "203.147.94.71", - AZ: "85.132.17.38", - BA: "217.23.199.77", - BB: "64.210.41.182", - BD: "103.245.96.174", - BE: "194.6.227.60", - BF: "41.216.155.125", - BG: "194.12.224.14", - BH: "157.175.58.241", - BJ: "154.66.142.8", - BM: "199.68.195.249", - BN: "61.6.225.234", - BO: "190.186.245.140", - BQ: "190.107.252.178", - BR: "131.72.141.149", - BW: "154.73.39.241", - BY: "195.222.86.106", - BZ: "200.123.208.126", - CA: "23.252.239.1", - CD: "102.68.154.33", - CG: "41.207.125.57", - CH: "34.65.77.163", - CL: "45.7.228.232", - CM: "41.211.108.4", - CN: "223.6.6.195", - CO: "190.156.237.85", - CR: "190.106.79.228", - CU: "152.206.201.137", - CY: "46.199.74.102", - CZ: "188.116.91.246", - DE: "188.68.45.12", - DJ: "196.201.198.165", - DK: "152.115.91.203", - DO: "181.232.190.197", - DZ: "41.100.61.49", - EC: "181.198.11.152", - EE: "188.127.234.193", - EG: "41.176.151.71", - ES: "185.2.68.79", - ET: "196.188.168.146", - FI: "87.92.251.93", - FJ: "202.129.231.250", - FR: "217.111.148.201", - GA: "41.158.1.162", - GB: "87.242.157.62", - GE: "188.169.44.18", - GF: "217.108.102.6", - GH: "102.176.81.182", - GM: "197.148.74.19", - GN: "102.176.160.107", - GP: "81.248.145.154", - GQ: "102.164.255.149", - GR: "2.84.38.146", - GT: "38.52.208.199", - GU: "114.142.243.170", - HK: "202.155.202.75", - HN: "190.185.118.104", - HR: "85.114.40.50", - HU: "79.172.213.213", - ID: "175.103.40.42", - IE: "86.43.125.181", - IL: "31.154.9.62", - IM: "185.246.128.162", - IN: "164.52.223.153", - IQ: "213.32.252.91", - IR: "2.188.21.131", - IS: "193.4.89.2", - IT: "217.70.144.8", - JM: "63.143.125.14", - JO: "92.253.127.65", - JP: "153.182.23.230", - KE: "197.232.155.222", - KG: "92.62.65.237", - KH: "116.212.143.233", - KR: "121.124.124.196", - KW: "195.39.131.78", - KY: "216.144.84.145", - KZ: "87.76.54.213", - LA: "202.137.128.6", - LB: "89.108.139.246", - LK: "203.143.42.24", - LT: "88.119.87.88", - LU: "185.44.142.137", - LV: "86.63.169.194", - LY: "165.16.39.113", - MA: "197.230.103.202", - MD: "212.28.88.241", - ME: "95.155.31.120", - MH: "103.202.148.251", - MK: "146.255.89.51", - MM: "103.115.23.44", - MN: "202.55.190.30", - MO: "182.93.25.100", - MR: "82.151.74.36", - MT: "194.105.32.2", - MU: "41.216.125.179", - MV: "202.1.194.16", - MW: "41.216.228.228", - MX: "189.234.24.31", - MY: "60.51.247.44", - MZ: "197.219.229.86", - NA: "197.234.98.210", - NC: "202.22.148.124", - NG: "41.184.148.252", - NI: "200.62.96.39", - NL: "185.81.8.252", - NO: "80.202.239.184", - NP: "103.126.245.139", - NZ: "121.79.252.57", - OM: "5.21.239.143", - PA: "190.140.202.124", - PE: "200.37.203.90", - PF: "113.197.68.20", - PG: "124.240.199.23", - PH: "124.107.101.26", - PK: "110.39.8.113", - PL: "80.54.219.186", - PR: "12.205.65.7", - PS: "213.6.32.10", - PT: "62.28.205.202", - PW: "202.124.226.133", - PY: "201.217.51.46", - RE: "102.35.162.43", - RO: "188.27.244.73", - RS: "185.248.172.8", - RU: "89.110.59.24", - RW: "41.215.248.143", - SA: "51.211.38.5", - SB: "202.1.172.187", - SC: "185.247.225.17", - SD: "196.1.210.35", - SE: "158.174.37.226", - SG: "119.75.28.242", - SI: "195.230.121.12", - SK: "87.197.154.105", - SN: "213.154.80.203", - SV: "190.87.164.207", - SY: "5.134.255.230", - SZ: "102.215.99.18", - TD: "102.223.194.134", - TG: "41.207.186.166", - TH: "1.4.206.84", - TJ: "85.9.129.36", - TN: "197.15.87.14", - TR: "176.236.129.141", - TT: "200.1.104.36", - TW: "36.237.20.227", - TZ: "41.59.200.123", - UA: "176.126.123.42", - UG: "154.72.199.202", - US: "68.228.28.247", - UY: "190.64.151.74", - UZ: "185.183.242.130", - VE: "190.75.2.41", - VI: "8.26.19.118", - VN: "171.235.173.79", - YE: "134.35.132.212", - YT: "41.242.116.25", - ZA: "102.36.123.181", - ZW: "41.174.104.223", + AD: "194.158.64.0", + AE: "86.96.130.0", + AF: "103.132.98.0", + AL: "134.0.41.0", + AM: "195.43.74.0", + AO: "154.116.255.0", + AR: "200.108.146.0", + AT: "131.130.249.0", + AU: "103.29.195.0", + AZ: "217.14.97.0", + BA: "195.130.35.0", + BD: "103.48.18.0", + BE: "193.191.245.0", + BG: "212.122.160.0", + BO: "166.114.1.0", + BR: "200.160.4.0", + BS: "24.51.118.0", + BT: "202.144.128.0", + BY: "193.232.92.0", + CA: "15.156.223.0", + CD: "154.72.55.0", + CH: "130.59.31.0", + CL: "200.7.7.0", + CZ: "82.117.137.0", + DE: "81.91.170.0", + DK: "77.66.88.0", + DO: "190.113.72.0", + DZ: "193.194.90.0", + EC: "200.110.119.0", + ES: "212.128.109.0", + FI: "195.197.95.0", + FR: "192.134.5.0", + GE: "185.19.99.0", + GH: "197.253.95.0", + GR: "139.91.247.0", + HR: "161.53.160.0", + HU: "5.28.3.0", + ID: "45.126.58.0", + IL: "147.237.12.0", + IN: "103.249.97.0", + IR: "194.225.70.0", + IS: "193.4.58.0", + IT: "192.12.192.0", + JM: "196.2.1.0", + JO: "193.188.64.0", + JP: "117.104.133.0", + KR: "116.67.79.0", + KZ: "93.191.231.0", + LT: "185.150.40.0", + LU: "185.106.24.0", + LV: "5.179.1.0", + MD: "185.108.181.0", + ME: "185.132.160.0", + MG: "196.43.214.0", + ML: "197.155.158.0", + MN: "202.131.4.0", + MT: "193.188.46.0", + MW: "41.87.2.0", + MX: "200.94.180.0", + MY: "103.233.161.0", + NI: "200.9.187.0", + NL: "212.114.120.0", + NO: "51.120.98.0", + PE: "209.45.67.0", + PL: "62.181.3.0", + PT: "185.39.208.0", + PY: "200.10.228.0", + QA: "78.100.129.0", + RO: "85.120.75.0", + SE: "159.253.30.0", + RU: "31.177.80.0", + SA: "86.111.192.0", + SE: "159.253.30.0", + SI: "153.5.81.0", + SK: "212.57.32.0", + TH: "122.155.199.0", + TN: "193.95.68.0", + TR: "144.122.95.0", + UA: "193.29.220.0", + UK: "80.87.129.0", + US: "208.109.192.0", + UY: "164.73.128.0", + UZ: "91.212.89.0", + ZA: "163.195.1.0", }; export default function anonymousIpByCC(countryCode) { - return cc2ip[countryCode] ?? "127.0.0.1"; + return cc2ip[countryCode] ?? "10.10.10.10"; } diff --git a/libs/pong/click.d.ts b/libs/pong/click.d.ts deleted file mode 100644 index 532d9f1bc90c..000000000000 --- a/libs/pong/click.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Coder } from "./coding.js"; - -export function createPongClickHandler(coder: Coder): ( - params: URLSearchParams -) => Promise<{ - status: number; - location: string; -}>; diff --git a/libs/pong/click.js b/libs/pong/click.js deleted file mode 100644 index 3b3b4e065b3c..000000000000 --- a/libs/pong/click.js +++ /dev/null @@ -1,18 +0,0 @@ -/* global fetch */ -export function createPongClickHandler(coder) { - return async (params) => { - const click = coder.decodeAndVerify(params.get("code")); - const fallback = coder.decodeAndVerify(params.get("fallback")); - const res = await fetch(click, { redirect: "manual" }); - let status = res.status; - let headers = res.headers; - if (fallback) { - const fallbackRes = await fetch(`https:${fallback}`, { - redirect: "manual", - }); - status = fallbackRes.status; - headers = fallbackRes.headers; - } - return { status, location: headers.get("location") }; - }; -} diff --git a/libs/pong/fallback.d.ts b/libs/pong/fallback.d.ts deleted file mode 100644 index be648df1cc60..000000000000 --- a/libs/pong/fallback.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Coder } from "./coding.js"; - -export function fallbackHandler( - coder: Coder, - carbonZoneKey: string, - userAgent: string, - anonymousIp: string -): Promise<{ - click: string; - view: string; - image: string; - copy: string; - by: string; -}>; diff --git a/libs/pong/fallback.js b/libs/pong/fallback.js deleted file mode 100644 index 7ca3fa16cbb1..000000000000 --- a/libs/pong/fallback.js +++ /dev/null @@ -1,31 +0,0 @@ -/* global fetch */ -export async function fallbackHandler( - coder, - carbonZoneKey, - userAgent, - anonymousIp -) { - try { - const { - ads: [ - { description = null, statlink, statimp, smallImage, ad_via_link }, - ] = [], - } = await ( - await fetch( - `https://srv.buysellads.com/ads/${carbonZoneKey}.json?forwardedip=${encodeURIComponent( - anonymousIp - )}${userAgent ? `&useragent=${encodeURIComponent(userAgent)}` : ""}` - ) - ).json(); - return { - click: coder.encodeAndSign(statlink), - view: coder.encodeAndSign(statimp), - image: coder.encodeAndSign(smallImage), - copy: description, - by: ad_via_link, - }; - } catch (e) { - console.log(e); - return null; - } -} diff --git a/libs/pong/get.d.ts b/libs/pong/get.d.ts deleted file mode 100644 index 82da869105f8..000000000000 --- a/libs/pong/get.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Client } from "@adzerk/decision-sdk"; -import { Coder } from "./coding.js"; - -type Fallback = { - click: string; - view: string; - copy: string; - image: string; - by: string; -}; - -type Colors = { - textColor?: string; - backgroundColor?: string; - ctaTextColor?: string; - ctaBackgroundColor?: string; -}; - -type Payload = { - status: Status; - click: string; - view: string; - copy?: string; - image?: string; - fallback?: Fallback; - cta?: string; - colors?: Colors; -}; - -export function createPongGetHandler( - client: Client, - coder: Coder, - env: { KEVEL_SITE_ID: number; KEVEL_NETWORK_ID: number; SIGN_SECRET: string } -): ( - body: string, - countryCode: string, - userAgent: string -) => Promise<{ - statusCode: number; - payload: Payload; -}>; diff --git a/libs/pong/index.d.ts b/libs/pong/index.d.ts index 9cd2405e9b84..42345d30ed44 100644 --- a/libs/pong/index.d.ts +++ b/libs/pong/index.d.ts @@ -1,6 +1,4 @@ export * from "./coding.js"; -export * from "./get.js"; export * from "./image.js"; -export * from "./click.js"; -export * from "./viewed.js"; -export * from "./fallback.js"; +export * from "./pong.js"; +export * from "./pong2.js"; diff --git a/libs/pong/index.js b/libs/pong/index.js index 9cd2405e9b84..42345d30ed44 100644 --- a/libs/pong/index.js +++ b/libs/pong/index.js @@ -1,6 +1,4 @@ export * from "./coding.js"; -export * from "./get.js"; export * from "./image.js"; -export * from "./click.js"; -export * from "./viewed.js"; -export * from "./fallback.js"; +export * from "./pong.js"; +export * from "./pong2.js"; diff --git a/libs/pong/pong.d.ts b/libs/pong/pong.d.ts new file mode 100644 index 000000000000..cf6f471374b7 --- /dev/null +++ b/libs/pong/pong.d.ts @@ -0,0 +1,26 @@ +import { Coder } from "./coding.js"; +import { Payload } from "./types.js"; + +export function createPongGetHandler( + zoneKeys: object, + coder: Coder, + env: { KEVEL_SITE_ID: number; KEVEL_NETWORK_ID: number; SIGN_SECRET: string } +): ( + body: string, + countryCode: string, + userAgent: string +) => Promise<{ + statusCode: number; + payload: Payload; +}>; + +export function createPongClickHandler(coder: Coder): ( + params: URLSearchParams +) => Promise<{ + status: number; + location: string; +}>; + +export function createPongViewedHandler( + coder: Coder +): (params: URLSearchParams) => Promise; diff --git a/libs/pong/get.js b/libs/pong/pong.js similarity index 81% rename from libs/pong/get.js rename to libs/pong/pong.js index 2c2ee601b2c7..8d2413a82aab 100644 --- a/libs/pong/get.js +++ b/libs/pong/pong.js @@ -1,3 +1,4 @@ +/* global fetch */ import he from "he"; import anonymousIpByCC from "./cc2ip.js"; @@ -58,6 +59,7 @@ export function createPongGetHandler(client, coder) { colors, click: coder.encodeAndSign(clickUrl), view: coder.encodeAndSign(impressionUrl), + version: 1, }, ]; } else if (p === "top") { @@ -73,6 +75,7 @@ export function createPongGetHandler(client, coder) { colors, click: coder.encodeAndSign(clickUrl), view: coder.encodeAndSign(impressionUrl), + version: 1, }, ]; } @@ -82,3 +85,20 @@ export function createPongGetHandler(client, coder) { return { statusCode: 200, payload }; }; } + +export function createPongClickHandler(coder) { + return async (params) => { + const click = coder.decodeAndVerify(params.get("code")); + const res = await fetch(click, { redirect: "manual" }); + const status = res.status; + const location = res.headers.get("location"); + return { status, location }; + }; +} + +export function createPongViewedHandler(coder) { + return async (params) => { + const view = coder.decodeAndVerify(params.get("code")); + await fetch(view, { redirect: "manual" }); + }; +} diff --git a/libs/pong/pong2.d.ts b/libs/pong/pong2.d.ts new file mode 100644 index 000000000000..4b4dd1fafdab --- /dev/null +++ b/libs/pong/pong2.d.ts @@ -0,0 +1,26 @@ +import { Coder } from "./coding.js"; +import { Payload } from "./types.js"; + +export function createPong2GetHandler( + zoneKeys: object, + coder: Coder, + env: { KEVEL_SITE_ID: number; KEVEL_NETWORK_ID: number; SIGN_SECRET: string } +): ( + body: string, + countryCode: string, + userAgent: string +) => Promise<{ + statusCode: number; + payload: Payload; +}>; + +export function createPong2ClickHandler(coder: Coder): ( + params: URLSearchParams +) => Promise<{ + status: number; + location: string; +}>; + +export function createPong2ViewedHandler( + coder: Coder +): (params: URLSearchParams) => Promise; diff --git a/libs/pong/pong2.js b/libs/pong/pong2.js new file mode 100644 index 000000000000..0435d0801fbe --- /dev/null +++ b/libs/pong/pong2.js @@ -0,0 +1,133 @@ +/* global fetch */ +import he from "he"; +import anonymousIpByCC from "./cc2ip.js"; + +export function createPong2GetHandler(zoneKeys, coder) { + return async (body, countryCode, userAgent) => { + const { pongs = null } = body; + const anonymousIp = anonymousIpByCC(countryCode); + + const placements = pongs + .filter((p) => p in zoneKeys) + .map((p) => { + return { name: p, zoneKey: [zoneKeys[p]] }; + }); + + const requests = placements.map(async ({ name, zoneKey }) => { + const { + ads: [ + { + description = null, + statlink, + statimp, + smallImage, + backgroundColor, + backgroundHoverColor, + callToAction, + ctaBackgroundColor, + ctaBackgroundHoverColor, + ctaTextColor, + ctaTextColorHover, + textColor, + textColorHover, + }, + ] = [], + } = await ( + await fetch( + `https://srv.buysellads.com/ads/${zoneKey}.json?forwardedip=${encodeURIComponent( + anonymousIp + )}${userAgent ? `&useragent=${encodeURIComponent(userAgent)}` : ""}` + ) + ).json(); + return { + name, + p: + description === null + ? null + : { + click: coder.encodeAndSign(statlink), + view: coder.encodeAndSign(statimp), + image: coder.encodeAndSign(smallImage), + copy: description && he.decode(description), + cta: callToAction && he.decode(callToAction), + colors: { + textColor, + backgroundColor, + ctaTextColor, + ctaBackgroundColor, + textColorDark: textColorHover, + backgroundColorDark: backgroundHoverColor, + ctaTextColorDark: ctaTextColorHover, + ctaBackgroundColorDark: ctaBackgroundHoverColor, + }, + }, + }; + }); + const decisionRes = (await Promise.allSettled(requests)) + .filter((p) => { + if (p.status === "rejected") { + console.log(`rejected ad request: ${p.reason}`); + } + return p.status === "fulfilled"; + }) + .map((p) => p.value); + + const decisions = Object.fromEntries( + decisionRes.map(({ name, p }) => [name, p]) + ); + + if (pongs.every((p) => decisions[p] === null)) { + let status = "geo_unsupported"; + return { statusCode: 200, payload: { status } }; + } + const payload = Object.fromEntries( + Object.entries(decisions) + .map(([p, v]) => { + if (v === null) { + return null; + } + const { copy, image, click, view, cta, colors = {} } = v; + return [ + p, + { + status: "success", + copy, + image, + cta, + colors, + click, + view, + version: 2, + }, + ]; + }) + .filter(Boolean) + ); + return { statusCode: 200, payload }; + }; +} + +export function createPong2ClickHandler(coder) { + return async (params) => { + const click = coder.decodeAndVerify(params.get("code")); + + if (!click) { + return {}; + } + const res = await fetch(`https:${click}`, { redirect: "manual" }); + const status = res.status; + const location = res.headers.get("location"); + return { status, location }; + }; +} + +export function createPong2ViewedHandler(coder) { + return async (params) => { + const view = coder.decodeAndVerify(params.get("code")); + if (view) { + await fetch(`https:${view}`, { + redirect: "manual", + }); + } + }; +} diff --git a/libs/pong/types.d.ts b/libs/pong/types.d.ts new file mode 100644 index 000000000000..0117b80fa088 --- /dev/null +++ b/libs/pong/types.d.ts @@ -0,0 +1,21 @@ +type Colors = { + textColor?: string; + backgroundColor?: string; + ctaTextColor?: string; + ctaBackgroundColor?: string; + textColorDark?: string; + backgroundColorDark?: string; + ctaTextColorDark?: string; + ctaBackgroundColorDark?: string; +}; + +export type Payload = { + status: Status; + click: string; + view: string; + copy?: string; + image?: string; + cta?: string; + colors?: Colors; + version: number; +}; diff --git a/libs/pong/viewed.d.ts b/libs/pong/viewed.d.ts deleted file mode 100644 index 48c1d38fc77b..000000000000 --- a/libs/pong/viewed.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Coder } from "./coding.js"; - -export function createPongViewedHandler( - coder: Coder -): (params: URLSearchParams) => Promise; diff --git a/libs/pong/viewed.js b/libs/pong/viewed.js deleted file mode 100644 index 8cd229dd48e9..000000000000 --- a/libs/pong/viewed.js +++ /dev/null @@ -1,9 +0,0 @@ -/* global fetch */ -export function createPongViewedHandler(coder) { - return async (params) => { - const view = coder.decodeAndVerify(params.get("code")); - const fallback = coder.decodeAndVerify(params.get("fallback")); - fallback && (await fetch(`https:${fallback}`, { redirect: "manual" })); - await fetch(view, { redirect: "manual" }); - }; -}