mirror of
https://github.com/paul-nameless/tg
synced 2025-02-16 18:48:24 +00:00
Add users status in chat flags
Rotate logs Add ability to configure flags representation
This commit is contained in:
parent
468950bc6a
commit
0151e8df6a
8 changed files with 99 additions and 26 deletions
26
README.md
26
README.md
|
@ -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": "💩",
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
34
tg/models.py
34
tg/models.py
|
@ -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]
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
33
tg/views.py
33
tg/views.py
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue