TODOs, improved README and in general code improvements.

- Added upgrading instructions in README
- Improved installation instructions
- Better keymapping support (more customisable)
- More low-level customisation
- Proper daemon initialisation
- Completed some TODOs in code and in README
- Made input design more consistant
- Custom extensions
- Removed/renabled useless variables
- Fixed bugs with update handlers
- Fixed requirement formatting and versioning
This commit is contained in:
Ari Archer 2021-12-02 19:40:19 +02:00
parent 71426da2c5
commit 3f768ff00f
Signed by untrusted user who does not match committer: ari
GPG key ID: A50D5B4B599AF8A2
13 changed files with 477 additions and 256 deletions

15
BUGS.md Normal file
View file

@ -0,0 +1,15 @@
# Bugs
A place of important bugs to fix outside of issues
## States
- 0 -- unsolved
- 1 -- solved (dev build)
- 2 -- solved (release)
- 3 -- won't solve
- 4 -- intended
- 5 -- critical
| `state` | `issue` | `extra_info` | `id` |
| :---------: | :---------------------------------------------------------------------: | :-----------------------------------------------: | :----------: |
| 0 | When pinning chats the UI does not update until arigram is quit | | 0x0000000000 |

View file

@ -7,12 +7,12 @@ A fork of [tg](https://github.com/paul-nameless/tg) -- a hackable telegram TUI c
## Features
- [X] view mediafiles: photo, video, voice/video notes, documents
- [X] view media: photo, video, voice/video notes, documents
- [X] ability to send pictures, documents, audio, video
- [X] reply, edit, forward, delete, send messages
- [X] stickers
- [X] notifications
- [X] record and send voice msgs
- [X] record and send voice messages
- [X] auto download files
- [X] toggle chats: pin/unpin, mark as read/unread, mute/unmute
- [X] message history
@ -22,8 +22,15 @@ A fork of [tg](https://github.com/paul-nameless/tg) -- a hackable telegram TUI c
- [x] automation
- [x] better default file picker
- [x] custom keybindings
- [x] consistent styling
- [ ] drafts
- [ ] scheduled messages
- [ ] local passwords
- [ ] debug mode
- [ ] modules/addons
- [ ] stickers (sticker keyboard)
- [ ] search
- [ ] less crowded UI
- [ ] search (for users)
- [ ] bots (bot keyboard)
@ -66,12 +73,34 @@ brew install arigram
This option is recommended:
```sh
mkdir ~/.local/src
cd ~/.local/src
git clone https://github.com/TruncatedDinosour/arigram.git
cd arigram
pip install --upgrade --user -r requirements.txt
pip install --upgrade --user .
```
## Upgrading
### Homebrew
```sh
brew upgrade
```
### From sources
This option is recommended:
```sh
cd ~/.local/src/arigram
git reset --hard # This discards every change you made
git pull
pip install --upgrade --user -r requirements.txt
pip install --upgrade --user .
```
And add this to `~/.bashrc` or whatever POSIX complient shell you use:
```sh
@ -227,10 +256,17 @@ def send_hello(ctrl, *args) -> None:
ctrl.model.send_message(text=f"Hello people!") # Sends a message
# CUSTOM_KEYBINDS = {"KEY": {"func": SOME_FUNCTION, "handler": CONTEXT_HANDLER}}
CUSTOM_KEYBINDS = {"z": {"func": send_hello, "handler": msg_handler}}
CUSTOM_KEYBINDS = {"z": {"func": send_hello, "handler": msg_handler, "repeat": False, "is_remap": False}}
# What to add before file picker (while using fzf (default))
EXTRA_FILE_CHOOSER_PATHS = ["..", "/", "~"]
# This is the max truncation limit when truncating paths, messages, etc.
TRUNCATE_LIMIT = 10
# If you set this to True this will automatically disable link previews
# WARNING: only do this if you know what you are doing, this is a dangerous option
EXTRA_TDLIB_HEADERS = {"disable_web_page_preview": True}
```
### Mailcap file

View file

@ -38,7 +38,7 @@ def run(tg: Tdlib, stdscr: window) -> None:
tg.add_update_handler(msg_type, partial(handler, controller))
thread = threading.Thread(target=controller.run)
thread.daemon = True
thread.setDaemon(True)
thread.start()
controller.load_custom_keybindings()
@ -60,6 +60,7 @@ def main() -> None:
parse_args()
utils.cleanup_cache()
tg = Tdlib(
config.EXTRA_TDLIB_HEADEARS,
api_id=config.API_ID,
api_hash=config.API_HASH,
phone=config.PHONE,

View file

@ -2,6 +2,7 @@
Every parameter (except for CONFIG_FILE) can be
overwritten by external config file
"""
import mailcap
import os
import platform
import runpy
@ -9,6 +10,7 @@ from typing import Any, Dict, Optional
_os_name = platform.system()
_linux = "Linux"
_global_mailcap = mailcap.getcaps()
CONFIG_DIR = os.path.expanduser("~/.config/arigram/")
@ -30,7 +32,6 @@ TDLIB_VERBOSITY = 0
MAX_DOWNLOAD_SIZE = "10MB"
# TODO: check platform
NOTIFY_FUNCTION = None
VIEW_TEXT_CMD = "less"
@ -46,9 +47,14 @@ else:
"ffmpeg -f avfoundation -i ':0' -c:a libopus -b:a 32k {file_path}"
)
# TODO: use mailcap instead of editor
EDITOR = os.environ.get("EDITOR", "vim")
LONG_MSG_CMD = f"{EDITOR} -- {{file_path}}"
_, __MAILCAP_EDITOR = mailcap.findmatch(_global_mailcap, "text/markdown")
if __MAILCAP_EDITOR:
EDITOR = str(__MAILCAP_EDITOR["view"]).split(" ", 1)[0]
LONG_MSG_CMD = f"{EDITOR} '{{file_path}}'"
if _os_name == _linux:
DEFAULT_OPEN = "xdg-open {file_path}"
@ -83,6 +89,10 @@ EXTRA_FILE_CHOOSER_PATHS = ["..", "/", "~"]
CUSTOM_KEYBINDS: Dict[str, Dict[str, Any]] = {}
TRUNCATE_LIMIT: int = 15
EXTRA_TDLIB_HEADEARS: Dict[Any, Any] = {}
if os.path.isfile(CONFIG_FILE):
config_params = runpy.run_path(CONFIG_FILE) # type: ignore
for param, value in config_params.items():
@ -95,9 +105,9 @@ else:
print(
"Enter your phone number in international format, including country code (example: +5037754762346)"
)
PHONE = input("phone: ")
PHONE = input("(phone) ")
if not PHONE.startswith("+"):
PHONE = "+" + PHONE
with open(CONFIG_FILE, "a") as f:
f.write(f"PHONE = '{PHONE}'\n")
f.write(f'\nPHONE = "{PHONE}"\n')

View file

@ -30,6 +30,8 @@ from arigram.utils import (
)
from arigram.views import View
from arigram import exceptions as ag_exception
log = logging.getLogger(__name__)
# start scrolling to next page when number of the msgs left is less than value.
@ -48,6 +50,7 @@ def bind(
binding: Dict[str, HandlerType],
keys: List[str],
repeat_factor: bool = False,
remap: bool = False,
) -> Callable:
"""bind handlers to given keys"""
@ -61,9 +64,20 @@ def bind(
return fun(self)
for key in keys:
assert (
key not in binding
), f"Key {key} already binded to {binding[key]}"
if key in binding and not remap:
val = binding.get(key)
val_mem = hex(id(val))
if val is None:
log.error(
f"Most likely a bug: {key} is bound to {val} (in {val_mem}), this should not happen"
)
message = f"Key {key} already bound to {val} (in {val_mem})"
log.error(message)
raise ag_exception.KeyBoundError(message)
binding[key] = fun if repeat_factor else _no_repeat_factor # type: ignore
return wrapper
@ -289,7 +303,7 @@ class Controller:
if chat_id is None:
return
reply_to_msg = self.model.current_msg_id
if msg := self.view.status.get_input():
if msg := self.view.status.get_input("reply"):
self.model.view_all_msgs()
self.tg.reply_message(chat_id, reply_to_msg, msg)
self.present_info("Message reply sent")
@ -327,7 +341,7 @@ class Controller:
self.present_info("Can't send msg in this chat")
return
self.tg.send_chat_action(chat_id, ChatAction.chatActionTyping)
if msg := self.view.status.get_input():
if msg := self.view.status.get_input("message"):
self.model.send_message(text=msg)
self.present_info("Message sent")
else:
@ -395,7 +409,7 @@ class Controller:
with suspend(self.view) as s:
resp = self.view.status.get_input(
f"Review <{file_path}>? [Y/n]: "
f"review <{file_path}>? (Y/n)"
)
if resp is None:
@ -405,7 +419,7 @@ class Controller:
sv.open_file(file_path, None)
resp_up = self.view.status.get_input(
f"Upload <{file_path}>? [y/N]: "
f"upload <{file_path}>? (y/N)"
)
if resp_up is None or is_no(resp_up):
@ -423,7 +437,7 @@ class Controller:
mime = get_mime(file_path)
if mime in ("image", "video", "animation"):
resp = self.view.status.get_input(
f"Upload <{file_path}> compressed? [Y/n]: "
f"upload <{file_path}> compressed? (Y/n)"
)
self.render_status()
if resp is None:
@ -457,7 +471,7 @@ class Controller:
@bind(msg_handler, ["sv"])
def send_video(self) -> None:
"""Enter file path and send compressed video"""
file_path = self.view.status.get_input()
file_path = self.view.status.get_input("file path")
if not file_path or not os.path.isfile(file_path):
return
chat_id = self.model.chats.id_by_index(self.model.current_chat)
@ -474,7 +488,7 @@ class Controller:
self,
send_file_fun: Callable[[str, int], AsyncResult],
) -> None:
_input = self.view.status.get_input()
_input = self.view.status.get_input("file path")
if _input is None:
return
file_path = os.path.expanduser(_input)
@ -495,7 +509,7 @@ class Controller:
)
)
resp = self.view.status.get_input(
f"Do you want to send recording: {file_path}? [Y/n]"
f"do you want to send recording: <{file_path}>? (Y/n)"
)
if resp is None or not is_yes(resp):
return self.present_info("Voice message discarded")
@ -556,7 +570,7 @@ class Controller:
def open_msg_with_cmd(self) -> None:
"""Open msg or file with cmd: less %s"""
msg = MsgProxy(self.model.current_msg)
cmd = self.view.status.get_input()
cmd = self.view.status.get_input("command")
if not cmd:
return
if "%s" not in cmd:
@ -569,6 +583,22 @@ class Controller:
def open_current_msg(self) -> None:
"""Open msg or file with cmd in mailcap"""
msg = MsgProxy(self.model.current_msg)
extra = str(
msg.text_content
if msg.is_text
else msg.caption or msg.local_path or msg.msg_id
)
resp = self.view.status.get_input(
f"open <{os.path.basename(extra)[:config.TRUNCATE_LIMIT] + '...'}>? (Y/n)"
)
if resp is None or not is_yes(resp):
self.present_info(
f"not opening message <{msg.msg_id}> from <{msg.sender_id}>"
)
return
self._open_msg(msg)
@bind(msg_handler, ["e"])
@ -627,7 +657,7 @@ class Controller:
user_ids = self._get_user_ids(is_multiple=True)
if not user_ids:
return
title = self.view.status.get_input("Group name: ")
title = self.view.status.get_input("group name")
if title is None:
return self.present_info("Cancelling creating group")
if not title:
@ -647,7 +677,7 @@ class Controller:
ChatType.channel,
):
resp = self.view.status.get_input(
"Are you sure you want to leave this group/channel?[y/N]"
"are you sure you want to leave this group/channel? (y/N)"
)
if is_no(resp or ""):
return self.present_info("Not leaving group/channel")
@ -658,14 +688,14 @@ class Controller:
return
resp = self.view.status.get_input(
"Are you sure you want to delete the chat?[y/N]"
"are you sure you want to delete the chat? (y/N)"
)
if is_no(resp or ""):
return self.present_info("Not deleting chat")
is_revoke = False
if chat["can_be_deleted_for_all_users"]:
resp = self.view.status.get_input("Delete for all users?[y/N]")
resp = self.view.status.get_input("delete for all users? (y/N)")
if resp is None:
return self.present_info("Not deleting chat")
self.render_status()
@ -698,7 +728,7 @@ class Controller:
@bind(chat_handler, ["/"])
def search_contacts(self) -> None:
"""Search contacts and set jumps to it if found"""
msg = self.view.status.get_input("/")
msg = self.view.status.get_input("search")
if not msg:
return self.present_info("Search discarded")
@ -789,7 +819,7 @@ class Controller:
def toggle_pin(self) -> None:
chat = self.model.chats.chats[self.model.current_chat]
chat_id = chat["id"]
toggle = not chat["is_pinned"]
toggle = not chat["positions"][0].get("is_pinned")
self.tg.toggle_chat_is_pinned(chat_id, toggle)
self.render()
@ -852,10 +882,10 @@ class Controller:
log.exception("Error happened in draw loop")
def present_error(self, msg: str) -> None:
return self.update_status("Error", msg)
return self.update_status("Error", msg.capitalize())
def present_info(self, msg: str) -> None:
return self.update_status("Info", msg)
return self.update_status("Info", msg.capitalize())
def update_status(self, level: str, msg: str) -> None:
self.queue.put(partial(self._update_status, level, msg))
@ -914,12 +944,11 @@ class Controller:
return
# TODO: handle cases when all chats muted on global level
if chat["notification_settings"]["mute_for"]:
if chat["notification_settings"]["mute_for"] or self.model.is_me(
msg["sender"].get("user_id")
):
return
# notify
if self.model.is_me(msg["sender"].get("user_id")):
return
user = self.model.users.get_user(msg.sender_id)
name = f"{user['first_name']} {user['last_name']}"
@ -946,8 +975,10 @@ class Controller:
function = binding_info["func"]
handler = binding_info["handler"]
repeat = binding_info.get("is_repeat", False)
remap = binding_info.get("is_remap", False)
@bind(handler, [binding])
@bind(handler, [binding], repeat_factor=repeat, remap=remap)
@rename_function(function_name)
def handle(*args, fn: Callable = function) -> None: # type: ignore
try:
@ -958,7 +989,7 @@ class Controller:
)
log.info(
f"{function_name} = {function.__name__}() --> {binding}"
f"{function_name} = {function.__name__}() (repeat = {repeat}, remap = {remap}) --> {binding}"
)
setattr(

4
arigram/exceptions.py Normal file
View file

@ -0,0 +1,4 @@
class KeyBoundError(Exception):
"""Exception to raise when a key is already bound"""
pass

View file

@ -466,7 +466,7 @@ class ChatModel:
)
def update_chat(self, chat_id: int, **updates: Dict[str, Any]) -> bool:
for i, chat in enumerate(self.chats):
for _, chat in enumerate(self.chats):
if chat["id"] != chat_id:
continue
chat.update(updates)

View file

@ -56,6 +56,16 @@ class SecretChatState(Enum):
class Tdlib(Telegram):
def __init__(self, extra_headers, *args, **kwargs) -> None:
self.extra_headers = extra_headers
super().__init__(*args, **kwargs)
def _parse_data(self, data: Dict[Any, Any]) -> Dict[Any, Any]:
_tmp_data = data.copy()
_tmp_data.update(self.extra_headers)
return _tmp_data
def parse_text_entities(
self,
text: str,
@ -63,11 +73,13 @@ class Tdlib(Telegram):
version: int = 2,
) -> AsyncResult:
"""Offline synchronous method which returns parsed entities"""
data = {
"@type": "parseTextEntities",
"text": text,
"parse_mode": {"@type": parse_mode.name, "version": version},
}
data = self._parse_data(
{
"@type": "parseTextEntities",
"text": text,
"parse_mode": {"@type": parse_mode.name, "version": version},
}
)
return self._send_data(data)
@ -79,14 +91,16 @@ class Tdlib(Telegram):
if not result.error:
text = result.update
data = {
"@type": "sendMessage",
"chat_id": chat_id,
"input_message_content": {
"@type": "inputMessageText",
"text": text,
},
}
data = self._parse_data(
{
"@type": "sendMessage",
"chat_id": chat_id,
"input_message_content": {
"@type": "inputMessageText",
"text": text,
},
}
)
return self._send_data(data)
@ -98,77 +112,99 @@ class Tdlib(Telegram):
limit: int = 0,
synchronous: bool = False,
) -> None:
data = {
"@type": "downloadFile",
"file_id": file_id,
"priority": priority,
"offset": offset,
"limit": limit,
"synchronous": synchronous,
}
data = self._parse_data(
{
"@type": "downloadFile",
"file_id": file_id,
"priority": priority,
"offset": offset,
"limit": limit,
"synchronous": synchronous,
}
)
return self._send_data(data)
def reply_message(
self, chat_id: int, reply_to_message_id: int, text: str
) -> AsyncResult:
data = {
"@type": "sendMessage",
"chat_id": chat_id,
"reply_to_message_id": reply_to_message_id,
"input_message_content": {
"@type": "inputMessageText",
"text": {"@type": "formattedText", "text": text},
},
}
data = self._parse_data(
{
"@type": "sendMessage",
"chat_id": chat_id,
"reply_to_message_id": reply_to_message_id,
"input_message_content": {
"@type": "inputMessageText",
"text": {"@type": "formattedText", "text": text},
},
}
)
return self._send_data(data)
def search_contacts(self, target: str, limit: int = 10) -> AsyncResult:
data = {"@type": "searchChats", "query": target, "limit": limit}
data = self._parse_data(
{"@type": "searchChats", "query": target, "limit": limit}
)
return self._send_data(data, block=True)
def send_doc(self, file_path: str, chat_id: int) -> AsyncResult:
data = {
"@type": "sendMessage",
"chat_id": chat_id,
"input_message_content": {
"@type": "inputMessageDocument",
"document": {"@type": "inputFileLocal", "path": file_path},
},
}
data = self._parse_data(
{
"@type": "sendMessage",
"chat_id": chat_id,
"input_message_content": {
"@type": "inputMessageDocument",
"document": {"@type": "inputFileLocal", "path": file_path},
},
}
)
return self._send_data(data)
def send_audio(self, file_path: str, chat_id: int) -> AsyncResult:
data = {
"@type": "sendMessage",
"chat_id": chat_id,
"input_message_content": {
"@type": "inputMessageAudio",
"audio": {"@type": "inputFileLocal", "path": file_path},
},
}
data = self._parse_data(
{
"@type": "sendMessage",
"chat_id": chat_id,
"input_message_content": {
"@type": "inputMessageAudio",
"audio": {"@type": "inputFileLocal", "path": file_path},
},
}
)
return self._send_data(data)
def send_animation(self, file_path: str, chat_id: int) -> AsyncResult:
data = {
"@type": "sendMessage",
"chat_id": chat_id,
"input_message_content": {
"@type": "inputMessageAnimation",
"animation": {"@type": "inputFileLocal", "path": file_path},
},
}
data = self._parse_data(
{
"@type": "sendMessage",
"chat_id": chat_id,
"input_message_content": {
"@type": "inputMessageAnimation",
"animation": {
"@type": "inputFileLocal",
"path": file_path,
},
},
}
)
return self._send_data(data)
def send_photo(self, file_path: str, chat_id: int) -> AsyncResult:
data = {
"@type": "sendMessage",
"chat_id": chat_id,
"input_message_content": {
"@type": "inputMessagePhoto",
"photo": {"@type": "inputFileLocal", "path": file_path},
},
}
data = self._parse_data(
{
"@type": "sendMessage",
"chat_id": chat_id,
"input_message_content": {
"@type": "inputMessagePhoto",
"photo": {"@type": "inputFileLocal", "path": file_path},
},
}
)
return self._send_data(data)
def send_video(
@ -179,97 +215,124 @@ class Tdlib(Telegram):
height: int,
duration: int,
) -> AsyncResult:
data = {
"@type": "sendMessage",
"chat_id": chat_id,
"input_message_content": {
"@type": "inputMessageVideo",
"width": width,
"height": height,
"duration": duration,
"video": {"@type": "inputFileLocal", "path": file_path},
},
}
data = self._parse_data(
{
"@type": "sendMessage",
"chat_id": chat_id,
"input_message_content": {
"@type": "inputMessageVideo",
"width": width,
"height": height,
"duration": duration,
"video": {"@type": "inputFileLocal", "path": file_path},
},
}
)
return self._send_data(data)
def send_voice(
self, file_path: str, chat_id: int, duration: int, waveform: str
) -> AsyncResult:
data = {
"@type": "sendMessage",
"chat_id": chat_id,
"input_message_content": {
"@type": "inputMessageVoiceNote",
"duration": duration,
"waveform": waveform,
"voice_note": {"@type": "inputFileLocal", "path": file_path},
},
}
data = self._parse_data(
{
"@type": "sendMessage",
"chat_id": chat_id,
"input_message_content": {
"@type": "inputMessageVoiceNote",
"duration": duration,
"waveform": waveform,
"voice_note": {
"@type": "inputFileLocal",
"path": file_path,
},
},
}
)
return self._send_data(data)
def edit_message_text(
self, chat_id: int, message_id: int, text: str
) -> AsyncResult:
data = {
"@type": "editMessageText",
"message_id": message_id,
"chat_id": chat_id,
"input_message_content": {
"@type": "inputMessageText",
"text": {"@type": "formattedText", "text": text},
},
}
data = self._parse_data(
{
"@type": "editMessageText",
"message_id": message_id,
"chat_id": chat_id,
"input_message_content": {
"@type": "inputMessageText",
"text": {"@type": "formattedText", "text": text},
},
}
)
return self._send_data(data)
def toggle_chat_is_marked_as_unread(
self, chat_id: int, is_marked_as_unread: bool
) -> AsyncResult:
data = {
"@type": "toggleChatIsMarkedAsUnread",
"chat_id": chat_id,
"is_marked_as_unread": is_marked_as_unread,
}
data = self._parse_data(
{
"@type": "toggleChatIsMarkedAsUnread",
"chat_id": chat_id,
"is_marked_as_unread": is_marked_as_unread,
}
)
return self._send_data(data)
def toggle_chat_is_pinned(
self, chat_id: int, is_pinned: bool
) -> AsyncResult:
data = {
"@type": "toggleChatIsPinned",
"chat_id": chat_id,
"is_pinned": is_pinned,
}
data = self._parse_data(
{
"@type": "toggleChatIsPinned",
"chat_id": chat_id,
"is_pinned": is_pinned,
}
)
return self._send_data(data)
def set_chat_nottification_settings(
self, chat_id: int, notification_settings: dict
) -> AsyncResult:
data = {
"@type": "setChatNotificationSettings",
"chat_id": chat_id,
"notification_settings": notification_settings,
}
data = self._parse_data(
{
"@type": "setChatNotificationSettings",
"chat_id": chat_id,
"notification_settings": notification_settings,
}
)
return self._send_data(data)
def view_messages(
self, chat_id: int, message_ids: list, force_read: bool = True
) -> AsyncResult:
data = {
"@type": "viewMessages",
"chat_id": chat_id,
"message_ids": message_ids,
"force_read": force_read,
}
data = self._parse_data(
{
"@type": "viewMessages",
"chat_id": chat_id,
"message_ids": message_ids,
"force_read": force_read,
}
)
return self._send_data(data)
def open_message_content(
self, chat_id: int, message_id: int
) -> AsyncResult:
data = {
"@type": "openMessageContent",
"chat_id": chat_id,
"message_id": message_id,
}
data = self._parse_data(
{
"@type": "openMessageContent",
"chat_id": chat_id,
"message_id": message_id,
}
)
return self._send_data(data)
def forward_messages(
@ -282,120 +345,159 @@ class Tdlib(Telegram):
remove_caption: bool = False,
options: Dict[str, Any] = {},
) -> AsyncResult:
data = {
"@type": "forwardMessages",
"chat_id": chat_id,
"from_chat_id": from_chat_id,
"message_ids": message_ids,
"as_album": as_album,
"send_copy": send_copy,
"remove_caption": remove_caption,
"options": options,
}
data = self._parse_data(
{
"@type": "forwardMessages",
"chat_id": chat_id,
"from_chat_id": from_chat_id,
"message_ids": message_ids,
"as_album": as_album,
"send_copy": send_copy,
"remove_caption": remove_caption,
"options": options,
}
)
return self._send_data(data)
def get_basic_group(
self,
basic_group_id: int,
) -> AsyncResult:
data = {
"@type": "getBasicGroup",
"basic_group_id": basic_group_id,
}
data = self._parse_data(
{
"@type": "getBasicGroup",
"basic_group_id": basic_group_id,
}
)
return self._send_data(data)
def get_basic_group_full_info(
self,
basic_group_id: int,
) -> AsyncResult:
data = {
"@type": "getBasicGroupFullInfo",
"basic_group_id": basic_group_id,
}
data = self._parse_data(
{
"@type": "getBasicGroupFullInfo",
"basic_group_id": basic_group_id,
}
)
return self._send_data(data)
def get_supergroup(
self,
supergroup_id: int,
) -> AsyncResult:
data = {
"@type": "getSupergroup",
"supergroup_id": supergroup_id,
}
data = self._parse_data(
{
"@type": "getSupergroup",
"supergroup_id": supergroup_id,
}
)
return self._send_data(data)
def get_supergroup_full_info(
self,
supergroup_id: int,
) -> AsyncResult:
data = {
"@type": "getSupergroupFullInfo",
"supergroup_id": supergroup_id,
}
data = self._parse_data(
{
"@type": "getSupergroupFullInfo",
"supergroup_id": supergroup_id,
}
)
return self._send_data(data)
def get_secret_chat(
self,
secret_chat_id: int,
) -> AsyncResult:
data = {
"@type": "getSecretChat",
"secret_chat_id": secret_chat_id,
}
data = self._parse_data(
{
"@type": "getSecretChat",
"secret_chat_id": secret_chat_id,
}
)
return self._send_data(data)
def send_chat_action(
self, chat_id: int, action: ChatAction, progress: int = None
) -> AsyncResult:
data = {
"@type": "sendChatAction",
"chat_id": chat_id,
"action": {"@type": action.name, "progress": progress},
}
data = self._parse_data(
{
"@type": "sendChatAction",
"chat_id": chat_id,
"action": {"@type": action.name, "progress": progress},
}
)
return self._send_data(data)
def get_contacts(self) -> AsyncResult:
data = {
"@type": "getContacts",
}
data = self._parse_data(
{
"@type": "getContacts",
}
)
return self._send_data(data)
def leave_chat(self, chat_id: int) -> AsyncResult:
data = {
"@type": "leaveChat",
"chat_id": chat_id,
}
data = self._parse_data(
{
"@type": "leaveChat",
"chat_id": chat_id,
}
)
return self._send_data(data)
def join_chat(self, chat_id: int) -> AsyncResult:
data = {
"@type": "joinChat",
"chat_id": chat_id,
}
data = self._parse_data(
{
"@type": "joinChat",
"chat_id": chat_id,
}
)
return self._send_data(data)
def close_secret_chat(self, secret_chat_id: int) -> AsyncResult:
data = {
"@type": "closeSecretChat",
"secret_chat_id": secret_chat_id,
}
data = self._parse_data(
{
"@type": "closeSecretChat",
"secret_chat_id": secret_chat_id,
}
)
return self._send_data(data)
def create_new_secret_chat(self, user_id: int) -> AsyncResult:
data = {
"@type": "createNewSecretChat",
"user_id": user_id,
}
data = self._parse_data(
{
"@type": "createNewSecretChat",
"user_id": user_id,
}
)
return self._send_data(data)
def create_new_basic_group_chat(
self, user_ids: List[int], title: str
) -> AsyncResult:
data = {
"@type": "createNewBasicGroupChat",
"user_ids": user_ids,
"title": title,
}
data = self._parse_data(
{
"@type": "createNewBasicGroupChat",
"user_ids": user_ids,
"title": title,
}
)
return self._send_data(data)
def delete_chat_history(
@ -404,26 +506,35 @@ class Tdlib(Telegram):
"""
revoke: Pass true to try to delete chat history for all users
"""
data = {
"@type": "deleteChatHistory",
"chat_id": chat_id,
"remove_from_chat_list": remove_from_chat_list,
"revoke": revoke,
}
data = self._parse_data(
{
"@type": "deleteChatHistory",
"chat_id": chat_id,
"remove_from_chat_list": remove_from_chat_list,
"revoke": revoke,
}
)
return self._send_data(data)
def get_user(self, user_id: int) -> AsyncResult:
data = {
"@type": "getUser",
"user_id": user_id,
}
data = self._parse_data(
{
"@type": "getUser",
"user_id": user_id,
}
)
return self._send_data(data)
def get_user_full_info(self, user_id: int) -> AsyncResult:
data = {
"@type": "getUserFullInfo",
"user_id": user_id,
}
data = self._parse_data(
{
"@type": "getUserFullInfo",
"user_id": user_id,
}
)
return self._send_data(data)

View file

@ -28,8 +28,11 @@ def update_handler(
def wrapper(controller: Controller, update: Dict[str, Any]) -> None:
try:
fun(controller, update)
except Exception:
log.exception("Error happened in handler: %s", update_type)
except Exception as e:
log.exception(
f"Error happened in handler: {update_type}; {e.__class__.__name__}: {e}"
)
exit()
handlers[update_type] = wrapper
return wrapper
@ -84,26 +87,12 @@ def update_new_message(controller: Controller, update: Dict[str, Any]) -> None:
def update_chat_order(controller: Controller, update: Dict[str, Any]) -> None:
current_chat_id = controller.model.current_chat_id
chat_id = update["chat_id"]
order = update["order"]
order = update["position"]["order"]
if controller.model.chats.update_chat(chat_id, order=order):
controller.refresh_current_chat(current_chat_id)
@update_handler("updateChatPosition")
def update_chat_position(
controller: Controller, update: Dict[str, Any]
) -> None:
current_chat_id = controller.model.current_chat_id
chat_id = update["chat_id"]
info = {}
info["order"] = update["position"]["order"]
if "is_pinned" in update:
info["is_pinned"] = update["is_pinned"]
if controller.model.chats.update_chat(chat_id, **info):
controller.refresh_current_chat(current_chat_id)
@update_handler("updateChatTitle")
def update_chat_title(controller: Controller, update: Dict[str, Any]) -> None:
chat_id = update["chat_id"]
@ -139,8 +128,8 @@ def update_chat_is_pinned(
controller: Controller, update: Dict[str, Any]
) -> None:
chat_id = update["chat_id"]
is_pinned = update["is_pinned"]
order = update["order"]
is_pinned = update["position"]["is_pinned"]
order = update["position"]["order"]
current_chat_id = controller.model.current_chat_id
if controller.model.chats.update_chat(
@ -149,6 +138,22 @@ def update_chat_is_pinned(
controller.refresh_current_chat(current_chat_id)
@update_handler("updateChatPosition")
def update_chat_position(
controller: Controller, update: Dict[str, Any]
) -> None:
current_chat_id = controller.model.current_chat_id
chat_id = update["chat_id"]
info = {}
info["order"] = update["position"]["order"]
if update["position"]["is_pinned"]:
info["is_pinned"] = update["position"]["is_pinned"]
update_chat_is_pinned(controller, update)
if controller.model.chats.update_chat(chat_id, **info):
controller.refresh_current_chat(current_chat_id)
controller.render()
@update_handler("updateChatReadOutbox")
def update_chat_read_outbox(
controller: Controller, update: Dict[str, Any]
@ -187,7 +192,7 @@ def update_chat_draft_message(
chat_id = update["chat_id"]
# FIXME: ignoring draft message itself for now because UI can't show it
# draft_message = update["draft_message"]
order = update["order"]
order = update["position"]["order"]
current_chat_id = controller.model.current_chat_id
if controller.model.chats.update_chat(chat_id, order=order):

View file

@ -110,6 +110,8 @@ def get_file_handler(file_path: str) -> str:
caps = get_mailcap()
handler, view = mailcap.findmatch(caps, mtype, filename=file_path)
del view
if not handler:
return config.DEFAULT_OPEN.format(file_path=shlex.quote(file_path))
return handler

View file

@ -137,19 +137,22 @@ class StatusView:
self.win.addstr(0, 0, msg.replace("\n", " ")[: self.w])
self._refresh()
def get_input(self, prefix: str = "") -> Optional[str]:
def get_input(self, prefix: str = "input") -> Optional[str]:
curses.curs_set(1)
buff = ""
try:
while True:
prompt = f"({prefix}) "
self.win.erase()
line = buff[-(self.w - 1) :]
self.win.addstr(0, 0, f"{prefix}{line}")
line = buff[-(self.w - string_len_dwc(prompt) - 1) :]
self.win.addstr(0, 0, f"{prompt}{line}")
key = self.win.get_wch(
0, min(string_len_dwc(buff + prefix), self.w - 1)
0, min(string_len_dwc(buff + prompt), self.w - 1)
)
key = ord(key)
if key == 10: # return
break
@ -179,6 +182,8 @@ class ChatView:
self.model = model
def resize(self, rows: int, cols: int, width: int) -> None:
del cols
self.h = rows - 1
self.w = width
self.win.resize(self.h, self.w)
@ -294,7 +299,7 @@ class ChatView:
if self.model.users.is_online(chat["id"]):
flags.append("online")
if "is_pinned" in chat and chat["is_pinned"]:
if chat["positions"][0].get("is_pinned"):
flags.append("pinned")
if chat["notification_settings"]["mute_for"]:

View file

@ -12,6 +12,7 @@ repository = "https://github.com/TruncatedDinosour/arigram"
[tool.poetry.dependencies]
python = "^3.8"
python-telegram = "0.14.0"
pyfzf = "^0.2.2"
[tool.poetry.dev-dependencies]
black = "20.8b1"

View file

@ -1,3 +1,3 @@
python-telegram
pyfzf
python-telegram==0.14.0
pyfzf>=0.2.2