diff --git a/package-lock.json b/package-lock.json index d02a360e7..c32a8ef7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,8 @@ "png-chunks-encode": "^1.0.0", "png-chunks-extract": "^1.0.0", "rimraf": "^3.0.2", - "sanitize-filename": "^1.6.3" + "sanitize-filename": "^1.6.3", + "ws": "^8.13.0" }, "bin": { "TavernAI": "server.js" @@ -1964,6 +1965,26 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xhr": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", @@ -3491,6 +3512,12 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "requires": {} + }, "xhr": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", diff --git a/package.json b/package.json index a69b31652..8c861fb1e 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "png-chunks-encode": "^1.0.0", "png-chunks-extract": "^1.0.0", "rimraf": "^3.0.2", - "sanitize-filename": "^1.6.3" + "sanitize-filename": "^1.6.3", + "ws": "^8.13.0" }, "name": "TavernAI", "version": "1.2.0", diff --git a/poe-test.js b/poe-test.js new file mode 100644 index 000000000..b5a216213 --- /dev/null +++ b/poe-test.js @@ -0,0 +1,21 @@ +const poe = require('./poe'); + +async function test() { + const client = new poe.Client(); + await client.init('pb-cookie'); + + const bots = client.get_bot_names(); + console.log(bots); + + await client.purge_conversation('a2', -1); + + let reply; + for await (const mes of client.send_message('a2', 'Hello')) { + reply = mes.text; + } + + console.log(reply); + client.disconnect_ws(); +} + +test(); \ No newline at end of file diff --git a/poe.js b/poe.js new file mode 100644 index 000000000..eabbfbca9 --- /dev/null +++ b/poe.js @@ -0,0 +1,403 @@ +const WebSocket = require('ws'); +const axios = require('axios'); +const fs = require('fs'); +const path = require('path'); +const http = require('http'); +const https = require('https'); + +const parent_path = path.resolve(__dirname); +const queries_path = path.join(parent_path, "poe_graphql"); +let queries = {}; + +const logger = console; + +const user_agent = "Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0"; + +function load_queries() { + const files = fs.readdirSync(queries_path); + for (const filename of files) { + const ext = path.extname(filename); + if (ext !== '.graphql') { + continue; + } + const queryName = path.basename(filename, ext); + const query = fs.readFileSync(path.join(queries_path, filename), 'utf-8'); + queries[queryName] = query; + } +} + +function generate_payload(query_name, variables) { + return { + query: queries[query_name], + variables: variables, + } +} + +async function request_with_retries(method, attempts = 10) { + const url = ''; + for (let i = 0; i < attempts; i++) { + try { + const response = await method(); + if (response.status === 200) { + return response; + } + logger.warn(`Server returned a status code of ${response.status} while downloading ${url}. Retrying (${i + 1}/${attempts})...`); + } + catch (err) { + console.log(err); + } + } + throw new Error(`Failed to download ${url} too many times.`); +} + +class Client { + gql_url = "https://poe.com/api/gql_POST"; + gql_recv_url = "https://poe.com/api/receive_POST"; + home_url = "https://poe.com"; + settings_url = "https://poe.com/api/settings"; + + formkey = ""; + next_data = {}; + bots = {}; + active_messages = {}; + message_queues = {}; + bot_names = []; + ws = null; + ws_connected = false; + auto_reconnect = false; + + constructor(auto_reconnect = false) { + this.auto_reconnect = auto_reconnect; + } + + async init(token, proxy = null) { + this.proxy = proxy; + this.session = axios.default.create({ + timeout: 60000, + httpAgent: new http.Agent({ keepAlive: true }), + httpsAgent: new https.Agent({ keepAlive: true }), + }); + if (proxy) { + this.session.defaults.proxy = { + "http": proxy, + "https": proxy, + }; + logger.info(`Proxy enabled: ${proxy}`); + } + const cookies = `p-b=${token}; Domain=poe.com`; + this.headers = { + "User-Agent": user_agent, + "Referrer": "https://poe.com/", + "Origin": "https://poe.com", + "Cookie": cookies, + }; + this.ws_domain = `tch${Math.floor(Math.random() * 1e6)}`; + this.session.defaults.headers.common = this.headers; + this.next_data = await this.get_next_data(); + this.channel = await this.get_channel_data(); + await this.connect_ws(); + this.bots = await this.get_bots(); + this.bot_names = this.get_bot_names(); + this.gql_headers = { + "poe-formkey": this.formkey, + "poe-tchannel": this.channel["channel"], + ...this.headers, + }; + await this.subscribe(); + } + + async get_next_data() { + logger.info('Downloading next_data...'); + + const r = await request_with_retries(() => this.session.get(this.home_url)); + const jsonRegex = /