mirror of
https://github.com/cdryzun/tg_bot_collections.git
synced 2025-08-04 04:36:42 +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