country-linkify 24.482.11825
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +12 -0
- package/.github/FUNDING.yml +1 -0
- package/Dockerfile +23 -0
- package/README.md +75 -0
- package/assets/404.html +17 -0
- package/assets/images/aliexpress.png +0 -0
- package/assets/images/amazon.png +0 -0
- package/assets/images/amex.png +0 -0
- package/assets/images/bikecomponents.png +0 -0
- package/assets/images/bitpanda.png +0 -0
- package/assets/images/decathlon.png +0 -0
- package/assets/images/halfords.png +0 -0
- package/assets/images/ican.png +0 -0
- package/assets/images/n26.png +0 -0
- package/assets/images/nextdns.png +0 -0
- package/assets/images/probikekit.png +0 -0
- package/assets/images/revolut.png +0 -0
- package/assets/images/ribble.png +0 -0
- package/assets/images/scalable.png +0 -0
- package/assets/images/traderepublic.png +0 -0
- package/assets/redir.html +23 -0
- package/assets/robots.txt +2 -0
- package/assets/styles.css +49 -0
- package/lib/countrymanager.d.ts +13 -0
- package/lib/countrymanager.js +93 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +28 -0
- package/lib/linkmanager.d.ts +20 -0
- package/lib/linkmanager.js +203 -0
- package/lib/server.d.ts +17 -0
- package/lib/server.js +164 -0
- package/lib/start.d.ts +1 -0
- package/lib/start.js +45 -0
- package/lib/types.d.ts +11 -0
- package/lib/types.js +2 -0
- package/lib/utils.d.ts +1 -0
- package/lib/utils.js +13 -0
- package/package.json +54 -0
- package/settings.development.json +18 -0
- package/settings.json +52 -0
- package/settings.production.sample.json +36 -0
package/.dockerignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
github: igoramadas
|
package/Dockerfile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# BUILDER
|
|
2
|
+
FROM node:22-alpine AS cl-builder
|
|
3
|
+
ENV NODE_ENV=development
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
COPY . .
|
|
6
|
+
RUN npm install && ./node_modules/.bin/tsc
|
|
7
|
+
|
|
8
|
+
# DEPENDENCIES
|
|
9
|
+
FROM node:22-alpine AS cl-dependencies
|
|
10
|
+
ENV NODE_ENV=production
|
|
11
|
+
WORKDIR /app
|
|
12
|
+
COPY . .
|
|
13
|
+
RUN npm install --production
|
|
14
|
+
|
|
15
|
+
# FINAL IMAGE
|
|
16
|
+
FROM node:22-alpine AS cl-final
|
|
17
|
+
ENV NODE_ENV=production
|
|
18
|
+
WORKDIR /app
|
|
19
|
+
COPY . .
|
|
20
|
+
COPY --from=cl-dependencies ./app/node_modules ./node_modules
|
|
21
|
+
COPY --from=cl-builder ./app/lib ./lib
|
|
22
|
+
|
|
23
|
+
CMD ["npm", "start"]
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Country Linkify
|
|
2
|
+
|
|
3
|
+
Simple Node.js library / service to redirect links based on the client's country and link ID.
|
|
4
|
+
|
|
5
|
+
## Link definitions
|
|
6
|
+
|
|
7
|
+
All links should be defined in .json files under the [/links](https://github.com/igoramadas/country-linkify/tree/master/links) folder (or another folder, if you change the `settings.links.path` key). Each file represents a `source`, for example, a specific website or category of links.
|
|
8
|
+
|
|
9
|
+
Each key inside the JSON specs represents a link ID, and the target URLs by country. If a key gets a single string as its value, this will be the URL valid for any country.
|
|
10
|
+
|
|
11
|
+
There are also 2 standard keys, prefixed with an underscore:
|
|
12
|
+
|
|
13
|
+
- **\_default**: the default target URL(s) for that source.
|
|
14
|
+
- **\_search**: URL for the search page of the source, must have a `{{query}}` tag defined
|
|
15
|
+
|
|
16
|
+
A sample definition for "Amazon":
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"_default": {
|
|
21
|
+
"de": "https://www.amazon.de/",
|
|
22
|
+
"us": "https://www.amazon.com/",
|
|
23
|
+
"uk": "https://www.amazon.co.uk/"
|
|
24
|
+
},
|
|
25
|
+
"_search": {
|
|
26
|
+
"us": "https://www.amazon.com/s?k={{query}}"
|
|
27
|
+
},
|
|
28
|
+
"deals": {
|
|
29
|
+
"de": "https://www.amazon.de/-/en/deals",
|
|
30
|
+
"us": "https://www.amazon.com/gp/goldbox",
|
|
31
|
+
"uk": "https://www.amazon.co.uk/gp/deals"
|
|
32
|
+
},
|
|
33
|
+
"help": "https://www.amazon.com/gp/help/customer/display.html"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
And a second file for "AliExpress":
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"_default": "https://aliexpress.com",
|
|
42
|
+
"_search": {
|
|
43
|
+
"de": "https://de.aliexpress.com/w/{{query}}.html"
|
|
44
|
+
},
|
|
45
|
+
"deals": {
|
|
46
|
+
"de": "https://www.aliexpress.com/sp/campaign/wow/gcp-plus/300000444/njcQZ4CkBb"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
With the samples above, a user from Germany (de) will get redirected to the following target URLs:
|
|
52
|
+
|
|
53
|
+
- **/l/amazon**: https://www.amazon.de/
|
|
54
|
+
- **/l/amazon-help**: https://www.amazon.com/gp/help/customer/display.html
|
|
55
|
+
- **/l/amazon-deals**: https://www.amazon.de/-/en/deals
|
|
56
|
+
- **/l/deals**: https://www.amazon.de/-/en/deals or https://www.aliexpress.com/sp/campaign/wow/gcp-plus/300000444/njcQZ4CkBb
|
|
57
|
+
- **/s/some-item**: https://de.aliexpress.com/w/some-item.html
|
|
58
|
+
|
|
59
|
+
And the results for someone from US:
|
|
60
|
+
|
|
61
|
+
- **/l/amazon**: https://www.amazon.com/
|
|
62
|
+
- **/l/amazon-help**: https://www.amazon.com/gp/help/customer/display.html
|
|
63
|
+
- **/l/amazon-deals**: https://www.amazon.com/gp/goldbox
|
|
64
|
+
- **/s/some-item**: "https://www.amazon.com/s?k=some+item"
|
|
65
|
+
|
|
66
|
+
## Settings
|
|
67
|
+
|
|
68
|
+
This tool is using the [SetMeUp](https://github.com/igoramadas/setmeup) module to handle its settings, so for detailed info please check its [docs](https://setmeup.devv.com).
|
|
69
|
+
|
|
70
|
+
- **settings.json** - settings shared by all environments, targeting production by default
|
|
71
|
+
- **settings.development.json** - development settings, mostly when running on your dev machine
|
|
72
|
+
- **settings.production.json** - production-only settings, except credentials and secrets (optional)
|
|
73
|
+
- **settings.local.json** - private local-only settings, excluded from the GIT repo
|
|
74
|
+
|
|
75
|
+
Settings are self explanatory, please open each file to check the available options.
|
package/assets/404.html
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Devv</title>
|
|
5
|
+
<meta name="description" content="Free software, power to the people! Node.js Javascript TypeScript Docker GCP." />
|
|
6
|
+
<meta name="viewport" content="height=device-height, width=device-width, initial-scale=1" />
|
|
7
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Crete+Round&display=swap" />
|
|
8
|
+
<link rel="stylesheet" href="/styles.css" />
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div class="wrapper">
|
|
12
|
+
<h1>Devv</h1>
|
|
13
|
+
<p class="margin-top">We could not find the destination you were looking for. Perhaps our affiliate links?</p>
|
|
14
|
+
<div class="margin-top logos">${logos}</div>
|
|
15
|
+
</div>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Redirecting from ${from} to ${target}</title>
|
|
5
|
+
<meta http-equiv="refresh" content="2;URL='${url}'" />
|
|
6
|
+
<meta name="viewport" content="height=device-height, width=device-width, initial-scale=1" />
|
|
7
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Crete+Round&display=swap" />
|
|
8
|
+
<link rel="stylesheet" href="/styles.css" />
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div class="wrapper">
|
|
12
|
+
<h2>Redirecting from ${from}</h2>
|
|
13
|
+
<p class="margin-top">You'll be redirected to an affiliate link in a few seconds...</p>
|
|
14
|
+
<div class="redir-logo">
|
|
15
|
+
<a href="${url}"><img src="/images/${logo}.png" class="logo" /></a>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
<script type="text/javascript">
|
|
19
|
+
const redirDelay = () => (window.location.href = "${url}")
|
|
20
|
+
setTimeout(redirDelay, 2000)
|
|
21
|
+
</script>
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
html {
|
|
2
|
+
background: rgb(255, 255, 255);
|
|
3
|
+
height: 100%;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
body {
|
|
7
|
+
background: linear-gradient(180deg, rgba(25, 25, 25, 1) 0%, rgba(5, 5, 5, 1) 5%, rgba(0, 0, 0, 1) 100%);
|
|
8
|
+
color: rgb(255, 255, 255);
|
|
9
|
+
font-family: "Crete Round", Arial, Helvetica, sans-serif;
|
|
10
|
+
margin: auto;
|
|
11
|
+
min-height: 100%;
|
|
12
|
+
text-align: center;
|
|
13
|
+
width: auto;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
h1 {
|
|
17
|
+
font-size: 9em;
|
|
18
|
+
margin: 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.wrapper {
|
|
22
|
+
padding-top: 20px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.margin-top {
|
|
26
|
+
margin-top: 20px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.logos {
|
|
30
|
+
background: rgb(220, 220, 220);
|
|
31
|
+
border-radius: 20px;
|
|
32
|
+
margin: auto;
|
|
33
|
+
max-width: 960px;
|
|
34
|
+
padding: 5px 0 40px 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.logos img {
|
|
38
|
+
margin: 5px 20px 5px 20px;
|
|
39
|
+
max-width: 260px;
|
|
40
|
+
vertical-align: middle;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.redir-logo {
|
|
44
|
+
margin-top: 50px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.redir-logo img {
|
|
48
|
+
max-width: 260px;
|
|
49
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare class CountryManager {
|
|
2
|
+
private constructor();
|
|
3
|
+
private static _instance;
|
|
4
|
+
static get Instance(): CountryManager;
|
|
5
|
+
aliases: {
|
|
6
|
+
[id: string]: string;
|
|
7
|
+
};
|
|
8
|
+
init: () => Promise<void>;
|
|
9
|
+
load: () => Promise<void>;
|
|
10
|
+
getForIP: (ip: string) => Promise<string>;
|
|
11
|
+
}
|
|
12
|
+
declare const _default: CountryManager;
|
|
13
|
+
export default _default;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CountryManager = void 0;
|
|
7
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
8
|
+
const bent = require("bent");
|
|
9
|
+
const bitecache_1 = __importDefault(require("bitecache"));
|
|
10
|
+
const anyhow_1 = __importDefault(require("anyhow"));
|
|
11
|
+
let settings;
|
|
12
|
+
class CountryManager {
|
|
13
|
+
constructor() { }
|
|
14
|
+
static _instance;
|
|
15
|
+
static get Instance() {
|
|
16
|
+
return this._instance || (this._instance = new this());
|
|
17
|
+
}
|
|
18
|
+
aliases = {};
|
|
19
|
+
init = async () => {
|
|
20
|
+
settings = require("setmeup").settings.countryLinkify;
|
|
21
|
+
try {
|
|
22
|
+
await this.load();
|
|
23
|
+
bitecache_1.default.setup(settings.cacheName, settings.country.cacheDuration);
|
|
24
|
+
}
|
|
25
|
+
catch (ex) {
|
|
26
|
+
anyhow_1.default.error("CountryManager.init", "Failed to init", ex);
|
|
27
|
+
throw ex;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
load = async () => {
|
|
31
|
+
const countryAliases = Object.entries(settings.country);
|
|
32
|
+
let code;
|
|
33
|
+
let aliases;
|
|
34
|
+
for ([code, aliases] of countryAliases) {
|
|
35
|
+
if (code == "cacheDuration" || code == "default")
|
|
36
|
+
continue;
|
|
37
|
+
code = code.toLowerCase();
|
|
38
|
+
if (lodash_1.default.isString(aliases)) {
|
|
39
|
+
aliases = [aliases];
|
|
40
|
+
}
|
|
41
|
+
anyhow_1.default.debug("Country.load", `Country ${code} aliases: ${aliases.join(", ")}`);
|
|
42
|
+
for (let alias of aliases) {
|
|
43
|
+
this.aliases[alias] = code;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
getForIP = async (ip) => {
|
|
48
|
+
if (!ip) {
|
|
49
|
+
anyhow_1.default.warn("CountryManager.getForIP", "Missing IP address");
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const cached = bitecache_1.default.get(settings.cacheName, ip);
|
|
53
|
+
if (cached) {
|
|
54
|
+
anyhow_1.default.debug("CountryManager.getForIP", ip, "From cache");
|
|
55
|
+
return cached;
|
|
56
|
+
}
|
|
57
|
+
let apiHost;
|
|
58
|
+
anyhow_1.default.debug("CountryManager.getForIP", ip);
|
|
59
|
+
try {
|
|
60
|
+
apiHost = "geojs.io";
|
|
61
|
+
const ipUrl = `https://get.geojs.io/v1/ip/country/${ip}.json`;
|
|
62
|
+
const apiGet = bent(ipUrl, "GET", "json");
|
|
63
|
+
const ipData = await apiGet();
|
|
64
|
+
if (ipData && ipData.country) {
|
|
65
|
+
anyhow_1.default.debug("CountryManager.getForIP", ip, ipData.country, `Via ${apiHost}`);
|
|
66
|
+
const result = ipData.country.toLowerCase();
|
|
67
|
+
bitecache_1.default.set(settings.cacheName, ip, result);
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (ex) {
|
|
72
|
+
anyhow_1.default.error("CountryManager.getForIP", ip, apiHost, ex);
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
apiHost = "ip-api.com";
|
|
76
|
+
const ipUrl = `http://ip-api.com/json/${ip}`;
|
|
77
|
+
const apiGet = bent(ipUrl, "GET", "json");
|
|
78
|
+
const ipData = await apiGet();
|
|
79
|
+
if (ipData && ipData.countryCode) {
|
|
80
|
+
anyhow_1.default.debug("CountryManager.getForIP", ip, ipData.countryCode, `Via ${apiHost}`);
|
|
81
|
+
const result = ipData.countryCode.toLowerCase();
|
|
82
|
+
bitecache_1.default.set(settings.cacheName, ip, result);
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (ex) {
|
|
87
|
+
anyhow_1.default.error("CountryManager.getForIP", ip, apiHost, ex);
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
exports.CountryManager = CountryManager;
|
|
93
|
+
exports.default = CountryManager.Instance;
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const anyhow_1 = __importDefault(require("anyhow"));
|
|
7
|
+
const setmeup_1 = __importDefault(require("setmeup"));
|
|
8
|
+
const countrymanager_1 = __importDefault(require("./countrymanager"));
|
|
9
|
+
const linkmanager_1 = __importDefault(require("./linkmanager"));
|
|
10
|
+
const server_1 = __importDefault(require("./server"));
|
|
11
|
+
const startup = async (app) => {
|
|
12
|
+
anyhow_1.default.info("CountryLinkify.startup", `PID ${process.pid}`);
|
|
13
|
+
try {
|
|
14
|
+
if (!setmeup_1.default.settings.countryLinkify) {
|
|
15
|
+
setmeup_1.default.load();
|
|
16
|
+
setmeup_1.default.loadFromEnv();
|
|
17
|
+
setmeup_1.default.load("settings.local.json");
|
|
18
|
+
}
|
|
19
|
+
await countrymanager_1.default.init();
|
|
20
|
+
await linkmanager_1.default.init();
|
|
21
|
+
await server_1.default.init(app);
|
|
22
|
+
}
|
|
23
|
+
catch (ex) {
|
|
24
|
+
anyhow_1.default.warn("CountryLinkify.startup", ex);
|
|
25
|
+
return process.exit();
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
exports.default = startup;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Link, TargetURL } from "./types";
|
|
2
|
+
export declare class LinkManager {
|
|
3
|
+
private constructor();
|
|
4
|
+
private static _instance;
|
|
5
|
+
static get Instance(): LinkManager;
|
|
6
|
+
private watchers;
|
|
7
|
+
sources: string[];
|
|
8
|
+
links: {
|
|
9
|
+
[id: string]: Link;
|
|
10
|
+
};
|
|
11
|
+
aliases: {
|
|
12
|
+
[alias: string]: string;
|
|
13
|
+
};
|
|
14
|
+
init: () => Promise<void>;
|
|
15
|
+
load: (reset?: boolean) => Promise<void>;
|
|
16
|
+
loadFile: (filepath: string) => Promise<void>;
|
|
17
|
+
urlFor: (id: string, country: string, sources?: string[], search?: boolean) => TargetURL;
|
|
18
|
+
}
|
|
19
|
+
declare const _default: LinkManager;
|
|
20
|
+
export default _default;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LinkManager = void 0;
|
|
7
|
+
const utils_1 = require("./utils");
|
|
8
|
+
const countrymanager_1 = __importDefault(require("./countrymanager"));
|
|
9
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const anyhow_1 = __importDefault(require("anyhow"));
|
|
13
|
+
let settings;
|
|
14
|
+
class LinkManager {
|
|
15
|
+
constructor() { }
|
|
16
|
+
static _instance;
|
|
17
|
+
static get Instance() {
|
|
18
|
+
return this._instance || (this._instance = new this());
|
|
19
|
+
}
|
|
20
|
+
watchers = [];
|
|
21
|
+
sources = [];
|
|
22
|
+
links = {};
|
|
23
|
+
aliases = {};
|
|
24
|
+
init = async () => {
|
|
25
|
+
settings = require("setmeup").settings.countryLinkify;
|
|
26
|
+
try {
|
|
27
|
+
await this.load();
|
|
28
|
+
}
|
|
29
|
+
catch (ex) {
|
|
30
|
+
anyhow_1.default.error("LinkManager.init", "Failed to init", ex);
|
|
31
|
+
throw ex;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
load = async (reset) => {
|
|
35
|
+
try {
|
|
36
|
+
const directory = settings.links.path.substring(0, 1) == "/" ? settings.links.path : path_1.default.join(process.cwd(), settings.links.path);
|
|
37
|
+
if (!fs_1.default.existsSync(directory)) {
|
|
38
|
+
anyhow_1.default.warn("LinkManager.load", `Directory ${directory} not found, no links were loaded`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const files = fs_1.default.readdirSync(directory);
|
|
42
|
+
if (reset) {
|
|
43
|
+
const count = Object.keys(this.links).length;
|
|
44
|
+
anyhow_1.default.info("LinkManager.load", "Reset", `Cleared ${count} links`);
|
|
45
|
+
for (let watcher of this.watchers) {
|
|
46
|
+
try {
|
|
47
|
+
watcher.close();
|
|
48
|
+
}
|
|
49
|
+
catch (ex) {
|
|
50
|
+
anyhow_1.default.error("LinkManager.load.reset", ex);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
this.links = {};
|
|
54
|
+
this.watchers = [];
|
|
55
|
+
}
|
|
56
|
+
anyhow_1.default.debug("LinkManager.load", `Will load ${files.length} file(s)`);
|
|
57
|
+
for (let filename of files) {
|
|
58
|
+
try {
|
|
59
|
+
const filepath = path_1.default.join(directory, filename);
|
|
60
|
+
await this.loadFile(filepath);
|
|
61
|
+
if (settings.links.autoReload) {
|
|
62
|
+
const watcher = fs_1.default.watch(filepath, async (e, changedfile) => {
|
|
63
|
+
if (changedfile && e == "change") {
|
|
64
|
+
const basename = path_1.default.basename(changedfile, ".json");
|
|
65
|
+
try {
|
|
66
|
+
changedfile = path_1.default.join(directory, changedfile);
|
|
67
|
+
anyhow_1.default.info("LinkManager.load.watch", basename, "Reloading");
|
|
68
|
+
await this.loadFile(changedfile);
|
|
69
|
+
}
|
|
70
|
+
catch (ex) {
|
|
71
|
+
anyhow_1.default.warn("LinkManager.load.watch", basename, "Failed to reload");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
this.watchers.push(watcher);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (fileEx) {
|
|
79
|
+
anyhow_1.default.warn("LinkManager.load", filename, "Failed to load");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const aEntries = Object.entries(settings.links.aliases);
|
|
83
|
+
if (aEntries.length > 0) {
|
|
84
|
+
aEntries.forEach((e) => e[1].forEach((a) => {
|
|
85
|
+
this.aliases[a] = e[0];
|
|
86
|
+
if (settings.links.autoPlural) {
|
|
87
|
+
this.aliases[`${a}s`] = e[0];
|
|
88
|
+
}
|
|
89
|
+
}));
|
|
90
|
+
anyhow_1.default.info("LinkManager.load", `${Object.keys(this.aliases).length} link aliases (${settings.links.autoPlural ? "with" : "no"} plurals)`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (ex) {
|
|
94
|
+
anyhow_1.default.error("LinkManager.load", ex);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
loadFile = async (filepath) => {
|
|
98
|
+
try {
|
|
99
|
+
const source = path_1.default.basename(filepath, ".json").toLowerCase();
|
|
100
|
+
const data = fs_1.default.readFileSync(filepath, { encoding: settings.encoding });
|
|
101
|
+
const entries = Object.entries(JSON.parse(data));
|
|
102
|
+
if (!this.sources.includes(source)) {
|
|
103
|
+
this.sources.push(source);
|
|
104
|
+
}
|
|
105
|
+
for (let [id, countryUrls] of entries) {
|
|
106
|
+
id = id.toLowerCase();
|
|
107
|
+
if (lodash_1.default.isString(countryUrls)) {
|
|
108
|
+
countryUrls = { any: [countryUrls] };
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
const targets = Object.entries(countryUrls);
|
|
112
|
+
for (let [country, urls] of targets) {
|
|
113
|
+
if (!Array.isArray(urls)) {
|
|
114
|
+
countryUrls[country] = [urls];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const urlReducer = (total, arr) => total + arr.length;
|
|
119
|
+
const urlCount = Object.values(countryUrls).reduce(urlReducer, 0);
|
|
120
|
+
const countryCodes = Object.keys(countryUrls).join(", ");
|
|
121
|
+
anyhow_1.default.info("LinkManager.loadFile", id, `${urlCount} links for countries ${countryCodes}`);
|
|
122
|
+
const linkData = {
|
|
123
|
+
id: id,
|
|
124
|
+
source: source,
|
|
125
|
+
urls: countryUrls
|
|
126
|
+
};
|
|
127
|
+
if (id == "default" || id == source) {
|
|
128
|
+
this.links[source] = linkData;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
this.links[`${source}-${id}`] = linkData;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (ex) {
|
|
136
|
+
anyhow_1.default.error("LinkManager.loadFile", filepath, ex);
|
|
137
|
+
throw ex;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
urlFor = (id, country, sources, search) => {
|
|
141
|
+
const sourcesLog = sources?.length > 0 ? sources.join(", ") : "any source";
|
|
142
|
+
if (!sources || sources.length == 0) {
|
|
143
|
+
sources = this.sources;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
if (!id) {
|
|
147
|
+
throw new Error("Missing link ID");
|
|
148
|
+
}
|
|
149
|
+
const foundLinks = [];
|
|
150
|
+
id = id.toLowerCase();
|
|
151
|
+
const alias = this.aliases[id];
|
|
152
|
+
const directLink = this.links[id] || this.links[alias];
|
|
153
|
+
if (directLink) {
|
|
154
|
+
foundLinks.push(directLink);
|
|
155
|
+
}
|
|
156
|
+
else if (sources?.length > 0) {
|
|
157
|
+
for (let source of sources) {
|
|
158
|
+
let sourceLink = this.links[`${source}-${id}`] || this.links[`${source}-${alias}`];
|
|
159
|
+
if (!sourceLink && settings.links.autoPlural) {
|
|
160
|
+
sourceLink = this.links[`${source}-${id}s`] || this.links[`${source}-${alias}s`];
|
|
161
|
+
}
|
|
162
|
+
if (sourceLink) {
|
|
163
|
+
foundLinks.push(sourceLink);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (foundLinks.length == 0 && search) {
|
|
168
|
+
for (let source of sources) {
|
|
169
|
+
const sourceLink = this.links[`${source}-search`];
|
|
170
|
+
if (sourceLink) {
|
|
171
|
+
foundLinks.push(sourceLink);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (foundLinks.length == 0) {
|
|
176
|
+
anyhow_1.default.warn("LinkManager.urlFor", id, "Link not found");
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
const countryAlias = countrymanager_1.default.aliases[country];
|
|
180
|
+
const countryLinks = foundLinks.filter((link) => link.urls[country] || link.urls[countryAlias] || link.urls.any);
|
|
181
|
+
if (countryLinks.length == 0) {
|
|
182
|
+
anyhow_1.default.warn("LinkManager.urlFor", id, `${search ? "Search" : "Link"} not found for country ${country}`);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const singleLink = lodash_1.default.sample(countryLinks);
|
|
186
|
+
const result = {
|
|
187
|
+
url: lodash_1.default.sample(singleLink.urls[country] || singleLink.urls[countryAlias] || singleLink.urls.any),
|
|
188
|
+
source: singleLink.source
|
|
189
|
+
};
|
|
190
|
+
if (search) {
|
|
191
|
+
result.url = result.url.replace("{{query}}", (0, utils_1.getSearchQuery)(id, result.source));
|
|
192
|
+
}
|
|
193
|
+
anyhow_1.default.info("LinkManager.urlFor", id, country, sourcesLog, result.url);
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
catch (ex) {
|
|
197
|
+
anyhow_1.default.error("LinkManager.urlFor", id, country, sourcesLog, ex);
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
exports.LinkManager = LinkManager;
|
|
203
|
+
exports.default = LinkManager.Instance;
|
package/lib/server.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import express = require("express");
|
|
2
|
+
export declare class Server {
|
|
3
|
+
private constructor();
|
|
4
|
+
private static _instance;
|
|
5
|
+
static get Instance(): Server;
|
|
6
|
+
app: express.Express;
|
|
7
|
+
init: (app?: express.Express, basePath?: string) => Promise<void>;
|
|
8
|
+
indexRoute: (req: express.Request, res: express.Response) => Promise<void>;
|
|
9
|
+
notFoundRoute: (req: express.Request, res: express.Response) => Promise<void>;
|
|
10
|
+
linkRoute: (req: express.Request, res: express.Response) => Promise<void>;
|
|
11
|
+
apiListRoute: (req: express.Request, res: express.Response) => Promise<void>;
|
|
12
|
+
apiCheckCredentials: (req: express.Request, res: express.Response) => boolean;
|
|
13
|
+
getClientIP: (req: express.Request) => string;
|
|
14
|
+
getClientCountry: (req: express.Request, ip?: string) => Promise<string>;
|
|
15
|
+
}
|
|
16
|
+
declare const _default: Server;
|
|
17
|
+
export default _default;
|
package/lib/server.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Server = void 0;
|
|
7
|
+
const countrymanager_1 = __importDefault(require("./countrymanager"));
|
|
8
|
+
const linkmanager_1 = __importDefault(require("./linkmanager"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const anyhow_1 = __importDefault(require("anyhow"));
|
|
11
|
+
const jaul_1 = __importDefault(require("jaul"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const express = require("express");
|
|
14
|
+
let settings;
|
|
15
|
+
class Server {
|
|
16
|
+
constructor() { }
|
|
17
|
+
static _instance;
|
|
18
|
+
static get Instance() {
|
|
19
|
+
return this._instance || (this._instance = new this());
|
|
20
|
+
}
|
|
21
|
+
app;
|
|
22
|
+
init = async (app, basePath) => {
|
|
23
|
+
settings = require("setmeup").settings.countryLinkify;
|
|
24
|
+
if (!settings.server.apiKey) {
|
|
25
|
+
throw new Error(`Missing "server.apiKey" on settings`);
|
|
26
|
+
}
|
|
27
|
+
if (!settings.server.apiToken) {
|
|
28
|
+
throw new Error(`Missing "server.apiToken" on settings`);
|
|
29
|
+
}
|
|
30
|
+
if (!app) {
|
|
31
|
+
if (!settings.server.url) {
|
|
32
|
+
throw new Error(`Missing "server.url" on settings`);
|
|
33
|
+
}
|
|
34
|
+
if (!settings.server.port) {
|
|
35
|
+
throw new Error(`The "server.port" must be a valid port number`);
|
|
36
|
+
}
|
|
37
|
+
this.app = express();
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
this.app = app;
|
|
41
|
+
}
|
|
42
|
+
if (!basePath) {
|
|
43
|
+
basePath = "/";
|
|
44
|
+
}
|
|
45
|
+
this.app.use(basePath, express.static(path_1.default.join(__dirname, "../assets")));
|
|
46
|
+
this.app.get(basePath, this.indexRoute);
|
|
47
|
+
this.app.get(`${basePath}404`, this.notFoundRoute);
|
|
48
|
+
this.app.get(`${basePath}${settings.server.apiKey}/list`, this.apiListRoute);
|
|
49
|
+
this.app.get(`${basePath}l/:id`, this.linkRoute);
|
|
50
|
+
this.app.get(`${basePath}s/:search`, this.linkRoute);
|
|
51
|
+
if (!app) {
|
|
52
|
+
this.app.listen(settings.server.port, () => anyhow_1.default.info("Server", `Listeing on port ${settings.server.port}`));
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
indexRoute = async (req, res) => {
|
|
56
|
+
anyhow_1.default.debug("Server.indexRoute", req.originalUrl);
|
|
57
|
+
res.redirect(settings.app.homeUrl || "/404");
|
|
58
|
+
};
|
|
59
|
+
notFoundRoute = async (req, res) => {
|
|
60
|
+
anyhow_1.default.debug("Server.notFoundRoute", req.originalUrl);
|
|
61
|
+
const template = fs_1.default.readFileSync(path_1.default.join(__dirname, "../assets/404.html"), "utf8");
|
|
62
|
+
const linkIds = Object.keys(linkmanager_1.default.links);
|
|
63
|
+
const logoIds = [];
|
|
64
|
+
for (let id of linkIds) {
|
|
65
|
+
const imagePath = path_1.default.join(__dirname, `../assets/images/${id}.png`);
|
|
66
|
+
if (fs_1.default.existsSync(imagePath)) {
|
|
67
|
+
logoIds.push(id);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const aTags = logoIds.map((id) => `<a href="https://links.devv.com/l/${id}"><img src="/images/${id}.png" /></a>`);
|
|
71
|
+
res.send(jaul_1.default.data.replaceTags(template, { logos: aTags.join(" ") }));
|
|
72
|
+
};
|
|
73
|
+
linkRoute = async (req, res) => {
|
|
74
|
+
let ip = this.getClientIP(req);
|
|
75
|
+
let country = await this.getClientCountry(req, ip);
|
|
76
|
+
let countryLog = country;
|
|
77
|
+
if (!country) {
|
|
78
|
+
country = settings.country.default;
|
|
79
|
+
countryLog = `default ${settings.country.default}`;
|
|
80
|
+
}
|
|
81
|
+
const search = req.params.search;
|
|
82
|
+
const linkId = decodeURIComponent(req.params.id || search);
|
|
83
|
+
const sources = req.query.sources ? req.query.sources.toString().split(",") : null;
|
|
84
|
+
const target = linkmanager_1.default.urlFor(linkId, country, sources, search ? true : false);
|
|
85
|
+
if (!target) {
|
|
86
|
+
anyhow_1.default.debug("Server.linkRoute", req.params.id, "404");
|
|
87
|
+
res.redirect("/404");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
anyhow_1.default.debug("Server.linkRoute", req.params.id, target);
|
|
91
|
+
if (req.query.rn != "1") {
|
|
92
|
+
anyhow_1.default.info("Server.linkRoute", req.params.id, `IP: ${ip}`, `Country: ${countryLog}`, target.source, target.url);
|
|
93
|
+
res.redirect(target ? target.url : "/404");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const template = fs_1.default.readFileSync(path_1.default.join(__dirname, "../assets/redir.html"), "utf8");
|
|
97
|
+
const tags = {
|
|
98
|
+
from: req.query.from || "Devv",
|
|
99
|
+
target: target.source,
|
|
100
|
+
url: target.url,
|
|
101
|
+
logo: fs_1.default.existsSync(path_1.default.join(__dirname, `../assets/images/${target.source}.png`)) ? target.source : "nologo"
|
|
102
|
+
};
|
|
103
|
+
anyhow_1.default.info("Server.linkRoute", req.params.id, `IP: ${ip}`, `Country: ${countryLog}`, "Redirection notice", target.source, target.url);
|
|
104
|
+
res.send(jaul_1.default.data.replaceTags(template, tags));
|
|
105
|
+
};
|
|
106
|
+
apiListRoute = async (req, res) => {
|
|
107
|
+
if (!this.apiCheckCredentials(req, res))
|
|
108
|
+
return;
|
|
109
|
+
res.json(linkmanager_1.default.links);
|
|
110
|
+
};
|
|
111
|
+
apiCheckCredentials = (req, res) => {
|
|
112
|
+
const denied = { message: "Access denied" };
|
|
113
|
+
try {
|
|
114
|
+
const header = req.headers["authorization"];
|
|
115
|
+
const token = header && header.indexOf(" ") > 0 ? header.split(" ")[1] : null;
|
|
116
|
+
if (token != settings.server.apiToken) {
|
|
117
|
+
anyhow_1.default.warn("Server.apiCheckCredentials", req.originalUrl, "Access denied");
|
|
118
|
+
res.status(401).json(denied);
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (ex) {
|
|
123
|
+
anyhow_1.default.error("Server.apiCheckCredentials", req.originalUrl, ex);
|
|
124
|
+
res.status(401).json(denied);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
};
|
|
129
|
+
getClientIP = (req) => {
|
|
130
|
+
const xfor = req.headers["x-forwarded-for"];
|
|
131
|
+
if (xfor != null && xfor != "") {
|
|
132
|
+
const ip = xfor.toString().split(",")[0];
|
|
133
|
+
anyhow_1.default.debug("Server.getClientIP", req.originalUrl, `From header: ${ip}`);
|
|
134
|
+
return ip;
|
|
135
|
+
}
|
|
136
|
+
if (req.socket && req.socket.remoteAddress) {
|
|
137
|
+
const ip = req.socket.remoteAddress;
|
|
138
|
+
anyhow_1.default.debug("Server.getClientIP", req.originalUrl, `From socket: ${ip}`);
|
|
139
|
+
return ip;
|
|
140
|
+
}
|
|
141
|
+
anyhow_1.default.debug("Server.getClientIP", req.originalUrl, `From req.ip: ${req.ip}`);
|
|
142
|
+
return req.ip;
|
|
143
|
+
};
|
|
144
|
+
getClientCountry = async (req, ip) => {
|
|
145
|
+
if (!ip)
|
|
146
|
+
ip = this.getClientIP(req);
|
|
147
|
+
if (!ip || ip.includes("127.0.0.1")) {
|
|
148
|
+
return settings.country.default;
|
|
149
|
+
}
|
|
150
|
+
const cfHeader = req.headers["cf-ipcountry"];
|
|
151
|
+
if (cfHeader) {
|
|
152
|
+
anyhow_1.default.debug("Server.getClientCountry", `IP ${ip}`, `From CF header: ${cfHeader}`);
|
|
153
|
+
return cfHeader.toString().toLowerCase();
|
|
154
|
+
}
|
|
155
|
+
const ipCountry = await countrymanager_1.default.getForIP(ip);
|
|
156
|
+
if (ipCountry) {
|
|
157
|
+
anyhow_1.default.debug("Server.getClientCountry", `IP ${ip}`, `From IP: ${ipCountry}`);
|
|
158
|
+
return ipCountry;
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
exports.Server = Server;
|
|
164
|
+
exports.default = Server.Instance;
|
package/lib/start.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/start.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
if (!process.env.NODE_ENV) {
|
|
7
|
+
process.env.NODE_ENV = "development";
|
|
8
|
+
}
|
|
9
|
+
const anyhow_1 = __importDefault(require("anyhow"));
|
|
10
|
+
anyhow_1.default.setup("console");
|
|
11
|
+
anyhow_1.default.setOptions({
|
|
12
|
+
appName: "Country Linkify",
|
|
13
|
+
timestamp: false,
|
|
14
|
+
levelOnConsole: true,
|
|
15
|
+
preprocessors: ["friendlyErrors", "maskSecrets"]
|
|
16
|
+
});
|
|
17
|
+
if (process.env.NODE_ENV == "production" && process.env.JSON_LOGGING) {
|
|
18
|
+
const consoleLog = (level, message) => {
|
|
19
|
+
level = level.toUpperCase();
|
|
20
|
+
if (level == "WARN")
|
|
21
|
+
level = "WARNING";
|
|
22
|
+
console.log(JSON.stringify({ severity: level, message: message }));
|
|
23
|
+
};
|
|
24
|
+
const gcloudLogging = {
|
|
25
|
+
name: "gcloud",
|
|
26
|
+
log: consoleLog
|
|
27
|
+
};
|
|
28
|
+
anyhow_1.default.setOptions({ levelOnConsole: false });
|
|
29
|
+
anyhow_1.default.info("CountryLinkify.startup", "Switching to JSON logging now");
|
|
30
|
+
anyhow_1.default.setup(gcloudLogging);
|
|
31
|
+
}
|
|
32
|
+
let terminating = false;
|
|
33
|
+
const shutdown = async (code) => {
|
|
34
|
+
if (terminating)
|
|
35
|
+
return;
|
|
36
|
+
terminating = true;
|
|
37
|
+
if (!code)
|
|
38
|
+
code = 0;
|
|
39
|
+
anyhow_1.default.warn("CountryLinkify.shutdown", `Code ${code}`, "Terminating the service now...");
|
|
40
|
+
process.exit();
|
|
41
|
+
};
|
|
42
|
+
process.on("SIGINT", shutdown);
|
|
43
|
+
process.on("SIGTERM", shutdown);
|
|
44
|
+
const index_1 = __importDefault(require("./index"));
|
|
45
|
+
(0, index_1.default)();
|
package/lib/types.d.ts
ADDED
package/lib/types.js
ADDED
package/lib/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const getSearchQuery: (id: string, source: string) => string;
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getSearchQuery = void 0;
|
|
4
|
+
const getSearchQuery = (id, source) => {
|
|
5
|
+
if (source == "aliexpress") {
|
|
6
|
+
return id.replace(/[^a-zA-Z ]/g, "").replace(/ /g, "-");
|
|
7
|
+
}
|
|
8
|
+
if (source == "decathlon" || source == "ribble") {
|
|
9
|
+
return encodeURIComponent(encodeURIComponent(id));
|
|
10
|
+
}
|
|
11
|
+
return encodeURIComponent(id);
|
|
12
|
+
};
|
|
13
|
+
exports.getSearchQuery = getSearchQuery;
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "country-linkify",
|
|
3
|
+
"version": "24.482.11825",
|
|
4
|
+
"description": "Simple service to redirect links based on the client's country.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bit.ly",
|
|
7
|
+
"country",
|
|
8
|
+
"linkify",
|
|
9
|
+
"redirect"
|
|
10
|
+
],
|
|
11
|
+
"author": "Igor Ramadas <igor@devv.com>",
|
|
12
|
+
"homepage": "https://country-linkify.devv.com",
|
|
13
|
+
"subdomain": "country-linkify",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"main": "lib/index.js",
|
|
16
|
+
"types": "lib/index.d.ts",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/igoramadas/country-linkify.git"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">= 16.0"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"prepublishOnly": "./node_modules/.bin/tsc --removeComments",
|
|
26
|
+
"docs": "make docs",
|
|
27
|
+
"start": "node lib/start.js"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"anyhow": "^3.4.0",
|
|
31
|
+
"bent": "^7.3.12",
|
|
32
|
+
"bitecache": "^1.3.3",
|
|
33
|
+
"chalk": "^4.1.2",
|
|
34
|
+
"express": "^5.0.1",
|
|
35
|
+
"jaul": "^1.7.1",
|
|
36
|
+
"lodash": "^4.17.21",
|
|
37
|
+
"setmeup": "^1.9.3"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/express": "^5.0.0",
|
|
41
|
+
"@types/lodash": "^4.17.13",
|
|
42
|
+
"@types/node": "^22.10.0",
|
|
43
|
+
"typescript": "^5.7.2"
|
|
44
|
+
},
|
|
45
|
+
"prettier": {
|
|
46
|
+
"trailingComma": "none",
|
|
47
|
+
"tabWidth": 4,
|
|
48
|
+
"semi": false,
|
|
49
|
+
"singleQuote": false,
|
|
50
|
+
"arrowParens": "always",
|
|
51
|
+
"bracketSpacing": false,
|
|
52
|
+
"printWidth": 250
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// COUNTRY LINKIFY SETTINGS (DEVELOPMENT)
|
|
2
|
+
|
|
3
|
+
{
|
|
4
|
+
"countryLinkify": {
|
|
5
|
+
// Debugging is active on dev.
|
|
6
|
+
"debug": true,
|
|
7
|
+
// Server options on dev.
|
|
8
|
+
"server": {
|
|
9
|
+
"url": "http://linkify.local:3579/",
|
|
10
|
+
// API identifier, by default it's "api" meaning Admin API endpoints will be available at /api/.
|
|
11
|
+
"apiKey": "api",
|
|
12
|
+
// Bearer token to be passed for authentication.
|
|
13
|
+
"apiToken": "123",
|
|
14
|
+
// Server port on dev.
|
|
15
|
+
"port": 3579
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
package/settings.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// COUNTRY LINKIFY DEFAULT SETTINGS
|
|
2
|
+
|
|
3
|
+
{
|
|
4
|
+
"countryLinkify": {
|
|
5
|
+
"app": {
|
|
6
|
+
// Homepage should redirect to this URL. If set to "null",
|
|
7
|
+
// it will display all links with logos instead.
|
|
8
|
+
"homeUrl": null,
|
|
9
|
+
// Application title.
|
|
10
|
+
"title": "Country Linkify"
|
|
11
|
+
},
|
|
12
|
+
"country": {
|
|
13
|
+
// Cache country for IP results for up to 30 minutes.
|
|
14
|
+
"cacheDuration": 900,
|
|
15
|
+
// Default country.
|
|
16
|
+
"default": "us"
|
|
17
|
+
// If country can't be determined or there's no link for the user's country, you can add
|
|
18
|
+
// aliases / target countries here. For example, the following will set Spain as default
|
|
19
|
+
// for Portuguese users, Germany for Austrian and Swiss users, and United Kingdom for Great Britain.
|
|
20
|
+
// "es": ["pt"]
|
|
21
|
+
// "de": ["at", "ch"]
|
|
22
|
+
// "uk": ["gb"]
|
|
23
|
+
},
|
|
24
|
+
"links": {
|
|
25
|
+
// Default folder is /links on the application root. This can also be set to an absolute path.
|
|
26
|
+
"path": "./links",
|
|
27
|
+
// Auto reload links when the JSON files change. Set to false to disable.
|
|
28
|
+
"autoReload": true,
|
|
29
|
+
// Also check for plural names when matching link IDs.
|
|
30
|
+
"autoPlural": true,
|
|
31
|
+
// Link aliases.
|
|
32
|
+
// Sample: "light": ["led", "bulb"]
|
|
33
|
+
"aliases": {}
|
|
34
|
+
},
|
|
35
|
+
"server": {
|
|
36
|
+
// Base server URL, can be left as null if using as part of another website / app.
|
|
37
|
+
// Must be set when running in standalone mode.
|
|
38
|
+
"url": null,
|
|
39
|
+
// Server port, required if the url above is also set.
|
|
40
|
+
"port": null,
|
|
41
|
+
// Admin API key used as part of the API endpoint. For example if you set this to "my-api"
|
|
42
|
+
// then the Admin API will be available at /my-api/.
|
|
43
|
+
"apiKey": null,
|
|
44
|
+
// Bearer token to be passed on Admin API requests.
|
|
45
|
+
"apiToken": null
|
|
46
|
+
},
|
|
47
|
+
// Name of the bitecache collection.
|
|
48
|
+
"cacheName": "ip-country",
|
|
49
|
+
// Default encoding is set to UTF-8.
|
|
50
|
+
"encoding": "utf8"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// COUNTRY LINKIFY SETTINGS (PRODUCTION SAMPLE)
|
|
2
|
+
|
|
3
|
+
{
|
|
4
|
+
"countryLinkify": {
|
|
5
|
+
"app": {
|
|
6
|
+
// Homepage should redirect to devv.com.
|
|
7
|
+
"homeUrl": "https://devv.com/",
|
|
8
|
+
// Application title.
|
|
9
|
+
"title": "My Links"
|
|
10
|
+
},
|
|
11
|
+
"country": {
|
|
12
|
+
// Set default country to Brazil, and add some aliases.
|
|
13
|
+
"default": "br",
|
|
14
|
+
"es": ["mt", "pt"],
|
|
15
|
+
"de": ["at", "be", "ch", "cz", "dk", "ee", "gi", "fi", "fr", "hu", "ie", "it", "nl", "no", "pl", "se", "sk"],
|
|
16
|
+
"uk": ["gb"],
|
|
17
|
+
"us": ["ca"]
|
|
18
|
+
},
|
|
19
|
+
"links": {
|
|
20
|
+
// Cycling aliases.
|
|
21
|
+
"aliases": {
|
|
22
|
+
"bike": ["bicycle"],
|
|
23
|
+
"tire": ["tyre"]
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
// Server options on prod.
|
|
27
|
+
"server": {
|
|
28
|
+
"url": "https://links.devv.com/",
|
|
29
|
+
"port": 8080,
|
|
30
|
+
// Admin API base endpoint should be https://links.devv.com/secret-api/.
|
|
31
|
+
"apiKey": "secret-api",
|
|
32
|
+
// Admin API bearer token.
|
|
33
|
+
"apiToken": "some-token"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|