diff --git a/.env.example b/.env.example index 40aa1d9..31843a3 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,6 @@ API_HASH=b18441a1ff607e10a989891a5462e627 # Конфиг CURRENT_USER = "user" -DO_NOTIFY = False +DO_NOTIFY = 0 # Linux-only for now UTC_OFFSET = 0 -LANGUAGE = "en" \ No newline at end of file +LANGUAGE = "en" # en, ru diff --git a/README.md b/README.md index 9b2b307..917a31e 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ pip install -r requirements.txt 4. Настройте переменные окружения: ```bash cp .env.example .env -# Отредактируйте .env файл, добавив свои API ключи +# Настройте .env файл и добавьте свои API ключи # Получите ключи на https://my.telegram.org/apps ``` diff --git a/src/app.py b/src/app.py index 5a62dfa..452d201 100644 --- a/src/app.py +++ b/src/app.py @@ -4,30 +4,60 @@ from os import getenv from dotenv import load_dotenv from telethon import TelegramClient from textual.app import App +from textual.keys import Keys, _character_to_key +from datetime import timezone, timedelta from src.screens import AuthScreen, ChatScreen import src.locales load_dotenv() API_ID = getenv("API_ID") API_HASH = getenv("API_HASH") +LANGUAGE = getenv("LANGUAGE") +UTC_OFFSET = getenv("UTC_OFFSET") -if not API_ID or not API_HASH: +if "" in [API_ID, API_HASH, LANGUAGE, UTC_OFFSET]: raise ValueError( - "API_ID и API_HASH не найдены в .env файле. " - "Пожалуйста, скопируйте .env.example в .env и заполните свои ключи." + "Недостаточно параметров в .env файле." + "Скопируйте .env.example в .env и заполните свои API-ключи." + "Not enough settings in .env file." + "Copy .env.example into .env and fill your API-keys." ) API_ID = int(API_ID) - -#locale = locales. +locale = dict(zip( + getattr(src.locales, "codes"), getattr(src.locales, LANGUAGE) +)) class Talc(App): """Класс приложения""" CSS_PATH = "style.tcss" + BINDINGS = [ + (Keys.ControlE, "notify(\"Нажата кнопка профиля\")", locale["you"]), + (Keys.ControlF, "notify(\"Нажата кнопка папок\")", locale["folders"]), + (Keys.Tab, "notify(\"Нажат таб\")", locale["switch_focus"]), + (Keys.Enter, "notify(\"Нажат энтер\")", locale["enter"]), + (Keys.Escape, "notify(\"Нажат эскейп\")", locale["back"]), + (_character_to_key("/"), "notify(\"Нажат слэш\")", locale["search"]) + ] + + def __init__( + self, + driver_class = None, + css_path = None, + watch_css = False, + ansi_color = False + ): + super().__init__(driver_class, css_path, watch_css, ansi_color) + self.locale = locale + self.timezone = timezone(timedelta(hours=int(UTC_OFFSET))) async def on_mount(self) -> None: - self.telegram_client = TelegramClient(getenv("CURRENT_USER"), API_ID, API_HASH) + self.telegram_client = TelegramClient( + getenv("CURRENT_USER"), + API_ID, + API_HASH + ) await self.telegram_client.connect() chat_screen = ChatScreen(telegram_client=self.telegram_client) diff --git a/src/locales.py b/src/locales.py index bab2c0e..3b31e54 100644 --- a/src/locales.py +++ b/src/locales.py @@ -1,2 +1,3 @@ -ru = {"greeting_auth": "Добро пожаловать в Тальк", "phone_number": "Номер телефона", "code": "Код", "password": "Пароль", "you": "Вы", "mention": "Вас упомянули"} -en = {"greeting_auth": "Welcome to Talc", "phone_number": "Phone number", "code": "Code", "password": "Password", "you": "You", "mention": "You got mentioned"} \ No newline at end of file +codes = ["auth_greeting", "phone_number", "code", "password", "you", "mention", "media", "message", "folders", "switch_focus", "enter", "back", "search", "start_converse"] +ru = ["Добро пожаловать в Тальк", "Номер телефона", "Код", "Пароль", "Вы", "Вас упомянули", "Медиа", "Сообщение", "Папки", "Переключение фокуса", "Открыть", "Назад", "Поиск", "Нажмите на чат в панели слева, чтобы начать общаться"] +en = ["Welcome to Talc", "Phone number", "Code", "Password", "You", "You got mentioned", "Media", "Message", "Folders", "Switch focus", "Enter", "Back", "Search", "Click on the chat to start conversation"] \ No newline at end of file diff --git a/src/screens.py b/src/screens.py index 4ceef18..5cbe30f 100644 --- a/src/screens.py +++ b/src/screens.py @@ -22,17 +22,18 @@ class AuthScreen(Screen): ) -> None: super().__init__(name, id, classes) self.client = telegram_client + self.locale = self.app.locale def on_mount(self) -> None: self.ac = self.query_one("#auth_container") def compose(self) -> ComposeResult: with Vertical(id="auth_container"): - yield Label("Добро пожаловать в Talc") - yield Input(placeholder="Номер телефона", id="phone") - yield Input(placeholder="Код", id="code", disabled=True) + yield Label(self.locale["auth_greeting"]) + yield Input(placeholder=self.locale["phone_number"], id="phone") + yield Input(placeholder=self.locale["code"], id="code", disabled=True) yield Input( - placeholder="Пароль", + placeholder=self.locale["password"], id="password", password=True, disabled=True @@ -75,6 +76,7 @@ class ChatScreen(Screen): super().__init__(name, id, classes) self.telegram_client = telegram_client self.DO_NOTIFY = getenv("DO_NOTIFY") + self.locale = self.app.locale async def on_mount(self) -> None: self.limit = 100 @@ -152,6 +154,7 @@ class ChatScreen(Screen): chat.peername = str(dialogs[i].name) chat.is_group = dialogs[i].is_group + chat.is_channel = dialogs[i].is_channel chat.peer_id = dialogs[i].id try: @@ -161,7 +164,7 @@ class ChatScreen(Screen): is_my_msg = dialogs[i].id == self.me_id if dialogs[i].is_group and is_my_msg: - chat.username = "Вы" + chat.username = self.locale["you"] chat.msg = str(dialogs[i].message.message) elif dialogs[i].is_group: chat.username = str( @@ -169,7 +172,7 @@ class ChatScreen(Screen): ) chat.msg = str(dialogs[i].message.message) elif is_my_msg: - chat.msg = "Вы: " * is_my_msg + str( + chat.msg = f"{self.locale["you"]}: " * is_my_msg + str( dialogs[i].message.message ) else: @@ -183,8 +186,8 @@ class ChatScreen(Screen): def notify_send(self, event) -> None: if not event: return None - if bool(self.DO_NOTIFY) and event.mentioned and not self.app.focused: - system(f"notify-send \"Вас упомянули\" Talc") + if int(self.DO_NOTIFY) and not self.app.focused and event.mentioned: + system(f"notify-send \"{self.locale["mention"]}\" Talc") def compose(self) -> ComposeResult: yield Footer() # Нижняя панель с подсказками @@ -196,6 +199,6 @@ class ChatScreen(Screen): with ContentSwitcher(id="dialog_switcher"): # ↑ Внутри него как раз крутятся диалоги yield Label( - "Нажмите на чат в панели слева, чтобы начать общаться", - id="begin_talk_label" + self.locale["start_converse"], + id="start_converse_label" ) #TODO: не показывается надпись, надо будет исправить diff --git a/src/style.tcss b/src/style.tcss index 178f964..139d5cd 100644 --- a/src/style.tcss +++ b/src/style.tcss @@ -74,7 +74,7 @@ ContentSwitcher { height: 100%; } -.begin_talk_label { +.start_converse_label { width: 100%; height: 100%; text-align: center; diff --git a/src/widgets.py b/src/widgets.py index 782b7ca..6007b34 100644 --- a/src/widgets.py +++ b/src/widgets.py @@ -6,6 +6,7 @@ from textual.reactive import Reactive from textual.widgets import Input, Button, Label, Static, ContentSwitcher from textual.app import ComposeResult, RenderResult from textual.content import Content +from textual.style import Style from telethon import TelegramClient, events, utils, types import datetime from os import getenv @@ -17,6 +18,7 @@ class Chat(Widget): peername: Reactive[str] = Reactive(" ", recompose=True) msg: Reactive[str] = Reactive(" ", recompose=True) is_group: Reactive[bool] = Reactive(False, recompose=True) + is_channel: Reactive[bool] = Reactive(False, recompose=True) peer_id: Reactive[int] = Reactive(0) def __init__( @@ -46,7 +48,8 @@ class Chat(Widget): self.switcher.mount(Dialog( telegram_client=self.app.telegram_client, chat_id=self.peer_id, - id=dialog_id + id=dialog_id, + is_channel=self.is_channel and not self.is_group )) except: # Диалог уже есть: ничего не делаем @@ -59,10 +62,10 @@ class Chat(Widget): with Horizontal(): yield Label(f"┌───┐\n│ {self.peername[:1]:1} │\n└───┘") with Vertical(): - yield Label(self.peername, id="peername") + yield Label(self.peername, id="peername", markup=False) if self.is_group: - yield Label(self.username, id="name") - yield Label(self.msg, id="last_msg") + yield Label(self.username, id="name", markup=False) + yield Label(self.msg, id="last_msg", markup=False) class Dialog(Widget): """Класс окна диалога""" @@ -73,20 +76,21 @@ class Dialog(Widget): classes=None, disabled=None, telegram_client: TelegramClient | None = None, - chat_id = None + chat_id: int | None = None, + is_channel: bool | None = None ) -> None: super().__init__(id=id, classes=classes, disabled=disabled) self.telegram_client = telegram_client self.chat_id = chat_id self.is_msg_update_blocked = False - self.timezone = datetime.timezone( - datetime.timedelta(hours=int(getenv("UTC_OFFSET"))) - ) + self.timezone = self.app.timezone + self.is_channel = is_channel async def on_mount(self) -> None: self.limit = 10 - self.msg_input = self.query_one("#msg_input") + if not self.is_channel: + self.msg_input = self.query_one("#msg_input") self.dialog = self.query_one(Vertical).query_one("#dialog") self.me = await self.telegram_client.get_me() @@ -102,7 +106,7 @@ class Dialog(Widget): event(chats=(self.chat_id)) )(self.update_dialog) - self.dialog.scroll_down(animate=False, immediate=True) + #self.dialog.scroll_down(animate=False, immediate=True) def mount_messages(self, limit: int) -> None: print("Загрузка виджетов сообщений...") @@ -135,45 +139,45 @@ class Dialog(Widget): for i in range(limit): msg = self.dialog.query_one(f"#msg-{i + 1}") + message = Content(str(messages[i].message)) if str(messages[i].message): - message = Content( - "[Медиа] " * \ - bool(messages[i].media) + \ - str(messages[i].message) - ) - else: - message = Content("[Медиа]" * bool(messages[i].media) + "") - - entities = messages[i].entities - if entities: - for entity in entities: - match type(entity): - case types.MessageEntityBold: - message = message.stylize( - "bold", - entity.offset, - entity.offset + entity.length - ) - case types.MessageEntityUnderline: - message = message.stylize( - "underline", - entity.offset, - entity.offset + entity.length - ) - case types.MessageEntityItalic: - message = message.stylize( - "italic", - entity.offset, - entity.offset + entity.length - ) - case types.MessageEntityStrike: - message = message.stylize( - "strike", - entity.offset, - entity.offset + entity.length - ) + entities = messages[i].entities + if entities: + for entity in entities: + match type(entity): + case types.MessageEntityBold: + message = message.stylize( + "bold", + entity.offset, + entity.offset + entity.length + ) + case types.MessageEntityUnderline: + message = message.stylize( + "underline", + entity.offset, + entity.offset + entity.length + ) + case types.MessageEntityItalic: + message = message.stylize( + "italic", + entity.offset, + entity.offset + entity.length + ) + case types.MessageEntityStrike: + message = message.stylize( + "strike", + entity.offset, + entity.offset + entity.length + ) - msg.message = message + if messages[i].media and str(message): + msg.message = Content(f"[{self.app.locale["media"]}] ")\ + .stylize("link", 0, 6) + message + elif messages[i].media: + msg.message = Content(f"[{self.app.locale["media"]}]")\ + .stylize("link", 0, 6) + else: + msg.message = message try: is_me = messages[i].from_id.user_id == self.me.id @@ -195,9 +199,13 @@ class Dialog(Widget): def compose(self) -> ComposeResult: with Vertical(): yield VerticalScroll(id="dialog") - with Horizontal(id="input_place"): - yield Input(placeholder="Сообщение", id="msg_input") - yield Button(label="➤", id="send", variant="primary") + if not self.is_channel: + with Horizontal(id="input_place"): + yield Input( + placeholder=self.app.locale["message"], + id="msg_input" + ) + yield Button(label="➤", id="send", variant="primary") async def on_button_pressed(self, event = None) -> None: await self.send_message() @@ -212,7 +220,7 @@ class Dialog(Widget): str(self.msg_input.value) ) except ValueError: - self.app.notify("Ошибка отправки") + print("Ошибка отправки") self.msg_input.value = "" await self.update_dialog()