Implement conditional donates

This commit is contained in:
Roman Tsisyk 2022-09-11 15:10:46 +03:00
parent ddd4dea2f3
commit 7d8797c56a
9 changed files with 7563 additions and 25 deletions

View file

@ -29,3 +29,6 @@ jobs:
- name: Format
run: npm run format:ci
- name: Test
run: npm test

19
jest.config.json Normal file
View file

@ -0,0 +1,19 @@
{
"preset": "ts-jest/presets/default-esm",
"transform": {
"^.+\\.(t|j)sx?$": [
"ts-jest",
{
"tsconfig": "./tsconfig.json",
"useESM": true
}
]
},
"testRegex": "/test/.*\\.test\\.ts$",
"testEnvironment": "miniflare",
"testEnvironmentOptions": {
"scriptPath": "./src/index.ts",
"modules": true
},
"collectCoverageFrom": ["src/**/*.{ts,tsx}"]
}

7348
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,7 @@
"main": "dist/index.js",
"scripts": {
"build": "esbuild src/index.ts --bundle --outfile=dist/index.js",
"test": "eslint src/**/*.ts && tsc --noEmit",
"test": "jest",
"lint": "eslint --fix --ext .tsx,.ts src/",
"lint:ci": "eslint --ext .tsx,.ts src/ --max-warnings 0",
"format": "prettier --write '{src,test}/**/*.{ts,tsx}' '*.json' '*.yml' '*.toml' '.github/**/*.yml'",
@ -17,12 +17,18 @@
"devDependencies": {
"@cloudflare/workers-types": "^3.3.1",
"@cloudflare/wrangler": "^1.19.8",
"@types/jest": "^29.0.0",
"@typescript-eslint/eslint-plugin": "^5.11.0",
"@typescript-eslint/parser": "^5.11.0",
"esbuild": "^0.15.7",
"eslint": "^8.9.0",
"jest": "^29.0.3",
"jest-environment-miniflare": "^2.8.2",
"miniflare": "^2.8.2",
"prettier": "^2.5.1",
"prettier-plugin-toml": "^0.3.1",
"typescript": "^4.5.5"
"ts-jest": "^29.0.0",
"typescript": "^4.5.5",
"wrangler": "^2.0.29"
}
}

View file

@ -1,8 +1,10 @@
export {};
import { parseDataVersion, parseAppVersion } from './utils';
// TODO: Implement automated version checks from this metaserver script.
// It should check by cron if actual files are really available on all servers.
const SERVER = {
export const SERVER = {
backblaze: {
// BackBlaze + CloudFlare (US-West) unmetered.
url: 'https://cdn-us1.organicmaps.app/',
@ -43,27 +45,15 @@ const SERVER = {
},
};
const DONATE_URL = 'https://donate.organicmaps.app';
const DONATE_URL_RU = 'https://donate.organicmaps.ru';
// Main entry point.
addEventListener('fetch', (event) => {
event.respondWith(handleRequest(event.request).catch((err) => new Response(err.stack, { status: 500 })));
});
// Starting from September release, our clients have 'X-OM-DataVersion' header with the value
// of their current maps data version, for example, "211022" (October 22, 2021).
// It is lowercased by Cloudflare.
// Returns 0 if data version is absent or invalid, or a valid integer version.
function extractDataVersion(request: Request): number {
const strDataVersion = request.headers.get('x-om-dataversion');
if (strDataVersion) {
const dataVersion = parseInt(strDataVersion);
if (!Number.isNaN(dataVersion) && dataVersion >= 210000 && dataVersion <= 500000) {
return dataVersion;
}
}
return 0;
}
async function handleRequest(request: Request) {
export async function handleRequest(request: Request) {
const { pathname } = new URL(request.url);
switch (pathname) {
@ -72,10 +62,13 @@ async function handleRequest(request: Request) {
case '/servers': {
// Private for map files.
let servers;
const dataVersion = extractDataVersion(request);
if (dataVersion == 0) {
// Starting from 2021-09, our clients have 'X-OM-DataVersion' header with the value
// of their current maps data version, for example, "211022" (October 22, 2021).
// It is lowercased by Cloudflare.
const dataVersion = parseDataVersion(request.headers.get('x-om-dataversion'));
if (dataVersion === null) {
servers = [SERVER.backblaze];
} else
} else {
switch (request.cf?.continent) {
// See https://developers.cloudflare.com/firewall/cf-firewall-language/fields for a list of all continents.
case 'NA': // North America
@ -101,8 +94,56 @@ async function handleRequest(request: Request) {
}
}
}
}
servers = servers.map((server) => server.url);
const appVersion = parseAppVersion(request.headers.get('x-om-appversion'));
if (!appVersion) {
// Old format for <220823
return new Response(JSON.stringify(servers), {
headers: { 'Content-Type': 'application/json' },
});
}
// New format for >=220823
const response: {
servers: string[];
settings?: {
DonateUrl?: string;
};
} = {
servers: servers,
};
// Donates
let donatesEnabled = false;
if (appVersion.flavor == 'fdroid' || appVersion.flavor == 'web') {
donatesEnabled = true;
} else if (appVersion.flavor == 'huawei') {
donatesEnabled = true;
} else if (
appVersion.flavor == 'google' &&
!(request.cf?.asOrganization || '').toLowerCase().includes('google') &&
(request.cf?.country == 'DE' ||
request.cf?.country == 'TR' ||
request.cf?.country == 'CY' ||
request.cf?.country == 'CH')
) {
donatesEnabled = true;
}
if (donatesEnabled) {
if (request.cf?.country == 'RU') {
response.settings = {
DonateUrl: DONATE_URL_RU,
};
} else {
response.settings = {
DonateUrl: DONATE_URL,
};
}
}
const response = servers.map((server) => server.url);
return new Response(JSON.stringify(response), {
headers: { 'Content-Type': 'application/json' },
});

52
src/utils.ts Normal file
View file

@ -0,0 +1,52 @@
export {};
export function parseDataVersion(strDataVersion: string | null): number | null {
if (!strDataVersion) {
return null;
}
const dataVersion = parseInt(strDataVersion);
if (Number.isNaN(dataVersion) || dataVersion < 210000 || dataVersion > 500000) {
return null;
}
return dataVersion;
}
const VERSION_RE = new RegExp('(\\d{4}).(\\d{1,2}).(\\d{1,2})-(\\d{1,9})(?:-([^-]+))?');
export function parseAppVersion(
versionName: string | null,
): { code: number; build: number | undefined; flavor: string | undefined } | null {
if (!versionName) {
return null;
}
const m = versionName.match(VERSION_RE);
if (m === null || m.length < 6) {
return null;
}
const yyyy = parseInt(m[1]);
const mm = parseInt(m[2]);
const dd = parseInt(m[3]);
const build = Number.isNaN(parseInt(m[4])) ? undefined : parseInt(m[4]);
const flavor = (m[5] !== undefined && m[5].toLowerCase()) || undefined;
if (
Number.isNaN(yyyy) ||
yyyy > 2099 ||
yyyy < 2022 ||
Number.isNaN(mm) ||
mm > 12 ||
mm < 1 ||
Number.isNaN(dd) ||
dd > 31 ||
dd < 1
) {
return null;
}
const code = parseInt(String(yyyy % 100) + String(mm).padStart(2, '0') + String(dd).padStart(2, '0'));
return {
code: code,
flavor: flavor,
build: build,
};
}

35
test/index.test.ts Normal file
View file

@ -0,0 +1,35 @@
import { describe, expect, test } from '@jest/globals';
import { handleRequest, SERVER } from '../src/index';
const URL = 'https://worker/servers';
const LAST_DATA_VERSION = SERVER.planet.dataVersions[SERVER.planet.dataVersions.length - 1];
describe('old versions', () => {
test('no dataVersion', async () => {
const req = new Request(URL);
const result = await handleRequest(req);
expect(result.status).toBe(200);
expect(JSON.parse(await result.text())).toEqual([SERVER.backblaze.url]);
});
test('has dataVersion', async () => {
const server = SERVER.fi1;
let req = new Request(URL, {
headers: {
'X-OM-DataVersion': String(server.dataVersions[0]),
},
});
const result = await handleRequest(req);
expect(result.status).toBe(200);
expect(JSON.parse(await result.text())).toContain(server.url);
});
test('default routing to planet', async () => {
let req = new Request(URL, {
headers: {
'X-OM-DataVersion': '210000', // this version doesn't exist on servers
},
});
const result = await handleRequest(req);
expect(result.status).toBe(200);
expect(JSON.parse(await result.text())).toEqual([SERVER.planet.url]);
});
});

36
test/utils.test.ts Normal file
View file

@ -0,0 +1,36 @@
import { describe, expect, test } from '@jest/globals';
import { parseDataVersion, parseAppVersion } from '../src/utils';
describe('parseDataVersion', () => {
test('220801', () => expect(parseDataVersion('220801')).toEqual(220801));
test('210000', () => expect(parseDataVersion('210000')).toEqual(210000));
test('500000', () => expect(parseDataVersion('500000')).toEqual(500000));
test('200000', () => expect(parseDataVersion('200000')).toEqual(null));
test('500001', () => expect(parseDataVersion('500001')).toEqual(null));
test('garbage', () => expect(parseDataVersion('garbage')).toEqual(null));
test('', () => expect(parseDataVersion('')).toEqual(null));
test('', () => expect(parseDataVersion(null)).toEqual(null));
});
describe('parseAppVersion', () => {
test('2022.08.01-1', () => expect(parseAppVersion('2022.08.01-1')).toEqual({ code: 220801, build: 1 }));
test('2022.08.01-1-Google', () =>
expect(parseAppVersion('2022.08.01-1-Google')).toEqual({ code: 220801, build: 1, flavor: 'google' }));
// -debug is ignored
test('2022.08.01-1-Google-debug', () =>
expect(parseAppVersion('2022.08.01-1-Google-debug')).toEqual({ code: 220801, build: 1, flavor: 'google' }));
test('2022.1.1-0', () => expect(parseAppVersion('2022.1.2-0')).toEqual({ code: 220102, build: 0 }));
test('2099.12.31-999999999', () =>
expect(parseAppVersion('2099.12.31-999999999')).toEqual({ code: 991231, build: 999999999 }));
test('2021.01.31-1', () => expect(parseAppVersion('2021.01.31-1')).toEqual(null));
test('2100.01.31-1', () => expect(parseAppVersion('2100.01.31-1')).toEqual(null));
test('2022.00.31-1', () => expect(parseAppVersion('2022.00.31-1')).toEqual(null));
test('2022.13.31-1', () => expect(parseAppVersion('2022.13.31-1')).toEqual(null));
test('2022.01.00-1', () => expect(parseAppVersion('2022.01.00-1')).toEqual(null));
test('2022.01.32-1', () => expect(parseAppVersion('2022.01.32-1')).toEqual(null));
test('202.01.31-1', () => expect(parseAppVersion('2100.01.31-1')).toEqual(null));
test('2022..31-11', () => expect(parseAppVersion('2100.01.31-1')).toEqual(null));
test('garbage', () => expect(parseAppVersion('garbage')).toEqual(null));
test('', () => expect(parseAppVersion('')).toEqual(null));
test('null', () => expect(parseAppVersion('null')).toEqual(null));
});

View file

@ -9,7 +9,7 @@ workers_dev = true
[build]
upload.format = 'service-worker'
command = 'npm i --prefer-offline --no-audit && npm run build'
command = 'npm ci --prefer-offline --no-audit && npm run build'
[vars]
DEBUG = true