Add users status in chat flags

Rotate logs
Add ability to configure flags representation
This commit is contained in:
Paul Nameless 2020-06-11 18:13:42 +08:00
parent 468950bc6a
commit 0151e8df6a
8 changed files with 99 additions and 26 deletions

View file

@ -1,8 +1,6 @@
# tg # tg
Terminal telegram client. Telegram terminal client.
(!) usable but still in development
## Features ## Features
@ -55,6 +53,7 @@ docker run -it --rm tg
```sh ```sh
brew install tdlib brew install tdlib
``` ```
and then set in config `TDLIB_PATH`
- `python3.8` - `python3.8`
- `pip3 install python-telegram` - dependency for running from sources - `pip3 install python-telegram` - dependency for running from sources
- `terminal-notifier` or other program for notifications (see configuration) - `terminal-notifier` or other program for notifications (see configuration)
@ -69,7 +68,6 @@ Config file should be stored at `~/.config/tg/conf.py`. This is simple python fi
```python ```python
PHONE = "[your phone number]" PHONE = "[your phone number]"
ENC_KEY = "[telegram db encryption key]"
``` ```
### Advanced configuration: ### Advanced configuration:
@ -86,6 +84,7 @@ def get_pass(key):
PHONE = get_pass("i/telegram-phone") PHONE = get_pass("i/telegram-phone")
# encrypt you local tdlib database with the key
ENC_KEY = get_pass("i/telegram-enc-key") ENC_KEY = get_pass("i/telegram-enc-key")
# log level for debugging # log level for debugging
@ -107,6 +106,25 @@ NOTIFY_CMD = '/usr/local/bin/terminal-notifier -title "{title}" -subtitle "{subt
# The voice note must be encoded with the Opus codec, and stored inside an OGG # The voice note must be encoded with the Opus codec, and stored inside an OGG
# container. Voice notes can have only a single audio channel. # container. Voice notes can have only a single audio channel.
VOICE_RECORD_CMD = "ffmpeg -f avfoundation -i ':0' -c:a libopus -b:a 32k '{file_path}'" VOICE_RECORD_CMD = "ffmpeg -f avfoundation -i ':0' -c:a libopus -b:a 32k '{file_path}'"
# You can customize chat and msg flags however you want.
# By default words will be used for readability, but you can make
# it as simple as one letter flags like in mutt or add emojies
CHAT_FLAGS = {
"online": "●",
"pinned": "P",
"muted": "M",
"unread": "U",
}
MSG_FLAGS = {
"selected": "*",
"forwarded": "F",
"new": "N",
"unseen": "U",
"edited": "E",
"pending": "...",
"failed": "💩",
}
``` ```

View file

@ -5,6 +5,7 @@ overwritten by external config file
import os import os
import platform import platform
import runpy import runpy
from typing import Dict
_os_name = platform.system() _os_name = platform.system()
_darwin = "Darwin" _darwin = "Darwin"
@ -55,6 +56,9 @@ if _os_name == _linux:
else: else:
COPY_CMD = "pbcopy" COPY_CMD = "pbcopy"
CHAT_FLAGS: Dict[str, str] = {}
MSG_FLAGS: Dict[str, str] = {}
if os.path.isfile(CONFIG_FILE): if os.path.isfile(CONFIG_FILE):
config_params = runpy.run_path(CONFIG_FILE) config_params = runpy.run_path(CONFIG_FILE)

View file

@ -477,6 +477,14 @@ class Controller:
self.queue.put(self._render) self.queue.put(self._render)
def _render(self) -> None: def _render(self) -> None:
self.render_chats()
self.render_msgs()
self.view.status.draw()
def render_chats(self) -> None:
self.queue.put(self._render_chats)
def _render_chats(self) -> None:
page_size = self.view.chats.h page_size = self.view.chats.h
chats = self.model.get_chats( chats = self.model.get_chats(
self.model.current_chat, page_size, MSGS_LEFT_SCROLL_THRESHOLD self.model.current_chat, page_size, MSGS_LEFT_SCROLL_THRESHOLD
@ -484,10 +492,7 @@ class Controller:
selected_chat = min( selected_chat = min(
self.model.current_chat, page_size - MSGS_LEFT_SCROLL_THRESHOLD self.model.current_chat, page_size - MSGS_LEFT_SCROLL_THRESHOLD
) )
self.view.chats.draw(selected_chat, chats) self.view.chats.draw(selected_chat, chats)
self.render_msgs()
self.view.status.draw()
def render_msgs(self) -> None: def render_msgs(self) -> None:
self.queue.put(self._render_msgs) self.queue.put(self._render_msgs)

View file

@ -26,7 +26,7 @@ def run(tg: Tdlib, stdscr: window) -> None:
model = Model(tg) model = Model(tg)
status_view = StatusView(stdscr) status_view = StatusView(stdscr)
msg_view = MsgView(stdscr, model.msgs, model, model.users) msg_view = MsgView(stdscr, model.msgs, model, model.users)
chat_view = ChatView(stdscr) chat_view = ChatView(stdscr, model.users)
view = View(stdscr, chat_view, msg_view, status_view) view = View(stdscr, chat_view, msg_view, status_view)
controller = Controller(model, view, tg) controller = Controller(model, view, tg)

View file

@ -1,4 +1,5 @@
import logging import logging
import time
from collections import defaultdict from collections import defaultdict
from typing import Any, Dict, List, Optional, Set, Tuple from typing import Any, Dict, List, Optional, Set, Tuple
@ -431,6 +432,23 @@ class MsgModel:
class UserModel: class UserModel:
statuses = {
"userStatusEmpty": "",
"userStatusOnline": "online",
"userStatusOffline": "offline",
"userStatusRecently": "recently",
"userStatusLastWeek": "last week",
"userStatusLastMonth": "last month",
}
types = {
"userTypeUnknown": "unknown",
"userTypeBot": "bot",
"userTypeDeleted": "deleted",
"userTypeRegular": "regular",
}
def __init__(self, tg: Tdlib) -> None: def __init__(self, tg: Tdlib) -> None:
self.tg = tg self.tg = tg
self.me = None self.me = None
@ -447,6 +465,22 @@ class UserModel:
self.me = result.update self.me = result.update
return self.me return self.me
def set_status(self, user_id: int, status: Dict[str, Any]):
if user_id not in self.users:
self.get_user(user_id)
self.users[user_id]["status"] = status
def is_online(self, user_id: int):
user = self.get_user(user_id)
if (
user
and user["type"]["@type"] != "userTypeBot"
and user["status"]["@type"] == "userStatusOnline"
and user["status"]["expires"] > time.time()
):
return True
return False
def get_user(self, user_id: int) -> Dict[str, Any]: def get_user(self, user_id: int) -> Dict[str, Any]:
if user_id in self.users: if user_id in self.users:
return self.users[user_id] return self.users[user_id]

View file

@ -263,3 +263,9 @@ def update_connection_state(controller: Controller, update: Dict[str, Any]):
} }
msg = states.get(state, "Unknown state") msg = states.get(state, "Unknown state")
controller.present_info(msg) controller.present_info(msg)
@update_handler("updateUserStatus")
def update_user_status(controller: Controller, update: Dict[str, Any]):
controller.model.users.set_status(update["user_id"], update["status"])
controller.render_chats()

View file

@ -18,7 +18,6 @@ from typing import Optional
from tg import config from tg import config
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
emoji_pattern = re.compile( emoji_pattern = re.compile(
"[" "["
"\U0001F600-\U0001F64F" # emoticons "\U0001F600-\U0001F64F" # emoticons
@ -51,12 +50,16 @@ def setup_log():
for level, filename in zip( for level, filename in zip(
(config.LOG_LEVEL, logging.ERROR), ("all.log", "error.log"), (config.LOG_LEVEL, logging.ERROR), ("all.log", "error.log"),
): ):
handler = logging.FileHandler(os.path.join(config.LOG_PATH, filename)) handler = logging.handlers.RotatingFileHandler(
os.path.join(config.LOG_PATH, filename),
maxBytes=parse_size("32MB"),
backupCount=1,
)
handler.setLevel(level) handler.setLevel(level)
handlers.append(handler) handlers.append(handler)
logging.basicConfig( logging.basicConfig(
format="%(levelname)-8s [%(asctime)s] %(name)s %(message).1000s", format="%(levelname)-8s [%(asctime)s] %(name)s %(message)s",
handlers=handlers, handlers=handlers,
) )
logging.getLogger().setLevel(config.LOG_LEVEL) logging.getLogger().setLevel(config.LOG_LEVEL)

View file

@ -4,6 +4,7 @@ from _curses import window # type: ignore
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple, cast from typing import Any, Dict, List, Optional, Tuple, cast
from tg import config
from tg.colors import blue, cyan, get_color, magenta, reverse, white, yellow from tg.colors import blue, cyan, get_color, magenta, reverse, white, yellow
from tg.models import Model, MsgModel, UserModel from tg.models import Model, MsgModel, UserModel
from tg.msg import MsgProxy from tg.msg import MsgProxy
@ -126,12 +127,13 @@ class StatusView:
class ChatView: class ChatView:
def __init__(self, stdscr: window, p: float = 0.5) -> None: def __init__(self, stdscr: window, users: UserModel, p: float = 0.5):
self.stdscr = stdscr self.stdscr = stdscr
self.h = 0 self.h = 0
self.w = 0 self.w = 0
self.win = stdscr.subwin(self.h, self.w, 0, 0) self.win = stdscr.subwin(self.h, self.w, 0, 0)
self._refresh = self.win.refresh self._refresh = self.win.refresh
self.users = users
def resize(self, rows: int, cols: int, p: float = 0.25) -> None: def resize(self, rows: int, cols: int, p: float = 0.25) -> None:
self.h = rows - 1 self.h = rows - 1
@ -193,33 +195,34 @@ class ChatView:
i, offset, last_msg, self._msg_color(is_selected) i, offset, last_msg, self._msg_color(is_selected)
) )
if left_label := self._get_chat_label( if flags := self._get_flags(unread_count, is_pinned, chat):
unread_count, is_pinned, chat
):
self.win.addstr( self.win.addstr(
i, i,
self.w - len(left_label) - 1, self.w - len(flags) - 1,
left_label, flags,
self._unread_color(is_selected), self._unread_color(is_selected),
) )
self._refresh() self._refresh()
@staticmethod def _get_flags(
def _get_chat_label( self, unread_count: int, is_pinned: bool, chat: Dict[str, Any]
unread_count: int, is_pinned: bool, chat: Dict[str, Any]
) -> str: ) -> str:
labels = [] flags = []
if self.users.is_online(chat["id"]):
flags.append("online")
if is_pinned: if is_pinned:
labels.append("pinned") flags.append("pinned")
if chat["notification_settings"]["mute_for"]: if chat["notification_settings"]["mute_for"]:
labels.append("muted") flags.append("muted")
if unread_count: if unread_count:
labels.append(str(unread_count)) flags.append(str(unread_count))
label = " ".join(labels) label = " ".join(config.CHAT_FLAGS.get(flag, flag) for flag in flags)
if label: if label:
return f" {label}" return f" {label}"
return label return label
@ -285,7 +288,7 @@ class MsgView:
if not flags: if not flags:
return "" return ""
return " ".join(flags) return " ".join(config.MSG_FLAGS.get(flag, flag) for flag in flags)
def _format_reply_msg( def _format_reply_msg(
self, chat_id: int, msg: str, reply_to: int, width_limit: int self, chat_id: int, msg: str, reply_to: int, width_limit: int