mirror of
				https://github.com/cdryzun/tg_bot_collections.git
				synced 2025-11-04 08:46:44 +08:00 
			
		
		
		
	feat: add summary and search commands (#54)
* feat: add summary and search commands Signed-off-by: Frost Ming <me@frostming.com> * fix formats Signed-off-by: Frost Ming <me@frostming.com> * fix: clean up Signed-off-by: Frost Ming <me@frostming.com>
This commit is contained in:
		
							
								
								
									
										16
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								.env.example
									
									
									
									
									
								
							@ -1,8 +1,8 @@
 | 
				
			|||||||
Google_Gemini_API_Key="your_gemini_api_key"
 | 
					GOOGLE_GEMINI_API_KEY="your_gemini_api_key"
 | 
				
			||||||
Telegram_Bot_Token="your_telegram_bot_token"
 | 
					TELEGRAM_BOT_TOKEN="your_telegram_bot_token"
 | 
				
			||||||
Anthropic_API_Key="your_anthropic_api_key"
 | 
					ANTHROPIC_API_KEY="your_anthropic_api_key"
 | 
				
			||||||
Openai_API_Key="your_openai_api_key"
 | 
					OPENAI_API_KEY="your_openai_api_key"
 | 
				
			||||||
Yi_API_Key="your_yi_api_key"
 | 
					YI_API_KEY="your_yi_api_key"
 | 
				
			||||||
Yi_Base_Url="your_yi_base_url"
 | 
					YI_BASE_URL="your_yi_base_url"
 | 
				
			||||||
Python_Bin_Path=""
 | 
					PYTHON_BIN_PATH=""
 | 
				
			||||||
Python_Venv_Path="venv"
 | 
					PYTHON_VENV_PATH="venv"
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -170,3 +170,5 @@ nohup.out
 | 
				
			|||||||
.pdm-python
 | 
					.pdm-python
 | 
				
			||||||
*.wav
 | 
					*.wav
 | 
				
			||||||
token_key.json
 | 
					token_key.json
 | 
				
			||||||
 | 
					messages.db
 | 
				
			||||||
 | 
					*.session
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										35
									
								
								config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								config.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					from functools import cached_property
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import openai
 | 
				
			||||||
 | 
					from pydantic_settings import BaseSettings, SettingsConfigDict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Settings(BaseSettings):
 | 
				
			||||||
 | 
					    model_config = SettingsConfigDict(env_file=".env")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    telegram_bot_token: str
 | 
				
			||||||
 | 
					    timezone: str = "Asia/Shanghai"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    openai_api_key: str | None = None
 | 
				
			||||||
 | 
					    openai_model: str = "gpt-4o-mini"
 | 
				
			||||||
 | 
					    openai_base_url: str = "https://api.openai.com/v1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    google_gemini_api_key: str | None = None
 | 
				
			||||||
 | 
					    anthropic_api_key: str | None = None
 | 
				
			||||||
 | 
					    telegra_ph_token: str | None = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @cached_property
 | 
				
			||||||
 | 
					    def openai_client(self) -> openai.OpenAI:
 | 
				
			||||||
 | 
					        return openai.OpenAI(
 | 
				
			||||||
 | 
					            api_key=self.openai_api_key,
 | 
				
			||||||
 | 
					            base_url=self.openai_base_url,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @cached_property
 | 
				
			||||||
 | 
					    def telegraph_client(self):
 | 
				
			||||||
 | 
					        from handlers._telegraph import TelegraphAPI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return TelegraphAPI(self.telegra_ph_token)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					settings = Settings()  # type: ignore
 | 
				
			||||||
@ -1,173 +1,22 @@
 | 
				
			|||||||
from __future__ import annotations
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import base64
 | 
					 | 
				
			||||||
import importlib
 | 
					import importlib
 | 
				
			||||||
import re
 | 
					 | 
				
			||||||
import traceback
 | 
					 | 
				
			||||||
from functools import update_wrapper
 | 
					 | 
				
			||||||
from mimetypes import guess_type
 | 
					 | 
				
			||||||
from pathlib import Path
 | 
					from pathlib import Path
 | 
				
			||||||
from typing import Any, Callable, TypeVar
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import requests
 | 
					 | 
				
			||||||
from telebot import TeleBot
 | 
					from telebot import TeleBot
 | 
				
			||||||
from telebot.types import BotCommand, Message
 | 
					from telebot.types import BotCommand
 | 
				
			||||||
from telebot.util import smart_split
 | 
					 | 
				
			||||||
import telegramify_markdown
 | 
					 | 
				
			||||||
from telegramify_markdown.customize import markdown_symbol
 | 
					 | 
				
			||||||
from urlextract import URLExtract
 | 
					 | 
				
			||||||
from expiringdict import ExpiringDict
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
markdown_symbol.head_level_1 = "📌"  # If you want, Customizing the head level 1 symbol
 | 
					from ._utils import logger, wrap_handler
 | 
				
			||||||
markdown_symbol.link = "🔗"  # If you want, Customizing the link symbol
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
T = TypeVar("T", bound=Callable)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
DEFAULT_LOAD_PRIORITY = 10
 | 
					DEFAULT_LOAD_PRIORITY = 10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
BOT_MESSAGE_LENGTH = 4000
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
REPLY_MESSAGE_CACHE = ExpiringDict(max_len=1000, max_age_seconds=600)
 | 
					def list_available_commands() -> list[str]:
 | 
				
			||||||
 | 
					    commands = []
 | 
				
			||||||
 | 
					    this_path = Path(__file__).parent
 | 
				
			||||||
def bot_reply_first(message: Message, who: str, bot: TeleBot) -> Message:
 | 
					    for child in this_path.iterdir():
 | 
				
			||||||
    """Create the first reply message which make user feel the bot is working."""
 | 
					        if child.name.startswith("_"):
 | 
				
			||||||
    return bot.reply_to(
 | 
					            continue
 | 
				
			||||||
        message, f"*{who}* is _thinking_ \.\.\.", parse_mode="MarkdownV2"
 | 
					        commands.append(child.stem)
 | 
				
			||||||
    )
 | 
					    return commands
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def bot_reply_markdown(
 | 
					 | 
				
			||||||
    reply_id: Message,
 | 
					 | 
				
			||||||
    who: str,
 | 
					 | 
				
			||||||
    text: str,
 | 
					 | 
				
			||||||
    bot: TeleBot,
 | 
					 | 
				
			||||||
    split_text: bool = True,
 | 
					 | 
				
			||||||
    disable_web_page_preview: bool = False,
 | 
					 | 
				
			||||||
) -> bool:
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    reply the Markdown by take care of the message length.
 | 
					 | 
				
			||||||
    it will fallback to plain text in case of any failure
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    try:
 | 
					 | 
				
			||||||
        cache_key = f"{reply_id.chat.id}_{reply_id.message_id}"
 | 
					 | 
				
			||||||
        if cache_key in REPLY_MESSAGE_CACHE and REPLY_MESSAGE_CACHE[cache_key] == text:
 | 
					 | 
				
			||||||
            print(f"Skipping duplicate message for {cache_key}")
 | 
					 | 
				
			||||||
            return True
 | 
					 | 
				
			||||||
        REPLY_MESSAGE_CACHE[cache_key] = text
 | 
					 | 
				
			||||||
        if len(text.encode("utf-8")) <= BOT_MESSAGE_LENGTH or not split_text:
 | 
					 | 
				
			||||||
            bot.edit_message_text(
 | 
					 | 
				
			||||||
                f"*{who}*:\n{telegramify_markdown.convert(text)}",
 | 
					 | 
				
			||||||
                chat_id=reply_id.chat.id,
 | 
					 | 
				
			||||||
                message_id=reply_id.message_id,
 | 
					 | 
				
			||||||
                parse_mode="MarkdownV2",
 | 
					 | 
				
			||||||
                disable_web_page_preview=disable_web_page_preview,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            return True
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Need a split of message
 | 
					 | 
				
			||||||
        msgs = smart_split(text, BOT_MESSAGE_LENGTH)
 | 
					 | 
				
			||||||
        bot.edit_message_text(
 | 
					 | 
				
			||||||
            f"*{who}* \[1/{len(msgs)}\]:\n{telegramify_markdown.convert(msgs[0])}",
 | 
					 | 
				
			||||||
            chat_id=reply_id.chat.id,
 | 
					 | 
				
			||||||
            message_id=reply_id.message_id,
 | 
					 | 
				
			||||||
            parse_mode="MarkdownV2",
 | 
					 | 
				
			||||||
            disable_web_page_preview=disable_web_page_preview,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        for i in range(1, len(msgs)):
 | 
					 | 
				
			||||||
            bot.reply_to(
 | 
					 | 
				
			||||||
                reply_id.reply_to_message,
 | 
					 | 
				
			||||||
                f"*{who}* \[{i+1}/{len(msgs)}\]:\n{telegramify_markdown.convert(msgs[i])}",
 | 
					 | 
				
			||||||
                parse_mode="MarkdownV2",
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return True
 | 
					 | 
				
			||||||
    except Exception as e:
 | 
					 | 
				
			||||||
        print(traceback.format_exc())
 | 
					 | 
				
			||||||
        # print(f"wrong markdown format: {text}")
 | 
					 | 
				
			||||||
        bot.edit_message_text(
 | 
					 | 
				
			||||||
            f"*{who}*:\n{text}",
 | 
					 | 
				
			||||||
            chat_id=reply_id.chat.id,
 | 
					 | 
				
			||||||
            message_id=reply_id.message_id,
 | 
					 | 
				
			||||||
            disable_web_page_preview=disable_web_page_preview,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        return False
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def extract_prompt(message: str, bot_name: str) -> str:
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    This function filters messages for prompts.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Returns:
 | 
					 | 
				
			||||||
      str: If it is not a prompt, return None. Otherwise, return the trimmed prefix of the actual prompt.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    # remove '@bot_name' as it is considered part of the command when in a group chat.
 | 
					 | 
				
			||||||
    message = re.sub(re.escape(f"@{bot_name}"), "", message).strip()
 | 
					 | 
				
			||||||
    # add a whitespace after the first colon as we separate the prompt from the command by the first whitespace.
 | 
					 | 
				
			||||||
    message = re.sub(":", ": ", message, count=1).strip()
 | 
					 | 
				
			||||||
    try:
 | 
					 | 
				
			||||||
        left, message = message.split(maxsplit=1)
 | 
					 | 
				
			||||||
    except ValueError:
 | 
					 | 
				
			||||||
        return ""
 | 
					 | 
				
			||||||
    if ":" not in left:
 | 
					 | 
				
			||||||
        # the replacement happens in the right part, restore it.
 | 
					 | 
				
			||||||
        message = message.replace(": ", ":", 1)
 | 
					 | 
				
			||||||
    return message.strip()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def remove_prompt_prefix(message: str) -> str:
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    Remove "/cmd" or "/cmd@bot_name" or "cmd:"
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    message += " "
 | 
					 | 
				
			||||||
    # Explanation of the regex pattern:
 | 
					 | 
				
			||||||
    # ^                        - Match the start of the string
 | 
					 | 
				
			||||||
    # (                        - Start of the group
 | 
					 | 
				
			||||||
    #   /                      - Literal forward slash
 | 
					 | 
				
			||||||
    #   [a-zA-Z]               - Any letter (start of the command)
 | 
					 | 
				
			||||||
    #   [a-zA-Z0-9_]*          - Any number of letters, digits, or underscores
 | 
					 | 
				
			||||||
    #   (@\w+)?                - Optionally match @ followed by one or more word characters (for bot name)
 | 
					 | 
				
			||||||
    #   \s                     - A single whitespace character (space or newline)
 | 
					 | 
				
			||||||
    # |                        - OR
 | 
					 | 
				
			||||||
    #   [a-zA-Z]               - Any letter (start of the command)
 | 
					 | 
				
			||||||
    #   [a-zA-Z0-9_]*          - Any number of letters, digits, or underscores
 | 
					 | 
				
			||||||
    #   :\s                    - Colon followed by a single whitespace character
 | 
					 | 
				
			||||||
    # )                        - End of the group
 | 
					 | 
				
			||||||
    pattern = r"^(/[a-zA-Z][a-zA-Z0-9_]*(@\w+)?\s|[a-zA-Z][a-zA-Z0-9_]*:\s)"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return re.sub(pattern, "", message).strip()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def wrap_handler(handler: T, bot: TeleBot) -> T:
 | 
					 | 
				
			||||||
    def wrapper(message: Message, *args: Any, **kwargs: Any) -> None:
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            m = ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if message.text and message.text.find("answer_it") != -1:
 | 
					 | 
				
			||||||
                # for answer_it no args
 | 
					 | 
				
			||||||
                return handler(message, *args, **kwargs)
 | 
					 | 
				
			||||||
            elif message.text is not None:
 | 
					 | 
				
			||||||
                m = message.text = extract_prompt(message.text, bot.get_me().username)
 | 
					 | 
				
			||||||
            elif message.caption is not None:
 | 
					 | 
				
			||||||
                m = message.caption = extract_prompt(
 | 
					 | 
				
			||||||
                    message.caption, bot.get_me().username
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            elif message.location and message.location.latitude is not None:
 | 
					 | 
				
			||||||
                # for location map handler just return
 | 
					 | 
				
			||||||
                return handler(message, *args, **kwargs)
 | 
					 | 
				
			||||||
            if not m:
 | 
					 | 
				
			||||||
                bot.reply_to(message, "Please provide info after start words.")
 | 
					 | 
				
			||||||
                return
 | 
					 | 
				
			||||||
            return handler(message, *args, **kwargs)
 | 
					 | 
				
			||||||
        except Exception as e:
 | 
					 | 
				
			||||||
            traceback.print_exc()
 | 
					 | 
				
			||||||
            # handle more here
 | 
					 | 
				
			||||||
            if str(e).find("RECITATION") > 0:
 | 
					 | 
				
			||||||
                bot.reply_to(message, "Your prompt `RECITATION` please check the log")
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                bot.reply_to(message, "Something wrong, please check the log")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return update_wrapper(wrapper, handler)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def load_handlers(bot: TeleBot, disable_commands: list[str]) -> None:
 | 
					def load_handlers(bot: TeleBot, disable_commands: list[str]) -> None:
 | 
				
			||||||
@ -183,16 +32,13 @@ def load_handlers(bot: TeleBot, disable_commands: list[str]) -> None:
 | 
				
			|||||||
    modules_with_priority.sort(key=lambda x: x[-1])
 | 
					    modules_with_priority.sort(key=lambda x: x[-1])
 | 
				
			||||||
    for module, name, priority in modules_with_priority:
 | 
					    for module, name, priority in modules_with_priority:
 | 
				
			||||||
        if hasattr(module, "register"):
 | 
					        if hasattr(module, "register"):
 | 
				
			||||||
            print(f"Loading {name} handlers with priority {priority}.")
 | 
					            logger.debug(f"Loading {name} handlers with priority {priority}.")
 | 
				
			||||||
            module.register(bot)
 | 
					            module.register(bot)
 | 
				
			||||||
    print("Loading handlers done.")
 | 
					    logger.info("Loading handlers done.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    all_commands: list[BotCommand] = []
 | 
					    all_commands: list[BotCommand] = []
 | 
				
			||||||
    for handler in bot.message_handlers:
 | 
					    for handler in bot.message_handlers:
 | 
				
			||||||
        help_text = getattr(handler["function"], "__doc__", "")
 | 
					        help_text = getattr(handler["function"], "__doc__", "")
 | 
				
			||||||
        # tricky ignore the latest_handle_messages
 | 
					 | 
				
			||||||
        if help_text and help_text == "ignore":
 | 
					 | 
				
			||||||
            continue
 | 
					 | 
				
			||||||
        # Add pre-processing and error handling to all callbacks
 | 
					        # Add pre-processing and error handling to all callbacks
 | 
				
			||||||
        handler["function"] = wrap_handler(handler["function"], bot)
 | 
					        handler["function"] = wrap_handler(handler["function"], bot)
 | 
				
			||||||
        for command in handler["filters"].get("commands", []):
 | 
					        for command in handler["filters"].get("commands", []):
 | 
				
			||||||
@ -200,309 +46,4 @@ def load_handlers(bot: TeleBot, disable_commands: list[str]) -> None:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    if all_commands:
 | 
					    if all_commands:
 | 
				
			||||||
        bot.set_my_commands(all_commands)
 | 
					        bot.set_my_commands(all_commands)
 | 
				
			||||||
        print("Setting commands done.")
 | 
					        logger.info("Setting commands done.")
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def list_available_commands() -> list[str]:
 | 
					 | 
				
			||||||
    commands = []
 | 
					 | 
				
			||||||
    this_path = Path(__file__).parent
 | 
					 | 
				
			||||||
    for child in this_path.iterdir():
 | 
					 | 
				
			||||||
        if child.name.startswith("_"):
 | 
					 | 
				
			||||||
            continue
 | 
					 | 
				
			||||||
        commands.append(child.stem)
 | 
					 | 
				
			||||||
    return commands
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def extract_url_from_text(text: str) -> list[str]:
 | 
					 | 
				
			||||||
    extractor = URLExtract()
 | 
					 | 
				
			||||||
    urls = extractor.find_urls(text)
 | 
					 | 
				
			||||||
    return urls
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def get_text_from_jina_reader(url: str):
 | 
					 | 
				
			||||||
    try:
 | 
					 | 
				
			||||||
        r = requests.get(f"https://r.jina.ai/{url}")
 | 
					 | 
				
			||||||
        return r.text
 | 
					 | 
				
			||||||
    except Exception as e:
 | 
					 | 
				
			||||||
        print(e)
 | 
					 | 
				
			||||||
        return None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def enrich_text_with_urls(text: str) -> str:
 | 
					 | 
				
			||||||
    urls = extract_url_from_text(text)
 | 
					 | 
				
			||||||
    for u in urls:
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            url_text = get_text_from_jina_reader(u)
 | 
					 | 
				
			||||||
            url_text = f"\n```markdown\n{url_text}\n```\n"
 | 
					 | 
				
			||||||
            text = text.replace(u, url_text)
 | 
					 | 
				
			||||||
        except Exception as e:
 | 
					 | 
				
			||||||
            # just ignore the error
 | 
					 | 
				
			||||||
            pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return text
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def image_to_data_uri(file_path):
 | 
					 | 
				
			||||||
    content_type = guess_type(file_path)[0]
 | 
					 | 
				
			||||||
    with open(file_path, "rb") as image_file:
 | 
					 | 
				
			||||||
        encoded_image = base64.b64encode(image_file.read()).decode("utf-8")
 | 
					 | 
				
			||||||
        return f"data:{content_type};base64,{encoded_image}"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import json
 | 
					 | 
				
			||||||
import requests
 | 
					 | 
				
			||||||
import os
 | 
					 | 
				
			||||||
from bs4 import BeautifulSoup
 | 
					 | 
				
			||||||
import markdown
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class TelegraphAPI:
 | 
					 | 
				
			||||||
    def __init__(
 | 
					 | 
				
			||||||
        self,
 | 
					 | 
				
			||||||
        access_token=None,
 | 
					 | 
				
			||||||
        short_name="tg_bot_collections",
 | 
					 | 
				
			||||||
        author_name="Telegram Bot Collections",
 | 
					 | 
				
			||||||
        author_url=None,
 | 
					 | 
				
			||||||
    ):
 | 
					 | 
				
			||||||
        self.access_token = (
 | 
					 | 
				
			||||||
            access_token
 | 
					 | 
				
			||||||
            if access_token
 | 
					 | 
				
			||||||
            else self._create_ph_account(short_name, author_name, author_url)
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        self.base_url = "https://api.telegra.ph"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Get account info on initialization
 | 
					 | 
				
			||||||
        account_info = self.get_account_info()
 | 
					 | 
				
			||||||
        self.short_name = account_info.get("short_name")
 | 
					 | 
				
			||||||
        self.author_name = account_info.get("author_name")
 | 
					 | 
				
			||||||
        self.author_url = account_info.get("author_url")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def _create_ph_account(self, short_name, author_name, author_url):
 | 
					 | 
				
			||||||
        Store_Token = False
 | 
					 | 
				
			||||||
        TELEGRAPH_API_URL = "https://api.telegra.ph/createAccount"
 | 
					 | 
				
			||||||
        TOKEN_FILE = "token_key.json"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Try to load existing token information
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            with open(TOKEN_FILE, "r") as f:
 | 
					 | 
				
			||||||
                tokens = json.load(f)
 | 
					 | 
				
			||||||
            if "TELEGRA_PH_TOKEN" in tokens and tokens["TELEGRA_PH_TOKEN"] != "example":
 | 
					 | 
				
			||||||
                return tokens["TELEGRA_PH_TOKEN"]
 | 
					 | 
				
			||||||
        except FileNotFoundError:
 | 
					 | 
				
			||||||
            tokens = {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # If no existing valid token in TOKEN_FILE, create a new account
 | 
					 | 
				
			||||||
        data = {
 | 
					 | 
				
			||||||
            "short_name": short_name,
 | 
					 | 
				
			||||||
            "author_name": author_name,
 | 
					 | 
				
			||||||
            "author_url": author_url,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Make API request
 | 
					 | 
				
			||||||
        response = requests.post(TELEGRAPH_API_URL, data=data)
 | 
					 | 
				
			||||||
        response.raise_for_status()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        account = response.json()
 | 
					 | 
				
			||||||
        access_token = account["result"]["access_token"]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Update the token in the dictionary
 | 
					 | 
				
			||||||
        tokens["TELEGRA_PH_TOKEN"] = access_token
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Store the updated tokens
 | 
					 | 
				
			||||||
        if Store_Token:
 | 
					 | 
				
			||||||
            with open(TOKEN_FILE, "w") as f:
 | 
					 | 
				
			||||||
                json.dump(tokens, f, indent=4)
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            print(f"Token not stored to file, but here is your token:\n{access_token}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Store it to the environment variable
 | 
					 | 
				
			||||||
        os.environ["TELEGRA_PH_TOKEN"] = access_token
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return access_token
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def create_page(
 | 
					 | 
				
			||||||
        self, title, content, author_name=None, author_url=None, return_content=False
 | 
					 | 
				
			||||||
    ):
 | 
					 | 
				
			||||||
        url = f"{self.base_url}/createPage"
 | 
					 | 
				
			||||||
        data = {
 | 
					 | 
				
			||||||
            "access_token": self.access_token,
 | 
					 | 
				
			||||||
            "title": title,
 | 
					 | 
				
			||||||
            "content": json.dumps(content),
 | 
					 | 
				
			||||||
            "return_content": return_content,
 | 
					 | 
				
			||||||
            "author_name": author_name if author_name else self.author_name,
 | 
					 | 
				
			||||||
            "author_url": author_url if author_url else self.author_url,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Max 65,536 characters/64KB.
 | 
					 | 
				
			||||||
        if len(json.dumps(content)) > 65536:
 | 
					 | 
				
			||||||
            content = content[:64000]
 | 
					 | 
				
			||||||
            data["content"] = json.dumps(content)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            response = requests.post(url, data=data)
 | 
					 | 
				
			||||||
            response.raise_for_status()
 | 
					 | 
				
			||||||
            response = response.json()
 | 
					 | 
				
			||||||
            page_url = response["result"]["url"]
 | 
					 | 
				
			||||||
            return page_url
 | 
					 | 
				
			||||||
        except:
 | 
					 | 
				
			||||||
            return "https://telegra.ph/api"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_account_info(self):
 | 
					 | 
				
			||||||
        url = f'{self.base_url}/getAccountInfo?access_token={self.access_token}&fields=["short_name","author_name","author_url","auth_url"]'
 | 
					 | 
				
			||||||
        response = requests.get(url)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if response.status_code == 200:
 | 
					 | 
				
			||||||
            return response.json()["result"]
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            print(f"Fail getting telegra.ph token info: {response.status_code}")
 | 
					 | 
				
			||||||
            return None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def edit_page(
 | 
					 | 
				
			||||||
        self,
 | 
					 | 
				
			||||||
        path,
 | 
					 | 
				
			||||||
        title,
 | 
					 | 
				
			||||||
        content,
 | 
					 | 
				
			||||||
        author_name=None,
 | 
					 | 
				
			||||||
        author_url=None,
 | 
					 | 
				
			||||||
        return_content=False,
 | 
					 | 
				
			||||||
    ):
 | 
					 | 
				
			||||||
        url = f"{self.base_url}/editPage"
 | 
					 | 
				
			||||||
        data = {
 | 
					 | 
				
			||||||
            "access_token": self.access_token,
 | 
					 | 
				
			||||||
            "path": path,
 | 
					 | 
				
			||||||
            "title": title,
 | 
					 | 
				
			||||||
            "content": json.dumps(content),
 | 
					 | 
				
			||||||
            "return_content": return_content,
 | 
					 | 
				
			||||||
            "author_name": author_name if author_name else self.author_name,
 | 
					 | 
				
			||||||
            "author_url": author_url if author_url else self.author_url,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        response = requests.post(url, data=data)
 | 
					 | 
				
			||||||
        response.raise_for_status()
 | 
					 | 
				
			||||||
        response = response.json()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        page_url = response["result"]["url"]
 | 
					 | 
				
			||||||
        return page_url
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_page(self, path):
 | 
					 | 
				
			||||||
        url = f"{self.base_url}/getPage/{path}?return_content=true"
 | 
					 | 
				
			||||||
        response = requests.get(url)
 | 
					 | 
				
			||||||
        response.raise_for_status()
 | 
					 | 
				
			||||||
        return response.json()["result"]["content"]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def create_page_md(
 | 
					 | 
				
			||||||
        self,
 | 
					 | 
				
			||||||
        title,
 | 
					 | 
				
			||||||
        markdown_text,
 | 
					 | 
				
			||||||
        author_name=None,
 | 
					 | 
				
			||||||
        author_url=None,
 | 
					 | 
				
			||||||
        return_content=False,
 | 
					 | 
				
			||||||
    ):
 | 
					 | 
				
			||||||
        content = self._md_to_dom(markdown_text)
 | 
					 | 
				
			||||||
        return self.create_page(title, content, author_name, author_url, return_content)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def edit_page_md(
 | 
					 | 
				
			||||||
        self,
 | 
					 | 
				
			||||||
        path,
 | 
					 | 
				
			||||||
        title,
 | 
					 | 
				
			||||||
        markdown_text,
 | 
					 | 
				
			||||||
        author_name=None,
 | 
					 | 
				
			||||||
        author_url=None,
 | 
					 | 
				
			||||||
        return_content=False,
 | 
					 | 
				
			||||||
    ):
 | 
					 | 
				
			||||||
        content = self._md_to_dom(markdown_text)
 | 
					 | 
				
			||||||
        return self.edit_page(
 | 
					 | 
				
			||||||
            path, title, content, author_name, author_url, return_content
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def authorize_browser(self):
 | 
					 | 
				
			||||||
        url = f'{self.base_url}/getAccountInfo?access_token={self.access_token}&fields=["auth_url"]'
 | 
					 | 
				
			||||||
        response = requests.get(url)
 | 
					 | 
				
			||||||
        response.raise_for_status()
 | 
					 | 
				
			||||||
        return response.json()["result"]["auth_url"]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def _md_to_dom(self, markdown_text):
 | 
					 | 
				
			||||||
        html = markdown.markdown(
 | 
					 | 
				
			||||||
            markdown_text,
 | 
					 | 
				
			||||||
            extensions=["markdown.extensions.extra", "markdown.extensions.sane_lists"],
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        soup = BeautifulSoup(html, "html.parser")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        def parse_element(element):
 | 
					 | 
				
			||||||
            tag_dict = {"tag": element.name}
 | 
					 | 
				
			||||||
            if element.name in ["h1", "h2", "h3", "h4", "h5", "h6"]:
 | 
					 | 
				
			||||||
                if element.name == "h1":
 | 
					 | 
				
			||||||
                    tag_dict["tag"] = "h3"
 | 
					 | 
				
			||||||
                elif element.name == "h2":
 | 
					 | 
				
			||||||
                    tag_dict["tag"] = "h4"
 | 
					 | 
				
			||||||
                else:
 | 
					 | 
				
			||||||
                    tag_dict["tag"] = "p"
 | 
					 | 
				
			||||||
                    tag_dict["children"] = [
 | 
					 | 
				
			||||||
                        {"tag": "strong", "children": element.contents}
 | 
					 | 
				
			||||||
                    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if element.attrs:
 | 
					 | 
				
			||||||
                    tag_dict["attrs"] = element.attrs
 | 
					 | 
				
			||||||
                if element.contents:
 | 
					 | 
				
			||||||
                    children = []
 | 
					 | 
				
			||||||
                    for child in element.contents:
 | 
					 | 
				
			||||||
                        if isinstance(child, str):
 | 
					 | 
				
			||||||
                            children.append(child.strip())
 | 
					 | 
				
			||||||
                        else:
 | 
					 | 
				
			||||||
                            children.append(parse_element(child))
 | 
					 | 
				
			||||||
                    tag_dict["children"] = children
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                if element.attrs:
 | 
					 | 
				
			||||||
                    tag_dict["attrs"] = element.attrs
 | 
					 | 
				
			||||||
                if element.contents:
 | 
					 | 
				
			||||||
                    children = []
 | 
					 | 
				
			||||||
                    for child in element.contents:
 | 
					 | 
				
			||||||
                        if isinstance(child, str):
 | 
					 | 
				
			||||||
                            children.append(child.strip())
 | 
					 | 
				
			||||||
                        else:
 | 
					 | 
				
			||||||
                            children.append(parse_element(child))
 | 
					 | 
				
			||||||
                    if children:
 | 
					 | 
				
			||||||
                        tag_dict["children"] = children
 | 
					 | 
				
			||||||
            return tag_dict
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        new_dom = []
 | 
					 | 
				
			||||||
        for element in soup.contents:
 | 
					 | 
				
			||||||
            if isinstance(element, str) and not element.strip():
 | 
					 | 
				
			||||||
                continue
 | 
					 | 
				
			||||||
            elif isinstance(element, str):
 | 
					 | 
				
			||||||
                new_dom.append({"tag": "text", "content": element.strip()})
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                new_dom.append(parse_element(element))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return new_dom
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def upload_image(self, file_name: str) -> str:
 | 
					 | 
				
			||||||
        base_url = "https://telegra.ph"
 | 
					 | 
				
			||||||
        upload_url = f"{base_url}/upload"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            content_type = guess_type(file_name)[0]
 | 
					 | 
				
			||||||
            with open(file_name, "rb") as f:
 | 
					 | 
				
			||||||
                response = requests.post(
 | 
					 | 
				
			||||||
                    upload_url, files={"file": ("blob", f, content_type)}
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                response.raise_for_status()
 | 
					 | 
				
			||||||
                # [{'src': '/file/xx.jpg'}]
 | 
					 | 
				
			||||||
                response = response.json()
 | 
					 | 
				
			||||||
                image_url = f"{base_url}{response[0]['src']}"
 | 
					 | 
				
			||||||
                return image_url
 | 
					 | 
				
			||||||
        except Exception as e:
 | 
					 | 
				
			||||||
            print(f"upload image: {e}")
 | 
					 | 
				
			||||||
            return "https://telegra.ph/api"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# `import *` will give you these
 | 
					 | 
				
			||||||
__all__ = [
 | 
					 | 
				
			||||||
    "bot_reply_first",
 | 
					 | 
				
			||||||
    "bot_reply_markdown",
 | 
					 | 
				
			||||||
    "remove_prompt_prefix",
 | 
					 | 
				
			||||||
    "enrich_text_with_urls",
 | 
					 | 
				
			||||||
    "image_to_data_uri",
 | 
					 | 
				
			||||||
    "TelegraphAPI",
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										252
									
								
								handlers/_telegraph.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								handlers/_telegraph.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,252 @@
 | 
				
			|||||||
 | 
					import json
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					from mimetypes import guess_type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import markdown
 | 
				
			||||||
 | 
					import requests
 | 
				
			||||||
 | 
					from bs4 import BeautifulSoup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from ._utils import logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TelegraphAPI:
 | 
				
			||||||
 | 
					    def __init__(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        access_token=None,
 | 
				
			||||||
 | 
					        short_name="tg_bot_collections",
 | 
				
			||||||
 | 
					        author_name="Telegram Bot Collections",
 | 
				
			||||||
 | 
					        author_url=None,
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        self.access_token = (
 | 
				
			||||||
 | 
					            access_token
 | 
				
			||||||
 | 
					            if access_token
 | 
				
			||||||
 | 
					            else self._create_ph_account(short_name, author_name, author_url)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.base_url = "https://api.telegra.ph"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Get account info on initialization
 | 
				
			||||||
 | 
					        account_info = self.get_account_info()
 | 
				
			||||||
 | 
					        self.short_name = account_info.get("short_name")
 | 
				
			||||||
 | 
					        self.author_name = account_info.get("author_name")
 | 
				
			||||||
 | 
					        self.author_url = account_info.get("author_url")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _create_ph_account(self, short_name, author_name, author_url):
 | 
				
			||||||
 | 
					        Store_Token = False
 | 
				
			||||||
 | 
					        TELEGRAPH_API_URL = "https://api.telegra.ph/createAccount"
 | 
				
			||||||
 | 
					        TOKEN_FILE = "token_key.json"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Try to load existing token information
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            with open(TOKEN_FILE, "r") as f:
 | 
				
			||||||
 | 
					                tokens = json.load(f)
 | 
				
			||||||
 | 
					            if "TELEGRA_PH_TOKEN" in tokens and tokens["TELEGRA_PH_TOKEN"] != "example":
 | 
				
			||||||
 | 
					                return tokens["TELEGRA_PH_TOKEN"]
 | 
				
			||||||
 | 
					        except FileNotFoundError:
 | 
				
			||||||
 | 
					            tokens = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # If no existing valid token in TOKEN_FILE, create a new account
 | 
				
			||||||
 | 
					        data = {
 | 
				
			||||||
 | 
					            "short_name": short_name,
 | 
				
			||||||
 | 
					            "author_name": author_name,
 | 
				
			||||||
 | 
					            "author_url": author_url,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Make API request
 | 
				
			||||||
 | 
					        response = requests.post(TELEGRAPH_API_URL, data=data)
 | 
				
			||||||
 | 
					        response.raise_for_status()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        account = response.json()
 | 
				
			||||||
 | 
					        access_token = account["result"]["access_token"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Update the token in the dictionary
 | 
				
			||||||
 | 
					        tokens["TELEGRA_PH_TOKEN"] = access_token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Store the updated tokens
 | 
				
			||||||
 | 
					        if Store_Token:
 | 
				
			||||||
 | 
					            with open(TOKEN_FILE, "w") as f:
 | 
				
			||||||
 | 
					                json.dump(tokens, f, indent=4)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            logger.info(
 | 
				
			||||||
 | 
					                f"Token not stored to file, but here is your token:\n{access_token}"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Store it to the environment variable
 | 
				
			||||||
 | 
					        os.environ["TELEGRA_PH_TOKEN"] = access_token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return access_token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def create_page(
 | 
				
			||||||
 | 
					        self, title, content, author_name=None, author_url=None, return_content=False
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        url = f"{self.base_url}/createPage"
 | 
				
			||||||
 | 
					        data = {
 | 
				
			||||||
 | 
					            "access_token": self.access_token,
 | 
				
			||||||
 | 
					            "title": title,
 | 
				
			||||||
 | 
					            "content": json.dumps(content),
 | 
				
			||||||
 | 
					            "return_content": return_content,
 | 
				
			||||||
 | 
					            "author_name": author_name if author_name else self.author_name,
 | 
				
			||||||
 | 
					            "author_url": author_url if author_url else self.author_url,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Max 65,536 characters/64KB.
 | 
				
			||||||
 | 
					        if len(json.dumps(content)) > 65536:
 | 
				
			||||||
 | 
					            content = content[:64000]
 | 
				
			||||||
 | 
					            data["content"] = json.dumps(content)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            response = requests.post(url, data=data)
 | 
				
			||||||
 | 
					            response.raise_for_status()
 | 
				
			||||||
 | 
					            response = response.json()
 | 
				
			||||||
 | 
					            page_url = response["result"]["url"]
 | 
				
			||||||
 | 
					            return page_url
 | 
				
			||||||
 | 
					        except requests.exceptions.RequestException:
 | 
				
			||||||
 | 
					            return "https://telegra.ph/api"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_account_info(self):
 | 
				
			||||||
 | 
					        url = f'{self.base_url}/getAccountInfo?access_token={self.access_token}&fields=["short_name","author_name","author_url","auth_url"]'
 | 
				
			||||||
 | 
					        response = requests.get(url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if response.status_code == 200:
 | 
				
			||||||
 | 
					            return response.json()["result"]
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            logger.info(f"Fail getting telegra.ph token info: {response.status_code}")
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def edit_page(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        path,
 | 
				
			||||||
 | 
					        title,
 | 
				
			||||||
 | 
					        content,
 | 
				
			||||||
 | 
					        author_name=None,
 | 
				
			||||||
 | 
					        author_url=None,
 | 
				
			||||||
 | 
					        return_content=False,
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        url = f"{self.base_url}/editPage"
 | 
				
			||||||
 | 
					        data = {
 | 
				
			||||||
 | 
					            "access_token": self.access_token,
 | 
				
			||||||
 | 
					            "path": path,
 | 
				
			||||||
 | 
					            "title": title,
 | 
				
			||||||
 | 
					            "content": json.dumps(content),
 | 
				
			||||||
 | 
					            "return_content": return_content,
 | 
				
			||||||
 | 
					            "author_name": author_name if author_name else self.author_name,
 | 
				
			||||||
 | 
					            "author_url": author_url if author_url else self.author_url,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        response = requests.post(url, data=data)
 | 
				
			||||||
 | 
					        response.raise_for_status()
 | 
				
			||||||
 | 
					        response = response.json()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        page_url = response["result"]["url"]
 | 
				
			||||||
 | 
					        return page_url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_page(self, path):
 | 
				
			||||||
 | 
					        url = f"{self.base_url}/getPage/{path}?return_content=true"
 | 
				
			||||||
 | 
					        response = requests.get(url)
 | 
				
			||||||
 | 
					        response.raise_for_status()
 | 
				
			||||||
 | 
					        return response.json()["result"]["content"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def create_page_md(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        title,
 | 
				
			||||||
 | 
					        markdown_text,
 | 
				
			||||||
 | 
					        author_name=None,
 | 
				
			||||||
 | 
					        author_url=None,
 | 
				
			||||||
 | 
					        return_content=False,
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        content = self._md_to_dom(markdown_text)
 | 
				
			||||||
 | 
					        return self.create_page(title, content, author_name, author_url, return_content)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def edit_page_md(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        path,
 | 
				
			||||||
 | 
					        title,
 | 
				
			||||||
 | 
					        markdown_text,
 | 
				
			||||||
 | 
					        author_name=None,
 | 
				
			||||||
 | 
					        author_url=None,
 | 
				
			||||||
 | 
					        return_content=False,
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        content = self._md_to_dom(markdown_text)
 | 
				
			||||||
 | 
					        return self.edit_page(
 | 
				
			||||||
 | 
					            path, title, content, author_name, author_url, return_content
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def authorize_browser(self):
 | 
				
			||||||
 | 
					        url = f'{self.base_url}/getAccountInfo?access_token={self.access_token}&fields=["auth_url"]'
 | 
				
			||||||
 | 
					        response = requests.get(url)
 | 
				
			||||||
 | 
					        response.raise_for_status()
 | 
				
			||||||
 | 
					        return response.json()["result"]["auth_url"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _md_to_dom(self, markdown_text):
 | 
				
			||||||
 | 
					        html = markdown.markdown(
 | 
				
			||||||
 | 
					            markdown_text,
 | 
				
			||||||
 | 
					            extensions=["markdown.extensions.extra", "markdown.extensions.sane_lists"],
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        soup = BeautifulSoup(html, "html.parser")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def parse_element(element):
 | 
				
			||||||
 | 
					            tag_dict = {"tag": element.name}
 | 
				
			||||||
 | 
					            if element.name in ["h1", "h2", "h3", "h4", "h5", "h6"]:
 | 
				
			||||||
 | 
					                if element.name == "h1":
 | 
				
			||||||
 | 
					                    tag_dict["tag"] = "h3"
 | 
				
			||||||
 | 
					                elif element.name == "h2":
 | 
				
			||||||
 | 
					                    tag_dict["tag"] = "h4"
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    tag_dict["tag"] = "p"
 | 
				
			||||||
 | 
					                    tag_dict["children"] = [
 | 
				
			||||||
 | 
					                        {"tag": "strong", "children": element.contents}
 | 
				
			||||||
 | 
					                    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if element.attrs:
 | 
				
			||||||
 | 
					                    tag_dict["attrs"] = element.attrs
 | 
				
			||||||
 | 
					                if element.contents:
 | 
				
			||||||
 | 
					                    children = []
 | 
				
			||||||
 | 
					                    for child in element.contents:
 | 
				
			||||||
 | 
					                        if isinstance(child, str):
 | 
				
			||||||
 | 
					                            children.append(child.strip())
 | 
				
			||||||
 | 
					                        else:
 | 
				
			||||||
 | 
					                            children.append(parse_element(child))
 | 
				
			||||||
 | 
					                    tag_dict["children"] = children
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                if element.attrs:
 | 
				
			||||||
 | 
					                    tag_dict["attrs"] = element.attrs
 | 
				
			||||||
 | 
					                if element.contents:
 | 
				
			||||||
 | 
					                    children = []
 | 
				
			||||||
 | 
					                    for child in element.contents:
 | 
				
			||||||
 | 
					                        if isinstance(child, str):
 | 
				
			||||||
 | 
					                            children.append(child.strip())
 | 
				
			||||||
 | 
					                        else:
 | 
				
			||||||
 | 
					                            children.append(parse_element(child))
 | 
				
			||||||
 | 
					                    if children:
 | 
				
			||||||
 | 
					                        tag_dict["children"] = children
 | 
				
			||||||
 | 
					            return tag_dict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        new_dom = []
 | 
				
			||||||
 | 
					        for element in soup.contents:
 | 
				
			||||||
 | 
					            if isinstance(element, str) and not element.strip():
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					            elif isinstance(element, str):
 | 
				
			||||||
 | 
					                new_dom.append({"tag": "text", "content": element.strip()})
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                new_dom.append(parse_element(element))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return new_dom
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def upload_image(self, file_name: str) -> str:
 | 
				
			||||||
 | 
					        base_url = "https://telegra.ph"
 | 
				
			||||||
 | 
					        upload_url = f"{base_url}/upload"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            content_type = guess_type(file_name)[0]
 | 
				
			||||||
 | 
					            with open(file_name, "rb") as f:
 | 
				
			||||||
 | 
					                response = requests.post(
 | 
				
			||||||
 | 
					                    upload_url, files={"file": ("blob", f, content_type)}
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                response.raise_for_status()
 | 
				
			||||||
 | 
					                # [{'src': '/file/xx.jpg'}]
 | 
				
			||||||
 | 
					                response = response.json()
 | 
				
			||||||
 | 
					                image_url = f"{base_url}{response[0]['src']}"
 | 
				
			||||||
 | 
					                return image_url
 | 
				
			||||||
 | 
					        except Exception as e:
 | 
				
			||||||
 | 
					            logger.info(f"upload image: {e}")
 | 
				
			||||||
 | 
					            return "https://telegra.ph/api"
 | 
				
			||||||
							
								
								
									
										209
									
								
								handlers/_utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								handlers/_utils.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,209 @@
 | 
				
			|||||||
 | 
					from __future__ import annotations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import base64
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					from functools import update_wrapper
 | 
				
			||||||
 | 
					from mimetypes import guess_type
 | 
				
			||||||
 | 
					from typing import Any, Callable, TypeVar
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import requests
 | 
				
			||||||
 | 
					import telegramify_markdown
 | 
				
			||||||
 | 
					from expiringdict import ExpiringDict
 | 
				
			||||||
 | 
					from telebot import TeleBot
 | 
				
			||||||
 | 
					from telebot.types import Message
 | 
				
			||||||
 | 
					from telebot.util import smart_split
 | 
				
			||||||
 | 
					from telegramify_markdown.customize import markdown_symbol
 | 
				
			||||||
 | 
					from urlextract import URLExtract
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					markdown_symbol.head_level_1 = "📌"  # If you want, Customizing the head level 1 symbol
 | 
				
			||||||
 | 
					markdown_symbol.link = "🔗"  # If you want, Customizing the link symbol
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					T = TypeVar("T", bound=Callable)
 | 
				
			||||||
 | 
					logger = logging.getLogger("bot")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					BOT_MESSAGE_LENGTH = 4000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					REPLY_MESSAGE_CACHE = ExpiringDict(max_len=1000, max_age_seconds=600)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def bot_reply_first(message: Message, who: str, bot: TeleBot) -> Message:
 | 
				
			||||||
 | 
					    """Create the first reply message which make user feel the bot is working."""
 | 
				
			||||||
 | 
					    return bot.reply_to(
 | 
				
			||||||
 | 
					        message, f"*{who}* is _thinking_ \.\.\.", parse_mode="MarkdownV2"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def bot_reply_markdown(
 | 
				
			||||||
 | 
					    reply_id: Message,
 | 
				
			||||||
 | 
					    who: str,
 | 
				
			||||||
 | 
					    text: str,
 | 
				
			||||||
 | 
					    bot: TeleBot,
 | 
				
			||||||
 | 
					    split_text: bool = True,
 | 
				
			||||||
 | 
					    disable_web_page_preview: bool = False,
 | 
				
			||||||
 | 
					) -> bool:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    reply the Markdown by take care of the message length.
 | 
				
			||||||
 | 
					    it will fallback to plain text in case of any failure
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        cache_key = f"{reply_id.chat.id}_{reply_id.message_id}"
 | 
				
			||||||
 | 
					        if cache_key in REPLY_MESSAGE_CACHE and REPLY_MESSAGE_CACHE[cache_key] == text:
 | 
				
			||||||
 | 
					            logger.info(f"Skipping duplicate message for {cache_key}")
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					        REPLY_MESSAGE_CACHE[cache_key] = text
 | 
				
			||||||
 | 
					        if len(text.encode("utf-8")) <= BOT_MESSAGE_LENGTH or not split_text:
 | 
				
			||||||
 | 
					            bot.edit_message_text(
 | 
				
			||||||
 | 
					                f"*{who}*:\n{telegramify_markdown.convert(text)}",
 | 
				
			||||||
 | 
					                chat_id=reply_id.chat.id,
 | 
				
			||||||
 | 
					                message_id=reply_id.message_id,
 | 
				
			||||||
 | 
					                parse_mode="MarkdownV2",
 | 
				
			||||||
 | 
					                disable_web_page_preview=disable_web_page_preview,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Need a split of message
 | 
				
			||||||
 | 
					        msgs = smart_split(text, BOT_MESSAGE_LENGTH)
 | 
				
			||||||
 | 
					        bot.edit_message_text(
 | 
				
			||||||
 | 
					            f"*{who}* \[1/{len(msgs)}\]:\n{telegramify_markdown.convert(msgs[0])}",
 | 
				
			||||||
 | 
					            chat_id=reply_id.chat.id,
 | 
				
			||||||
 | 
					            message_id=reply_id.message_id,
 | 
				
			||||||
 | 
					            parse_mode="MarkdownV2",
 | 
				
			||||||
 | 
					            disable_web_page_preview=disable_web_page_preview,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        for i in range(1, len(msgs)):
 | 
				
			||||||
 | 
					            bot.reply_to(
 | 
				
			||||||
 | 
					                reply_id.reply_to_message,
 | 
				
			||||||
 | 
					                f"*{who}* \[{i + 1}/{len(msgs)}\\]:\n{telegramify_markdown.convert(msgs[i])}",
 | 
				
			||||||
 | 
					                parse_mode="MarkdownV2",
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					    except Exception:
 | 
				
			||||||
 | 
					        logger.exception("Error in bot_reply_markdown")
 | 
				
			||||||
 | 
					        # logger.info(f"wrong markdown format: {text}")
 | 
				
			||||||
 | 
					        bot.edit_message_text(
 | 
				
			||||||
 | 
					            f"*{who}*:\n{text}",
 | 
				
			||||||
 | 
					            chat_id=reply_id.chat.id,
 | 
				
			||||||
 | 
					            message_id=reply_id.message_id,
 | 
				
			||||||
 | 
					            disable_web_page_preview=disable_web_page_preview,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def extract_prompt(message: str, bot_name: str) -> str:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    This function filters messages for prompts.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					      str: If it is not a prompt, return None. Otherwise, return the trimmed prefix of the actual prompt.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    # remove '@bot_name' as it is considered part of the command when in a group chat.
 | 
				
			||||||
 | 
					    message = re.sub(re.escape(f"@{bot_name}"), "", message).strip()
 | 
				
			||||||
 | 
					    # add a whitespace after the first colon as we separate the prompt from the command by the first whitespace.
 | 
				
			||||||
 | 
					    message = re.sub(":", ": ", message, count=1).strip()
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        left, message = message.split(maxsplit=1)
 | 
				
			||||||
 | 
					    except ValueError:
 | 
				
			||||||
 | 
					        return ""
 | 
				
			||||||
 | 
					    if ":" not in left:
 | 
				
			||||||
 | 
					        # the replacement happens in the right part, restore it.
 | 
				
			||||||
 | 
					        message = message.replace(": ", ":", 1)
 | 
				
			||||||
 | 
					    return message.strip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def remove_prompt_prefix(message: str) -> str:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Remove "/cmd" or "/cmd@bot_name" or "cmd:"
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    message += " "
 | 
				
			||||||
 | 
					    # Explanation of the regex pattern:
 | 
				
			||||||
 | 
					    # ^                        - Match the start of the string
 | 
				
			||||||
 | 
					    # (                        - Start of the group
 | 
				
			||||||
 | 
					    #   /                      - Literal forward slash
 | 
				
			||||||
 | 
					    #   [a-zA-Z]               - Any letter (start of the command)
 | 
				
			||||||
 | 
					    #   [a-zA-Z0-9_]*          - Any number of letters, digits, or underscores
 | 
				
			||||||
 | 
					    #   (@\w+)?                - Optionally match @ followed by one or more word characters (for bot name)
 | 
				
			||||||
 | 
					    #   \s                     - A single whitespace character (space or newline)
 | 
				
			||||||
 | 
					    # |                        - OR
 | 
				
			||||||
 | 
					    #   [a-zA-Z]               - Any letter (start of the command)
 | 
				
			||||||
 | 
					    #   [a-zA-Z0-9_]*          - Any number of letters, digits, or underscores
 | 
				
			||||||
 | 
					    #   :\s                    - Colon followed by a single whitespace character
 | 
				
			||||||
 | 
					    # )                        - End of the group
 | 
				
			||||||
 | 
					    pattern = r"^(/[a-zA-Z][a-zA-Z0-9_]*(@\w+)?\s|[a-zA-Z][a-zA-Z0-9_]*:\s)"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return re.sub(pattern, "", message).strip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def non_llm_handler(handler: T) -> T:
 | 
				
			||||||
 | 
					    handler.__is_llm_handler__ = False
 | 
				
			||||||
 | 
					    return handler
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def wrap_handler(handler: T, bot: TeleBot) -> T:
 | 
				
			||||||
 | 
					    def wrapper(message: Message, *args: Any, **kwargs: Any) -> None:
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            if getattr(handler, "__is_llm_handler__", True):
 | 
				
			||||||
 | 
					                m = ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if message.text is not None:
 | 
				
			||||||
 | 
					                    m = message.text = extract_prompt(
 | 
				
			||||||
 | 
					                        message.text, bot.get_me().username
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                elif message.caption is not None:
 | 
				
			||||||
 | 
					                    m = message.caption = extract_prompt(
 | 
				
			||||||
 | 
					                        message.caption, bot.get_me().username
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                elif message.location and message.location.latitude is not None:
 | 
				
			||||||
 | 
					                    # for location map handler just return
 | 
				
			||||||
 | 
					                    return handler(message, *args, **kwargs)
 | 
				
			||||||
 | 
					                if not m:
 | 
				
			||||||
 | 
					                    bot.reply_to(message, "Please provide info after start words.")
 | 
				
			||||||
 | 
					                    return
 | 
				
			||||||
 | 
					            return handler(message, *args, **kwargs)
 | 
				
			||||||
 | 
					        except Exception as e:
 | 
				
			||||||
 | 
					            logger.exception("Error in handler %s: %s", handler.__name__, e)
 | 
				
			||||||
 | 
					            # handle more here
 | 
				
			||||||
 | 
					            if str(e).find("RECITATION") > 0:
 | 
				
			||||||
 | 
					                bot.reply_to(message, "Your prompt `RECITATION` please check the log")
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                bot.reply_to(message, "Something wrong, please check the log")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return update_wrapper(wrapper, handler)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def extract_url_from_text(text: str) -> list[str]:
 | 
				
			||||||
 | 
					    extractor = URLExtract()
 | 
				
			||||||
 | 
					    urls = extractor.find_urls(text)
 | 
				
			||||||
 | 
					    return urls
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_text_from_jina_reader(url: str):
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        r = requests.get(f"https://r.jina.ai/{url}")
 | 
				
			||||||
 | 
					        return r.text
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        logger.exception("Error fetching text from Jina reader: %s", e)
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def enrich_text_with_urls(text: str) -> str:
 | 
				
			||||||
 | 
					    urls = extract_url_from_text(text)
 | 
				
			||||||
 | 
					    for u in urls:
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            url_text = get_text_from_jina_reader(u)
 | 
				
			||||||
 | 
					            url_text = f"\n```markdown\n{url_text}\n```\n"
 | 
				
			||||||
 | 
					            text = text.replace(u, url_text)
 | 
				
			||||||
 | 
					        except Exception:
 | 
				
			||||||
 | 
					            # just ignore the error
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def image_to_data_uri(file_path):
 | 
				
			||||||
 | 
					    content_type = guess_type(file_path)[0]
 | 
				
			||||||
 | 
					    with open(file_path, "rb") as image_file:
 | 
				
			||||||
 | 
					        encoded_image = base64.b64encode(image_file.read()).decode("utf-8")
 | 
				
			||||||
 | 
					        return f"data:{content_type};base64,{encoded_image}"
 | 
				
			||||||
@ -1,12 +1,12 @@
 | 
				
			|||||||
from os import environ
 | 
					 | 
				
			||||||
import time
 | 
					import time
 | 
				
			||||||
 | 
					from os import environ
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from openai import OpenAI
 | 
					 | 
				
			||||||
import requests
 | 
					import requests
 | 
				
			||||||
 | 
					from expiringdict import ExpiringDict
 | 
				
			||||||
 | 
					from openai import OpenAI
 | 
				
			||||||
from telebot import TeleBot
 | 
					from telebot import TeleBot
 | 
				
			||||||
from telebot.types import Message
 | 
					from telebot.types import Message
 | 
				
			||||||
from telegramify_markdown import convert
 | 
					from telegramify_markdown import convert
 | 
				
			||||||
from expiringdict import ExpiringDict
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import *
 | 
					from . import *
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -197,7 +197,7 @@ def yi_photo_handler(message: Message, bot: TeleBot) -> None:
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    response = requests.post(
 | 
					    response = requests.post(
 | 
				
			||||||
        f"https://api.lingyiwanwu.com/v1/chat/completions",
 | 
					        "https://api.lingyiwanwu.com/v1/chat/completions",
 | 
				
			||||||
        headers=headers,
 | 
					        headers=headers,
 | 
				
			||||||
        json=payload,
 | 
					        json=payload,
 | 
				
			||||||
    ).json()
 | 
					    ).json()
 | 
				
			||||||
@ -1,27 +1,29 @@
 | 
				
			|||||||
from os import environ
 | 
					 | 
				
			||||||
import time
 | 
					import time
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from openai import OpenAI
 | 
					from expiringdict import ExpiringDict
 | 
				
			||||||
from telebot import TeleBot
 | 
					from telebot import TeleBot
 | 
				
			||||||
from telebot.types import Message
 | 
					from telebot.types import Message
 | 
				
			||||||
from expiringdict import ExpiringDict
 | 
					 | 
				
			||||||
from rich import print
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from . import *
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from telegramify_markdown import convert
 | 
					from telegramify_markdown import convert
 | 
				
			||||||
from telegramify_markdown.customize import markdown_symbol
 | 
					from telegramify_markdown.customize import markdown_symbol
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from config import settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from ._utils import (
 | 
				
			||||||
 | 
					    bot_reply_first,
 | 
				
			||||||
 | 
					    bot_reply_markdown,
 | 
				
			||||||
 | 
					    enrich_text_with_urls,
 | 
				
			||||||
 | 
					    image_to_data_uri,
 | 
				
			||||||
 | 
					    logger,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
markdown_symbol.head_level_1 = "📌"  # If you want, Customizing the head level 1 symbol
 | 
					markdown_symbol.head_level_1 = "📌"  # If you want, Customizing the head level 1 symbol
 | 
				
			||||||
markdown_symbol.link = "🔗"  # If you want, Customizing the link symbol
 | 
					markdown_symbol.link = "🔗"  # If you want, Customizing the link symbol
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CHATGPT_API_KEY = environ.get("OPENAI_API_KEY")
 | 
					CHATGPT_MODEL = settings.openai_model
 | 
				
			||||||
CHATGPT_BASE_URL = environ.get("OPENAI_API_BASE") or "https://api.openai.com/v1"
 | 
					CHATGPT_PRO_MODEL = settings.openai_model
 | 
				
			||||||
CHATGPT_MODEL = "gpt-4o-mini-2024-07-18"
 | 
					 | 
				
			||||||
CHATGPT_PRO_MODEL = "gpt-4o-mini-2024-07-18"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
client = OpenAI(api_key=CHATGPT_API_KEY, base_url=CHATGPT_BASE_URL)
 | 
					client = settings.openai_client
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Global history cache
 | 
					# Global history cache
 | 
				
			||||||
@ -31,7 +33,7 @@ chatgpt_pro_player_dict = ExpiringDict(max_len=1000, max_age_seconds=600)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
def chatgpt_handler(message: Message, bot: TeleBot) -> None:
 | 
					def chatgpt_handler(message: Message, bot: TeleBot) -> None:
 | 
				
			||||||
    """gpt : /gpt <question>"""
 | 
					    """gpt : /gpt <question>"""
 | 
				
			||||||
    print(message)
 | 
					    logger.debug(message)
 | 
				
			||||||
    m = message.text.strip()
 | 
					    m = message.text.strip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    player_message = []
 | 
					    player_message = []
 | 
				
			||||||
@ -81,8 +83,8 @@ def chatgpt_handler(message: Message, bot: TeleBot) -> None:
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    except Exception as e:
 | 
					    except Exception:
 | 
				
			||||||
        print(e)
 | 
					        logger.exception("ChatGPT handler error")
 | 
				
			||||||
        bot.reply_to(message, "answer wrong maybe up to the max token")
 | 
					        bot.reply_to(message, "answer wrong maybe up to the max token")
 | 
				
			||||||
        # pop my user
 | 
					        # pop my user
 | 
				
			||||||
        player_message.pop()
 | 
					        player_message.pop()
 | 
				
			||||||
@ -134,7 +136,7 @@ def chatgpt_pro_handler(message: Message, bot: TeleBot) -> None:
 | 
				
			|||||||
        s = ""
 | 
					        s = ""
 | 
				
			||||||
        start = time.time()
 | 
					        start = time.time()
 | 
				
			||||||
        for chunk in r:
 | 
					        for chunk in r:
 | 
				
			||||||
            print(chunk)
 | 
					            logger.debug(chunk)
 | 
				
			||||||
            if chunk.choices:
 | 
					            if chunk.choices:
 | 
				
			||||||
                if chunk.choices[0].delta.content is None:
 | 
					                if chunk.choices[0].delta.content is None:
 | 
				
			||||||
                    break
 | 
					                    break
 | 
				
			||||||
@ -145,7 +147,7 @@ def chatgpt_pro_handler(message: Message, bot: TeleBot) -> None:
 | 
				
			|||||||
        # maybe not complete
 | 
					        # maybe not complete
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            bot_reply_markdown(reply_id, who, s, bot, split_text=True)
 | 
					            bot_reply_markdown(reply_id, who, s, bot, split_text=True)
 | 
				
			||||||
        except:
 | 
					        except Exception:
 | 
				
			||||||
            pass
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        player_message.append(
 | 
					        player_message.append(
 | 
				
			||||||
@ -155,8 +157,8 @@ def chatgpt_pro_handler(message: Message, bot: TeleBot) -> None:
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    except Exception as e:
 | 
					    except Exception:
 | 
				
			||||||
        print(e)
 | 
					        logger.exception("ChatGPT handler error")
 | 
				
			||||||
        # bot.reply_to(message, "answer wrong maybe up to the max token")
 | 
					        # bot.reply_to(message, "answer wrong maybe up to the max token")
 | 
				
			||||||
        player_message.clear()
 | 
					        player_message.clear()
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
@ -205,15 +207,15 @@ def chatgpt_photo_handler(message: Message, bot: TeleBot) -> None:
 | 
				
			|||||||
        # maybe not complete
 | 
					        # maybe not complete
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            bot_reply_markdown(reply_id, who, s, bot)
 | 
					            bot_reply_markdown(reply_id, who, s, bot)
 | 
				
			||||||
        except:
 | 
					        except Exception:
 | 
				
			||||||
            pass
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    except Exception as e:
 | 
					    except Exception:
 | 
				
			||||||
        print(e)
 | 
					        logger.exception("ChatGPT handler error")
 | 
				
			||||||
        bot.reply_to(message, "answer wrong maybe up to the max token")
 | 
					        bot.reply_to(message, "answer wrong maybe up to the max token")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if CHATGPT_API_KEY:
 | 
					if settings.openai_api_key:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def register(bot: TeleBot) -> None:
 | 
					    def register(bot: TeleBot) -> None:
 | 
				
			||||||
        bot.register_message_handler(chatgpt_handler, commands=["gpt"], pass_bot=True)
 | 
					        bot.register_message_handler(chatgpt_handler, commands=["gpt"], pass_bot=True)
 | 
				
			||||||
 | 
				
			|||||||
@ -1,17 +1,16 @@
 | 
				
			|||||||
 | 
					import time
 | 
				
			||||||
from os import environ
 | 
					from os import environ
 | 
				
			||||||
from pathlib import Path
 | 
					from pathlib import Path
 | 
				
			||||||
import time
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from anthropic import Anthropic, APITimeoutError
 | 
					from anthropic import Anthropic, APITimeoutError
 | 
				
			||||||
 | 
					from expiringdict import ExpiringDict
 | 
				
			||||||
from telebot import TeleBot
 | 
					from telebot import TeleBot
 | 
				
			||||||
from telebot.types import Message
 | 
					from telebot.types import Message
 | 
				
			||||||
from expiringdict import ExpiringDict
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from . import *
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from telegramify_markdown import convert
 | 
					from telegramify_markdown import convert
 | 
				
			||||||
from telegramify_markdown.customize import markdown_symbol
 | 
					from telegramify_markdown.customize import markdown_symbol
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from ._utils import bot_reply_first, bot_reply_markdown, enrich_text_with_urls
 | 
				
			||||||
 | 
					
 | 
				
			||||||
markdown_symbol.head_level_1 = "📌"  # If you want, Customizing the head level 1 symbol
 | 
					markdown_symbol.head_level_1 = "📌"  # If you want, Customizing the head level 1 symbol
 | 
				
			||||||
markdown_symbol.link = "🔗"  # If you want, Customizing the link symbol
 | 
					markdown_symbol.link = "🔗"  # If you want, Customizing the link symbol
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,18 +1,19 @@
 | 
				
			|||||||
from os import environ
 | 
					 | 
				
			||||||
import time
 | 
					 | 
				
			||||||
import datetime
 | 
					import datetime
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
from telebot import TeleBot
 | 
					from os import environ
 | 
				
			||||||
from telebot.types import Message
 | 
					 | 
				
			||||||
from expiringdict import ExpiringDict
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from . import *
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import cohere
 | 
					import cohere
 | 
				
			||||||
 | 
					from expiringdict import ExpiringDict
 | 
				
			||||||
 | 
					from telebot import TeleBot
 | 
				
			||||||
 | 
					from telebot.types import Message
 | 
				
			||||||
from telegramify_markdown import convert
 | 
					from telegramify_markdown import convert
 | 
				
			||||||
from telegramify_markdown.customize import markdown_symbol
 | 
					from telegramify_markdown.customize import markdown_symbol
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from config import settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from ._utils import bot_reply_first, bot_reply_markdown, enrich_text_with_urls
 | 
				
			||||||
 | 
					
 | 
				
			||||||
markdown_symbol.head_level_1 = "📌"  # If you want, Customizing the head level 1 symbol
 | 
					markdown_symbol.head_level_1 = "📌"  # If you want, Customizing the head level 1 symbol
 | 
				
			||||||
markdown_symbol.link = "🔗"  # If you want, Customizing the link symbol
 | 
					markdown_symbol.link = "🔗"  # If you want, Customizing the link symbol
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -21,8 +22,6 @@ COHERE_MODEL = "command-r-plus"  # command-r may cause Chinese garbled code, and
 | 
				
			|||||||
if COHERE_API_KEY:
 | 
					if COHERE_API_KEY:
 | 
				
			||||||
    co = cohere.Client(api_key=COHERE_API_KEY)
 | 
					    co = cohere.Client(api_key=COHERE_API_KEY)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
TELEGRA_PH_TOKEN = environ.get("TELEGRA_PH_TOKEN")
 | 
					 | 
				
			||||||
ph = TelegraphAPI(TELEGRA_PH_TOKEN)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Global history cache
 | 
					# Global history cache
 | 
				
			||||||
cohere_player_dict = ExpiringDict(max_len=1000, max_age_seconds=600)
 | 
					cohere_player_dict = ExpiringDict(max_len=1000, max_age_seconds=600)
 | 
				
			||||||
@ -140,7 +139,7 @@ def cohere_handler(message: Message, bot: TeleBot) -> None:
 | 
				
			|||||||
            + source
 | 
					            + source
 | 
				
			||||||
            + f"\nLast Update{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')} at UTC+8\n"
 | 
					            + f"\nLast Update{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')} at UTC+8\n"
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        ph_s = ph.create_page_md(
 | 
					        ph_s = settings.telegraph_client.create_page_md(
 | 
				
			||||||
            title="Cohere", markdown_text=content
 | 
					            title="Cohere", markdown_text=content
 | 
				
			||||||
        )  # or edit_page with get_page so not producing massive pages
 | 
					        )  # or edit_page with get_page so not producing massive pages
 | 
				
			||||||
        s += f"\n\n[View]({ph_s})"
 | 
					        s += f"\n\n[View]({ph_s})"
 | 
				
			||||||
@ -149,7 +148,7 @@ def cohere_handler(message: Message, bot: TeleBot) -> None:
 | 
				
			|||||||
            bot_reply_markdown(
 | 
					            bot_reply_markdown(
 | 
				
			||||||
                reply_id, who, s, bot, split_text=True, disable_web_page_preview=True
 | 
					                reply_id, who, s, bot, split_text=True, disable_web_page_preview=True
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        except:
 | 
					        except Exception:
 | 
				
			||||||
            pass
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        player_message.append(
 | 
					        player_message.append(
 | 
				
			||||||
 | 
				
			|||||||
@ -1,18 +1,16 @@
 | 
				
			|||||||
import json
 | 
					import json
 | 
				
			||||||
import time
 | 
					 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
from telebot import TeleBot
 | 
					 | 
				
			||||||
from telebot.types import Message
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from . import *
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# TODO: update requirements.txt and setup tools
 | 
					# TODO: update requirements.txt and setup tools
 | 
				
			||||||
# pip install dify-client
 | 
					# pip install dify-client
 | 
				
			||||||
from dify_client import ChatClient
 | 
					from dify_client import ChatClient
 | 
				
			||||||
 | 
					from telebot import TeleBot
 | 
				
			||||||
 | 
					from telebot.types import Message
 | 
				
			||||||
from telegramify_markdown.customize import markdown_symbol
 | 
					from telegramify_markdown.customize import markdown_symbol
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from ._utils import bot_reply_first, bot_reply_markdown, enrich_text_with_urls
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# If you want, Customizing the head level 1 symbol
 | 
					# If you want, Customizing the head level 1 symbol
 | 
				
			||||||
markdown_symbol.head_level_1 = "📌"
 | 
					markdown_symbol.head_level_1 = "📌"
 | 
				
			||||||
markdown_symbol.link = "🔗"  # If you want, Customizing the link symbol
 | 
					markdown_symbol.link = "🔗"  # If you want, Customizing the link symbol
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,10 @@
 | 
				
			|||||||
import random
 | 
					import random
 | 
				
			||||||
from PIL import Image, ImageDraw, ImageFont
 | 
					import re
 | 
				
			||||||
from os import listdir
 | 
					from os import listdir
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from PIL import Image, ImageDraw, ImageFont
 | 
				
			||||||
from telebot import TeleBot
 | 
					from telebot import TeleBot
 | 
				
			||||||
from telebot.types import Message
 | 
					from telebot.types import Message
 | 
				
			||||||
import re
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from . import *
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def split_lines(text, max_length=30):
 | 
					def split_lines(text, max_length=30):
 | 
				
			||||||
@ -157,7 +156,7 @@ def fake_photo_handler(message: Message, bot: TeleBot) -> None:
 | 
				
			|||||||
    s = s.replace("/fake", "").strip()
 | 
					    s = s.replace("/fake", "").strip()
 | 
				
			||||||
    s = s.replace("fake:", "").strip()
 | 
					    s = s.replace("fake:", "").strip()
 | 
				
			||||||
    prompt = s.strip()
 | 
					    prompt = s.strip()
 | 
				
			||||||
    bot.reply_to(message, f"Generating LiuNeng's fake image")
 | 
					    bot.reply_to(message, "Generating LiuNeng's fake image")
 | 
				
			||||||
    # get the high quaility picture.
 | 
					    # get the high quaility picture.
 | 
				
			||||||
    max_size_photo = max(message.photo, key=lambda p: p.file_size)
 | 
					    max_size_photo = max(message.photo, key=lambda p: p.file_size)
 | 
				
			||||||
    file_path = bot.get_file(max_size_photo.file_id).file_path
 | 
					    file_path = bot.get_file(max_size_photo.file_id).file_path
 | 
				
			||||||
 | 
				
			|||||||
@ -1,17 +1,16 @@
 | 
				
			|||||||
from os import environ
 | 
					 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
import time
 | 
					import time
 | 
				
			||||||
 | 
					from os import environ
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import google.generativeai as genai
 | 
					import google.generativeai as genai
 | 
				
			||||||
 | 
					from expiringdict import ExpiringDict
 | 
				
			||||||
from google.generativeai import ChatSession
 | 
					from google.generativeai import ChatSession
 | 
				
			||||||
from google.generativeai.types.generation_types import StopCandidateException
 | 
					from google.generativeai.types.generation_types import StopCandidateException
 | 
				
			||||||
from telebot import TeleBot
 | 
					from telebot import TeleBot
 | 
				
			||||||
from telebot.types import Message
 | 
					from telebot.types import Message
 | 
				
			||||||
from expiringdict import ExpiringDict
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from telegramify_markdown.customize import markdown_symbol
 | 
					from telegramify_markdown.customize import markdown_symbol
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import *
 | 
					from ._utils import bot_reply_first, bot_reply_markdown, enrich_text_with_urls, logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
markdown_symbol.head_level_1 = "📌"  # If you want, Customizing the head level 1 symbol
 | 
					markdown_symbol.head_level_1 = "📌"  # If you want, Customizing the head level 1 symbol
 | 
				
			||||||
markdown_symbol.link = "🔗"  # If you want, Customizing the link symbol
 | 
					markdown_symbol.link = "🔗"  # If you want, Customizing the link symbol
 | 
				
			||||||
@ -166,11 +165,11 @@ def gemini_pro_handler(message: Message, bot: TeleBot) -> None:
 | 
				
			|||||||
            player.history.clear()
 | 
					            player.history.clear()
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
    except Exception as e:
 | 
					    except Exception as e:
 | 
				
			||||||
        print(e)
 | 
					        logger.exception("Gemini audio handler error")
 | 
				
			||||||
        bot.reply_to(message, "answer wrong maybe up to the max token")
 | 
					        bot.reply_to(message, "answer wrong maybe up to the max token")
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            player.history.clear()
 | 
					            player.history.clear()
 | 
				
			||||||
        except:
 | 
					        except Exception:
 | 
				
			||||||
            print(f"\n------\n{who} history.clear() Error / Unstoppable\n------\n")
 | 
					            print(f"\n------\n{who} history.clear() Error / Unstoppable\n------\n")
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -207,10 +206,10 @@ def gemini_photo_handler(message: Message, bot: TeleBot) -> None:
 | 
				
			|||||||
        # maybe not complete
 | 
					        # maybe not complete
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            bot_reply_markdown(reply_id, who, s, bot)
 | 
					            bot_reply_markdown(reply_id, who, s, bot)
 | 
				
			||||||
        except:
 | 
					        except Exception:
 | 
				
			||||||
            pass
 | 
					            pass
 | 
				
			||||||
    except Exception as e:
 | 
					    except Exception as e:
 | 
				
			||||||
        print(e)
 | 
					        logger.exception("Gemini photo handler error")
 | 
				
			||||||
        bot.reply_to(message, "answer wrong maybe up to the max token")
 | 
					        bot.reply_to(message, "answer wrong maybe up to the max token")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -248,11 +247,11 @@ def gemini_audio_handler(message: Message, bot: TeleBot) -> None:
 | 
				
			|||||||
            player.history.clear()
 | 
					            player.history.clear()
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
    except Exception as e:
 | 
					    except Exception as e:
 | 
				
			||||||
        print(e)
 | 
					        logger.exception("Gemini audio handler error")
 | 
				
			||||||
        bot.reply_to(message, "answer wrong maybe up to the max token")
 | 
					        bot.reply_to(message, "answer wrong maybe up to the max token")
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            player.history.clear()
 | 
					            player.history.clear()
 | 
				
			||||||
        except:
 | 
					        except Exception:
 | 
				
			||||||
            print(f"\n------\n{who} history.clear() Error / Unstoppable\n------\n")
 | 
					            print(f"\n------\n{who} history.clear() Error / Unstoppable\n------\n")
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -15,18 +15,15 @@ def github_poster_handler(message: Message, bot: TeleBot):
 | 
				
			|||||||
        cmd_list.append("--year")
 | 
					        cmd_list.append("--year")
 | 
				
			||||||
        cmd_list.append(years.strip())
 | 
					        cmd_list.append(years.strip())
 | 
				
			||||||
    r = subprocess.check_output(cmd_list).decode("utf-8")
 | 
					    r = subprocess.check_output(cmd_list).decode("utf-8")
 | 
				
			||||||
    try:
 | 
					    if "done" in r:
 | 
				
			||||||
        if "done" in r:
 | 
					        # TODO windows path
 | 
				
			||||||
            # TODO windows path
 | 
					        r = subprocess.check_output(
 | 
				
			||||||
            r = subprocess.check_output(
 | 
					            ["cairosvg", "OUT_FOLDER/github.svg", "-o", f"github_{name}.png"]
 | 
				
			||||||
                ["cairosvg", "OUT_FOLDER/github.svg", "-o", f"github_{name}.png"]
 | 
					        ).decode("utf-8")
 | 
				
			||||||
            ).decode("utf-8")
 | 
					        with open(f"github_{name}.png", "rb") as photo:
 | 
				
			||||||
            with open(f"github_{name}.png", "rb") as photo:
 | 
					            bot.send_photo(
 | 
				
			||||||
                bot.send_photo(
 | 
					                message.chat.id, photo, reply_to_message_id=message.message_id
 | 
				
			||||||
                    message.chat.id, photo, reply_to_message_id=message.message_id
 | 
					            )
 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
    except:
 | 
					 | 
				
			||||||
        bot.reply_to(message, "github poster error")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def register(bot: TeleBot) -> None:
 | 
					def register(bot: TeleBot) -> None:
 | 
				
			||||||
 | 
				
			|||||||
@ -1,13 +1,13 @@
 | 
				
			|||||||
import re
 | 
					import re
 | 
				
			||||||
from telebot import TeleBot
 | 
					 | 
				
			||||||
from telebot.types import Message
 | 
					 | 
				
			||||||
from telebot.types import InputMediaPhoto
 | 
					 | 
				
			||||||
from os import environ
 | 
					from os import environ
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import requests
 | 
				
			||||||
from expiringdict import ExpiringDict
 | 
					from expiringdict import ExpiringDict
 | 
				
			||||||
from kling import ImageGen, VideoGen
 | 
					from kling import ImageGen, VideoGen
 | 
				
			||||||
import requests
 | 
					from telebot import TeleBot
 | 
				
			||||||
 | 
					from telebot.types import InputMediaPhoto, Message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import *
 | 
					from ._utils import logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
KLING_COOKIE = environ.get("KLING_COOKIE")
 | 
					KLING_COOKIE = environ.get("KLING_COOKIE")
 | 
				
			||||||
pngs_link_dict = ExpiringDict(max_len=100, max_age_seconds=60 * 10)
 | 
					pngs_link_dict = ExpiringDict(max_len=100, max_age_seconds=60 * 10)
 | 
				
			||||||
@ -17,7 +17,7 @@ def kling_handler(message: Message, bot: TeleBot):
 | 
				
			|||||||
    """kling: /kling <address>"""
 | 
					    """kling: /kling <address>"""
 | 
				
			||||||
    bot.reply_to(
 | 
					    bot.reply_to(
 | 
				
			||||||
        message,
 | 
					        message,
 | 
				
			||||||
        f"Generating pretty kling image may take some time please wait",
 | 
					        "Generating pretty kling image may take some time please wait",
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    m = message.text.strip()
 | 
					    m = message.text.strip()
 | 
				
			||||||
    prompt = m.strip()
 | 
					    prompt = m.strip()
 | 
				
			||||||
@ -47,7 +47,7 @@ def kling_pro_handler(message: Message, bot: TeleBot):
 | 
				
			|||||||
    """kling: /kling <address>"""
 | 
					    """kling: /kling <address>"""
 | 
				
			||||||
    bot.reply_to(
 | 
					    bot.reply_to(
 | 
				
			||||||
        message,
 | 
					        message,
 | 
				
			||||||
        f"Generating pretty kling video may take a long time about 2mins to 5mins please wait",
 | 
					        "Generating pretty kling video may take a long time about 2mins to 5mins please wait",
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    m = message.text.strip()
 | 
					    m = message.text.strip()
 | 
				
			||||||
    prompt = m.strip()
 | 
					    prompt = m.strip()
 | 
				
			||||||
@ -98,7 +98,7 @@ def kling_photo_handler(message: Message, bot: TeleBot) -> None:
 | 
				
			|||||||
    downloaded_file = bot.download_file(file_path)
 | 
					    downloaded_file = bot.download_file(file_path)
 | 
				
			||||||
    bot.reply_to(
 | 
					    bot.reply_to(
 | 
				
			||||||
        message,
 | 
					        message,
 | 
				
			||||||
        f"Generating pretty kling image using your photo may take some time please wait",
 | 
					        "Generating pretty kling image using your photo may take some time please wait",
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    with open("kling.jpg", "wb") as temp_file:
 | 
					    with open("kling.jpg", "wb") as temp_file:
 | 
				
			||||||
        temp_file.write(downloaded_file)
 | 
					        temp_file.write(downloaded_file)
 | 
				
			||||||
@ -109,10 +109,10 @@ def kling_photo_handler(message: Message, bot: TeleBot) -> None:
 | 
				
			|||||||
        # set the dict
 | 
					        # set the dict
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            pngs_link_dict[str(message.from_user.id)] = links
 | 
					            pngs_link_dict[str(message.from_user.id)] = links
 | 
				
			||||||
        except Exception as e:
 | 
					        except Exception:
 | 
				
			||||||
            print(str(e))
 | 
					            logger.exception("Kling photo handler error")
 | 
				
			||||||
    except Exception as e:
 | 
					    except Exception:
 | 
				
			||||||
        print(str(e))
 | 
					        logger.exception("Kling photo handler error")
 | 
				
			||||||
        bot.reply_to(message, "kling error maybe block the prompt")
 | 
					        bot.reply_to(message, "kling error maybe block the prompt")
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
    photos_list = [InputMediaPhoto(i) for i in links]
 | 
					    photos_list = [InputMediaPhoto(i) for i in links]
 | 
				
			||||||
 | 
				
			|||||||
@ -1,16 +1,15 @@
 | 
				
			|||||||
from os import environ
 | 
					 | 
				
			||||||
import time
 | 
					import time
 | 
				
			||||||
 | 
					from os import environ
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from expiringdict import ExpiringDict
 | 
				
			||||||
 | 
					from groq import Groq
 | 
				
			||||||
from telebot import TeleBot
 | 
					from telebot import TeleBot
 | 
				
			||||||
from telebot.types import Message
 | 
					from telebot.types import Message
 | 
				
			||||||
from expiringdict import ExpiringDict
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from . import *
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from groq import Groq
 | 
					 | 
				
			||||||
from telegramify_markdown import convert
 | 
					from telegramify_markdown import convert
 | 
				
			||||||
from telegramify_markdown.customize import markdown_symbol
 | 
					from telegramify_markdown.customize import markdown_symbol
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from ._utils import bot_reply_first, bot_reply_markdown, enrich_text_with_urls, logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
markdown_symbol.head_level_1 = "📌"  # If you want, Customizing the head level 1 symbol
 | 
					markdown_symbol.head_level_1 = "📌"  # If you want, Customizing the head level 1 symbol
 | 
				
			||||||
markdown_symbol.link = "🔗"  # If you want, Customizing the link symbol
 | 
					markdown_symbol.link = "🔗"  # If you want, Customizing the link symbol
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -75,8 +74,8 @@ def llama_handler(message: Message, bot: TeleBot) -> None:
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    except Exception as e:
 | 
					    except Exception:
 | 
				
			||||||
        print(e)
 | 
					        logger.exception("Llama handler error")
 | 
				
			||||||
        bot.reply_to(message, "answer wrong maybe up to the max token")
 | 
					        bot.reply_to(message, "answer wrong maybe up to the max token")
 | 
				
			||||||
        # pop my user
 | 
					        # pop my user
 | 
				
			||||||
        player_message.pop()
 | 
					        player_message.pop()
 | 
				
			||||||
@ -148,8 +147,8 @@ def llama_pro_handler(message: Message, bot: TeleBot) -> None:
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    except Exception as e:
 | 
					    except Exception:
 | 
				
			||||||
        print(e)
 | 
					        logger.exception("Llama Pro handler error")
 | 
				
			||||||
        bot.reply_to(message, "answer wrong maybe up to the max token")
 | 
					        bot.reply_to(message, "answer wrong maybe up to the max token")
 | 
				
			||||||
        player_message.clear()
 | 
					        player_message.clear()
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,11 @@
 | 
				
			|||||||
import gc
 | 
					import gc
 | 
				
			||||||
import shutil
 | 
					 | 
				
			||||||
import random
 | 
					import random
 | 
				
			||||||
 | 
					import shutil
 | 
				
			||||||
from tempfile import SpooledTemporaryFile
 | 
					from tempfile import SpooledTemporaryFile
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import numpy as np
 | 
					import numpy as np
 | 
				
			||||||
import PIL
 | 
					import PIL.Image
 | 
				
			||||||
from matplotlib import figure
 | 
					from matplotlib import figure
 | 
				
			||||||
from PIL import Image
 | 
					 | 
				
			||||||
from prettymapp.geo import get_aoi
 | 
					from prettymapp.geo import get_aoi
 | 
				
			||||||
from prettymapp.osm import get_osm_geometries
 | 
					from prettymapp.osm import get_osm_geometries
 | 
				
			||||||
from prettymapp.plotting import Plot as PrettyPlot
 | 
					from prettymapp.plotting import Plot as PrettyPlot
 | 
				
			||||||
@ -58,7 +57,7 @@ def sizeof_image(image):
 | 
				
			|||||||
def compress_image(input_image, output_image, target_size):
 | 
					def compress_image(input_image, output_image, target_size):
 | 
				
			||||||
    quality = 95
 | 
					    quality = 95
 | 
				
			||||||
    factor = 1.0
 | 
					    factor = 1.0
 | 
				
			||||||
    with Image.open(input_image) as img:
 | 
					    with PIL.Image.open(input_image) as img:
 | 
				
			||||||
        while sizeof_image(img) > target_size:
 | 
					        while sizeof_image(img) > target_size:
 | 
				
			||||||
            factor -= 0.05
 | 
					            factor -= 0.05
 | 
				
			||||||
            width, height = img.size
 | 
					            width, height = img.size
 | 
				
			||||||
 | 
				
			|||||||
@ -1,16 +1,15 @@
 | 
				
			|||||||
# qwen use https://api.together.xyz
 | 
					# qwen use https://api.together.xyz
 | 
				
			||||||
from os import environ
 | 
					 | 
				
			||||||
import time
 | 
					import time
 | 
				
			||||||
 | 
					from os import environ
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from expiringdict import ExpiringDict
 | 
				
			||||||
from telebot import TeleBot
 | 
					from telebot import TeleBot
 | 
				
			||||||
from telebot.types import Message
 | 
					from telebot.types import Message
 | 
				
			||||||
from expiringdict import ExpiringDict
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from . import *
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from together import Together
 | 
					 | 
				
			||||||
from telegramify_markdown import convert
 | 
					from telegramify_markdown import convert
 | 
				
			||||||
from telegramify_markdown.customize import markdown_symbol
 | 
					from telegramify_markdown.customize import markdown_symbol
 | 
				
			||||||
 | 
					from together import Together
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from ._utils import bot_reply_first, bot_reply_markdown, enrich_text_with_urls, logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
markdown_symbol.head_level_1 = "📌"  # If you want, Customizing the head level 1 symbol
 | 
					markdown_symbol.head_level_1 = "📌"  # If you want, Customizing the head level 1 symbol
 | 
				
			||||||
markdown_symbol.link = "🔗"  # If you want, Customizing the link symbol
 | 
					markdown_symbol.link = "🔗"  # If you want, Customizing the link symbol
 | 
				
			||||||
@ -77,8 +76,8 @@ def qwen_handler(message: Message, bot: TeleBot) -> None:
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    except Exception as e:
 | 
					    except Exception:
 | 
				
			||||||
        print(e)
 | 
					        logger.exception("Qwen handler error")
 | 
				
			||||||
        bot.reply_to(message, "answer wrong maybe up to the max token")
 | 
					        bot.reply_to(message, "answer wrong maybe up to the max token")
 | 
				
			||||||
        # pop my user
 | 
					        # pop my user
 | 
				
			||||||
        player_message.pop()
 | 
					        player_message.pop()
 | 
				
			||||||
@ -150,8 +149,8 @@ def qwen_pro_handler(message: Message, bot: TeleBot) -> None:
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    except Exception as e:
 | 
					    except Exception:
 | 
				
			||||||
        print(e)
 | 
					        logger.exception("Qwen Pro handler error")
 | 
				
			||||||
        bot.reply_to(message, "answer wrong maybe up to the max token")
 | 
					        bot.reply_to(message, "answer wrong maybe up to the max token")
 | 
				
			||||||
        player_message.clear()
 | 
					        player_message.clear()
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
				
			|||||||
@ -1,20 +1,15 @@
 | 
				
			|||||||
from telebot import TeleBot
 | 
					 | 
				
			||||||
from telebot.types import Message
 | 
					 | 
				
			||||||
import requests
 | 
					 | 
				
			||||||
from openai import OpenAI
 | 
					 | 
				
			||||||
from os import environ
 | 
					from os import environ
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import *
 | 
					import requests
 | 
				
			||||||
 | 
					from telebot import TeleBot
 | 
				
			||||||
 | 
					from telebot.types import Message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from config import settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
SD_API_KEY = environ.get("SD3_KEY")
 | 
					SD_API_KEY = environ.get("SD3_KEY")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# TODO refactor this shit to __init__
 | 
					# TODO refactor this shit to __init__
 | 
				
			||||||
CHATGPT_API_KEY = environ.get("OPENAI_API_KEY")
 | 
					CHATGPT_PRO_MODEL = settings.openai_model
 | 
				
			||||||
CHATGPT_BASE_URL = environ.get("OPENAI_API_BASE") or "https://api.openai.com/v1"
 | 
					 | 
				
			||||||
CHATGPT_PRO_MODEL = "gpt-4o-2024-05-13"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
client = OpenAI(api_key=CHATGPT_API_KEY, base_url=CHATGPT_BASE_URL)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_user_balance():
 | 
					def get_user_balance():
 | 
				
			||||||
@ -33,7 +28,7 @@ def get_user_balance():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
def generate_sd3_image(prompt):
 | 
					def generate_sd3_image(prompt):
 | 
				
			||||||
    response = requests.post(
 | 
					    response = requests.post(
 | 
				
			||||||
        f"https://api.stability.ai/v2beta/stable-image/generate/sd3",
 | 
					        "https://api.stability.ai/v2beta/stable-image/generate/sd3",
 | 
				
			||||||
        headers={"authorization": f"Bearer {SD_API_KEY}", "accept": "image/*"},
 | 
					        headers={"authorization": f"Bearer {SD_API_KEY}", "accept": "image/*"},
 | 
				
			||||||
        files={"none": ""},
 | 
					        files={"none": ""},
 | 
				
			||||||
        data={
 | 
					        data={
 | 
				
			||||||
@ -61,18 +56,14 @@ def sd_handler(message: Message, bot: TeleBot):
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
    m = message.text.strip()
 | 
					    m = message.text.strip()
 | 
				
			||||||
    prompt = m.strip()
 | 
					    prompt = m.strip()
 | 
				
			||||||
    try:
 | 
					    r = generate_sd3_image(prompt)
 | 
				
			||||||
        r = generate_sd3_image(prompt)
 | 
					    if r:
 | 
				
			||||||
        if r:
 | 
					        with open("sd3.jpeg", "rb") as photo:
 | 
				
			||||||
            with open(f"sd3.jpeg", "rb") as photo:
 | 
					            bot.send_photo(
 | 
				
			||||||
                bot.send_photo(
 | 
					                message.chat.id, photo, reply_to_message_id=message.message_id
 | 
				
			||||||
                    message.chat.id, photo, reply_to_message_id=message.message_id
 | 
					            )
 | 
				
			||||||
                )
 | 
					    else:
 | 
				
			||||||
        else:
 | 
					        bot.reply_to(message, "prompt error")
 | 
				
			||||||
            bot.reply_to(message, "prompt error")
 | 
					 | 
				
			||||||
    except Exception as e:
 | 
					 | 
				
			||||||
        print(e)
 | 
					 | 
				
			||||||
        bot.reply_to(message, "sd3 error")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def sd_pro_handler(message: Message, bot: TeleBot):
 | 
					def sd_pro_handler(message: Message, bot: TeleBot):
 | 
				
			||||||
@ -83,7 +74,7 @@ def sd_pro_handler(message: Message, bot: TeleBot):
 | 
				
			|||||||
    rewrite_prompt = (
 | 
					    rewrite_prompt = (
 | 
				
			||||||
        f"revise `{prompt}` to a DALL-E prompt only return the prompt in English."
 | 
					        f"revise `{prompt}` to a DALL-E prompt only return the prompt in English."
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    completion = client.chat.completions.create(
 | 
					    completion = settings.openai_client.chat.completions.create(
 | 
				
			||||||
        messages=[{"role": "user", "content": rewrite_prompt}],
 | 
					        messages=[{"role": "user", "content": rewrite_prompt}],
 | 
				
			||||||
        max_tokens=2048,
 | 
					        max_tokens=2048,
 | 
				
			||||||
        model=CHATGPT_PRO_MODEL,
 | 
					        model=CHATGPT_PRO_MODEL,
 | 
				
			||||||
@ -95,21 +86,17 @@ def sd_pro_handler(message: Message, bot: TeleBot):
 | 
				
			|||||||
        message,
 | 
					        message,
 | 
				
			||||||
        f"Generating pretty sd3-turbo image may take some time please left credits {credits} every try will cost 4 criedits wait:\n the real prompt is: {sd_prompt}",
 | 
					        f"Generating pretty sd3-turbo image may take some time please left credits {credits} every try will cost 4 criedits wait:\n the real prompt is: {sd_prompt}",
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    try:
 | 
					    r = generate_sd3_image(sd_prompt)
 | 
				
			||||||
        r = generate_sd3_image(sd_prompt)
 | 
					    if r:
 | 
				
			||||||
        if r:
 | 
					        with open("sd3.jpeg", "rb") as photo:
 | 
				
			||||||
            with open(f"sd3.jpeg", "rb") as photo:
 | 
					            bot.send_photo(
 | 
				
			||||||
                bot.send_photo(
 | 
					                message.chat.id, photo, reply_to_message_id=message.message_id
 | 
				
			||||||
                    message.chat.id, photo, reply_to_message_id=message.message_id
 | 
					            )
 | 
				
			||||||
                )
 | 
					    else:
 | 
				
			||||||
        else:
 | 
					        bot.reply_to(message, "prompt error")
 | 
				
			||||||
            bot.reply_to(message, "prompt error")
 | 
					 | 
				
			||||||
    except Exception as e:
 | 
					 | 
				
			||||||
        print(e)
 | 
					 | 
				
			||||||
        bot.reply_to(message, "sd3 error")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if SD_API_KEY and CHATGPT_API_KEY:
 | 
					if SD_API_KEY and settings.openai_api_key:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def register(bot: TeleBot) -> None:
 | 
					    def register(bot: TeleBot) -> None:
 | 
				
			||||||
        bot.register_message_handler(sd_handler, commands=["sd3"], pass_bot=True)
 | 
					        bot.register_message_handler(sd_handler, commands=["sd3"], pass_bot=True)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										139
									
								
								handlers/summary/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								handlers/summary/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,139 @@
 | 
				
			|||||||
 | 
					from __future__ import annotations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					from datetime import datetime, timezone
 | 
				
			||||||
 | 
					from functools import partial
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import telegramify_markdown
 | 
				
			||||||
 | 
					from telebot import TeleBot
 | 
				
			||||||
 | 
					from telebot.types import Message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from config import settings
 | 
				
			||||||
 | 
					from handlers._utils import non_llm_handler
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .messages import ChatMessage, MessageStore
 | 
				
			||||||
 | 
					from .utils import PROMPT, filter_message, parse_date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					logger = logging.getLogger("bot")
 | 
				
			||||||
 | 
					store = MessageStore("data/messages.db")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@non_llm_handler
 | 
				
			||||||
 | 
					def handle_message(message: Message):
 | 
				
			||||||
 | 
					    logger.debug(
 | 
				
			||||||
 | 
					        "Received message: %s, chat_id=%d, from=%s",
 | 
				
			||||||
 | 
					        message.text,
 | 
				
			||||||
 | 
					        message.chat.id,
 | 
				
			||||||
 | 
					        message.from_user.id,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    # 这里可以添加处理消息的逻辑
 | 
				
			||||||
 | 
					    store.add_message(
 | 
				
			||||||
 | 
					        ChatMessage(
 | 
				
			||||||
 | 
					            chat_id=message.chat.id,
 | 
				
			||||||
 | 
					            message_id=message.id,
 | 
				
			||||||
 | 
					            content=message.text or "",
 | 
				
			||||||
 | 
					            user_id=message.from_user.id,
 | 
				
			||||||
 | 
					            user_name=message.from_user.full_name,
 | 
				
			||||||
 | 
					            timestamp=datetime.fromtimestamp(message.date, tz=timezone.utc),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@non_llm_handler
 | 
				
			||||||
 | 
					def summary_command(message: Message, bot: TeleBot):
 | 
				
			||||||
 | 
					    """生成消息摘要。示例:/summary today; /summary 2d"""
 | 
				
			||||||
 | 
					    text_parts = message.text.split(maxsplit=1)
 | 
				
			||||||
 | 
					    if len(text_parts) < 2:
 | 
				
			||||||
 | 
					        date = "today"
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        date = text_parts[1].strip()
 | 
				
			||||||
 | 
					    since, now = parse_date(date, settings.timezone)
 | 
				
			||||||
 | 
					    messages = store.get_messages_since(message.chat.id, since)
 | 
				
			||||||
 | 
					    messages_text = "\n".join(
 | 
				
			||||||
 | 
					        f"{msg.timestamp.isoformat()} - @{msg.user_name}: {msg.content}"
 | 
				
			||||||
 | 
					        for msg in messages
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    if not messages_text:
 | 
				
			||||||
 | 
					        bot.reply_to(message, "没有找到指定时间范围内的历史消息。")
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    new_message = bot.reply_to(message, "正在生成摘要,请稍候...")
 | 
				
			||||||
 | 
					    response = settings.openai_client.chat.completions.create(
 | 
				
			||||||
 | 
					        model=settings.openai_model,
 | 
				
			||||||
 | 
					        messages=[
 | 
				
			||||||
 | 
					            {"role": "user", "content": PROMPT.format(messages=messages_text)},
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    reply_text = f"""*👇 前情提要 👇 \\({since.strftime("%Y/%m/%d %H:%M")} \\- {now.strftime("%Y/%m/%d %H:%M")}\\)*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{telegramify_markdown.convert(response.choices[0].message.content)}
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					    logger.debug("Generated summary:\n%s", reply_text)
 | 
				
			||||||
 | 
					    bot.edit_message_text(
 | 
				
			||||||
 | 
					        chat_id=new_message.chat.id,
 | 
				
			||||||
 | 
					        message_id=new_message.message_id,
 | 
				
			||||||
 | 
					        text=reply_text,
 | 
				
			||||||
 | 
					        parse_mode="MarkdownV2",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@non_llm_handler
 | 
				
			||||||
 | 
					def stats_command(message: Message, bot: TeleBot):
 | 
				
			||||||
 | 
					    """获取群组消息统计信息"""
 | 
				
			||||||
 | 
					    stats = store.get_stats(message.chat.id)
 | 
				
			||||||
 | 
					    if not stats:
 | 
				
			||||||
 | 
					        bot.reply_to(message, "没有找到任何统计信息。")
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    stats_text = "\n".join(
 | 
				
			||||||
 | 
					        f"{entry.date}: {entry.message_count} messages" for entry in stats
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    bot.reply_to(
 | 
				
			||||||
 | 
					        message,
 | 
				
			||||||
 | 
					        f"📊 群组消息统计信息:\n```\n{stats_text}\n```",
 | 
				
			||||||
 | 
					        parse_mode="MarkdownV2",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@non_llm_handler
 | 
				
			||||||
 | 
					def search_command(message: Message, bot: TeleBot):
 | 
				
			||||||
 | 
					    """搜索群组消息(示例:/search 关键词 [N])"""
 | 
				
			||||||
 | 
					    text_parts = message.text.split(maxsplit=2)
 | 
				
			||||||
 | 
					    if len(text_parts) < 2:
 | 
				
			||||||
 | 
					        bot.reply_to(message, "请提供要搜索的关键词。")
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    keyword = text_parts[1].strip()
 | 
				
			||||||
 | 
					    if len(text_parts) > 2 and text_parts[2].isdigit():
 | 
				
			||||||
 | 
					        limit = int(text_parts[2])
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        limit = 10
 | 
				
			||||||
 | 
					    messages = store.search_messages(message.chat.id, keyword, limit=limit)
 | 
				
			||||||
 | 
					    if not messages:
 | 
				
			||||||
 | 
					        bot.reply_to(message, "没有找到匹配的消息。")
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    chat_id = str(message.chat.id)
 | 
				
			||||||
 | 
					    if chat_id.startswith("-100"):
 | 
				
			||||||
 | 
					        chat_id = chat_id[4:]
 | 
				
			||||||
 | 
					    items = []
 | 
				
			||||||
 | 
					    for msg in messages:
 | 
				
			||||||
 | 
					        link = f"https://t.me/c/{chat_id}/{msg.message_id}"
 | 
				
			||||||
 | 
					        items.append(f"{link}\n```\n{msg.content}\n```")
 | 
				
			||||||
 | 
					    message_text = telegramify_markdown.convert("\n".join(items))
 | 
				
			||||||
 | 
					    bot.reply_to(
 | 
				
			||||||
 | 
					        message,
 | 
				
			||||||
 | 
					        f"🔍 *搜索结果(只显示前 {limit} 个):*\n{message_text}",
 | 
				
			||||||
 | 
					        parse_mode="MarkdownV2",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					load_priority = 5
 | 
				
			||||||
 | 
					if settings.openai_api_key:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def register(bot: TeleBot):
 | 
				
			||||||
 | 
					        """注册命令处理器"""
 | 
				
			||||||
 | 
					        bot.register_message_handler(
 | 
				
			||||||
 | 
					            summary_command, commands=["summary"], pass_bot=True
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        bot.register_message_handler(stats_command, commands=["stats"], pass_bot=True)
 | 
				
			||||||
 | 
					        bot.register_message_handler(search_command, commands=["search"], pass_bot=True)
 | 
				
			||||||
 | 
					        bot.register_message_handler(
 | 
				
			||||||
 | 
					            handle_message, func=partial(filter_message, bot=bot)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
							
								
								
									
										49
									
								
								handlers/summary/__main__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								handlers/summary/__main__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					from __future__ import annotations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import asyncio
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .messages import ChatMessage, MessageStore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async def fetch_messages(chat_id: int) -> None:
 | 
				
			||||||
 | 
					    from telethon import TelegramClient
 | 
				
			||||||
 | 
					    from telethon.tl.types import Message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    store = MessageStore("data/messages.db")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    api_id = int(os.getenv("TELEGRAM_API_ID"))
 | 
				
			||||||
 | 
					    api_hash = os.getenv("TELEGRAM_API_HASH")
 | 
				
			||||||
 | 
					    async with TelegramClient("test", api_id, api_hash) as client:
 | 
				
			||||||
 | 
					        assert isinstance(client, TelegramClient)
 | 
				
			||||||
 | 
					        with store.connect() as conn:
 | 
				
			||||||
 | 
					            async for message in client.iter_messages(chat_id, reverse=True):
 | 
				
			||||||
 | 
					                if not isinstance(message, Message) or not message.message:
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					                if not message.from_id:
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					                print(message.pretty_format(message))
 | 
				
			||||||
 | 
					                user = await client.get_entity(message.from_id)
 | 
				
			||||||
 | 
					                fullname = user.first_name
 | 
				
			||||||
 | 
					                if user.last_name:
 | 
				
			||||||
 | 
					                    fullname += f" {user.last_name}"
 | 
				
			||||||
 | 
					                store.add_message(
 | 
				
			||||||
 | 
					                    ChatMessage(
 | 
				
			||||||
 | 
					                        chat_id=chat_id,
 | 
				
			||||||
 | 
					                        message_id=message.id,
 | 
				
			||||||
 | 
					                        content=message.message,
 | 
				
			||||||
 | 
					                        user_id=message.from_id.user_id,
 | 
				
			||||||
 | 
					                        user_name=fullname,
 | 
				
			||||||
 | 
					                        timestamp=message.date,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    conn=conn,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
					    if len(sys.argv) != 2:
 | 
				
			||||||
 | 
					        print("Usage: python -m handlers.summary <chat_id>")
 | 
				
			||||||
 | 
					        sys.exit(1)
 | 
				
			||||||
 | 
					    chat_id = int(sys.argv[1])
 | 
				
			||||||
 | 
					    asyncio.run(fetch_messages(chat_id))  # 替换为实际的群组ID
 | 
				
			||||||
							
								
								
									
										164
									
								
								handlers/summary/messages.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								handlers/summary/messages.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,164 @@
 | 
				
			|||||||
 | 
					import os
 | 
				
			||||||
 | 
					import sqlite3
 | 
				
			||||||
 | 
					from dataclasses import dataclass
 | 
				
			||||||
 | 
					from datetime import datetime, timedelta, timezone
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclass(frozen=True)
 | 
				
			||||||
 | 
					class ChatMessage:
 | 
				
			||||||
 | 
					    chat_id: int
 | 
				
			||||||
 | 
					    message_id: int
 | 
				
			||||||
 | 
					    content: str
 | 
				
			||||||
 | 
					    user_id: int
 | 
				
			||||||
 | 
					    user_name: str
 | 
				
			||||||
 | 
					    timestamp: datetime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclass(frozen=True)
 | 
				
			||||||
 | 
					class StatsEntry:
 | 
				
			||||||
 | 
					    date: str
 | 
				
			||||||
 | 
					    message_count: int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MessageStore:
 | 
				
			||||||
 | 
					    def __init__(self, db_file: str):
 | 
				
			||||||
 | 
					        parent_folder = os.path.dirname(db_file)
 | 
				
			||||||
 | 
					        if not os.path.exists(parent_folder):
 | 
				
			||||||
 | 
					            os.makedirs(parent_folder)
 | 
				
			||||||
 | 
					        self._db_file = db_file
 | 
				
			||||||
 | 
					        self._init_db()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def connect(self) -> sqlite3.Connection:
 | 
				
			||||||
 | 
					        """Create a new database connection."""
 | 
				
			||||||
 | 
					        return sqlite3.connect(self._db_file)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _init_db(self):
 | 
				
			||||||
 | 
					        with self.connect() as conn:
 | 
				
			||||||
 | 
					            conn.execute(
 | 
				
			||||||
 | 
					                """
 | 
				
			||||||
 | 
					                CREATE TABLE IF NOT EXISTS messages (
 | 
				
			||||||
 | 
					                    chat_id INTEGER,
 | 
				
			||||||
 | 
					                    message_id INTEGER,
 | 
				
			||||||
 | 
					                    content TEXT,
 | 
				
			||||||
 | 
					                    user_id INTEGER,
 | 
				
			||||||
 | 
					                    user_name TEXT,
 | 
				
			||||||
 | 
					                    timestamp TEXT,
 | 
				
			||||||
 | 
					                    PRIMARY KEY (chat_id, message_id)
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            """
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            conn.execute(
 | 
				
			||||||
 | 
					                """
 | 
				
			||||||
 | 
					                CREATE INDEX IF NOT EXISTS idx_chat_timestamp ON messages (chat_id, timestamp);
 | 
				
			||||||
 | 
					            """
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            conn.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def add_message(
 | 
				
			||||||
 | 
					        self, message: ChatMessage, conn: sqlite3.Connection | None = None
 | 
				
			||||||
 | 
					    ) -> None:
 | 
				
			||||||
 | 
					        need_close = False
 | 
				
			||||||
 | 
					        if conn is None:
 | 
				
			||||||
 | 
					            conn = self.connect()
 | 
				
			||||||
 | 
					            need_close = True
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            conn.execute(
 | 
				
			||||||
 | 
					                """
 | 
				
			||||||
 | 
					                INSERT OR REPLACE INTO messages (chat_id, message_id, content, user_id, user_name, timestamp)
 | 
				
			||||||
 | 
					                VALUES (?, ?, ?, ?, ?, ?);
 | 
				
			||||||
 | 
					                """,
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    message.chat_id,
 | 
				
			||||||
 | 
					                    message.message_id,
 | 
				
			||||||
 | 
					                    message.content,
 | 
				
			||||||
 | 
					                    message.user_id,
 | 
				
			||||||
 | 
					                    message.user_name,
 | 
				
			||||||
 | 
					                    message.timestamp.isoformat(),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            self._clean_old_messages(message.chat_id, conn)
 | 
				
			||||||
 | 
					            conn.commit()
 | 
				
			||||||
 | 
					        finally:
 | 
				
			||||||
 | 
					            if need_close:
 | 
				
			||||||
 | 
					                conn.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_messages_since(self, chat_id: int, since: datetime) -> list[ChatMessage]:
 | 
				
			||||||
 | 
					        with self.connect() as conn:
 | 
				
			||||||
 | 
					            cursor = conn.cursor()
 | 
				
			||||||
 | 
					            cursor.execute(
 | 
				
			||||||
 | 
					                """
 | 
				
			||||||
 | 
					                SELECT chat_id, message_id, content, user_id, user_name, timestamp
 | 
				
			||||||
 | 
					                FROM messages
 | 
				
			||||||
 | 
					            WHERE chat_id = ? AND timestamp >= ?
 | 
				
			||||||
 | 
					            ORDER BY timestamp ASC;
 | 
				
			||||||
 | 
					        """,
 | 
				
			||||||
 | 
					                (chat_id, since.isoformat()),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            rows = cursor.fetchall()
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            ChatMessage(
 | 
				
			||||||
 | 
					                chat_id=row[0],
 | 
				
			||||||
 | 
					                message_id=row[1],
 | 
				
			||||||
 | 
					                content=row[2],
 | 
				
			||||||
 | 
					                user_id=row[3],
 | 
				
			||||||
 | 
					                user_name=row[4],
 | 
				
			||||||
 | 
					                timestamp=datetime.fromisoformat(row[5]),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            for row in rows
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_stats(self, chat_id: int) -> list[StatsEntry]:
 | 
				
			||||||
 | 
					        with self.connect() as conn:
 | 
				
			||||||
 | 
					            self._clean_old_messages(chat_id, conn)
 | 
				
			||||||
 | 
					            cursor = conn.cursor()
 | 
				
			||||||
 | 
					            cursor.execute(
 | 
				
			||||||
 | 
					                """
 | 
				
			||||||
 | 
					                SELECT DATE(timestamp), COUNT(*)
 | 
				
			||||||
 | 
					                FROM messages
 | 
				
			||||||
 | 
					                WHERE chat_id = ?
 | 
				
			||||||
 | 
					            GROUP BY DATE(timestamp)
 | 
				
			||||||
 | 
					            ORDER BY DATE(timestamp) ASC;
 | 
				
			||||||
 | 
					        """,
 | 
				
			||||||
 | 
					                (chat_id,),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            rows = cursor.fetchall()
 | 
				
			||||||
 | 
					        return [StatsEntry(date=row[0], message_count=row[1]) for row in rows]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def search_messages(
 | 
				
			||||||
 | 
					        self, chat_id: int, keyword: str, limit: int = 10
 | 
				
			||||||
 | 
					    ) -> list[ChatMessage]:
 | 
				
			||||||
 | 
					        # TODO: Fuzzy search with full-text search or similar
 | 
				
			||||||
 | 
					        with self.connect() as conn:
 | 
				
			||||||
 | 
					            cursor = conn.cursor()
 | 
				
			||||||
 | 
					            cursor.execute(
 | 
				
			||||||
 | 
					                """
 | 
				
			||||||
 | 
					                SELECT chat_id, message_id, content, user_id, user_name, timestamp
 | 
				
			||||||
 | 
					                FROM messages
 | 
				
			||||||
 | 
					                WHERE chat_id = ? AND content LIKE ?
 | 
				
			||||||
 | 
					                ORDER BY timestamp DESC
 | 
				
			||||||
 | 
					                LIMIT ?;
 | 
				
			||||||
 | 
					            """,
 | 
				
			||||||
 | 
					                (chat_id, f"%{keyword}%", limit),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            rows = cursor.fetchall()
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            ChatMessage(
 | 
				
			||||||
 | 
					                chat_id=row[0],
 | 
				
			||||||
 | 
					                message_id=row[1],
 | 
				
			||||||
 | 
					                content=row[2],
 | 
				
			||||||
 | 
					                user_id=row[3],
 | 
				
			||||||
 | 
					                user_name=row[4],
 | 
				
			||||||
 | 
					                timestamp=datetime.fromisoformat(row[5]),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            for row in rows
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _clean_old_messages(
 | 
				
			||||||
 | 
					        self, chat_id: int, conn: sqlite3.Connection, days: int = 7
 | 
				
			||||||
 | 
					    ) -> None:
 | 
				
			||||||
 | 
					        cursor = conn.cursor()
 | 
				
			||||||
 | 
					        threshold_date = datetime.now(tz=timezone.utc) - timedelta(days=days)
 | 
				
			||||||
 | 
					        cursor.execute(
 | 
				
			||||||
 | 
					            "DELETE FROM messages WHERE chat_id = ? AND timestamp < ?;",
 | 
				
			||||||
 | 
					            (chat_id, threshold_date.isoformat()),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
							
								
								
									
										48
									
								
								handlers/summary/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								handlers/summary/utils.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					import re
 | 
				
			||||||
 | 
					import zoneinfo
 | 
				
			||||||
 | 
					from datetime import datetime, timedelta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from telebot import TeleBot
 | 
				
			||||||
 | 
					from telebot.types import Message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PROMPT = """\
 | 
				
			||||||
 | 
					请将下面的聊天记录进行总结,包含讨论了哪些话题,有哪些亮点发言和主要观点。
 | 
				
			||||||
 | 
					引用用户名请加粗。直接返回内容即可,不要包含引导词和标题。
 | 
				
			||||||
 | 
					--- Messages Start ---
 | 
				
			||||||
 | 
					{messages}
 | 
				
			||||||
 | 
					--- Messages End ---
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def filter_message(message: Message, bot: TeleBot) -> bool:
 | 
				
			||||||
 | 
					    """过滤消息,排除非文本消息和命令消息"""
 | 
				
			||||||
 | 
					    if not message.text:
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					    if not message.from_user:
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					    if message.from_user.id == bot.get_me().id:
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					    if message.text.startswith("/"):
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					    return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					date_regex = re.compile(r"^(\d+)([dhm])$")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def parse_date(date_str: str, locale: str) -> tuple[datetime, datetime]:
 | 
				
			||||||
 | 
					    date_str = date_str.strip().lower()
 | 
				
			||||||
 | 
					    now = datetime.now(tz=zoneinfo.ZoneInfo(locale))
 | 
				
			||||||
 | 
					    if date_str == "today":
 | 
				
			||||||
 | 
					        return now.replace(hour=0, minute=0, second=0, microsecond=0), now
 | 
				
			||||||
 | 
					    elif m := date_regex.match(date_str):
 | 
				
			||||||
 | 
					        number = int(m.group(1))
 | 
				
			||||||
 | 
					        unit = m.group(2)
 | 
				
			||||||
 | 
					        match unit:
 | 
				
			||||||
 | 
					            case "d":
 | 
				
			||||||
 | 
					                return now - timedelta(days=number), now
 | 
				
			||||||
 | 
					            case "h":
 | 
				
			||||||
 | 
					                return now - timedelta(hours=number), now
 | 
				
			||||||
 | 
					            case "m":
 | 
				
			||||||
 | 
					                return now - timedelta(minutes=number), now
 | 
				
			||||||
 | 
					    raise ValueError(f"Unsupported date format: {date_str}")
 | 
				
			||||||
@ -1,8 +1,8 @@
 | 
				
			|||||||
from urlextract import URLExtract
 | 
					 | 
				
			||||||
from telebot import TeleBot
 | 
					from telebot import TeleBot
 | 
				
			||||||
from telebot.types import Message
 | 
					from telebot.types import Message
 | 
				
			||||||
 | 
					from urlextract import URLExtract
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import *
 | 
					from ._utils import bot_reply_first, bot_reply_markdown
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def tweet_handler(message: Message, bot: TeleBot):
 | 
					def tweet_handler(message: Message, bot: TeleBot):
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1147
									
								
								handlers/useful.py
									
									
									
									
									
								
							
							
						
						
									
										1147
									
								
								handlers/useful.py
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,5 +1,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[project]
 | 
					[project]
 | 
				
			||||||
 | 
					name = "tg_bot_collections"
 | 
				
			||||||
# PEP 621 project metadata
 | 
					# PEP 621 project metadata
 | 
				
			||||||
# See https://www.python.org/dev/peps/pep-0621/
 | 
					# See https://www.python.org/dev/peps/pep-0621/
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
@ -17,11 +18,20 @@ dependencies = [
 | 
				
			|||||||
    "groq",
 | 
					    "groq",
 | 
				
			||||||
    "together>=1.1.5",
 | 
					    "together>=1.1.5",
 | 
				
			||||||
    "dify-client>=0.1.10",
 | 
					    "dify-client>=0.1.10",
 | 
				
			||||||
    "chattts-fork>=0.0.1",
 | 
					 | 
				
			||||||
    "expiringdict>=1.2.2",
 | 
					    "expiringdict>=1.2.2",
 | 
				
			||||||
    "beautifulsoup4>=4.12.3",
 | 
					    "beautifulsoup4>=4.12.3",
 | 
				
			||||||
    "Markdown>=3.6",
 | 
					    "Markdown>=3.6",
 | 
				
			||||||
    "cohere>=5.5.8",
 | 
					    "cohere>=5.5.8",
 | 
				
			||||||
    "kling-creator>=0.0.3",
 | 
					    "kling-creator>=0.0.3",
 | 
				
			||||||
 | 
					    "pydantic-settings>=2.10.1",
 | 
				
			||||||
 | 
					    "pydantic>=2.11.7",
 | 
				
			||||||
 | 
					    "telethon>=1.40.0",
 | 
				
			||||||
 | 
					    "pysocks>=1.7.1",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
requires-python = ">=3.10"
 | 
					requires-python = ">=3.10"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[tool.pdm]
 | 
				
			||||||
 | 
					distribution = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[tool.pdm.scripts]
 | 
				
			||||||
 | 
					dev = "python tg.py --debug"
 | 
				
			||||||
@ -5,7 +5,6 @@ aiohttp==3.9.5
 | 
				
			|||||||
aiosignal==1.3.1
 | 
					aiosignal==1.3.1
 | 
				
			||||||
annotated-types==0.6.0
 | 
					annotated-types==0.6.0
 | 
				
			||||||
anthropic==0.32.0
 | 
					anthropic==0.32.0
 | 
				
			||||||
antlr4-python3-runtime==4.9.3
 | 
					 | 
				
			||||||
anyio==4.3.0
 | 
					anyio==4.3.0
 | 
				
			||||||
async-timeout==4.0.3; python_version < "3.11"
 | 
					async-timeout==4.0.3; python_version < "3.11"
 | 
				
			||||||
attrs==23.2.0
 | 
					attrs==23.2.0
 | 
				
			||||||
@ -18,7 +17,6 @@ cairosvg==2.7.1
 | 
				
			|||||||
certifi==2024.2.2
 | 
					certifi==2024.2.2
 | 
				
			||||||
cffi==1.16.0
 | 
					cffi==1.16.0
 | 
				
			||||||
charset-normalizer==3.3.2
 | 
					charset-normalizer==3.3.2
 | 
				
			||||||
chattts-fork==0.0.8
 | 
					 | 
				
			||||||
click==8.1.7
 | 
					click==8.1.7
 | 
				
			||||||
click-plugins==1.1.1
 | 
					click-plugins==1.1.1
 | 
				
			||||||
cligj==0.7.2
 | 
					cligj==0.7.2
 | 
				
			||||||
@ -31,10 +29,7 @@ cycler==0.12.1
 | 
				
			|||||||
defusedxml==0.7.1
 | 
					defusedxml==0.7.1
 | 
				
			||||||
dify-client==0.1.10
 | 
					dify-client==0.1.10
 | 
				
			||||||
distro==1.9.0
 | 
					distro==1.9.0
 | 
				
			||||||
einops==0.8.0
 | 
					 | 
				
			||||||
einx==0.2.2
 | 
					 | 
				
			||||||
emoji==2.11.1
 | 
					emoji==2.11.1
 | 
				
			||||||
encodec==0.1.1
 | 
					 | 
				
			||||||
eval-type-backport==0.2.0
 | 
					eval-type-backport==0.2.0
 | 
				
			||||||
exceptiongroup==1.2.1; python_version < "3.11"
 | 
					exceptiongroup==1.2.1; python_version < "3.11"
 | 
				
			||||||
expiringdict==1.2.2
 | 
					expiringdict==1.2.2
 | 
				
			||||||
@ -43,13 +38,12 @@ fastavro==1.9.4
 | 
				
			|||||||
filelock==3.14.0
 | 
					filelock==3.14.0
 | 
				
			||||||
fiona==1.9.6
 | 
					fiona==1.9.6
 | 
				
			||||||
fonttools==4.51.0
 | 
					fonttools==4.51.0
 | 
				
			||||||
frozendict==2.4.4
 | 
					 | 
				
			||||||
frozenlist==1.4.1
 | 
					frozenlist==1.4.1
 | 
				
			||||||
fsspec==2024.3.1
 | 
					fsspec==2024.3.1
 | 
				
			||||||
geopandas==0.14.4
 | 
					geopandas==0.14.4
 | 
				
			||||||
github-poster==2.7.4
 | 
					github-poster==2.7.4
 | 
				
			||||||
google-ai-generativelanguage==0.6.6
 | 
					google-ai-generativelanguage==0.6.6
 | 
				
			||||||
google-api-core==2.19.0
 | 
					google-api-core[grpc]==2.19.0
 | 
				
			||||||
google-api-python-client==2.128.0
 | 
					google-api-python-client==2.128.0
 | 
				
			||||||
google-auth==2.29.0
 | 
					google-auth==2.29.0
 | 
				
			||||||
google-auth-httplib2==0.2.0
 | 
					google-auth-httplib2==0.2.0
 | 
				
			||||||
@ -65,36 +59,18 @@ httpx==0.27.0
 | 
				
			|||||||
httpx-sse==0.4.0
 | 
					httpx-sse==0.4.0
 | 
				
			||||||
huggingface-hub==0.23.0
 | 
					huggingface-hub==0.23.0
 | 
				
			||||||
idna==3.7
 | 
					idna==3.7
 | 
				
			||||||
intel-openmp==2021.4.0; platform_system == "Windows"
 | 
					 | 
				
			||||||
jinja2==3.1.4
 | 
					 | 
				
			||||||
jiter==0.5.0
 | 
					jiter==0.5.0
 | 
				
			||||||
jmespath==1.0.1
 | 
					jmespath==1.0.1
 | 
				
			||||||
kiwisolver==1.4.5
 | 
					kiwisolver==1.4.5
 | 
				
			||||||
kling-creator==0.3.0
 | 
					kling-creator==0.3.0
 | 
				
			||||||
markdown==3.6
 | 
					markdown==3.6
 | 
				
			||||||
markdown-it-py==3.0.0
 | 
					markdown-it-py==3.0.0
 | 
				
			||||||
markupsafe==2.1.5
 | 
					 | 
				
			||||||
matplotlib==3.8.4
 | 
					matplotlib==3.8.4
 | 
				
			||||||
mdurl==0.1.2
 | 
					mdurl==0.1.2
 | 
				
			||||||
mistletoe==1.4.0
 | 
					mistletoe==1.4.0
 | 
				
			||||||
mkl==2021.4.0; platform_system == "Windows"
 | 
					 | 
				
			||||||
mpmath==1.3.0
 | 
					 | 
				
			||||||
multidict==6.0.5
 | 
					multidict==6.0.5
 | 
				
			||||||
networkx==3.3
 | 
					networkx==3.3
 | 
				
			||||||
numpy==1.26.4
 | 
					numpy==1.26.4
 | 
				
			||||||
nvidia-cublas-cu12==12.1.3.1; platform_system == "Linux" and platform_machine == "x86_64"
 | 
					 | 
				
			||||||
nvidia-cuda-cupti-cu12==12.1.105; platform_system == "Linux" and platform_machine == "x86_64"
 | 
					 | 
				
			||||||
nvidia-cuda-nvrtc-cu12==12.1.105; platform_system == "Linux" and platform_machine == "x86_64"
 | 
					 | 
				
			||||||
nvidia-cuda-runtime-cu12==12.1.105; platform_system == "Linux" and platform_machine == "x86_64"
 | 
					 | 
				
			||||||
nvidia-cudnn-cu12==8.9.2.26; platform_system == "Linux" and platform_machine == "x86_64"
 | 
					 | 
				
			||||||
nvidia-cufft-cu12==11.0.2.54; platform_system == "Linux" and platform_machine == "x86_64"
 | 
					 | 
				
			||||||
nvidia-curand-cu12==10.3.2.106; platform_system == "Linux" and platform_machine == "x86_64"
 | 
					 | 
				
			||||||
nvidia-cusolver-cu12==11.4.5.107; platform_system == "Linux" and platform_machine == "x86_64"
 | 
					 | 
				
			||||||
nvidia-cusparse-cu12==12.1.0.106; platform_system == "Linux" and platform_machine == "x86_64"
 | 
					 | 
				
			||||||
nvidia-nccl-cu12==2.20.5; platform_system == "Linux" and platform_machine == "x86_64"
 | 
					 | 
				
			||||||
nvidia-nvjitlink-cu12==12.5.40; platform_system == "Linux" and platform_machine == "x86_64"
 | 
					 | 
				
			||||||
nvidia-nvtx-cu12==12.1.105; platform_system == "Linux" and platform_machine == "x86_64"
 | 
					 | 
				
			||||||
omegaconf==2.3.0
 | 
					 | 
				
			||||||
openai==1.37.2
 | 
					openai==1.37.2
 | 
				
			||||||
osmnx==1.9.2
 | 
					osmnx==1.9.2
 | 
				
			||||||
packaging==24.0
 | 
					packaging==24.0
 | 
				
			||||||
@ -106,55 +82,50 @@ platformdirs==4.2.1
 | 
				
			|||||||
prettymapp==0.3.0
 | 
					prettymapp==0.3.0
 | 
				
			||||||
proto-plus==1.23.0
 | 
					proto-plus==1.23.0
 | 
				
			||||||
protobuf==4.25.3
 | 
					protobuf==4.25.3
 | 
				
			||||||
 | 
					pyaes==1.6.1
 | 
				
			||||||
pyarrow==16.0.0
 | 
					pyarrow==16.0.0
 | 
				
			||||||
pyasn1==0.6.0
 | 
					pyasn1==0.6.0
 | 
				
			||||||
pyasn1-modules==0.4.0
 | 
					pyasn1-modules==0.4.0
 | 
				
			||||||
pycparser==2.22
 | 
					pycparser==2.22
 | 
				
			||||||
pydantic==2.7.1
 | 
					pydantic==2.11.7
 | 
				
			||||||
pydantic-core==2.18.2
 | 
					pydantic-core==2.33.2
 | 
				
			||||||
 | 
					pydantic-settings==2.10.1
 | 
				
			||||||
pygments==2.18.0
 | 
					pygments==2.18.0
 | 
				
			||||||
pyogrio==0.7.2
 | 
					pyogrio==0.7.2
 | 
				
			||||||
pyparsing==3.1.2
 | 
					pyparsing==3.1.2
 | 
				
			||||||
pyproj==3.6.1
 | 
					pyproj==3.6.1
 | 
				
			||||||
 | 
					pysocks==1.7.1
 | 
				
			||||||
pytelegrambotapi==4.21.0
 | 
					pytelegrambotapi==4.21.0
 | 
				
			||||||
python-dateutil==2.9.0.post0
 | 
					python-dateutil==2.9.0.post0
 | 
				
			||||||
 | 
					python-dotenv==1.1.1
 | 
				
			||||||
pytz==2024.1
 | 
					pytz==2024.1
 | 
				
			||||||
pyyaml==6.0.1
 | 
					pyyaml==6.0.1
 | 
				
			||||||
regex==2024.5.15
 | 
					 | 
				
			||||||
requests==2.32.3
 | 
					requests==2.32.3
 | 
				
			||||||
rich==13.7.1
 | 
					rich==13.7.1
 | 
				
			||||||
rsa==4.9
 | 
					rsa==4.9
 | 
				
			||||||
s3transfer==0.10.2
 | 
					s3transfer==0.10.2
 | 
				
			||||||
safetensors==0.4.3
 | 
					 | 
				
			||||||
scipy==1.13.1
 | 
					 | 
				
			||||||
shapely==2.0.4
 | 
					shapely==2.0.4
 | 
				
			||||||
shellingham==1.5.4
 | 
					shellingham==1.5.4
 | 
				
			||||||
six==1.16.0
 | 
					six==1.16.0
 | 
				
			||||||
sniffio==1.3.1
 | 
					sniffio==1.3.1
 | 
				
			||||||
soupsieve==2.5
 | 
					soupsieve==2.5
 | 
				
			||||||
svgwrite==1.4.3
 | 
					svgwrite==1.4.3
 | 
				
			||||||
sympy==1.12
 | 
					 | 
				
			||||||
tabulate==0.9.0
 | 
					tabulate==0.9.0
 | 
				
			||||||
tbb==2021.12.0; platform_system == "Windows"
 | 
					 | 
				
			||||||
telegramify-markdown==0.1.9
 | 
					telegramify-markdown==0.1.9
 | 
				
			||||||
 | 
					telethon==1.40.0
 | 
				
			||||||
time-machine==2.14.1; implementation_name != "pypy"
 | 
					time-machine==2.14.1; implementation_name != "pypy"
 | 
				
			||||||
tinycss2==1.3.0
 | 
					tinycss2==1.3.0
 | 
				
			||||||
together==1.2.5
 | 
					together==1.2.5
 | 
				
			||||||
tokenizers==0.19.1
 | 
					tokenizers==0.19.1
 | 
				
			||||||
torch==2.3.0
 | 
					 | 
				
			||||||
torchaudio==2.3.0
 | 
					 | 
				
			||||||
tqdm==4.66.4
 | 
					tqdm==4.66.4
 | 
				
			||||||
transformers==4.41.1
 | 
					 | 
				
			||||||
triton==2.3.0; platform_system == "Linux" and platform_machine == "x86_64" and python_version < "3.12"
 | 
					 | 
				
			||||||
typer==0.12.3
 | 
					typer==0.12.3
 | 
				
			||||||
types-requests==2.32.0.20240622
 | 
					types-requests==2.32.0.20240622
 | 
				
			||||||
typing-extensions==4.11.0
 | 
					typing-extensions==4.14.1
 | 
				
			||||||
 | 
					typing-inspection==0.4.1
 | 
				
			||||||
tzdata==2024.1
 | 
					tzdata==2024.1
 | 
				
			||||||
uritemplate==4.1.1
 | 
					uritemplate==4.1.1
 | 
				
			||||||
uritools==4.0.2
 | 
					uritools==4.0.2
 | 
				
			||||||
urlextract==1.9.0
 | 
					urlextract==1.9.0
 | 
				
			||||||
urllib3==2.2.1
 | 
					urllib3==2.2.1
 | 
				
			||||||
vector-quantize-pytorch==1.14.24
 | 
					 | 
				
			||||||
vocos==0.1.0
 | 
					 | 
				
			||||||
webencodings==0.5.1
 | 
					webencodings==0.5.1
 | 
				
			||||||
yarl==1.9.4
 | 
					yarl==1.9.4
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										20
									
								
								setup.sh
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								setup.sh
									
									
									
									
									
								
							@ -7,20 +7,20 @@ service_name="tgbotyh"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
source .env
 | 
					source .env
 | 
				
			||||||
 | 
					
 | 
				
			||||||
google_gemini_api_key="${Google_Gemini_API_Key}"
 | 
					google_gemini_api_key="${GOOGLE_GEMINI_API_KEY}"
 | 
				
			||||||
telegram_bot_token="${Telegram_Bot_Token}"
 | 
					telegram_bot_token="${TELEGRAM_BOT_TOKEN}"
 | 
				
			||||||
anthropic_api_key="${Anthropic_API_Key}"
 | 
					anthropic_api_key="${ANTHROPIC_API_KEY}"
 | 
				
			||||||
openai_api_key="${Openai_API_Key}"
 | 
					openai_api_key="${OPENAI_API_KEY}"
 | 
				
			||||||
yi_api_key="${Yi_API_Key}"
 | 
					yi_api_key="${YI_API_KEY}"
 | 
				
			||||||
yi_base_url="${Yi_Base_Url}"
 | 
					yi_base_url="${YI_BASE_URL}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if [ -n "$Python_Bin_Path" ]; then
 | 
					if [ -n "$PYTHON_BIN_PATH" ]; then
 | 
				
			||||||
    python_bin_path="$Python_Bin_Path"
 | 
					    python_bin_path="$PYTHON_BIN_PATH"
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if [ -n "$Python_Venv_Path" ]; then
 | 
					if [ -n "$PYTHON_VENV_PATH" ]; then
 | 
				
			||||||
    venv_dir="${Python_Venv_Path}"
 | 
					    venv_dir="${PYTHON_VENV_PATH}"
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
sudoCmd=""
 | 
					sudoCmd=""
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										29
									
								
								tg.py
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								tg.py
									
									
									
									
									
								
							@ -1,13 +1,34 @@
 | 
				
			|||||||
import argparse
 | 
					import argparse
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from telebot import TeleBot
 | 
					from telebot import TeleBot
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from config import settings
 | 
				
			||||||
from handlers import list_available_commands, load_handlers
 | 
					from handlers import list_available_commands, load_handlers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					logger = logging.getLogger("bot")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def setup_logging(debug: bool):
 | 
				
			||||||
 | 
					    logger.setLevel(logging.DEBUG if debug else logging.INFO)
 | 
				
			||||||
 | 
					    handler = logging.StreamHandler()
 | 
				
			||||||
 | 
					    handler.setFormatter(
 | 
				
			||||||
 | 
					        logging.Formatter(
 | 
				
			||||||
 | 
					            "%(asctime)s - [%(levelname)s] - %(filename)s:%(lineno)d - %(message)s"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    logger.addHandler(handler)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def main():
 | 
					def main():
 | 
				
			||||||
    # Init args
 | 
					    # Init args
 | 
				
			||||||
    parser = argparse.ArgumentParser()
 | 
					    parser = argparse.ArgumentParser()
 | 
				
			||||||
    parser.add_argument("tg_token", help="tg token")
 | 
					    parser.add_argument(
 | 
				
			||||||
 | 
					        "tg_token", help="tg token", default=settings.telegram_bot_token, nargs="?"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    parser.add_argument(
 | 
				
			||||||
 | 
					        "--debug", "--verbose", "-v", action="store_true", help="Enable debug mode"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # 'disable-command' option
 | 
					    # 'disable-command' option
 | 
				
			||||||
    # The action 'append' will allow multiple entries to be saved into a list
 | 
					    # The action 'append' will allow multiple entries to be saved into a list
 | 
				
			||||||
@ -22,15 +43,15 @@ def main():
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    options = parser.parse_args()
 | 
					    options = parser.parse_args()
 | 
				
			||||||
    print("Arg parse done.")
 | 
					    setup_logging(options.debug)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Init bot
 | 
					    # Init bot
 | 
				
			||||||
    bot = TeleBot(options.tg_token)
 | 
					    bot = TeleBot(options.tg_token)
 | 
				
			||||||
    load_handlers(bot, options.disable_commands)
 | 
					    load_handlers(bot, options.disable_commands)
 | 
				
			||||||
    print("Bot init done.")
 | 
					    logger.info("Bot init done.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Start bot
 | 
					    # Start bot
 | 
				
			||||||
    print("Starting tg collections bot.")
 | 
					    logger.info("Starting tg collections bot.")
 | 
				
			||||||
    bot.infinity_polling(timeout=10, long_polling_timeout=5)
 | 
					    bot.infinity_polling(timeout=10, long_polling_timeout=5)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user