Compare commits
8 Commits
cursor
...
investigat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f052c47d09 | ||
|
|
19af89c220 | ||
|
|
0496171d20 | ||
|
|
e2e9d5b2a1 | ||
|
|
2ab8dac662 | ||
|
|
f2f78cf0fd | ||
|
|
0a82d58de7 | ||
|
|
3ea653fc6f |
22
README.md
22
README.md
@@ -14,4 +14,24 @@ You can read more about how to get them [here](https://core.telegram.org/api/obt
|
|||||||
Once you obtained your own api key and hash, you need to set them as your environment variables
|
Once you obtained your own api key and hash, you need to set them as your environment variables
|
||||||
`TTTC_API_ID` and `TTTC_API_HASH`, respectively.
|
`TTTC_API_ID` and `TTTC_API_HASH`, respectively.
|
||||||
|
|
||||||
The client can be run with `python3 tttc.py`.
|
The client can be run with `python3 tttc.py`.
|
||||||
|
|
||||||
|
## Keybindings
|
||||||
|
Currently, there is no way of changing the keybindings in a config. This is subject to change in a future update.
|
||||||
|
|
||||||
|
The default key bindings are
|
||||||
|
|
||||||
|
Key | Function
|
||||||
|
--|--
|
||||||
|
i| Enter insert mode (to compose a message)
|
||||||
|
y, Return | Send message
|
||||||
|
Esc | Cancel, Exit current mode
|
||||||
|
c/C | Previous/Next Dialog
|
||||||
|
E | Toggle emoji ASCII display
|
||||||
|
`n` e | Edit message `n` (ESC to open prompt to save changes)
|
||||||
|
`n` r | Reply to message `n` (submit draft)
|
||||||
|
`n` d | Delete message `n`
|
||||||
|
/ | enter search mode
|
||||||
|
n/N | Previous/Next search result
|
||||||
|
Q | exit TTTC
|
||||||
|
q `r` | Record macro into register `r`
|
||||||
|
|||||||
52
authview.py
52
authview.py
@@ -22,6 +22,7 @@ class AuthView():
|
|||||||
self.inputs = ""
|
self.inputs = ""
|
||||||
self.w, self.h = curses.COLS, curses.LINES
|
self.w, self.h = curses.COLS, curses.LINES
|
||||||
self.fin = False
|
self.fin = False
|
||||||
|
self.showinput = True
|
||||||
|
|
||||||
|
|
||||||
async def textinput(self):
|
async def textinput(self):
|
||||||
@@ -47,36 +48,44 @@ class AuthView():
|
|||||||
self.stdscr.refresh()
|
self.stdscr.refresh()
|
||||||
self.phone = await self.textinput()
|
self.phone = await self.textinput()
|
||||||
try:
|
try:
|
||||||
response = await self.client.send_code_request(self.phone)
|
response = await self.client.send_code_request(self.phone.replace("+","00").replace(" ",""))
|
||||||
if not response.phone_registered:
|
if not response.phone_registered:
|
||||||
self.stdscr.addstr("This phone number is not registered in telegram. ")
|
self.stdscr.addstr("This phone number is not registered in telegram. ")
|
||||||
self.stdscr.refresh()
|
self.stdscr.refresh()
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
except telethon.errors.rpcerrorlist.FloodWaitError as err:
|
except telethon.errors.rpcerrorlist.FloodWaitError as err:
|
||||||
self.stdscr.addstr(f"The telegram servers blocked you for too many retries ({err.seconds}s remaining). ")
|
self.stdscr.addstr(f"The telegram servers blocked you for too many retries. ({err.seconds}s remaining).")
|
||||||
self.stdscr.refresh()
|
self.stdscr.refresh()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.stdscr.addstr("Incorrect phone number. ")
|
self.stdscr.addstr(f"An error occured: {str(e)}")
|
||||||
self.stdscr.refresh()
|
self.stdscr.refresh()
|
||||||
self.stdscr.addstr("auth with code now.")
|
self.stdscr.addstr("Now authentificate with the code telegram sent to you.")
|
||||||
self.stdscr.refresh()
|
self.stdscr.refresh()
|
||||||
self.code = await self.textinput()
|
while True:
|
||||||
try:
|
done = False
|
||||||
await self.client.sign_in(self.phone, self.code)
|
|
||||||
except telethon.errors.SessionPasswordNeededError:
|
|
||||||
self.stdscr.addstr("Password required to log in")
|
|
||||||
self.stdscr.refresh()
|
|
||||||
self.passwd = await self.textinput()
|
|
||||||
try:
|
try:
|
||||||
await self.client.sign_in(password=self.passwd)
|
self.code = await self.textinput()
|
||||||
# TODO: debug me
|
await self.client.sign_in(self.phone.replace("+","00").replace(" ",""), self.code)
|
||||||
except:
|
except telethon.errors.rpcerrorlist.PhoneCodeInvalidError:
|
||||||
show_stacktrace()
|
self.stdscr.addstr("The authentification code was wrong. Please try again.")
|
||||||
except telethon.errors.rpcerrorlist.PhoneCodeInvalidError:
|
self.stdscr.refresh()
|
||||||
pass
|
except telethon.errors.SessionPasswordNeededError:
|
||||||
|
self.showinput = False
|
||||||
self.stdscr.addstr(f"auth successful. ")
|
self.stdscr.addstr("A 2FA password is required to log in.")
|
||||||
|
self.stdscr.refresh()
|
||||||
|
while True:
|
||||||
|
self.passwd = await self.textinput()
|
||||||
|
try:
|
||||||
|
await self.client.sign_in(password=self.passwd)
|
||||||
|
done = True
|
||||||
|
break
|
||||||
|
except telethon.errors.PasswordHashInvalidError:
|
||||||
|
self.stdscr.addstr("Incorrect password. Try again.")
|
||||||
|
self.stdscr.refresh()
|
||||||
|
if done:
|
||||||
|
break
|
||||||
|
self.stdscr.addstr("Authentification successfull. Please wait until the client has finished loading.")
|
||||||
self.stdscr.refresh()
|
self.stdscr.refresh()
|
||||||
|
|
||||||
async def handle_key(self, key):
|
async def handle_key(self, key):
|
||||||
@@ -87,7 +96,10 @@ class AuthView():
|
|||||||
self.inputs = self.inputs[0:-1]
|
self.inputs = self.inputs[0:-1]
|
||||||
else:
|
else:
|
||||||
self.inputs += key
|
self.inputs += key
|
||||||
self.stdscr.addstr(20, 50, self.inputs)
|
if self.showinput:
|
||||||
|
self.stdscr.addstr(20, 50, self.inputs)
|
||||||
|
else:
|
||||||
|
self.stdscr.addstr(20, 50, "*"*len(self.inputs))
|
||||||
self.stdscr.clrtoeol()
|
self.stdscr.clrtoeol()
|
||||||
self.stdscr.refresh()
|
self.stdscr.refresh()
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ def handle():
|
|||||||
messaging.add_argument("--me", action="store_true", help="Send the message to yourself")
|
messaging.add_argument("--me", action="store_true", help="Send the message to yourself")
|
||||||
messaging.add_argument("--target", "-t", metavar="chatid", type=int, help="Send the message to the given chat")
|
messaging.add_argument("--target", "-t", metavar="chatid", type=int, help="Send the message to the given chat")
|
||||||
messaging.add_argument("--message", "--msg", "-m", help="Provide a message.")
|
messaging.add_argument("--message", "--msg", "-m", help="Provide a message.")
|
||||||
messaging.add_argument("--stdin", "-i", action="store_true", help="Read a message from stdin.")
|
|
||||||
parsed = parser.parse_args()
|
parsed = parser.parse_args()
|
||||||
global debug
|
global debug
|
||||||
if parsed.verbose:
|
if parsed.verbose:
|
||||||
@@ -58,6 +57,8 @@ def handle():
|
|||||||
debug("Fetching chats...", file=sys.stderr)
|
debug("Fetching chats...", file=sys.stderr)
|
||||||
chats = client.get_dialogs()
|
chats = client.get_dialogs()
|
||||||
|
|
||||||
|
stdinput = "".join([line for line in sys.stdin])
|
||||||
|
|
||||||
filtered = chats if parsed.list else filter_chats((parsed.startswith, parsed.contains, parsed.matches))
|
filtered = chats if parsed.list else filter_chats((parsed.startswith, parsed.contains, parsed.matches))
|
||||||
unique = None
|
unique = None
|
||||||
if filtered:
|
if filtered:
|
||||||
@@ -67,12 +68,12 @@ def handle():
|
|||||||
for result in reversed(filtered):
|
for result in reversed(filtered):
|
||||||
print(str(result.id).rjust(16) + " "*4 + result.name)
|
print(str(result.id).rjust(16) + " "*4 + result.name)
|
||||||
else:
|
else:
|
||||||
if not (parsed.message or parsed.stdin):
|
if not (parsed.message or stdinput):
|
||||||
for result in reversed(filtered):
|
for result in reversed(filtered):
|
||||||
print(str(result.id).rjust(16) + " "*4 + result.name)
|
print(str(result.id).rjust(16) + " "*4 + result.name)
|
||||||
exit()
|
exit()
|
||||||
unique = filtered[0].id
|
unique = filtered[0].id
|
||||||
if not (parsed.message or parsed.stdin):
|
if not (parsed.message or stdinput):
|
||||||
exit() # we are done here
|
exit() # we are done here
|
||||||
|
|
||||||
recipient = unique or parsed.target
|
recipient = unique or parsed.target
|
||||||
@@ -92,11 +93,11 @@ def handle():
|
|||||||
# print("Illegal entity id. Aborting.", file=sys.stderr)
|
# print("Illegal entity id. Aborting.", file=sys.stderr)
|
||||||
# exit(1)
|
# exit(1)
|
||||||
|
|
||||||
if parsed.message or parsed.stdin:
|
#if parsed.message or parsed.stdin:
|
||||||
send_message(client, recipient, message=parsed.message)
|
send_message(client, recipient, message=parsed.message, stdinput=stdinput)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def send_message(client, chat_id, message):
|
def send_message(client, chat_id, message, stdinput):
|
||||||
#print(f"call to {client} {chat_id} {message}")
|
#print(f"call to {client} {chat_id} {message}")
|
||||||
try:
|
try:
|
||||||
recipient = client.get_input_entity(chat_id)
|
recipient = client.get_input_entity(chat_id)
|
||||||
@@ -104,13 +105,14 @@ def send_message(client, chat_id, message):
|
|||||||
print("Could not find the entity for this entity id. Aborting.", file=sys.stderr)
|
print("Could not find the entity for this entity id. Aborting.", file=sys.stderr)
|
||||||
exit(1)
|
exit(1)
|
||||||
debug("Chat exists. Sending message.", file=sys.stderr)
|
debug("Chat exists. Sending message.", file=sys.stderr)
|
||||||
if message is None:
|
if message and stdinput:
|
||||||
debug("No message specified. Reading from stdin.", file=sys.stderr)
|
out = f"{message}\n{stdinput}"
|
||||||
message = "".join([line for line in sys.stdin])
|
else:
|
||||||
if message.strip() == "":
|
out = message or stdinput
|
||||||
|
if not out.strip():
|
||||||
print("The message must not be empty.", file=sys.stderr)
|
print("The message must not be empty.", file=sys.stderr)
|
||||||
exit()
|
exit()
|
||||||
client.send_message(chat_id, message)
|
client.send_message(chat_id, out)
|
||||||
|
|
||||||
def filter_chats(filt):
|
def filter_chats(filt):
|
||||||
if filt == (None, None, None):
|
if filt == (None, None, None):
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class Drawtool():
|
|||||||
self.stdscr.addstr(self.H - 1, 0, "/" + self.main_view.search_box, self.main_view.colors["error"])
|
self.stdscr.addstr(self.H - 1, 0, "/" + self.main_view.search_box, self.main_view.colors["error"])
|
||||||
else:
|
else:
|
||||||
self.stdscr.addstr(self.H - 1, 0, "/" + self.main_view.search_box)
|
self.stdscr.addstr(self.H - 1, 0, "/" + self.main_view.search_box)
|
||||||
elif self.main_view.mode == "popup":
|
elif self.main_view.mode in ["popup", "popupmessage"]:
|
||||||
_, question = self.main_view.popup
|
_, question = self.main_view.popup
|
||||||
self.stdscr.addstr(self.H - 1, 0, question)
|
self.stdscr.addstr(self.H - 1, 0, question)
|
||||||
elif self.main_view.mode == "vimmode":
|
elif self.main_view.mode == "vimmode":
|
||||||
@@ -91,7 +91,7 @@ class Drawtool():
|
|||||||
for index, line in enumerate(self._get_input_lines(self.main_view.inputs, width = self.W - 4)[-self.input_lines:]):
|
for index, line in enumerate(self._get_input_lines(self.main_view.inputs, width = self.W - 4)[-self.input_lines:]):
|
||||||
self.stdscr.addstr(self.H - self.input_lines - 2 + index, 2, f"{line}")
|
self.stdscr.addstr(self.H - self.input_lines - 2 + index, 2, f"{line}")
|
||||||
|
|
||||||
if self.main_view.mode == "insert":
|
if self.main_view.mode in ["insert", "edit"]:
|
||||||
curses.curs_set(1)
|
curses.curs_set(1)
|
||||||
y, x = self._get_cursor_position(self.main_view.inputs, width = self.W - 4)
|
y, x = self._get_cursor_position(self.main_view.inputs, width = self.W - 4)
|
||||||
self.stdscr.move(self.H - self.input_lines - 2 + y, 2 + x)
|
self.stdscr.move(self.H - self.input_lines - 2 + y, 2 + x)
|
||||||
|
|||||||
97
mainview.py
97
mainview.py
@@ -27,9 +27,15 @@ class MainView():
|
|||||||
# self.client.add_event_handler(self.on_read, events.MessageRead)
|
# self.client.add_event_handler(self.on_read, events.MessageRead)
|
||||||
self.text_emojis = True
|
self.text_emojis = True
|
||||||
|
|
||||||
|
self.macros = {}
|
||||||
|
self.macro_recording = None
|
||||||
|
self.macro_sequence = []
|
||||||
|
|
||||||
self.inputs = ""
|
self.inputs = ""
|
||||||
self.inputs_cursor = 0
|
self.inputs_cursor = 0
|
||||||
|
|
||||||
|
self.edit_message = None
|
||||||
|
|
||||||
self.popup = None
|
self.popup = None
|
||||||
|
|
||||||
self.drawtool = drawtool.Drawtool(self)
|
self.drawtool = drawtool.Drawtool(self)
|
||||||
@@ -52,6 +58,7 @@ class MainView():
|
|||||||
self.selected_message = None
|
self.selected_message = None
|
||||||
|
|
||||||
self.mode = "normal"
|
self.mode = "normal"
|
||||||
|
self.modestack = []
|
||||||
|
|
||||||
async def quit(self):
|
async def quit(self):
|
||||||
self.fin = True
|
self.fin = True
|
||||||
@@ -301,21 +308,29 @@ class MainView():
|
|||||||
subprocess.Popen(["xdg-open", f"{path}"], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL)
|
subprocess.Popen(["xdg-open", f"{path}"], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL)
|
||||||
|
|
||||||
def popup_message(self, question):
|
def popup_message(self, question):
|
||||||
self.mode = "popup"
|
self.modestack.append(self.mode)
|
||||||
|
self.mode = "popupmessage"
|
||||||
async def action_handler(self, key):
|
async def action_handler(self, key):
|
||||||
self.mode = "normal"
|
pass
|
||||||
self.popup = (action_handler, question)
|
self.popup = (action_handler, question)
|
||||||
|
|
||||||
def spawn_popup(self, action_handler, question):
|
def spawn_popup(self, action_handler, question):
|
||||||
|
# on q press
|
||||||
|
self.modestack.append(self.mode)
|
||||||
self.mode = "popup"
|
self.mode = "popup"
|
||||||
self.popup = (action_handler, question)
|
self.popup = (action_handler, question)
|
||||||
|
|
||||||
async def handle_key(self, key):
|
async def handle_key(self, key, redraw = True):
|
||||||
|
if self.mode == "popupmessage":
|
||||||
|
self.mode = self.modestack.pop()
|
||||||
if not self.ready:
|
if not self.ready:
|
||||||
return
|
return
|
||||||
if key == "RESIZE":
|
if key == "RESIZE":
|
||||||
await self.drawtool.resize()
|
await self.drawtool.resize()
|
||||||
return
|
return
|
||||||
|
if self.macro_recording:
|
||||||
|
if key != "q":
|
||||||
|
self.macro_sequence.append(key)
|
||||||
if self.mode == "search":
|
if self.mode == "search":
|
||||||
if key == "ESCAPE" or key == "RETURN":
|
if key == "ESCAPE" or key == "RETURN":
|
||||||
self.mode = "normal"
|
self.mode = "normal"
|
||||||
@@ -359,19 +374,41 @@ class MainView():
|
|||||||
self.vimline_box = ""
|
self.vimline_box = ""
|
||||||
elif key == "RETURN" or key == "y":
|
elif key == "RETURN" or key == "y":
|
||||||
await self.send_message()
|
await self.send_message()
|
||||||
elif key == "q":
|
elif key == "Q":
|
||||||
await self.quit()
|
await self.quit()
|
||||||
#elif key == "D":
|
elif key == "q":
|
||||||
# for i in range(10):
|
if self.macro_recording == None:
|
||||||
# self.select_prev_chat()
|
# start macro recording
|
||||||
#elif key == "d":
|
async def record_macro(self, key):
|
||||||
# for i in range(10):
|
if "a" < key.lower() < "z":
|
||||||
# self.select_next_chat()
|
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":
|
elif key == "C":
|
||||||
self.select_prev_chat()
|
self.select_prev_chat()
|
||||||
elif key == "c":
|
elif key == "c":
|
||||||
self.select_next_chat()
|
self.select_next_chat()
|
||||||
elif key == "e":
|
elif key == "E":
|
||||||
self.text_emojis ^= True
|
self.text_emojis ^= True
|
||||||
elif key == "R":
|
elif key == "R":
|
||||||
await self.mark_read()
|
await self.mark_read()
|
||||||
@@ -397,6 +434,16 @@ class MainView():
|
|||||||
self.spawn_popup(action_handler, question)
|
self.spawn_popup(action_handler, question)
|
||||||
|
|
||||||
await self.drawtool.redraw()
|
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":
|
elif key == "r":
|
||||||
if self.command_box:
|
if self.command_box:
|
||||||
try:
|
try:
|
||||||
@@ -436,7 +483,32 @@ class MainView():
|
|||||||
self.drawtool.show_indices ^= True
|
self.drawtool.show_indices ^= True
|
||||||
elif self.mode == "popup":
|
elif self.mode == "popup":
|
||||||
action, _ = self.popup
|
action, _ = self.popup
|
||||||
|
# I think this could break
|
||||||
|
self.mode = self.modestack.pop()
|
||||||
await action(self, key)
|
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":
|
elif self.mode == "insert":
|
||||||
if key == "ESCAPE":
|
if key == "ESCAPE":
|
||||||
self.mode = "normal"
|
self.mode = "normal"
|
||||||
@@ -451,7 +523,8 @@ class MainView():
|
|||||||
else:
|
else:
|
||||||
self.inputs += key
|
self.inputs += key
|
||||||
self.command_box = ""
|
self.command_box = ""
|
||||||
await self.drawtool.redraw()
|
if redraw:
|
||||||
|
await self.drawtool.redraw()
|
||||||
|
|
||||||
def insert_move_left(self):
|
def insert_move_left(self):
|
||||||
self.inputs_cursor = max(0, self.cursor - 1)
|
self.inputs_cursor = max(0, self.cursor - 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user