489 lines
18 KiB
Python
489 lines
18 KiB
Python
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.inputs = ""
|
|
self.cursor = 0
|
|
|
|
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"
|
|
|
|
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
|
|
front = self.dialogs.pop(idx)
|
|
self.dialogs = [front] + self.dialogs
|
|
break
|
|
# 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.mode = "popup"
|
|
async def action_handler(self, key):
|
|
self.mode = "normal"
|
|
self.popup = (action_handler, question)
|
|
|
|
def spawn_popup(self, action_handler, question):
|
|
self.mode = "popup"
|
|
self.popup = (action_handler, question)
|
|
|
|
async def handle_key(self, key):
|
|
if not self.ready:
|
|
return
|
|
if key == "RESIZE":
|
|
await self.drawtool.resize()
|
|
return
|
|
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 == "D":
|
|
# for i in range(10):
|
|
# self.select_prev_chat()
|
|
#elif key == "d":
|
|
# for i in range(10):
|
|
# self.select_next_chat()
|
|
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 == "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
|
|
await action(self, key)
|
|
elif self.mode == "insert":
|
|
if key == "ESCAPE":
|
|
self.mode = "normal"
|
|
elif key == "LEFT":
|
|
self.insert_move_left()
|
|
elif key == "UP":
|
|
self.drawtool.move_cursor_up()
|
|
elif key == "RIGHT":
|
|
self.insert_move_right()
|
|
elif key == "HOME":
|
|
self.drawtool.move_cursor_home()
|
|
#self.cursor = 0
|
|
elif key == "END":
|
|
self.drawtool.move_cursor_end()
|
|
#self.cursor = len(self.inputs)
|
|
elif key == "DEL":
|
|
try:
|
|
if len(self.inputs) > self.cursor:
|
|
inp = list(self.inputs)
|
|
inp.pop(self.cursor)
|
|
self.inputs = "".join(inp)
|
|
except:
|
|
debug(f"{self.cursor}, {self.inputs}")
|
|
show_stacktrace()
|
|
elif key == "BACKSPACE":
|
|
try:
|
|
if len(self.inputs) > 0:
|
|
inp = list(self.inputs)
|
|
inp.pop(self.cursor - 1)
|
|
self.inputs = "".join(inp)
|
|
self.insert_move_left()
|
|
except:
|
|
debug(f"{self.cursor}, {self.inputs}")
|
|
show_stacktrace()
|
|
else:
|
|
if key == "RETURN":
|
|
key = "\n"
|
|
inp = list(self.inputs)
|
|
inp.insert(self.cursor, key)
|
|
self.inputs = "".join(inp)
|
|
self.cursor += 1
|
|
self.command_box = ""
|
|
await self.drawtool.redraw()
|
|
|
|
def insert_move_left(self):
|
|
self.cursor = max(0, self.cursor - 1)
|
|
|
|
def insert_move_right(self):
|
|
self.cursor = min(len(self.inputs), self.cursor + 1)
|