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