Compare commits
145 commits
versions-r
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
882b7a7cec | ||
18c36b18d2 | |||
|
7c48e43fc4 | ||
|
7c92fc3ce6 | ||
71c8217bb8 | |||
|
fc751e37aa | ||
|
9a794b9652 | ||
a2be921690 | |||
878dfa9c06 | |||
eab3488455 | |||
e65565a1e8 | |||
22617fa964 | |||
|
e41c085fd7 | ||
|
cc43bceb03 | ||
3ec9d9e5b9 | |||
|
4b8c381c33 | ||
|
a269f44aa4 | ||
|
bb534682fd | ||
|
7fb90b0444 | ||
14b002618e | |||
|
05100af769 | ||
|
9eb4405f03 | ||
|
5d26908ec2 | ||
|
d7ac8bc5fa | ||
|
fde585a122 | ||
|
13ad21530d | ||
|
50c140ebcb | ||
|
cbe27ccda4 | ||
|
087cbaf316 | ||
|
30cbe4463c | ||
0cb3ff3dc1 | |||
b8200aadc8 | |||
375533572c | |||
ecc91e7737 | |||
|
225cc73df7 | ||
|
2783718dd0 | ||
37b706fa51 | |||
9c8fac62f2 | |||
983913c1af | |||
|
0fdfb8a2ea | ||
|
fe6e1d889c | ||
|
3a7b3fe8cd | ||
|
a177326249 | ||
671d4934db | |||
|
9a9af6fdcc | ||
|
84689e9ec6 | ||
|
db02d73f9e | ||
|
9587929d85 | ||
|
424bfab1f1 | ||
|
9fc90d1dd5 | ||
f7840e6a5a | |||
|
3e1d1e7aab | ||
|
265f65c8e3 | ||
|
6bafd059da | ||
|
da32ce8662 | ||
|
9afb5094d2 | ||
d4e1a65e23 | |||
8b3b48d689 | |||
02e58cb29b | |||
441e08f964 | |||
73e5aed304 | |||
36e12a6497 | |||
c2ae1236eb | |||
|
6bfc5c45c4 | ||
2a92abd741 | |||
ea3cef7ca5 | |||
7410279e36 | |||
a7e13bea9a | |||
988c8d4c7d | |||
6c95780f09 | |||
accf6a5607 | |||
944fe69dcb | |||
342050ba56 | |||
fc36848ecb | |||
|
2341c15787 | ||
|
9bb0348fca | ||
28e9b826d1 | |||
|
a2225f5d04 | ||
2f43b868ab | |||
|
83919b9a79 | ||
|
d0524e78d1 | ||
c57a691110 | |||
57daba6e09 | |||
|
8fa182c15b | ||
245f3f61ad | |||
b08cf5bff0 | |||
|
fccf37e9e7 | ||
f02e1ec6a4 | |||
923b8f7274 | |||
4468cdd776 | |||
|
dc91b633ba | ||
|
9b2e72600d | ||
38e9ce98bb | |||
428d8a7cf1 | |||
8261dad9e7 | |||
|
749a23eaaf | ||
|
a51a9a1a66 | ||
8811f9eb9f | |||
|
b72a43642c | ||
3cb3a0b021 | |||
|
646d1d742a | ||
|
9f8037a8b8 | ||
|
daefc2b9f8 | ||
|
b9923075d7 | ||
8e58be8700 | |||
|
f9cda06dce | ||
|
7956637a53 | ||
8eff170a1c | |||
4e0f234f0d | |||
|
1f2b52bc34 | ||
6ce9fc1bdf | |||
|
64a0fc7363 | ||
|
535a7e1f56 | ||
|
e1e4cbbd0d | ||
|
d41bc4228d | ||
|
c5f24e9912 | ||
0b2824e067 | |||
ce61738a7e | |||
|
351fcfbfca | ||
|
524651fe87 | ||
156e7d1cc3 | |||
|
51ba0a3752 | ||
|
c523ab02a6 | ||
b41355f6c4 | |||
90693bac24 | |||
|
6552994012 | ||
|
1912710579 | ||
3b60daee40 | |||
f919baf1e5 | |||
ffe727de33 | |||
fdf37d67dd | |||
adaed55bc3 | |||
|
9bcd62f236 | ||
|
f21651f4cf | ||
|
e969d97dee | ||
|
e7ba263f54 | ||
|
489df04590 | ||
2dacf66a2c | |||
6bd219559a | |||
|
cda02e3a23 | ||
|
8a4330d582 | ||
|
7c3947d978 | ||
|
feb0f468ef | ||
|
5a7540fc88 | ||
|
8779625701 |
25 changed files with 3055 additions and 8112 deletions
|
@ -1,18 +0,0 @@
|
|||
env:
|
||||
es2020: true
|
||||
worker: true
|
||||
extends: ['plugin:@typescript-eslint/recommended']
|
||||
parser: '@typescript-eslint/parser'
|
||||
parserOptions:
|
||||
ecmaVersion: 2020
|
||||
ecmaFeatures:
|
||||
impliedStrict: true
|
||||
sourceType: module
|
||||
root: true
|
||||
rules:
|
||||
indent: [error, 2, { SwitchCase: 1 }]
|
||||
semi: [error, always]
|
||||
quotes: [error, single, avoid-escape]
|
||||
no-trailing-spaces: [error]
|
||||
no-unused-vars: [error, { argsIgnorePattern: ^_ }]
|
||||
prefer-const: [error, { destructuring: all }]
|
5
.github/workflows/check.yml
vendored
5
.github/workflows/check.yml
vendored
|
@ -24,8 +24,9 @@ jobs:
|
|||
- name: Dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint:ci
|
||||
# TODO(AB): ESLint dependencies are broken. Replace with a better and less complex linter.
|
||||
# - name: Lint
|
||||
# run: npm run lint:ci
|
||||
|
||||
- name: Format
|
||||
run: npm run format:ci
|
||||
|
|
5
.github/workflows/deploy-master-to-prod.yml
vendored
5
.github/workflows/deploy-master-to-prod.yml
vendored
|
@ -7,10 +7,11 @@ on:
|
|||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: wrangler publish
|
||||
uses: cloudflare/wrangler-action@1.3.0
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
environment: prod
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,4 +1,7 @@
|
|||
dist/
|
||||
.wrangler/
|
||||
# Autogenerated by esbuild.
|
||||
workers-site/index.js
|
||||
node_modules/
|
||||
# Created by Finder on Mac
|
||||
.DS_Store
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"plugins": ["prettier-plugin-toml"],
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
|
|
4
CONTRIBUTORS
Normal file
4
CONTRIBUTORS
Normal file
|
@ -0,0 +1,4 @@
|
|||
Alexander Borsuk <me@alex.bio>
|
||||
Roman Tsisyk <roman@organicmaps.app>
|
||||
Viktor Havaka <viktor.govako@gmail.com>
|
||||
|
1
LICENSE
1
LICENSE
|
@ -1,4 +1,5 @@
|
|||
Copyright 2021 Alexander Borsuk <me@alex.bio>
|
||||
Copyright 2024 Organic Maps Contributors (see the CONTRIBUTORS file).
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
|
28
README.md
28
README.md
|
@ -12,22 +12,42 @@ npm i
|
|||
|
||||
## Development
|
||||
|
||||
Use `npx wrangler dev` for localhost development.
|
||||
Use `npx wrangler dev` for localhost development and for testing using Cloudflare dev tools.
|
||||
|
||||
```
|
||||
curl -H "X-OM-DataVersion: 241001" -H "X-OM-AppVersion: 2024.10.22-10-Google" -H 'Accept-Language: fr-FR' http://localhost:8787/maps
|
||||
```
|
||||
|
||||
## Update node dependencies to their major versions
|
||||
|
||||
```bash
|
||||
npm run upgrade
|
||||
```
|
||||
|
||||
## Preview on workers.dev
|
||||
|
||||
Use `npx wrangler preview` to open and test deployed worker in browser.
|
||||
Use `npx wrangler publish` to open and test deployed worker in browser at https://meta-dev.omaps.workers.dev
|
||||
|
||||
## Monitor Worker logs
|
||||
|
||||
For dev: `npx wrangler tail [--format json]`
|
||||
|
||||
For production: `npx wrangler tail --env prod [--format json]`
|
||||
or
|
||||
`npm run logs`
|
||||
|
||||
## Deployment
|
||||
|
||||
All pushes to master automatically deploy dev version to https://meta.omaps.workers.dev/
|
||||
All pushes to master automatically deploy prod version to https://meta.omaps.workers.dev/ and https://meta.omaps.app/
|
||||
|
||||
Deploy to prod manually using `npx wrangler publish --env prod` or this
|
||||
[action](https://github.com/organicmaps/meta/actions/workflows/deploy-master-to-prod.yml).
|
||||
|
||||
Deploy to test dev version live at https://meta-dev.omaps.workers.dev/ manually using `npx wrangler publish`.
|
||||
|
||||
## Known issues
|
||||
|
||||
- Cloudflare's free Flexible SSL certificates does not support 4-th level
|
||||
- Cloudflare's free Flexible SSL certificates do not support 4-th level
|
||||
subdomains like a.b.example.com, so you can see strange SSL errors.
|
||||
- HTTPS `fetch` requests from Workers are converted to HTTP ones if the target
|
||||
host is in the same Cloudflare zone, see [here](https://community.cloudflare.com/t/does-cloudflare-worker-allow-secure-https-connection-to-fetch-even-on-flexible-ssl/68051/12)
|
||||
|
|
10003
package-lock.json
generated
10003
package-lock.json
generated
File diff suppressed because it is too large
Load diff
36
package.json
36
package.json
|
@ -7,29 +7,25 @@
|
|||
"scripts": {
|
||||
"build": "esbuild src/index.ts --bundle --outfile=dist/index.js",
|
||||
"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'",
|
||||
"format:ci": "prettier --check '{src,test}/**/*.{ts,tsx}' '*.json' '*.yml' '*.toml' '.github/**/*.yml'",
|
||||
"upgrade": "npx npm-check-updates -u"
|
||||
"format": "prettier --write '{src,test}/**/*.{ts,tsx,json}' '*.json' '*.toml' '.github/**/*.yml'",
|
||||
"format:ci": "prettier --check '{src,test}/**/*.{ts,tsx,json}' '*.json' '*.toml' '.github/**/*.yml'",
|
||||
"upgrade": "npx npm-check-updates -u && npm install",
|
||||
"logs": "npx wrangler tail --env prod --format json"
|
||||
},
|
||||
"author": "Alexander Borsuk <me@alex.bio>",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^3.16.0",
|
||||
"@cloudflare/wrangler": "^1.19.13",
|
||||
"@types/jest": "^29.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.38.0",
|
||||
"@typescript-eslint/parser": "^5.38.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"eslint": "^8.24.0",
|
||||
"jest": "^29.0.3",
|
||||
"jest-environment-miniflare": "^2.9.0",
|
||||
"miniflare": "^2.9.0",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-toml": "^0.3.1",
|
||||
"ts-jest": "^29.0.2",
|
||||
"typescript": "^4.8.3",
|
||||
"wrangler": "^2.1.6"
|
||||
"@cloudflare/workers-types": "^4.20241205.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"esbuild": "^0.24.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-miniflare": "^2.14.4",
|
||||
"miniflare": "^3.20241205.0",
|
||||
"npm-check-updates": "^17.1.11",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-toml": "^2.0.1",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.7.2",
|
||||
"wrangler": "^3.95.0"
|
||||
}
|
||||
}
|
||||
|
|
137
src/index.ts
137
src/index.ts
|
@ -1,54 +1,6 @@
|
|||
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.
|
||||
export const SERVER = {
|
||||
backblaze: {
|
||||
// BackBlaze + CloudFlare (US-West) unmetered.
|
||||
url: 'https://cdn-us1.organicmaps.app/',
|
||||
dataVersions: [
|
||||
210529, 210703, 210729, 210825, 211002, 211022, 211122, 220103, 220204, 220314, 220415, 220515, 220613, 220718,
|
||||
220816, 220912, 221029,
|
||||
],
|
||||
},
|
||||
uk1: {
|
||||
// Mythic Beasts VPS (London, UK) 200TB/mo.
|
||||
url: 'https://cdn-uk1.organicmaps.app/',
|
||||
dataVersions: [220816, 220912, 221029],
|
||||
},
|
||||
nl1: {
|
||||
// // Mythic Beasts VPS (Amsterdam, NL) 200TB/mo.
|
||||
url: 'https://cdn-nl1.organicmaps.app/',
|
||||
dataVersions: [220816, 220912, 221029],
|
||||
},
|
||||
planet: {
|
||||
// Hetzner BareMetal (Falkenstein, Germany) unmetered
|
||||
url: 'https://cdn.organicmaps.app/',
|
||||
dataVersions: [
|
||||
220103, 220204, 220314, 220415, 220515, 220613, 220718, 220816, 220912, 221019 /* beta only */, 221029,
|
||||
],
|
||||
},
|
||||
fi1: {
|
||||
// Hetzner Cloud (Helsinki, Finland), 20TB/mo
|
||||
url: 'https://cdn-fi1.organicmaps.app/',
|
||||
dataVersions: [220912, 221029],
|
||||
},
|
||||
de1: {
|
||||
// Hetzner Cloud (Falkenstein, Germany), 20TB/mo
|
||||
url: 'https://cdn-eu2.organicmaps.app/',
|
||||
dataVersions: [220912, 221029],
|
||||
},
|
||||
us2: {
|
||||
// Hetzner Cloud (Asburn, US East), 20TB/mo
|
||||
url: 'https://cdn-us2.organicmaps.app/',
|
||||
dataVersions: [220912, 221029],
|
||||
},
|
||||
};
|
||||
|
||||
const DONATE_URL = 'https://organicmaps.app/donate/';
|
||||
const DONATE_URL_RU = 'https://organicmaps.app/ru/donate/';
|
||||
import { getServersList } from './servers';
|
||||
|
||||
// Main entry point.
|
||||
addEventListener('fetch', (event) => {
|
||||
|
@ -61,91 +13,8 @@ export async function handleRequest(request: Request) {
|
|||
switch (pathname) {
|
||||
case '/maps': // Public for map files.
|
||||
case '/resources': // Public for resources.
|
||||
case '/servers': {
|
||||
// Private for map files.
|
||||
let servers;
|
||||
// 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) {
|
||||
// Older clients download from the archive.
|
||||
servers = [SERVER.backblaze];
|
||||
} 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
|
||||
case 'SA': // South America
|
||||
case 'OC': // Oceania
|
||||
servers = [SERVER.backblaze, SERVER.us2, SERVER.uk1, SERVER.nl1, SERVER.planet].filter((server) =>
|
||||
server.dataVersions.includes(dataVersion),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// Every other continent + Tor networks.
|
||||
servers = [SERVER.planet, SERVER.uk1, SERVER.nl1, SERVER.fi1, SERVER.de1].filter((server) =>
|
||||
server.dataVersions.includes(dataVersion),
|
||||
);
|
||||
// Only fallback to the archive in the US if nothing was found closer.
|
||||
if (servers.length == 0 && SERVER.backblaze.dataVersions.includes(dataVersion)) {
|
||||
servers = [SERVER.backblaze];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback to the planet with freshly generated/beta data.
|
||||
if (servers.length == 0) {
|
||||
servers = [SERVER.planet];
|
||||
}
|
||||
servers = servers.map((server) => server.url);
|
||||
|
||||
// Header "X-OM-AppVersion: 2022.09.22-3-Google" (lowercased by CF) is supported from August 23, 2022.
|
||||
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,
|
||||
};
|
||||
|
||||
// Disable donates for Google reviewers for all google app versions AFTER this one.
|
||||
const lastApprovedAndReleasedGoogleAppVersionCode = 221102;
|
||||
let donatesEnabled = true;
|
||||
if (
|
||||
appVersion.flavor === 'google' &&
|
||||
((request.cf?.asOrganization || '').toLowerCase().includes('google') ||
|
||||
appVersion.code > lastApprovedAndReleasedGoogleAppVersionCode)
|
||||
) {
|
||||
donatesEnabled = false;
|
||||
}
|
||||
|
||||
if (donatesEnabled) {
|
||||
// To count enabled donations.
|
||||
console.log('Donates enabled');
|
||||
if (request.cf?.country == 'RU') {
|
||||
response.settings = {
|
||||
DonateUrl: DONATE_URL_RU,
|
||||
};
|
||||
} else {
|
||||
response.settings = {
|
||||
DonateUrl: DONATE_URL,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(response), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
case '/servers':
|
||||
return getServersList(request);
|
||||
}
|
||||
return new Response('', { status: 404 });
|
||||
}
|
||||
|
|
44
src/locales.json
Normal file
44
src/locales.json
Normal file
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"en": {
|
||||
"placePagePrompt": "Organic Maps app is free for everyone, thanks to your donations. No ads. No trackers. Open source.",
|
||||
"perMonth": "/month",
|
||||
"perYear": "/year",
|
||||
"otherAmount": "Other"
|
||||
},
|
||||
"de": {
|
||||
"placePagePrompt": "Organic Maps ist dank deiner Spenden für alle kostenlos. Keine Werbung. Keine Tracker. Open Source.",
|
||||
"perMonth": "/Monat",
|
||||
"perYear": "/Jahr",
|
||||
"otherAmount": "Andere"
|
||||
},
|
||||
"fr": {
|
||||
"placePagePrompt": "L'application Organic Maps est gratuite pour tout le monde grâce à vos dons. Pas de publicité. Pas de trackers. Open-source.",
|
||||
"perMonth": "/mois",
|
||||
"perYear": "/an",
|
||||
"otherAmount": "Autre"
|
||||
},
|
||||
"nl": {
|
||||
"placePagePrompt": "De Organic Maps app is gratis voor iedereen dankzij jullie donaties. Geen advertenties. Geen trackers. Open-source.",
|
||||
"perMonth": "/maand",
|
||||
"perYear": "/jaar",
|
||||
"otherAmount": "Ander"
|
||||
},
|
||||
"it": {
|
||||
"placePagePrompt": "L'app Organic Maps è gratuita per tutti grazie alle vostre donazioni. Nessuna pubblicità. Nessun tracker. Open-source.",
|
||||
"perMonth": "/mese",
|
||||
"perYear": "/anno",
|
||||
"otherAmount": "Altro"
|
||||
},
|
||||
"es": {
|
||||
"placePagePrompt": "Organic Maps es gratis para todos gracias a sus donaciones. Sin anuncios. Sin rastreadores. Código abierto.",
|
||||
"perMonth": "/mes",
|
||||
"perYear": "/año",
|
||||
"otherAmount": "Otro"
|
||||
},
|
||||
"pt": {
|
||||
"placePagePrompt": "O app Organic Maps é gratuito para todos graças às suas doações. Sem anúncios. Sem rastreadores. Código aberto.",
|
||||
"perMonth": "/mês",
|
||||
"perYear": "/ano",
|
||||
"otherAmount": "Outro"
|
||||
}
|
||||
}
|
15
src/locales.ts
Normal file
15
src/locales.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import LOCALES_JSON from './locales.json';
|
||||
|
||||
export interface Locale {
|
||||
placePagePrompt: string;
|
||||
perMonth: string;
|
||||
perYear: string;
|
||||
otherAmount: string;
|
||||
}
|
||||
|
||||
export interface Locales {
|
||||
[key: string]: Locale;
|
||||
}
|
||||
|
||||
const LOCALES = LOCALES_JSON as Locales;
|
||||
export default LOCALES;
|
198
src/products.json
Normal file
198
src/products.json
Normal file
|
@ -0,0 +1,198 @@
|
|||
{
|
||||
"FR": [
|
||||
{
|
||||
"title": "4,99€$per_month",
|
||||
"link": "https://donate.stripe.com/6oEg2912f2c81r200j"
|
||||
},
|
||||
{
|
||||
"title": "34,99€$per_year",
|
||||
"link": "https://donate.stripe.com/eVa2bjdP19EA0mY6oI"
|
||||
},
|
||||
{
|
||||
"title": "$other",
|
||||
"link": "https://donate.stripe.com/3csg29aCP0401r2dRj"
|
||||
}
|
||||
],
|
||||
"DE": [
|
||||
{
|
||||
"title": "4,99€$per_month",
|
||||
"link": "https://donate.stripe.com/6oEg2912f2c81r200j"
|
||||
},
|
||||
{
|
||||
"title": "34,99€$per_year",
|
||||
"link": "https://donate.stripe.com/eVa2bjdP19EA0mY6oI"
|
||||
},
|
||||
{
|
||||
"title": "$other",
|
||||
"link": "https://donate.stripe.com/3csg29aCP0401r2dRj"
|
||||
}
|
||||
],
|
||||
"NL": [
|
||||
{
|
||||
"title": "4,99€$per_month",
|
||||
"link": "https://donate.stripe.com/6oEg2912f2c81r200j"
|
||||
},
|
||||
{
|
||||
"title": "34,99€$per_year",
|
||||
"link": "https://donate.stripe.com/eVa2bjdP19EA0mY6oI"
|
||||
},
|
||||
{
|
||||
"title": "$other",
|
||||
"link": "https://donate.stripe.com/3csg29aCP0401r2dRj"
|
||||
}
|
||||
],
|
||||
"IT": [
|
||||
{
|
||||
"title": "4,99€$per_month",
|
||||
"link": "https://donate.stripe.com/6oEg2912f2c81r200j"
|
||||
},
|
||||
{
|
||||
"title": "34,99€$per_year",
|
||||
"link": "https://donate.stripe.com/eVa2bjdP19EA0mY6oI"
|
||||
},
|
||||
{
|
||||
"title": "$other",
|
||||
"link": "https://donate.stripe.com/3csg29aCP0401r2dRj"
|
||||
}
|
||||
],
|
||||
"ES": [
|
||||
{
|
||||
"title": "4,99€$per_month",
|
||||
"link": "https://donate.stripe.com/6oEg2912f2c81r200j"
|
||||
},
|
||||
{
|
||||
"title": "34,99€$per_year",
|
||||
"link": "https://donate.stripe.com/eVa2bjdP19EA0mY6oI"
|
||||
},
|
||||
{
|
||||
"title": "$other",
|
||||
"link": "https://donate.stripe.com/3csg29aCP0401r2dRj"
|
||||
}
|
||||
],
|
||||
"PT": [
|
||||
{
|
||||
"title": "4,99€$per_month",
|
||||
"link": "https://donate.stripe.com/6oEg2912f2c81r200j"
|
||||
},
|
||||
{
|
||||
"title": "34,99€$per_year",
|
||||
"link": "https://donate.stripe.com/eVa2bjdP19EA0mY6oI"
|
||||
},
|
||||
{
|
||||
"title": "$other",
|
||||
"link": "https://donate.stripe.com/3csg29aCP0401r2dRj"
|
||||
}
|
||||
],
|
||||
"BE": [
|
||||
{
|
||||
"title": "4,99€$per_month",
|
||||
"link": "https://donate.stripe.com/6oEg2912f2c81r200j"
|
||||
},
|
||||
{
|
||||
"title": "34,99€$per_year",
|
||||
"link": "https://donate.stripe.com/eVa2bjdP19EA0mY6oI"
|
||||
},
|
||||
{
|
||||
"title": "$other",
|
||||
"link": "https://donate.stripe.com/3csg29aCP0401r2dRj"
|
||||
}
|
||||
],
|
||||
"AT": [
|
||||
{
|
||||
"title": "4,99€$per_month",
|
||||
"link": "https://donate.stripe.com/6oEg2912f2c81r200j"
|
||||
},
|
||||
{
|
||||
"title": "34,99€$per_year",
|
||||
"link": "https://donate.stripe.com/eVa2bjdP19EA0mY6oI"
|
||||
},
|
||||
{
|
||||
"title": "$other",
|
||||
"link": "https://donate.stripe.com/3csg29aCP0401r2dRj"
|
||||
}
|
||||
],
|
||||
"LU": [
|
||||
{
|
||||
"title": "4,99€$per_month",
|
||||
"link": "https://donate.stripe.com/6oEg2912f2c81r200j"
|
||||
},
|
||||
{
|
||||
"title": "34,99€$per_year",
|
||||
"link": "https://donate.stripe.com/eVa2bjdP19EA0mY6oI"
|
||||
},
|
||||
{
|
||||
"title": "$other",
|
||||
"link": "https://donate.stripe.com/3csg29aCP0401r2dRj"
|
||||
}
|
||||
],
|
||||
"MC": [
|
||||
{
|
||||
"title": "4,99€$per_month",
|
||||
"link": "https://donate.stripe.com/6oEg2912f2c81r200j"
|
||||
},
|
||||
{
|
||||
"title": "34,99€$per_year",
|
||||
"link": "https://donate.stripe.com/eVa2bjdP19EA0mY6oI"
|
||||
},
|
||||
{
|
||||
"title": "$other",
|
||||
"link": "https://donate.stripe.com/3csg29aCP0401r2dRj"
|
||||
}
|
||||
],
|
||||
"AD": [
|
||||
{
|
||||
"title": "4,99€$per_month",
|
||||
"link": "https://donate.stripe.com/6oEg2912f2c81r200j"
|
||||
},
|
||||
{
|
||||
"title": "34,99€$per_year",
|
||||
"link": "https://donate.stripe.com/eVa2bjdP19EA0mY6oI"
|
||||
},
|
||||
{
|
||||
"title": "$other",
|
||||
"link": "https://donate.stripe.com/3csg29aCP0401r2dRj"
|
||||
}
|
||||
],
|
||||
"SM": [
|
||||
{
|
||||
"title": "4,99€$per_month",
|
||||
"link": "https://donate.stripe.com/6oEg2912f2c81r200j"
|
||||
},
|
||||
{
|
||||
"title": "34,99€$per_year",
|
||||
"link": "https://donate.stripe.com/eVa2bjdP19EA0mY6oI"
|
||||
},
|
||||
{
|
||||
"title": "$other",
|
||||
"link": "https://donate.stripe.com/3csg29aCP0401r2dRj"
|
||||
}
|
||||
],
|
||||
"GB": [
|
||||
{
|
||||
"title": "£4.99$per_month",
|
||||
"link": "https://donate.stripe.com/8wMg29fX98Awd9K28u"
|
||||
},
|
||||
{
|
||||
"title": "£34.99$per_year",
|
||||
"link": "https://donate.stripe.com/eVabLT9yL2c89Xy7sP"
|
||||
},
|
||||
{
|
||||
"title": "$other",
|
||||
"link": "https://donate.stripe.com/9AQg29h1dcQMedO3cH"
|
||||
}
|
||||
],
|
||||
"US": [
|
||||
{
|
||||
"title": "$5.49$per_month",
|
||||
"link": "https://donate.stripe.com/00g3fncKXcQMedO5kL"
|
||||
},
|
||||
{
|
||||
"title": "$36.99$per_year",
|
||||
"link": "https://donate.stripe.com/bIY6rz5ivcQM9XyaF6"
|
||||
},
|
||||
{
|
||||
"title": "$other",
|
||||
"link": "https://donate.stripe.com/eVa8zH5ivbMIfhS6oS"
|
||||
}
|
||||
]
|
||||
}
|
40
src/products.ts
Normal file
40
src/products.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import LOCALES from './locales';
|
||||
import PRODUCTS_JSON from './products.json';
|
||||
|
||||
export interface Product {
|
||||
title: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
const PRODUCTS = PRODUCTS_JSON as Record<string, Product[]>;
|
||||
|
||||
export interface ProductsConfig {
|
||||
placePagePrompt: string;
|
||||
products: Product[];
|
||||
}
|
||||
|
||||
export function getProducts(locale: string | null): ProductsConfig | undefined {
|
||||
if (!locale) {
|
||||
return undefined;
|
||||
}
|
||||
const parts = locale.split(/[-_]/);
|
||||
const language = parts[0].toLowerCase();
|
||||
const country = parts[1] ? parts[1].toUpperCase() : '';
|
||||
|
||||
const products = PRODUCTS[country];
|
||||
const trans = LOCALES[language];
|
||||
if (products === undefined || trans === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
placePagePrompt: trans.placePagePrompt,
|
||||
products: products.map((product) => ({
|
||||
...product,
|
||||
title: product.title
|
||||
.replace('$other', trans.otherAmount)
|
||||
.replace('$per_month', trans.perMonth)
|
||||
.replace('$per_year', trans.perYear),
|
||||
})),
|
||||
};
|
||||
}
|
239
src/servers.ts
Normal file
239
src/servers.ts
Normal file
|
@ -0,0 +1,239 @@
|
|||
import { parseDataVersion, parseAppVersion } from './versions';
|
||||
import { getProducts, ProductsConfig } from './products';
|
||||
|
||||
export const DATA_VERSIONS = [
|
||||
210529, //
|
||||
210703,
|
||||
210729,
|
||||
210825,
|
||||
211002,
|
||||
211022,
|
||||
211122,
|
||||
220103,
|
||||
220204,
|
||||
220314,
|
||||
220415,
|
||||
220515,
|
||||
220613,
|
||||
220718,
|
||||
220816,
|
||||
220912,
|
||||
221029,
|
||||
221119,
|
||||
221216,
|
||||
230121,
|
||||
230210,
|
||||
230227,
|
||||
230329,
|
||||
230503,
|
||||
230602,
|
||||
230710,
|
||||
230814,
|
||||
230920,
|
||||
231113,
|
||||
231213,
|
||||
240105,
|
||||
240202,
|
||||
240228,
|
||||
240326,
|
||||
240429,
|
||||
240528,
|
||||
240613,
|
||||
240702,
|
||||
240723,
|
||||
240810,
|
||||
240904,
|
||||
241001,
|
||||
241017,
|
||||
241107,
|
||||
241122,
|
||||
250121,
|
||||
250213,
|
||||
250227,
|
||||
];
|
||||
|
||||
const kUnlimited = 99999;
|
||||
|
||||
// TODO: Implement automated version checks from this metaserver script.
|
||||
// It should check by cron if actual files are really available on all servers.
|
||||
export const SERVER = {
|
||||
backblaze: {
|
||||
// BackBlaze + CloudFlare (US-West) unmetered.
|
||||
url: 'https://cdn-us1.organicmaps.app/',
|
||||
dataVersions: kUnlimited,
|
||||
},
|
||||
uk1: {
|
||||
// Mythic Beasts VPS (London, UK) 200TB/mo.
|
||||
url: 'https://cdn-uk1.organicmaps.app/',
|
||||
dataVersions: 3,
|
||||
},
|
||||
nl1: {
|
||||
// // Mythic Beasts VPS (Amsterdam, NL) 200TB/mo.
|
||||
url: 'https://cdn-nl1.organicmaps.app/',
|
||||
dataVersions: 3,
|
||||
},
|
||||
planet: {
|
||||
// Hetzner BareMetal (Helsinki, Finland) unmetered
|
||||
url: 'https://cdn.organicmaps.app/',
|
||||
dataVersions: kUnlimited,
|
||||
},
|
||||
beta: {
|
||||
// Alias of the planet above that is proxied via CF and with enabled /maps/ *.mwm caching,
|
||||
// to speed-up downloads for beta testers.
|
||||
url: 'https://cdn-beta.organicmaps.app/',
|
||||
// Can have any non-publicly available maps data version.
|
||||
dataVersions: [],
|
||||
},
|
||||
fi1: {
|
||||
// Hetzner Cloud (Helsinki, Finland), 20TB/mo
|
||||
url: 'https://cdn-fi1.organicmaps.app/',
|
||||
dataVersions: 2,
|
||||
},
|
||||
de1: {
|
||||
// Hetzner Cloud (Falkenstein, Germany), 20TB/mo
|
||||
url: 'https://cdn-eu2.organicmaps.app/',
|
||||
dataVersions: 2,
|
||||
},
|
||||
de2: {
|
||||
// Hetzner Cloud (Falkenstein, Germany), 20TB/mo
|
||||
url: 'https://cdn-de2.organicmaps.app/',
|
||||
dataVersions: 3,
|
||||
},
|
||||
de3: {
|
||||
// Hetzner Cloud (Nuremberg, Germany), 20TB/mo
|
||||
url: 'https://cdn-de3.organicmaps.app/',
|
||||
dataVersions: 4,
|
||||
},
|
||||
us_east1: {
|
||||
// Hetzner Cloud (Ashburn, US East), 20TB/mo
|
||||
url: 'https://cdn-us-east1.organicmaps.app/',
|
||||
dataVersions: 3,
|
||||
},
|
||||
us_west1: {
|
||||
// Hetzner Cloud (Hillsdate, US West), 20TB/mo
|
||||
url: 'https://cdn-us-west1.organicmaps.app/',
|
||||
dataVersions: 2,
|
||||
},
|
||||
vi1: {
|
||||
// Vietnam, unlimited 1Gbps bandwidth
|
||||
url: 'https://cdn-vi1.organicmaps.app/',
|
||||
dataVersions: 5,
|
||||
},
|
||||
};
|
||||
|
||||
// Exported for tests.
|
||||
export const DONATE_URL = 'https://organicmaps.app/donate/';
|
||||
|
||||
export async function getServersList(request: Request) {
|
||||
// Private for map files.
|
||||
let servers;
|
||||
// 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'));
|
||||
const abusedVersions = ['1.8.6-4-ios', '1.8.7-1-ios', '1.8.8-1-ios'];
|
||||
if (dataVersion === null) {
|
||||
// Older clients download from the archive.
|
||||
servers = [SERVER.backblaze];
|
||||
} else if (dataVersion == 240702 && abusedVersions.includes(request.headers.get('x-om-appversion') || 'unknown')) {
|
||||
// Redirect https://apps.apple.com/us/app/mapxplorer-navigation-radar/id6463052823
|
||||
// who abuses our servers to a slow download "trap" node.
|
||||
return new Response('["https://cdn-fi2.organicmaps.app/"]', {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore workarounds error TS2339: Property 'continent' does not exist on type 'IncomingRequestCfProperties<unknown>'.
|
||||
switch (request.cf?.continent) {
|
||||
// See https://developers.cloudflare.com/firewall/cf-firewall-language/fields for a list of all continents.
|
||||
case 'NA': // North America
|
||||
case 'SA': // South America
|
||||
servers = [SERVER.us_east1, SERVER.us_west1, SERVER.uk1, SERVER.nl1, SERVER.planet].filter((server) =>
|
||||
DATA_VERSIONS.slice(-server.dataVersions).includes(dataVersion),
|
||||
);
|
||||
break;
|
||||
case 'OC': // Oceania
|
||||
servers = [SERVER.vi1, SERVER.us_east1, SERVER.us_west1, SERVER.planet].filter((server) =>
|
||||
DATA_VERSIONS.slice(-server.dataVersions).includes(dataVersion),
|
||||
);
|
||||
break;
|
||||
case 'AS': // Asia
|
||||
servers = [SERVER.vi1, SERVER.uk1, SERVER.nl1, SERVER.planet].filter((server) =>
|
||||
DATA_VERSIONS.slice(-server.dataVersions).includes(dataVersion),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// Every other continent + Tor networks.
|
||||
servers = [SERVER.planet, SERVER.uk1, SERVER.nl1, SERVER.fi1, SERVER.de1, SERVER.de2, SERVER.de3].filter(
|
||||
(server) => DATA_VERSIONS.slice(-server.dataVersions).includes(dataVersion),
|
||||
);
|
||||
// Only fallback to the archive in the US if nothing was found closer.
|
||||
if (servers.length == 0 && DATA_VERSIONS.slice(-SERVER.backblaze.dataVersions).includes(dataVersion)) {
|
||||
servers = [SERVER.backblaze];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback to the planet with freshly generated/beta data.
|
||||
if (servers.length == 0) {
|
||||
servers = [SERVER.beta, SERVER.planet];
|
||||
}
|
||||
servers = servers.map((server) => server.url);
|
||||
|
||||
// Header "X-OM-AppVersion: 2022.09.22-3-Google" (lowercased by CF) is supported from August 23, 2022.
|
||||
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;
|
||||
NY?: string;
|
||||
};
|
||||
productsConfig?: ProductsConfig;
|
||||
} = {
|
||||
servers: servers,
|
||||
};
|
||||
|
||||
// Disable donates for reviewers for all app versions AFTER this one.
|
||||
const lastApprovedAndReleasedGoogleAppVersionCode = 250217;
|
||||
const lastApprovedAndReleasediOSAppVersionCode = 250303;
|
||||
let donatesEnabled = true;
|
||||
if (
|
||||
appVersion.flavor === 'google' &&
|
||||
((typeof request.cf?.asOrganization === 'string' && request.cf?.asOrganization.toLowerCase().includes('google')) ||
|
||||
appVersion.code > lastApprovedAndReleasedGoogleAppVersionCode)
|
||||
) {
|
||||
donatesEnabled = false;
|
||||
} else if (appVersion.build === undefined) {
|
||||
// Disable donates for older iOS versions without donates menu support.
|
||||
donatesEnabled = false;
|
||||
} else if (
|
||||
appVersion.flavor === 'ios' &&
|
||||
((typeof request.cf?.asOrganization === 'string' && request.cf?.asOrganization.toLowerCase().includes('apple')) ||
|
||||
appVersion.code > lastApprovedAndReleasediOSAppVersionCode)
|
||||
) {
|
||||
donatesEnabled = false;
|
||||
}
|
||||
|
||||
if (donatesEnabled) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
response.settings = {
|
||||
DonateUrl: DONATE_URL,
|
||||
NY: 'false', // Must be `string` instead of `bool`, otherwise clients will crash
|
||||
};
|
||||
if (appVersion.code >= 241022) {
|
||||
const locale = request.headers.get('accept-language');
|
||||
response.productsConfig = getProducts(locale);
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(response), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
52
src/utils.ts
52
src/utils.ts
|
@ -1,52 +0,0 @@
|
|||
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,
|
||||
};
|
||||
}
|
64
src/versions.ts
Normal file
64
src/versions.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
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;
|
||||
}
|
||||
|
||||
// 2022.11.20 for iOS versions released before November 21 (without donate menu)
|
||||
// 2022.11.24-4-ios for newer iOS versions (with donate menu)
|
||||
// 2022.12.24-10-Google for Android
|
||||
// 2022.12.24-3-3f4ca43-Linux or 2022.12.24-3-3f4ca43-dirty-Linux for Linux
|
||||
// 2022.12.24-3-3f4ca43-Darwin for Mac
|
||||
const VERSION_RE =
|
||||
/(?<year>\d{4})\.(?<month>\d{1,2})\.(?<day>\d{1,2})(?:$|-(?<build>[0-9]+)(?:-[0-9a-f]+)?(?:-dirty)?-(?<flavor>[A-Za-z3264]+))/;
|
||||
// Returns code like 221224 for both platforms, build and flavor for Android and newer iOS versions.
|
||||
export function parseAppVersion(
|
||||
versionName: string | null,
|
||||
): { code: number; build?: number; flavor?: string | undefined } | null {
|
||||
if (!versionName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const m = versionName.match(VERSION_RE);
|
||||
if (m === null || m.length < 4 || !m.groups) {
|
||||
return null;
|
||||
}
|
||||
const yyyy = parseInt(m.groups.year);
|
||||
if (Number.isNaN(yyyy) || yyyy > 2099 || yyyy < 2022) {
|
||||
return null;
|
||||
}
|
||||
const mm = parseInt(m.groups.month);
|
||||
if (Number.isNaN(mm) || mm > 12 || mm < 1) {
|
||||
return null;
|
||||
}
|
||||
const dd = parseInt(m.groups.day);
|
||||
if (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'));
|
||||
// Older iOS versions without donate button.
|
||||
if (!m.groups.build) {
|
||||
return { code: code };
|
||||
}
|
||||
|
||||
const buildNumber = parseInt(m.groups.build);
|
||||
const build = Number.isNaN(buildNumber) ? 0 : buildNumber;
|
||||
// 'ios' for iOS devices.
|
||||
const flavor = (m.groups.flavor !== undefined && m.groups.flavor.toLowerCase()) || undefined;
|
||||
|
||||
return {
|
||||
code: code,
|
||||
flavor: flavor,
|
||||
build: build,
|
||||
};
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
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]);
|
||||
});
|
||||
});
|
14
test/products.test.ts
Normal file
14
test/products.test.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { describe, expect, test } from '@jest/globals';
|
||||
import { getProducts } from '../src/products';
|
||||
|
||||
describe('getProducts', () => {
|
||||
test('fr-FR', () => {
|
||||
const fr_FR = getProducts('fr-FR');
|
||||
expect(fr_FR).toBeDefined();
|
||||
if (!fr_FR) return;
|
||||
expect(fr_FR.placePagePrompt).toBe(
|
||||
"L'application Organic Maps est gratuite pour tout le monde grâce à vos dons. Pas de publicité. Pas de trackers. Open-source.",
|
||||
);
|
||||
expect(fr_FR.products[fr_FR.products.length - 1].title).toEqual('Autre');
|
||||
});
|
||||
});
|
109
test/servers.test.ts
Normal file
109
test/servers.test.ts
Normal file
|
@ -0,0 +1,109 @@
|
|||
import { describe, expect, test } from '@jest/globals';
|
||||
import { getServersList, SERVER, DATA_VERSIONS, DONATE_URL } from '../src/servers';
|
||||
|
||||
const URL = 'https://worker/servers';
|
||||
const LAST_DATA_VERSION = DATA_VERSIONS[DATA_VERSIONS.length - 1];
|
||||
|
||||
// Note: CF lowercases all headers.
|
||||
describe('X-OM-DataVersion', () => {
|
||||
test('no X-OM-DataVersion', async () => {
|
||||
const req = new Request(URL);
|
||||
const result = await getServersList(req);
|
||||
expect(result.status).toBe(200);
|
||||
expect(JSON.parse(await result.text())).toEqual([SERVER.backblaze.url]);
|
||||
});
|
||||
|
||||
test('has X-OM-DataVersion', async () => {
|
||||
const server = SERVER.fi1;
|
||||
let req = new Request(URL, {
|
||||
headers: {
|
||||
'X-OM-DataVersion': String(LAST_DATA_VERSION),
|
||||
},
|
||||
});
|
||||
const result = await getServersList(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 getServersList(req);
|
||||
expect(result.status).toBe(200);
|
||||
expect(JSON.parse(await result.text())).toEqual([SERVER.beta.url, SERVER.planet.url]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('X-OM-AppVersion DonateUrl', () => {
|
||||
test('Old versions without X-OM-AppVersion and old metaserver JSON format', async () => {
|
||||
const req = new Request(URL);
|
||||
const response = await getServersList(req);
|
||||
expect(response.status).toBe(200);
|
||||
expect(JSON.parse(await response.text())).toEqual([SERVER.backblaze.url]);
|
||||
});
|
||||
|
||||
const server = SERVER.fi1;
|
||||
|
||||
test('Newer metaserver JSON format with donates support', async () => {
|
||||
let req = new Request(URL, {
|
||||
headers: {
|
||||
'X-OM-AppVersion': '2022.08.23-1-Google',
|
||||
'X-OM-DataVersion': String(LAST_DATA_VERSION),
|
||||
},
|
||||
});
|
||||
const response = await getServersList(req);
|
||||
expect(response.status).toBe(200);
|
||||
const result = JSON.parse(await response.text());
|
||||
expect(result.servers).toBeDefined();
|
||||
expect(result.servers.length).toBeGreaterThan(0);
|
||||
expect(result.servers).toContain(server.url);
|
||||
expect(result.settings).toBeDefined();
|
||||
expect(result.settings.DonateUrl).toBeDefined();
|
||||
expect(result.settings.DonateUrl).toEqual(DONATE_URL);
|
||||
});
|
||||
|
||||
test('Newer metaserver JSON format with donates support', async () => {
|
||||
let req = new Request(URL, {
|
||||
headers: {
|
||||
'X-OM-AppVersion': '2022.08.23-1-Google',
|
||||
'X-OM-DataVersion': String(LAST_DATA_VERSION),
|
||||
},
|
||||
//@ts-ignore
|
||||
cf: { country: 'RU' },
|
||||
});
|
||||
const response = await getServersList(req);
|
||||
expect(response.status).toBe(200);
|
||||
const result = JSON.parse(await response.text());
|
||||
expect(result.settings.DonateUrl).toBeDefined();
|
||||
});
|
||||
|
||||
test('Older iOS versions with X-OM-AppVersion but without donates', async () => {
|
||||
let req = new Request(URL, {
|
||||
headers: {
|
||||
'X-OM-AppVersion': '2022.11.20',
|
||||
'X-OM-DataVersion': String(LAST_DATA_VERSION),
|
||||
},
|
||||
});
|
||||
const response = await getServersList(req);
|
||||
expect(response.status).toBe(200);
|
||||
const result = JSON.parse(await response.text());
|
||||
expect(result.settings).not.toBeDefined();
|
||||
});
|
||||
|
||||
test('Newer iOS versions with donate menu support', async () => {
|
||||
let req = new Request(URL, {
|
||||
headers: {
|
||||
'X-OM-AppVersion': '2022.11.20-4-ios',
|
||||
'X-OM-DataVersion': String(LAST_DATA_VERSION),
|
||||
},
|
||||
});
|
||||
const response = await getServersList(req);
|
||||
expect(response.status).toBe(200);
|
||||
const result = JSON.parse(await response.text());
|
||||
expect(result.settings.DonateUrl).toBeDefined();
|
||||
expect(result.settings.DonateUrl).toEqual(DONATE_URL);
|
||||
});
|
||||
});
|
|
@ -1,36 +0,0 @@
|
|||
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));
|
||||
});
|
68
test/versions.test.ts
Normal file
68
test/versions.test.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
import { describe, expect, test } from '@jest/globals';
|
||||
import { parseDataVersion, parseAppVersion } from '../src/versions';
|
||||
|
||||
describe('parseDataVersion', () => {
|
||||
const tests: { [key: string]: number | null } = {
|
||||
'220801': 220801,
|
||||
'210000': 210000,
|
||||
'500000': 500000,
|
||||
'200000': null,
|
||||
'500001': null,
|
||||
garbage: null,
|
||||
null: null,
|
||||
'': null,
|
||||
};
|
||||
for (const input in tests) {
|
||||
test(input, () => expect(parseDataVersion(input)).toEqual(tests[input]));
|
||||
}
|
||||
test('', () => expect(parseDataVersion(null)).toEqual(null));
|
||||
});
|
||||
|
||||
describe('parseAppVersion', () => {
|
||||
const tests: { [key: string]: object | null } = {
|
||||
// Older iOS releases without donate menu
|
||||
'2022.08.01': { code: 220801 },
|
||||
// Newer iOS releases with donate menu
|
||||
'2022.11.25-5-ios': { code: 221125, build: 5, flavor: 'ios' },
|
||||
// There were no such versions in production.
|
||||
'2022.08.01-1': null,
|
||||
'2022.08.01-1-Google': { code: 220801, build: 1, flavor: 'google' },
|
||||
// -debug is ignored
|
||||
'2022.08.01-1-Google-debug': { code: 220801, build: 1, flavor: 'google' },
|
||||
// TODO: Fix regexp. Not it should not happen in production.
|
||||
//'2022.08.01-1-fd-debug': { code: 220801, build: 1, flavor: 'fd' },
|
||||
'2022.1.1-0': null,
|
||||
'2099.12.31-999999999': null,
|
||||
'2023.03.22-1-4fac32de-Linux': { code: 230322, build: 1, flavor: 'linux' },
|
||||
'2023.03.22-1-4fac32de-dirty-Linux': { code: 230322, build: 1, flavor: 'linux' },
|
||||
'2023.03.22-1-4fac32de-Darwin': { code: 230322, build: 1, flavor: 'darwin' },
|
||||
'2023.03.22-1-4fac32de-dirty-Darwin': { code: 230322, build: 1, flavor: 'darwin' },
|
||||
'2021.01.31-1': null,
|
||||
'2100.01.31-1': null,
|
||||
'2022.00.31-1': null,
|
||||
'2022.13.31-1': null,
|
||||
'2022.01.00-1': null,
|
||||
'2022.01.32-1': null,
|
||||
'22.01.31-1': null,
|
||||
'22.01.31': null,
|
||||
'22.01.31-3-flavor': null,
|
||||
'202.01.31-1': null,
|
||||
'202.01.312-1': null,
|
||||
'202.01.312': null,
|
||||
'202.01.31-aa-flavor': null,
|
||||
'202.01.31-5a-flavor': null,
|
||||
'202.01.31-a5-flavor': null,
|
||||
'2022..31-11': null,
|
||||
'2022..31-11-flavor': null,
|
||||
'.11.31-11': null,
|
||||
'.11.31-11-flavor': null,
|
||||
'.11..31-11-flavor': null,
|
||||
garbage: null,
|
||||
'': null,
|
||||
null: null,
|
||||
};
|
||||
for (const input in tests) {
|
||||
test(input, () => expect(parseAppVersion(input)).toEqual(tests[input]));
|
||||
}
|
||||
test('', () => expect(parseAppVersion(null)).toEqual(null));
|
||||
});
|
|
@ -5,12 +5,13 @@
|
|||
"module": "ES6",
|
||||
"moduleResolution": "node",
|
||||
"target": "ESNext",
|
||||
"lib": ["ESNext", "webworker"],
|
||||
"lib": ["ESNext"],
|
||||
"alwaysStrict": true,
|
||||
"strict": true,
|
||||
"preserveConstEnums": true,
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"types": ["@cloudflare/workers-types"]
|
||||
},
|
||||
"include": ["src/"],
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
# Default worker is for dev only.
|
||||
# See omaps and organicmaps environments below for production.
|
||||
name = 'meta-dev'
|
||||
type = 'javascript'
|
||||
compatibility_date = '2021-11-11'
|
||||
main = 'src/index.ts'
|
||||
compatibility_date = '2022-11-21'
|
||||
# Organic Maps CF Account ID.
|
||||
account_id = '462f578f0939f041e2c24ec99adce458'
|
||||
workers_dev = true
|
||||
# Whether Wrangler should send usage metrics to Cloudflare for this project.
|
||||
send_metrics = false
|
||||
|
||||
[build]
|
||||
upload.format = 'service-worker'
|
||||
command = 'npm ci --prefer-offline --no-audit && npm run build'
|
||||
|
||||
[vars]
|
||||
|
@ -17,8 +18,6 @@ DEBUG = true
|
|||
[env.prod]
|
||||
name = 'meta'
|
||||
workers_dev = false
|
||||
# omaps.app CF zone ID.
|
||||
zone_id = '3fce06554abc3899504e11d928be0ee7'
|
||||
# See the full list of handled paths in the code.
|
||||
route = 'meta.omaps.app/*'
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue