|
|
|
@ -1,19 +1,3 @@
|
|
|
|
|
# ading2210/poe-api: a reverse engineered Python API wrapepr for Quora's Poe
|
|
|
|
|
# Copyright (C) 2023 ading2210
|
|
|
|
|
|
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
# (at your option) any later version.
|
|
|
|
|
|
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
|
|
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
|
|
import requests
|
|
|
|
|
import re
|
|
|
|
|
import json
|
|
|
|
@ -71,14 +55,6 @@ class Client:
|
|
|
|
|
home_url = "https://poe.com"
|
|
|
|
|
settings_url = "https://poe.com/api/settings"
|
|
|
|
|
|
|
|
|
|
formkey = ""
|
|
|
|
|
next_data = {}
|
|
|
|
|
bots = {}
|
|
|
|
|
active_messages = {}
|
|
|
|
|
message_queues = {}
|
|
|
|
|
ws = None
|
|
|
|
|
ws_connected = False
|
|
|
|
|
|
|
|
|
|
def __init__(self, token, proxy=None):
|
|
|
|
|
self.proxy = proxy
|
|
|
|
|
self.session = requests.Session()
|
|
|
|
@ -90,6 +66,9 @@ class Client:
|
|
|
|
|
}
|
|
|
|
|
logger.info(f"Proxy enabled: {self.proxy}")
|
|
|
|
|
|
|
|
|
|
self.active_messages = {}
|
|
|
|
|
self.message_queues = {}
|
|
|
|
|
|
|
|
|
|
self.session.cookies.set("p-b", token, domain="poe.com")
|
|
|
|
|
self.headers = {
|
|
|
|
|
"User-Agent": user_agent,
|
|
|
|
@ -99,10 +78,10 @@ class Client:
|
|
|
|
|
self.ws_domain = f"tch{random.randint(1, 1e6)}"
|
|
|
|
|
|
|
|
|
|
self.session.headers.update(self.headers)
|
|
|
|
|
self.next_data = self.get_next_data()
|
|
|
|
|
self.next_data = self.get_next_data(overwrite_vars=True)
|
|
|
|
|
self.channel = self.get_channel_data()
|
|
|
|
|
self.connect_ws()
|
|
|
|
|
self.bots = self.get_bots()
|
|
|
|
|
self.bots = self.get_bots(download_next_data=False)
|
|
|
|
|
self.bot_names = self.get_bot_names()
|
|
|
|
|
|
|
|
|
|
self.gql_headers = {
|
|
|
|
@ -112,7 +91,7 @@ class Client:
|
|
|
|
|
self.gql_headers = {**self.gql_headers, **self.headers}
|
|
|
|
|
self.subscribe()
|
|
|
|
|
|
|
|
|
|
def get_next_data(self):
|
|
|
|
|
def get_next_data(self, overwrite_vars=False):
|
|
|
|
|
logger.info("Downloading next_data...")
|
|
|
|
|
|
|
|
|
|
r = request_with_retries(self.session.get, self.home_url)
|
|
|
|
@ -120,8 +99,9 @@ class Client:
|
|
|
|
|
json_text = re.search(json_regex, r.text).group(1)
|
|
|
|
|
next_data = json.loads(json_text)
|
|
|
|
|
|
|
|
|
|
self.formkey = next_data["props"]["formkey"]
|
|
|
|
|
self.viewer = next_data["props"]["pageProps"]["payload"]["viewer"]
|
|
|
|
|
if overwrite_vars:
|
|
|
|
|
self.formkey = next_data["props"]["formkey"]
|
|
|
|
|
self.viewer = next_data["props"]["pageProps"]["payload"]["viewer"]
|
|
|
|
|
|
|
|
|
|
return next_data
|
|
|
|
|
|
|
|
|
@ -134,17 +114,23 @@ class Client:
|
|
|
|
|
chat_data = r.json()["pageProps"]["payload"]["chatOfBotDisplayName"]
|
|
|
|
|
return chat_data
|
|
|
|
|
|
|
|
|
|
def get_bots(self):
|
|
|
|
|
viewer = self.next_data["props"]["pageProps"]["payload"]["viewer"]
|
|
|
|
|
if not "availableBots" in viewer:
|
|
|
|
|
raise RuntimeError("Invalid token.")
|
|
|
|
|
bot_list = viewer["availableBots"]
|
|
|
|
|
def get_bots(self, download_next_data=True):
|
|
|
|
|
if download_next_data:
|
|
|
|
|
next_data = self.get_next_data()
|
|
|
|
|
else:
|
|
|
|
|
next_data = self.next_data
|
|
|
|
|
|
|
|
|
|
if not "availableBots" in self.viewer:
|
|
|
|
|
raise RuntimeError("Invalid token or no bots are available.")
|
|
|
|
|
bot_list = self.viewer["availableBots"]
|
|
|
|
|
|
|
|
|
|
bots = {}
|
|
|
|
|
for bot in bot_list:
|
|
|
|
|
chat_data = self.get_bot(bot["displayName"].lower())
|
|
|
|
|
chat_data = self.get_bot(bot["displayName"])
|
|
|
|
|
bots[chat_data["defaultBotObject"]["nickname"]] = chat_data
|
|
|
|
|
|
|
|
|
|
self.bots = bots
|
|
|
|
|
self.bot_names = self.get_bot_names()
|
|
|
|
|
return bots
|
|
|
|
|
|
|
|
|
|
def get_bot_names(self):
|
|
|
|
@ -170,9 +156,20 @@ class Client:
|
|
|
|
|
|
|
|
|
|
def send_query(self, query_name, variables):
|
|
|
|
|
for i in range(20):
|
|
|
|
|
payload = generate_payload(query_name, variables)
|
|
|
|
|
json_data = generate_payload(query_name, variables)
|
|
|
|
|
payload = json.dumps(json_data, separators=(',', ':'))
|
|
|
|
|
|
|
|
|
|
base_string = payload + self.gql_headers['poe-formkey'] + 'WpuLMiXEKKE98j56k'
|
|
|
|
|
|
|
|
|
|
from hashlib import md5
|
|
|
|
|
headers = self.gql_headers |{
|
|
|
|
|
"content-type": "application/json",
|
|
|
|
|
"poe-tag-id": md5(base_string.encode()).hexdigest()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r = request_with_retries(
|
|
|
|
|
self.session.post, self.gql_url, json=payload, headers=self.gql_headers)
|
|
|
|
|
self.session.post, self.gql_url, data=payload, headers=headers)
|
|
|
|
|
|
|
|
|
|
data = r.json()
|
|
|
|
|
if data["data"] == None:
|
|
|
|
|
logger.warn(
|
|
|
|
@ -212,6 +209,7 @@ class Client:
|
|
|
|
|
self.ws.run_forever(**kwargs)
|
|
|
|
|
|
|
|
|
|
def connect_ws(self):
|
|
|
|
|
self.ws_connected = False
|
|
|
|
|
self.ws = websocket.WebSocketApp(
|
|
|
|
|
self.get_websocket_url(),
|
|
|
|
|
header={"User-Agent": user_agent},
|
|
|
|
@ -233,9 +231,10 @@ class Client:
|
|
|
|
|
def on_ws_connect(self, ws):
|
|
|
|
|
self.ws_connected = True
|
|
|
|
|
|
|
|
|
|
def on_ws_close(self, ws, close_status_code):
|
|
|
|
|
def on_ws_close(self, ws, close_status_code, close_message):
|
|
|
|
|
self.ws_connected = False
|
|
|
|
|
logger.warn(f"Websocket closed with status {close_status_code}")
|
|
|
|
|
logger.warn(
|
|
|
|
|
f"Websocket closed with status {close_status_code}: {close_message}")
|
|
|
|
|
|
|
|
|
|
def on_ws_error(self, ws, error):
|
|
|
|
|
self.disconnect_ws()
|
|
|
|
@ -345,22 +344,27 @@ class Client:
|
|
|
|
|
def get_message_history(self, chatbot, count=25, cursor=None):
|
|
|
|
|
logger.info(f"Downloading {count} messages from {chatbot}")
|
|
|
|
|
|
|
|
|
|
messages = []
|
|
|
|
|
if cursor == None:
|
|
|
|
|
chat_data = self.get_bot(self.bot_names[chatbot])
|
|
|
|
|
if not chat_data["messagesConnection"]["edges"]:
|
|
|
|
|
return []
|
|
|
|
|
cursor = chat_data["messagesConnection"]["edges"][-1]["cursor"]
|
|
|
|
|
messages = chat_data["messagesConnection"]["edges"][:count]
|
|
|
|
|
cursor = chat_data["messagesConnection"]["pageInfo"]["startCursor"]
|
|
|
|
|
count -= len(messages)
|
|
|
|
|
|
|
|
|
|
cursor = str(cursor)
|
|
|
|
|
if count > 50:
|
|
|
|
|
messages = self.get_message_history(
|
|
|
|
|
chatbot, count=50, cursor=cursor)
|
|
|
|
|
chatbot, count=50, cursor=cursor) + messages
|
|
|
|
|
while count > 0:
|
|
|
|
|
count -= 50
|
|
|
|
|
new_cursor = messages[0]["cursor"]
|
|
|
|
|
new_messages = self.get_message_history(
|
|
|
|
|
chatbot, min(50, count), cursor=new_cursor)
|
|
|
|
|
messages = new_messages + messages
|
|
|
|
|
count -= 50
|
|
|
|
|
return messages
|
|
|
|
|
elif count <= 0:
|
|
|
|
|
return messages
|
|
|
|
|
|
|
|
|
|
result = self.send_query("ChatListPaginationQuery", {
|
|
|
|
@ -368,7 +372,9 @@ class Client:
|
|
|
|
|
"cursor": cursor,
|
|
|
|
|
"id": self.bots[chatbot]["id"]
|
|
|
|
|
})
|
|
|
|
|
return result["data"]["node"]["messagesConnection"]["edges"]
|
|
|
|
|
query_messages = result["data"]["node"]["messagesConnection"]["edges"]
|
|
|
|
|
messages = query_messages + messages
|
|
|
|
|
return messages
|
|
|
|
|
|
|
|
|
|
def delete_message(self, message_ids):
|
|
|
|
|
logger.info(f"Deleting messages: {message_ids}")
|
|
|
|
@ -398,4 +404,4 @@ class Client:
|
|
|
|
|
logger.info(f"No more messages left to delete.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
load_queries()
|
|
|
|
|
load_queries()
|
|
|
|
|