@mathieuc/tradingview 3.0.0 → 3.3.0

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.
Files changed (51) hide show
  1. package/.eslintrc.js +6 -0
  2. package/README.md +29 -3
  3. package/docs/DOCS.md +3 -0
  4. package/examples/AllPrivateIndicators.js +4 -4
  5. package/examples/BuiltInIndicator.js +1 -1
  6. package/examples/CustomChartType.js +1 -0
  7. package/examples/Errors.js +147 -153
  8. package/examples/FakeReplayMode.js +41 -0
  9. package/examples/FromToData.js +29 -0
  10. package/examples/GetDrawings.js +26 -0
  11. package/examples/GraphicIndicator.js +7 -5
  12. package/examples/PinePermManage.js +57 -0
  13. package/examples/ReplayMode.js +97 -0
  14. package/examples/UserLogin.js +16 -0
  15. package/main.js +2 -0
  16. package/package.json +6 -9
  17. package/src/FormData.js +36 -0
  18. package/src/chart/session.js +205 -24
  19. package/src/chart/study.js +68 -68
  20. package/src/classes/BuiltInIndicator.js +7 -1
  21. package/src/classes/PineIndicator.js +7 -7
  22. package/src/classes/PinePermManager.js +143 -0
  23. package/src/client.js +15 -10
  24. package/src/miscRequests.js +144 -101
  25. package/src/protocol.js +7 -1
  26. package/src/quote/session.js +1 -0
  27. package/src/request.js +37 -0
  28. package/src/types.js +6 -2
  29. package/src/utils.js +6 -0
  30. package/test.js +58 -147
  31. package/tests/1. Search.js +18 -0
  32. package/tests/2. QuoteSession.js +54 -0
  33. package/tests/3. SimpleChart.js +61 -0
  34. package/tests/4. Indicators.js +100 -0
  35. package/tests/5. BuiltInIndicator.js +41 -0
  36. package/tests/6. FakeReplayMode.js +59 -0
  37. package/tests/7. ReplayMode.js +51 -0
  38. package/tests/8. CustomChartTypes.js +115 -0
  39. package/tests/9. AllErrors.js +146 -0
  40. package/.github/FUNDING.yml +0 -1
  41. package/examples/_RenkoIndicator.js +0 -41
  42. package/tests/alerts/discordWH.js +0 -76
  43. package/tests/alerts/drawings.js +0 -41
  44. package/tests/alerts/envLoader.js +0 -11
  45. package/tests/alerts/manualAlert.js +0 -77
  46. package/tests/alerts/sentNotifs.js +0 -16
  47. package/tests/alerts/strategyAlert.js +0 -94
  48. package/tests/analysis.js +0 -13
  49. package/tests/indicator.js +0 -42
  50. package/tests/prices.js +0 -33
  51. package/tests/strategy.js +0 -56
@@ -1,45 +1,16 @@
1
+ const os = require('os');
1
2
  const https = require('https');
3
+ const request = require('./request');
4
+ const FormData = require('./FormData');
2
5
 
6
+ const PinePermManager = require('./classes/PinePermManager');
3
7
  const PineIndicator = require('./classes/PineIndicator');
4
8
 
5
9
  const indicators = ['Recommend.Other', 'Recommend.All', 'Recommend.MA'];
6
10
  const builtInIndicList = [];
7
11
 
8
- /**
9
- * @param {https.RequestOptions} options HTTPS Request options
10
- * @param {boolean} [raw] Get raw or JSON data
11
- * @param {string} [content] Request body content
12
- * @returns {Promise<string | object | array>} Result
13
- */
14
- function request(options = {}, raw = false, content = '') {
15
- return new Promise((cb, err) => {
16
- const req = https.request(options, (res) => {
17
- let data = '';
18
- res.on('data', (c) => { data += c; });
19
- res.on('end', () => {
20
- if (raw) {
21
- cb(data);
22
- return;
23
- }
24
-
25
- try {
26
- data = JSON.parse(data);
27
- } catch (error) {
28
- err(new Error('Can\'t parse server response'));
29
- return;
30
- }
31
-
32
- cb(data);
33
- });
34
- });
35
-
36
- req.on('error', err);
37
- req.end(content);
38
- });
39
- }
40
-
41
12
  async function fetchScanData(tickers = [], type = '', columns = []) {
42
- let data = await request({
13
+ let { data } = await request({
43
14
  method: 'POST',
44
15
  hostname: 'scanner.tradingview.com',
45
16
  path: `/${type}/scan`,
@@ -59,11 +30,17 @@ async function fetchScanData(tickers = [], type = '', columns = []) {
59
30
  return data;
60
31
  }
61
32
 
33
+ /** @typedef {number} advice */
34
+
35
+ /**
36
+ * @typedef {{
37
+ * Other: advice,
38
+ * All: advice,
39
+ * MA: advice
40
+ * }} Period
41
+ */
42
+
62
43
  /**
63
- * @typedef {number} advice
64
- *
65
- * @typedef {{ Other: advice, All: advice, MA: advice }} Period
66
- *
67
44
  * @typedef {{
68
45
  * '1': Period,
69
46
  * '5': Period,
@@ -72,21 +49,25 @@ async function fetchScanData(tickers = [], type = '', columns = []) {
72
49
  * '240': Period,
73
50
  * '1D': Period,
74
51
  * '1W': Period,
75
- * '1M': Period,
52
+ * '1M': Period
76
53
  * }} Periods
77
- *
54
+ */
55
+
56
+ /**
78
57
  * @typedef {string | 'forex' | 'crypto'
79
58
  * | 'america' | 'australia' | 'canada' | 'egypt'
80
59
  * | 'germany' | 'india' | 'israel' | 'italy'
81
60
  * | 'luxembourg' | 'poland' | 'sweden' | 'turkey'
82
- * | 'uk' | 'vietnam'} screener
61
+ * | 'uk' | 'vietnam'} Screener
83
62
  * You can use `getScreener(exchange)` function for non-forex and non-crypto markets.
84
63
  */
85
64
 
86
65
  module.exports = {
87
66
  /**
67
+ * Get a screener from an exchange
68
+ * @function getScreener
88
69
  * @param {string} exchange Example: BINANCE, EURONEXT, NASDAQ
89
- * @returns {screener}
70
+ * @returns {Screener}
90
71
  */
91
72
  getScreener(exchange) {
92
73
  const e = exchange.toUpperCase();
@@ -109,7 +90,8 @@ module.exports = {
109
90
 
110
91
  /**
111
92
  * Get technical analysis
112
- * @param {screener} screener
93
+ * @function getTA
94
+ * @param {Screener} screener Market screener
113
95
  * @param {string} id Full market id (Example: COINBASE:BTCEUR)
114
96
  * @returns {Promise<Periods>} results
115
97
  */
@@ -135,24 +117,28 @@ module.exports = {
135
117
 
136
118
  /**
137
119
  * @typedef {Object} SearchMarketResult
138
- * @property {string} id
139
- * @property {string} exchange
140
- * @property {string} fullExchange
141
- * @property {string} screener
142
- * @property {string} symbol
143
- * @property {string} description
144
- * @property {string} type
145
- * @property {() => Promise<Periods>} getTA
120
+ * @prop {string} id Market full symbol
121
+ * @prop {string} exchange Market exchange name
122
+ * @prop {string} fullExchange Market exchange full name
123
+ * @prop {Screener | 'forex' | 'crypto'} screener Market screener
124
+ * @prop {string} symbol Market symbol
125
+ * @prop {string} description Market name
126
+ * @prop {string} type Market type
127
+ * @prop {() => Promise<Periods>} getTA Get market technical analysis
146
128
  */
147
129
 
148
130
  /**
149
131
  * Find a symbol
132
+ * @function searchMarket
150
133
  * @param {string} search Keywords
151
- * @param {'stock' | 'futures' | 'forex' | 'cfd' | 'crypto' | 'index' | 'economic'} [filter]
134
+ * @param {'stock'
135
+ * | 'futures' | 'forex' | 'cfd'
136
+ * | 'crypto' | 'index' | 'economic'
137
+ * } [filter] Caterogy filter
152
138
  * @returns {Promise<SearchMarketResult[]>} Search results
153
139
  */
154
140
  async searchMarket(search, filter = '') {
155
- const data = await request({
141
+ const { data } = await request({
156
142
  host: 'symbol-search.tradingview.com',
157
143
  path: `/symbol_search/?text=${search.replace(/ /g, '%20')}&type=${filter}`,
158
144
  origin: 'https://www.tradingview.com',
@@ -182,34 +168,35 @@ module.exports = {
182
168
 
183
169
  /**
184
170
  * @typedef {Object} SearchIndicatorResult
185
- * @property {string} id Script ID
186
- * @property {string} version Script version
187
- * @property {string} name Script complete name
188
- * @property {{ id: number, username: string }} author Author user ID
189
- * @property {string} image Image ID https://tradingview.com/i/${image}
190
- * @property {string | ''} source Script source (if available)
191
- * @property {'study' | 'strategy'} type Script type (study / strategy)
192
- * @property {'open_source' | 'closed_source' | 'invite_only'
171
+ * @prop {string} id Script ID
172
+ * @prop {string} version Script version
173
+ * @prop {string} name Script complete name
174
+ * @prop {{ id: number, username: string }} author Author user ID
175
+ * @prop {string} image Image ID https://tradingview.com/i/${image}
176
+ * @prop {string | ''} source Script source (if available)
177
+ * @prop {'study' | 'strategy'} type Script type (study / strategy)
178
+ * @prop {'open_source' | 'closed_source' | 'invite_only'
193
179
  * | 'private' | 'other'} access Script access type
194
- * @property {() => Promise<PineIndicator>} get Get the full indicator informations
180
+ * @prop {() => Promise<PineIndicator>} get Get the full indicator informations
195
181
  */
196
182
 
197
183
  /**
198
184
  * Find an indicator
185
+ * @function searchIndicator
199
186
  * @param {string} search Keywords
200
187
  * @returns {Promise<SearchIndicatorResult[]>} Search results
201
188
  */
202
189
  async searchIndicator(search = '') {
203
190
  if (!builtInIndicList.length) {
204
191
  await Promise.all(['standard', 'candlestick', 'fundamental'].map(async (type) => {
205
- builtInIndicList.push(...await request({
192
+ builtInIndicList.push(...(await request({
206
193
  host: 'pine-facade.tradingview.com',
207
194
  path: `/pine-facade/list/?filter=${type}`,
208
- }));
195
+ })).data);
209
196
  }));
210
197
  }
211
198
 
212
- const data = await request({
199
+ const { data } = await request({
213
200
  host: 'www.tradingview.com',
214
201
  path: `/pubscripts-suggest-json/?search=${search.replace(/ /g, '%20')}`,
215
202
  });
@@ -260,6 +247,7 @@ module.exports = {
260
247
 
261
248
  /**
262
249
  * Get an indicator
250
+ * @function getIndicator
263
251
  * @param {string} id Indicator ID (Like: PUB;XXXXXXXXXXXXXXXXXXXXX)
264
252
  * @param {'last' | string} [version] Wanted version of the indicator
265
253
  * @returns {Promise<PineIndicator>} Indicator
@@ -267,7 +255,7 @@ module.exports = {
267
255
  async getIndicator(id, version = 'last') {
268
256
  const indicID = id.replace(/ |%/g, '%25');
269
257
 
270
- let data = await request({
258
+ let { data } = await request({
271
259
  host: 'pine-facade.tradingview.com',
272
260
  path: `/pine-facade/translate/${indicID}/${version}`,
273
261
  }, true);
@@ -340,25 +328,74 @@ module.exports = {
340
328
 
341
329
  /**
342
330
  * @typedef {Object} User Instance of User
343
- * @property {number} id User ID
344
- * @property {string} username User username
345
- * @property {string} firstName User first name
346
- * @property {string} lastName User last name
347
- * @property {number} reputation User reputation
348
- * @property {number} following Number of following accounts
349
- * @property {number} followers Number of followers
350
- * @property {Object} notifications User's notifications
351
- * @property {number} notifications.user User notifications
352
- * @property {number} notifications.following Notification from following accounts
353
- * @property {string} session User session
354
- * @property {string} sessionHash User session hash
355
- * @property {string} privateChannel User private channel
356
- * @property {string} authToken User auth token
357
- * @property {Date} joinDate Account creation date
331
+ * @prop {number} id User ID
332
+ * @prop {string} username User username
333
+ * @prop {string} firstName User first name
334
+ * @prop {string} lastName User last name
335
+ * @prop {number} reputation User reputation
336
+ * @prop {number} following Number of following accounts
337
+ * @prop {number} followers Number of followers
338
+ * @prop {Object} notifications User's notifications
339
+ * @prop {number} notifications.user User notifications
340
+ * @prop {number} notifications.following Notification from following accounts
341
+ * @prop {string} session User session
342
+ * @prop {string} sessionHash User session hash
343
+ * @prop {string} privateChannel User private channel
344
+ * @prop {string} authToken User auth token
345
+ * @prop {Date} joinDate Account creation date
346
+ */
347
+
348
+ /**
349
+ * Get user and sessionid from username/email and password
350
+ * @function loginUser
351
+ * @param {string} username User username/email
352
+ * @param {string} password User password
353
+ * @param {boolean} [remember] Remember the session (default: false)
354
+ * @param {string} [UA] Custom UserAgent
355
+ * @returns {Promise<User>} Token
358
356
  */
357
+ async loginUser(username, password, remember = true, UA = 'TWAPI/3.0') {
358
+ const formData = new FormData();
359
+ formData.append('username', username);
360
+ formData.append('password', password);
361
+ if (remember) formData.append('remember', 'on');
362
+
363
+ const { data, cookies } = await request({
364
+ method: 'POST',
365
+ host: 'www.tradingview.com',
366
+ path: '/accounts/signin/',
367
+ headers: {
368
+ referer: 'https://www.tradingview.com',
369
+ 'Content-Type': `multipart/form-data; boundary=${formData.boundary}`,
370
+ 'User-agent': `${UA} (${os.version()}; ${os.platform()}; ${os.arch()})`,
371
+ },
372
+ }, false, formData.toString());
373
+
374
+ if (data.error) throw new Error(data.error);
375
+
376
+ const cookie = cookies.find((c) => c.includes('sessionid='));
377
+ const session = (cookie.match(/sessionid=(.*?);/) ?? [])[1];
378
+
379
+ return {
380
+ id: data.user.id,
381
+ username: data.user.username,
382
+ firstName: data.user.first_name,
383
+ lastName: data.user.last_name,
384
+ reputation: data.user.reputation,
385
+ following: data.user.following,
386
+ followers: data.user.followers,
387
+ notifications: data.user.notification_count,
388
+ session,
389
+ sessionHash: data.user.session_hash,
390
+ privateChannel: data.user.private_channel,
391
+ authToken: data.user.auth_token,
392
+ joinDate: new Date(data.user.date_joined),
393
+ };
394
+ },
359
395
 
360
396
  /**
361
- * Get a token for an user from a 'sessionid' cookie
397
+ * Get user from 'sessionid' cookie
398
+ * @function getUser
362
399
  * @param {string} session User 'sessionid' cookie
363
400
  * @param {string} [location] Auth page location (For france: https://fr.tradingview.com/)
364
401
  * @returns {Promise<User>} Token
@@ -404,6 +441,7 @@ module.exports = {
404
441
 
405
442
  /**
406
443
  * Get user's private indicators from a 'sessionid' cookie
444
+ * @function getPrivateIndicators
407
445
  * @param {string} session User 'sessionid' cookie
408
446
  * @returns {Promise<SearchIndicatorResult[]>} Search results
409
447
  */
@@ -437,6 +475,9 @@ module.exports = {
437
475
  get() {
438
476
  return module.exports.getIndicator(ind.scriptIdPart, ind.version);
439
477
  },
478
+ getManager() {
479
+ return new PinePermManager(ind.scriptIdPart);
480
+ },
440
481
  })));
441
482
  });
442
483
 
@@ -448,12 +489,13 @@ module.exports = {
448
489
  /**
449
490
  * User credentials
450
491
  * @typedef {Object} UserCredentials
451
- * @property {number} id User ID
452
- * @property {string} session User session ('sessionid' cookie)
492
+ * @prop {number} id User ID
493
+ * @prop {string} session User session ('sessionid' cookie)
453
494
  */
454
495
 
455
496
  /**
456
497
  * Get a chart token from a layout ID and the user credentials if the layout is not public
498
+ * @function getChartToken
457
499
  * @param {string} layout The layout ID found in the layout URL (Like: 'XXXXXXXX')
458
500
  * @param {UserCredentials} [credentials] User credentials (id + session)
459
501
  * @returns {Promise<string>} Token
@@ -463,7 +505,7 @@ module.exports = {
463
505
  const userID = creds ? credentials.id : -1;
464
506
  const session = creds ? credentials.session : null;
465
507
 
466
- const data = await request({
508
+ const { data } = await request({
467
509
  host: 'www.tradingview.com',
468
510
  path: `/chart-token/?image_url=${layout}&user_id=${userID}`,
469
511
  headers: { cookie: session ? `sessionid=${session}` : '' },
@@ -476,32 +518,33 @@ module.exports = {
476
518
 
477
519
  /**
478
520
  * @typedef {Object} DrawingPoint Drawing poitn
479
- * @property {number} time_t Point X time position
480
- * @property {number} price Point Y price position
481
- * @property {number} offset Point offset
521
+ * @prop {number} time_t Point X time position
522
+ * @prop {number} price Point Y price position
523
+ * @prop {number} offset Point offset
482
524
  */
483
525
 
484
526
  /**
485
527
  * @typedef {Object} Drawing
486
- * @property {string} id Drawing ID (Like: 'XXXXXX')
487
- * @property {string} symbol Layout market symbol (Like: 'BINANCE:BUCEUR')
488
- * @property {string} ownerSource Owner user ID (Like: 'XXXXXX')
489
- * @property {string} serverUpdateTime Drawing last update timestamp
490
- * @property {string} currencyId Currency ID (Like: 'EUR')
491
- * @property {any} unitId Unit ID
492
- * @property {string} type Drawing type
493
- * @property {DrawingPoint[]} points List of drawing points
494
- * @property {number} zorder Drawing Z order
495
- * @property {string} linkKey Drawing link key
496
- * @property {Object} state Drawing state
528
+ * @prop {string} id Drawing ID (Like: 'XXXXXX')
529
+ * @prop {string} symbol Layout market symbol (Like: 'BINANCE:BUCEUR')
530
+ * @prop {string} ownerSource Owner user ID (Like: 'XXXXXX')
531
+ * @prop {string} serverUpdateTime Drawing last update timestamp
532
+ * @prop {string} currencyId Currency ID (Like: 'EUR')
533
+ * @prop {any} unitId Unit ID
534
+ * @prop {string} type Drawing type
535
+ * @prop {DrawingPoint[]} points List of drawing points
536
+ * @prop {number} zorder Drawing Z order
537
+ * @prop {string} linkKey Drawing link key
538
+ * @prop {Object} state Drawing state
497
539
  */
498
540
 
499
541
  /**
500
542
  * Get a chart token from a layout ID and the user credentials if the layout is not public
543
+ * @function getDrawings
501
544
  * @param {string} layout The layout ID found in the layout URL (Like: 'XXXXXXXX')
502
545
  * @param {string | ''} [symbol] Market filter (Like: 'BINANCE:BTCEUR')
503
546
  * @param {UserCredentials} [credentials] User credentials (id + session)
504
- * @param {number} [chartID = 1] Chart ID
547
+ * @param {number} [chartID] Chart ID
505
548
  * @returns {Promise<Drawing[]>} Drawings
506
549
  */
507
550
  async getDrawings(layout, symbol = '', credentials = {}, chartID = 1) {
@@ -509,7 +552,7 @@ module.exports = {
509
552
  const creds = credentials.id && credentials.session;
510
553
  const session = creds ? credentials.session : null;
511
554
 
512
- const data = await request({
555
+ const { data } = await request({
513
556
  host: 'charts-storage.tradingview.com',
514
557
  path: `/charts-storage/layout/${layout}/sources?chart_id=${chartID
515
558
  }&jwt=${chartToken}${symbol ? `&symbol=${symbol}` : ''}`,
package/src/protocol.js CHANGED
@@ -6,14 +6,18 @@ const JSZip = require('jszip');
6
6
  * @prop {[session: string, {}]} [p] Packet data
7
7
  */
8
8
 
9
+ const cleanerRgx = /~h~/g;
10
+ const splitterRgx = /~m~[0-9]{1,}~m~/g;
11
+
9
12
  module.exports = {
10
13
  /**
11
14
  * Parse websocket packet
15
+ * @function parseWSPacket
12
16
  * @param {string} str Websocket raw data
13
17
  * @returns {TWPacket[]} TradingView packets
14
18
  */
15
19
  parseWSPacket(str) {
16
- return str.replace(/~h~/g, '').split(/~m~[0-9]{1,}~m~/g)
20
+ return str.replace(cleanerRgx, '').split(splitterRgx)
17
21
  .map((p) => {
18
22
  if (!p) return false;
19
23
  try {
@@ -28,6 +32,7 @@ module.exports = {
28
32
 
29
33
  /**
30
34
  * Format websocket packet
35
+ * @function formatWSPacket
31
36
  * @param {TWPacket} packet TradingView packet
32
37
  * @returns {string} Websocket raw data
33
38
  */
@@ -40,6 +45,7 @@ module.exports = {
40
45
 
41
46
  /**
42
47
  * Parse compressed data
48
+ * @function parseCompressed
43
49
  * @param {string} data Compressed data
44
50
  * @returns {Promise<{}>} Parsed data
45
51
  */
@@ -120,5 +120,6 @@ module.exports = (client) => class QuoteSession {
120
120
  /** Delete the quote session */
121
121
  delete() {
122
122
  this.#client.send('quote_delete_session', [this.#sessionID]);
123
+ delete this.#client.sessions[this.#sessionID];
123
124
  }
124
125
  };
package/src/request.js ADDED
@@ -0,0 +1,37 @@
1
+ const https = require('https');
2
+
3
+ /**
4
+ * @param {https.RequestOptions} options HTTPS Request options
5
+ * @param {boolean} [raw] Get raw or JSON data
6
+ * @param {string} [content] Request body content
7
+ * @returns {Promise<{ data: (string | object | array), cookies: string[] }>} Result
8
+ */
9
+ function request(options = {}, raw = false, content = '') {
10
+ return new Promise((cb, err) => {
11
+ const req = https.request(options, (res) => {
12
+ let data = '';
13
+ res.on('data', (c) => { data += c; });
14
+ res.on('end', () => {
15
+ if (raw) {
16
+ cb({ data, cookies: res.headers['set-cookie'] });
17
+ return;
18
+ }
19
+
20
+ try {
21
+ data = JSON.parse(data);
22
+ } catch (error) {
23
+ console.log(data);
24
+ err(new Error('Can\'t parse server response'));
25
+ return;
26
+ }
27
+
28
+ cb({ data, cookies: res.headers['set-cookie'] });
29
+ });
30
+ });
31
+
32
+ req.on('error', err);
33
+ req.end(content);
34
+ });
35
+ }
36
+
37
+ module.exports = request;
package/src/types.js CHANGED
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * @typedef {string} MarketSymbol Market symbol (like: 'BTCEUR' or 'KRAKEN:BTCEUR')
3
- *
3
+ */
4
+
5
+ /**
4
6
  * @typedef {'Etc/UTC' | 'exchange'
5
7
  * | 'Pacific/Honolulu' | 'America/Juneau' | 'America/Los_Angeles'
6
8
  * | 'America/Phoenix' | 'America/Vancouver' | 'US/Mountain'
@@ -23,7 +25,9 @@
23
25
  * | 'Asia/Taipei' | 'Asia/Seoul' | 'Asia/Tokyo' | 'Australia/Brisbane'
24
26
  * | 'Australia/Adelaide' | 'Australia/Sydney' | 'Pacific/Norfolk'
25
27
  * | 'Pacific/Auckland' | 'Pacific/Fakaofo' | 'Pacific/Chatham'} Timezone (Chart) timezone
26
- *
28
+ */
29
+
30
+ /**
27
31
  * @typedef {'1' | '3' | '5' | '15' | '30'
28
32
  * | '45' | '60' | '120' | '180' | '240'
29
33
  * | '1D' | '1W' | '1M' | 'D' | 'W' | 'M'} TimeFrame
package/src/utils.js CHANGED
@@ -1,4 +1,10 @@
1
1
  module.exports = {
2
+ /**
3
+ * Generates a session id
4
+ * @function genSessionID
5
+ * @param {String} type Session type
6
+ * @returns {string}
7
+ */
2
8
  genSessionID(type = 'xs') {
3
9
  let r = '';
4
10
  const c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';