8 Commits

Author SHA1 Message Date
Dominic Zimmer
f052c47d09 Add more verbose error message 2019-09-20 20:03:13 +02:00
Dominic Zimmer
19af89c220 Merge branch 'master' of github.com:thamma/tttc 2019-09-13 21:16:39 +02:00
Dominic Zimmer
0496171d20 Fix 2FA, now tested 2019-09-13 21:16:34 +02:00
Dominic Z
e2e9d5b2a1 Update README.md 2019-09-11 16:31:55 +02:00
Dominic Zimmer
2ab8dac662 Fix Two-Factor-Authentification. Add minor tweaks to authview. Close #5 2019-09-11 11:40:00 +02:00
Dominic Zimmer
f2f78cf0fd Add edits 2019-08-21 23:04:13 +02:00
Dominic Zimmer
0a82d58de7 Add macros, implement modestack for popupmessages within popups 2019-08-21 22:17:44 +02:00
Dominic Zimmer
3ea653fc6f Add stdinput detection, remove commandline flag 2019-08-12 15:40:55 +02:00
5 changed files with 153 additions and 46 deletions

View File

@@ -15,3 +15,23 @@ Once you obtained your own api key and hash, you need to set them as your enviro
`TTTC_API_ID` and `TTTC_API_HASH`, respectively.
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`

View File

@@ -22,6 +22,7 @@ class AuthView():
self.inputs = ""
self.w, self.h = curses.COLS, curses.LINES
self.fin = False
self.showinput = True
async def textinput(self):
@@ -47,36 +48,44 @@ class AuthView():
self.stdscr.refresh()
self.phone = await self.textinput()
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:
self.stdscr.addstr("This phone number is not registered in telegram. ")
self.stdscr.refresh()
else:
break
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()
except Exception as e:
self.stdscr.addstr("Incorrect phone number. ")
self.stdscr.addstr(f"An error occured: {str(e)}")
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.code = await self.textinput()
while True:
done = False
try:
await self.client.sign_in(self.phone, self.code)
except telethon.errors.SessionPasswordNeededError:
self.stdscr.addstr("Password required to log in")
self.code = await self.textinput()
await self.client.sign_in(self.phone.replace("+","00").replace(" ",""), self.code)
except telethon.errors.rpcerrorlist.PhoneCodeInvalidError:
self.stdscr.addstr("The authentification code was wrong. Please try again.")
self.stdscr.refresh()
except telethon.errors.SessionPasswordNeededError:
self.showinput = False
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)
# TODO: debug me
except:
show_stacktrace()
except telethon.errors.rpcerrorlist.PhoneCodeInvalidError:
pass
self.stdscr.addstr(f"auth successful. ")
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()
async def handle_key(self, key):
@@ -87,7 +96,10 @@ class AuthView():
self.inputs = self.inputs[0:-1]
else:
self.inputs += key
if self.showinput:
self.stdscr.addstr(20, 50, self.inputs)
else:
self.stdscr.addstr(20, 50, "*"*len(self.inputs))
self.stdscr.clrtoeol()
self.stdscr.refresh()

View File

@@ -26,7 +26,6 @@ def handle():
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("--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()
global debug
if parsed.verbose:
@@ -58,6 +57,8 @@ def handle():
debug("Fetching chats...", file=sys.stderr)
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))
unique = None
if filtered:
@@ -67,12 +68,12 @@ def handle():
for result in reversed(filtered):
print(str(result.id).rjust(16) + " "*4 + result.name)
else:
if not (parsed.message or parsed.stdin):
if not (parsed.message or stdinput):
for result in reversed(filtered):
print(str(result.id).rjust(16) + " "*4 + result.name)
exit()
unique = filtered[0].id
if not (parsed.message or parsed.stdin):
if not (parsed.message or stdinput):
exit() # we are done here
recipient = unique or parsed.target
@@ -92,11 +93,11 @@ def handle():
# print("Illegal entity id. Aborting.", file=sys.stderr)
# exit(1)
if parsed.message or parsed.stdin:
send_message(client, recipient, message=parsed.message)
#if parsed.message or parsed.stdin:
send_message(client, recipient, message=parsed.message, stdinput=stdinput)
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}")
try:
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)
exit(1)
debug("Chat exists. Sending message.", file=sys.stderr)
if message is None:
debug("No message specified. Reading from stdin.", file=sys.stderr)
message = "".join([line for line in sys.stdin])
if message.strip() == "":
if message and stdinput:
out = f"{message}\n{stdinput}"
else:
out = message or stdinput
if not out.strip():
print("The message must not be empty.", file=sys.stderr)
exit()
client.send_message(chat_id, message)
client.send_message(chat_id, out)
def filter_chats(filt):
if filt == (None, None, None):

View File

@@ -79,7 +79,7 @@ class Drawtool():
self.stdscr.addstr(self.H - 1, 0, "/" + self.main_view.search_box, self.main_view.colors["error"])
else:
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
self.stdscr.addstr(self.H - 1, 0, question)
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:]):
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)
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)

View File

@@ -27,9 +27,15 @@ class MainView():
# 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)
@@ -52,6 +58,7 @@ class MainView():
self.selected_message = None
self.mode = "normal"
self.modestack = []
async def quit(self):
self.fin = True
@@ -301,21 +308,29 @@ class MainView():
subprocess.Popen(["xdg-open", f"{path}"], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL)
def popup_message(self, question):
self.mode = "popup"
self.modestack.append(self.mode)
self.mode = "popupmessage"
async def action_handler(self, key):
self.mode = "normal"
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):
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"
@@ -359,19 +374,41 @@ class MainView():
self.vimline_box = ""
elif key == "RETURN" or key == "y":
await self.send_message()
elif key == "q":
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 == "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":
elif key == "E":
self.text_emojis ^= True
elif key == "R":
await self.mark_read()
@@ -397,6 +434,16 @@ class MainView():
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:
@@ -436,7 +483,32 @@ class MainView():
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"
@@ -451,6 +523,7 @@ class MainView():
else:
self.inputs += key
self.command_box = ""
if redraw:
await self.drawtool.redraw()
def insert_move_left(self):