TTTC/mainview.py
2019-10-31 23:56:18 +01:00

550 lines
21 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()