def update_msg_content_opened(self, chat_id: int, msg_id: int) -> None: msg = self.msgs[chat_id].get(msg_id) if not msg: return msg_proxy = MsgProxy(msg) if msg_proxy.content_type == "voice": msg_proxy.is_listened = True elif msg_proxy.content_type == "recording": msg_proxy.is_viewed = True
def update_msg_content_opened(self, chat_id: int, msg_id: int) -> None: index = self.msg_idx[chat_id].get(msg_id) if not index: return msg = MsgProxy(self.msgs[chat_id][index]) if msg.content_type == "voice": msg.is_listened = True elif msg.content_type == "recording": msg.is_viewed = True
def update_msg_content_opened(self, chat_id: int, msg_id: int): for message in self.msgs[chat_id]: if message["id"] != msg_id: continue msg = MsgProxy(message) if msg.content_type == "voice": msg.is_listened = True elif msg.content_type == "recording": msg.is_viewed = True # TODO: start the TTL timer for self-destructing messages # that is the last case to implement # https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1update_message_content_opened.html return
def open_url(self) -> None: msg = MsgProxy(self.model.current_msg) if not msg.is_text: return self.present_error("Does not contain urls") text = msg["content"]["text"]["text"] urls = [] for entity in msg["content"]["text"]["entities"]: _type = entity["type"]["@type"] if _type == "textEntityTypeUrl": offset = entity["offset"] length = entity["length"] url = text[offset:offset + length] elif _type == "textEntityTypeTextUrl": url = entity["type"]["url"] else: continue urls.append(url) if not urls: return self.present_error("No url to open") if len(urls) == 1: with suspend(self.view) as s: s.call( config.DEFAULT_OPEN.format(file_path=shlex.quote(urls[0]))) return with suspend(self.view) as s: s.run_with_input(config.URL_VIEW, "\n".join(urls))
def update_file(controller: Controller, update: Dict[str, Any]) -> None: file_id = update["file"]["id"] local = update["file"]["local"] chat_id, msg_id = controller.model.downloads.get(file_id, (None, None)) if chat_id is None or msg_id is None: log.warning("Can't find information about file with file_id=%s", file_id) return msg = controller.model.msgs.msgs[chat_id].get(msg_id) if not msg: return proxy = MsgProxy(msg) proxy.local = local controller.render_msgs() if proxy.is_downloaded: controller.model.downloads.pop(file_id)
def get_last_msg(chat: Dict[str, Any], users: UserModel) -> Tuple[Optional[int], str]: last_msg = chat.get("last_message") if not last_msg: return None, "<No messages yet>" return ( last_msg["sender"].get("user_id"), parse_content(MsgProxy(last_msg), users), )
def update_file(controller: Controller, update): log.info("update_file: %s", update) file_id = update["file"]["id"] local = update["file"]["local"] chat_id, msg_id = controller.model.downloads.get(file_id, (None, None)) if chat_id is None: log.warning("Can't find information about file with file_id=%s", file_id) return msgs = controller.model.msgs.msgs[chat_id] for msg in msgs: if msg["id"] == msg_id: proxy = MsgProxy(msg) proxy.local = local controller.render_msgs() if proxy.is_downloaded: controller.model.downloads.pop(file_id) break
def download_current_file(self) -> None: msg = MsgProxy(self.model.current_msg) log.debug("Downloading msg: %s", msg.msg) file_id = msg.file_id if not file_id: self.present_info("File can't be downloaded") return self.download(file_id, msg["chat_id"], msg["id"]) self.present_info("File started downloading")
def _toggle_select_msg(self) -> None: chat_id = self.model.chats.id_by_index(self.model.current_chat) if not chat_id: return msg = MsgProxy(self.model.current_msg) if msg.msg_id in self.model.selected[chat_id]: self.model.selected[chat_id].remove(msg.msg_id) else: self.model.selected[chat_id].append(msg.msg_id)
def update_new_message(controller: Controller, update: Dict[str, Any]) -> None: msg = MsgProxy(update["message"]) controller.model.msgs.add_message(msg.chat_id, msg.msg) current_chat_id = controller.model.current_chat_id if current_chat_id == msg.chat_id: controller.render_msgs() if msg.file_id and msg.size and msg.size <= max_download_size: controller.download(msg.file_id, msg.chat_id, msg["id"]) controller.notify_for_message(msg.chat_id, msg)
def show_user_info(self) -> None: """Show user profile""" msg = MsgProxy(self.model.current_msg) user_id = msg.sender_id info = self.model.get_user_info(user_id) with suspend(self.view) as s: s.run_with_input( config.VIEW_TEXT_CMD, "\n".join(f"{k}: {v}" for k, v in info.items() if v), )
def jump_to_reply_msg(self) -> None: chat_id = self.model.chats.id_by_index(self.model.current_chat) if not chat_id: return msg = MsgProxy(self.model.current_msg) if not msg.reply_msg_id: return self.present_error("This msg does not reply") if not self.model.msgs.jump_to_msg_by_id(chat_id, msg.reply_msg_id): return self.present_error( "Can't jump to reply msg: it's not preloaded or deleted") return self.render_msgs()
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() if not cmd: return if "%s" not in cmd: return self.present_error( "command should contain <%s> which will be replaced by file path" ) return self._open_msg(msg, cmd)
def _format_reply_msg(self, chat_id: int, msg: str, reply_to: int, width_limit: int) -> str: reply_msg = MsgProxy(self.msg_model.get_message(chat_id, reply_to)) if reply_msg_content := self._parse_msg(reply_msg): reply_msg_content = reply_msg_content.replace("\n", " ") reply_sender = self._get_user_by_id(reply_msg.sender_id) sender_name = f" {reply_sender}:" if reply_sender else "" reply_line = f">{sender_name} {reply_msg_content}" if len(reply_line) >= width_limit: reply_line = f"{reply_line[:width_limit - 4]}..." msg = f"{reply_line}\n{msg}"
def _format_reply_msg(self, chat_id: int, msg: str, reply_to: int, width_limit: int) -> str: _msg = self.model.msgs.get_message(chat_id, reply_to) if not _msg: return msg reply_msg = MsgProxy(_msg) if reply_msg_content := self._parse_msg(reply_msg): reply_sender = self.model.users.get_user_label(reply_msg.sender_id) sender_name = f" {reply_sender}:" if reply_sender else "" reply_line = f">{sender_name} {reply_msg_content}" if len(reply_line) >= width_limit: reply_line = f"{reply_line[:width_limit - 4]}..." msg = f"{reply_line}\n{msg}"
def copy_files(self, chat_id: int, msg_ids: List[int], dest_dir: str) -> bool: is_copied = False for msg_id in msg_ids: _msg = self.msgs.get_message(chat_id, msg_id) if not _msg: return False msg = MsgProxy(_msg) if msg.file_id and msg.local_path: file_path = msg.local_path shutil.copy2(file_path, dest_dir) is_copied = True return is_copied
def copy_msgs_text(self): """Copies current msg text or path to file if it's file""" buffer = [] from_chat_id, msg_ids = self.copied_msgs if not msg_ids: return False for msg_id in msg_ids: msg = MsgProxy(self.msgs.get_message(from_chat_id, msg_id)) if msg.file_id: buffer.append(msg.local_path) elif msg.is_text: buffer.append(msg.text_content) copy_to_clipboard("\n".join(buffer))
def reply_with_long_message(self): if not self.can_send_msg(): self.present_info("Can't send msg in this chat") return chat_id = self.model.current_chat_id reply_to_msg = self.model.current_msg_id msg = MsgProxy(self.model.current_msg) with NamedTemporaryFile("w+", suffix=".txt") as f, suspend( self.view ) as s: f.write(insert_replied_msg(msg)) f.seek(0) s.call(config.LONG_MSG_CMD.format(file_path=shlex.quote(f.name))) with open(f.name) as f: if msg := strip_replied_msg(f.read().strip()): self.tg.reply_message(chat_id, reply_to_msg, msg) self.present_info("Message sent") else:
def edit_msg(self) -> None: msg = MsgProxy(self.model.current_msg) log.info("Editing msg: %s", msg.msg) if not self.model.is_me(msg.sender_id): return self.present_error("You can edit only your messages!") if not msg.is_text: return self.present_error("You can edit text messages only!") if not msg.can_be_edited: return self.present_error("Meessage can't be edited!") with NamedTemporaryFile("r+", suffix=".txt") as f, suspend(self.view) as s: f.write(msg.text_content) f.flush() s.call(f"{config.EDITOR} {f.name}") with open(f.name) as f: if text := f.read().strip(): self.model.edit_message(text=text) self.present_info("Message edited")
def open_current_msg(self): msg = MsgProxy(self.model.current_msg) if msg.is_text: with NamedTemporaryFile("w", suffix=".txt") as f: f.write(msg.text_content) f.flush() with suspend(self.view) as s: s.open_file(f.name) return path = msg.local_path if not path: self.present_info("File should be downloaded first") return chat_id = self.model.chats.id_by_index(self.model.current_chat) if not chat_id: return self.tg.open_message_content(chat_id, msg.msg_id) with suspend(self.view) as s: s.open_file(path)
def parse_content(content: Dict[str, Any]) -> str: msg = MsgProxy({"content": content}) if msg.is_text: return content["text"]["text"] if not msg.content_type: # not implemented _type = content["@type"] return f"[{_type}]" fields = dict( name=msg.file_name, download=get_download(msg.local, msg.size), size=msg.human_size, duration=msg.duration, listened=format_bool(msg.is_listened), viewed=format_bool(msg.is_viewed), ) info = ", ".join(f"{k}={v}" for k, v in fields.items() if v) return f"[{msg.content_type}: {info}]"
def parse_content(content: Dict[str, Any]) -> str: msg = MsgProxy({"content": content}) if msg.is_text: return content["text"]["text"].replace("\n", " ") _type = content["@type"] if _type == "messageBasicGroupChatCreate": return "[created the group]" if _type == "messageChatAddMembers": return "[joined the group]" if not msg.content_type: # not implemented return f"[{_type}]" content_text = "" if msg.is_poll: content_text = f"\n {msg.poll_question}" for option in msg.poll_options: content_text += f"\n * {option['voter_count']} ({option['vote_percentage']}%) | {option['text']}" fields = dict( name=msg.file_name, download=get_download(msg.local, msg.size), size=msg.human_size, duration=msg.duration, listened=format_bool(msg.is_listened), viewed=format_bool(msg.is_viewed), animated=msg.is_animated, emoji=msg.sticker_emoji, closed=msg.is_closed_poll, ) info = ", ".join(f"{k}={v}" for k, v in fields.items() if v is not None) return f"[{msg.content_type}: {info}]{content_text}"
def _collect_msgs_to_draw( self, current_msg_idx: int, msgs: List[Tuple[int, Dict[str, Any]]], min_msg_padding: int, ) -> List[Tuple[Tuple[str, ...], bool, int]]: """ Tries to collect list of messages that will satisfy `min_msg_padding` theshold. Long messages could prevent other messages from displaying on the screen. In order to prevent scenario when *selected* message moved out from the visible area of the screen by some long messages, this function will remove message one by one from the start until selected message could be visible on the screen. """ selected_item_idx: Optional[int] = None collected_items: List[Tuple[Tuple[str, ...], bool, int]] = [] for ignore_before in range(len(msgs)): if selected_item_idx is not None: break collected_items = [] line_num = self.h for msg_idx, msg_item in msgs[ignore_before:]: is_selected_msg = current_msg_idx == msg_idx msg_proxy = MsgProxy(msg_item) dt = msg_proxy.date.strftime("%H:%M:%S") user_id_item = msg_proxy.sender_id user_id = self._get_user_by_id(user_id_item) flags = self._get_flags(msg_proxy) if user_id and flags: # if not channel add space between name and flags flags = " " + flags label_elements = f" {dt} ", user_id, flags label_len = sum(len(e) for e in label_elements) msg = self._format_msg( msg_proxy, user_id_item, width_limit=self.w - label_len - 1 ) elements = *label_elements, f" {msg}" needed_lines = 0 for i, msg_line in enumerate(msg.split("\n")): # count wide character utf-8 symbols that take > 1 bytes to # print it causes invalid offset emojies_count = sum( map(len, emoji_pattern.findall(msg_line)) ) line_len = len(msg_line) + emojies_count # first line cotains msg lable, e.g user name, date if i == 0: line_len += label_len needed_lines += (line_len // self.w) + 1 line_num -= needed_lines if line_num < 0: tail_lines = needed_lines + line_num - 1 # try preview long message that did fit in the screen if tail_lines > 0 and not is_selected_msg: limit = self.w * tail_lines tail_chatacters = len(msg) - limit - 3 elements = ( "", "", "", f" ...{msg[tail_chatacters:]}", ) collected_items.append((elements, is_selected_msg, 0)) break collected_items.append((elements, is_selected_msg, line_num)) if is_selected_msg: selected_item_idx = len(collected_items) - 1 if ( # ignore first and last msg selected_item_idx not in (0, len(msgs) - 1, None) and selected_item_idx is not None and len(collected_items) - 1 - selected_item_idx < min_msg_padding ): selected_item_idx = None return collected_items
def open_current_msg(self) -> None: """Open msg or file with cmd in mailcap""" msg = MsgProxy(self.model.current_msg) self._open_msg(msg)
def view_current_msg(self) -> None: msg = MsgProxy(self.current_msg) msg_id = msg["id"] if chat_id := self.chats.id_by_index(self.current_chat): self.tg.view_messages(chat_id, [msg_id])
def view_current_msg(self): chat_id = self.chats.id_by_index(self.current_chat) msg = MsgProxy(self.current_msg) msg_id = msg["id"] self.tg.view_messages(chat_id, [msg_id])