diff --git a/examples/Proxy.js b/examples/Proxy.js new file mode 100644 index 0000000..3b1edde --- /dev/null +++ b/examples/Proxy.js @@ -0,0 +1,16 @@ +const TradingView = require('../main') + +/* + Test Proxy +*/ + +if (!process.argv[2]) throw Error('Please specify your \'sessionid\' cookie') +if (!process.argv[3]) throw Error('Please specify proxy') + +TradingView.SetAgent(process.argv[3]) + +TradingView.getPrivateIndicators(process.argv[2]).then((indicList) => { + indicList.forEach(async (indic) => { + console.log('Loading indicator', indic.name, '...') + }) +}) diff --git a/main.js b/main.js index 721ab7a..81bd445 100644 --- a/main.js +++ b/main.js @@ -1,11 +1,13 @@ -const miscRequests = require('./src/miscRequests'); -const Client = require('./src/client'); -const BuiltInIndicator = require('./src/classes/BuiltInIndicator'); -const PineIndicator = require('./src/classes/PineIndicator'); -const PinePermManager = require('./src/classes/PinePermManager'); +const miscRequests = require('./src/miscRequests') +const Client = require('./src/client') +const BuiltInIndicator = require('./src/classes/BuiltInIndicator') +const PineIndicator = require('./src/classes/PineIndicator') +const PinePermManager = require('./src/classes/PinePermManager') +const { setAgent } = require('./src/proxy') -module.exports = { ...miscRequests }; -module.exports.Client = Client; -module.exports.BuiltInIndicator = BuiltInIndicator; -module.exports.PineIndicator = PineIndicator; -module.exports.PinePermManager = PinePermManager; +module.exports = { ...miscRequests } +module.exports.Client = Client +module.exports.BuiltInIndicator = BuiltInIndicator +module.exports.PineIndicator = PineIndicator +module.exports.PinePermManager = PinePermManager +module.exports.SetAgent = setAgent diff --git a/package-lock.json b/package-lock.json index 3a9e400..faa6e3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,17 @@ { "name": "@mathieuc/tradingview", - "version": "3.3.3", + "version": "3.4.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@mathieuc/tradingview", - "version": "3.3.3", + "version": "3.4.1", "license": "ISC", "dependencies": { + "https-proxy-agent": "^7.0.0", "jszip": "^3.7.1", + "socks-proxy-agent": "^8.0.1", "ws": "^7.4.3" }, "devDependencies": { @@ -594,6 +596,17 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -846,10 +859,9 @@ } }, "node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, + "version": "4.3.4", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { "ms": "2.1.2" }, @@ -1519,6 +1531,18 @@ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, + "node_modules/https-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.0.tgz", + "integrity": "sha512-0euwPCRyAPSgGdzD1IVN9nJYHtBhJwb6XPfbpQcYbPCwrBidX6GzxmchnaF4sfF/jPb74Ojx5g4yTg3sixlyPw==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -1573,6 +1597,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ip": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -1920,8 +1949,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanocolors": { "version": "0.2.12", @@ -2395,6 +2423,41 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.7.1", + "resolved": "https://registry.npmmirror.com/socks/-/socks-2.7.1.tgz", + "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/socks-proxy-agent/-/socks-proxy-agent-8.0.1.tgz", + "integrity": "sha512-59EjPbbgg8U3x62hhKOFVAmySQUcfRQ4C7Q/D5sEHnZTQRrQlNKINks44DMR1gwXp0p4LaVIeccX2KHTTcHVqQ==", + "dependencies": { + "agent-base": "^7.0.1", + "debug": "^4.3.4", + "socks": "^2.7.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -3195,6 +3258,14 @@ "dev": true, "requires": {} }, + "agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "requires": { + "debug": "^4.3.4" + } + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3384,10 +3455,9 @@ } }, "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, + "version": "4.3.4", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { "ms": "2.1.2" } @@ -3914,6 +3984,15 @@ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, + "https-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.0.tgz", + "integrity": "sha512-0euwPCRyAPSgGdzD1IVN9nJYHtBhJwb6XPfbpQcYbPCwrBidX6GzxmchnaF4sfF/jPb74Ojx5g4yTg3sixlyPw==", + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -3956,6 +4035,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "ip": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" + }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -4210,8 +4294,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "nanocolors": { "version": "0.2.12", @@ -4567,6 +4650,30 @@ "is-fullwidth-code-point": "^3.0.0" } }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" + }, + "socks": { + "version": "2.7.1", + "resolved": "https://registry.npmmirror.com/socks/-/socks-2.7.1.tgz", + "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "requires": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + } + }, + "socks-proxy-agent": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/socks-proxy-agent/-/socks-proxy-agent-8.0.1.tgz", + "integrity": "sha512-59EjPbbgg8U3x62hhKOFVAmySQUcfRQ4C7Q/D5sEHnZTQRrQlNKINks44DMR1gwXp0p4LaVIeccX2KHTTcHVqQ==", + "requires": { + "agent-base": "^7.0.1", + "debug": "^4.3.4", + "socks": "^2.7.1" + } + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", diff --git a/package.json b/package.json index 8abd01f..b1d5b5c 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,9 @@ "author": "Mathieu Colmon", "license": "ISC", "dependencies": { + "https-proxy-agent": "^7.0.0", "jszip": "^3.7.1", + "socks-proxy-agent": "^8.0.1", "ws": "^7.4.3" }, "devDependencies": { diff --git a/src/client.js b/src/client.js index 8a07c4d..81f43f7 100644 --- a/src/client.js +++ b/src/client.js @@ -1,10 +1,11 @@ -const WebSocket = require('ws'); +const WebSocket = require('ws') -const misc = require('./miscRequests'); -const protocol = require('./protocol'); +const misc = require('./miscRequests') +const protocol = require('./protocol') -const quoteSessionGenerator = require('./quote/session'); -const chartSessionGenerator = require('./chart/session'); +const quoteSessionGenerator = require('./quote/session') +const chartSessionGenerator = require('./chart/session') +const proxy = require('./proxy') /** * @typedef {Object} Session @@ -36,18 +37,18 @@ const chartSessionGenerator = require('./chart/session'); /** @class */ module.exports = class Client { - #ws; + #ws #logged = false; /** If the client is logged in */ get isLogged() { - return this.#logged; + return this.#logged } /** If the cient was closed */ get isOpen() { - return this.#ws.readyState === this.#ws.OPEN; + return this.#ws.readyState === this.#ws.OPEN } /** @type {SessionList} */ @@ -69,13 +70,13 @@ module.exports = class Client { * @param {...{}} data Packet data */ #handleEvent(ev, ...data) { - this.#callbacks[ev].forEach((e) => e(...data)); - this.#callbacks.event.forEach((e) => e(ev, ...data)); + this.#callbacks[ev].forEach((e) => e(...data)) + this.#callbacks.event.forEach((e) => e(ev, ...data)) } #handleError(...msgs) { - if (this.#callbacks.error.length === 0) console.error(...msgs); - else this.#handleEvent('error', ...msgs); + if (this.#callbacks.error.length === 0) console.error(...msgs) + else this.#handleEvent('error', ...msgs) } /** @@ -84,7 +85,7 @@ module.exports = class Client { * @event onConnected */ onConnected(cb) { - this.#callbacks.connected.push(cb); + this.#callbacks.connected.push(cb) } /** @@ -93,7 +94,7 @@ module.exports = class Client { * @event onDisconnected */ onDisconnected(cb) { - this.#callbacks.disconnected.push(cb); + this.#callbacks.disconnected.push(cb) } /** @@ -115,7 +116,7 @@ module.exports = class Client { * @event onLogged */ onLogged(cb) { - this.#callbacks.logged.push(cb); + this.#callbacks.logged.push(cb) } /** @@ -124,7 +125,7 @@ module.exports = class Client { * @event onPing */ onPing(cb) { - this.#callbacks.ping.push(cb); + this.#callbacks.ping.push(cb) } /** @@ -133,7 +134,7 @@ module.exports = class Client { * @event onData */ onData(cb) { - this.#callbacks.data.push(cb); + this.#callbacks.data.push(cb) } /** @@ -142,7 +143,7 @@ module.exports = class Client { * @event onError */ onError(cb) { - this.#callbacks.error.push(cb); + this.#callbacks.error.push(cb) } /** @@ -151,63 +152,63 @@ module.exports = class Client { * @event onEvent */ onEvent(cb) { - this.#callbacks.event.push(cb); + this.#callbacks.event.push(cb) } #parsePacket(str) { - if (!this.isOpen) return; + if (!this.isOpen) return protocol.parseWSPacket(str).forEach((packet) => { - if (global.TW_DEBUG) console.log('§90§30§107 CLIENT §0 PACKET', packet); + if (global.TW_DEBUG) console.log('§90§30§107 CLIENT §0 PACKET', packet) if (typeof packet === 'number') { // Ping - this.#ws.send(protocol.formatWSPacket(`~h~${packet}`)); - this.#handleEvent('ping', packet); - return; + this.#ws.send(protocol.formatWSPacket(`~h~${packet}`)) + this.#handleEvent('ping', packet) + return } if (packet.m === 'protocol_error') { // Error - this.#handleError('Client critical error:', packet.p); - this.#ws.close(); - return; + this.#handleError('Client critical error:', packet.p) + this.#ws.close() + return } if (packet.m && packet.p) { // Normal packet const parsed = { type: packet.m, data: packet.p, - }; + } - const session = packet.p[0]; + const session = packet.p[0] if (session && this.#sessions[session]) { - this.#sessions[session].onData(parsed); - return; + this.#sessions[session].onData(parsed) + return } } if (!this.#logged) { - this.#handleEvent('logged', packet); - return; + this.#handleEvent('logged', packet) + return } - this.#handleEvent('data', packet); - }); + this.#handleEvent('data', packet) + }) } #sendQueue = []; /** @type {SendPacket} Send a custom packet */ send(t, p = []) { - this.#sendQueue.push(protocol.formatWSPacket({ m: t, p })); - this.sendQueue(); + this.#sendQueue.push(protocol.formatWSPacket({ m: t, p })) + this.sendQueue() } /** Send all waiting packets */ sendQueue() { while (this.isOpen && this.#logged && this.#sendQueue.length > 0) { - const packet = this.#sendQueue.shift(); - this.#ws.send(packet); - if (global.TW_DEBUG) console.log('§90§30§107 > §0', packet); + const packet = this.#sendQueue.shift() + this.#ws.send(packet) + if (global.TW_DEBUG) console.log('§90§30§107 > §0', packet) } } @@ -223,12 +224,13 @@ module.exports = class Client { * @param {ClientOptions} clientOptions TradingView client options */ constructor(clientOptions = {}) { - if (clientOptions.DEBUG) global.TW_DEBUG = clientOptions.DEBUG; + if (clientOptions.DEBUG) global.TW_DEBUG = clientOptions.DEBUG - const server = clientOptions.server || 'data'; + const server = clientOptions.server || 'data' this.#ws = new WebSocket(`wss://${server}.tradingview.com/socket.io/websocket?&type=chart`, { origin: 'https://s.tradingview.com', - }); + agent: proxy(), + }) if (clientOptions.token) { misc.getUser( @@ -238,32 +240,32 @@ module.exports = class Client { this.#sendQueue.unshift(protocol.formatWSPacket({ m: 'set_auth_token', p: [user.authToken], - })); - this.#logged = true; - this.sendQueue(); + })) + this.#logged = true + this.sendQueue() }).catch((err) => { - this.#handleError('Credentials error:', err.message); - }); + this.#handleError('Credentials error:', err.message) + }) } else { this.#sendQueue.unshift(protocol.formatWSPacket({ m: 'set_auth_token', p: ['unauthorized_user_token'], - })); - this.#logged = true; - this.sendQueue(); + })) + this.#logged = true + this.sendQueue() } this.#ws.on('open', () => { - this.#handleEvent('connected'); - this.sendQueue(); - }); + this.#handleEvent('connected') + this.sendQueue() + }) this.#ws.on('close', () => { - this.#logged = false; - this.#handleEvent('disconnected'); - }); + this.#logged = false + this.#handleEvent('disconnected') + }) - this.#ws.on('message', (data) => this.#parsePacket(data)); + this.#ws.on('message', (data) => this.#parsePacket(data)) } /** @type {ClientBridge} */ @@ -284,8 +286,8 @@ module.exports = class Client { */ end() { return new Promise((cb) => { - if (this.#ws.readyState) this.#ws.close(); - cb(); - }); + if (this.#ws.readyState) this.#ws.close() + cb() + }) } -}; +} diff --git a/src/miscRequests.js b/src/miscRequests.js index 1af6d0b..d4021d3 100644 --- a/src/miscRequests.js +++ b/src/miscRequests.js @@ -1,13 +1,14 @@ -const os = require('os'); -const https = require('https'); -const request = require('./request'); -const FormData = require('./FormData'); +const os = require('os') +const https = require('https') +const request = require('./request') +const FormData = require('./FormData') -const PinePermManager = require('./classes/PinePermManager'); -const PineIndicator = require('./classes/PineIndicator'); +const PinePermManager = require('./classes/PinePermManager') +const PineIndicator = require('./classes/PineIndicator') +const proxy = require('./proxy') -const indicators = ['Recommend.Other', 'Recommend.All', 'Recommend.MA']; -const builtInIndicList = []; +const indicators = ['Recommend.Other', 'Recommend.All', 'Recommend.MA'] +const builtInIndicList = [] async function fetchScanData(tickers = [], type = '', columns = []) { let { data } = await request({ @@ -17,17 +18,18 @@ async function fetchScanData(tickers = [], type = '', columns = []) { headers: { 'Content-Type': 'application/json', }, - }, true, JSON.stringify({ symbols: { tickers }, columns })); + agent: proxy(), + }, true, JSON.stringify({ symbols: { tickers }, columns })) - if (!data.startsWith('{')) throw new Error('Wrong screener or symbol'); + if (!data.startsWith('{')) throw new Error('Wrong screener or symbol') try { - data = JSON.parse(data); + data = JSON.parse(data) } catch (e) { - throw new Error('Can\'t parse server response'); + throw new Error('Can\'t parse server response') } - return data; + return data } /** @typedef {number} advice */ @@ -70,22 +72,22 @@ module.exports = { * @returns {Screener} */ getScreener(exchange) { - const e = exchange.toUpperCase(); - if (['NASDAQ', 'NYSE', 'NYSE ARCA', 'OTC'].includes(e)) return 'america'; - if (['ASX'].includes(e)) return 'australia'; - if (['TSX', 'TSXV', 'CSE', 'NEO'].includes(e)) return 'canada'; - if (['EGX'].includes(e)) return 'egypt'; - if (['FWB', 'SWB', 'XETR'].includes(e)) return 'germany'; - if (['BSE', 'NSE'].includes(e)) return 'india'; - if (['TASE'].includes(e)) return 'israel'; - if (['MIL', 'MILSEDEX'].includes(e)) return 'italy'; - if (['LUXSE'].includes(e)) return 'luxembourg'; - if (['NEWCONNECT'].includes(e)) return 'poland'; - if (['NGM'].includes(e)) return 'sweden'; - if (['BIST'].includes(e)) return 'turkey'; - if (['LSE', 'LSIN'].includes(e)) return 'uk'; - if (['HNX'].includes(e)) return 'vietnam'; - return exchange.toLowerCase(); + const e = exchange.toUpperCase() + if (['NASDAQ', 'NYSE', 'NYSE ARCA', 'OTC'].includes(e)) return 'america' + if (['ASX'].includes(e)) return 'australia' + if (['TSX', 'TSXV', 'CSE', 'NEO'].includes(e)) return 'canada' + if (['EGX'].includes(e)) return 'egypt' + if (['FWB', 'SWB', 'XETR'].includes(e)) return 'germany' + if (['BSE', 'NSE'].includes(e)) return 'india' + if (['TASE'].includes(e)) return 'israel' + if (['MIL', 'MILSEDEX'].includes(e)) return 'italy' + if (['LUXSE'].includes(e)) return 'luxembourg' + if (['NEWCONNECT'].includes(e)) return 'poland' + if (['NGM'].includes(e)) return 'sweden' + if (['BIST'].includes(e)) return 'turkey' + if (['LSE', 'LSIN'].includes(e)) return 'uk' + if (['HNX'].includes(e)) return 'vietnam' + return exchange.toLowerCase() }, /** @@ -96,23 +98,23 @@ module.exports = { * @returns {Promise} results */ async getTA(screener, id) { - const advice = {}; + const advice = {} const cols = ['1', '5', '15', '60', '240', '1D', '1W', '1M'] .map((t) => indicators.map((i) => (t !== '1D' ? `${i}|${t}` : i))) - .flat(); + .flat() - const rs = await fetchScanData([id], screener, cols); - if (!rs.data || !rs.data[0]) return false; + const rs = await fetchScanData([id], screener, cols) + if (!rs.data || !rs.data[0]) return false rs.data[0].d.forEach((val, i) => { - const [name, period] = cols[i].split('|'); - const pName = period || '1D'; - if (!advice[pName]) advice[pName] = {}; - advice[pName][name.split('.').pop()] = Math.round(val * 1000) / 500; - }); + const [name, period] = cols[i].split('|') + const pName = period || '1D' + if (!advice[pName]) advice[pName] = {} + advice[pName][name.split('.').pop()] = Math.round(val * 1000) / 500 + }) - return advice; + return advice }, /** @@ -142,16 +144,16 @@ module.exports = { host: 'symbol-search.tradingview.com', path: `/symbol_search/?text=${search.replace(/ /g, '%20')}&type=${filter}`, origin: 'https://www.tradingview.com', - }); + }) return data.map((s) => { - const exchange = s.exchange.split(' ')[0]; - const id = `${exchange}:${s.symbol}`; + const exchange = s.exchange.split(' ')[0] + const id = `${exchange}:${s.symbol}` const screener = (['forex', 'crypto'].includes(s.type) ? s.type : this.getScreener(exchange) - ); + ) return { id, @@ -162,8 +164,8 @@ module.exports = { description: s.description, type: s.type, getTA: () => this.getTA(screener, id), - }; - }); + } + }) }, /** @@ -192,17 +194,17 @@ module.exports = { builtInIndicList.push(...(await request({ host: 'pine-facade.tradingview.com', path: `/pine-facade/list/?filter=${type}`, - })).data); - })); + })).data) + })) } const { data } = await request({ host: 'www.tradingview.com', path: `/pubscripts-suggest-json/?search=${search.replace(/ /g, '%20')}`, - }); + }) function norm(str = '') { - return str.toUpperCase().replace(/[^A-Z]/g, ''); + return str.toUpperCase().replace(/[^A-Z]/g, '') } return [ @@ -222,7 +224,7 @@ module.exports = { source: '', type: (ind.extra && ind.extra.kind) ? ind.extra.kind : 'study', get() { - return module.exports.getIndicator(ind.scriptIdPart, ind.version); + return module.exports.getIndicator(ind.scriptIdPart, ind.version) }, })), @@ -239,10 +241,10 @@ module.exports = { source: ind.scriptSource, type: (ind.extra && ind.extra.kind) ? ind.extra.kind : 'study', get() { - return module.exports.getIndicator(ind.scriptIdPart, ind.version); + return module.exports.getIndicator(ind.scriptIdPart, ind.version) }, })), - ]; + ] }, /** @@ -253,29 +255,29 @@ module.exports = { * @returns {Promise} Indicator */ async getIndicator(id, version = 'last') { - const indicID = id.replace(/ |%/g, '%25'); + const indicID = id.replace(/ |%/g, '%25') let { data } = await request({ host: 'pine-facade.tradingview.com', path: `/pine-facade/translate/${indicID}/${version}`, - }, true); + }, true) try { - data = JSON.parse(data); + data = JSON.parse(data) } catch (e) { - throw new Error(`Inexistent or unsupported indicator: '${id}'`); + throw new Error(`Inexistent or unsupported indicator: '${id}'`) } if (!data.success || !data.result.metaInfo || !data.result.metaInfo.inputs) { - throw new Error(`Inexistent or unsupported indicator: "${data.reason}"`); + throw new Error(`Inexistent or unsupported indicator: "${data.reason}"`) } - const inputs = {}; + const inputs = {} data.result.metaInfo.inputs.forEach((input) => { - if (['text', 'pineId', 'pineVersion'].includes(input.id)) return; + if (['text', 'pineId', 'pineVersion'].includes(input.id)) return - const inlineName = input.name.replace(/ /g, '_').replace(/[^a-zA-Z0-9_]/g, ''); + const inlineName = input.name.replace(/ /g, '_').replace(/[^a-zA-Z0-9_]/g, '') inputs[input.id] = { name: input.name, @@ -287,12 +289,12 @@ module.exports = { value: input.defval, isHidden: !!input.isHidden, isFake: !!input.isFake, - }; + } - if (input.options) inputs[input.id].options = input.options; - }); + if (input.options) inputs[input.id].options = input.options + }) - const plots = {}; + const plots = {} Object.keys(data.result.metaInfo.styles).forEach((plotId) => { const plotTitle = data @@ -301,21 +303,21 @@ module.exports = { .styles[plotId] .title .replace(/ /g, '_') - .replace(/[^a-zA-Z0-9_]/g, ''); + .replace(/[^a-zA-Z0-9_]/g, '') - const titles = Object.values(plots); + const titles = Object.values(plots) if (titles.includes(plotTitle)) { - let i = 2; - while (titles.includes(`${plotTitle}_${i}`)) i += 1; - plots[plotId] = `${plotTitle}_${i}`; - } else plots[plotId] = plotTitle; - }); + let i = 2 + while (titles.includes(`${plotTitle}_${i}`)) i += 1 + plots[plotId] = `${plotTitle}_${i}` + } else plots[plotId] = plotTitle + }) data.result.metaInfo.plots.forEach((plot) => { - if (!plot.target) return; - plots[plot.id] = `${plots[plot.target] ?? plot.target}_${plot.type}`; - }); + if (!plot.target) return + plots[plot.id] = `${plots[plot.target] ?? plot.target}_${plot.type}` + }) return new PineIndicator({ pineId: data.result.metaInfo.scriptIdPart || indicID, @@ -325,7 +327,7 @@ module.exports = { inputs, plots, script: data.result.ilTemplate, - }); + }) }, /** @@ -357,10 +359,10 @@ module.exports = { * @returns {Promise} Token */ async loginUser(username, password, remember = true, UA = 'TWAPI/3.0') { - const formData = new FormData(); - formData.append('username', username); - formData.append('password', password); - if (remember) formData.append('remember', 'on'); + const formData = new FormData() + formData.append('username', username) + formData.append('password', password) + if (remember) formData.append('remember', 'on') const { data, cookies } = await request({ method: 'POST', @@ -371,15 +373,15 @@ module.exports = { 'Content-Type': `multipart/form-data; boundary=${formData.boundary}`, 'User-agent': `${UA} (${os.version()}; ${os.platform()}; ${os.arch()})`, }, - }, false, formData.toString()); + }, false, formData.toString()) - if (data.error) throw new Error(data.error); + if (data.error) throw new Error(data.error) - const sessionCookie = cookies.find((c) => c.includes('sessionid=')); - const session = (sessionCookie.match(/sessionid=(.*?);/) ?? [])[1]; + const sessionCookie = cookies.find((c) => c.includes('sessionid=')) + const session = (sessionCookie.match(/sessionid=(.*?);/) ?? [])[1] - const signCookie = cookies.find((c) => c.includes('sessionid_sign=')); - const signature = (signCookie.match(/sessionid_sign=(.*?);/) ?? [])[1]; + const signCookie = cookies.find((c) => c.includes('sessionid_sign=')) + const signature = (signCookie.match(/sessionid_sign=(.*?);/) ?? [])[1] return { id: data.user.id, @@ -396,7 +398,7 @@ module.exports = { privateChannel: data.user.private_channel, authToken: data.user.auth_token, joinDate: new Date(data.user.date_joined), - }; + } }, /** @@ -411,13 +413,14 @@ module.exports = { return new Promise((cb, err) => { https.get(location, { headers: { cookie: `sessionid=${session}${signature ? `;sessionid_sign=${signature};` : ''}` }, + agent: proxy(), }, (res) => { - let rs = ''; - res.on('data', (d) => { rs += d; }); + let rs = '' + res.on('data', (d) => { rs += d }) res.on('end', async () => { if (res.headers.location && location !== res.headers.location) { - cb(await module.exports.getUser(session, signature, res.headers.location)); - return; + cb(await module.exports.getUser(session, signature, res.headers.location)) + return } if (rs.includes('auth_token')) { cb({ @@ -438,13 +441,13 @@ module.exports = { privateChannel: /"private_channel":"(.*?)"/.exec(rs)[1], authToken: /"auth_token":"(.*?)"/.exec(rs)[1], joinDate: new Date(/"date_joined":"(.*?)"/.exec(rs)[1] || 0), - }); - } else err(new Error('Wrong or expired sessionid/signature')); - }); + }) + } else err(new Error('Wrong or expired sessionid/signature')) + }) - res.on('error', err); - }).end(); - }); + res.on('error', err) + }).end() + }) }, /** @@ -458,15 +461,16 @@ module.exports = { return new Promise((cb, err) => { https.get('https://pine-facade.tradingview.com/pine-facade/list?filter=saved', { headers: { cookie: `sessionid=${session}${signature ? `;sessionid_sign=${signature};` : ''}` }, + agent: proxy(), }, (res) => { - let rs = ''; - res.on('data', (d) => { rs += d; }); + let rs = '' + res.on('data', (d) => { rs += d }) res.on('end', async () => { try { - rs = JSON.parse(rs); + rs = JSON.parse(rs) } catch (error) { - err(new Error('Can\'t parse private indicator list')); - return; + err(new Error('Can\'t parse private indicator list')) + return } cb(rs.map((ind) => ({ @@ -482,17 +486,17 @@ module.exports = { source: ind.scriptSource, type: (ind.extra && ind.extra.kind) ? ind.extra.kind : 'study', get() { - return module.exports.getIndicator(ind.scriptIdPart, ind.version); + return module.exports.getIndicator(ind.scriptIdPart, ind.version) }, getManager() { - return new PinePermManager(ind.scriptIdPart); + return new PinePermManager(ind.scriptIdPart) }, - }))); - }); + }))) + }) - res.on('error', err); - }).end(); - }); + res.on('error', err) + }).end() + }) }, /** @@ -510,20 +514,20 @@ module.exports = { * @returns {Promise} Token */ async getChartToken(layout, credentials = {}) { - const creds = credentials.id && credentials.session; - const userID = creds ? credentials.id : -1; - const session = creds ? credentials.session : null; - const signature = creds ? credentials.signature : null; + const creds = credentials.id && credentials.session + const userID = creds ? credentials.id : -1 + const session = creds ? credentials.session : null + const signature = creds ? credentials.signature : null const { data } = await request({ host: 'www.tradingview.com', path: `/chart-token/?image_url=${layout}&user_id=${userID}`, headers: { cookie: session ? `sessionid=${session}${signature ? `;sessionid_sign=${signature};` : ''}` : '' }, - }); + }) - if (!data.token) throw new Error('Wrong layout or credentials'); + if (!data.token) throw new Error('Wrong layout or credentials') - return data.token; + return data.token }, /** @@ -558,22 +562,22 @@ module.exports = { * @returns {Promise} Drawings */ async getDrawings(layout, symbol = '', credentials = {}, chartID = 1) { - const chartToken = await module.exports.getChartToken(layout, credentials); - const creds = credentials.id && credentials.session; - const session = creds ? credentials.session : null; - const signature = creds ? credentials.signature : null; + const chartToken = await module.exports.getChartToken(layout, credentials) + const creds = credentials.id && credentials.session + const session = creds ? credentials.session : null + const signature = creds ? credentials.signature : null const { data } = await request({ host: 'charts-storage.tradingview.com', path: `/charts-storage/layout/${layout}/sources?chart_id=${chartID - }&jwt=${chartToken}${symbol ? `&symbol=${symbol}` : ''}`, + }&jwt=${chartToken}${symbol ? `&symbol=${symbol}` : ''}`, headers: { cookie: session ? `sessionid=${session}${signature ? `;sessionid_sign=${signature};` : ''}` : '' }, - }); + }) - if (!data.payload) throw new Error('Wrong layout, user credentials, or chart id.'); + if (!data.payload) throw new Error('Wrong layout, user credentials, or chart id.') return Object.values(data.payload.sources || {}).map((drawing) => ({ ...drawing, ...drawing.state, - })); + })) }, -}; +} diff --git a/src/proxy.js b/src/proxy.js new file mode 100644 index 0000000..6515c63 --- /dev/null +++ b/src/proxy.js @@ -0,0 +1,20 @@ +const { SocksProxyAgent } = require('socks-proxy-agent') +const { HttpsProxyAgent } = require('https-proxy-agent') + +let proxyAgent = null + +module.exports = () => proxyAgent + +module.exports.setAgent = (proxy) => { + if (proxy === '') { + proxyAgent = null + return + } + + if (proxy.includes('http')) { + proxyAgent = new HttpsProxyAgent(proxy) + return + } + + proxyAgent = new SocksProxyAgent(proxy.replace('socks5://', 'socks://')) +} diff --git a/src/request.js b/src/request.js index fa6e057..62f134e 100644 --- a/src/request.js +++ b/src/request.js @@ -1,4 +1,5 @@ -const https = require('https'); +const https = require('https') +const proxy = require('./proxy') /** * @param {https.RequestOptions} options HTTPS Request options @@ -8,30 +9,30 @@ const https = require('https'); */ function request(options = {}, raw = false, content = '') { return new Promise((cb, err) => { - const req = https.request(options, (res) => { - let data = ''; - res.on('data', (c) => { data += c; }); + const req = https.request({ agent: proxy(), ...options }, (res) => { + let data = '' + res.on('data', (c) => { data += c }) res.on('end', () => { if (raw) { - cb({ data, cookies: res.headers['set-cookie'] }); - return; + cb({ data, cookies: res.headers['set-cookie'] }) + return } try { - data = JSON.parse(data); + data = JSON.parse(data) } catch (error) { - console.log(data); - err(new Error('Can\'t parse server response')); - return; + console.log(data) + err(new Error('Can\'t parse server response')) + return } - cb({ data, cookies: res.headers['set-cookie'] }); - }); - }); + cb({ data, cookies: res.headers['set-cookie'] }) + }) + }) - req.on('error', err); - req.end(content); - }); + req.on('error', err) + req.end(content) + }) } -module.exports = request; +module.exports = request