from asyncio import Condition import telethon from telethon import events import resources import os import curses from subprocess import call import drawtool import emojis import shlex import sqlite3 from telethon.utils import get_display_name import datetime import re from tttcutils import debug, show_stacktrace import subprocess class MainView(): def __init__(self, client, stdscr): self.stdscr = stdscr self.client = client self.inputevent = Condition() self.client.add_event_handler(self.on_message, events.NewMessage) self.client.add_event_handler(self.on_user_update, events.UserUpdate) # TODO # self.client.add_event_handler(self.on_read, events.MessageRead) self.text_emojis = True self.macros = {} self.macro_recording = None self.macro_sequence = [] self.inputs = "" self.inputs_cursor = 0 self.edit_message = None self.popup = None self.drawtool = drawtool.Drawtool(self) self.fin = False from config import colors as colorconfig self.colors = colorconfig.get_colors() self.ready = False self.search_result = None self.search_index = None self.search_box = "" self.vimline_box = "" self.command_box = "" # index corresponds to the index in self.dialogs self.selected_chat = 0 # index offset self.selected_chat_offset = 0 self.selected_message = None self.mode = "normal" self.modestack = [] async def quit(self): self.fin = True with await self.inputevent: self.inputevent.notify() async def on_user_update(self, event): user_id = event.user_id if event.online != None: for dialog in self.dialogs: if event.online == True: dialog["online_until"] = event.until elif dialog["online_until"]: now = datetime.datetime.now().astimezone() until = dialog["online_until"].astimezone() if (now - until).seconds > 0: dialog["online_until"] = None dialog["online"] = False if dialog["dialog"].entity.id == user_id: dialog["online"] = event.online async def on_message(self, event): # move chats with news up for idx, dialog in enumerate(self.dialogs): if dialog["dialog"].id == event.chat_id: # stuff to do upon arriving messages newmessage = await self.client.get_messages(dialog["dialog"], 1) dialog["messages"].insert(0, newmessage[0]) if not event.out: dialog["unread_count"] += 1 os.system(f"notify-send -i apps/telegram \"{dialog['dialog'].name}\" \"{newmessage[0].message}\"") front = self.dialogs.pop(idx) self.dialogs = [front] + self.dialogs break #old dead code # # auto adjust relative replys to match shifted message offsets # if event.chat_id == self.dialogs[self.selected_chat]["dialog"].id: # if self.inputs.startswith("r"): # num = self.inputs[1:].split()[0] # try: ## num = int(s[1:].split()[0]) # number = int(num) # msg = self.inputs.replace("r" + num, "r" + str(number+1)) # self.inputs = msg # except: # pass # dont switch the dialoge upon arriving messages if idx == self.selected_chat: self.selected_chat = 0 elif idx > self.selected_chat: self.selected_chat += 1 elif idx < self.selected_chat: pass await self.drawtool.redraw() async def textinput(self): self.inputs = "" with await self.inputevent: await self.inputevent.wait() if self.fin: return "" out = self.inputs self.inputs = "" return out async def run(self): try: chats = await self.client.get_dialogs() except sqlite3.OperationalError: self.stdscr.addstr("Database is locked. Cannot connect with this session. Aborting") self.stdscr.refresh() await self.textinput() await self.quit() self.dialogs = [ { "dialog": dialog, "unread_count": dialog.unread_count, "online": dialog.entity.status.to_dict()["_"] == "UserStatusOnline" if hasattr(dialog.entity, "status") and dialog.entity.status else None, "online_until": None, # "last_seen": dialog.entity.status.to_dict()["was_online"] if online == False else None, } for dialog in chats ] await self.drawtool.redraw() self.ready = True while True: s = await self.textinput() if self.fin: return if s.startswith("r"): try: num = int(s[1:].split()[0]) except: continue s = s.replace("r" + str(num) + " ", "") reply_msg = self.dialogs[self.selected_chat]["messages"][num] s = emojis.encode(s) reply = await reply_msg.reply(s) await self.on_message(reply) elif s.startswith("media"): try: num = int(s[5:].split()[0]) except: continue message = self.dialogs[self.selected_chat]["messages"][num] if message.media: os.makedirs("/tmp/tttc/", exist_ok=True) path = await self.client.download_media(message.media, "/tmp/tttc/") # TODO mute calls if message.media.photo: sizes = message.media.photo.sizes w, h = sizes[0].w, sizes[0].h # w, h basesize = 1500 w3m_command=f"0;1;0;0;{basesize};{int(basesize*h/w)};;;;;{path}\n4;\n3;" W3MIMGDISPLAY="/usr/lib/w3m/w3mimgdisplay" os.system(f"echo -e '{w3m_command}' | {W3MIMGDISPLAY} & disown") await self.textinput() else: subprocess.call(["xdg-open", "{shlex.quote(path)}"], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL) #os.system(f"(xdg-open {shlex.quote(path)} 2>&1 > /dev/null) & disown") else: s = emojis.encode(s) outgoing_message = await self.dialogs[self.selected_chat]["dialog"].send_message(s) await self.on_message(outgoing_message) await self.drawtool.redraw() def select_next_chat(self): # if wrapping not allowed: # self.selected_chat = min(self.selected_chat + 1, len(self.dialogs) - 1) self.selected_chat = (self.selected_chat + 1) % (len(self.dialogs)) self.center_selected_chat() def select_prev_chat(self): # if wrapping not allowed: # self.selected_chat = max(self.selected_chat - 1, 0) self.selected_chat = (self.selected_chat - 1) % (len(self.dialogs)) self.center_selected_chat() def center_selected_chat(self): if self.selected_chat < self.drawtool.chats_num // 2: self.selected_chat_offset = 0 elif self.selected_chat > len(self.dialogs) - self.drawtool.chats_num // 2: self.selected_chat_offset = len(self.dialogs) - self.drawtool.chats_num else: self.selected_chat_offset = self.selected_chat - self.drawtool.chats_num // 2 def select_chat(self, index): if index < -1 or index >= len(self.dialogs): return if index == -1: index = len(self.dialogs) - 1 while index < self.selected_chat: self.select_prev_chat() else: while index > self.selected_chat: self.select_next_chat() def is_subsequence(self, xs, ys): xs = list(xs) for y in ys: if xs and xs[0] == y: xs.pop(0) return not xs def search_chats(self, query = None): if query is None: query = self.search_box if query is None: return # we dont search for "" filter_function = self.is_subsequence filter_function = lambda x, y: x in y self.search_result = [ idx for (idx, dialog) in enumerate(self.dialogs) if filter_function(query.lower(), get_display_name(dialog["dialog"].entity).lower())] self.search_index = -1 def search_next(self): if not self.search_result: return if self.search_index == -1: import bisect self.search_index = bisect.bisect_left(self.search_result, self.selected_chat) self.select_chat(self.search_result[self.search_index % len(self.search_result)]) self.center_selected_chat() return self.search_index = (self.search_index + 1) % len(self.search_result) index = self.search_result[self.search_index] self.select_chat(index) self.center_selected_chat() def search_prev(self): if not self.search_result: return if self.search_index == -1: import bisect self.search_index = bisect.bisect_right(self.search_result, self.selected_chat) self.select_chat(self.search_result[self.search_index]) self.center_selected_chat() return self.search_index = (self.search_index - 1) % len(self.search_result) self.select_chat(self.search_result[self.search_index % len(self.search_result)]) self.center_selected_chat() async def call_command(self): command = self.vimline_box if command == "q": await self.quit() elif command == "pfd": m = "" for i in range(len(self.inputs)): m += self.inputs[i].lower() if i%2==0 else self.inputs[i].lower().swapcase() self.inputs = m async def send_message(self): if not self.inputs: return s = self.inputs s = emojis.encode(s) outgoing_message = await self.dialogs[self.selected_chat]["dialog"].send_message(s) await self.on_message(outgoing_message) await self.mark_read() self.center_selected_chat() self.inputs = "" async def mark_read(self): chat = self.dialogs[self.selected_chat] dialog = chat["dialog"] lastmessage = chat["messages"][0] await self.client.send_read_acknowledge(dialog, lastmessage) self.dialogs[self.selected_chat]["unread_count"] = 0 async def show_media(self, num = None): if not num: return message = self.dialogs[self.selected_chat]["messages"][num] if message.media: os.makedirs("/tmp/tttc/", exist_ok=True) # TODO test if file exists, ask for confirmation to replace or download again path = await self.client.download_media(message.media, "/tmp/tttc/") if hasattr(message.media, "photo") and False: sizes = message.media.photo.sizes w, h = sizes[0].w, sizes[0].h # w, h basesize = 300 w3m_command=f"0;1;0;0;{basesize};{int(basesize*h/w)};;;;;{path}\n4;\n3;" W3MIMGDISPLAY="/usr/lib/w3m/w3mimgdisplay" echo_sp = subprocess.Popen(["echo", "-e", f"{w3m_command}"], stdout = subprocess.PIPE) w3m_sp = subprocess.Popen([f"{W3MIMGDISPLAY}"], stdin = echo_sp.stdout) else: subprocess.Popen(["xdg-open", f"{path}"], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL) def popup_message(self, question): self.modestack.append(self.mode) self.mode = "popupmessage" async def action_handler(self, key): pass self.popup = (action_handler, question) def spawn_popup(self, action_handler, question): # on q press self.modestack.append(self.mode) self.mode = "popup" self.popup = (action_handler, question) async def handle_key(self, key, redraw = True): if self.mode == "popupmessage": self.mode = self.modestack.pop() if not self.ready: return if key == "RESIZE": await self.drawtool.resize() return if self.macro_recording: if key != "q": self.macro_sequence.append(key) if self.mode == "search": if key == "ESCAPE" or key == "RETURN": self.mode = "normal" elif key == "BACKSPACE": if self.search_box == "": self.mode = "normal" else: self.search_box = self.search_box[0:-1] self.search_chats() self.search_next() else: self.search_box += key self.search_chats() self.search_next() elif self.mode == "vimmode": if key == "ESCAPE": self.mode = "normal" elif key == "RETURN": await self.call_command() self.vimline_box = "" self.mode = "normal" elif key == "BACKSPACE": if self.vimline_box == "": self.mode = "normal" else: self.vimline_box = self.vimline_box[0:-1] else: self.vimline_box += key elif self.mode == "normal": num = None try: num = int(key) except: pass if num is not None: self.command_box += str(num) await self.drawtool.redraw() return elif key == ":": self.mode = "vimmode" self.vimline_box = "" elif key == "RETURN" or key == "y": await self.send_message() elif key == "Q": await self.quit() elif key == "q": if self.macro_recording == None: # start macro recording async def record_macro(self, key): if "a" < key.lower() < "z": self.macro_recording = key self.popup_message(f"recording into {key}") else: self.popup_message(f"Register must be [a-zA-Z]") self.spawn_popup(record_macro, "Record into which register?") else: # end macro recording self.macros[self.macro_recording] = self.macro_sequence self.macro_recording = None self.macro_sequence = [] elif key == "@": # execute macro async def ask_macro(self, key): if key in self.macros.keys(): macro = self.macros[key] debug(macro) for k in macro: await self.handle_key(k, redraw = False) else: self.popup_message(f"No such macro @{key}") self.spawn_popup(ask_macro, "Execute which macro?") elif key == "C": self.select_prev_chat() elif key == "c": self.select_next_chat() elif key == "E": self.text_emojis ^= True elif key == "R": await self.mark_read() elif key == "d": if self.command_box: try: n = int(self.command_box) except: return if n >= len(self.dialogs[self.selected_chat]["messages"]): #TODO: alert user self.popup_message("No message by that id.") await self.drawtool.redraw() return async def action_handler(self, key): if key in ["y","Y"]: to_delete = self.dialogs[self.selected_chat]["messages"][n] await to_delete.delete() self.dialogs[self.selected_chat]["messages"].pop(n) self.command_box = "" self.mode = "normal" question = f"Are you really sure you want to delete message {n}? [y/N]" self.spawn_popup(action_handler, question) await self.drawtool.redraw() elif key == "e": if self.command_box: try: n = int(self.command_box) except: return self.edit_message = self.dialogs[self.selected_chat]["messages"][n] self.mode = "edit" self.inputs = emojis.decode(self.edit_message.text) self.command_box = "" elif key == "r": if self.command_box: try: n = int(self.command_box) except: return reply_to = self.dialogs[self.selected_chat]["messages"][n] s = emojis.encode(self.inputs) reply = await reply_to.reply(s) await self.on_message(reply) self.command_box = "" self.inputs = "" elif key == "m": if self.command_box: try: n = int(self.command_box) except: return self.command_box = "" await self.show_media(n) elif key == "M": self.center_selected_chat() elif key == "HOME" or key == "g": self.select_chat(0) elif key == "END" or key == "G": self.select_chat(-1) elif key == "i": self.mode = "insert" elif key == "n": self.search_next() elif key == "N": self.search_prev() elif key == "/": self.mode = "search" self.search_box = "" elif key == " ": self.drawtool.show_indices ^= True elif self.mode == "popup": action, _ = self.popup # I think this could break self.mode = self.modestack.pop() await action(self, key) elif self.mode == "edit": if key == "ESCAPE": async def ah(self, key): if key in ["Y", "y", "RETURN"]: edit = await self.edit_message.edit(self.inputs) await self.on_message(edit) # TODO: update message in chat # this on_message call does not work reliably self.mode = "normal" else: self.popup_message("Edit discarded.") self.mode = "normal" self.spawn_popup(ah, "Do you want to save the edit? [Y/n]") elif key == "LEFT": self.insert_move_left() elif key == "RIGHT": self.insert_move_right() elif key == "BACKSPACE": self.inputs = self.inputs[0:-1] elif key == "RETURN": self.inputs += "\n" else: self.inputs += key elif self.mode == "insert": if key == "ESCAPE": self.mode = "normal" elif key == "LEFT": self.insert_move_left() elif key == "RIGHT": self.insert_move_right() elif key == "BACKSPACE": self.inputs = self.inputs[0:-1] elif key == "RETURN": self.inputs += "\n" else: self.inputs += key self.command_box = "" if redraw: await self.drawtool.redraw() def insert_move_left(self): self.inputs_cursor = max(0, self.cursor - 1) def insert_move_right(self): self.inputs_cursor = min(len(self.inputs), self.cursor + 1) async def handle_key_old(self, key): if key == "RETURN": with await self.inputevent: self.inputevent.notify() elif key == "": chat = self.dialogs[self.selected_chat]["dialog"] last_message = self.dialogs[self.selected_chat]["messages"][0] await self.client.send_read_acknowledge(chat, max_id=last_message.id) self.dialogs[self.selected_chat]["unread_count"] = 0 else: self.inputs += key self.drawtool.redraw()