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

@@ -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`

View File

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

View File

@@ -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):

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"]) 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)

View File

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