diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..4f7bc1e --- /dev/null +++ b/src/app.py @@ -0,0 +1,28 @@ +from telethon import TelegramClient, events +from textual.app import App +from tokens import api_id, api_hash +from src.screens import AuthScreen, ChatScreen + +class TelegramTUI(App): + """Класс приложения""" + + CSS_PATH = "style.tcss" + + async def on_mount(self) -> None: + self.telegram_client = TelegramClient("user", api_id, api_hash) + await self.telegram_client.connect() + + chat_screen = ChatScreen(telegram_client=self.telegram_client) + self.install_screen(chat_screen, name="chats") + + if not await self.telegram_client.is_user_authorized(): + auth_screen = AuthScreen(telegram_client=self.telegram_client) + self.install_screen(auth_screen, name="auth") + self.push_screen("auth") + else: + await self.telegram_client.start() + self.push_screen("chats") + + async def on_exit_app(self): + await self.telegram_client.disconnect() + return super()._on_exit_app() diff --git a/src/screens.py b/src/screens.py new file mode 100644 index 0000000..c94f7ba --- /dev/null +++ b/src/screens.py @@ -0,0 +1,149 @@ +from textual.screen import Screen +from textual.widgets import Label, Input, Footer, Static +from textual.containers import Vertical, Horizontal, VerticalScroll +from telethon.errors import SessionPasswordNeededError +from telethon import TelegramClient, events, utils +from src.widgets import Dialog, Chat + +class AuthScreen(Screen): + """Класс логина в аккаунт""" + + def __init__( + self, + name = None, + id = None, + classes = None, + telegram_client: TelegramClient | None = None + ): + super().__init__(name, id, classes) + self.client = telegram_client + + def on_mount(self): + self.ac = self.query_one("#auth_container") + + def compose(self): + with Vertical(id="auth_container"): + yield Label("Добро пожаловать в Telegram TUI") + yield Input(placeholder="Номер телефона", id="phone") + yield Input(placeholder="Код", id="code", disabled=True) + yield Input( + placeholder="Пароль", + id="password", + password=True, + disabled=True + ) + + async def on_input_submitted(self, event: Input.Submitted) -> None: + if event.input.id == "phone": + self.phone = event.value + self.ac.query_one("#phone").disabled = True + self.ac.query_one("#code").disabled = False + await self.client.send_code_request(phone=self.phone) + elif event.input.id == "code": + try: + self.code = event.value + self.ac.query_one("#code").disabled = True + await self.client.sign_in(phone=self.phone, code=self.code) + self.app.pop_screen() + self.app.push_screen("chats") + except SessionPasswordNeededError: + self.ac.query_one("#code").disabled = True + self.ac.query_one("#password").disabled = False + elif event.input.id == "password": + self.password = event.value + await self.client.sign_in(password=self.password) + await self.client.start() + self.app.pop_screen() + self.app.push_screen("chats") + self.app.notify("") + +class ChatScreen(Screen): + """Класс экрана чатов, он же основной экран приложения""" + + def __init__( + self, + name = None, + id = None, + classes = None, + telegram_client: TelegramClient | None = None + ): + super().__init__(name, id, classes) + self.telegram_client = telegram_client + + async def on_mount(self): + self.limit = 30 + + self.chat_container = self\ + .query_one("#main_container")\ + .query_one("#chats")\ + .query_one("#chat_container") + + print("Первоначальная загрузка виджетов чатов...") + self.mount_chats( + len( + await self.telegram_client.get_dialogs( + limit=self.limit, archived=False + ) + ) + ) + print("Первоначальная загрузка виджетов чата завершена") + + self.is_chat_update_blocked = False + await self.update_chat_list() + + print("Первоначальная загрузка чатов завершена") + + for event in ( + events.NewMessage(), + events.MessageDeleted(), + events.MessageEdited() + ): + self.telegram_client.on(event)(self.update_chat_list) + + def mount_chats(self, limit: int): + print("Загрузка виджетов чатов...") + + chats_amount = len(self.chat_container.query(Chat)) + + if limit > chats_amount: + for i in range(limit - chats_amount): + chat = Chat(id=f"chat-{i + chats_amount + 1}") + self.chat_container.mount(chat) + elif limit < chats_amount: + for i in range(chats_amount - limit): + self.chat_container.query(Chat).last().remove() + + print("Виджеты чатов загружены") + + async def update_chat_list(self, event = None): + print("Запрос обновления чатов") + if not self.is_chat_update_blocked: + self.is_chat_update_blocked = True + dialogs = await self.telegram_client.get_dialogs( + limit=self.limit, archived=False + ) + print("Получены диалоги") + limit = len(dialogs) + #limit = 30 + self.mount_chats(limit) + + for i in range(limit): + chat = self.chat_container.query_one(f"#chat-{i + 1}") + chat.username = str(dialogs[i].name) + chat.msg = str(dialogs[i].message.message) + chat.peer_id = dialogs[i].id + #self.notify("Новое сообщение") #колхоз дебаг + + self.is_chat_update_blocked = False + print("Чаты обновлены") + else: + print("Обновление чатов невозможно: уже выполняется") + + def compose(self): + yield Footer() + with Horizontal(id="main_container"): + with Horizontal(id="chats"): + yield VerticalScroll(Static(id="chat_container")) + #TODO: сделать кнопку чтобы прогрузить больше чатов + + yield Dialog(telegram_client=self.telegram_client) diff --git a/src/style.tcss b/src/style.tcss new file mode 100644 index 0000000..cde8031 --- /dev/null +++ b/src/style.tcss @@ -0,0 +1,47 @@ +#chats { + width: 30%; +} + +#dialog { + width: 70%; +} + +Chat { + height: 3; +} + +Rule { + color: #FFFFFF; +} + +.message { + height: 3; + padding: 1; +} + +Message { + height: auto; + width: auto; +} + +Message Container { + height: auto; +} + +#input_place { + height: 3; + width: 70%; + align-horizontal: center; +} + +#msg_input { + width: 65%; +} + +#send { + +} + +#auth_container{ + align: center middle; +} diff --git a/src/widgets.py b/src/widgets.py new file mode 100644 index 0000000..b150cde --- /dev/null +++ b/src/widgets.py @@ -0,0 +1,109 @@ +from textual.containers import Horizontal, Vertical, Container, VerticalScroll +from textual.widget import Widget +from textual.reactive import Reactive +from textual.widgets import Input, Button, Label +from telethon import TelegramClient, events, utils + +class Chat(Widget): + """Класс виджета чата для панели чатов""" + + username = Reactive(" ", recompose=True) + msg = Reactive(" ", recompose=True) + peer_id = Reactive(0) + + def __init__( + self, + name: str | None = None, + notify_func = None, + id: str | None = None, + classes: str | None = None, + disabled: bool = False + ): + super().__init__( + name=str(name), + id=id, + classes=classes, + disabled=disabled + ) + self.notify = notify_func + + + def _on_click(self): + self.msg = str(self.peer_id) + self.notify("нажат чат") + + def compose(self): + with Horizontal(): + yield Label(f"┌───┐\n│ {self.username[:1]} │\n└───┘") + with Vertical(): + yield Label(self.username, id="name") + yield Label(self.msg, id="last_msg") + +class Dialog(Widget): + """Класс окна диалога""" + + def __init__(self, id=None, classes=None, disabled=False, telegram_client: TelegramClient | None = None): + super().__init__(id=id, classes=classes, disabled=disabled) + self.telegram_client = telegram_client + + + def compose(self): + with Vertical(): + with VerticalScroll(id="dialog"): + yield Message(message="привет, я ыплыжлп", is_me=True) + yield Message(message="о, дщытрапшщцрущ", is_me=False) + yield Message(message="ДАТОУШЩАРШЩУРЩША!!!!", is_me=False) + # должно быть примерно + # is_me = message.from_id == client.get_peer_id("me") + + # но я могу ошибаться, я это фиш если что + + #TODO: сделать кнопку чтобы прогрузить больше сообщений, + #но при этом чтобы при перезаходе в чат оставались + #прогруженными только 10 сообщений, + #а остальные декомпоузились + + with Horizontal(id="input_place"): + yield Input(placeholder="Сообщение", id="msg_input") + yield Button(label="➤", id="send", variant="primary") + + def on_button_pressed(self, event): # self добавил + self.app.notify("Нажато отправить") + self.message_text = self.query_one("#msg_input").value + self.telegram_client.send_message("ultimate_fish", self.message_text) + + + +class Message(Widget): + """Класс виджета сообщений для окна диалога""" + + def __init__( + self, + name=None, + message=None, + is_me=None, + id=None, + classes=None, + disabled=False + ): + super().__init__(name=name, id=id, classes=classes, disabled=disabled) + self.message = message + self.is_me = is_me + + def on_mount(self): + container = self.query_one(Container) + label = container.query_one(Label) + if self.is_me: + self.styles.padding = (0, 0, 0, 15) + label.styles.text_align = "right" + container.styles.align_horizontal = "right" + label.styles.border = ("solid", "#4287f5") + else: + self.styles.padding = (0, 15, 0, 0) + label.styles.text_align = "left" + container.styles.align_horizontal = "left" + label.styles.border = ("solid", "#ffffff") + + def compose(self): + with Container(): + yield Label(str(self.message))