Chore: fix interactive verification not work in container environment

This commit is contained in:
hibobmaster 2023-04-11 01:04:06 +08:00
parent 1482e201a2
commit 83745f1174
Signed by: bobmaster
GPG key ID: 316B77D7914D713C
2 changed files with 325 additions and 84 deletions

View file

@ -3,36 +3,77 @@ Code derived from:
https://github.com/acheong08/EdgeGPT/blob/f940cecd24a4818015a8b42a2443dd97c3c2a8f4/src/ImageGen.py https://github.com/acheong08/EdgeGPT/blob/f940cecd24a4818015a8b42a2443dd97c3c2a8f4/src/ImageGen.py
""" """
from log import getlogger from log import getlogger
from typing import Union
from uuid import uuid4 from uuid import uuid4
import os import os
import urllib import contextlib
import aiohttp
import asyncio
import random
import time import time
import requests import requests
import regex import regex
BING_URL = "https://www.bing.com"
logger = getlogger() logger = getlogger()
BING_URL = "https://www.bing.com"
# Generate random IP between range 13.104.0.0/14
FORWARDED_IP = (
f"13.{random.randint(104, 107)}.{random.randint(0, 255)}.{random.randint(0, 255)}"
)
HEADERS = {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"accept-language": "en-US,en;q=0.9",
"cache-control": "max-age=0",
"content-type": "application/x-www-form-urlencoded",
"referrer": "https://www.bing.com/images/create/",
"origin": "https://www.bing.com",
"user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63",
"x-forwarded-for": FORWARDED_IP,
}
# Error messages
error_timeout = "Your request has timed out."
error_redirect = "Redirect failed"
error_blocked_prompt = (
"Your prompt has been blocked by Bing. Try to change any bad words and try again."
)
error_noresults = "Could not get results"
error_unsupported_lang = "\nthis language is currently not supported by bing"
error_bad_images = "Bad images"
error_no_images = "No images"
#
sending_message = "Sending request..."
wait_message = "Waiting for results..."
download_message = "\nDownloading images..."
def debug(debug_file, text_var):
"""helper function for debug"""
with open(f"{debug_file}", "a") as f:
f.write(str(text_var))
class ImageGen: class ImageGen:
""" """
Image generation by Microsoft Bing Image generation by Microsoft Bing
Parameters: Parameters:3
auth_cookie: str auth_cookie: str
""" """
def __init__(self, auth_cookie: str) -> None: def __init__(
self, auth_cookie: str, debug_file: Union[str, None] = None, quiet: bool = False
) -> None:
self.session: requests.Session = requests.Session() self.session: requests.Session = requests.Session()
self.session.headers = { self.session.headers = HEADERS
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"accept-language": "en-US,en;q=0.9",
"cache-control": "max-age=0",
"content-type": "application/x-www-form-urlencoded",
"referrer": "https://www.bing.com/images/create/",
"origin": "https://www.bing.com",
"user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63",
}
self.session.cookies.set("_U", auth_cookie) self.session.cookies.set("_U", auth_cookie)
self.quiet = quiet
self.debug_file = debug_file
if self.debug_file:
self.debug = partial(debug, self.debug_file)
def get_images(self, prompt: str) -> list: def get_images(self, prompt: str) -> list:
""" """
@ -40,19 +81,38 @@ class ImageGen:
Parameters: Parameters:
prompt: str prompt: str
""" """
print("Sending request...") if not self.quiet:
url_encoded_prompt = urllib.parse.quote(prompt) print(sending_message)
# https://www.bing.com/images/create?q=<PROMPT>&rt=4&FORM=GENCRE if self.debug_file:
self.debug(sending_message)
url_encoded_prompt = requests.utils.quote(prompt)
# https://www.bing.com/images/create?q=<PROMPT>&rt=3&FORM=GENCRE
url = f"{BING_URL}/images/create?q={url_encoded_prompt}&rt=4&FORM=GENCRE" url = f"{BING_URL}/images/create?q={url_encoded_prompt}&rt=4&FORM=GENCRE"
response = self.session.post(url, allow_redirects=False) response = self.session.post(url, allow_redirects=False)
# check for content waring message
if "this prompt has been blocked" in response.text.lower():
if self.debug_file:
self.debug(f"ERROR: {error_blocked_prompt}")
raise Exception(
error_blocked_prompt,
)
if (
"we're working hard to offer image creator in more languages"
in response.text.lower()
):
if self.debug_file:
self.debug(f"ERROR: {error_unsupported_lang}")
raise Exception(error_unsupported_lang)
if response.status_code != 302: if response.status_code != 302:
#if rt4 fails, try rt3 # if rt4 fails, try rt3
url= f"{BING_URL}/images/create?q={url_encoded_prompt}&rt=3&FORM=GENCRE" url = f"{BING_URL}/images/create?q={url_encoded_prompt}&rt=3&FORM=GENCRE"
response3 = self.session.post(url, allow_redirects=False) response3 = self.session.post(url, allow_redirects=False, timeout=200)
if response3.status_code != 302: if response3.status_code != 302:
logger.error(f"ERROR: {response.text}") if self.debug_file:
return [] self.debug(f"ERROR: {error_redirect}")
response=response3 print(f"ERROR: {response3.text}")
raise Exception(error_redirect)
response = response3
# Get redirect URL # Get redirect URL
redirect_url = response.headers["Location"].replace("&nfy=1", "") redirect_url = response.headers["Location"].replace("&nfy=1", "")
request_id = redirect_url.split("id=")[-1] request_id = redirect_url.split("id=")[-1]
@ -60,50 +120,196 @@ class ImageGen:
# https://www.bing.com/images/create/async/results/{ID}?q={PROMPT} # https://www.bing.com/images/create/async/results/{ID}?q={PROMPT}
polling_url = f"{BING_URL}/images/create/async/results/{request_id}?q={url_encoded_prompt}" polling_url = f"{BING_URL}/images/create/async/results/{request_id}?q={url_encoded_prompt}"
# Poll for results # Poll for results
print("Waiting for results...") if self.debug_file:
self.debug("Polling and waiting for result")
if not self.quiet:
print("Waiting for results...")
start_wait = time.time()
while True: while True:
print(".", end="", flush=True) if int(time.time() - start_wait) > 200:
if self.debug_file:
self.debug(f"ERROR: {error_timeout}")
raise Exception(error_timeout)
if not self.quiet:
print(".", end="", flush=True)
response = self.session.get(polling_url) response = self.session.get(polling_url)
if response.status_code != 200: if response.status_code != 200:
logger.error("Could not get results", exc_info=True) if self.debug_file:
return [] self.debug(f"ERROR: {error_noresults}")
if response.text == "": raise Exception(error_noresults)
if not response.text or response.text.find("errorMessage") != -1:
time.sleep(1) time.sleep(1)
continue continue
else: else:
break break
# Use regex to search for src="" # Use regex to search for src=""
image_links = regex.findall(r'src="([^"]+)"', response.text) image_links = regex.findall(r'src="([^"]+)"', response.text)
# Remove size limit # Remove size limit
normal_image_links = [link.split("?w=")[0] for link in image_links] normal_image_links = [link.split("?w=")[0] for link in image_links]
# Remove duplicates # Remove duplicates
return list(set(normal_image_links)) normal_image_links = list(set(normal_image_links))
# bad_images = [
# "https://r.bing.com/rp/in-2zU3AJUdkgFe7ZKv19yPBHVs.png",
# "https://r.bing.com/rp/TX9QuO3WzcCJz1uaaSwQAz39Kb0.jpg",
# ]
# for img in normal_image_links:
# if img in bad_images:
# raise Exception("Bad images")
# No images
if not normal_image_links:
raise Exception(error_no_images)
return normal_image_links
def save_images(self, links: list, output_dir: str) -> str: def save_images(self, links: list, output_dir: str) -> str:
""" """
Saves images to output directory Saves images to output directory
""" """
print("\nDownloading images...")
try:
os.mkdir(output_dir)
except FileExistsError:
pass
# image name # image name
image_name = str(uuid4()) image_name = str(uuid4())
# since matrix only support one media attachment per message, we just need one link # since matrix only support one media attachment per message, we just need one link
if links: if links:
link = links.pop() link = links.pop()
else:
logger.error("Get Image URL failed")
# return "" if there is no link
return ""
with self.session.get(link, stream=True) as response: image_path = os.path.join(output_dir, f"{image_name}.jpeg")
# save response to file
response.raise_for_status()
with open(f"{output_dir}/{image_name}.jpeg", "wb") as output_file:
for chunk in response.iter_content(chunk_size=8192):
output_file.write(chunk)
return f"{output_dir}/{image_name}.jpeg" with contextlib.suppress(FileExistsError):
os.mkdir(output_dir)
try:
with self.session.get(link, stream=True) as response:
# save response to file
response.raise_for_status()
with open(
os.path.join(output_dir, image_path), "wb"
) as output_file:
for chunk in response.iter_content(chunk_size=8192):
output_file.write(chunk)
return image_path
except requests.exceptions.MissingSchema as url_exception:
raise Exception(
"Inappropriate contents found in the generated images. Please try again or try another prompt.",
) from url_exception
class ImageGenAsync:
"""
Image generation by Microsoft Bing
Parameters:
auth_cookie: str
"""
def __init__(self, auth_cookie: str, quiet: bool = True) -> None:
self.session = aiohttp.ClientSession(
headers=HEADERS,
cookies={"_U": auth_cookie},
)
self.quiet = quiet
async def __aenter__(self):
return self
async def __aexit__(self, *excinfo) -> None:
await self.session.close()
async def get_images(self, prompt: str) -> list:
"""
Fetches image links from Bing
Parameters:
prompt: str
"""
if not self.quiet:
print("Sending request...")
url_encoded_prompt = requests.utils.quote(prompt)
# https://www.bing.com/images/create?q=<PROMPT>&rt=3&FORM=GENCRE
url = f"{BING_URL}/images/create?q={url_encoded_prompt}&rt=4&FORM=GENCRE"
async with self.session.post(url, allow_redirects=False) as response:
content = await response.text()
if "this prompt has been blocked" in content.lower():
raise Exception(
"Your prompt has been blocked by Bing. Try to change any bad words and try again.",
)
if response.status != 302:
# if rt4 fails, try rt3
url = (
f"{BING_URL}/images/create?q={url_encoded_prompt}&rt=3&FORM=GENCRE"
)
async with self.session.post(
url,
allow_redirects=False,
timeout=200,
) as response3:
if response3.status != 302:
print(f"ERROR: {response3.text}")
raise Exception("Redirect failed")
response = response3
# Get redirect URL
redirect_url = response.headers["Location"].replace("&nfy=1", "")
request_id = redirect_url.split("id=")[-1]
await self.session.get(f"{BING_URL}{redirect_url}")
# https://www.bing.com/images/create/async/results/{ID}?q={PROMPT}
polling_url = f"{BING_URL}/images/create/async/results/{request_id}?q={url_encoded_prompt}"
# Poll for results
if not self.quiet:
print("Waiting for results...")
while True:
if not self.quiet:
print(".", end="", flush=True)
# By default, timeout is 300s, change as needed
response = await self.session.get(polling_url)
if response.status != 200:
raise Exception("Could not get results")
content = await response.text()
if content and content.find("errorMessage") == -1:
break
await asyncio.sleep(1)
continue
# Use regex to search for src=""
image_links = regex.findall(r'src="([^"]+)"', content)
# Remove size limit
normal_image_links = [link.split("?w=")[0] for link in image_links]
# Remove duplicates
normal_image_links = list(set(normal_image_links))
# Bad images
bad_images = [
"https://r.bing.com/rp/in-2zU3AJUdkgFe7ZKv19yPBHVs.png",
"https://r.bing.com/rp/TX9QuO3WzcCJz1uaaSwQAz39Kb0.jpg",
]
for im in normal_image_links:
if im in bad_images:
raise Exception("Bad images")
# No images
if not normal_image_links:
raise Exception("No images")
return normal_image_links
async def save_images(self, links: list, output_dir: str) -> str:
"""
Saves images to output directory
"""
if not self.quiet:
print("\nDownloading images...")
with contextlib.suppress(FileExistsError):
os.mkdir(output_dir)
# image name
image_name = str(uuid4())
# since matrix only support one media attachment per message, we just need one link
if links:
link = links.pop()
image_path = os.path.join(output_dir, f"{image_name}.jpeg")
try:
async with self.session.get(link, raise_for_status=True) as response:
# save response to file
with open(image_path, "wb") as output_file:
async for chunk in response.content.iter_chunked(8192):
output_file.write(chunk)
return f"{output_dir}/{image_name}.jpeg"
except aiohttp.client_exceptions.InvalidURL as url_exception:
raise Exception(
"Inappropriate contents found in the generated images. Please try again or try another prompt.",
) from url_exception

103
bot.py
View file

@ -2,6 +2,7 @@ import sys
import asyncio import asyncio
import re import re
import os import os
from functools import partial
import traceback import traceback
from typing import Optional, Union from typing import Optional, Union
from nio import ( from nio import (
@ -20,14 +21,14 @@ from nio import (
KeyVerificationKey, KeyVerificationKey,
KeyVerificationMac, KeyVerificationMac,
AsyncClientConfig AsyncClientConfig
) )
from nio.store.database import SqliteStore from nio.store.database import SqliteStore
from askgpt import askGPT from askgpt import askGPT
from send_message import send_room_message from send_message import send_room_message
from v3 import Chatbot from v3 import Chatbot
from log import getlogger from log import getlogger
from bing import BingBot from bing import BingBot
from BingImageGen import ImageGen from BingImageGen import ImageGenAsync
from send_image import send_room_image from send_image import send_room_image
logger = getlogger() logger = getlogger()
@ -39,7 +40,8 @@ class Bot:
homeserver: str, homeserver: str,
user_id: str, user_id: str,
device_id: str, device_id: str,
chatgpt_api_endpoint: str = os.environ.get("CHATGPT_API_ENDPOINT") or "https://api.openai.com/v1/chat/completions", chatgpt_api_endpoint: str = os.environ.get(
"CHATGPT_API_ENDPOINT") or "https://api.openai.com/v1/chat/completions",
api_key: Optional[str] = os.environ.get("OPENAI_API_KEY") or "", api_key: Optional[str] = os.environ.get("OPENAI_API_KEY") or "",
room_id: Union[str, None] = None, room_id: Union[str, None] = None,
bing_api_endpoint: Optional[str] = '', bing_api_endpoint: Optional[str] = '',
@ -48,8 +50,8 @@ class Bot:
jailbreakEnabled: Optional[bool] = True, jailbreakEnabled: Optional[bool] = True,
bing_auth_cookie: Optional[str] = '', bing_auth_cookie: Optional[str] = '',
): ):
if (homeserver is None or user_id is None \ if (homeserver is None or user_id is None
or device_id is None): or device_id is None):
logger.warning("homeserver && user_id && device_id is required") logger.warning("homeserver && user_id && device_id is required")
sys.exit(1) sys.exit(1)
@ -82,10 +84,14 @@ class Bot:
self.client.access_token = self.access_token self.client.access_token = self.access_token
# setup event callbacks # setup event callbacks
self.client.add_event_callback(self.message_callback, (RoomMessageText, )) self.client.add_event_callback(
self.client.add_event_callback(self.decryption_failure, (MegolmEvent, )) self.message_callback, (RoomMessageText, ))
self.client.add_event_callback(self.invite_callback, (InviteMemberEvent, )) self.client.add_event_callback(
self.client.add_to_device_callback(self.to_device_callback, (KeyVerificationEvent, )) self.decryption_failure, (MegolmEvent, ))
self.client.add_event_callback(
self.invite_callback, (InviteMemberEvent, ))
self.client.add_to_device_callback(
self.to_device_callback, (KeyVerificationEvent, ))
# regular expression to match keyword [!gpt {prompt}] [!chat {prompt}] # regular expression to match keyword [!gpt {prompt}] [!chat {prompt}]
self.gpt_prog = re.compile(r"^\s*!gpt\s*(.+)$") self.gpt_prog = re.compile(r"^\s*!gpt\s*(.+)$")
@ -115,11 +121,15 @@ class Bot:
# initialize bingbot # initialize bingbot
if self.bing_api_endpoint != '': if self.bing_api_endpoint != '':
self.bingbot = BingBot(bing_api_endpoint, jailbreakEnabled=self.jailbreakEnabled) self.bingbot = BingBot(
bing_api_endpoint, jailbreakEnabled=self.jailbreakEnabled)
# initialize BingImageGen # initialize BingImageGenAsync
if self.bing_auth_cookie != '': if self.bing_auth_cookie != '':
self.imageGen = ImageGen(self.bing_auth_cookie) self.imageGen = ImageGenAsync(self.bing_auth_cookie, quiet=True)
# get current event loop
self.loop = asyncio.get_running_loop()
# message_callback RoomMessageText event # message_callback RoomMessageText event
async def message_callback(self, room: MatrixRoom, event: RoomMessageText) -> None: async def message_callback(self, room: MatrixRoom, event: RoomMessageText) -> None:
@ -157,7 +167,13 @@ class Bot:
prompt = n.group(1) prompt = n.group(1)
if self.api_key != '': if self.api_key != '':
try: try:
await self.chat(room_id, reply_to_event_id, prompt, sender_id, raw_user_message) await self.chat(room_id,
reply_to_event_id,
prompt,
sender_id,
raw_user_message
)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
await send_room_message(self.client, room_id, reply_message=str(e)) await send_room_message(self.client, room_id, reply_message=str(e))
@ -169,7 +185,12 @@ class Bot:
if m: if m:
prompt = m.group(1) prompt = m.group(1)
try: try:
await self.gpt(room_id, reply_to_event_id, prompt, sender_id, raw_user_message) await self.gpt(
room_id,
reply_to_event_id,
prompt, sender_id,
raw_user_message
)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
await send_room_message(self.client, room_id, reply_message=str(e)) await send_room_message(self.client, room_id, reply_message=str(e))
@ -181,7 +202,14 @@ class Bot:
prompt = b.group(1) prompt = b.group(1)
# raw_content_body used for construct formatted_body # raw_content_body used for construct formatted_body
try: try:
await self.bing(room_id, reply_to_event_id, prompt, sender_id, raw_user_message) await self.bing(
room_id,
reply_to_event_id,
prompt,
sender_id,
raw_user_message
)
except Exception as e: except Exception as e:
await send_room_message(self.client, room_id, reply_message=str(e)) await send_room_message(self.client, room_id, reply_message=str(e))
@ -206,9 +234,9 @@ class Bot:
return return
logger.error( logger.error(
f"Failed to decrypt message: {event.event_id} from {event.sender} in {room.room_id}\n" + \ f"Failed to decrypt message: {event.event_id} from {event.sender} in {room.room_id}\n" +
"Please make sure the bot current session is verified" "Please make sure the bot current session is verified"
) )
# invite_callback event # invite_callback event
async def invite_callback(self, room: MatrixRoom, event: InviteMemberEvent) -> None: async def invite_callback(self, room: MatrixRoom, event: InviteMemberEvent) -> None:
@ -346,7 +374,9 @@ class Bot:
# keyboard so that user can accept/reject via keyboard. # keyboard so that user can accept/reject via keyboard.
# For emoji verification bot must not run as service or # For emoji verification bot must not run as service or
# in background. # in background.
yn = input("Do the emojis match? (Y/N) (C for Cancel) ") # yn = input("Do the emojis match? (Y/N) (C for Cancel) ")
# automatic match, so we use y
yn = "y"
if yn.lower() == "y": if yn.lower() == "y":
estr = ("Match! The verification for this " estr = ("Match! The verification for this "
"device will be accepted.") "device will be accepted.")
@ -455,7 +485,7 @@ class Bot:
text = text.strip() text = text.strip()
try: try:
await send_room_message(self.client, room_id, reply_message=text, await send_room_message(self.client, room_id, reply_message=text,
reply_to_event_id=reply_to_event_id, sender_id=sender_id, user_message=raw_user_message) reply_to_event_id=reply_to_event_id, sender_id=sender_id, user_message=raw_user_message)
except Exception as e: except Exception as e:
logger.error(f"Error: {e}", exc_info=True) logger.error(f"Error: {e}", exc_info=True)
@ -478,7 +508,7 @@ class Bot:
logger.error(f"Error: {e}", exc_info=True) logger.error(f"Error: {e}", exc_info=True)
# !bing command # !bing command
async def bing(self, room_id, reply_to_event_id, prompt, sender_id, raw_content_body): async def bing(self, room_id, reply_to_event_id, prompt, sender_id, raw_user_message):
try: try:
# sending typing state # sending typing state
await self.client.room_typing(room_id, timeout=180000) await self.client.room_typing(room_id, timeout=180000)
@ -490,7 +520,7 @@ class Bot:
text = text.strip() text = text.strip()
try: try:
await send_room_message(self.client, room_id, reply_message=text, await send_room_message(self.client, room_id, reply_message=text,
reply_to_event_id=reply_to_event_id, sender=sender_id, raw_content_body=raw_content_body) reply_to_event_id=reply_to_event_id, sender_id=sender_id, user_message=raw_user_message)
except Exception as e: except Exception as e:
logger.error(f"Error: {e}", exc_info=True) logger.error(f"Error: {e}", exc_info=True)
@ -499,14 +529,20 @@ class Bot:
try: try:
await self.client.room_typing(room_id, timeout=180000) await self.client.room_typing(room_id, timeout=180000)
# generate image # generate image
generated_image_path = self.imageGen.save_images( try:
self.imageGen.get_images(prompt),
"images", links = await self.imageGen.get_images(prompt)
) image_path = await self.imageGen.save_images(links, "images")
except Exception as e:
logger.error(f"Image Generation error: {e}", exc_info=True)
# send image # send image
if generated_image_path != "": try:
await send_room_image(self.client, room_id, generated_image_path) await send_room_image(self.client, room_id, image_path)
await self.client.room_typing(room_id, bool=False) await self.client.room_typing(room_id, typing_state=False)
except Exception as e:
logger.error(e, exc_info=True)
except Exception as e: except Exception as e:
logger.error(f"Error: {e}", exc_info=True) logger.error(f"Error: {e}", exc_info=True)
@ -554,12 +590,11 @@ class Bot:
async def trust_own_devices(self) -> None: async def trust_own_devices(self) -> None:
await self.client.sync(timeout=30000, full_state=True) await self.client.sync(timeout=30000, full_state=True)
for device_id, olm_device in self.client.device_store[ for device_id, olm_device in self.client.device_store[
self.user_id].items(): self.user_id].items():
logger.debug("My other devices are: " logger.debug("My other devices are: "
f"device_id={device_id}, " f"device_id={device_id}, "
f"olm_device={olm_device}.") f"olm_device={olm_device}.")
logger.info("Setting up trust for my own " logger.info("Setting up trust for my own "
f"device {device_id} and session key " f"device {device_id} and session key "
f"{olm_device.keys['ed25519']}.") f"{olm_device.keys['ed25519']}.")
self.client.verify_device(olm_device) self.client.verify_device(olm_device)