| 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 |
|
|
| |
| logging.basicConfig( |
| level=logging.INFO, |
| format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" |
| ) |
| logger = logging.getLogger("poke-client") |
|
|
| |
| |
| |
| 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) |
|
|
|
|
| |
| |
| |
|
|
| 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() |
|
|
| |
| 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: |
| |
| 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: |
| |
| 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 |
|
|
| |
| if text: |
| self.messages.append({"role": "user", "content": text}) |
| await self.client.send_message(self.target_entity, text) |
|
|
|
|
| manager = PokeClientManager() |
|
|
|
|
| |
| |
| |
|
|
| 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: |
| |
| 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...**") |
|
|
| |
| chatbot = gr.Chatbot( |
| value=[], |
| elem_id="poke_chatbot", |
| show_label=False, |
| avatar_images=None, |
| scale=1, |
| ) |
|
|
| |
| 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") |
|
|
| |
| 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] |
| ) |
|
|
| |
| 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]) |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| demo.queue().launch( |
| server_name="0.0.0.0", |
| server_port=7860, |
| theme=custom_theme |
| ) |