@mathieuc/tradingview 3.0.1 → 3.3.1

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 (50) hide show
  1. package/.eslintrc.js +6 -0
  2. package/README.md +28 -2
  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/GetDrawings.js +26 -0
  10. package/examples/PinePermManage.js +57 -0
  11. package/examples/ReplayMode.js +97 -0
  12. package/examples/UserLogin.js +16 -0
  13. package/main.js +2 -0
  14. package/package.json +6 -9
  15. package/src/FormData.js +36 -0
  16. package/src/chart/session.js +193 -17
  17. package/src/chart/study.js +68 -68
  18. package/src/classes/BuiltInIndicator.js +1 -0
  19. package/src/classes/PineIndicator.js +7 -7
  20. package/src/classes/PinePermManager.js +143 -0
  21. package/src/client.js +17 -10
  22. package/src/miscRequests.js +148 -103
  23. package/src/protocol.js +7 -1
  24. package/src/quote/session.js +1 -0
  25. package/src/request.js +37 -0
  26. package/src/types.js +6 -2
  27. package/src/utils.js +6 -0
  28. package/test.js +58 -147
  29. package/tests/1. Search.js +18 -0
  30. package/tests/2. QuoteSession.js +54 -0
  31. package/tests/3. SimpleChart.js +61 -0
  32. package/tests/4. Indicators.js +100 -0
  33. package/tests/5. BuiltInIndicator.js +41 -0
  34. package/tests/6. FakeReplayMode.js +59 -0
  35. package/tests/7. ReplayMode.js +51 -0
  36. package/tests/8. CustomChartTypes.js +115 -0
  37. package/tests/9. AllErrors.js +146 -0
  38. package/.github/FUNDING.yml +0 -1
  39. package/examples/CurrentTimeframe.js +0 -34
  40. package/examples/_RenkoIndicator.js +0 -41
  41. package/tests/alerts/discordWH.js +0 -76
  42. package/tests/alerts/drawings.js +0 -41
  43. package/tests/alerts/envLoader.js +0 -11
  44. package/tests/alerts/manualAlert.js +0 -77
  45. package/tests/alerts/sentNotifs.js +0 -16
  46. package/tests/alerts/strategyAlert.js +0 -94
  47. package/tests/analysis.js +0 -13
  48. package/tests/indicator.js +0 -42
  49. package/tests/prices.js +0 -33
  50. package/tests/strategy.js +0 -56
@@ -0,0 +1,143 @@
1
+ const request = require('../request');
2
+ const FormData = require('../FormData');
3
+
4
+ /**
5
+ * @typedef {Object} AuthorizationUser
6
+ * @prop {id} id User id
7
+ * @prop {string} username User's username
8
+ * @prop {string} userpic User's profile picture URL
9
+ * @prop {string} expiration Authorization expiration date
10
+ * @prop {string} created Authorization creation date
11
+ */
12
+
13
+ /** @class */
14
+ class PinePermManager {
15
+ sessionId;
16
+
17
+ pineId;
18
+
19
+ /**
20
+ * Creates a PinePermManager instance
21
+ * @param {string} sessionId Token from sessionid cookie
22
+ * @param {string} pineId Indicator ID (Like: PUB;XXXXXXXXXXXXXXXXXXXXX)
23
+ */
24
+ constructor(sessionId, pineId) {
25
+ if (!sessionId) throw new Error('Please provide a SessionID');
26
+ if (!pineId) throw new Error('Please provide a PineID');
27
+ this.pineId = pineId;
28
+ this.sessionId = sessionId;
29
+ }
30
+
31
+ /**
32
+ * Get list of authorized users
33
+ * @param {number} limit Fetching limit
34
+ * @param {'user__username'
35
+ * | '-user__username'
36
+ * | 'created' | 'created'
37
+ * | 'expiration,user__username'
38
+ * | '-expiration,user__username'
39
+ * } order Fetching order
40
+ * @returns {AuthorizationUser[]}
41
+ */
42
+ async getUsers(limit = 10, order = '-created') {
43
+ const { data } = await request({
44
+ method: 'POST',
45
+ host: 'www.tradingview.com',
46
+ path: `/pine_perm/list_users/?limit=${limit}&order_by=${order}`,
47
+ headers: {
48
+ origin: 'https://www.tradingview.com',
49
+ 'Content-Type': 'application/x-www-form-urlencoded',
50
+ cookie: `sessionid=${this.sessionId}`,
51
+ },
52
+ }, false, `pine_id=${this.pineId.replace(/;/g, '%3B')}`);
53
+
54
+ if (!data.results) throw new Error('Wrong sessionId or pineId');
55
+
56
+ return data.results;
57
+ }
58
+
59
+ /**
60
+ * Adds an user to the authorized list
61
+ * @param {string} username User's username
62
+ * @param {Date} [expiration] Expiration date
63
+ * @returns {'ok' | 'exists' | null}
64
+ */
65
+ async addUser(username, expiration = null) {
66
+ const formData = new FormData();
67
+ formData.append('pine_id', this.pineId);
68
+ formData.append('username_recip', username);
69
+ if (expiration && expiration instanceof Date) {
70
+ formData.append('expiration', expiration.toString());
71
+ }
72
+
73
+ const { data } = await request({
74
+ method: 'POST',
75
+ host: 'www.tradingview.com',
76
+ path: '/pine_perm/add/',
77
+ headers: {
78
+ origin: 'https://www.tradingview.com',
79
+ 'Content-Type': `multipart/form-data; boundary=${formData.boundary}`,
80
+ cookie: `sessionid=${this.sessionId}`,
81
+ },
82
+ }, false, formData.toString());
83
+
84
+ if (!data.status) throw new Error('Wrong sessionId or pineId');
85
+ return data.status;
86
+ }
87
+
88
+ /**
89
+ * Modify an authorization expiration date
90
+ * @param {string} username User's username
91
+ * @param {Date} [expiration] New expiration date
92
+ * @returns {'ok' | null}
93
+ */
94
+ async modifyExpiration(username, expiration = null) {
95
+ const formData = new FormData();
96
+ formData.append('pine_id', this.pineId);
97
+ formData.append('username_recip', username);
98
+ if (expiration && expiration instanceof Date) {
99
+ formData.append('expiration', expiration.toISOString());
100
+ }
101
+
102
+ const { data } = await request({
103
+ method: 'POST',
104
+ host: 'www.tradingview.com',
105
+ path: '/pine_perm/modify_user_expiration/',
106
+ headers: {
107
+ origin: 'https://www.tradingview.com',
108
+ 'Content-Type': `multipart/form-data; boundary=${formData.boundary}`,
109
+ cookie: `sessionid=${this.sessionId}`,
110
+ },
111
+ }, false, formData.toString());
112
+
113
+ if (!data.status) throw new Error('Wrong sessionId or pineId');
114
+ return data.status;
115
+ }
116
+
117
+ /**
118
+ * Removes an user to the authorized list
119
+ * @param {string} username User's username
120
+ * @returns {'ok' | null}
121
+ */
122
+ async removeUser(username) {
123
+ const formData = new FormData();
124
+ formData.append('pine_id', this.pineId);
125
+ formData.append('username_recip', username);
126
+
127
+ const { data } = await request({
128
+ method: 'POST',
129
+ host: 'www.tradingview.com',
130
+ path: '/pine_perm/remove/',
131
+ headers: {
132
+ origin: 'https://www.tradingview.com',
133
+ 'Content-Type': `multipart/form-data; boundary=${formData.boundary}`,
134
+ cookie: `sessionid=${this.sessionId}`,
135
+ },
136
+ }, false, formData.toString());
137
+
138
+ if (!data.status) throw new Error('Wrong sessionId or pineId');
139
+ return data.status;
140
+ }
141
+ }
142
+
143
+ module.exports = PinePermManager;
package/src/client.js CHANGED
@@ -15,9 +15,10 @@ const chartSessionGenerator = require('./chart/session');
15
15
  /** @typedef {Object<string, Session>} SessionList Session list */
16
16
 
17
17
  /**
18
- * @typedef {(t: string, p: []) => null} SendPacket Send a custom packet
18
+ * @callback SendPacket Send a custom packet
19
19
  * @param {string} t Packet type
20
20
  * @param {string[]} p Packet data
21
+ * @returns {void}
21
22
  */
22
23
 
23
24
  /**
@@ -33,6 +34,7 @@ const chartSessionGenerator = require('./chart/session');
33
34
  * } ClientEvent
34
35
  */
35
36
 
37
+ /** @class */
36
38
  module.exports = class Client {
37
39
  #ws;
38
40
 
@@ -79,7 +81,7 @@ module.exports = class Client {
79
81
  /**
80
82
  * When client is connected
81
83
  * @param {() => void} cb Callback
82
- * @event
84
+ * @event onConnected
83
85
  */
84
86
  onConnected(cb) {
85
87
  this.#callbacks.connected.push(cb);
@@ -88,7 +90,7 @@ module.exports = class Client {
88
90
  /**
89
91
  * When client is disconnected
90
92
  * @param {() => void} cb Callback
91
- * @event
93
+ * @event onDisconnected
92
94
  */
93
95
  onDisconnected(cb) {
94
96
  this.#callbacks.disconnected.push(cb);
@@ -105,10 +107,12 @@ module.exports = class Client {
105
107
  * @prop {string} javastudies Javastudies
106
108
  * @prop {number} auth_scheme_vsn Auth scheme type
107
109
  * @prop {string} via Socket IP
110
+ */
108
111
 
112
+ /**
109
113
  * When client is logged
110
114
  * @param {(SocketSession: SocketSession) => void} cb Callback
111
- * @event
115
+ * @event onLogged
112
116
  */
113
117
  onLogged(cb) {
114
118
  this.#callbacks.logged.push(cb);
@@ -117,7 +121,7 @@ module.exports = class Client {
117
121
  /**
118
122
  * When server is pinging the client
119
123
  * @param {(i: number) => void} cb Callback
120
- * @event
124
+ * @event onPing
121
125
  */
122
126
  onPing(cb) {
123
127
  this.#callbacks.ping.push(cb);
@@ -126,7 +130,7 @@ module.exports = class Client {
126
130
  /**
127
131
  * When unparsed data is received
128
132
  * @param {(...{}) => void} cb Callback
129
- * @event
133
+ * @event onData
130
134
  */
131
135
  onData(cb) {
132
136
  this.#callbacks.data.push(cb);
@@ -135,7 +139,7 @@ module.exports = class Client {
135
139
  /**
136
140
  * When a client error happens
137
141
  * @param {(...{}) => void} cb Callback
138
- * @event
142
+ * @event onError
139
143
  */
140
144
  onError(cb) {
141
145
  this.#callbacks.error.push(cb);
@@ -144,7 +148,7 @@ module.exports = class Client {
144
148
  /**
145
149
  * When a client event happens
146
150
  * @param {(...{}) => void} cb Callback
147
- * @event
151
+ * @event onEvent
148
152
  */
149
153
  onEvent(cb) {
150
154
  this.#callbacks.event.push(cb);
@@ -203,6 +207,7 @@ module.exports = class Client {
203
207
  while (this.isOpen && this.#logged && this.#sendQueue.length > 0) {
204
208
  const packet = this.#sendQueue.shift();
205
209
  this.#ws.send(packet);
210
+ if (global.TW_DEBUG) console.log('§90§30§107 > §0', packet);
206
211
  }
207
212
  }
208
213
 
@@ -210,6 +215,7 @@ module.exports = class Client {
210
215
  * @typedef {Object} ClientOptions
211
216
  * @prop {string} [token] User auth token (in 'sessionid' cookie)
212
217
  * @prop {boolean} [DEBUG] Enable debug mode
218
+ * @prop {'data' | 'prodata' | 'widgetdata'} [server] Server type
213
219
  */
214
220
 
215
221
  /** Client object
@@ -218,7 +224,8 @@ module.exports = class Client {
218
224
  constructor(clientOptions = {}) {
219
225
  if (clientOptions.DEBUG) global.TW_DEBUG = clientOptions.DEBUG;
220
226
 
221
- this.#ws = new WebSocket('wss://widgetdata.tradingview.com/socket.io/websocket', {
227
+ const server = clientOptions.server || 'data';
228
+ this.#ws = new WebSocket(`wss://${server}.tradingview.com/socket.io/websocket`, {
222
229
  origin: 'https://s.tradingview.com',
223
230
  });
224
231
 
@@ -273,7 +280,7 @@ module.exports = class Client {
273
280
  */
274
281
  end() {
275
282
  return new Promise((cb) => {
276
- this.#ws.close();
283
+ if (this.#ws.readyState) this.#ws.close();
277
284
  cb();
278
285
  });
279
286
  }
@@ -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);
@@ -287,10 +275,12 @@ module.exports = {
287
275
  data.result.metaInfo.inputs.forEach((input) => {
288
276
  if (['text', 'pineId', 'pineVersion'].includes(input.id)) return;
289
277
 
278
+ const inlineName = input.name.replace(/ /g, '_').replace(/[^a-zA-Z0-9_]/g, '');
279
+
290
280
  inputs[input.id] = {
291
281
  name: input.name,
292
- inline: input.inline || input.name.replace(/ /g, '_').replace(/[^a-zA-Z0-9_]/g, ''),
293
- internalID: input.internalID,
282
+ inline: input.inline || inlineName,
283
+ internalID: input.internalID || inlineName,
294
284
  tooltip: input.tooltip,
295
285
 
296
286
  type: input.type,
@@ -340,25 +330,74 @@ module.exports = {
340
330
 
341
331
  /**
342
332
  * @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
333
+ * @prop {number} id User ID
334
+ * @prop {string} username User username
335
+ * @prop {string} firstName User first name
336
+ * @prop {string} lastName User last name
337
+ * @prop {number} reputation User reputation
338
+ * @prop {number} following Number of following accounts
339
+ * @prop {number} followers Number of followers
340
+ * @prop {Object} notifications User's notifications
341
+ * @prop {number} notifications.user User notifications
342
+ * @prop {number} notifications.following Notification from following accounts
343
+ * @prop {string} session User session
344
+ * @prop {string} sessionHash User session hash
345
+ * @prop {string} privateChannel User private channel
346
+ * @prop {string} authToken User auth token
347
+ * @prop {Date} joinDate Account creation date
358
348
  */
359
349
 
360
350
  /**
361
- * Get a token for an user from a 'sessionid' cookie
351
+ * Get user and sessionid from username/email and password
352
+ * @function loginUser
353
+ * @param {string} username User username/email
354
+ * @param {string} password User password
355
+ * @param {boolean} [remember] Remember the session (default: false)
356
+ * @param {string} [UA] Custom UserAgent
357
+ * @returns {Promise<User>} Token
358
+ */
359
+ async loginUser(username, password, remember = true, UA = 'TWAPI/3.0') {
360
+ const formData = new FormData();
361
+ formData.append('username', username);
362
+ formData.append('password', password);
363
+ if (remember) formData.append('remember', 'on');
364
+
365
+ const { data, cookies } = await request({
366
+ method: 'POST',
367
+ host: 'www.tradingview.com',
368
+ path: '/accounts/signin/',
369
+ headers: {
370
+ referer: 'https://www.tradingview.com',
371
+ 'Content-Type': `multipart/form-data; boundary=${formData.boundary}`,
372
+ 'User-agent': `${UA} (${os.version()}; ${os.platform()}; ${os.arch()})`,
373
+ },
374
+ }, false, formData.toString());
375
+
376
+ if (data.error) throw new Error(data.error);
377
+
378
+ const cookie = cookies.find((c) => c.includes('sessionid='));
379
+ const session = (cookie.match(/sessionid=(.*?);/) ?? [])[1];
380
+
381
+ return {
382
+ id: data.user.id,
383
+ username: data.user.username,
384
+ firstName: data.user.first_name,
385
+ lastName: data.user.last_name,
386
+ reputation: data.user.reputation,
387
+ following: data.user.following,
388
+ followers: data.user.followers,
389
+ notifications: data.user.notification_count,
390
+ session,
391
+ sessionHash: data.user.session_hash,
392
+ privateChannel: data.user.private_channel,
393
+ authToken: data.user.auth_token,
394
+ joinDate: new Date(data.user.date_joined),
395
+ };
396
+ },
397
+
398
+ /**
399
+ * Get user from 'sessionid' cookie
400
+ * @function getUser
362
401
  * @param {string} session User 'sessionid' cookie
363
402
  * @param {string} [location] Auth page location (For france: https://fr.tradingview.com/)
364
403
  * @returns {Promise<User>} Token
@@ -404,6 +443,7 @@ module.exports = {
404
443
 
405
444
  /**
406
445
  * Get user's private indicators from a 'sessionid' cookie
446
+ * @function getPrivateIndicators
407
447
  * @param {string} session User 'sessionid' cookie
408
448
  * @returns {Promise<SearchIndicatorResult[]>} Search results
409
449
  */
@@ -437,6 +477,9 @@ module.exports = {
437
477
  get() {
438
478
  return module.exports.getIndicator(ind.scriptIdPart, ind.version);
439
479
  },
480
+ getManager() {
481
+ return new PinePermManager(ind.scriptIdPart);
482
+ },
440
483
  })));
441
484
  });
442
485
 
@@ -448,12 +491,13 @@ module.exports = {
448
491
  /**
449
492
  * User credentials
450
493
  * @typedef {Object} UserCredentials
451
- * @property {number} id User ID
452
- * @property {string} session User session ('sessionid' cookie)
494
+ * @prop {number} id User ID
495
+ * @prop {string} session User session ('sessionid' cookie)
453
496
  */
454
497
 
455
498
  /**
456
499
  * Get a chart token from a layout ID and the user credentials if the layout is not public
500
+ * @function getChartToken
457
501
  * @param {string} layout The layout ID found in the layout URL (Like: 'XXXXXXXX')
458
502
  * @param {UserCredentials} [credentials] User credentials (id + session)
459
503
  * @returns {Promise<string>} Token
@@ -463,7 +507,7 @@ module.exports = {
463
507
  const userID = creds ? credentials.id : -1;
464
508
  const session = creds ? credentials.session : null;
465
509
 
466
- const data = await request({
510
+ const { data } = await request({
467
511
  host: 'www.tradingview.com',
468
512
  path: `/chart-token/?image_url=${layout}&user_id=${userID}`,
469
513
  headers: { cookie: session ? `sessionid=${session}` : '' },
@@ -476,32 +520,33 @@ module.exports = {
476
520
 
477
521
  /**
478
522
  * @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
523
+ * @prop {number} time_t Point X time position
524
+ * @prop {number} price Point Y price position
525
+ * @prop {number} offset Point offset
482
526
  */
483
527
 
484
528
  /**
485
529
  * @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
530
+ * @prop {string} id Drawing ID (Like: 'XXXXXX')
531
+ * @prop {string} symbol Layout market symbol (Like: 'BINANCE:BUCEUR')
532
+ * @prop {string} ownerSource Owner user ID (Like: 'XXXXXX')
533
+ * @prop {string} serverUpdateTime Drawing last update timestamp
534
+ * @prop {string} currencyId Currency ID (Like: 'EUR')
535
+ * @prop {any} unitId Unit ID
536
+ * @prop {string} type Drawing type
537
+ * @prop {DrawingPoint[]} points List of drawing points
538
+ * @prop {number} zorder Drawing Z order
539
+ * @prop {string} linkKey Drawing link key
540
+ * @prop {Object} state Drawing state
497
541
  */
498
542
 
499
543
  /**
500
544
  * Get a chart token from a layout ID and the user credentials if the layout is not public
545
+ * @function getDrawings
501
546
  * @param {string} layout The layout ID found in the layout URL (Like: 'XXXXXXXX')
502
547
  * @param {string | ''} [symbol] Market filter (Like: 'BINANCE:BTCEUR')
503
548
  * @param {UserCredentials} [credentials] User credentials (id + session)
504
- * @param {number} [chartID = 1] Chart ID
549
+ * @param {number} [chartID] Chart ID
505
550
  * @returns {Promise<Drawing[]>} Drawings
506
551
  */
507
552
  async getDrawings(layout, symbol = '', credentials = {}, chartID = 1) {
@@ -509,7 +554,7 @@ module.exports = {
509
554
  const creds = credentials.id && credentials.session;
510
555
  const session = creds ? credentials.session : null;
511
556
 
512
- const data = await request({
557
+ const { data } = await request({
513
558
  host: 'charts-storage.tradingview.com',
514
559
  path: `/charts-storage/layout/${layout}/sources?chart_id=${chartID
515
560
  }&jwt=${chartToken}${symbol ? `&symbol=${symbol}` : ''}`,