From bf8965fb4b6bb2c256e489069d1c2216e064b0ed Mon Sep 17 00:00:00 2001 From: mathieui Date: Sat, 10 Apr 2021 22:50:29 +0200 Subject: feature: Add a MAM history filler --- poezio/log_loader.py | 117 ++++++++++++++++++++++++++++++++++++++++-------- poezio/tabs/basetabs.py | 2 +- poezio/tabs/muctab.py | 9 ++-- 3 files changed, 105 insertions(+), 23 deletions(-) (limited to 'poezio') diff --git a/poezio/log_loader.py b/poezio/log_loader.py index 92a0306c..82cefd2a 100644 --- a/poezio/log_loader.py +++ b/poezio/log_loader.py @@ -1,8 +1,16 @@ +import asyncio import logging -from datetime import datetime, timedelta +import json +from datetime import datetime, timedelta, timezone from typing import List, Dict, Any +from poezio.config import config from poezio import tabs -from poezio.logger import iterate_messages_reverse, Logger +from poezio.logger import ( + build_log_message, + iterate_messages_reverse, + last_message_in_archive, + Logger, +) from poezio.mam import ( fetch_history, NoMAMSupportException, @@ -25,26 +33,27 @@ def make_line_local(tab: tabs.ChatTab, msg: Dict[str, Any]) -> Message: jid.resource = msg['nickname'] else: jid = JID(tab.jid) + msg['time'] = msg['time'].astimezone(tz=timezone.utc) return make_line(tab, msg['txt'], msg['time'], jid, '') -STATUS = {'mam_only', 'local_only', 'local_mam_completed'} +STATUS = {'mam_only', 'local_only'} class LogLoader: """ - An ephemeral class that loads history in a tab + An ephemeral class that loads history in a tab. + + Loading from local logs is blocked until history has been fetched from + MAM to fill the local archive. """ - load_status: str = 'mam_only' logger: Logger tab: tabs.ChatTab + mam_only: bool def __init__(self, logger: Logger, tab: tabs.ChatTab, - load_status: str = 'local_only'): - if load_status not in STATUS: - self.load_status = 'mam_only' - else: - self.load_status = load_status + mam_only: bool = True): + self.mam_only = mam_only self.logger = logger self.tab = tab @@ -53,12 +62,12 @@ class LogLoader: amount = 2 * self.tab.text_win.height gap = self.tab._text_buffer.find_last_gap_muc() if gap is not None: - if self.load_status == 'local_only': - messages = await self.local_fill_gap(gap) - else: + if self.mam_only: messages = await self.mam_fill_gap(gap) + else: + messages = await self.local_fill_gap(gap) else: - if self.load_status == 'mam_only': + if self.mam_only: messages = await self.mam_tab_open(amount) else: messages = await self.local_tab_open(amount) @@ -84,6 +93,7 @@ class LogLoader: tab.query_status = False async def local_tab_open(self, nb: int) -> List[BaseMessage]: + await self.wait_mam() results: List[BaseMessage] = [] filepath = self.logger.get_file_path(self.tab.jid) for msg in iterate_messages_reverse(filepath): @@ -111,6 +121,7 @@ class LogLoader: tab.query_status = False async def local_fill_gap(self, gap: HistoryGap) -> List[BaseMessage]: + await self.wait_mam() start = gap.last_timestamp_before_leave end = gap.first_timestamp_after_join @@ -141,16 +152,16 @@ class LogLoader: if rest > 1: return None - if self.load_status == 'mam_only': + if self.mam_only: messages = await self.mam_scroll_requested(height) else: messages = await self.local_scroll_requested(height) - log.debug('%s %s', messages[0].txt, messages[0].time) - tab._text_buffer.add_history_messages(messages) if messages: + tab._text_buffer.add_history_messages(messages) tab.core.refresh_window() async def local_scroll_requested(self, nb: int) -> List[BaseMessage]: + await self.wait_mam() tab = self.tab last_message_time = None if tab._text_buffer.messages: @@ -183,12 +194,80 @@ class LogLoader: messages = [EndOfArchive('End of archive reached', time=time)] return messages except NoMAMSupportException: - return await self.local_scroll_requested(nb) + return [] except (MAMQueryException, DiscoInfoException): tab.core.information( f'An error occured when fetching MAM for {tab.jid}', 'Error' ) - return await self.local_scroll_requested(nb) + return [] finally: tab.query_status = False + + async def wait_mam(self) -> None: + if not isinstance(self.tab, tabs.MucTab): + return + if self.tab.mam_filler is None: + return + await self.tab.mam_filler.done.wait() + + +class MAMFiller: + """Class that loads messages from MAM history into the local logs. + """ + tab: tabs.ChatTab + logger: Logger + future: asyncio.Future + done: asyncio.Event + + def __init__(self, tab: tabs.ChatTab, logger: Logger): + self.tab = tab + self.logger = logger + logger.fd_busy(str(tab.jid)) + self.future = asyncio.ensure_future(self.fetch_routine()) + self.done = asyncio.Event() + + def cancel(self): + self.future.cancel() + self.end() + + async def fetch_routine(self) -> None: + filepath = self.logger.get_file_path(self.tab.jid) + try: + last_msg = last_message_in_archive(filepath) + last_msg_time = None + if last_msg: + last_msg_time = last_msg['time'] + timedelta(seconds=1) + try: + messages = await fetch_history( + self.tab, + start=last_msg_time, + amount=2000, + ) + except (DiscoInfoException, NoMAMSupportException, MAMQueryException): + log.debug('Failed for %s', self.tab.jid, exc_info=True) + return + log.debug('Fetched %s:\n%s', len(messages), messages) + + def build_message(msg): + return build_log_message( + msg.nickname, + msg.txt, + msg.time, + prefix='MR', + ) + + logs = ''.join(map(build_message, messages)) + log.debug(logs) + + self.logger.log_raw(self.tab.jid, logs, force=True) + except Exception as exc: + log.debug('exception: %s', exc, exc_info=True) + finally: + log.debug('finishing fill for %s', self.tab.jid) + self.end() + + def end(self): + self.logger.fd_available(str(self.tab.jid)) + self.tab.mam_filler = None + self.done.set() diff --git a/poezio/tabs/basetabs.py b/poezio/tabs/basetabs.py index 306c79f9..0a31931b 100644 --- a/poezio/tabs/basetabs.py +++ b/poezio/tabs/basetabs.py @@ -967,7 +967,7 @@ class ChatTab(Tab): if not self.query_status: from poezio.log_loader import LogLoader asyncio.ensure_future( - LogLoader(logger, self).scroll_requested() + LogLoader(logger, self, config.get('use_log')).scroll_requested() ) return self.text_win.scroll_up(self.text_win.height - 1) diff --git a/poezio/tabs/muctab.py b/poezio/tabs/muctab.py index 54e78c72..9334ce4c 100644 --- a/poezio/tabs/muctab.py +++ b/poezio/tabs/muctab.py @@ -48,7 +48,7 @@ from poezio.config import config from poezio.core.structs import Command from poezio.decorators import refresh_wrapper, command_args_parser from poezio.logger import logger -from poezio.log_loader import LogLoader +from poezio.log_loader import LogLoader, MAMFiller from poezio.roster import roster from poezio.theming import get_theme, dump_tuple from poezio.user import User @@ -83,7 +83,8 @@ class MucTab(ChatTab): plugin_commands: Dict[str, Command] = {} plugin_keys: Dict[str, Callable[..., Any]] = {} additional_information: Dict[str, Callable[[str], str]] = {} - lagged = False + lagged: bool = False + mam_filler: Optional[MAMFiller] def __init__(self, core: Core, jid: JID, nick: str, password: Optional[str] = None) -> None: ChatTab.__init__(self, core, jid) @@ -104,6 +105,7 @@ class MucTab(ChatTab): self.topic_from = '' # Self ping event, so we can cancel it when we leave the room self.self_ping_event: Optional[timed_events.DelayedEvent] = None + self.mam_filler = None # UI stuff self.topic_win = windows.Topic() self.v_separator = windows.VerticalSeparator() @@ -179,6 +181,7 @@ class MucTab(ChatTab): seconds = None if last_message is not None: seconds = (datetime.now() - last_message.time).seconds + self.mam_filler = MAMFiller(self, logger) muc.join_groupchat( self.core, self.jid.bare, @@ -605,7 +608,7 @@ class MucTab(ChatTab): ), ) asyncio.ensure_future( - LogLoader(logger, self).tab_open(), + LogLoader(logger, self, config.get('use_log')).tab_open(), ) def handle_presence_joined(self, presence: Presence, status_codes: Set[int]) -> None: -- cgit v1.2.3