mirror of
https://github.com/avitoras/telegram-tui.git
synced 2025-07-27 19:26:10 +00:00
UNSTABLE | Pictures to ascii
This commit is contained in:
parent
cca1249526
commit
3592da4503
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,6 +12,7 @@ venv/
|
|||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
.env
|
.env
|
||||||
|
pics*
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
urwid>=2.1.2
|
telethon
|
||||||
telethon>=1.34.0
|
python-dotenv
|
||||||
python-dotenv>=1.0.0
|
urwid
|
||||||
emoji>=2.10.1
|
nest_asyncio
|
||||||
nest_asyncio>=1.6.0
|
emoji
|
||||||
|
Pillow
|
@ -15,6 +15,10 @@ from telethon import TelegramClient, events, utils
|
|||||||
from telethon.errors import SessionPasswordNeededError
|
from telethon.errors import SessionPasswordNeededError
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import datetime
|
import datetime
|
||||||
|
from PIL import Image
|
||||||
|
import io
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
|
||||||
# Разрешаем вложенные event loops
|
# Разрешаем вложенные event loops
|
||||||
nest_asyncio.apply()
|
nest_asyncio.apply()
|
||||||
@ -49,6 +53,113 @@ def normalize_text(text: str) -> str:
|
|||||||
print(f"Ошибка нормализации текста: {e}")
|
print(f"Ошибка нормализации текста: {e}")
|
||||||
return "Ошибка отображения"
|
return "Ошибка отображения"
|
||||||
|
|
||||||
|
def image_to_ascii(image_data, max_width=80, max_height=24):
|
||||||
|
"""Конвертирует изображение в ASCII-арт"""
|
||||||
|
try:
|
||||||
|
# Открываем изображение из байтов
|
||||||
|
image = Image.open(io.BytesIO(image_data))
|
||||||
|
|
||||||
|
# Конвертируем в оттенки серого
|
||||||
|
image = image.convert('L')
|
||||||
|
|
||||||
|
# Определяем новые размеры с сохранением пропорций
|
||||||
|
width, height = image.size
|
||||||
|
aspect_ratio = height/width
|
||||||
|
new_width = min(max_width, width)
|
||||||
|
new_height = int(new_width * aspect_ratio * 0.5) # * 0.5 потому что символы в терминале выше, чем шире
|
||||||
|
|
||||||
|
if new_height > max_height:
|
||||||
|
new_height = max_height
|
||||||
|
new_width = int(new_height / aspect_ratio * 2)
|
||||||
|
|
||||||
|
# Изменяем размер
|
||||||
|
image = image.resize((new_width, new_height))
|
||||||
|
|
||||||
|
# Символы от темного к светлому
|
||||||
|
ascii_chars = '@%#*+=-:. '
|
||||||
|
|
||||||
|
# Конвертируем пиксели в ASCII
|
||||||
|
pixels = image.getdata()
|
||||||
|
ascii_str = ''
|
||||||
|
for i, pixel in enumerate(pixels):
|
||||||
|
ascii_str += ascii_chars[pixel//32] # 256//32 = 8 уровней
|
||||||
|
if (i + 1) % new_width == 0:
|
||||||
|
ascii_str += '\n'
|
||||||
|
|
||||||
|
# Добавляем рамку вокруг ASCII-арта для стабильности
|
||||||
|
lines = ascii_str.split('\n')
|
||||||
|
if lines and lines[-1] == '':
|
||||||
|
lines = lines[:-1] # Удаляем последнюю пустую строку
|
||||||
|
|
||||||
|
max_line_length = max(len(line) for line in lines)
|
||||||
|
border_top = '┌' + '─' * max_line_length + '┐\n'
|
||||||
|
border_bottom = '└' + '─' * max_line_length + '┘'
|
||||||
|
|
||||||
|
framed_ascii = border_top
|
||||||
|
for line in lines:
|
||||||
|
padding = ' ' * (max_line_length - len(line))
|
||||||
|
framed_ascii += '│' + line + padding + '│\n'
|
||||||
|
framed_ascii += border_bottom
|
||||||
|
|
||||||
|
return framed_ascii
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка конвертации изображения: {e}")
|
||||||
|
return "[Ошибка конвертации изображения]"
|
||||||
|
|
||||||
|
class AsciiArtCache:
|
||||||
|
"""Класс для кэширования ASCII-артов"""
|
||||||
|
def __init__(self, cache_dir='pics'):
|
||||||
|
self.cache_dir = cache_dir
|
||||||
|
os.makedirs(cache_dir, exist_ok=True)
|
||||||
|
self.index_file = os.path.join(cache_dir, 'index.json')
|
||||||
|
self.load_index()
|
||||||
|
|
||||||
|
def load_index(self):
|
||||||
|
"""Загружает индекс кэшированных изображений"""
|
||||||
|
try:
|
||||||
|
with open(self.index_file, 'r') as f:
|
||||||
|
self.index = json.load(f)
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
self.index = {}
|
||||||
|
self.save_index()
|
||||||
|
|
||||||
|
def save_index(self):
|
||||||
|
"""Сохраняет индекс кэшированных изображений"""
|
||||||
|
with open(self.index_file, 'w') as f:
|
||||||
|
json.dump(self.index, f)
|
||||||
|
|
||||||
|
def get_cache_key(self, image_data):
|
||||||
|
"""Генерирует ключ кэша для изображения"""
|
||||||
|
return hashlib.md5(image_data).hexdigest()
|
||||||
|
|
||||||
|
def get_cached_art(self, image_data):
|
||||||
|
"""Получает ASCII-арт из кэша или создает новый"""
|
||||||
|
cache_key = self.get_cache_key(image_data)
|
||||||
|
|
||||||
|
# Проверяем наличие в индексе
|
||||||
|
if cache_key in self.index:
|
||||||
|
try:
|
||||||
|
with open(os.path.join(self.cache_dir, cache_key + '.txt'), 'r') as f:
|
||||||
|
return f.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Если нет в кэше, создаем новый
|
||||||
|
ascii_art = image_to_ascii(image_data)
|
||||||
|
|
||||||
|
# Сохраняем в кэш
|
||||||
|
with open(os.path.join(self.cache_dir, cache_key + '.txt'), 'w') as f:
|
||||||
|
f.write(ascii_art)
|
||||||
|
|
||||||
|
# Обновляем индекс
|
||||||
|
self.index[cache_key] = {
|
||||||
|
'created_at': datetime.datetime.now().isoformat(),
|
||||||
|
'size': len(image_data)
|
||||||
|
}
|
||||||
|
self.save_index()
|
||||||
|
|
||||||
|
return ascii_art
|
||||||
|
|
||||||
class ChatWidget(urwid.WidgetWrap):
|
class ChatWidget(urwid.WidgetWrap):
|
||||||
"""Виджет чата"""
|
"""Виджет чата"""
|
||||||
|
|
||||||
@ -134,39 +245,77 @@ class ChatWidget(urwid.WidgetWrap):
|
|||||||
class MessageWidget(urwid.WidgetWrap):
|
class MessageWidget(urwid.WidgetWrap):
|
||||||
"""Виджет сообщения"""
|
"""Виджет сообщения"""
|
||||||
|
|
||||||
def __init__(self, text="", username="", is_me=False, send_time=""):
|
def __init__(self, message_id, text="", username="", is_me=False, send_time="", status="", is_selected=False, media_data=None):
|
||||||
|
self.message_id = message_id
|
||||||
self.text = normalize_text(text)
|
self.text = normalize_text(text)
|
||||||
self.username = normalize_text(username)
|
self.username = normalize_text(username)
|
||||||
self.is_me = is_me
|
self.is_me = is_me
|
||||||
self.send_time = send_time
|
self.send_time = send_time
|
||||||
|
self.status = status
|
||||||
|
self.is_selected = is_selected
|
||||||
|
self._media_data = None
|
||||||
|
self._cached_content = None
|
||||||
|
self.set_media_data(media_data)
|
||||||
|
|
||||||
# Создаем содержимое виджета
|
# Создаем содержимое виджета
|
||||||
self.update_widget()
|
self.update_widget()
|
||||||
super().__init__(self.widget)
|
super().__init__(self.widget)
|
||||||
|
|
||||||
|
def set_media_data(self, media_data):
|
||||||
|
"""Устанавливает медиа-данные и обновляет кэш"""
|
||||||
|
if self._media_data != media_data:
|
||||||
|
self._media_data = media_data
|
||||||
|
self._cached_content = None
|
||||||
|
|
||||||
|
def get_content(self):
|
||||||
|
"""Получает содержимое сообщения с кэшированием"""
|
||||||
|
if self._cached_content is None:
|
||||||
|
text = self.text if self.text else "Пустое сообщение"
|
||||||
|
if self._media_data:
|
||||||
|
text = self._media_data + "\n" + text
|
||||||
|
self._cached_content = text
|
||||||
|
return self._cached_content
|
||||||
|
|
||||||
def update_widget(self):
|
def update_widget(self):
|
||||||
"""Обновляет внешний вид виджета"""
|
"""Обновляет внешний вид виджета"""
|
||||||
# Подготавливаем текст
|
# Подготавливаем текст
|
||||||
text = self.text if self.text else "Пустое сообщение"
|
|
||||||
username = self.username if self.username else "Неизвестный"
|
username = self.username if self.username else "Неизвестный"
|
||||||
|
|
||||||
|
# Добавляем статус к времени для исходящих сообщений
|
||||||
|
time_text = self.send_time
|
||||||
|
if self.is_me and self.status:
|
||||||
|
time_text = f"{self.send_time} {self.status}"
|
||||||
|
|
||||||
# Создаем заголовок
|
# Создаем заголовок
|
||||||
header = urwid.Columns([
|
header = urwid.Columns([
|
||||||
urwid.Text(username),
|
urwid.Text(username),
|
||||||
('fixed', 5, urwid.Text(self.send_time, align='right'))
|
('fixed', 10 if self.is_me else 5, urwid.Text(time_text, align='right'))
|
||||||
])
|
])
|
||||||
|
|
||||||
|
# Определяем стиль
|
||||||
|
style = 'message_selected' if self.is_selected else ('message_me' if self.is_me else 'message_other')
|
||||||
|
|
||||||
|
# Создаем виджет с фиксированной шириной для ASCII-арта
|
||||||
|
content = urwid.Text(self.get_content())
|
||||||
|
if self._media_data:
|
||||||
|
content = urwid.BoxAdapter(urwid.Filler(content), len(self._media_data.split('\n')))
|
||||||
|
|
||||||
# Создаем виджет
|
# Создаем виджет
|
||||||
self.widget = urwid.AttrMap(
|
self.widget = urwid.AttrMap(
|
||||||
urwid.Pile([
|
urwid.Pile([
|
||||||
urwid.AttrMap(header, 'chat_name'),
|
urwid.AttrMap(header, 'chat_name'),
|
||||||
urwid.Text(text)
|
content
|
||||||
]),
|
]),
|
||||||
'message_me' if self.is_me else 'message_other'
|
style
|
||||||
)
|
)
|
||||||
|
|
||||||
def selectable(self):
|
def selectable(self):
|
||||||
return False
|
return True
|
||||||
|
|
||||||
|
def keypress(self, size, key):
|
||||||
|
if key == 'ctrl r':
|
||||||
|
return 'reply'
|
||||||
|
return key
|
||||||
|
|
||||||
class SearchEdit(urwid.Edit):
|
class SearchEdit(urwid.Edit):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -205,6 +354,8 @@ class TelegramTUI:
|
|||||||
('message_other', 'white', 'black'),
|
('message_other', 'white', 'black'),
|
||||||
('help', 'yellow', 'black'),
|
('help', 'yellow', 'black'),
|
||||||
('error', 'light red', 'black'),
|
('error', 'light red', 'black'),
|
||||||
|
('message_selected', 'black', 'light gray'),
|
||||||
|
('input_disabled', 'dark gray', 'black'),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, telegram_client: TelegramClient):
|
def __init__(self, telegram_client: TelegramClient):
|
||||||
@ -249,7 +400,7 @@ class TelegramTUI:
|
|||||||
# Создаем левую панель (чаты)
|
# Создаем левую панель (чаты)
|
||||||
self.left_panel = urwid.LineBox(
|
self.left_panel = urwid.LineBox(
|
||||||
urwid.Pile([
|
urwid.Pile([
|
||||||
('pack', urwid.Text(('help', "Tab - переключение фокуса, ↑↓ - выбор чата, Enter - открыть чат, Esc - назад, / - поиск"), align='center')),
|
('pack', urwid.Text(('help', "Tab - переключение фокуса, ↑↓ - выбор чата, Enter - открыть чат, Esc - назад, / - поиск, [] - папки"), align='center')),
|
||||||
('pack', self.search_edit),
|
('pack', self.search_edit),
|
||||||
urwid.BoxAdapter(self.chat_list, 30) # Фиксированная высота для списка чатов
|
urwid.BoxAdapter(self.chat_list, 30) # Фиксированная высота для списка чатов
|
||||||
])
|
])
|
||||||
@ -297,6 +448,36 @@ class TelegramTUI:
|
|||||||
self.update_interval = 3 # секунды для чатов
|
self.update_interval = 3 # секунды для чатов
|
||||||
self.message_update_interval = 1 # секунда для сообщений
|
self.message_update_interval = 1 # секунда для сообщений
|
||||||
self.last_message_update_time = 0
|
self.last_message_update_time = 0
|
||||||
|
|
||||||
|
# Добавляем отслеживание отправляемых сообщений
|
||||||
|
self.pending_messages = {} # message_id -> widget
|
||||||
|
|
||||||
|
# Добавляем обработчик обновления сообщений
|
||||||
|
@telegram_client.on(events.MessageEdited())
|
||||||
|
async def handle_edit(event):
|
||||||
|
try:
|
||||||
|
if event.message.out:
|
||||||
|
await self.update_message_status(event.message)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка обработки редактирования: {e}")
|
||||||
|
|
||||||
|
@telegram_client.on(events.NewMessage())
|
||||||
|
async def handle_new(event):
|
||||||
|
try:
|
||||||
|
if event.message.out:
|
||||||
|
await self.update_message_status(event.message)
|
||||||
|
elif event.message.chat_id == self.current_chat_id:
|
||||||
|
# Обновляем сообщения если это текущий чат
|
||||||
|
self.last_message_update_time = 0
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка обработки нового сообщения: {e}")
|
||||||
|
|
||||||
|
# Добавляем состояния для ответов
|
||||||
|
self.selected_message = None
|
||||||
|
self.replying_to = None
|
||||||
|
self.can_send_messages = False
|
||||||
|
|
||||||
|
self.ascii_cache = AsciiArtCache()
|
||||||
|
|
||||||
def switch_screen(self, screen_name: str):
|
def switch_screen(self, screen_name: str):
|
||||||
"""Переключение между экранами"""
|
"""Переключение между экранами"""
|
||||||
@ -344,19 +525,27 @@ class TelegramTUI:
|
|||||||
async def update_chat_list(self):
|
async def update_chat_list(self):
|
||||||
"""Обновляет список чатов"""
|
"""Обновляет список чатов"""
|
||||||
try:
|
try:
|
||||||
|
# Сохраняем текущий фокус и ID выбранного чата
|
||||||
|
current_focus = self.chat_list.focus_position if self.chat_walker else 0
|
||||||
|
current_chat_id = self.current_chat_id
|
||||||
|
|
||||||
# Получаем папки
|
# Получаем папки
|
||||||
if not self.folders:
|
if not self.folders:
|
||||||
try:
|
try:
|
||||||
# Проверяем наличие архива
|
folders = await self.telegram_client.get_dialogs(folder=1)
|
||||||
self.folders = [1] if await self.telegram_client.get_dialogs(limit=1, folder=1) else []
|
if folders:
|
||||||
|
self.folders = [0, 1]
|
||||||
|
else:
|
||||||
|
self.folders = [0]
|
||||||
|
print(f"Доступные папки: {self.folders}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка получения папок: {e}")
|
print(f"Ошибка получения папок: {e}")
|
||||||
self.folders = []
|
self.folders = [0]
|
||||||
|
|
||||||
# Получаем диалоги
|
# Получаем диалоги
|
||||||
try:
|
try:
|
||||||
dialogs = await self.telegram_client.get_dialogs(
|
dialogs = await self.telegram_client.get_dialogs(
|
||||||
limit=100,
|
limit=50, # Уменьшаем лимит для стабильности
|
||||||
folder=self.current_folder
|
folder=self.current_folder
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -369,7 +558,6 @@ class TelegramTUI:
|
|||||||
filtered_dialogs = []
|
filtered_dialogs = []
|
||||||
for dialog in dialogs:
|
for dialog in dialogs:
|
||||||
try:
|
try:
|
||||||
# Поиск по имени
|
|
||||||
name = ""
|
name = ""
|
||||||
if hasattr(dialog.entity, 'title') and dialog.entity.title:
|
if hasattr(dialog.entity, 'title') and dialog.entity.title:
|
||||||
name = dialog.entity.title
|
name = dialog.entity.title
|
||||||
@ -378,12 +566,10 @@ class TelegramTUI:
|
|||||||
if hasattr(dialog.entity, 'last_name') and dialog.entity.last_name:
|
if hasattr(dialog.entity, 'last_name') and dialog.entity.last_name:
|
||||||
name += f" {dialog.entity.last_name}"
|
name += f" {dialog.entity.last_name}"
|
||||||
|
|
||||||
# Поиск по последнему сообщению
|
|
||||||
last_message = ""
|
last_message = ""
|
||||||
if dialog.message and hasattr(dialog.message, 'message'):
|
if dialog.message and hasattr(dialog.message, 'message'):
|
||||||
last_message = dialog.message.message
|
last_message = dialog.message.message
|
||||||
|
|
||||||
# Если есть совпадение, добавляем диалог
|
|
||||||
if (search_query in normalize_text(name).lower() or
|
if (search_query in normalize_text(name).lower() or
|
||||||
search_query in normalize_text(last_message).lower()):
|
search_query in normalize_text(last_message).lower()):
|
||||||
filtered_dialogs.append(dialog)
|
filtered_dialogs.append(dialog)
|
||||||
@ -392,16 +578,18 @@ class TelegramTUI:
|
|||||||
|
|
||||||
dialogs = filtered_dialogs
|
dialogs = filtered_dialogs
|
||||||
|
|
||||||
|
# Сохраняем старые чаты для сравнения
|
||||||
|
old_chats = {chat.chat_id: chat for chat in self.chat_walker}
|
||||||
|
|
||||||
# Очищаем список
|
# Очищаем список
|
||||||
self.chat_walker[:] = []
|
self.chat_walker[:] = []
|
||||||
|
|
||||||
# Добавляем чаты
|
# Добавляем чаты
|
||||||
|
restored_focus = False
|
||||||
for i, dialog in enumerate(dialogs):
|
for i, dialog in enumerate(dialogs):
|
||||||
try:
|
try:
|
||||||
# Получаем имя и сообщение
|
|
||||||
entity = dialog.entity
|
entity = dialog.entity
|
||||||
|
|
||||||
# Определяем имя чата
|
|
||||||
if hasattr(entity, 'title') and entity.title:
|
if hasattr(entity, 'title') and entity.title:
|
||||||
name = entity.title
|
name = entity.title
|
||||||
elif hasattr(entity, 'first_name'):
|
elif hasattr(entity, 'first_name'):
|
||||||
@ -411,27 +599,39 @@ class TelegramTUI:
|
|||||||
else:
|
else:
|
||||||
name = "Без названия"
|
name = "Без названия"
|
||||||
|
|
||||||
# Получаем последнее сообщение
|
|
||||||
if dialog.message:
|
if dialog.message:
|
||||||
message = dialog.message.message if hasattr(dialog.message, 'message') else ""
|
message = dialog.message.message if hasattr(dialog.message, 'message') else ""
|
||||||
else:
|
else:
|
||||||
message = ""
|
message = ""
|
||||||
|
|
||||||
|
# Проверяем, был ли этот чат раньше
|
||||||
|
old_chat = old_chats.get(dialog.id)
|
||||||
|
is_selected = (dialog.id == current_chat_id)
|
||||||
|
|
||||||
chat = ChatWidget(
|
chat = ChatWidget(
|
||||||
chat_id=dialog.id,
|
chat_id=dialog.id,
|
||||||
name=name,
|
name=name,
|
||||||
message=message,
|
message=message,
|
||||||
is_selected=(i == self.selected_chat_index),
|
is_selected=is_selected,
|
||||||
folder=1 if self.current_folder else 0
|
folder=1 if self.current_folder else 0
|
||||||
)
|
)
|
||||||
|
|
||||||
self.chat_walker.append(chat)
|
self.chat_walker.append(chat)
|
||||||
|
|
||||||
|
# Восстанавливаем фокус если это текущий чат
|
||||||
|
if dialog.id == current_chat_id and not restored_focus:
|
||||||
|
current_focus = i
|
||||||
|
restored_focus = True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка создания виджета чата: {e}")
|
print(f"Ошибка создания виджета чата: {e}")
|
||||||
|
|
||||||
# Обновляем фокус
|
# Восстанавливаем фокус
|
||||||
if self.chat_walker:
|
if self.chat_walker:
|
||||||
self.selected_chat_index = min(self.selected_chat_index, len(self.chat_walker) - 1)
|
if current_focus >= len(self.chat_walker):
|
||||||
self.chat_list.set_focus(self.selected_chat_index)
|
current_focus = len(self.chat_walker) - 1
|
||||||
|
self.chat_list.set_focus(max(0, current_focus))
|
||||||
|
self.selected_chat_index = current_focus
|
||||||
self.update_selected_chat()
|
self.update_selected_chat()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -451,31 +651,63 @@ class TelegramTUI:
|
|||||||
async def update_message_list(self, chat_id):
|
async def update_message_list(self, chat_id):
|
||||||
"""Обновляет список сообщений"""
|
"""Обновляет список сообщений"""
|
||||||
try:
|
try:
|
||||||
|
if not chat_id:
|
||||||
|
self.message_walker[:] = []
|
||||||
|
return
|
||||||
|
|
||||||
|
# Сохраняем текущий фокус
|
||||||
|
current_focus = self.message_list.focus_position if self.message_walker else None
|
||||||
|
|
||||||
# Получаем сообщения
|
# Получаем сообщения
|
||||||
messages = await self.telegram_client.get_messages(
|
messages = await self.telegram_client.get_messages(
|
||||||
entity=chat_id,
|
entity=chat_id,
|
||||||
limit=50
|
limit=30 # Уменьшаем лимит для стабильности
|
||||||
)
|
)
|
||||||
|
|
||||||
# Получаем информацию о себе
|
# Получаем информацию о себе
|
||||||
me = await self.telegram_client.get_me()
|
me = await self.telegram_client.get_me()
|
||||||
|
|
||||||
|
# Сохраняем отслеживаемые сообщения
|
||||||
|
tracked_messages = {
|
||||||
|
msg_id: widget
|
||||||
|
for msg_id, widget in self.pending_messages.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Сохраняем старые сообщения для сравнения
|
||||||
|
old_messages = {msg.message_id: msg for msg in self.message_walker}
|
||||||
|
|
||||||
# Очищаем список
|
# Очищаем список
|
||||||
self.message_walker[:] = []
|
self.message_walker[:] = []
|
||||||
|
|
||||||
# Добавляем сообщения
|
# Добавляем сообщения
|
||||||
for msg in reversed(messages):
|
for msg in reversed(messages):
|
||||||
try:
|
try:
|
||||||
# Определяем, отправлено ли сообщение нами
|
# Проверяем, было ли это сообщение раньше
|
||||||
|
old_message = old_messages.get(msg.id)
|
||||||
|
if old_message and not msg.photo:
|
||||||
|
# Переиспользуем существующий виджет для не-фото сообщений
|
||||||
|
self.message_walker.append(old_message)
|
||||||
|
continue
|
||||||
|
|
||||||
is_me = False
|
is_me = False
|
||||||
if hasattr(msg, 'from_id') and msg.from_id:
|
if hasattr(msg, 'from_id') and msg.from_id:
|
||||||
if hasattr(msg.from_id, 'user_id'):
|
if hasattr(msg.from_id, 'user_id'):
|
||||||
is_me = msg.from_id.user_id == me.id
|
is_me = msg.from_id.user_id == me.id
|
||||||
|
|
||||||
# Получаем текст сообщения
|
text = msg.message if hasattr(msg, 'message') else ""
|
||||||
text = msg.message if hasattr(msg, 'message') else "Медиа"
|
media_data = None
|
||||||
|
|
||||||
|
if hasattr(msg, 'photo') and msg.photo:
|
||||||
|
try:
|
||||||
|
photo_data = await self.telegram_client.download_media(msg.photo, bytes)
|
||||||
|
if photo_data:
|
||||||
|
media_data = self.ascii_cache.get_cached_art(photo_data)
|
||||||
|
if not text:
|
||||||
|
text = "[Фото]"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка загрузки фото: {e}")
|
||||||
|
text = "[Ошибка загрузки фото]"
|
||||||
|
|
||||||
# Получаем имя отправителя
|
|
||||||
username = ""
|
username = ""
|
||||||
if hasattr(msg, 'sender') and msg.sender:
|
if hasattr(msg, 'sender') and msg.sender:
|
||||||
if hasattr(msg.sender, 'first_name'):
|
if hasattr(msg.sender, 'first_name'):
|
||||||
@ -485,126 +717,251 @@ class TelegramTUI:
|
|||||||
elif hasattr(msg.sender, 'title'):
|
elif hasattr(msg.sender, 'title'):
|
||||||
username = msg.sender.title
|
username = msg.sender.title
|
||||||
|
|
||||||
# Если не удалось получить имя, используем Me/Другой
|
|
||||||
if not username:
|
if not username:
|
||||||
username = "Я" if is_me else "Неизвестный"
|
username = "Я" if is_me else "Неизвестный"
|
||||||
|
|
||||||
|
status = ""
|
||||||
|
if is_me:
|
||||||
|
if msg.id in tracked_messages:
|
||||||
|
status = tracked_messages[msg.id].status
|
||||||
|
else:
|
||||||
|
status = "✓✓"
|
||||||
|
|
||||||
message = MessageWidget(
|
message = MessageWidget(
|
||||||
|
message_id=msg.id,
|
||||||
text=text,
|
text=text,
|
||||||
username=username,
|
username=username,
|
||||||
is_me=is_me,
|
is_me=is_me,
|
||||||
send_time=msg.date.strftime("%H:%M")
|
send_time=msg.date.strftime("%H:%M"),
|
||||||
|
status=status,
|
||||||
|
is_selected=(msg.id == self.selected_message),
|
||||||
|
media_data=media_data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if msg.id in tracked_messages:
|
||||||
|
self.pending_messages[msg.id] = message
|
||||||
|
|
||||||
self.message_walker.append(message)
|
self.message_walker.append(message)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка создания виджета сообщения: {e}")
|
print(f"Ошибка создания виджета сообщения: {e}")
|
||||||
|
|
||||||
# Прокручиваем к последнему сообщению
|
# Восстанавливаем фокус
|
||||||
if self.message_walker:
|
if current_focus is not None and current_focus < len(self.message_walker):
|
||||||
|
self.message_list.set_focus(current_focus)
|
||||||
|
elif self.message_walker:
|
||||||
self.message_list.set_focus(len(self.message_walker) - 1)
|
self.message_list.set_focus(len(self.message_walker) - 1)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка обновления сообщений: {e}")
|
print(f"Ошибка обновления сообщений: {e}")
|
||||||
|
self.message_walker[:] = []
|
||||||
|
|
||||||
|
async def update_message_status(self, message):
|
||||||
|
"""Обновляет статус сообщения"""
|
||||||
|
try:
|
||||||
|
if message.id in self.pending_messages:
|
||||||
|
widget = self.pending_messages[message.id]
|
||||||
|
# Определяем статус
|
||||||
|
if getattr(message, 'from_id', None):
|
||||||
|
widget.status = "✓✓" # Доставлено
|
||||||
|
else:
|
||||||
|
widget.status = "✓" # Отправлено
|
||||||
|
widget.update_widget()
|
||||||
|
# Если сообщение доставлено, удаляем из отслеживания
|
||||||
|
if widget.status == "✓✓":
|
||||||
|
del self.pending_messages[message.id]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка обновления статуса: {e}")
|
||||||
|
|
||||||
|
async def check_chat_permissions(self, chat_id):
|
||||||
|
"""Проверяет права на отправку сообщений в чате"""
|
||||||
|
try:
|
||||||
|
# Получаем информацию о чате
|
||||||
|
chat = await self.telegram_client.get_entity(chat_id)
|
||||||
|
|
||||||
|
# Проверяем, является ли чат каналом
|
||||||
|
if hasattr(chat, 'broadcast') and chat.broadcast:
|
||||||
|
# Для каналов проверяем права администратора
|
||||||
|
participant = await self.telegram_client.get_permissions(chat)
|
||||||
|
self.can_send_messages = participant.is_admin
|
||||||
|
else:
|
||||||
|
# Для всех остальных чатов разрешаем отправку
|
||||||
|
self.can_send_messages = True
|
||||||
|
|
||||||
|
# Обновляем видимость поля ввода
|
||||||
|
self.update_input_visibility()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка проверки прав: {e}")
|
||||||
|
# В случае ошибки разрешаем отправку для не-каналов
|
||||||
|
self.can_send_messages = not (hasattr(chat, 'broadcast') and chat.broadcast)
|
||||||
|
self.update_input_visibility()
|
||||||
|
|
||||||
|
def update_input_visibility(self):
|
||||||
|
"""Обновляет видимость поля ввода"""
|
||||||
|
if self.can_send_messages:
|
||||||
|
if self.replying_to:
|
||||||
|
self.input_edit = InputEdit(('header', f"Ответ на '{self.replying_to[:30]}...': "))
|
||||||
|
else:
|
||||||
|
self.input_edit = InputEdit(('header', "Сообщение: "))
|
||||||
|
else:
|
||||||
|
self.input_edit = InputEdit(('input_disabled', "Отправка сообщений недоступна"))
|
||||||
|
self.input_edit.set_edit_text("")
|
||||||
|
|
||||||
|
# Обновляем правую панель
|
||||||
|
if len(self.right_panel.original_widget.widget_list) > 1:
|
||||||
|
self.right_panel.original_widget.widget_list[-1] = self.input_edit
|
||||||
|
|
||||||
async def handle_chat_input(self, key):
|
async def handle_chat_input(self, key):
|
||||||
"""Обработка ввода в экране чатов"""
|
"""Обработка ввода в экране чатов"""
|
||||||
if key == 'tab':
|
try:
|
||||||
if self.focused_element == "search":
|
if key == 'reply' and self.focused_element == "messages":
|
||||||
self.focused_element = "chat_list"
|
# Получаем выбранное сообщение
|
||||||
self.left_panel.original_widget.focus_position = 2
|
if self.message_walker and self.message_list.focus is not None:
|
||||||
# Обновляем список при переключении на чаты
|
msg_widget = self.message_walker[self.message_list.focus_position]
|
||||||
await self.update_chat_list()
|
self.selected_message = msg_widget.message_id
|
||||||
elif self.focused_element == "chat_list":
|
self.replying_to = msg_widget.text
|
||||||
if self.current_chat_id:
|
self.update_input_visibility()
|
||||||
self.focused_element = "messages"
|
# Переключаемся на ввод
|
||||||
self.chat_widget.focus_position = 1
|
|
||||||
self.right_panel.original_widget.focus_position = 0
|
|
||||||
# Обновляем сообщения при переключении на них
|
|
||||||
await self.update_message_list(self.current_chat_id)
|
|
||||||
else:
|
|
||||||
self.focused_element = "search"
|
|
||||||
self.left_panel.original_widget.focus_position = 1
|
|
||||||
elif self.focused_element == "messages":
|
|
||||||
self.focused_element = "input"
|
|
||||||
self.right_panel.original_widget.focus_position = 1
|
|
||||||
elif self.focused_element == "input":
|
|
||||||
self.focused_element = "search"
|
|
||||||
self.chat_widget.focus_position = 0
|
|
||||||
self.left_panel.original_widget.focus_position = 1
|
|
||||||
# Обновляем поиск при переключении на него
|
|
||||||
await self.update_chat_list()
|
|
||||||
|
|
||||||
elif key in ('up', 'down'):
|
|
||||||
if self.focused_element == "chat_list" and self.chat_walker:
|
|
||||||
if key == 'up':
|
|
||||||
if self.chat_list.focus_position > 0:
|
|
||||||
self.chat_list.focus_position -= 1
|
|
||||||
else:
|
|
||||||
if self.chat_list.focus_position < len(self.chat_walker) - 1:
|
|
||||||
self.chat_list.focus_position += 1
|
|
||||||
|
|
||||||
# Обновляем выделение
|
|
||||||
self.selected_chat_index = self.chat_list.focus_position
|
|
||||||
self.update_selected_chat()
|
|
||||||
|
|
||||||
# Если чат открыт, обновляем его содержимое
|
|
||||||
if self.current_chat_id:
|
|
||||||
focused = self.chat_walker[self.selected_chat_index]
|
|
||||||
self.current_chat_id = focused.chat_id
|
|
||||||
await self.update_message_list(focused.chat_id)
|
|
||||||
|
|
||||||
elif self.focused_element == "messages" and self.message_walker:
|
|
||||||
if key == 'up':
|
|
||||||
if self.message_list.focus_position > 0:
|
|
||||||
self.message_list.focus_position -= 1
|
|
||||||
else:
|
|
||||||
if self.message_list.focus_position < len(self.message_walker) - 1:
|
|
||||||
self.message_list.focus_position += 1
|
|
||||||
|
|
||||||
elif key == 'enter':
|
|
||||||
if self.focused_element == "search":
|
|
||||||
await self.update_chat_list()
|
|
||||||
self.focused_element = "chat_list"
|
|
||||||
self.left_panel.original_widget.focus_position = 2
|
|
||||||
elif self.focused_element == "chat_list" and self.chat_walker:
|
|
||||||
try:
|
|
||||||
focused = self.chat_walker[self.chat_list.focus_position]
|
|
||||||
self.current_chat_id = focused.chat_id
|
|
||||||
self.selected_chat_index = self.chat_list.focus_position
|
|
||||||
await self.update_message_list(focused.chat_id)
|
|
||||||
self.focused_element = "input"
|
self.focused_element = "input"
|
||||||
self.chat_widget.focus_position = 1
|
|
||||||
self.right_panel.original_widget.focus_position = 1
|
self.right_panel.original_widget.focus_position = 1
|
||||||
# Сбрасываем время последнего обновления сообщений
|
return
|
||||||
self.last_message_update_time = 0
|
|
||||||
except Exception as e:
|
elif key == 'esc':
|
||||||
print(f"Ошибка при открытии чата: {e}")
|
if self.replying_to:
|
||||||
elif self.focused_element == "input" and self.current_chat_id:
|
# Отменяем ответ
|
||||||
message = self.input_edit.get_edit_text()
|
self.selected_message = None
|
||||||
if message.strip():
|
self.replying_to = None
|
||||||
|
self.update_input_visibility()
|
||||||
|
return
|
||||||
|
if self.focused_element in ("input", "messages"):
|
||||||
|
# Закрываем текущий чат
|
||||||
|
self.current_chat_id = None
|
||||||
|
self.message_walker[:] = []
|
||||||
|
self.input_edit.set_edit_text("")
|
||||||
|
self.focused_element = "chat_list"
|
||||||
|
self.chat_widget.focus_position = 0
|
||||||
|
self.left_panel.original_widget.focus_position = 2
|
||||||
|
elif self.focused_element == "search":
|
||||||
|
self.search_edit.set_edit_text("")
|
||||||
|
await self.update_chat_list()
|
||||||
|
self.focused_element = "chat_list"
|
||||||
|
self.left_panel.original_widget.focus_position = 2
|
||||||
|
|
||||||
|
elif key == 'enter':
|
||||||
|
if self.focused_element == "chat_list" and self.chat_walker:
|
||||||
try:
|
try:
|
||||||
await self.telegram_client.send_message(self.current_chat_id, message)
|
focused = self.chat_walker[self.chat_list.focus_position]
|
||||||
self.input_edit.set_edit_text("")
|
if focused.chat_id != self.current_chat_id:
|
||||||
# Сразу обновляем сообщения после отправки
|
self.current_chat_id = focused.chat_id
|
||||||
self.last_message_update_time = 0
|
self.selected_chat_index = self.chat_list.focus_position
|
||||||
await self.update_message_list(self.current_chat_id)
|
|
||||||
|
# Проверяем права при открытии чата
|
||||||
|
await self.check_chat_permissions(focused.chat_id)
|
||||||
|
|
||||||
|
await self.update_message_list(focused.chat_id)
|
||||||
|
self.focused_element = "input"
|
||||||
|
self.chat_widget.focus_position = 1
|
||||||
|
self.right_panel.original_widget.focus_position = 1
|
||||||
|
self.last_message_update_time = 0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка отправки сообщения: {e}")
|
print(f"Ошибка при открытии чата: {e}")
|
||||||
|
|
||||||
|
elif self.focused_element == "input" and self.current_chat_id and self.can_send_messages:
|
||||||
|
message = self.input_edit.get_edit_text()
|
||||||
|
if message.strip():
|
||||||
|
try:
|
||||||
|
# Приостанавливаем автообновление на время отправки
|
||||||
|
self.last_update_time = datetime.datetime.now().timestamp()
|
||||||
|
self.last_message_update_time = self.last_update_time
|
||||||
|
|
||||||
|
# Создаем виджет сообщения до отправки
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
msg_widget = MessageWidget(
|
||||||
|
message_id=0, # Временный ID
|
||||||
|
text=message,
|
||||||
|
username="Я",
|
||||||
|
is_me=True,
|
||||||
|
send_time=now.strftime("%H:%M"),
|
||||||
|
status="⋯" # Отправляется
|
||||||
|
)
|
||||||
|
|
||||||
|
# Отправляем сообщение
|
||||||
|
sent_message = await self.telegram_client.send_message(
|
||||||
|
self.current_chat_id,
|
||||||
|
message,
|
||||||
|
reply_to=self.selected_message
|
||||||
|
)
|
||||||
|
|
||||||
|
# Обновляем ID сообщения и добавляем в отслеживание
|
||||||
|
msg_widget.message_id = sent_message.id
|
||||||
|
self.pending_messages[sent_message.id] = msg_widget
|
||||||
|
|
||||||
|
# Добавляем сообщение в список
|
||||||
|
self.message_walker.append(msg_widget)
|
||||||
|
self.message_list.set_focus(len(self.message_walker) - 1)
|
||||||
|
|
||||||
|
# Очищаем поле ввода и сбрасываем ответ
|
||||||
|
self.input_edit.set_edit_text("")
|
||||||
|
self.selected_message = None
|
||||||
|
self.replying_to = None
|
||||||
|
self.update_input_visibility()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка отправки сообщения: {e}")
|
||||||
|
|
||||||
|
elif key == 'tab':
|
||||||
|
if self.focused_element == "search":
|
||||||
|
self.focused_element = "chat_list"
|
||||||
|
self.left_panel.original_widget.focus_position = 2
|
||||||
|
elif self.focused_element == "chat_list":
|
||||||
|
if self.current_chat_id:
|
||||||
|
self.focused_element = "messages"
|
||||||
|
self.chat_widget.focus_position = 1
|
||||||
|
self.right_panel.original_widget.focus_position = 0
|
||||||
|
else:
|
||||||
|
self.focused_element = "search"
|
||||||
|
self.left_panel.original_widget.focus_position = 1
|
||||||
|
elif self.focused_element == "messages":
|
||||||
|
self.focused_element = "input"
|
||||||
|
self.right_panel.original_widget.focus_position = 1
|
||||||
|
elif self.focused_element == "input":
|
||||||
|
self.focused_element = "search"
|
||||||
|
self.chat_widget.focus_position = 0
|
||||||
|
self.left_panel.original_widget.focus_position = 1
|
||||||
|
|
||||||
|
elif key in ('up', 'down'):
|
||||||
|
if self.focused_element == "chat_list" and self.chat_walker:
|
||||||
|
if key == 'up':
|
||||||
|
if self.chat_list.focus_position > 0:
|
||||||
|
self.chat_list.focus_position -= 1
|
||||||
|
else:
|
||||||
|
if self.chat_list.focus_position < len(self.chat_walker) - 1:
|
||||||
|
self.chat_list.focus_position += 1
|
||||||
|
|
||||||
|
# Обновляем выделение
|
||||||
|
self.selected_chat_index = self.chat_list.focus_position
|
||||||
|
self.update_selected_chat()
|
||||||
|
|
||||||
|
# Если чат открыт, обновляем его содержимое
|
||||||
|
if self.current_chat_id:
|
||||||
|
focused = self.chat_walker[self.selected_chat_index]
|
||||||
|
if self.current_chat_id != focused.chat_id:
|
||||||
|
self.current_chat_id = focused.chat_id
|
||||||
|
await self.update_message_list(focused.chat_id)
|
||||||
|
|
||||||
|
elif self.focused_element == "messages" and self.message_walker:
|
||||||
|
if key == 'up':
|
||||||
|
if self.message_list.focus_position > 0:
|
||||||
|
self.message_list.focus_position -= 1
|
||||||
|
else:
|
||||||
|
if self.message_list.focus_position < len(self.message_walker) - 1:
|
||||||
|
self.message_list.focus_position += 1
|
||||||
|
|
||||||
elif key == 'esc':
|
except Exception as e:
|
||||||
if self.focused_element in ("input", "messages"):
|
print(f"Ошибка обработки ввода: {e}")
|
||||||
# Закрываем текущий чат
|
# Восстанавливаем состояние в случае ошибки
|
||||||
self.current_chat_id = None
|
self.focused_element = "chat_list"
|
||||||
self.message_walker[:] = []
|
self.chat_widget.focus_position = 0
|
||||||
self.input_edit.set_edit_text("")
|
|
||||||
self.focused_element = "chat_list"
|
|
||||||
self.chat_widget.focus_position = 0
|
|
||||||
self.left_panel.original_widget.focus_position = 2
|
|
||||||
elif self.focused_element == "search":
|
|
||||||
self.search_edit.set_edit_text("")
|
|
||||||
await self.update_chat_list()
|
|
||||||
self.focused_element = "chat_list"
|
|
||||||
self.left_panel.original_widget.focus_position = 2
|
|
||||||
|
|
||||||
def unhandled_input(self, key):
|
def unhandled_input(self, key):
|
||||||
"""Обработка необработанных нажатий клавиш"""
|
"""Обработка необработанных нажатий клавиш"""
|
||||||
@ -627,10 +984,12 @@ class TelegramTUI:
|
|||||||
async def chat_update_loop():
|
async def chat_update_loop():
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
current_time = datetime.datetime.now().timestamp()
|
# Обновляем только если не в процессе отправки сообщения
|
||||||
if current_time - self.last_update_time >= self.update_interval:
|
if not self.pending_messages:
|
||||||
await self.update_chat_list()
|
current_time = datetime.datetime.now().timestamp()
|
||||||
self.last_update_time = current_time
|
if current_time - self.last_update_time >= self.update_interval:
|
||||||
|
await self.update_chat_list()
|
||||||
|
self.last_update_time = current_time
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка в цикле обновления чатов: {e}")
|
print(f"Ошибка в цикле обновления чатов: {e}")
|
||||||
@ -639,7 +998,7 @@ class TelegramTUI:
|
|||||||
async def message_update_loop():
|
async def message_update_loop():
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
if self.current_chat_id:
|
if self.current_chat_id and not self.pending_messages:
|
||||||
current_time = datetime.datetime.now().timestamp()
|
current_time = datetime.datetime.now().timestamp()
|
||||||
if current_time - self.last_message_update_time >= self.message_update_interval:
|
if current_time - self.last_message_update_time >= self.message_update_interval:
|
||||||
await self.update_message_list(self.current_chat_id)
|
await self.update_message_list(self.current_chat_id)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user