user-agents 1.1.13 → 2.0.0-alpha.10
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/dist/index.cjs +3 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +40 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/user-agents.json +162098 -0
- package/package.json +53 -29
- package/src/{gunzip-data.js → gunzip-data.ts} +4 -5
- package/src/index.ts +4 -0
- package/src/{update-data.js → update-data.ts} +36 -40
- package/src/user-agent.ts +217 -0
- package/.babelrc +0 -31
- package/.circleci/config.yml +0 -210
- package/.clabot +0 -7
- package/.eslintrc +0 -15
- package/.github/pull_request_template.md +0 -7
- package/CLA.md +0 -126
- package/CONTRIBUTING.md +0 -8
- package/media/ycombinator.png +0 -0
- package/src/index.js +0 -4
- package/src/user-agent.js +0 -154
- package/src/user-agents.json.gz +0 -0
- package/test/test-user-agent.js +0 -113
- package/webpack.config.js +0 -49
package/package.json
CHANGED
|
@@ -1,47 +1,71 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "user-agents",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-alpha.10",
|
|
4
4
|
"description": "A JavaScript library for generating random user agents. ",
|
|
5
|
-
"main": "dist/index.
|
|
6
|
-
"
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+ssh://git@github.com/intoli/user-agents.git"
|
|
10
|
+
},
|
|
7
11
|
"author": "Intoli, LLC <contact@intoli.com>",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist/",
|
|
14
|
+
"src/**/*",
|
|
15
|
+
"!src/user-agents.json",
|
|
16
|
+
"!src/user-agents.json.gz"
|
|
17
|
+
],
|
|
18
|
+
"exports": {
|
|
19
|
+
"import": "./dist/index.js",
|
|
20
|
+
"require": "./dist/index.cjs"
|
|
21
|
+
},
|
|
8
22
|
"license": "BSD-2-Clause",
|
|
9
23
|
"private": false,
|
|
24
|
+
"type": "module",
|
|
10
25
|
"scripts": {
|
|
11
|
-
"build": "
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"postbuild": "yarn gunzip-data && cp src/user-agents.json dist/",
|
|
28
|
+
"gunzip-data": "node --loader ts-node/esm src/gunzip-data.ts src/user-agents.json.gz",
|
|
29
|
+
"lint": "eslint src/ && prettier --check src/",
|
|
30
|
+
"test": "NODE_ENV=testing mocha --exit --require @babel/register --extensions '.ts, .js'",
|
|
31
|
+
"update-data": "node --loader ts-node/esm src/update-data.ts src/user-agents.json.gz"
|
|
16
32
|
},
|
|
17
33
|
"devDependencies": {
|
|
18
|
-
"@babel/cli": "^7.
|
|
19
|
-
"@babel/core": "^7.
|
|
20
|
-
"@babel/
|
|
21
|
-
"@babel/
|
|
22
|
-
"@babel/plugin-
|
|
23
|
-
"@babel/plugin-
|
|
24
|
-
"@babel/
|
|
25
|
-
"@babel/preset-
|
|
26
|
-
"@babel/register": "^7.
|
|
27
|
-
"
|
|
34
|
+
"@babel/cli": "^7.23.0",
|
|
35
|
+
"@babel/core": "^7.23.2",
|
|
36
|
+
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
|
37
|
+
"@babel/plugin-proposal-object-rest-spread": "^7.20.7",
|
|
38
|
+
"@babel/plugin-syntax-import-assertions": "^7.22.5",
|
|
39
|
+
"@babel/plugin-transform-classes": "^7.22.15",
|
|
40
|
+
"@babel/preset-env": "^7.23.2",
|
|
41
|
+
"@babel/preset-typescript": "^7.23.2",
|
|
42
|
+
"@babel/register": "^7.22.15",
|
|
43
|
+
"@types/lodash.clonedeep": "^4.5.8",
|
|
44
|
+
"@types/ua-parser-js": "^0.7.38",
|
|
45
|
+
"@typescript-eslint/eslint-plugin": "^6.9.0",
|
|
46
|
+
"@typescript-eslint/parser": "^6.9.0",
|
|
47
|
+
"babel-loader": "^9.1.3",
|
|
28
48
|
"babel-plugin-add-module-exports": "^1.0.4",
|
|
29
49
|
"babel-preset-power-assert": "^3.0.0",
|
|
30
50
|
"dynamoose": "^3.2.1",
|
|
31
|
-
"
|
|
32
|
-
"eslint
|
|
33
|
-
"eslint-
|
|
34
|
-
"eslint-
|
|
51
|
+
"esbuild": "^0.19.5",
|
|
52
|
+
"eslint": "^8.52.0",
|
|
53
|
+
"eslint-config-airbnb": "^19.0.4",
|
|
54
|
+
"eslint-config-prettier": "^9.0.0",
|
|
55
|
+
"eslint-plugin-import": "^2.29.0",
|
|
35
56
|
"fast-json-stable-stringify": "^2.1.0",
|
|
36
|
-
"imports-loader": "^
|
|
57
|
+
"imports-loader": "^4.0.1",
|
|
37
58
|
"isbot": "^3.7.0",
|
|
38
|
-
"mocha": "^
|
|
59
|
+
"mocha": "^10.2.0",
|
|
39
60
|
"power-assert": "^1.6.1",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
61
|
+
"prettier": "^3.0.3",
|
|
62
|
+
"random": "^4.1.0",
|
|
63
|
+
"source-map-support": "^0.5.21",
|
|
64
|
+
"ts-node": "^10.9.1",
|
|
65
|
+
"tsup": "^7.2.0",
|
|
66
|
+
"typescript": "^5.2.2",
|
|
67
|
+
"typescript-language-server": "^4.0.0",
|
|
68
|
+
"ua-parser-js": "^1.0.36"
|
|
45
69
|
},
|
|
46
70
|
"dependencies": {
|
|
47
71
|
"lodash.clonedeep": "^4.5.0"
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
import { argv } from 'process';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
2
4
|
import { gunzipSync } from 'zlib';
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
const gunzipData = (inputFilename) => {
|
|
6
|
+
const gunzipData = (inputFilename?: string) => {
|
|
6
7
|
if (!inputFilename || !inputFilename.endsWith('.gz')) {
|
|
7
8
|
throw new Error('Filename must be specified and end with `.gz` for gunzipping.');
|
|
8
9
|
}
|
|
@@ -12,11 +13,9 @@ const gunzipData = (inputFilename) => {
|
|
|
12
13
|
fs.writeFileSync(outputFilename, data);
|
|
13
14
|
};
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
if (!module.parent) {
|
|
16
|
+
if (fileURLToPath(import.meta.url) === argv[1]) {
|
|
17
17
|
const inputFilename = process.argv[2];
|
|
18
18
|
gunzipData(inputFilename);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
21
|
export default gunzipData;
|
package/src/index.ts
ADDED
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
/* eslint-disable import/no-extraneous-dependencies */
|
|
2
|
-
import fs from
|
|
3
|
-
import {
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { argv } from 'process';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { gzipSync } from 'zlib';
|
|
4
6
|
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
7
|
+
import dynamoose from 'dynamoose';
|
|
8
|
+
import { Item } from 'dynamoose/dist/Item.js';
|
|
9
|
+
import stableStringify from 'fast-json-stable-stringify';
|
|
10
|
+
import isbot from 'isbot';
|
|
11
|
+
import random from 'random';
|
|
12
|
+
import UAParser from 'ua-parser-js';
|
|
13
|
+
|
|
14
|
+
import { UserAgentData } from './user-agent';
|
|
10
15
|
|
|
11
16
|
const ddb = new dynamoose.aws.ddb.DynamoDB({
|
|
12
|
-
region:
|
|
17
|
+
region: 'us-east-2',
|
|
13
18
|
});
|
|
14
19
|
dynamoose.aws.ddb.set(ddb);
|
|
15
20
|
|
|
16
|
-
const SubmissionModel = dynamoose.model
|
|
17
|
-
|
|
21
|
+
const SubmissionModel = dynamoose.model<
|
|
22
|
+
{
|
|
23
|
+
ip: string;
|
|
24
|
+
profile: { [key: string]: unknown };
|
|
25
|
+
} & UserAgentData &
|
|
26
|
+
Item
|
|
27
|
+
>(
|
|
28
|
+
'userAgentsAnalyticsSubmissionTable',
|
|
18
29
|
new dynamoose.Schema(
|
|
19
30
|
{
|
|
20
31
|
id: {
|
|
@@ -25,8 +36,8 @@ const SubmissionModel = dynamoose.model(
|
|
|
25
36
|
profile: Object,
|
|
26
37
|
},
|
|
27
38
|
{
|
|
28
|
-
saveUnknown: [
|
|
29
|
-
timestamps: { createdAt:
|
|
39
|
+
saveUnknown: ['profile.**'],
|
|
40
|
+
timestamps: { createdAt: 'timestamp', updatedAt: undefined },
|
|
30
41
|
},
|
|
31
42
|
),
|
|
32
43
|
{ create: false, update: false },
|
|
@@ -37,13 +48,11 @@ const getUserAgentTable = async (limit = 1e4) => {
|
|
|
37
48
|
|
|
38
49
|
// Scan through all recent profiles keeping track of the count of each.
|
|
39
50
|
let lastKey = null;
|
|
40
|
-
const countsByProfile = {};
|
|
41
|
-
|
|
42
|
-
let uniqueCount = 0;
|
|
43
|
-
let ipAddressAlreadySeen = {};
|
|
51
|
+
const countsByProfile: { [stringifiedProfile: string]: number } = {};
|
|
52
|
+
const ipAddressAlreadySeen: { [ipAddress: string]: boolean } = {};
|
|
44
53
|
do {
|
|
45
54
|
const scan = SubmissionModel.scan(
|
|
46
|
-
new dynamoose.Condition().filter(
|
|
55
|
+
new dynamoose.Condition().filter('timestamp').gt(minimumTimestamp),
|
|
47
56
|
);
|
|
48
57
|
if (lastKey) {
|
|
49
58
|
scan.startAt(lastKey);
|
|
@@ -62,10 +71,8 @@ const getUserAgentTable = async (limit = 1e4) => {
|
|
|
62
71
|
const stringifiedProfile = stableStringify(profile);
|
|
63
72
|
if (!countsByProfile[stringifiedProfile]) {
|
|
64
73
|
countsByProfile[stringifiedProfile] = 0;
|
|
65
|
-
uniqueCount += 1;
|
|
66
74
|
}
|
|
67
75
|
countsByProfile[stringifiedProfile] += 1;
|
|
68
|
-
totalCount += 1;
|
|
69
76
|
});
|
|
70
77
|
|
|
71
78
|
lastKey = response.lastKey;
|
|
@@ -76,46 +83,37 @@ const getUserAgentTable = async (limit = 1e4) => {
|
|
|
76
83
|
Object.entries(countsByProfile).forEach(([stringifiedProfile, count]) => {
|
|
77
84
|
const unnormalizedWeight =
|
|
78
85
|
Array(2 * count)
|
|
79
|
-
.fill()
|
|
86
|
+
.fill(undefined)
|
|
80
87
|
.reduce((sum) => sum + n()() ** 2, 0) / 2;
|
|
81
88
|
countsByProfile[stringifiedProfile] = unnormalizedWeight;
|
|
82
89
|
});
|
|
83
90
|
|
|
84
91
|
// Accumulate the profiles and add/remove a few properties to match the historical format.
|
|
85
|
-
const profiles = [];
|
|
86
|
-
|
|
92
|
+
const profiles: UserAgentData[] = [];
|
|
93
|
+
Object.entries(countsByProfile).forEach(([stringifiedProfile, weight]) => {
|
|
87
94
|
if (countsByProfile.hasOwnProperty(stringifiedProfile)) {
|
|
88
95
|
const profile = JSON.parse(stringifiedProfile);
|
|
89
|
-
profile.weight =
|
|
96
|
+
profile.weight = weight;
|
|
90
97
|
delete profile.sessionId;
|
|
91
98
|
|
|
92
|
-
// Deleting these because they weren't in the old format, but we should leave them in...
|
|
93
|
-
delete profile.language;
|
|
94
|
-
delete profile.oscpu;
|
|
95
|
-
|
|
96
99
|
// Find the device category.
|
|
97
100
|
const parser = new UAParser(profile.userAgent);
|
|
98
101
|
const device = parser.getDevice();
|
|
99
102
|
// Sketchy, but I validated this on historical data and it is a 100% match.
|
|
100
103
|
profile.deviceCategory =
|
|
101
|
-
{ mobile:
|
|
102
|
-
`${device.type}`
|
|
103
|
-
] ?? "desktop";
|
|
104
|
+
{ mobile: 'mobile', tablet: 'tablet', undefined: 'desktop' }[`${device.type}`] ?? 'desktop';
|
|
104
105
|
|
|
105
106
|
profiles.push(profile);
|
|
106
107
|
delete countsByProfile[stringifiedProfile];
|
|
107
108
|
}
|
|
108
|
-
}
|
|
109
|
+
});
|
|
109
110
|
|
|
110
111
|
// Sort by descending weight.
|
|
111
112
|
profiles.sort((a, b) => b.weight - a.weight);
|
|
112
113
|
|
|
113
114
|
// Apply the count limit and normalize the weights.
|
|
114
115
|
profiles.splice(limit);
|
|
115
|
-
const totalWeight = profiles.reduce(
|
|
116
|
-
(total, profile) => total + profile.weight,
|
|
117
|
-
0,
|
|
118
|
-
);
|
|
116
|
+
const totalWeight = profiles.reduce((total, profile) => total + profile.weight, 0);
|
|
119
117
|
profiles.forEach((profile) => {
|
|
120
118
|
profile.weight /= totalWeight;
|
|
121
119
|
});
|
|
@@ -123,18 +121,16 @@ const getUserAgentTable = async (limit = 1e4) => {
|
|
|
123
121
|
return profiles;
|
|
124
122
|
};
|
|
125
123
|
|
|
126
|
-
if (
|
|
124
|
+
if (fileURLToPath(import.meta.url) === argv[1]) {
|
|
127
125
|
const filename = process.argv[2];
|
|
128
126
|
if (!filename) {
|
|
129
|
-
throw new Error(
|
|
130
|
-
"An output filename must be passed as an argument to the command.",
|
|
131
|
-
);
|
|
127
|
+
throw new Error('An output filename must be passed as an argument to the command.');
|
|
132
128
|
}
|
|
133
129
|
getUserAgentTable()
|
|
134
130
|
.then(async (userAgents) => {
|
|
135
131
|
const stringifiedUserAgents = JSON.stringify(userAgents, null, 2);
|
|
136
132
|
// Compress the content if the extension ends with `.gz`.
|
|
137
|
-
const content = filename.endsWith(
|
|
133
|
+
const content = filename.endsWith('.gz')
|
|
138
134
|
? gzipSync(stringifiedUserAgents)
|
|
139
135
|
: stringifiedUserAgents;
|
|
140
136
|
fs.writeFileSync(filename, content);
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import cloneDeep from 'lodash.clonedeep';
|
|
2
|
+
|
|
3
|
+
import untypedUserAgents from './user-agents.json' assert { type: 'json' };
|
|
4
|
+
|
|
5
|
+
const userAgents: UserAgentData[] = untypedUserAgents as UserAgentData[];
|
|
6
|
+
|
|
7
|
+
type NestedValueOf<T> = T extends object ? T[keyof T] | NestedValueOf<T[keyof T]> : T;
|
|
8
|
+
|
|
9
|
+
export type Filter<T extends UserAgentData | NestedValueOf<UserAgentData> = UserAgentData> =
|
|
10
|
+
| ((parentObject: T) => boolean)
|
|
11
|
+
| RegExp
|
|
12
|
+
| Array<Filter<T>>
|
|
13
|
+
| { [key: string]: Filter<T> }
|
|
14
|
+
| string;
|
|
15
|
+
|
|
16
|
+
export interface UserAgentData {
|
|
17
|
+
appName: 'Netscape';
|
|
18
|
+
connection: {
|
|
19
|
+
downlink: number;
|
|
20
|
+
effectiveType: '3g' | '4g';
|
|
21
|
+
rtt: number;
|
|
22
|
+
downlinkMax?: number | null;
|
|
23
|
+
type?: 'cellular' | 'wifi';
|
|
24
|
+
};
|
|
25
|
+
language?: string | null;
|
|
26
|
+
oscpu?: string | null;
|
|
27
|
+
platform:
|
|
28
|
+
| 'iPad'
|
|
29
|
+
| 'iPhone'
|
|
30
|
+
| 'Linux aarch64'
|
|
31
|
+
| 'Linux armv81'
|
|
32
|
+
| 'Linux armv8l'
|
|
33
|
+
| 'Linux x86_64'
|
|
34
|
+
| 'MacIntel'
|
|
35
|
+
| 'Win32';
|
|
36
|
+
pluginsLength: number;
|
|
37
|
+
screenHeight: number;
|
|
38
|
+
screenWidth: number;
|
|
39
|
+
userAgent: string;
|
|
40
|
+
vendor: 'Apple Computer, Inc.' | 'Google Inc.' | '';
|
|
41
|
+
weight: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
declare module './user-agent' {
|
|
45
|
+
export interface UserAgent extends Readonly<UserAgentData> {
|
|
46
|
+
readonly cumulativeWeightIndexPairs: Array<[number, number]>;
|
|
47
|
+
readonly data: UserAgentData;
|
|
48
|
+
(): UserAgent;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Normalizes the total weight to 1 and constructs a cumulative distribution.
|
|
53
|
+
const makeCumulativeWeightIndexPairs = (
|
|
54
|
+
weightIndexPairs: Array<[number, number]>,
|
|
55
|
+
): Array<[number, number]> => {
|
|
56
|
+
const totalWeight = weightIndexPairs.reduce((sum, [weight]) => sum + weight, 0);
|
|
57
|
+
let sum = 0;
|
|
58
|
+
return weightIndexPairs.map(([weight, index]) => {
|
|
59
|
+
sum += weight / totalWeight;
|
|
60
|
+
return [sum, index];
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Precompute these so that we can quickly generate unfiltered user agents.
|
|
65
|
+
const defaultWeightIndexPairs: Array<[number, number]> = userAgents.map(({ weight }, index) => [
|
|
66
|
+
weight,
|
|
67
|
+
index,
|
|
68
|
+
]);
|
|
69
|
+
const defaultCumulativeWeightIndexPairs = makeCumulativeWeightIndexPairs(defaultWeightIndexPairs);
|
|
70
|
+
|
|
71
|
+
// Turn the various filter formats into a single filter function that acts on raw user agents.
|
|
72
|
+
const constructFilter = <T extends UserAgentData | NestedValueOf<UserAgentData>>(
|
|
73
|
+
filters: Filter<T>,
|
|
74
|
+
accessor: (parentObject: T) => T | NestedValueOf<T> = (parentObject: T): T => parentObject,
|
|
75
|
+
): ((profile: T) => boolean) => {
|
|
76
|
+
// WARNING: This type and a lot of the types in here are wrong, but I can't get TypeScript to
|
|
77
|
+
// resolve things correctly so this will have to do for now.
|
|
78
|
+
let childFilters: Array<(parentObject: T) => boolean>;
|
|
79
|
+
if (typeof filters === 'function') {
|
|
80
|
+
childFilters = [filters];
|
|
81
|
+
} else if (filters instanceof RegExp) {
|
|
82
|
+
childFilters = [
|
|
83
|
+
(value: T | NestedValueOf<T>) =>
|
|
84
|
+
typeof value === 'object' && value && 'userAgent' in value && value.userAgent
|
|
85
|
+
? filters.test(value.userAgent)
|
|
86
|
+
: filters.test(value as string),
|
|
87
|
+
];
|
|
88
|
+
} else if (filters instanceof Array) {
|
|
89
|
+
childFilters = filters.map((childFilter) => constructFilter(childFilter));
|
|
90
|
+
} else if (typeof filters === 'object') {
|
|
91
|
+
childFilters = Object.entries(filters).map(([key, valueFilter]) =>
|
|
92
|
+
constructFilter(
|
|
93
|
+
valueFilter as Filter<T>,
|
|
94
|
+
(parentObject: T): T | NestedValueOf<T> =>
|
|
95
|
+
(parentObject as unknown as { [key: string]: NestedValueOf<T> })[key] as NestedValueOf<T>,
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
} else {
|
|
99
|
+
childFilters = [
|
|
100
|
+
(value: T | NestedValueOf<T>) =>
|
|
101
|
+
typeof value === 'object' && value && 'userAgent' in value && value.userAgent
|
|
102
|
+
? filters === value.userAgent
|
|
103
|
+
: filters === value,
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return (parentObject: T) => {
|
|
108
|
+
try {
|
|
109
|
+
const value = accessor(parentObject);
|
|
110
|
+
return childFilters.every((childFilter) => childFilter(value as T));
|
|
111
|
+
} catch (error) {
|
|
112
|
+
// This happens when a user-agent lacks a nested property.
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Construct normalized cumulative weight index pairs given the filters.
|
|
119
|
+
const constructCumulativeWeightIndexPairsFromFilters = (
|
|
120
|
+
filters?: Filter<UserAgentData>,
|
|
121
|
+
): Array<[number, number]> => {
|
|
122
|
+
if (!filters) {
|
|
123
|
+
return defaultCumulativeWeightIndexPairs;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const filter = constructFilter(filters);
|
|
127
|
+
|
|
128
|
+
const weightIndexPairs: Array<[number, number]> = [];
|
|
129
|
+
userAgents.forEach((rawUserAgent, index) => {
|
|
130
|
+
if (filter(rawUserAgent)) {
|
|
131
|
+
weightIndexPairs.push([rawUserAgent.weight, index]);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
return makeCumulativeWeightIndexPairs(weightIndexPairs);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const setCumulativeWeightIndexPairs = (
|
|
138
|
+
userAgent: UserAgent,
|
|
139
|
+
cumulativeWeightIndexPairs: Array<[number, number]>,
|
|
140
|
+
) => {
|
|
141
|
+
Object.defineProperty(userAgent, 'cumulativeWeightIndexPairs', {
|
|
142
|
+
configurable: true,
|
|
143
|
+
enumerable: false,
|
|
144
|
+
writable: false,
|
|
145
|
+
value: cumulativeWeightIndexPairs,
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export class UserAgent extends Function {
|
|
150
|
+
constructor(filters?: Filter) {
|
|
151
|
+
super();
|
|
152
|
+
setCumulativeWeightIndexPairs(this, constructCumulativeWeightIndexPairsFromFilters(filters));
|
|
153
|
+
if (this.cumulativeWeightIndexPairs.length === 0) {
|
|
154
|
+
throw new Error('No user agents matched your filters.');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.randomize();
|
|
158
|
+
|
|
159
|
+
// eslint-disable-next-line no-constructor-return
|
|
160
|
+
return new Proxy(this, {
|
|
161
|
+
apply: () => this.random(),
|
|
162
|
+
get: (target, property, receiver) => {
|
|
163
|
+
const dataCandidate =
|
|
164
|
+
target.data &&
|
|
165
|
+
typeof property === 'string' &&
|
|
166
|
+
Object.prototype.hasOwnProperty.call(target.data, property) &&
|
|
167
|
+
Object.prototype.propertyIsEnumerable.call(target.data, property);
|
|
168
|
+
if (dataCandidate) {
|
|
169
|
+
const value = target.data[property as keyof UserAgentData];
|
|
170
|
+
if (value !== undefined) {
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return Reflect.get(target, property, receiver);
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
static random = (filters: Filter) => {
|
|
181
|
+
try {
|
|
182
|
+
return new UserAgent(filters);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
//
|
|
189
|
+
// Standard Object Methods
|
|
190
|
+
//
|
|
191
|
+
|
|
192
|
+
[Symbol.toPrimitive] = (): string => this.data.userAgent;
|
|
193
|
+
|
|
194
|
+
toString = (): string => this.data.userAgent;
|
|
195
|
+
|
|
196
|
+
random = (): UserAgent => {
|
|
197
|
+
const userAgent = new UserAgent();
|
|
198
|
+
setCumulativeWeightIndexPairs(userAgent, this.cumulativeWeightIndexPairs);
|
|
199
|
+
userAgent.randomize();
|
|
200
|
+
return userAgent;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
randomize = (): void => {
|
|
204
|
+
// Find a random raw random user agent.
|
|
205
|
+
const randomNumber = Math.random();
|
|
206
|
+
const [, index] =
|
|
207
|
+
this.cumulativeWeightIndexPairs.find(
|
|
208
|
+
([cumulativeWeight]) => cumulativeWeight > randomNumber,
|
|
209
|
+
) ?? [];
|
|
210
|
+
if (index == null) {
|
|
211
|
+
throw new Error('Error finding a random user agent.');
|
|
212
|
+
}
|
|
213
|
+
const rawUserAgent = userAgents[index];
|
|
214
|
+
|
|
215
|
+
(this as { data: UserAgentData }).data = cloneDeep(rawUserAgent);
|
|
216
|
+
};
|
|
217
|
+
}
|
package/.babelrc
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"env": {
|
|
3
|
-
"testing": {
|
|
4
|
-
"presets": [
|
|
5
|
-
["@babel/preset-env", {
|
|
6
|
-
"targets": {
|
|
7
|
-
"node": "current"
|
|
8
|
-
}
|
|
9
|
-
}],
|
|
10
|
-
"power-assert"
|
|
11
|
-
]
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
"presets": [["@babel/preset-env", {
|
|
15
|
-
"modules": "commonjs",
|
|
16
|
-
"targets": {
|
|
17
|
-
"browsers": [
|
|
18
|
-
"last 2 chrome versions",
|
|
19
|
-
"last 2 firefox versions",
|
|
20
|
-
],
|
|
21
|
-
"node": "6.10"
|
|
22
|
-
}
|
|
23
|
-
}]],
|
|
24
|
-
"plugins": [
|
|
25
|
-
"@babel/plugin-proposal-class-properties",
|
|
26
|
-
"@babel/plugin-proposal-object-rest-spread",
|
|
27
|
-
"@babel/plugin-transform-classes",
|
|
28
|
-
"babel-plugin-add-module-exports"
|
|
29
|
-
]
|
|
30
|
-
}
|
|
31
|
-
|