Skip to content
This repository was archived by the owner on Aug 18, 2023. It is now read-only.

Commit 8ef5946

Browse files
authored
Merge pull request #6 from muharamdani/feature/stream-support
Feature | Stream support
2 parents 3499f6b + 2d9fca4 commit 8ef5946

File tree

9 files changed

+263
-1275
lines changed

9 files changed

+263
-1275
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ This is a CLI tool to call the Quora Poe API through GraphQL. It is a work in pr
33
- Auto login using temporary email, so you don't need to use your own email/phone number.
44
- Semi auto login using your own email/phone number, you need to enter the OTP manually.
55
- Chat with 4 types of bots (Sage, Claude, ChatGPT, and Dragonfly).
6+
- Stream responses support from the bot.
67
- Clear the chat history.
78

89
## Requirements
@@ -26,8 +27,10 @@ npm start
2627
```
2728

2829
## TODO List
29-
- [ ] Add support for relogin after session expires
30-
- [ ] Add stream support
30+
- [ ] Make it modular, so it can be used as a library
31+
- [ ] Add support for re-login after session expires
32+
- [ ] Add support for get chat history
33+
- [ ] Add support for delete message
3134

3235
## Contributing
3336

dist/credential.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fetch from 'cross-fetch';
2-
export const scrape = async () => {
2+
import { readFileSync, writeFile } from "fs";
3+
const scrape = async () => {
34
const _pb = await fetch("https://poe.com/login"), pbCookie = _pb.headers.get("set-cookie")?.split(";")[0];
45
const _setting = await fetch('https://poe.com/api/settings', { headers: { cookie: `${pbCookie}` } });
56
if (_setting.status !== 200)
@@ -11,4 +12,20 @@ export const scrape = async () => {
1112
appSettings,
1213
};
1314
};
14-
export default scrape;
15+
const getUpdatedSettings = async (channelName, pbCookie) => {
16+
const _setting = await fetch(`https://poe.com/api/settings?channel=${channelName}`, { headers: { cookie: `${pbCookie}` } });
17+
if (_setting.status !== 200)
18+
throw new Error("Failed to fetch token");
19+
const appSettings = await _setting.json(), { tchannelData: { minSeq: minSeq } } = appSettings;
20+
const credentials = JSON.parse(readFileSync("config.json", "utf8"));
21+
credentials.app_settings.tchannelData.minSeq = minSeq;
22+
writeFile("config.json", JSON.stringify(credentials), function (err) {
23+
if (err) {
24+
console.log(err);
25+
}
26+
});
27+
return {
28+
minSeq,
29+
};
30+
};
31+
export { scrape, getUpdatedSettings };

dist/index.js

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import prompts from "prompts";
33
import ora from "ora";
44
import * as dotenv from "dotenv";
55
import { readFileSync, writeFile } from "fs";
6-
import scrape from "./credential.js";
6+
import { scrape, getUpdatedSettings } from "./credential.js";
7+
import { listenWs, connectWs, disconnectWs } from "./websocket.js";
78
import * as mail from "./mail.js";
89
dotenv.config();
910
const spinner = ora({
@@ -24,28 +25,26 @@ class ChatBot {
2425
constructor() {
2526
this.headers = {
2627
'Content-Type': 'application/json',
27-
'User-Agent': 'PostmanRuntime/7.31.1',
2828
'Accept': '*/*',
2929
'Host': 'poe.com',
3030
'Accept-Encoding': 'gzip, deflate, br',
3131
'Connection': 'keep-alive',
32+
'Origin': 'https://poe.com',
3233
};
3334
this.chatId = 0;
3435
this.bot = "";
3536
}
3637
async getCredentials() {
3738
const credentials = JSON.parse(readFileSync("config.json", "utf8"));
3839
const { quora_formkey, quora_cookie } = credentials;
39-
console.log("FROM GET CREDENTIALS");
40-
console.log(quora_formkey);
41-
console.log(quora_cookie);
4240
if (quora_formkey.length > 0 && quora_cookie.length > 0) {
4341
formkey = quora_formkey;
4442
pbCookie = quora_cookie;
4543
// For websocket later feature
4644
channelName = credentials.channel_name;
4745
appSettings = credentials.app_settings;
4846
this.headers["poe-formkey"] = formkey;
47+
this.headers["poe-tchannel"] = channelName;
4948
this.headers["Cookie"] = pbCookie;
5049
}
5150
return quora_formkey.length > 0 && quora_cookie.length > 0;
@@ -61,20 +60,37 @@ class ChatBot {
6160
// set value
6261
formkey = result.appSettings.formkey;
6362
pbCookie = result.pbCookie;
64-
console.log("FROM SET CREDENTIALS");
65-
console.log(formkey);
66-
console.log(pbCookie);
6763
// For websocket later feature
6864
channelName = result.channelName;
6965
appSettings = result.appSettings;
7066
this.headers["poe-formkey"] = formkey;
67+
this.headers["poe-tchannel"] = channelName;
7168
this.headers["Cookie"] = pbCookie;
7269
writeFile("config.json", JSON.stringify(credentials), function (err) {
7370
if (err) {
7471
console.log(err);
7572
}
7673
});
7774
}
75+
async subscribe() {
76+
const query = {
77+
queryName: 'subscriptionsMutation',
78+
variables: {
79+
subscriptions: [
80+
{
81+
subscriptionName: 'messageAdded',
82+
query: 'subscription subscriptions_messageAdded_Subscription(\n $chatId: BigInt!\n) {\n messageAdded(chatId: $chatId) {\n id\n messageId\n creationTime\n state\n ...ChatMessage_message\n ...chatHelpers_isBotMessage\n }\n}\n\nfragment ChatMessageDownvotedButton_message on Message {\n ...MessageFeedbackReasonModal_message\n ...MessageFeedbackOtherModal_message\n}\n\nfragment ChatMessageDropdownMenu_message on Message {\n id\n messageId\n vote\n text\n ...chatHelpers_isBotMessage\n}\n\nfragment ChatMessageFeedbackButtons_message on Message {\n id\n messageId\n vote\n voteReason\n ...ChatMessageDownvotedButton_message\n}\n\nfragment ChatMessageOverflowButton_message on Message {\n text\n ...ChatMessageDropdownMenu_message\n ...chatHelpers_isBotMessage\n}\n\nfragment ChatMessageSuggestedReplies_SuggestedReplyButton_message on Message {\n messageId\n}\n\nfragment ChatMessageSuggestedReplies_message on Message {\n suggestedReplies\n ...ChatMessageSuggestedReplies_SuggestedReplyButton_message\n}\n\nfragment ChatMessage_message on Message {\n id\n messageId\n text\n author\n linkifiedText\n state\n ...ChatMessageSuggestedReplies_message\n ...ChatMessageFeedbackButtons_message\n ...ChatMessageOverflowButton_message\n ...chatHelpers_isHumanMessage\n ...chatHelpers_isBotMessage\n ...chatHelpers_isChatBreak\n ...chatHelpers_useTimeoutLevel\n ...MarkdownLinkInner_message\n}\n\nfragment MarkdownLinkInner_message on Message {\n messageId\n}\n\nfragment MessageFeedbackOtherModal_message on Message {\n id\n messageId\n}\n\nfragment MessageFeedbackReasonModal_message on Message {\n id\n messageId\n}\n\nfragment chatHelpers_isBotMessage on Message {\n ...chatHelpers_isHumanMessage\n ...chatHelpers_isChatBreak\n}\n\nfragment chatHelpers_isChatBreak on Message {\n author\n}\n\nfragment chatHelpers_isHumanMessage on Message {\n author\n}\n\nfragment chatHelpers_useTimeoutLevel on Message {\n id\n state\n text\n messageId\n}\n'
83+
},
84+
{
85+
subscriptionName: 'viewerStateUpdated',
86+
query: 'subscription subscriptions_viewerStateUpdated_Subscription {\n viewerStateUpdated {\n id\n ...ChatPageBotSwitcher_viewer\n }\n}\n\nfragment BotHeader_bot on Bot {\n displayName\n ...BotImage_bot\n}\n\nfragment BotImage_bot on Bot {\n profilePicture\n displayName\n}\n\nfragment BotLink_bot on Bot {\n displayName\n}\n\nfragment ChatPageBotSwitcher_viewer on Viewer {\n availableBots {\n id\n ...BotLink_bot\n ...BotHeader_bot\n }\n}\n'
87+
}
88+
]
89+
},
90+
query: 'mutation subscriptionsMutation(\n $subscriptions: [AutoSubscriptionQuery!]!\n) {\n autoSubscribe(subscriptions: $subscriptions) {\n viewer {\n id\n }\n }\n}\n'
91+
};
92+
await this.makeRequest(query);
93+
}
7894
async start() {
7995
const isFormkeyAvailable = await this.getCredentials();
8096
if (!isFormkeyAvailable) {
@@ -92,8 +108,12 @@ class ChatBot {
92108
process.exit(0);
93109
}
94110
await this.setCredentials();
111+
await this.subscribe();
95112
await this.login(mode);
96113
}
114+
await getUpdatedSettings(channelName, pbCookie);
115+
await this.subscribe();
116+
const ws = await connectWs();
97117
const { bot } = await prompts({
98118
type: "select",
99119
name: "bot",
@@ -111,6 +131,7 @@ class ChatBot {
111131
"\n!exit - exit the chat" +
112132
"\n!clear - clear chat history" +
113133
"\n!submit - submit prompt";
134+
await this.clearContext();
114135
console.log(helpMsg);
115136
let submitedPrompt = "";
116137
while (true) {
@@ -124,7 +145,8 @@ class ChatBot {
124145
console.log(helpMsg);
125146
}
126147
else if (prompt === "!exit") {
127-
break;
148+
await disconnectWs(ws);
149+
process.exit(0);
128150
}
129151
else if (prompt === "!clear") {
130152
spinner.start("Clearing chat history...");
@@ -138,12 +160,11 @@ class ChatBot {
138160
console.log("No prompt to submit");
139161
continue;
140162
}
141-
spinner.start("Waiting for response...");
142163
await this.sendMsg(submitedPrompt);
143-
let response = await this.getResponse();
144-
spinner.stop();
164+
process.stdout.write("Response: ");
165+
await listenWs(ws);
166+
console.log('\n');
145167
submitedPrompt = "";
146-
console.log(response);
147168
}
148169
else {
149170
submitedPrompt += prompt + "\n";
@@ -163,8 +184,6 @@ class ChatBot {
163184
async login(mode) {
164185
if (mode === "auto") {
165186
const { email, sid_token } = await mail.createNewEmail();
166-
console.log("EMAIL: " + email);
167-
console.log("SID_TOKEN: " + sid_token);
168187
const status = await this.sendVerifCode(null, email);
169188
spinner.start("Waiting for OTP code...");
170189
const otp_code = await mail.getPoeOTPCode(sid_token);
@@ -332,6 +351,7 @@ class ChatBot {
332351
throw new Error("Could not send message");
333352
}
334353
}
354+
// Responce without stream
335355
async getResponse() {
336356
let text;
337357
let state;

dist/websocket.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import WebSocket from 'ws';
2+
import * as diff from 'diff';
3+
import { readFileSync } from "fs";
4+
const getSocketUrl = async () => {
5+
const socketUrl = 'wss://tch252531.tch.quora.com';
6+
const credentials = JSON.parse(readFileSync("config.json", "utf8"));
7+
const appSettings = credentials.app_settings.tchannelData;
8+
const boxName = appSettings.boxName;
9+
const minSeq = appSettings.minSeq;
10+
const channel = appSettings.channel;
11+
const hash = appSettings.channelHash;
12+
return `${socketUrl}/up/${boxName}/updates?min_seq=${minSeq}&channel=${channel}&hash=${hash}`;
13+
};
14+
export const connectWs = async () => {
15+
const url = await getSocketUrl();
16+
const ws = new WebSocket(url);
17+
return new Promise((resolve, reject) => {
18+
ws.on('open', function open() {
19+
console.log("Connected to websocket");
20+
return resolve(ws);
21+
});
22+
});
23+
};
24+
export const disconnectWs = async (ws) => {
25+
return new Promise((resolve, reject) => {
26+
ws.on('close', function close() {
27+
return resolve(true);
28+
});
29+
ws.close();
30+
});
31+
};
32+
export const listenWs = async (ws) => {
33+
let previousText = '';
34+
return new Promise((resolve, reject) => {
35+
const onMessage = function incoming(data) {
36+
let jsonData = JSON.parse(data);
37+
if (jsonData.messages && jsonData.messages.length > 0) {
38+
const messages = JSON.parse(jsonData.messages[0]);
39+
const dataPayload = messages.payload.data;
40+
const text = dataPayload.messageAdded.text;
41+
const state = dataPayload.messageAdded.state;
42+
if (state !== 'complete') {
43+
const differences = diff.diffChars(previousText, text);
44+
let result = '';
45+
differences.forEach((part) => {
46+
if (part.added) {
47+
result += part.value;
48+
}
49+
});
50+
previousText = text;
51+
process.stdout.write(result);
52+
}
53+
else {
54+
ws.removeListener('message', onMessage);
55+
return resolve(true);
56+
}
57+
}
58+
};
59+
ws.on('message', onMessage);
60+
});
61+
};

0 commit comments

Comments
 (0)