diff --git a/README.md b/README.md index a3fe36e..7d3f97e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,33 @@ # Quora Poe -This is a CLI tool to call the Quora Poe API through GraphQL. It is a work in progress, and currently supports the following: +This is a tool to call the Quora Poe API through GraphQL. It is a work in progress, and currently supports the following: - Auto login using temporary email, so you don't need to use your own email/phone number. -- Semi auto login using your own email/phone number, you need to enter the OTP manually. +- Manual login using your own email/phone number, you need to enter the OTP manually. - Chat with 4 types of bots (Sage, Claude, ChatGPT, and Dragonfly). - Stream responses support from the bot. - Clear the chat history. +- Auto re-login after session expires (only for auto login). +- Module support, now you can use this tool as a module. + +# Demo's +## CLI +
+ +
+ +## Client module (after login) ++ +
+ +## Client module (before login) ++ +
+ +## Client module (auto re-login after session expires) ++ +
## Requirements - NodeJS 16.0.0 or higher @@ -19,19 +42,36 @@ npm install ``` ## Usage - -To start, run: - +### Module +Please see file [`./src/client_auto.ts`](./src/client_auto.ts) or [`./src/client_manual.ts`](./src/client_manual.ts) for example. +Or you can try to run the following command: ``` -npm start +node ./dist/client_auto.js ``` +### CLI +``` +npm run cli +``` +If you don't want stream responses, you can change the `stream_response` variable in the `config.json` file to `false`. ## TODO List -- [ ] Make it modular, so it can be used as a library -- [ ] Add support for re-login after session expires - [ ] Add support for get chat history - [ ] Add support for delete message +## Notes +- Since I have to work on this project in my free time, I can't guarantee that I will be able to update this project frequently. +- I'm not have much experience with TypeScript, so if you have any suggestions, best practices, or anything, please let me know or create a pull request. +- I'm not publishing this tool to NPM yet. If you want to use this tool as a module, you can clone this repo and build it yourself. +- If you have any questions, please create an issue. + ## Contributing +To contribute to this repo, fork it first, then create a new branch, and create a pull request. + +## Disclaimer +This tool is not affiliated with Quora in any way. I am not responsible for any misuse of this tool. +Don't sue me. + +Also, please don't use auto login feature to spam the bot, like creating a lot of accounts for any purpose. I don't want this temporary email service to be banned. -To contribute to this repo, fork first and create a pull request. +## License +[MIT](https://choosealicense.com/licenses/mit/) \ No newline at end of file diff --git a/config.example.json b/config.example.json index fa7f775..b8ea8e3 100644 --- a/config.example.json +++ b/config.example.json @@ -1 +1,27 @@ -{"quora_formkey":"","quora_cookie":""} \ No newline at end of file +{ + "stream_response": true, + "quora_formkey": "", + "quora_cookie": "", + "channel_name": "", + "app_settings": { + "formkey": "", + "tchannelData": { + "minSeq": "", + "channel": "", + "channelHash": "", + "boxName": "", + "baseHost": "", + "targetUrl": "", + "enableWebsocket": true + } + }, + "chat_ids": { + "a2": 0, + "capybara": 0, + "nutria": 0, + "chinchilla": 0 + }, + "auto_login": true, + "email": "", + "sid_token": "" +} \ No newline at end of file diff --git a/demos/demo-client-auto-after-login.gif b/demos/demo-client-auto-after-login.gif new file mode 100644 index 0000000..6d59f1f Binary files /dev/null and b/demos/demo-client-auto-after-login.gif differ diff --git a/demos/demo-client-auto-before-login.gif b/demos/demo-client-auto-before-login.gif new file mode 100644 index 0000000..81d5625 Binary files /dev/null and b/demos/demo-client-auto-before-login.gif differ diff --git a/demos/demo-client-auto-re-login.gif b/demos/demo-client-auto-re-login.gif new file mode 100644 index 0000000..2623928 Binary files /dev/null and b/demos/demo-client-auto-re-login.gif differ diff --git a/demos/demo-converstation-cli.gif b/demos/demo-converstation-cli.gif new file mode 100644 index 0000000..9aa3d65 Binary files /dev/null and b/demos/demo-converstation-cli.gif differ diff --git a/dist/cli.js b/dist/cli.js new file mode 100644 index 0000000..5daf215 --- /dev/null +++ b/dist/cli.js @@ -0,0 +1,3 @@ +import ChatBot from "./index.js"; +const bot = new ChatBot(); +await bot.startCli(); diff --git a/dist/client.js b/dist/client.js new file mode 100644 index 0000000..fb6b744 --- /dev/null +++ b/dist/client.js @@ -0,0 +1,11 @@ +import ChatBot from "./index.js"; +const bot = new ChatBot(); +// Used to check if the formkey and cookie is available +const isFormkeyAvailable = await bot.getCredentials(); +if (!isFormkeyAvailable) { + console.log("Formkey and cookie not available"); + // Set the formkey, cookie and any other data needed and save it into config.json + await bot.setCredentials(); + const chatId = await bot.getChatId("a2"); + console.log(chatId); +} diff --git a/dist/client_auto.js b/dist/client_auto.js new file mode 100644 index 0000000..08051f8 --- /dev/null +++ b/dist/client_auto.js @@ -0,0 +1,26 @@ +import ChatBot from "./index.js"; +const bot = new ChatBot(); +// Used to check if the formkey and cookie is available +const isFormkeyAvailable = await bot.getCredentials(); +if (!isFormkeyAvailable) { + await bot.setCredentials(); + await bot.subscribe(); // for websocket(stream response) purpose + await bot.login("auto"); +} +const ai = "a2"; // bot list are in config.example.json, key "chat_ids" +// If you want to clear the chat context, you can use this +await bot.clearContext(ai); +// If you want to get the response (with stream), you can use this +// NOTE that you need to call this before you send the message +// await getUpdatedSettings(bot.config.channel_name, bot.config.quora_cookie); +// await bot.subscribe(); +// const ws = await connectWs(); +// If you want to send a message, you can use this +await bot.sendMsg(ai, "Hello, who are you?"); +// If you want to get the response (without stream), you can use this +const response = await bot.getResponse(ai); +console.log(response); +// // If you want to get the response (with stream), you can use this +// process.stdout.write("Response: "); +// await listenWs(ws); +// console.log('\n'); diff --git a/dist/client_manual.js b/dist/client_manual.js new file mode 100644 index 0000000..765ec9a --- /dev/null +++ b/dist/client_manual.js @@ -0,0 +1,39 @@ +import ChatBot from "./index.js"; +const bot = new ChatBot(); +// Used to check if the formkey and cookie is available +const isFormkeyAvailable = await bot.getCredentials(); +if (!isFormkeyAvailable) { + console.log("Formkey and cookie not available"); + // Set the formkey, cookie and any other data needed and save it into config.json + await bot.setCredentials(); + const myEmail = "myemail@mail.com"; + const signInStatus = await bot.sendVerifCode(null, myEmail); + // After you get the verification code, you can use this step to log in + // then check signInStatus + let loginStatus = "invalid_verification_code"; + while (loginStatus !== "success") { + if (signInStatus === 'user_with_confirmed_phone_number_not_found') { + loginStatus = await bot.signUpWithVerificationCode(myEmail, null, 123456); // 123456 is the verification code + } + else { + loginStatus = await bot.signInOrUp(myEmail, null, 123456); // 123456 is the verification code + } + } +} +const ai = "a2"; // bot list are in config.example.json, key "chat_ids" +// If you want to clear the chat context, you can use this +await bot.clearContext(ai); +// If you want to get the response (with stream), you can use this +// NOTE that you need to call this before you send the message +// await getUpdatedSettings(bot.config.channel_name, bot.config.quora_cookie); +// await bot.subscribe(); +// const ws = await connectWs(); +// If you want to send a message, you can use this +await bot.sendMsg(ai, "Hello, who are you?"); +// If you want to get the response (without stream), you can use this +const response = await bot.getResponse(ai); +console.log(response); +// // If you want to get the response (with stream), you can use this +// process.stdout.write("Response: "); +// await listenWs(ws); +// console.log('\n'); diff --git a/dist/credential.js b/dist/credential.js index 6c495a6..d5c813f 100644 --- a/dist/credential.js +++ b/dist/credential.js @@ -19,7 +19,7 @@ const getUpdatedSettings = async (channelName, pbCookie) => { const appSettings = await _setting.json(), { tchannelData: { minSeq: minSeq } } = appSettings; const credentials = JSON.parse(readFileSync("config.json", "utf8")); credentials.app_settings.tchannelData.minSeq = minSeq; - writeFile("config.json", JSON.stringify(credentials), function (err) { + writeFile("config.json", JSON.stringify(credentials, null, 4), function (err) { if (err) { console.log(err); } diff --git a/dist/index.js b/dist/index.js index ec401f1..861c527 100644 --- a/dist/index.js +++ b/dist/index.js @@ -3,9 +3,10 @@ import prompts from "prompts"; import ora from "ora"; import * as dotenv from "dotenv"; import { readFileSync, writeFile } from "fs"; -import { scrape, getUpdatedSettings } from "./credential.js"; -import { listenWs, connectWs, disconnectWs } from "./websocket.js"; +import { getUpdatedSettings, scrape } from "./credential.js"; +import { connectWs, disconnectWs, listenWs } from "./websocket.js"; import * as mail from "./mail.js"; +import randomUseragent from 'random-useragent'; dotenv.config(); const spinner = ora({ color: "cyan", @@ -20,53 +21,66 @@ const queries = { signUpWithVerificationCodeMutation: readFileSync(gqlDir + "/SignupWithVerificationCodeMutation.graphql", "utf8"), sendVerificationCodeMutation: readFileSync(gqlDir + "/SendVerificationCodeForLoginMutation.graphql", "utf8"), }; -let [pbCookie, channelName, appSettings, formkey] = ["", "", "", ""]; class ChatBot { constructor() { + this.config = JSON.parse(readFileSync("config.json", "utf8")); this.headers = { 'Content-Type': 'application/json', - 'Accept': '*/*', 'Host': 'poe.com', - 'Accept-Encoding': 'gzip, deflate, br', 'Connection': 'keep-alive', 'Origin': 'https://poe.com', + 'User-Agent': randomUseragent.getRandom(), }; this.chatId = 0; this.bot = ""; + this.reConnectWs = false; } async getCredentials() { - const credentials = JSON.parse(readFileSync("config.json", "utf8")); - const { quora_formkey, quora_cookie } = credentials; + const { quora_formkey, channel_name, quora_cookie } = this.config; if (quora_formkey.length > 0 && quora_cookie.length > 0) { - formkey = quora_formkey; - pbCookie = quora_cookie; - // For websocket later feature - channelName = credentials.channel_name; - appSettings = credentials.app_settings; - this.headers["poe-formkey"] = formkey; - this.headers["poe-tchannel"] = channelName; - this.headers["Cookie"] = pbCookie; + this.headers["poe-formkey"] = quora_formkey; + this.headers["poe-tchannel"] = channel_name; + this.headers["Cookie"] = quora_cookie; } return quora_formkey.length > 0 && quora_cookie.length > 0; } + async setChatIds() { + const [a2, capybara, nutria, chinchilla] = await Promise.all([ + this.getChatId("a2"), + this.getChatId("capybara"), + this.getChatId("nutria"), + this.getChatId("chinchilla"), + ]); + const credentials = JSON.parse(readFileSync("config.json", "utf8")); + credentials.chat_ids = { + a2, + capybara, + nutria, + chinchilla, + }; + this.config.chat_ids = { + a2, + capybara, + nutria, + chinchilla, + }; + writeFile("config.json", JSON.stringify(credentials, null, 4), function (err) { + if (err) { + console.log(err); + } + }); + } async setCredentials() { let result = await scrape(); - const credentials = JSON.parse(readFileSync("config.json", "utf8")); - credentials.quora_formkey = result.appSettings.formkey; - credentials.quora_cookie = result.pbCookie; - // For websocket later feature - credentials.channel_name = result.channelName; - credentials.app_settings = result.appSettings; + this.config.quora_formkey = result.appSettings.formkey; + this.config.quora_cookie = result.pbCookie; + this.config.channel_name = result.channelName; + this.config.app_settings = result.appSettings; // set value - formkey = result.appSettings.formkey; - pbCookie = result.pbCookie; - // For websocket later feature - channelName = result.channelName; - appSettings = result.appSettings; - this.headers["poe-formkey"] = formkey; - this.headers["poe-tchannel"] = channelName; - this.headers["Cookie"] = pbCookie; - writeFile("config.json", JSON.stringify(credentials), function (err) { + this.headers["poe-formkey"] = this.config.quora_formkey; + this.headers["poe-tchannel"] = this.config.channel_name; + this.headers["Cookie"] = this.config.quora_cookie; + writeFile("config.json", JSON.stringify(this.config, null, 4), function (err) { if (err) { console.log(err); } @@ -91,89 +105,7 @@ class ChatBot { }; await this.makeRequest(query); } - async start() { - const isFormkeyAvailable = await this.getCredentials(); - if (!isFormkeyAvailable) { - const { mode } = await prompts({ - type: "select", - name: "mode", - message: "Select", - choices: [ - { title: "Auto [This will use temp email to get Verification Code]", value: "auto" }, - { title: "Semi-Auto [Use you own email/phone number]", value: "semi" }, - { title: "exit", value: "exit" } - ], - }); - if (mode === "exit") { - process.exit(0); - } - await this.setCredentials(); - await this.subscribe(); - await this.login(mode); - } - await getUpdatedSettings(channelName, pbCookie); - await this.subscribe(); - const ws = await connectWs(); - const { bot } = await prompts({ - type: "select", - name: "bot", - message: "Select", - choices: [ - { title: "Claude (Powered by Anthropic)", value: "a2" }, - { title: "Sage (Powered by OpenAI - logical)", value: "capybara" }, - { title: "Dragonfly (Powered by OpenAI - simpler)", value: "nutria" }, - { title: "ChatGPT (Powered by OpenAI - current)", value: "chinchilla" }, - ], - }); - await this.getChatId(bot); - let helpMsg = "Available commands: !help !exit, !clear, !submit" + - "\n!help - show this message" + - "\n!exit - exit the chat" + - "\n!clear - clear chat history" + - "\n!submit - submit prompt"; - await this.clearContext(); - console.log(helpMsg); - let submitedPrompt = ""; - while (true) { - const { prompt } = await prompts({ - type: "text", - name: "prompt", - message: "Ask:", - }); - if (prompt.length > 0) { - if (prompt === "!help") { - console.log(helpMsg); - } - else if (prompt === "!exit") { - await disconnectWs(ws); - process.exit(0); - } - else if (prompt === "!clear") { - spinner.start("Clearing chat history..."); - await this.clearContext(); - submitedPrompt = ""; - spinner.stop(); - console.log("Chat history cleared"); - } - else if (prompt === "!submit") { - if (submitedPrompt.length === 0) { - console.log("No prompt to submit"); - continue; - } - await this.sendMsg(submitedPrompt); - process.stdout.write("Response: "); - await listenWs(ws); - console.log('\n'); - submitedPrompt = ""; - } - else { - submitedPrompt += prompt + "\n"; - } - } - } - } async makeRequest(request) { - this.headers["Content-Length"] = Buffer.byteLength(JSON.stringify(request), 'utf8'); const response = await fetch('https://poe.com/api/gql_POST', { method: 'POST', headers: this.headers, @@ -248,6 +180,7 @@ class ChatBot { } spinner.stop(); } + await this.setChatIds(); } async signInOrUp(phoneNumber, email, verifyCode) { console.log("Signing in/up..."); @@ -305,9 +238,19 @@ class ChatBot { return status; } catch (e) { - throw e; + console.log("Error sending verification code, please try again " + e); + await this.resetConfig(); } } + async resetConfig() { + const defaultConfig = JSON.parse(readFileSync("config.example.json", "utf8")); + console.log("Resetting config..."); + writeFile("config.json", JSON.stringify(defaultConfig, null, 4), function (err) { + if (err) { + console.log(err); + } + }); + } async getChatId(bot) { try { const { data: { chatOfBot: { chatId } } } = await this.makeRequest({ @@ -318,65 +261,235 @@ class ChatBot { }); this.chatId = chatId; this.bot = bot; + return chatId; } catch (e) { + console.log(e); + await this.resetConfig(); throw new Error("Could not get chat id, invalid formkey or cookie! Please remove the quora_formkey value from the config.json file and try again."); } } - async clearContext() { + async clearContext(bot) { try { - await this.makeRequest({ + const data = await this.makeRequest({ query: `${queries.addMessageBreakMutation}`, - variables: { chatId: this.chatId }, + variables: { chatId: this.config.chat_ids[bot] }, }); + if (!data.data) { + this.reConnectWs = true; // for websocket purpose + console.log("ON TRY! Could not clear context! Trying to reLogin.."); + await this.reLogin(); + await this.clearContext(bot); + } + return data; } catch (e) { - throw new Error("Could not clear context"); + this.reConnectWs = true; // for websocket purpose + console.log("ON CATCH! Could not clear context! Trying to reLogin.."); + await this.reLogin(); + await this.clearContext(bot); + return e; } } - async sendMsg(query) { + async sendMsg(bot, query) { try { - await this.makeRequest({ + const data = await this.makeRequest({ query: `${queries.addHumanMessageMutation}`, variables: { - bot: this.bot, - chatId: this.chatId, + bot: bot, + chatId: this.config.chat_ids[bot], query: query, source: null, withChatBreak: false }, }); + if (!data.data) { + this.reConnectWs = true; // for cli websocket purpose + console.log("Could not send message! Trying to reLogin.."); + await this.reLogin(); + await this.sendMsg(bot, query); + } + return data; } catch (e) { - throw new Error("Could not send message"); + this.reConnectWs = true; // for cli websocket purpose + console.log("ON CATCH! Could not send message! Trying to reLogin.."); + await this.reLogin(); + await this.sendMsg(bot, query); + return e; } } - // Responce without stream - async getResponse() { + async getResponse(bot) { let text; let state; let authorNickname; + try { + while (true) { + await new Promise((resolve) => setTimeout(resolve, 2000)); + let response = await this.makeRequest({ + query: `${queries.chatPaginationQuery}`, + variables: { + before: null, + bot: bot, + last: 1, + }, + }); + let base = response.data.chatOfBot.messagesConnection.edges; + let lastEdgeIndex = base.length - 1; + text = base[lastEdgeIndex].node.text; + authorNickname = base[lastEdgeIndex].node.authorNickname; + state = base[lastEdgeIndex].node.state; + if (state === "complete" && authorNickname === bot) { + break; + } + } + } + catch (e) { + console.log("Could not get response!"); + return { + status: false, + message: "failed", + data: null, + }; + } + return { + status: true, + message: "success", + data: text, + }; + } + async reLogin() { + await this.setCredentials(); + if (!this.config.email || !this.config.sid_token) { + console.log("No email or sid_token found, creating new email and sid_token.."); + const { email, sid_token } = await mail.createNewEmail(); + this.config.email = email; + this.config.sid_token = sid_token; + } + const status = await this.sendVerifCode(null, this.config.email); + spinner.start("Waiting for OTP code..."); + const otp_code = await mail.getPoeOTPCode(this.config.sid_token); + spinner.stop(); + if (status === 'user_with_confirmed_email_not_found') { + await this.signUpWithVerificationCode(null, this.config.email, otp_code); + } + else { + await this.signInOrUp(null, this.config.email, otp_code); + } + const newConfig = JSON.parse(readFileSync("config.json", "utf8")); + this.config = newConfig; + this.headers["poe-formkey"] = newConfig.quora_formkey; + this.headers["poe-tchannel"] = newConfig.channel_name; + this.headers["Cookie"] = newConfig.quora_cookie; + await this.setChatIds(); + } + async startCli() { + const isFormkeyAvailable = await this.getCredentials(); + if (!isFormkeyAvailable) { + const { mode } = await prompts({ + type: "select", + name: "mode", + message: "Select", + choices: [ + { title: "Auto [This will use temp email to get Verification Code]", value: "auto" }, + { title: "Semi-Auto [Use you own email/phone number]", value: "semi" }, + { title: "exit", value: "exit" } + ], + }); + if (mode === "exit") { + process.exit(0); + } + await this.setCredentials(); + await this.subscribe(); + await this.login(mode); + } + let ws; + if (this.config.stream_response) { + await getUpdatedSettings(this.config.channel_name, this.config.quora_cookie); + await this.subscribe(); + ws = await connectWs(); + } + const { bot } = await prompts({ + type: "select", + name: "bot", + message: "Select", + choices: [ + { title: "Claude (Powered by Anthropic)", value: "a2" }, + { title: "Sage (Powered by OpenAI - logical)", value: "capybara" }, + { title: "Dragonfly (Powered by OpenAI - simpler)", value: "nutria" }, + { title: "ChatGPT (Powered by OpenAI - current)", value: "chinchilla" }, + ], + }); + this.chatId = this.config.chat_ids[bot]; + this.bot = bot; + let helpMsg = "Available commands: !help !exit, !clear, !submit" + + "\n!help - show this message" + + "\n!exit - exit the chat" + + "\n!clear - clear chat history" + + "\n!submit - submit prompt"; + // await this.clearContext(this.chatId); + console.log(helpMsg); + let submitedPrompt = ""; while (true) { - await new Promise((resolve) => setTimeout(resolve, 2000)); - let response = await this.makeRequest({ - query: `${queries.chatPaginationQuery}`, - variables: { - before: null, - bot: this.bot, - last: 1, - }, + const { prompt } = await prompts({ + type: "text", + name: "prompt", + message: "Ask:", }); - let base = response.data.chatOfBot.messagesConnection.edges; - let lastEdgeIndex = base.length - 1; - text = base[lastEdgeIndex].node.text; - authorNickname = base[lastEdgeIndex].node.authorNickname; - state = base[lastEdgeIndex].node.state; - if (state === "complete" && authorNickname === this.bot) { - break; + if (prompt.length > 0) { + if (prompt === "!help") { + console.log(helpMsg); + } + else if (prompt === "!exit") { + process.exit(0); + } + else if (prompt === "!clear") { + spinner.start("Clearing chat history..."); + await this.clearContext(bot); + if (this.config.stream_response) { + if (this.reConnectWs) { + await disconnectWs(ws); + await getUpdatedSettings(this.config.channel_name, this.config.quora_cookie); + await this.subscribe(); + ws = await connectWs(); + this.reConnectWs = false; + } + } + submitedPrompt = ""; + spinner.stop(); + console.log("Chat history cleared"); + } + else if (prompt === "!submit") { + if (submitedPrompt.length === 0) { + console.log("No prompt to submit"); + continue; + } + await this.sendMsg(this.bot, submitedPrompt); + if (this.config.stream_response) { + if (this.reConnectWs) { + await disconnectWs(ws); + await getUpdatedSettings(this.config.channel_name, this.config.quora_cookie); + await this.subscribe(); + ws = await connectWs(); + this.reConnectWs = false; + } + process.stdout.write("Response: "); + await listenWs(ws); + console.log('\n'); + } + else { + spinner.start("Waiting for response..."); + let response = await this.getResponse(this.bot); + spinner.stop(); + console.log(response.data); + } + submitedPrompt = ""; + } + else { + submitedPrompt += prompt + "\n"; + } } } - return text; } } -const chatBot = new ChatBot(); -await chatBot.start(); +export default ChatBot; diff --git a/dist/mail.js b/dist/mail.js index 1cdac77..5221d8f 100644 --- a/dist/mail.js +++ b/dist/mail.js @@ -7,7 +7,7 @@ const createNewEmail = async () => { const credentials = JSON.parse(readFileSync("config.json", "utf8")); credentials.email = response_json.email_addr; credentials.sid_token = response_json.sid_token; - writeFile("config.json", JSON.stringify(credentials), function (err) { + writeFile("config.json", JSON.stringify(credentials, null, 4), function (err) { if (err) { console.log(err); } @@ -30,7 +30,7 @@ const getLatestEmail = async (sid_token) => { let emailList = await getEmailList(sid_token); let emailListLength = emailList.list.length; while (true) { - await new Promise(r => setTimeout(r, 10000)); + await new Promise(r => setTimeout(r, 15000)); emailList = await getEmailList(sid_token); emailListLength = emailList.list.length; if (emailListLength > 1) { diff --git a/package-lock.json b/package-lock.json index 8464571..998696e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "poe-node", - "version": "1.3.1", + "version": "1.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "poe-node", - "version": "1.3.1", + "version": "1.5.0", "license": "ISC", "dependencies": { "cross-fetch": "^3.1.5", @@ -14,6 +14,7 @@ "dotenv": "^16.0.3", "ora": "^6.1.2", "prompts": "^2.4.2", + "random-useragent": "^0.5.0", "ws": "^8.12.1" }, "devDependencies": { @@ -234,6 +235,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -313,6 +319,25 @@ "node": ">= 6" } }, + "node_modules/random-seed": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/random-seed/-/random-seed-0.3.0.tgz", + "integrity": "sha512-y13xtn3kcTlLub3HKWXxJNeC2qK4mB59evwZ5EkeRlolx+Bp2ztF7LbcZmyCnOqlHQrLnfuNbi1sVmm9lPDlDA==", + "dependencies": { + "json-stringify-safe": "^5.0.1" + }, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/random-useragent": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/random-useragent/-/random-useragent-0.5.0.tgz", + "integrity": "sha512-FUMkqVdZeoSff5tErNL3FFGYXElDWZ1bEuedhm5u9MdCFwANriJWbHvDRYrLTOzp/fBsBGu5J1cWtDgifa97aQ==", + "dependencies": { + "random-seed": "^0.3.0" + } + }, "node_modules/readable-stream": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.1.tgz", @@ -573,6 +598,11 @@ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==" }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -625,6 +655,22 @@ "sisteransi": "^1.0.5" } }, + "random-seed": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/random-seed/-/random-seed-0.3.0.tgz", + "integrity": "sha512-y13xtn3kcTlLub3HKWXxJNeC2qK4mB59evwZ5EkeRlolx+Bp2ztF7LbcZmyCnOqlHQrLnfuNbi1sVmm9lPDlDA==", + "requires": { + "json-stringify-safe": "^5.0.1" + } + }, + "random-useragent": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/random-useragent/-/random-useragent-0.5.0.tgz", + "integrity": "sha512-FUMkqVdZeoSff5tErNL3FFGYXElDWZ1bEuedhm5u9MdCFwANriJWbHvDRYrLTOzp/fBsBGu5J1cWtDgifa97aQ==", + "requires": { + "random-seed": "^0.3.0" + } + }, "readable-stream": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.1.tgz", diff --git a/package.json b/package.json index 59b515c..0fd4cab 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,30 @@ { "name": "poe-node", - "version": "1.4.0", - "description": "A CLI tool to call the Quora Poe API through GraphQL", + "version": "1.5.0", + "description": "Work as CLI or module to call the Quora Poe API through GraphQL", "main": "index.js", "type": "module", "scripts": { "compile": "tsc", "dev": "npm run compile && node ./dist/index.js", - "start": "node ./dist/index.js", - "test": "echo \"Error: no test specified\" && exit 1" + "cli": "node ./dist/cli.js" }, - "keywords": [], - "author": "", + "keywords": [ + "poe", + "quora", + "graphql", + "api", + "cli", + "library", + "chatbot", + "bot", + "openai", + "gpt3", + "gpt-3", + "gpt4", + "chatgpt" + ], + "author": "Ramdani", "license": "ISC", "dependencies": { "cross-fetch": "^3.1.5", @@ -19,6 +32,7 @@ "dotenv": "^16.0.3", "ora": "^6.1.2", "prompts": "^2.4.2", + "random-useragent": "^0.5.0", "ws": "^8.12.1" }, "devDependencies": { diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..7ee64da --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,4 @@ +import ChatBot from "./index.js"; + +const bot = new ChatBot(); +await bot.startCli(); \ No newline at end of file diff --git a/src/client_auto.ts b/src/client_auto.ts new file mode 100644 index 0000000..72fc55e --- /dev/null +++ b/src/client_auto.ts @@ -0,0 +1,37 @@ +import ChatBot from "./index.js"; +import {getUpdatedSettings} from "./credential.js"; +import {connectWs, listenWs} from "./websocket.js"; + +const bot = new ChatBot(); + +// Used to check if the formkey and cookie is available +const isFormkeyAvailable = await bot.getCredentials(); + +if (!isFormkeyAvailable) { + await bot.setCredentials(); + await bot.subscribe() // for websocket(stream response) purpose + await bot.login("auto"); +} + +const ai = "a2"; // bot list are in config.example.json, key "chat_ids" + +// If you want to clear the chat context, you can use this +await bot.clearContext(ai); + +// If you want to get the response (with stream), you can use this +// NOTE that you need to call this before you send the message +// await getUpdatedSettings(bot.config.channel_name, bot.config.quora_cookie); +// await bot.subscribe(); +// const ws = await connectWs(); + +// If you want to send a message, you can use this +await bot.sendMsg(ai, "Hello, who are you?") + +// If you want to get the response (without stream), you can use this +const response = await bot.getResponse(ai); +console.log(response); + +// // If you want to get the response (with stream), you can use this +// process.stdout.write("Response: "); +// await listenWs(ws); +// console.log('\n'); \ No newline at end of file diff --git a/src/client_manual.ts b/src/client_manual.ts new file mode 100644 index 0000000..6f2f7ca --- /dev/null +++ b/src/client_manual.ts @@ -0,0 +1,53 @@ +import ChatBot from "./index.js"; +import {getUpdatedSettings} from "./credential.js"; +import {connectWs, listenWs} from "./websocket.js"; + +const bot = new ChatBot(); + +// Used to check if the formkey and cookie is available +const isFormkeyAvailable = await bot.getCredentials(); + +if (!isFormkeyAvailable) { + console.log("Formkey and cookie not available"); + + // Set the formkey, cookie and any other data needed and save it into config.json + await bot.setCredentials(); + + const myEmail = "myemail@mail.com" + const signInStatus = await bot.sendVerifCode(null, myEmail); + + // After you get the verification code, you can use this step to log in + // then check signInStatus + let loginStatus = "invalid_verification_code"; + while (loginStatus !== "success") { + if (signInStatus === 'user_with_confirmed_phone_number_not_found') { + loginStatus = await bot.signUpWithVerificationCode(myEmail, null, 123456); // 123456 is the verification code + } else { + loginStatus = await bot.signInOrUp(myEmail, null, 123456); // 123456 is the verification code + } + } +} + + +const ai = "a2"; // bot list are in config.example.json, key "chat_ids" + +// If you want to clear the chat context, you can use this +await bot.clearContext(ai); + +// If you want to get the response (with stream), you can use this +// NOTE that you need to call this before you send the message +// await getUpdatedSettings(bot.config.channel_name, bot.config.quora_cookie); +// await bot.subscribe(); +// const ws = await connectWs(); + +// If you want to send a message, you can use this +await bot.sendMsg(ai, "Hello, who are you?") + +// If you want to get the response (without stream), you can use this +const response = await bot.getResponse(ai); +console.log(response); + +// // If you want to get the response (with stream), you can use this +// process.stdout.write("Response: "); +// await listenWs(ws); +// console.log('\n'); \ No newline at end of file diff --git a/src/credential.ts b/src/credential.ts index 427064f..3e5cccc 100644 --- a/src/credential.ts +++ b/src/credential.ts @@ -28,7 +28,7 @@ const getUpdatedSettings = async (channelName, pbCookie) => { { tchannelData: { minSeq: minSeq } } = appSettings; const credentials = JSON.parse(readFileSync("config.json", "utf8")); credentials.app_settings.tchannelData.minSeq = minSeq - writeFile("config.json", JSON.stringify(credentials), function (err) { + writeFile("config.json", JSON.stringify(credentials, null, 4), function (err) { if (err) { console.log(err); } diff --git a/src/index.ts b/src/index.ts index cdec9ad..e6e0137 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,9 +3,10 @@ import prompts from "prompts"; import ora from "ora"; import * as dotenv from "dotenv"; import {readFileSync, writeFile} from "fs"; -import {scrape, getUpdatedSettings} from "./credential.js"; -import {listenWs, connectWs, disconnectWs} from "./websocket.js"; +import {getUpdatedSettings, scrape} from "./credential.js"; +import {connectWs, disconnectWs, listenWs} from "./websocket.js"; import * as mail from "./mail.js"; +import randomUseragent from 'random-useragent' dotenv.config(); @@ -25,63 +26,83 @@ const queries = { sendVerificationCodeMutation: readFileSync(gqlDir + "/SendVerificationCodeForLoginMutation.graphql", "utf8"), }; -let [pbCookie, channelName, appSettings, formkey] = ["", "", "", ""]; - class ChatBot { + public config = JSON.parse(readFileSync("config.json", "utf8")); + private headers = { 'Content-Type': 'application/json', - 'Accept': '*/*', 'Host': 'poe.com', - 'Accept-Encoding': 'gzip, deflate, br', 'Connection': 'keep-alive', 'Origin': 'https://poe.com', + 'User-Agent': randomUseragent.getRandom(), } - private chatId: number = 0; - private bot: string = ""; + public chatId: number = 0; + public bot: string = ""; - private async getCredentials() { - const credentials = JSON.parse(readFileSync("config.json", "utf8")); - const {quora_formkey, quora_cookie} = credentials; + public reConnectWs = false; + + public async getCredentials() { + const {quora_formkey, channel_name, quora_cookie} = this.config; if (quora_formkey.length > 0 && quora_cookie.length > 0) { - formkey = quora_formkey; - pbCookie = quora_cookie; - // For websocket later feature - channelName = credentials.channel_name; - appSettings = credentials.app_settings; - this.headers["poe-formkey"] = formkey; - this.headers["poe-tchannel"] = channelName; - this.headers["Cookie"] = pbCookie; + this.headers["poe-formkey"] = quora_formkey; + this.headers["poe-tchannel"] = channel_name; + this.headers["Cookie"] = quora_cookie; } return quora_formkey.length > 0 && quora_cookie.length > 0; } - private async setCredentials() { - let result = await scrape(); + public async setChatIds() { + const [a2, capybara, nutria, chinchilla] = await Promise.all([ + this.getChatId("a2"), + this.getChatId("capybara"), + this.getChatId("nutria"), + this.getChatId("chinchilla"), + ]); + const credentials = JSON.parse(readFileSync("config.json", "utf8")); - credentials.quora_formkey = result.appSettings.formkey; - credentials.quora_cookie = result.pbCookie; - // For websocket later feature - credentials.channel_name = result.channelName; - credentials.app_settings = result.appSettings; + + credentials.chat_ids = { + a2, + capybara, + nutria, + chinchilla, + }; + + this.config.chat_ids = { + a2, + capybara, + nutria, + chinchilla, + } + + writeFile("config.json", JSON.stringify(credentials, null, 4), function (err) { + if (err) { + console.log(err); + } + }); + } + + public async setCredentials() { + let result = await scrape(); + this.config.quora_formkey = result.appSettings.formkey; + this.config.quora_cookie = result.pbCookie; + this.config.channel_name = result.channelName; + this.config.app_settings = result.appSettings; // set value - formkey = result.appSettings.formkey; - pbCookie = result.pbCookie; - // For websocket later feature - channelName = result.channelName; - appSettings = result.appSettings; - this.headers["poe-formkey"] = formkey; - this.headers["poe-tchannel"] = channelName; - this.headers["Cookie"] = pbCookie; - writeFile("config.json", JSON.stringify(credentials), function (err) { + this.headers["poe-formkey"] = this.config.quora_formkey; + this.headers["poe-tchannel"] = this.config.channel_name; + this.headers["Cookie"] = this.config.quora_cookie; + + writeFile("config.json", JSON.stringify(this.config, null, 4), function (err) { if (err) { console.log(err); } }); } - private async subscribe() { + public async subscribe() { const query = { queryName: 'subscriptionsMutation', variables: { @@ -102,94 +123,7 @@ class ChatBot { await this.makeRequest(query); } - public async start() { - const isFormkeyAvailable = await this.getCredentials(); - if (!isFormkeyAvailable) { - const {mode} = await prompts({ - type: "select", - name: "mode", - message: "Select", - choices: [ - {title: "Auto [This will use temp email to get Verification Code]", value: "auto"}, - {title: "Semi-Auto [Use you own email/phone number]", value: "semi"}, - {title: "exit", value: "exit"} - ], - }); - - if (mode === "exit") { - process.exit(0); - } - - await this.setCredentials(); - await this.subscribe(); - await this.login(mode); - } - - await getUpdatedSettings(channelName, pbCookie); - await this.subscribe(); - const ws = await connectWs(); - const {bot} = await prompts({ - type: "select", - name: "bot", - message: "Select", - choices: [ - {title: "Claude (Powered by Anthropic)", value: "a2"}, - {title: "Sage (Powered by OpenAI - logical)", value: "capybara"}, - {title: "Dragonfly (Powered by OpenAI - simpler)", value: "nutria"}, - {title: "ChatGPT (Powered by OpenAI - current)", value: "chinchilla"}, - ], - }); - - await this.getChatId(bot); - - let helpMsg = "Available commands: !help !exit, !clear, !submit" + - "\n!help - show this message" + - "\n!exit - exit the chat" + - "\n!clear - clear chat history" + - "\n!submit - submit prompt"; - - await this.clearContext(); - console.log(helpMsg) - let submitedPrompt = ""; - while (true) { - const {prompt} = await prompts({ - type: "text", - name: "prompt", - message: "Ask:", - }); - - if (prompt.length > 0) { - if (prompt === "!help") { - console.log(helpMsg); - } else if (prompt === "!exit") { - await disconnectWs(ws); - process.exit(0); - } else if (prompt === "!clear") { - spinner.start("Clearing chat history..."); - await this.clearContext(); - submitedPrompt = ""; - spinner.stop(); - console.log("Chat history cleared"); - } else if (prompt === "!submit") { - if (submitedPrompt.length === 0) { - console.log("No prompt to submit"); - continue; - } - await this.sendMsg(submitedPrompt); - process.stdout.write("Response: "); - await listenWs(ws); - console.log('\n'); - submitedPrompt = ""; - } else { - submitedPrompt += prompt + "\n"; - } - } - } - } - - private async makeRequest(request) { - this.headers["Content-Length"] = Buffer.byteLength(JSON.stringify(request), 'utf8'); - + public async makeRequest(request) { const response = await fetch('https://poe.com/api/gql_POST', { method: 'POST', headers: this.headers, @@ -264,9 +198,11 @@ class ChatBot { } spinner.stop(); } + + await this.setChatIds(); } - private async signInOrUp(phoneNumber, email, verifyCode) { + public async signInOrUp(phoneNumber, email, verifyCode) { console.log("Signing in/up...") console.log("Phone number: " + phoneNumber) console.log("Email: " + email) @@ -291,7 +227,7 @@ class ChatBot { } } - private async signUpWithVerificationCode(phoneNumber, email, verifyCode) { + public async signUpWithVerificationCode(phoneNumber, email, verifyCode) { console.log("Signing in/up...") console.log("Phone number: " + phoneNumber) console.log("Email: " + email) @@ -316,7 +252,7 @@ class ChatBot { } } - private async sendVerifCode(phoneNumber, email) { + public async sendVerifCode(phoneNumber, email) { try { // status error case: success, user_with_confirmed_phone_number_not_found, user_with_confirmed_email_not_found const {data: {sendVerificationCode: {status}}} = await this.makeRequest({ @@ -329,11 +265,22 @@ class ChatBot { console.log("Verification code sent. Status: " + status) return status; } catch (e) { - throw e; + console.log("Error sending verification code, please try again " + e) + await this.resetConfig(); } } - private async getChatId(bot: string) { + public async resetConfig() { + const defaultConfig = JSON.parse(readFileSync("config.example.json", "utf8")); + console.log("Resetting config...") + writeFile("config.json", JSON.stringify(defaultConfig, null, 4), function (err) { + if (err) { + console.log(err); + } + }); + } + + public async getChatId(bot: string) { try { const {data: {chatOfBot: {chatId}}} = await this.makeRequest({ query: `${queries.chatViewQuery}`, @@ -343,67 +290,241 @@ class ChatBot { }); this.chatId = chatId; this.bot = bot; + return chatId; } catch (e) { + console.log(e) + await this.resetConfig(); throw new Error("Could not get chat id, invalid formkey or cookie! Please remove the quora_formkey value from the config.json file and try again."); } } - private async clearContext() { + public async clearContext(bot: string) { try { - await this.makeRequest({ + const data = await this.makeRequest({ query: `${queries.addMessageBreakMutation}`, - variables: {chatId: this.chatId}, + variables: {chatId: this.config.chat_ids[bot]}, }); + + if (!data.data) { + this.reConnectWs = true; // for websocket purpose + console.log("ON TRY! Could not clear context! Trying to reLogin.."); + await this.reLogin(); + await this.clearContext(bot); + } + return data } catch (e) { - throw new Error("Could not clear context"); + this.reConnectWs = true; // for websocket purpose + console.log("ON CATCH! Could not clear context! Trying to reLogin.."); + await this.reLogin(); + await this.clearContext(bot); + return e } } - private async sendMsg(query: string) { + public async sendMsg(bot: string, query: string) { try { - await this.makeRequest({ + const data = await this.makeRequest({ query: `${queries.addHumanMessageMutation}`, variables: { - bot: this.bot, - chatId: this.chatId, + bot: bot, + chatId: this.config.chat_ids[bot], query: query, source: null, withChatBreak: false }, }); + + if (!data.data) { + this.reConnectWs = true; // for cli websocket purpose + console.log("Could not send message! Trying to reLogin.."); + await this.reLogin(); + await this.sendMsg(bot, query); + } + return data } catch (e) { - throw new Error("Could not send message"); + this.reConnectWs = true; // for cli websocket purpose + console.log("ON CATCH! Could not send message! Trying to reLogin.."); + await this.reLogin(); + await this.sendMsg(bot, query); + return e } } - // Responce without stream - private async getResponse(): Promise