ozellik: telegram uzerinden drive yukleme ve clean_chat komutunu ekle

This commit is contained in:
2026-03-22 18:50:59 +03:00
parent ad847b1cf4
commit 99eea4632b
2 changed files with 155 additions and 0 deletions

View File

@@ -176,6 +176,9 @@ Bu yaklaşım belge tabanlı RAG akışına daha uygun olduğu için doğrudan D
- `/otomasyon_baslat <id>`
- `/otomasyon_sil <id>`
- `/notlarima_ekle`
- `/clean_chat`
`/clean_chat` yalnızca Telegram konuşma ekranını temizlemeye çalışır; veritabanındaki memory, audit log, profil veya second brain kayıtlarını silmez.
## ⏱️ Otomasyonlar

View File

@@ -1,13 +1,17 @@
import asyncio
import json
from contextlib import suppress
from pathlib import Path
from typing import Any
from telegram import BotCommand, InputMediaPhoto, Update
from telegram.constants import ChatAction
from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters
from app.db import AuditLogORM
from app.orchestrator import WiseClawOrchestrator
from app.telegram.auth import is_authorized
from app.tools.registry import build_tools
class TelegramBotService:
@@ -75,6 +79,7 @@ class TelegramBotService:
return
self.application = Application.builder().token(self.token).build()
self.application.add_handler(CommandHandler("start", self._on_start))
self.application.add_handler(CommandHandler("clean_chat", self._on_clean_chat))
self.application.add_handler(CommandHandler("tanisalim", self._on_command_passthrough))
self.application.add_handler(CommandHandler("profilim", self._on_command_passthrough))
self.application.add_handler(CommandHandler("tercihlerim", self._on_command_passthrough))
@@ -85,6 +90,7 @@ class TelegramBotService:
self.application.add_handler(CommandHandler("otomasyon_baslat", self._on_command_passthrough))
self.application.add_handler(CommandHandler("otomasyon_sil", self._on_command_passthrough))
self.application.add_handler(CommandHandler("notlarima_ekle", self._on_command_passthrough))
self.application.add_handler(MessageHandler(filters.Document.ALL | filters.PHOTO, self._on_attachment))
self.application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self._on_text))
await self.application.initialize()
await self.application.bot.set_my_commands(self._telegram_commands())
@@ -110,6 +116,8 @@ class TelegramBotService:
async def _on_text(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if update.message is None or update.effective_user is None or update.message.text is None:
return
if await self._maybe_handle_drive_upload_from_reply(update, context):
return
typing_task = asyncio.create_task(self._send_typing(update.effective_chat.id, context))
try:
reply = await self.process_message_payload(update.effective_user.id, update.message.text)
@@ -127,6 +135,36 @@ class TelegramBotService:
for chunk in self._chunk_message(text_reply):
await update.message.reply_text(chunk)
async def _on_attachment(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if update.message is None or update.effective_user is None:
return
if not self._message_has_supported_attachment(update.message):
return
if self._looks_like_drive_upload_request(update.message.caption or ""):
await self._handle_drive_upload(update, context, update.message)
return
await update.message.reply_text(
"Dosyayi aldim. Google Drive'a yuklemek icin bu mesaja reply yapip `Bunu Google Drive'a yukle` yazabilirsin.",
)
async def _on_clean_chat(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if update.message is None or update.effective_chat is None:
return
chat_id = update.effective_chat.id
latest_message_id = update.message.message_id
consecutive_failures = 0
for message_id in range(latest_message_id, 0, -1):
try:
await context.bot.delete_message(chat_id=chat_id, message_id=message_id)
consecutive_failures = 0
except Exception:
consecutive_failures += 1
if consecutive_failures >= 50:
break
continue
async def _on_command_passthrough(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
del context
if update.message is None or update.effective_user is None or update.message.text is None:
@@ -147,6 +185,119 @@ class TelegramBotService:
await context.bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING)
await asyncio.sleep(4)
async def _maybe_handle_drive_upload_from_reply(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
if update.message is None or update.effective_user is None or update.message.text is None:
return False
if not self._looks_like_drive_upload_request(update.message.text):
return False
reply_target = update.message.reply_to_message
if reply_target is None or not self._message_has_supported_attachment(reply_target):
await update.message.reply_text(
"Google Drive'a yuklemek icin once dosya veya fotograf gonder, sonra o mesaja reply yapip `Bunu Google Drive'a yukle` yaz.",
)
return True
await self._handle_drive_upload(update, context, reply_target)
return True
async def _handle_drive_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE, source_message: Any) -> None:
if update.message is None or update.effective_user is None or update.effective_chat is None:
return
with self.orchestrator_factory() as session:
if not is_authorized(session, update.effective_user.id):
await update.message.reply_text("This Telegram user is not authorized for WiseClaw.")
return
orchestrator = WiseClawOrchestrator(session)
runtime = orchestrator.get_runtime_settings()
tools = build_tools(runtime, Path(__file__).resolve().parents[2], session)
drive_tool = tools.get("google_drive")
if drive_tool is None:
await update.message.reply_text("Google Drive araci etkin degil.")
return
temp_file = None
try:
attachment = await self._download_attachment(context, update.effective_chat.id, source_message)
if attachment is None:
await update.message.reply_text("Bu mesajdan yuklenebilir bir dosya cikarilamadi.")
return
temp_file = attachment["local_path"]
result = await drive_tool.run(
{
"action": "upload",
"local_path": attachment["local_path"],
"filename": attachment["filename"],
"mime_type": attachment["mime_type"],
}
)
session.add(
AuditLogORM(
category="tool",
message=f"tool:google_drive:{json.dumps({'action': 'upload', 'filename': attachment['filename']}, ensure_ascii=False)}",
)
)
if result.get("status") != "ok":
message = str(result.get("message", "Google Drive upload failed."))
await update.message.reply_text(f"Dosyayi Google Drive'a yukleyemedim: {message}")
return
file_info = result.get("file", {})
if not isinstance(file_info, dict):
file_info = {}
link = str(file_info.get("web_view_link") or file_info.get("web_content_link") or "").strip()
file_id = str(file_info.get("id", "")).strip()
name = str(file_info.get("name", attachment["filename"])).strip()
response_lines = [f"Dosya Google Drive'a yuklendi: {name}"]
if link:
response_lines.append(f"Link: {link}")
if file_id:
response_lines.append(f"Dosya ID: {file_id}")
await update.message.reply_text("\n".join(response_lines))
finally:
if temp_file:
with suppress(OSError):
Path(temp_file).unlink()
session.commit()
async def _download_attachment(self, context: ContextTypes.DEFAULT_TYPE, chat_id: int, message: Any) -> dict[str, str] | None:
if getattr(message, "document", None) is not None:
document = message.document
tg_file = await context.bot.get_file(document.file_id)
filename = document.file_name or f"telegram_document_{message.message_id}"
mime_type = document.mime_type or "application/octet-stream"
elif getattr(message, "photo", None):
photo = message.photo[-1]
tg_file = await context.bot.get_file(photo.file_id)
filename = f"telegram_photo_{message.message_id}.jpg"
mime_type = "image/jpeg"
else:
return None
temp_dir = Path(__file__).resolve().parents[2] / "tmp" / "telegram_uploads"
temp_dir.mkdir(parents=True, exist_ok=True)
safe_name = self._sanitize_filename(filename)
local_path = temp_dir / f"{chat_id}_{message.message_id}_{safe_name}"
await tg_file.download_to_drive(custom_path=str(local_path))
return {
"local_path": str(local_path),
"filename": filename,
"mime_type": mime_type,
}
def _looks_like_drive_upload_request(self, text: str) -> bool:
normalized = text.casefold()
references_drive = "drive" in normalized or "google drive" in normalized
upload_intent = any(term in normalized for term in ("yukle", "yükle", "gonder", "gönder", "upload"))
return references_drive and upload_intent
def _message_has_supported_attachment(self, message: Any) -> bool:
return bool(getattr(message, "document", None) is not None or getattr(message, "photo", None))
def _sanitize_filename(self, filename: str) -> str:
cleaned = "".join(char if char.isalnum() or char in {"-", "_", "."} else "_" for char in filename.strip())
return cleaned or "attachment.bin"
def _chunk_message(self, text: str) -> list[str]:
if len(text) <= self.MAX_MESSAGE_LEN:
return [text]
@@ -170,6 +321,7 @@ class TelegramBotService:
BotCommand("profilim", "Kayitli profil ozetimi goster (wc)"),
BotCommand("tercihlerim", "Kayitli iletisim tercihlerini goster (wc)"),
BotCommand("tanisalim_sifirla", "Tanisma profilini sifirla (wc)"),
BotCommand("clean_chat", "Telegram ekranindaki mesajlari temizle (wc)"),
BotCommand("otomasyon_ekle", "Yeni otomasyon wizard'ini baslat (wc)"),
BotCommand("otomasyonlar", "Otomasyon listesini goster (wc)"),
BotCommand("otomasyon_durdur", "Bir otomasyonu durdur: /otomasyon_durdur <id> (wc)"),