11 Commits

Author SHA1 Message Date
f9f2c5276b Update 'README.md' 2019-11-04 00:47:04 +00:00
5d3021585e Update 'README.md' 2019-11-04 00:45:40 +00:00
cfac6b3d21 Update 'README.md' 2019-11-04 00:45:22 +00:00
Dominic Zimmer
5c585ba284 Remove dead code, improve notifications 2019-10-31 23:56:18 +01: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 187 additions and 131 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,7 +48,7 @@ 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()
@@ -59,24 +60,32 @@ class AuthView():
except Exception as e: except Exception as e:
self.stdscr.addstr("Incorrect phone number. ") self.stdscr.addstr("Incorrect phone number. ")
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

@@ -60,51 +60,12 @@ class Drawtool():
return newlines return newlines
#return textwrap.wrap(s, width = width) #return textwrap.wrap(s, width = width)
def move_cursor_home(self):
y, x = self._get_cursor_position(self.main_view.inputs, width = self.W - 4)
lines = self._get_input_lines(self.main_view.inputs, width = self.W - 4)[-self.input_lines:]
self.main_view.cursor -= x
#len(lines[y])
def move_cursor_end(self):
try:
y, x = self._get_cursor_position(self.main_view.inputs, width = self.W - 4)
lines = self._get_input_lines(self.main_view.inputs, width = self.W - 4)[-self.input_lines:]
self.main_view.cursor -= x - len(lines[y])
except:
show_stacktrace()
#len(lines[y])
def move_cursor_up(self):
y, x = self._get_cursor_position(self.main_view.inputs, width = self.W - 4)
lines = self._get_input_lines(self.main_view.inputs, width = self.W - 4)[-self.input_lines:]
debug(lines)
debug(f"x:{x} y:{y}")
if y > 0:
self.main_view.cursor -= x
self.main_view.cursor -= 1
def move_cursor_down(self):
pass
def _get_cursor_position(self, s, width = 50): def _get_cursor_position(self, s, width = 50):
lines = self._get_input_lines(s, width = width)[-self.input_lines:] lines = self._get_input_lines(s, width = width)[-self.input_lines:]
alllines = self._get_input_lines(s, width = width)
if not lines: if not lines:
return (0, 0) return (0, 0)
curs = self.main_view.cursor x = len(lines[-1])
y = 0 y = len(lines) - 1
x = 0
for line in alllines:
if curs > len(line):
curs -= len(line)
y += 1
else:
x = curs
break
curs -= 1
if y >= self.input_lines:
y = self.input_lines - 1
return y, x return y, x
async def redraw(self): async def redraw(self):
@@ -118,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":
@@ -130,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,8 +27,14 @@ 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.cursor = 0 self.inputs_cursor = 0
self.edit_message = None
self.popup = None self.popup = None
@@ -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
@@ -82,20 +89,22 @@ class MainView():
dialog["messages"].insert(0, newmessage[0]) dialog["messages"].insert(0, newmessage[0])
if not event.out: if not event.out:
dialog["unread_count"] += 1 dialog["unread_count"] += 1
os.system(f"notify-send -i apps/telegram \"{dialog['dialog'].name}\" \"{newmessage[0].message}\"")
front = self.dialogs.pop(idx) front = self.dialogs.pop(idx)
self.dialogs = [front] + self.dialogs self.dialogs = [front] + self.dialogs
break break
# auto adjust relative replys to match shifted message offsets #old dead code
if event.chat_id == self.dialogs[self.selected_chat]["dialog"].id: # # auto adjust relative replys to match shifted message offsets
if self.inputs.startswith("r"): # if event.chat_id == self.dialogs[self.selected_chat]["dialog"].id:
num = self.inputs[1:].split()[0] # if self.inputs.startswith("r"):
try: # num = self.inputs[1:].split()[0]
# num = int(s[1:].split()[0]) # try:
number = int(num) ## num = int(s[1:].split()[0])
msg = self.inputs.replace("r" + num, "r" + str(number+1)) # number = int(num)
self.inputs = msg # msg = self.inputs.replace("r" + num, "r" + str(number+1))
except: # self.inputs = msg
pass # except:
# pass
# dont switch the dialoge upon arriving messages # dont switch the dialoge upon arriving messages
if idx == self.selected_chat: if idx == self.selected_chat:
self.selected_chat = 0 self.selected_chat = 0
@@ -301,21 +310,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 +376,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 +436,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,53 +485,65 @@ 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"
elif key == "LEFT": elif key == "LEFT":
self.insert_move_left() self.insert_move_left()
elif key == "UP":
self.drawtool.move_cursor_up()
elif key == "RIGHT": elif key == "RIGHT":
self.insert_move_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": elif key == "BACKSPACE":
try: self.inputs = self.inputs[0:-1]
if len(self.inputs) > 0: elif key == "RETURN":
inp = list(self.inputs) self.inputs += "\n"
inp.pop(self.cursor - 1)
self.inputs = "".join(inp)
self.insert_move_left()
except:
debug(f"{self.cursor}, {self.inputs}")
show_stacktrace()
else: else:
if key == "RETURN": self.inputs += key
key = "\n"
inp = list(self.inputs)
inp.insert(self.cursor, key)
self.inputs = "".join(inp)
self.cursor += 1
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.cursor = max(0, self.cursor - 1) self.inputs_cursor = max(0, self.cursor - 1)
def insert_move_right(self): def insert_move_right(self):
self.cursor = min(len(self.inputs), self.cursor + 1) 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()