From 666c52dd18e428d3c6f0e524a5406a5ccb47a265 Mon Sep 17 00:00:00 2001 From: Hanssen Date: Sat, 9 Dec 2023 17:34:19 +0800 Subject: [PATCH 01/10] refactor: send photos * Allow text with photos, see `RequestResponseContainer.response_images` * All captions are formatted in Markdown * Use `md2tgmd` to escape markdown content, keep the original format --- BotHandler.py | 314 +++++++++++++++++++----------------- RequestResponseContainer.py | 3 + requirements.txt | 2 + 3 files changed, 174 insertions(+), 145 deletions(-) diff --git a/BotHandler.py b/BotHandler.py index 0a2ee8f5..dce3473a 100644 --- a/BotHandler.py +++ b/BotHandler.py @@ -17,6 +17,7 @@ from __future__ import annotations +import io import asyncio import datetime import logging @@ -25,9 +26,12 @@ import time from typing import List, Dict +import requests import telegram from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, InputMediaPhoto from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters, CallbackQueryHandler +import md2tgmd +from PIL import Image import LoggingHandler import ProxyAutomation @@ -59,14 +63,6 @@ BOT_COMMAND_ADMIN_UNBAN = "unban" BOT_COMMAND_ADMIN_BROADCAST = "broadcast" -# List of markdown chars to escape with \\ -MARKDOWN_ESCAPE = ["_", "*", "[", "]", "(", ")", "~", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"] -MARKDOWN_ESCAPE_MINIMUM = ["_", "[", "]", "(", ")", "~", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"] -MARKDOWN_MODE_ESCAPE_NONE = 0 -MARKDOWN_MODE_ESCAPE_MINIMUM = 1 -MARKDOWN_MODE_ESCAPE_ALL = 2 -MARKDOWN_MODE_NO_MARKDOWN = 3 - # After how many seconds restart bot polling if error occurs RESTART_ON_ERROR_DELAY = 30 @@ -198,47 +194,7 @@ async def send_message_async(config: dict, messages: List[Dict], # Construct markup request_response.reply_markup = InlineKeyboardMarkup(build_menu(buttons, n_cols=2)) - # Send message as image - if (request_response.request_type == RequestResponseContainer.REQUEST_TYPE_DALLE - or request_response.request_type == RequestResponseContainer.REQUEST_TYPE_BING_IMAGEGEN) \ - and not request_response.error: - # Single photo - if type(request_response.response) == str: - request_response.message_id = (await (telegram.Bot(config["telegram"]["api_key"]).sendPhoto( - chat_id=request_response.user["user_id"], - photo=request_response.response, - reply_to_message_id=request_response - .reply_message_id, - reply_markup=request_response.reply_markup))) \ - .message_id - - # Multiple photos (send media group + markup as seperate messages) - else: - # Collect media group - media_group = [] - for url in request_response.response: - if not url.lower().endswith(".svg"): - media_group.append(InputMediaPhoto(media=url)) - - # Send it - media_group_message_id = (await (telegram.Bot(config["telegram"]["api_key"]).sendMediaGroup( - chat_id=request_response.user["user_id"], - media=media_group, - reply_to_message_id=request_response.reply_message_id)))[0].message_id - - # Send reply markup and get message ID - request_response.message_id = await send_reply(config["telegram"]["api_key"], - request_response.user["user_id"], - messages[lang]["media_group_response"] - .format(request_response.request), - media_group_message_id, - markdown=False, - reply_markup=request_response.reply_markup, - edit_message_id=request_response.message_id) - - # Send message as text - else: - await _send_text_async_split(config, request_response, end) + await _send_text_async_split(config, messages[lang], request_response, end) # First or any other message (text only) else: @@ -260,7 +216,7 @@ async def send_message_async(config: dict, messages: List[Dict], request_response.reply_message_id)) request_response.reply_markup = InlineKeyboardMarkup(build_menu([button_stop])) - await _send_text_async_split(config, request_response, end) + await _send_text_async_split(config, messages[lang], request_response, end) # Save new data request_response.response_len_last = len(request_response.response.strip()) @@ -274,16 +230,42 @@ async def send_message_async(config: dict, messages: List[Dict], request_response.response_timestamp = time.time() +async def parse_img(img: str): + res = requests.get(img, timeout=30) + img = Image.open(io.BytesIO(res.content)) + try: + img.verify() + except Exception: + return None + + if img.format not in ["BMP", + "GIF", + "PNG", + "WEBP", + "JPEG", + "JPEG2000"]: + with io.BytesIO() as converted: + img.save(converted, "JPEG") + return converted.getvalue() + + return res.content + + async def _send_text_async_split(config: dict, + messages: Dict, request_response: RequestResponseContainer.RequestResponseContainer, end=False): """ Sends text in multiple messages if needed (must be previously split) :param config: + :param messages: :param request_response: :param end: :return: """ + request_response.response_images = [img for + img in (await asyncio.gather(*[parse_img(img) for img in request_response.response_images])) + if img is not None] # Send all parts of message response_part_counter_init = request_response.response_part_counter while True: @@ -307,37 +289,75 @@ async def _send_text_async_split(config: dict, if response_part_counter_init == request_response.response_part_counter: edit_id = request_response.message_id - # Check if it is not empty - if len(response_part) > 0: - # Send with markup and exit from loop if it's the last part - if response_part_index_stop == len(request_response.response): - # Add cursor symbol? - if not end and config["telegram"]["add_cursor_symbol"]: - response_part += config["telegram"]["cursor_symbol"] - - request_response.message_id = await send_reply(config["telegram"]["api_key"], - request_response.user["user_id"], - response_part, - reply_to_id, - markdown=True, - reply_markup=request_response.reply_markup, - edit_message_id=edit_id) - break - # Send as new message without markup and increment counter - else: - request_response.message_id = await send_reply(config["telegram"]["api_key"], - request_response.user["user_id"], - response_part, - reply_to_id, - markdown=True, - reply_markup=None, - edit_message_id=edit_id) - request_response.response_part_counter += 1 - - # Exit from loop if no response in current part - else: + # Send with markup and exit from loop if it's the last part + if response_part_index_stop == len(request_response.response): + try: + # Send message as image + # Single photo + if end and len(request_response.response_images) == 1: + request_response.message_id = await send_photo(config["telegram"]["api_key"], + request_response.user["user_id"], + request_response.response_images[0], + response_part, + reply_to_id, + True, + request_response.reply_markup) + break + + # Multiple photos (send media group + markup as seperate messages) + # Collect media group + if end and len(request_response.response_images) > 1: + media_group = [InputMediaPhoto(media=img) for img in request_response.response_images] + + # Send it + for imgs in (media_group[i:i + 9] for i in range(0, len(media_group), 9)): + reply_to_id = await send_media_group(config["telegram"]["api_key"], + request_response.user["user_id"], + imgs, + response_part, + reply_to_id, + True) + response_part = "" + + # Send reply markup and get message ID + request_response.message_id = await send_reply(config["telegram"]["api_key"], + request_response.user["user_id"], + messages["media_group_response"] + .format(request_response.request), + reply_to_id, + markdown=False, + reply_markup=request_response.reply_markup, + edit_message_id=edit_id) + break + except Exception as err: + logging.error("Error while sending images {} {}".format( + request_response.response_images, + str(err))) + + # Add cursor symbol? + if not end and config["telegram"]["add_cursor_symbol"]: + response_part += config["telegram"]["cursor_symbol"] + + request_response.message_id = await send_reply(config["telegram"]["api_key"], + request_response.user["user_id"], + response_part, + reply_to_id, + markdown=True, + reply_markup=request_response.reply_markup, + edit_message_id=edit_id) break + # Send as new message without markup and increment counter + else: + request_response.message_id = await send_reply(config["telegram"]["api_key"], + request_response.user["user_id"], + response_part, + reply_to_id, + markdown=True, + reply_markup=None, + edit_message_id=edit_id) + request_response.response_part_counter += 1 + async def send_reply(api_key: str, chat_id: int, message: str, reply_to_message_id: int | None, markdown=False, reply_markup=None, edit_message_id=None): @@ -352,68 +372,8 @@ async def send_reply(api_key: str, chat_id: int, message: str, reply_to_message_ :param edit_message_id: Set message id to edit it instead of sending a new one :return: message_id if sent correctly, or None if not """ - # Send as markdown - if markdown: - # MARKDOWN_MODE_ESCAPE_NONE - message_id = await _send_parse(api_key, chat_id, message, reply_to_message_id, - MARKDOWN_MODE_ESCAPE_NONE, reply_markup, edit_message_id) - if message_id is None or message_id < 0: - # MARKDOWN_MODE_ESCAPE_MINIMUM - message_id = await _send_parse(api_key, chat_id, message, reply_to_message_id, - MARKDOWN_MODE_ESCAPE_MINIMUM, reply_markup, edit_message_id) - if message_id is None or message_id < 0: - # MARKDOWN_MODE_ESCAPE_ALL - message_id = await _send_parse(api_key, chat_id, message, reply_to_message_id, - MARKDOWN_MODE_ESCAPE_ALL, reply_markup, edit_message_id) - if message_id is None or message_id < 0: - # MARKDOWN_MODE_NO_MARKDOWN - message_id = await _send_parse(api_key, chat_id, message, reply_to_message_id, - MARKDOWN_MODE_NO_MARKDOWN, reply_markup, edit_message_id) - if message_id is None or message_id < 0: - raise Exception("Unable to send message in any markdown escape mode!") - else: - return message_id - else: - return message_id - else: - return message_id - else: - return message_id - - # Markdown parsing is disabled - send as plain message - else: - return await _send_parse(api_key, chat_id, message, reply_to_message_id, - MARKDOWN_MODE_NO_MARKDOWN, reply_markup, edit_message_id) - - -async def _send_parse(api_key: str, chat_id: int, message: str, reply_to_message_id: int | None, escape_mode: int, - reply_markup, edit_message_id): - """ - Parses message and sends it as reply - :param api_key: - :param chat_id: - :param message: - :param reply_to_message_id: - :param escape_mode: - :param reply_markup: - :param edit_message_id: - :return: message_id if sent correctly, or None if not - """ try: - # Escape some chars - if escape_mode == MARKDOWN_MODE_ESCAPE_MINIMUM: - for i in range(len(MARKDOWN_ESCAPE_MINIMUM)): - escape_char = MARKDOWN_ESCAPE_MINIMUM[i] - message = message.replace(escape_char, "\\" + escape_char) - - # Escape all chars - elif escape_mode == MARKDOWN_MODE_ESCAPE_ALL: - for i in range(len(MARKDOWN_ESCAPE)): - escape_char = MARKDOWN_ESCAPE[i] - message = message.replace(escape_char, "\\" + escape_char) - - # Create parse mode - parse_mode = None if escape_mode == MARKDOWN_MODE_NO_MARKDOWN else "MarkdownV2" + parse_mode, message = ("MarkdownV2", md2tgmd.escape(message)) if markdown else (None, message) # Send as new message if edit_message_id is None or edit_message_id < 0: @@ -437,11 +397,75 @@ async def _send_parse(api_key: str, chat_id: int, message: str, reply_to_message return message_id except Exception as e: - if escape_mode < MARKDOWN_MODE_NO_MARKDOWN: - logging.warning("Error sending reply with escape_mode {0}: {1}\t You can ignore this message" - .format(escape_mode, str(e))) - else: - logging.error("Error sending reply with escape_mode {}!".format(escape_mode), exc_info=e) + if markdown: + logging.warning("Error sending reply with markdown {0}: {1}\t You can ignore this message" + .format(markdown, str(e))) + return await send_reply(api_key, chat_id, message, reply_to_message_id, False, reply_markup, edit_message_id) + logging.error("Error sending reply with markdown {}!".format(markdown), exc_info=e) + return None + + +async def send_photo(api_key: str, chat_id: int, photo, caption: str, + reply_to_message_id: int | None, markdown=False, reply_markup=None): + """ + Sends photo to chat + :param api_key: Telegram bot API key + :param chat_id: Chat id to send to + :param photo: Photo to send + :param caption: Message to send + :param reply_to_message_id: Message ID to reply on + :param markdown: True to parse as markdown + :param reply_markup: Buttons + :return: message_id if sent correctly, or None if not + """ + try: + parse_mode, caption = ("MarkdownV2", md2tgmd.escape(caption)) if markdown else (None, caption) + return (await (telegram.Bot(api_key).send_photo( + chat_id=chat_id, + photo=photo, + caption=caption, + parse_mode=parse_mode, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + write_timeout=60))).message_id + + except Exception as e: + if markdown: + logging.warning("Error sending photo with markdown {0}: {1}\t You can ignore this message" + .format(markdown, str(e))) + return await send_photo(api_key, chat_id, photo, caption, reply_to_message_id, False, reply_markup) + logging.error("Error sending photo with markdown {}!".format(markdown), exc_info=e) + return None + + +async def send_media_group(api_key: str, chat_id: int, media, caption: str, + reply_to_message_id: int | None, markdown=False): + """ + Sends photo to chat + :param api_key: Telegram bot API key + :param chat_id: Chat id to send to + :param photo: Photo to send + :param caption: Message to send + :param reply_to_message_id: Message ID to reply on + :param markdown: True to parse as markdown + :return: message_id if sent correctly, or None if not + """ + try: + parse_mode, caption = ("MarkdownV2", md2tgmd.escape(caption)) if markdown else (None, caption) + + return (await (telegram.Bot(api_key).sendMediaGroup( + chat_id=chat_id, + media=media, + caption=caption, + parse_mode=parse_mode, + reply_to_message_id=reply_to_message_id, + write_timeout=120)))[0].message_id + except Exception as e: + if markdown: + logging.warning("Error sending media group with markdown {0}: {1}\t You can ignore this message" + .format(markdown, str(e))) + return await send_media_group(api_key, chat_id, media, caption, reply_to_message_id, False) + logging.error("Error sending media group with markdown {}!".format(markdown), exc_info=e) return None diff --git a/RequestResponseContainer.py b/RequestResponseContainer.py index 955895c3..c66ceff4 100644 --- a/RequestResponseContainer.py +++ b/RequestResponseContainer.py @@ -43,6 +43,7 @@ def __init__(self, request="", response="", response_len_last=0, + response_images=[], request_type=REQUEST_TYPE_CHATGPT, request_timestamp="", response_timestamp="", @@ -59,6 +60,7 @@ def __init__(self, :param request: text request :param response: text response :param response_len_last: length of last response (for editing aka live replying) + :param response_images: images in the responses :param request_type: REQUEST_TYPE_CHATGPT / REQUEST_TYPE_DALLE / ... :param request_timestamp: timestamp of request (for data collecting) :param response_timestamp: timestamp of response (for data collecting) @@ -75,6 +77,7 @@ def __init__(self, self.request = request self.response = response self.response_len_last = response_len_last + self.response_images = response_images self.request_type = request_type self.request_timestamp = request_timestamp self.response_timestamp = response_timestamp diff --git a/requirements.txt b/requirements.txt index aeb3a753..0893c152 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ revChatGPT==6.8.6 git+https://github.com/F33RNI/EdgeGPT@main#egg=EdgeGPT git+https://github.com/dsdanielpark/Bard-API@main +git+https://github.com/handsome0hell/md2tgmd.git@main python-telegram-bot==20.3 openai>=0.26.4 tiktoken>=0.2.0 @@ -11,3 +12,4 @@ telegram>=0.0.1 psutil>=5.9.4 BingImageCreator>=0.5.0 langdetect>=1.0.9 +pillow>=10.1.0 From 60e0685363b5251d7a65fb29b1ebd10d458737a2 Mon Sep 17 00:00:00 2001 From: Hanssen Date: Sat, 9 Dec 2023 17:37:22 +0800 Subject: [PATCH 02/10] feat: show images from Bard's reply --- BardModule.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BardModule.py b/BardModule.py index 0899dd36..b643ab05 100644 --- a/BardModule.py +++ b/BardModule.py @@ -158,6 +158,8 @@ def process_request(self, request_response: RequestResponseContainer) -> None: logging.info("Response successfully processed for user {0} ({1})" .format(request_response.user["user_name"], request_response.user["user_id"])) request_response.response = bard_response["content"] + if "images" in bard_response and len(bard_response["images"]) > 0: + request_response.response_images = bard_response["images"] # Save conversation logging.info("Saving conversation_id as {} and response_id as {} and choice_id as {}". From 76983719f8513c7806c07eb35a6bfca08155888b Mon Sep 17 00:00:00 2001 From: Fern Lane Date: Sat, 9 Dec 2023 18:52:34 +0100 Subject: [PATCH 03/10] Update BotHandler.py Fix formatting --- BotHandler.py | 50 +++++++++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/BotHandler.py b/BotHandler.py index dce3473a..2bd874b5 100644 --- a/BotHandler.py +++ b/BotHandler.py @@ -24,11 +24,12 @@ import multiprocessing import threading import time -from typing import List, Dict +from typing import List, Dict, Sequence import requests import telegram -from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, InputMediaPhoto +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, InputMediaPhoto, InputMediaAudio, \ + InputMediaDocument, InputMediaVideo from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters, CallbackQueryHandler import md2tgmd from PIL import Image @@ -239,11 +240,11 @@ async def parse_img(img: str): return None if img.format not in ["BMP", - "GIF", - "PNG", - "WEBP", - "JPEG", - "JPEG2000"]: + "GIF", + "PNG", + "WEBP", + "JPEG", + "JPEG2000"]: with io.BytesIO() as converted: img.save(converted, "JPEG") return converted.getvalue() @@ -263,8 +264,10 @@ async def _send_text_async_split(config: dict, :param end: :return: """ - request_response.response_images = [img for - img in (await asyncio.gather(*[parse_img(img) for img in request_response.response_images])) + request_response.response_images = [img + for img in + (await asyncio.gather(*[parse_img(img) + for img in request_response.response_images])) if img is not None] # Send all parts of message response_part_counter_init = request_response.response_part_counter @@ -312,11 +315,11 @@ async def _send_text_async_split(config: dict, # Send it for imgs in (media_group[i:i + 9] for i in range(0, len(media_group), 9)): reply_to_id = await send_media_group(config["telegram"]["api_key"], - request_response.user["user_id"], - imgs, - response_part, - reply_to_id, - True) + request_response.user["user_id"], + imgs, + response_part, + reply_to_id, + True) response_part = "" # Send reply markup and get message ID @@ -331,8 +334,8 @@ async def _send_text_async_split(config: dict, break except Exception as err: logging.error("Error while sending images {} {}".format( - request_response.response_images, - str(err))) + request_response.response_images, + str(err))) # Add cursor symbol? if not end and config["telegram"]["add_cursor_symbol"]: @@ -400,13 +403,14 @@ async def send_reply(api_key: str, chat_id: int, message: str, reply_to_message_ if markdown: logging.warning("Error sending reply with markdown {0}: {1}\t You can ignore this message" .format(markdown, str(e))) - return await send_reply(api_key, chat_id, message, reply_to_message_id, False, reply_markup, edit_message_id) + return await send_reply(api_key, chat_id, message, reply_to_message_id, False, reply_markup, + edit_message_id) logging.error("Error sending reply with markdown {}!".format(markdown), exc_info=e) return None async def send_photo(api_key: str, chat_id: int, photo, caption: str, - reply_to_message_id: int | None, markdown=False, reply_markup=None): + reply_to_message_id: int | None, markdown=False, reply_markup=None): """ Sends photo to chat :param api_key: Telegram bot API key @@ -438,13 +442,17 @@ async def send_photo(api_key: str, chat_id: int, photo, caption: str, return None -async def send_media_group(api_key: str, chat_id: int, media, caption: str, - reply_to_message_id: int | None, markdown=False): +async def send_media_group(api_key: str, + chat_id: int, + media: Sequence[InputMediaAudio | InputMediaDocument | InputMediaPhoto | InputMediaVideo], + caption: str, + reply_to_message_id: int | None, + markdown=False): """ Sends photo to chat :param api_key: Telegram bot API key :param chat_id: Chat id to send to - :param photo: Photo to send + :param media: Media to send :param caption: Message to send :param reply_to_message_id: Message ID to reply on :param markdown: True to parse as markdown From 965802a242f3ef5b1c81c1d8d837fbff54d6a30f Mon Sep 17 00:00:00 2001 From: Hanssen Date: Sun, 10 Dec 2023 01:57:36 +0800 Subject: [PATCH 04/10] fix: bing image generator --- BingImageGenModule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BingImageGenModule.py b/BingImageGenModule.py index 2092ee3d..e5296362 100644 --- a/BingImageGenModule.py +++ b/BingImageGenModule.py @@ -132,7 +132,7 @@ def process_request(self, request_response: RequestResponseContainer) -> None: # Use all generated images logging.info("Response successfully processed for user {0} ({1})" .format(request_response.user["user_name"], request_response.user["user_id"])) - request_response.response = response_urls + request_response.response_images = response_urls # Exit requested except KeyboardInterrupt: From 993dd9e68356f3df5a5b7c1661528535672dc711 Mon Sep 17 00:00:00 2001 From: Hanssen Date: Sun, 10 Dec 2023 02:41:58 +0800 Subject: [PATCH 05/10] fix: svg images from bing image generator --- BotHandler.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/BotHandler.py b/BotHandler.py index 2bd874b5..10dc4e71 100644 --- a/BotHandler.py +++ b/BotHandler.py @@ -231,12 +231,14 @@ async def send_message_async(config: dict, messages: List[Dict], request_response.response_timestamp = time.time() -async def parse_img(img: str): - res = requests.get(img, timeout=30) - img = Image.open(io.BytesIO(res.content)) +async def parse_img(img_source: str): + img = None try: + res = requests.get(img_source, timeout=30) + img = Image.open(io.BytesIO(res.content)) img.verify() - except Exception: + except Exception as e: + logging.warning("Invalid image from {}: {}, You can ignore this message".format(img_source, str(e))) return None if img.format not in ["BMP", From d59f4f757c64042030795f76fa66227349f1d3bc Mon Sep 17 00:00:00 2001 From: F33RNI Date: Sat, 9 Dec 2023 22:15:19 +0300 Subject: [PATCH 06/10] Fix images --- BotHandler.py | 104 ++++++++++++++---------------------- QueueHandler.py | 65 ++++++++++++---------- RequestResponseContainer.py | 9 +++- 3 files changed, 85 insertions(+), 93 deletions(-) diff --git a/BotHandler.py b/BotHandler.py index 10dc4e71..c08d6d51 100644 --- a/BotHandler.py +++ b/BotHandler.py @@ -17,7 +17,6 @@ from __future__ import annotations -import io import asyncio import datetime import logging @@ -26,13 +25,11 @@ import time from typing import List, Dict, Sequence -import requests +import md2tgmd import telegram from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, InputMediaPhoto, InputMediaAudio, \ InputMediaDocument, InputMediaVideo from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters, CallbackQueryHandler -import md2tgmd -from PIL import Image import LoggingHandler import ProxyAutomation @@ -127,9 +124,7 @@ async def send_message_async(config: dict, messages: List[Dict], # Fix empty message if end: - if not request_response.response \ - or (type(request_response.response) == list and len(request_response.response) == 0) \ - or (type(request_response.response) == str and len(request_response.response.strip()) <= 0): + if not request_response.response and len(request_response.response_images) == 0: request_response.response = messages[lang]["empty_message"] # Reset message parts if new response is smaller than previous one (EdgeGPT API bug) @@ -140,7 +135,7 @@ async def send_message_async(config: dict, messages: List[Dict], request_response.response_raw_len_last = len(request_response.response) # Split large response into parts (by index) - if type(request_response.response) == str and len(request_response.response) > 0: + if request_response.response: while True: index_start = request_response.response_part_positions[-1] response_part_length = len(request_response.response[index_start:]) @@ -205,6 +200,7 @@ async def send_message_async(config: dict, messages: List[Dict], # It's time to edit message, and we have any text to send, and we have new text if time_current - request_response.response_send_timestamp_last \ >= config["telegram"]["edit_message_every_seconds_num"] \ + and request_response.response \ and len(request_response.response.strip()) > 0 \ and (request_response.response_len_last <= 0 or len(request_response.response.strip()) != request_response.response_len_last): @@ -231,29 +227,6 @@ async def send_message_async(config: dict, messages: List[Dict], request_response.response_timestamp = time.time() -async def parse_img(img_source: str): - img = None - try: - res = requests.get(img_source, timeout=30) - img = Image.open(io.BytesIO(res.content)) - img.verify() - except Exception as e: - logging.warning("Invalid image from {}: {}, You can ignore this message".format(img_source, str(e))) - return None - - if img.format not in ["BMP", - "GIF", - "PNG", - "WEBP", - "JPEG", - "JPEG2000"]: - with io.BytesIO() as converted: - img.save(converted, "JPEG") - return converted.getvalue() - - return res.content - - async def _send_text_async_split(config: dict, messages: Dict, request_response: RequestResponseContainer.RequestResponseContainer, @@ -266,23 +239,21 @@ async def _send_text_async_split(config: dict, :param end: :return: """ - request_response.response_images = [img - for img in - (await asyncio.gather(*[parse_img(img) - for img in request_response.response_images])) - if img is not None] # Send all parts of message response_part_counter_init = request_response.response_part_counter while True: - # Get current part of response - response_part_index_start \ - = request_response.response_part_positions[request_response.response_part_counter] - response_part_index_stop = len(request_response.response) - if request_response.response_part_counter < len(request_response.response_part_positions) - 1: - response_part_index_stop \ - = request_response.response_part_positions[request_response.response_part_counter + 1] - response_part \ - = request_response.response[response_part_index_start:response_part_index_stop].strip() + # Get current part of response (text) + if request_response.response: + response_part_index_start \ + = request_response.response_part_positions[request_response.response_part_counter] + response_part_index_stop = len(request_response.response) + if request_response.response_part_counter < len(request_response.response_part_positions) - 1: + response_part_index_stop \ + = request_response.response_part_positions[request_response.response_part_counter + 1] + response_part = request_response.response[response_part_index_start:response_part_index_stop].strip() + else: + response_part = None + response_part_index_stop = 0 # Get message ID to reply to reply_to_id = request_response.reply_message_id @@ -295,7 +266,7 @@ async def _send_text_async_split(config: dict, edit_id = request_response.message_id # Send with markup and exit from loop if it's the last part - if response_part_index_stop == len(request_response.response): + if not request_response.response or response_part_index_stop == len(request_response.response): try: # Send message as image # Single photo @@ -303,25 +274,29 @@ async def _send_text_async_split(config: dict, request_response.message_id = await send_photo(config["telegram"]["api_key"], request_response.user["user_id"], request_response.response_images[0], - response_part, - reply_to_id, - True, - request_response.reply_markup) + caption=response_part, + reply_to_message_id=reply_to_id, + markdown=True, + reply_markup=request_response.reply_markup) break - # Multiple photos (send media group + markup as seperate messages) + # Multiple photos (send media group + markup as separate messages) # Collect media group if end and len(request_response.response_images) > 1: - media_group = [InputMediaPhoto(media=img) for img in request_response.response_images] + # Build media_ground ignoring SVG images + media_group = [] + for image_url in request_response.response_images: + if not image_url.lower().endswith(".svg"): + media_group.append(InputMediaPhoto(media=image_url)) # Send it - for imgs in (media_group[i:i + 9] for i in range(0, len(media_group), 9)): + for image_url in (media_group[i:i + 9] for i in range(0, len(media_group), 9)): reply_to_id = await send_media_group(config["telegram"]["api_key"], - request_response.user["user_id"], - imgs, - response_part, - reply_to_id, - True) + chat_id=request_response.user["user_id"], + media=image_url, + caption=response_part, + reply_to_message_id=reply_to_id, + markdown=True) response_part = "" # Send reply markup and get message ID @@ -329,15 +304,13 @@ async def _send_text_async_split(config: dict, request_response.user["user_id"], messages["media_group_response"] .format(request_response.request), - reply_to_id, + reply_to_message_id=reply_to_id, markdown=False, reply_markup=request_response.reply_markup, edit_message_id=edit_id) break except Exception as err: - logging.error("Error while sending images {} {}".format( - request_response.response_images, - str(err))) + logging.error("Error while sending images {} {}".format(request_response.response_images, str(err))) # Add cursor symbol? if not end and config["telegram"]["add_cursor_symbol"]: @@ -411,7 +384,7 @@ async def send_reply(api_key: str, chat_id: int, message: str, reply_to_message_ return None -async def send_photo(api_key: str, chat_id: int, photo, caption: str, +async def send_photo(api_key: str, chat_id: int, photo, caption: str | None, reply_to_message_id: int | None, markdown=False, reply_markup=None): """ Sends photo to chat @@ -425,7 +398,10 @@ async def send_photo(api_key: str, chat_id: int, photo, caption: str, :return: message_id if sent correctly, or None if not """ try: - parse_mode, caption = ("MarkdownV2", md2tgmd.escape(caption)) if markdown else (None, caption) + if caption: + parse_mode, caption = ("MarkdownV2", md2tgmd.escape(caption)) if markdown else (None, caption) + else: + parse_mode = None return (await (telegram.Bot(api_key).send_photo( chat_id=chat_id, photo=photo, diff --git a/QueueHandler.py b/QueueHandler.py index e949db2e..8d269a62 100644 --- a/QueueHandler.py +++ b/QueueHandler.py @@ -685,7 +685,7 @@ def _collect_data(self, request_response: RequestResponseContainer, log_request= request_str_to_format = self.config["data_collecting"]["request_format"].replace("\\n", "\n") \ .replace("\\t", "\t").replace("\\r", "\r") - # Log image + # Log image request try: if request_response.image_url: logging.info("Downloading user image") @@ -712,34 +712,45 @@ def _collect_data(self, request_response: RequestResponseContainer, log_request= # Log response else: - response = "None" - try: - # DALL-E or BingImageGen response without error - if (request_response.request_type == RequestResponseContainer.REQUEST_TYPE_DALLE - or request_response.request_type == RequestResponseContainer.REQUEST_TYPE_BING_IMAGEGEN) \ - and not request_response.error: - response_url = request_response.response if type(request_response.response) == str \ - else request_response.response[0] - response = base64.b64encode(requests.get(response_url, timeout=120).content) \ - .decode("utf-8") - - # Text response (ChatGPT, EdgeGPT, Bard) - else: - response = request_response.response - except Exception as e: - logging.warning("Can't parse response for data logging!", exc_info=e) - response = str(response) - - # Log response + # Get formatter response_str_to_format = self.config["data_collecting"]["response_format"].replace("\\n", "\n") \ .replace("\\t", "\t").replace("\\r", "\r") - log_file.write(response_str_to_format.format(request_response.response_timestamp, - request_response.id, - request_response.user["user_name"], - request_response.user["user_id"], - RequestResponseContainer - .REQUEST_NAMES[request_response.request_type], - response)) + + # Text + if request_response.response and type(request_response.response) == str: + log_file.write(response_str_to_format.format(request_response.response_timestamp, + request_response.id, + request_response.user["user_name"], + request_response.user["user_id"], + RequestResponseContainer + .REQUEST_NAMES[request_response.request_type], + request_response.response)) + + # Images + for image_url in request_response.response_images: + try: + response = base64.b64encode(requests.get(image_url, timeout=120).content).decode("utf-8") + log_file.write(response_str_to_format.format(request_response.response_timestamp, + request_response.id, + request_response.user["user_name"], + request_response.user["user_id"], + RequestResponseContainer + .REQUEST_NAMES[request_response.request_type], + response)) + # Error logging image + except Exception as e: + logging.warning("Error logging image: {}".format(image_url), exc_info=e) + + if request_response.response and type(request_response.response) == str: + response_str_to_format = self.config["data_collecting"]["response_format"].replace("\\n", "\n") \ + .replace("\\t", "\t").replace("\\r", "\r") + log_file.write(response_str_to_format.format(request_response.response_timestamp, + request_response.id, + request_response.user["user_name"], + request_response.user["user_id"], + RequestResponseContainer + .REQUEST_NAMES[request_response.request_type], + request_response.response)) # Log confirmation logging.info("The {0} were written to the file: {1}".format("request" if log_request else "response", diff --git a/RequestResponseContainer.py b/RequestResponseContainer.py index c66ceff4..b4a5ab60 100644 --- a/RequestResponseContainer.py +++ b/RequestResponseContainer.py @@ -43,7 +43,7 @@ def __init__(self, request="", response="", response_len_last=0, - response_images=[], + response_images=None, request_type=REQUEST_TYPE_CHATGPT, request_timestamp="", response_timestamp="", @@ -77,7 +77,6 @@ def __init__(self, self.request = request self.response = response self.response_len_last = response_len_last - self.response_images = response_images self.request_type = request_type self.request_timestamp = request_timestamp self.response_timestamp = response_timestamp @@ -86,6 +85,12 @@ def __init__(self, self.pid = pid self.image_url = image_url + # Empty or response_images + if response_images is None: + self.response_images = [] + else: + self.response_images = response_images + self.processing_start_timestamp = 0. self.error = False From 99bfe62db8511ee0c10cb9c238445d91fdcd1797 Mon Sep 17 00:00:00 2001 From: F33RNI Date: Sat, 9 Dec 2023 22:23:17 +0300 Subject: [PATCH 07/10] Remove Pillow from requirements.txt --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0893c152..cb3b27d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,3 @@ telegram>=0.0.1 psutil>=5.9.4 BingImageCreator>=0.5.0 langdetect>=1.0.9 -pillow>=10.1.0 From 3ad56cf46c1ab99871b51e6cfdc3957d1b0ed5b6 Mon Sep 17 00:00:00 2001 From: Hanssen Date: Sun, 10 Dec 2023 03:41:47 +0800 Subject: [PATCH 08/10] feat: use content-type to check file type --- BotHandler.py | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/BotHandler.py b/BotHandler.py index c08d6d51..b5242593 100644 --- a/BotHandler.py +++ b/BotHandler.py @@ -30,6 +30,7 @@ from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, InputMediaPhoto, InputMediaAudio, \ InputMediaDocument, InputMediaVideo from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters, CallbackQueryHandler +import requests import LoggingHandler import ProxyAutomation @@ -227,6 +228,25 @@ async def send_message_async(config: dict, messages: List[Dict], request_response.response_timestamp = time.time() +async def parse_img(img_source: str): + """ + Test if an image source is valid + :param img_source: + :return: + """ + try: + res = requests.head(img_source, timeout=10, headers={ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/91.4472.114 Safari/537.36"}) + if res.headers.get("content-type") == "image/svg+xml": + return None + except Exception as e: + logging.warning("Invalid image from {}: {}, You can ignore this message".format(img_source, str(e))) + return None + return img_source + + async def _send_text_async_split(config: dict, messages: Dict, request_response: RequestResponseContainer.RequestResponseContainer, @@ -241,6 +261,10 @@ async def _send_text_async_split(config: dict, """ # Send all parts of message response_part_counter_init = request_response.response_part_counter + images = [img for img in + (await asyncio.gather(*[parse_img(img) + for img in request_response.response_images])) + if img is not None] while True: # Get current part of response (text) if request_response.response: @@ -270,10 +294,10 @@ async def _send_text_async_split(config: dict, try: # Send message as image # Single photo - if end and len(request_response.response_images) == 1: + if end and len(images) == 1: request_response.message_id = await send_photo(config["telegram"]["api_key"], request_response.user["user_id"], - request_response.response_images[0], + images[0], caption=response_part, reply_to_message_id=reply_to_id, markdown=True, @@ -282,12 +306,9 @@ async def _send_text_async_split(config: dict, # Multiple photos (send media group + markup as separate messages) # Collect media group - if end and len(request_response.response_images) > 1: + if end and len(images) > 1: # Build media_ground ignoring SVG images - media_group = [] - for image_url in request_response.response_images: - if not image_url.lower().endswith(".svg"): - media_group.append(InputMediaPhoto(media=image_url)) + media_group = [InputMediaPhoto(media=image_url) for image_url in images] # Send it for image_url in (media_group[i:i + 9] for i in range(0, len(media_group), 9)): @@ -310,7 +331,7 @@ async def _send_text_async_split(config: dict, edit_message_id=edit_id) break except Exception as err: - logging.error("Error while sending images {} {}".format(request_response.response_images, str(err))) + logging.error("Error while sending images {} {}".format(images, str(err))) # Add cursor symbol? if not end and config["telegram"]["add_cursor_symbol"]: From d9d3ddfddf2cafd7a17ccbb722ab59fb9a1e772e Mon Sep 17 00:00:00 2001 From: Hanssen Date: Sun, 10 Dec 2023 03:52:20 +0800 Subject: [PATCH 09/10] feat: a warning log for svg pictures --- BotHandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BotHandler.py b/BotHandler.py index b5242593..778959a1 100644 --- a/BotHandler.py +++ b/BotHandler.py @@ -240,7 +240,7 @@ async def parse_img(img_source: str): "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/91.4472.114 Safari/537.36"}) if res.headers.get("content-type") == "image/svg+xml": - return None + raise Exception("SVG Image") except Exception as e: logging.warning("Invalid image from {}: {}, You can ignore this message".format(img_source, str(e))) return None From cb98fd607eddad7a07fc1c0b2761aea721989e38 Mon Sep 17 00:00:00 2001 From: F33RNI Date: Sat, 9 Dec 2023 23:26:19 +0300 Subject: [PATCH 10/10] Add handsome0hell to the Contributors list --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7916b268..871f7f4c 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,10 @@ Or message me if you would like to donate 💰 ## 🤗 Contributors - 1️⃣ [Sprav04ka](https://github.com/Sprav04ka) - *Tofii'skovyi' language, Testing, [Super beautiful DIY jack o'lantern (for poster)](Banner.png), [Project Logo](Logo.png), Motivation* -- 2️⃣ [Sergey Krashevich](https://github.com/skrashevich) - *Docker, GitHub Actions* -- 3️⃣ [Wahit Fitriyanto](https://github.com/wahitftry) - *Indonesian language* -- 4️⃣ [Alexander Fadeyev](https://github.com/alfsoft) - *EdgeGPT Fix* +- 2️⃣ [Hanssen](https://github.com/handsome0hell) - *Markdown parsing, bard images, Chinese language, /chat command* +- 3️⃣ [Sergey Krashevich](https://github.com/skrashevich) - *Docker, GitHub Actions* +- 4️⃣ [Wahit Fitriyanto](https://github.com/wahitftry) - *Indonesian language* +- 5️⃣ [Alexander Fadeyev](https://github.com/alfsoft) - *EdgeGPT Fix* ----------