chore: initial commit
This commit is contained in:
508
integrated_bot.py
Normal file
508
integrated_bot.py
Normal file
@@ -0,0 +1,508 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
整合版客服机器人 - 包含镜像搜索功能
|
||||
修复了事件循环冲突问题
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
import os
|
||||
import httpx
|
||||
from typing import Dict, Optional
|
||||
from datetime import datetime
|
||||
|
||||
# Pyrogram imports
|
||||
from pyrogram import Client as PyrogramClient, filters
|
||||
from pyrogram.types import Message as PyrogramMessage
|
||||
from pyrogram.raw.functions.messages import GetBotCallbackAnswer
|
||||
|
||||
# Telegram Bot imports
|
||||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters as tg_filters
|
||||
from telegram.ext import ContextTypes
|
||||
|
||||
# 移除src依赖,使用环境变量
|
||||
|
||||
# ================== 配置 ==================
|
||||
API_ID = 24660516
|
||||
API_HASH = "eae564578880a59c9963916ff1bbbd3a"
|
||||
SESSION_NAME = "user_session"
|
||||
BOT_TOKEN = "8426529617:AAHAxzohSMFBAxInzbAVJsZfkB5bHnOyFC4"
|
||||
TARGET_BOT = "@openaiw_bot"
|
||||
ADMIN_ID = 7363537082
|
||||
|
||||
# 搜索命令列表
|
||||
SEARCH_COMMANDS = ['/topchat', '/search', '/text', '/human']
|
||||
|
||||
# 日志配置
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IntegratedBot:
|
||||
"""整合的客服机器人 - 包含镜像搜索功能"""
|
||||
|
||||
def __init__(self):
|
||||
# 直接使用常量配置,不依赖Settings类
|
||||
|
||||
# Bot应用
|
||||
self.app = None
|
||||
|
||||
# Pyrogram客户端(用于镜像)
|
||||
self.pyrogram_client: Optional[PyrogramClient] = None
|
||||
self.target_bot_id: Optional[int] = None
|
||||
|
||||
# 消息映射
|
||||
self.pyrogram_to_telegram = {} # pyrogram_msg_id -> telegram_msg_id
|
||||
self.telegram_to_pyrogram = {} # telegram_msg_id -> pyrogram_msg_id
|
||||
self.callback_data_map = {} # telegram_callback_id -> (pyrogram_msg_id, original_callback_data)
|
||||
self.user_search_sessions = {} # user_id -> search_session_info
|
||||
|
||||
async def setup_pyrogram(self):
|
||||
"""设置Pyrogram客户端用于镜像"""
|
||||
try:
|
||||
# 检查是否需要使用代理
|
||||
proxy_config = None
|
||||
if os.environ.get('ALL_PROXY'):
|
||||
proxy_url = os.environ.get('ALL_PROXY', '').replace('socks5://', '')
|
||||
if proxy_url:
|
||||
host, port = proxy_url.split(':')
|
||||
proxy_config = {
|
||||
"scheme": "socks5",
|
||||
"hostname": host,
|
||||
"port": int(port)
|
||||
}
|
||||
logger.info(f"使用代理: {host}:{port}")
|
||||
|
||||
self.pyrogram_client = PyrogramClient(
|
||||
SESSION_NAME,
|
||||
api_id=API_ID,
|
||||
api_hash=API_HASH,
|
||||
proxy=proxy_config if proxy_config else None
|
||||
)
|
||||
|
||||
await self.pyrogram_client.start()
|
||||
logger.info("✅ Pyrogram客户端已启动")
|
||||
|
||||
# 获取目标机器人信息
|
||||
target = await self.pyrogram_client.get_users(TARGET_BOT)
|
||||
self.target_bot_id = target.id
|
||||
logger.info(f"✅ 已连接到搜索机器人: {target.username} (ID: {target.id})")
|
||||
|
||||
# 设置消息监听器
|
||||
@self.pyrogram_client.on_message(filters.user(self.target_bot_id))
|
||||
async def on_bot_response(_, message: PyrogramMessage):
|
||||
await self.handle_search_response(message)
|
||||
|
||||
@self.pyrogram_client.on_edited_message(filters.user(self.target_bot_id))
|
||||
async def on_message_edited(_, message: PyrogramMessage):
|
||||
await self.handle_search_response(message, is_edit=True)
|
||||
|
||||
logger.info("✅ 搜索监听器已设置")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Pyrogram设置失败: {e}")
|
||||
return False
|
||||
|
||||
async def handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""处理/start命令"""
|
||||
user = update.effective_user
|
||||
welcome_text = (
|
||||
f"👋 您好 {user.first_name}!\n\n"
|
||||
"暂时支持的搜索指令:\n\n"
|
||||
"- 群组目录 /topchat\n"
|
||||
"- 群组搜索 /search\n"
|
||||
"- 按消息文本搜索 /text\n"
|
||||
"- 按名称搜索 /human\n\n"
|
||||
"您可以使用以上指令进行搜索,或直接发送消息联系客服。"
|
||||
)
|
||||
await update.message.reply_text(welcome_text)
|
||||
|
||||
# 通知管理员有新用户访问
|
||||
admin_notification = (
|
||||
f"🆕 新用户访问:\n"
|
||||
f"👤 姓名: {user.first_name} {user.last_name or ''}\n"
|
||||
f"🆔 ID: {user.id}\n"
|
||||
f"👤 用户名: @{user.username or '无'}\n"
|
||||
f"📱 命令: /start\n"
|
||||
f"⏰ 时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
)
|
||||
|
||||
await context.bot.send_message(
|
||||
chat_id=ADMIN_ID,
|
||||
text=admin_notification
|
||||
)
|
||||
|
||||
logger.info(f"新用户访问 /start: {user.id} ({user.first_name})")
|
||||
|
||||
async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""处理所有消息"""
|
||||
if not update.message or not update.message.text:
|
||||
return
|
||||
|
||||
user = update.effective_user
|
||||
text = update.message.text
|
||||
is_admin = user.id == ADMIN_ID
|
||||
|
||||
# 管理员回复逻辑
|
||||
if is_admin and update.message.reply_to_message:
|
||||
await self.handle_admin_reply(update, context)
|
||||
return
|
||||
|
||||
# 搜索命令处理
|
||||
if self.is_search_command(text):
|
||||
await self.handle_search_command(update, context)
|
||||
return
|
||||
|
||||
# 普通客服消息转发
|
||||
await self.forward_to_admin(update, context)
|
||||
|
||||
def is_search_command(self, text: str) -> bool:
|
||||
"""检查是否是搜索命令"""
|
||||
if not text:
|
||||
return False
|
||||
command = text.split()[0]
|
||||
return command in SEARCH_COMMANDS
|
||||
|
||||
async def handle_search_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""处理搜索命令 - 通过Pyrogram转发"""
|
||||
user = update.effective_user
|
||||
user_id = user.id
|
||||
command = update.message.text
|
||||
|
||||
try:
|
||||
# 通知管理员有用户执行搜索
|
||||
admin_notification = (
|
||||
f"🔍 用户执行搜索:\n"
|
||||
f"👤 姓名: {user.first_name} {user.last_name or ''}\n"
|
||||
f"🆔 ID: {user_id}\n"
|
||||
f"👤 用户名: @{user.username or '无'}\n"
|
||||
f"📝 搜索内容: {command}\n"
|
||||
f"⏰ 时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
)
|
||||
|
||||
await context.bot.send_message(
|
||||
chat_id=ADMIN_ID,
|
||||
text=admin_notification
|
||||
)
|
||||
|
||||
# 发送等待消息
|
||||
wait_msg = await update.message.reply_text("🔍 正在搜索,请稍候...")
|
||||
|
||||
# 记录搜索会话
|
||||
self.user_search_sessions[user_id] = {
|
||||
'chat_id': update.effective_chat.id,
|
||||
'wait_msg_id': wait_msg.message_id,
|
||||
'command': command,
|
||||
'timestamp': datetime.now()
|
||||
}
|
||||
|
||||
# 通过Pyrogram发送到搜索机器人
|
||||
await self.pyrogram_client.send_message(self.target_bot_id, command)
|
||||
logger.info(f"用户 {user.first_name}({user_id}) 执行搜索: {command}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"搜索命令处理失败: {e}")
|
||||
await update.message.reply_text("❌ 搜索失败,请稍后重试")
|
||||
|
||||
async def handle_search_response(self, message: PyrogramMessage, is_edit: bool = False):
|
||||
"""处理搜索机器人的响应"""
|
||||
try:
|
||||
# 查找最近的搜索请求
|
||||
if not self.user_search_sessions:
|
||||
return
|
||||
|
||||
# 获取最近的请求用户
|
||||
user_id = max(
|
||||
self.user_search_sessions.keys(),
|
||||
key=lambda k: self.user_search_sessions[k]['timestamp']
|
||||
)
|
||||
|
||||
session = self.user_search_sessions[user_id]
|
||||
|
||||
# 提取消息内容
|
||||
text = message.text or message.caption or "无结果"
|
||||
|
||||
# 处理HTML格式
|
||||
try:
|
||||
if message.text and hasattr(message.text, 'html'):
|
||||
text = message.text.html
|
||||
except:
|
||||
pass
|
||||
|
||||
# 转换键盘
|
||||
keyboard = self.convert_keyboard(message)
|
||||
|
||||
# 更新或发送消息
|
||||
if is_edit and message.id in self.pyrogram_to_telegram:
|
||||
# 编辑现有消息
|
||||
telegram_msg_id = self.pyrogram_to_telegram[message.id]
|
||||
await self.app.bot.edit_message_text(
|
||||
chat_id=session['chat_id'],
|
||||
message_id=telegram_msg_id,
|
||||
text=text[:4000],
|
||||
reply_markup=keyboard,
|
||||
parse_mode='HTML'
|
||||
)
|
||||
else:
|
||||
# 删除等待消息,发送新消息
|
||||
try:
|
||||
await self.app.bot.delete_message(
|
||||
chat_id=session['chat_id'],
|
||||
message_id=session['wait_msg_id']
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
sent = await self.app.bot.send_message(
|
||||
chat_id=session['chat_id'],
|
||||
text=text[:4000],
|
||||
reply_markup=keyboard,
|
||||
parse_mode='HTML'
|
||||
)
|
||||
|
||||
# 记录映射
|
||||
self.pyrogram_to_telegram[message.id] = sent.message_id
|
||||
self.telegram_to_pyrogram[sent.message_id] = message.id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理搜索响应失败: {e}")
|
||||
|
||||
def convert_keyboard(self, message: PyrogramMessage) -> Optional[InlineKeyboardMarkup]:
|
||||
"""转换Pyrogram键盘为Telegram键盘"""
|
||||
if not message.reply_markup or not message.reply_markup.inline_keyboard:
|
||||
return None
|
||||
|
||||
try:
|
||||
buttons = []
|
||||
for row in message.reply_markup.inline_keyboard:
|
||||
button_row = []
|
||||
for btn in row:
|
||||
if btn.url:
|
||||
button_row.append(InlineKeyboardButton(
|
||||
text=btn.text,
|
||||
url=btn.url
|
||||
))
|
||||
elif btn.callback_data:
|
||||
# 创建callback ID
|
||||
callback_id = f"cb_{time.time():.0f}_{len(self.callback_data_map)}"
|
||||
self.callback_data_map[callback_id] = (
|
||||
message.id,
|
||||
btn.callback_data
|
||||
)
|
||||
|
||||
button_row.append(InlineKeyboardButton(
|
||||
text=btn.text,
|
||||
callback_data=callback_id[:64]
|
||||
))
|
||||
|
||||
if button_row:
|
||||
buttons.append(button_row)
|
||||
|
||||
return InlineKeyboardMarkup(buttons) if buttons else None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"键盘转换失败: {e}")
|
||||
return None
|
||||
|
||||
async def handle_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""处理回调查询(翻页等)"""
|
||||
query = update.callback_query
|
||||
callback_id = query.data
|
||||
|
||||
await query.answer("正在加载...")
|
||||
|
||||
if callback_id not in self.callback_data_map:
|
||||
await query.answer("按钮已过期", show_alert=True)
|
||||
return
|
||||
|
||||
pyrogram_msg_id, original_callback = self.callback_data_map[callback_id]
|
||||
|
||||
try:
|
||||
# 准备callback数据
|
||||
if not isinstance(original_callback, bytes):
|
||||
original_callback = original_callback.encode() if original_callback else b''
|
||||
|
||||
# 调用原始callback
|
||||
result = await self.pyrogram_client.invoke(
|
||||
GetBotCallbackAnswer(
|
||||
peer=await self.pyrogram_client.resolve_peer(self.target_bot_id),
|
||||
msg_id=pyrogram_msg_id,
|
||||
data=original_callback
|
||||
)
|
||||
)
|
||||
|
||||
# 等待Bot编辑消息
|
||||
await asyncio.sleep(1)
|
||||
|
||||
logger.info("✅ Callback已处理")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Callback处理失败: {e}")
|
||||
await query.answer("操作失败", show_alert=True)
|
||||
|
||||
async def forward_to_admin(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""转发客户消息给管理员"""
|
||||
user = update.effective_user
|
||||
message = update.effective_message
|
||||
|
||||
# 构建转发消息
|
||||
forward_text = (
|
||||
f"📬 新消息来自客户:\n"
|
||||
f"👤 {user.first_name} {user.last_name or ''}\n"
|
||||
f"🆔 ID: {user.id}\n"
|
||||
f"👤 用户名: @{user.username or '无'}\n"
|
||||
f"💬 消息: {message.text}\n"
|
||||
f"⏰ 时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
)
|
||||
|
||||
# 发送给管理员
|
||||
sent = await context.bot.send_message(
|
||||
chat_id=ADMIN_ID,
|
||||
text=forward_text
|
||||
)
|
||||
|
||||
logger.info(f"已转发消息给管理员: 来自 {user.id}")
|
||||
|
||||
async def handle_admin_reply(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""处理管理员回复"""
|
||||
reply_to = update.message.reply_to_message
|
||||
|
||||
if not reply_to or not reply_to.text:
|
||||
return
|
||||
|
||||
# 从回复的消息中提取用户ID
|
||||
lines = reply_to.text.split('\n')
|
||||
user_id = None
|
||||
for line in lines:
|
||||
if 'ID:' in line or '🆔' in line:
|
||||
try:
|
||||
# 尝试多种格式提取ID
|
||||
if '🆔 ID:' in line:
|
||||
user_id = int(line.split('🆔 ID:')[1].strip())
|
||||
elif 'ID:' in line:
|
||||
id_part = line.split('ID:')[1].strip()
|
||||
# 提取数字部分
|
||||
import re
|
||||
numbers = re.findall(r'\d+', id_part)
|
||||
if numbers:
|
||||
user_id = int(numbers[0])
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"提取ID失败: {e}, line: {line}")
|
||||
|
||||
if not user_id:
|
||||
logger.warning(f"无法识别用户ID,消息内容:{reply_to.text}")
|
||||
await update.message.reply_text("❌ 无法识别用户ID")
|
||||
return
|
||||
|
||||
# 发送回复给用户
|
||||
try:
|
||||
await context.bot.send_message(
|
||||
chat_id=user_id,
|
||||
text=update.message.text
|
||||
)
|
||||
|
||||
# 给管理员确认
|
||||
await update.message.reply_text(f"✅ 已回复给用户 {user_id}")
|
||||
logger.info(f"管理员回复了用户 {user_id}: {update.message.text}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"回复失败: {e}")
|
||||
await update.message.reply_text(f"❌ 回复失败: {str(e)}")
|
||||
|
||||
async def initialize(self):
|
||||
"""初始化机器人"""
|
||||
try:
|
||||
logger.info("正在初始化整合机器人...")
|
||||
|
||||
# 初始化Pyrogram客户端
|
||||
if not await self.setup_pyrogram():
|
||||
logger.error("Pyrogram初始化失败")
|
||||
return False
|
||||
|
||||
# 创建Bot应用,配置代理
|
||||
builder = Application.builder().token(BOT_TOKEN)
|
||||
|
||||
# 如果设置了代理环境变量,配置httpx客户端
|
||||
if os.environ.get('HTTP_PROXY'):
|
||||
proxy_url = os.environ.get('HTTP_PROXY')
|
||||
logger.info(f"配置Telegram Bot代理: {proxy_url}")
|
||||
# 创建自定义httpx客户端
|
||||
request = httpx.AsyncClient(
|
||||
proxies={
|
||||
"http://": proxy_url,
|
||||
"https://": proxy_url,
|
||||
},
|
||||
timeout=30.0
|
||||
)
|
||||
builder = builder.request(request)
|
||||
|
||||
self.app = builder.build()
|
||||
|
||||
# 注册处理器
|
||||
self.app.add_handler(CommandHandler("start", self.handle_start))
|
||||
self.app.add_handler(CallbackQueryHandler(self.handle_callback))
|
||||
self.app.add_handler(MessageHandler(tg_filters.ALL, self.handle_message))
|
||||
|
||||
logger.info("✅ 整合机器人初始化完成")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"初始化失败: {e}")
|
||||
return False
|
||||
|
||||
async def run(self):
|
||||
"""运行机器人"""
|
||||
try:
|
||||
# 启动Bot
|
||||
await self.app.initialize()
|
||||
await self.app.start()
|
||||
await self.app.updater.start_polling(drop_pending_updates=True)
|
||||
|
||||
logger.info("="*50)
|
||||
logger.info("✅ 整合机器人已启动")
|
||||
logger.info(f"客服功能: 消息转发给管理员 {ADMIN_ID}")
|
||||
logger.info(f"搜索功能: 镜像 {TARGET_BOT}")
|
||||
logger.info("="*50)
|
||||
|
||||
# 保持运行
|
||||
await asyncio.Event().wait()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("收到停止信号")
|
||||
finally:
|
||||
await self.cleanup()
|
||||
|
||||
async def cleanup(self):
|
||||
"""清理资源"""
|
||||
logger.info("正在清理...")
|
||||
|
||||
if self.app:
|
||||
await self.app.updater.stop()
|
||||
await self.app.stop()
|
||||
await self.app.shutdown()
|
||||
|
||||
if self.pyrogram_client:
|
||||
await self.pyrogram_client.stop()
|
||||
|
||||
logger.info("✅ 清理完成")
|
||||
|
||||
|
||||
async def main():
|
||||
"""主函数"""
|
||||
bot = IntegratedBot()
|
||||
|
||||
if await bot.initialize():
|
||||
await bot.run()
|
||||
else:
|
||||
logger.error("初始化失败,退出")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user