import os import asyncio import logging from typing import List, Dict, Any, Optional import gradio as gr from telethon import TelegramClient, events from telethon.sessions import StringSession # Setup Logging logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" ) logger = logging.getLogger("poke-client") # ========================================== # Configurations # ========================================== TELEGRAM_API_ID = 33348237 TELEGRAM_API_HASH = "0845b005d5e30b49b828e157fbaa86bf" TARGET_BOT_USERNAME = "@interaction_poke_bot" SESSION_FILE_PATH = "/tmp/telethon_session.txt" MEDIA_DIR = "/tmp/poke_media" os.makedirs(MEDIA_DIR, exist_ok=True) # ========================================== # Realtime Poke Client Manager # ========================================== class PokeClientManager: def __init__(self): self.client: Optional[TelegramClient] = None self.session_string = os.getenv("TELEGRAM_SESSION_STRING", "") self.is_connected = False self.user_info = "Yhdistetään..." self.target_entity = None self.target_id = None self.messages: List[Dict[str, Any]] = [] self.msg_id_map: Dict[int, int] = {} self.lock = asyncio.Lock() async def ensure_connected(self): """Varmistaa, että Telegram-yhteys toimii aina oikeassa Gradio event loopissa.""" current_loop = asyncio.get_running_loop() # Jos loop on vaihtunut tai yhteys katkennut, alustetaan uudelleen if self.client is not None: client_loop = getattr(self.client, "_loop", None) if client_loop != current_loop or not self.client.is_connected(): logger.info("Loop vaihtunut tai yhteys katkennut, alustetaan uudelleen...") try: await self.client.disconnect() except Exception: pass self.client = None self.is_connected = False if not self.is_connected: await self.start() async def start(self): session_to_use = self.session_string if not session_to_use and os.path.exists(SESSION_FILE_PATH): try: with open(SESSION_FILE_PATH, "r") as f: session_to_use = f.read().strip() except Exception as e: logger.warning(f"Ei voitu lukea session-tiedostoa: {e}") if not session_to_use: self.user_info = "Puuttuu TELEGRAM_SESSION_STRING" return try: # Luodaan asiakas suoraan aktiiviseen event loopiin self.client = TelegramClient( StringSession(session_to_use), TELEGRAM_API_ID, TELEGRAM_API_HASH, flood_sleep_threshold=24 ) await self.client.connect() if await self.client.is_user_authorized(): me = await self.client.get_me() self.user_info = f"@{me.username}" if getattr(me, "username", None) else f"{me.first_name} ({me.id})" self.is_connected = True self.target_entity = await self.client.get_input_entity(TARGET_BOT_USERNAME) poke_full = await self.client.get_entity(TARGET_BOT_USERNAME) self.target_id = poke_full.id self._attach_event_listeners() await self.sync_history(limit=30) logger.info(f"Yhdistetty Telegramiin käyttäjänä {self.user_info}") else: self.user_info = "Istunto vanhentunut" except Exception as e: logger.exception(f"Yhdistysvirhe: {e}") self.is_connected = False self.user_info = f"Virhe: {e}" def _attach_event_listeners(self): @self.client.on(events.NewMessage(chats=self.target_entity)) async def handle_new_message(event): try: if event.sender_id == self.target_id: await self._record_incoming_message(event.message) except Exception as e: logger.error(f"Virhe saapuvassa viestissä: {e}") @self.client.on(events.MessageEdited(chats=self.target_entity)) async def handle_edited_message(event): try: if event.sender_id == self.target_id: await self._update_edited_message(event.message) except Exception as e: logger.error(f"Virhe muokatussa viestissä: {e}") async def _record_incoming_message(self, message): media_path = None if message.media: try: media_path = await self.client.download_media(message, file=MEDIA_DIR) except Exception as e: logger.warning(f"Kuvan lataus epäonnistui: {e}") text = message.text or "" if media_path: self.messages.append({"role": "assistant", "content": {"path": media_path}}) if text: self.messages.append({"role": "assistant", "content": text}) self.msg_id_map[message.id] = len(self.messages) - 1 async def _update_edited_message(self, message): if message.id in self.msg_id_map: idx = self.msg_id_map[message.id] if idx < len(self.messages): self.messages[idx]["content"] = message.text or "" else: await self._record_incoming_message(message) async def sync_history(self, limit: int = 30): if not self.client or not self.is_connected: return try: self.messages.clear() self.msg_id_map.clear() tg_messages = await self.client.get_messages(self.target_entity, limit=limit) for msg in reversed(tg_messages): role = "user" if msg.out else "assistant" media_path = None if msg.media and hasattr(msg.media, 'photo'): try: media_path = await self.client.download_media(msg, file=MEDIA_DIR) except Exception: pass if media_path: self.messages.append({"role": role, "content": {"path": media_path}}) if msg.text: self.messages.append({"role": role, "content": msg.text}) self.msg_id_map[msg.id] = len(self.messages) - 1 except Exception as e: logger.warning(f"Historian haku epäonnistui: {e}") async def send_message(self, text: str, files: Optional[List[str]] = None): await self.ensure_connected() if not self.is_connected or not self.client: raise RuntimeError("Ei yhteyttä Telegramiin.") text = (text or "").strip() files = files or [] async with self.lock: # Lähetetään kuvat if files: for file_path in files: self.messages.append({"role": "user", "content": {"path": file_path}}) await self.client.send_file( self.target_entity, file=file_path, caption=text if file_path == files[-1] else None ) if text: return # Lähetetään teksti if text: self.messages.append({"role": "user", "content": text}) await self.client.send_message(self.target_entity, text) manager = PokeClientManager() # ========================================== # Fullscreen Modern Gradio UI # ========================================== custom_css = """ /* Poistetaan ylimääräiset reunat ja marginaalit */ .gradio-container { padding: 6px !important; max-width: 100% !important; height: 100dvh !important; display: flex !important; flex-direction: column !important; box-sizing: border-box !important; } /* Tehdään chat-osiosta koko näytön korkuinen ja joustava */ #poke_chatbot { flex-grow: 1 !important; height: calc(100dvh - 145px) !important; min-height: 400px !important; border: none !important; background: transparent !important; box-shadow: none !important; } /* Piilotetaan Chatbot-otsikkolaatikko kokonaan */ .chatbot-header, .label-wrap { display: none !important; } /* Viestikuplien asettelu */ .message-row { padding: 4px 0 !important; } /* Pääpalkin tiivis muotoilu */ .header-bar { padding: 4px 8px !important; margin-bottom: 4px !important; } """ custom_theme = gr.themes.Soft( primary_hue="cyan", secondary_hue="slate", neutral_hue="slate" ).set( body_background_fill="*neutral_950", body_text_color="*neutral_100", block_background_fill="*neutral_900", block_border_width="0px", input_background_fill="*neutral_800", ) with gr.Blocks(title="Poke Live Client", fill_height=True, css=custom_css) as demo: # Kompakti yläpalkki with gr.Row(elem_classes=["header-bar"]): with gr.Column(scale=8): gr.Markdown(f"⚡ **Poke Client** (`{TARGET_BOT_USERNAME}`)") with gr.Column(scale=4): status_indicator = gr.Markdown(value="🟢 **Online**" if manager.is_connected else "🔴 **Yhdistetään...**") # Koko näytön chat ilman robotti-ikonia tai laatikko-otsikkoa chatbot = gr.Chatbot( value=[], elem_id="poke_chatbot", show_label=False, avatar_images=None, scale=1, ) # Viestikenttä ja lähetys with gr.Row(): chat_input = gr.MultimodalTextbox( placeholder="Kirjoita viesti tai liitä kuva...", show_label=False, file_types=["image"], scale=9, autofocus=True ) send_btn = gr.Button("Lähetä", variant="primary", scale=1) with gr.Row(): sync_btn = gr.Button("🔄 Päivitä historia", size="sm", variant="secondary") clear_btn = gr.Button("🗑️ Tyhjennä näyttö", size="sm", variant="stop") # 1 sekunnin automaattinen taustapäivitys sync_timer = gr.Timer(value=1.0) async def refresh_ui_messages(): await manager.ensure_connected() status_text = f"🟢 **{manager.user_info}**" if manager.is_connected else f"🔴 **{manager.user_info}**" return list(manager.messages), status_text sync_timer.tick( fn=refresh_ui_messages, inputs=[], outputs=[chatbot, status_indicator] ) # Tapahtumakäsittelijät async def handle_user_submit(msg_data): if not msg_data: return gr.MultimodalTextbox(value=None) text = msg_data.get("text", "") files = msg_data.get("files", []) try: await manager.send_message(text=text, files=files) except Exception as e: gr.Warning(f"Lähetys epäonnistui: {e}") return gr.MultimodalTextbox(value=None) chat_input.submit( fn=handle_user_submit, inputs=[chat_input], outputs=[chat_input] ) send_btn.click( fn=handle_user_submit, inputs=[chat_input], outputs=[chat_input] ) async def handle_resync(): await manager.sync_history(limit=30) return gr.Info("Historia päivitetty!") sync_btn.click(fn=handle_resync, inputs=[], outputs=[]) def handle_clear(): manager.messages.clear() return [] clear_btn.click(fn=handle_clear, inputs=[], outputs=[chatbot]) # ========================================== # Käynnistys # ========================================== if __name__ == "__main__": demo.queue().launch( server_name="0.0.0.0", server_port=7860, theme=custom_theme )