508 lines
18 KiB
Python
508 lines
18 KiB
Python
#!/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()) |