From ea438438f2c397958be8171029bd673b17c35c16 Mon Sep 17 00:00:00 2001 From: mathieui Date: Sun, 10 May 2020 15:16:24 +0200 Subject: Add specific types for own leave and join --- poezio/tabs/muctab.py | 25 ++++++++++++++++++------- poezio/ui/types.py | 8 ++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/poezio/tabs/muctab.py b/poezio/tabs/muctab.py index d16ac58a..f5d1c700 100644 --- a/poezio/tabs/muctab.py +++ b/poezio/tabs/muctab.py @@ -40,7 +40,14 @@ from poezio.roster import roster from poezio.theming import get_theme, dump_tuple from poezio.user import User from poezio.core.structs import Completion, Status -from poezio.ui.types import BaseMessage, Message, InfoMessage, StatusMessage +from poezio.ui.types import ( + BaseMessage, + InfoMessage, + Message, + MucOwnJoinMessage, + MucOwnLeaveMessage, + StatusMessage, +) log = logging.getLogger(__name__) @@ -200,7 +207,7 @@ class MucTab(ChatTab): 'color_spec': spec_col, 'nick': self.own_nick, } - self.add_message(InfoMessage(msg), typ=2) + self.add_message(MucOwnLeaveMessage(msg), typ=2) self.disconnect() muc.leave_groupchat(self.core.xmpp, self.jid.bare, self.own_nick, message) @@ -567,7 +574,7 @@ class MucTab(ChatTab): 'nick_col': color, 'info_col': info_col, } - self.add_message(InfoMessage(enable_message), typ=2) + self.add_message(MucOwnJoinMessage(enable_message), typ=2) self.core.enable_private_tabs(self.jid.bare, enable_message) if '201' in status_codes: self.add_message( @@ -651,7 +658,7 @@ class MucTab(ChatTab): def on_non_member_kicked(self): """We have been kicked because the MUC is members-only""" self.add_message( - InfoMessage( + MucOwnLeaveMessage( 'You have been kicked because you ' 'are not a member and the room is now members-only.' ), @@ -661,7 +668,7 @@ class MucTab(ChatTab): def on_muc_shutdown(self): """We have been kicked because the MUC service is shutting down""" self.add_message( - InfoMessage( + MucOwnLeaveMessage( 'You have been kicked because the' ' MUC service is shutting down.' ), @@ -759,6 +766,7 @@ class MucTab(ChatTab): """ When someone is banned from a muc """ + cls = InfoMessage self.users.remove(user) by = presence.xml.find('{%s}x/{%s}item/{%s}actor' % (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER)) @@ -774,6 +782,7 @@ class MucTab(ChatTab): char_kick = theme.CHAR_KICK if from_nick == self.own_nick: # we are banned + cls = MucOwnLeaveMessage if by: kick_msg = ('\x191}%(spec)s \x193}You\x19%(info_col)s}' ' have been banned by \x194}%(by)s') % { @@ -834,12 +843,13 @@ class MucTab(ChatTab): 'reason': reason.text, 'info_col': info_col } - self.add_message(InfoMessage(kick_msg), typ=2) + self.add_message(cls(kick_msg), typ=2) def on_user_kicked(self, presence, user, from_nick): """ When someone is kicked from a muc """ + cls = InfoMessage self.users.remove(user) actor_elem = presence.xml.find('{%s}x/{%s}item/{%s}actor' % (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER)) @@ -852,6 +862,7 @@ class MucTab(ChatTab): if actor_elem is not None: by = actor_elem.get('nick') or actor_elem.get('jid') if from_nick == self.own_nick: # we are kicked + cls = MucOwnLeaveMessage if by: kick_msg = ('\x191}%(spec)s \x193}You\x19' '%(info_col)s} have been kicked' @@ -912,7 +923,7 @@ class MucTab(ChatTab): 'reason': reason.text, 'info_col': info_col } - self.add_message(InfoMessage(kick_msg), typ=2) + self.add_message(cls(kick_msg), typ=2) def on_user_leave_groupchat(self, user: User, diff --git a/poezio/ui/types.py b/poezio/ui/types.py index ae72b6b9..34924112 100644 --- a/poezio/ui/types.py +++ b/poezio/ui/types.py @@ -33,6 +33,14 @@ class InfoMessage(BaseMessage): super().__init__(txt=txt, identifier=identifier, time=time) +class MucOwnLeaveMessage(InfoMessage): + """Status message displayed on our room leave/kick/ban""" + + +class MucOwnJoinMessage(InfoMessage): + """Status message displayed on our room join""" + + class XMLLog(BaseMessage): """XML Log message""" __slots__ = ('txt', 'time', 'identifier', 'incoming') -- cgit v1.2.3 From 7e29abf2e62892ec354d2a45d3bee8388d8d23bb Mon Sep 17 00:00:00 2001 From: mathieui Date: Sun, 10 May 2020 15:36:11 +0200 Subject: Add a concept of "history gap" --- poezio/text_buffer.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/poezio/text_buffer.py b/poezio/text_buffer.py index 3b3ac051..15869676 100644 --- a/poezio/text_buffer.py +++ b/poezio/text_buffer.py @@ -19,9 +19,15 @@ from typing import ( Tuple, Union, ) +from dataclasses import dataclass from datetime import datetime from poezio.config import config -from poezio.ui.types import Message, BaseMessage +from poezio.ui.types import ( + BaseMessage, + Message, + MucOwnJoinMessage, + MucOwnLeaveMessage, +) if TYPE_CHECKING: from poezio.windows.text_win import TextWin @@ -35,6 +41,15 @@ class AckError(Exception): pass +@dataclass +class HistoryGap: + """Class representing a period of non-presence inside a MUC""" + leave_message: Optional[MucOwnLeaveMessage] + join_message: Optional[MucOwnJoinMessage] + last_timestamp_before_leave: Optional[datetime] + first_timestamp_after_join: Optional[datetime] + + class TextBuffer: """ This class just keep trace of messages, in a list with various @@ -58,6 +73,72 @@ class TextBuffer: def add_window(self, win) -> None: self._windows.append(win) + def find_last_gap_muc(self) -> Optional[HistoryGap]: + """Find the last known history gap contained in buffer""" + leave, join = None, None + for i, item in enumerate(reversed(self.messages)): + if isinstance(item, MucOwnLeaveMessage): + leave = (i, item) + break + if isinstance(item, MucOwnJoinMessage): + join = (i, item) + if join and leave: # Skip if we find a message in the interval + real_leave = len(self.messages) - leave[0] - 1 + real_join = len(self.messages) - join[0] - 1 + for i in range(real_leave, real_join, 1): + if isinstance(self.messages[i], Message): + return None + elif not (join or leave): + return None + if leave is None: + last_timestamp, leave_msg = None, None + else: + last_timestamp = None + leave_msg = leave[1] + for i in range(len(self.messages) - leave[0] - 1, 0, -1): + if isinstance(self.messages[i], Message): + last_timestamp = self.messages[i].time + break + first_timestamp = datetime.now() + if join is None: + join_msg = None + else: + join_msg = join[1] + for i in range(len(self.messages) - join[0], len(self.messages)): + msg = self.messages[i] + if isinstance(msg, Message) and msg.time < first_timestamp: + first_timestamp = msg.time + break + return HistoryGap( + leave_message=leave_msg, + join_message=join_msg, + last_timestamp_before_leave=last_timestamp, + first_timestamp_after_join=first_timestamp, + ) + + def get_gap_index(self, gap: HistoryGap) -> Optional[int]: + """Find the first index to insert into inside a gap""" + if gap.leave_message is None: + return 0 + for i, msg in enumerate(self.messages): + if msg is gap.leave_message: + return i + 1 + return None + + def add_history_messages(self, messages: List[Message], gap: Optional[HistoryGap] = None) -> None: + """Insert history messages at their correct place """ + index = 0 + if gap is not None: + index = self.get_gap_index(gap) + if index is None: # Not sure what happened, abort + return + for message in messages: + self.messages.insert(index, message) + index += 1 + log.debug('inserted message: %s', message) + for window in self._windows: # make the associated windows + window.rebuild_everything(self) + @property def last_message(self) -> Optional[BaseMessage]: return self.messages[-1] if self.messages else None -- cgit v1.2.3 From e488b75737f0d691b606b4ac00afcae2ea4ed7b0 Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 22 May 2020 01:31:01 +0200 Subject: Add tests for text_buffer --- test/test_text_buffer.py | 157 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 test/test_text_buffer.py diff --git a/test/test_text_buffer.py b/test/test_text_buffer.py new file mode 100644 index 00000000..8e9829f4 --- /dev/null +++ b/test/test_text_buffer.py @@ -0,0 +1,157 @@ +""" +Tests for the TextBuffer class +""" +from pytest import fixture + +from poezio.text_buffer import ( + TextBuffer, + HistoryGap, +) + +from poezio.ui.types import ( + Message, + BaseMessage, + MucOwnJoinMessage, + MucOwnLeaveMessage, +) + + +@fixture(scope='function') +def buf2048(): + return TextBuffer(2048) + +@fixture(scope='function') +def msgs_nojoin(): + msg1 = Message('1', 'q') + msg2 = Message('2', 's') + leave = MucOwnLeaveMessage('leave') + return [msg1, msg2, leave] + + +@fixture(scope='function') +def msgs_noleave(): + join = MucOwnJoinMessage('join') + msg3 = Message('3', 'd') + msg4 = Message('4', 'f') + return [join, msg3, msg4] + + +def test_last_message(buf2048): + msg = BaseMessage('toto') + buf2048.add_message(BaseMessage('titi')) + buf2048.add_message(msg) + assert buf2048.last_message is msg + + +def test_message_nb_limit(): + buf = TextBuffer(5) + for i in range(10): + buf.add_message(BaseMessage("%s" % i)) + assert len(buf.messages) == 5 + + +def test_find_gap(buf2048, msgs_noleave): + msg1 = Message('1', 'q') + msg2 = Message('2', 's') + leave = MucOwnLeaveMessage('leave') + join = MucOwnJoinMessage('join') + msg3 = Message('3', 'd') + msg4 = Message('4', 'f') + msgs = [msg1, msg2, leave, join, msg3, msg4] + for msg in msgs: + buf2048.add_message(msg) + gap = buf2048.find_last_gap_muc() + assert gap.leave_message == leave + assert gap.join_message == join + assert gap.last_timestamp_before_leave == msg2.time + assert gap.first_timestamp_after_join == msg3.time + + +def test_find_gap_already_filled(buf2048): + msg1 = Message('1', 'q') + msg2 = Message('2', 's') + leave = MucOwnLeaveMessage('leave') + msg5 = Message('5', 'g') + msg6 = Message('6', 'h') + join = MucOwnJoinMessage('join') + msg3 = Message('3', 'd') + msg4 = Message('4', 'f') + msgs = [msg1, msg2, leave, msg5, msg6, join, msg3, msg4] + for msg in msgs: + buf2048.add_message(msg) + assert buf2048.find_last_gap_muc() is None + + +def test_find_gap_noleave(buf2048, msgs_noleave): + for msg in msgs_noleave: + buf2048.add_message(msg) + gap = buf2048.find_last_gap_muc() + assert gap.leave_message is None + assert gap.last_timestamp_before_leave is None + assert gap.join_message == msgs_noleave[0] + assert gap.first_timestamp_after_join == msgs_noleave[1].time + + +def test_find_gap_nojoin(buf2048, msgs_nojoin): + for msg in msgs_nojoin: + buf2048.add_message(msg) + gap = buf2048.find_last_gap_muc() + assert gap.leave_message == msgs_nojoin[-1] + assert gap.join_message is None + assert gap.last_timestamp_before_leave == msgs_nojoin[1].time + + +def test_get_gap_index(buf2048): + msg1 = Message('1', 'q') + msg2 = Message('2', 's') + leave = MucOwnLeaveMessage('leave') + join = MucOwnJoinMessage('join') + msg3 = Message('3', 'd') + msg4 = Message('4', 'f') + msgs = [msg1, msg2, leave, join, msg3, msg4] + for msg in msgs: + buf2048.add_message(msg) + gap = buf2048.find_last_gap_muc() + assert buf2048.get_gap_index(gap) == 3 + + +def test_get_gap_index_nojoin(buf2048, msgs_nojoin): + for msg in msgs_nojoin: + buf2048.add_message(msg) + gap = buf2048.find_last_gap_muc() + assert buf2048.get_gap_index(gap) == 3 + + +def test_get_gap_index_noleave(buf2048, msgs_noleave): + for msg in msgs_noleave: + buf2048.add_message(msg) + gap = buf2048.find_last_gap_muc() + assert buf2048.get_gap_index(gap) == 0 + + +def test_add_history_messages(buf2048): + msg1 = Message('1', 'q') + msg2 = Message('2', 's') + leave = MucOwnLeaveMessage('leave') + join = MucOwnJoinMessage('join') + msg3 = Message('3', 'd') + msg4 = Message('4', 'f') + msgs = [msg1, msg2, leave, join, msg3, msg4] + for msg in msgs: + buf2048.add_message(msg) + msg5 = Message('5', 'g') + msg6 = Message('6', 'h') + gap = buf2048.find_last_gap_muc() + buf2048.add_history_messages([msg5, msg6], gap=gap) + assert buf2048.messages == [msg1, msg2, leave, msg5, msg6, join, msg3, msg4] + + +def test_add_history_empty(buf2048): + msg1 = Message('1', 'q') + msg2 = Message('2', 's') + msg3 = Message('3', 'd') + msg4 = Message('4', 'f') + buf2048.add_message(msg1) + buf2048.add_history_messages([msg2, msg3, msg4]) + assert buf2048.messages == [msg2, msg3, msg4, msg1] + -- cgit v1.2.3 From b7423ee43bb14fc97b5db090206cee7a76e8b368 Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 22 May 2020 01:32:11 +0200 Subject: Remove "top" message logic --- poezio/text_buffer.py | 3 +-- poezio/ui/render.py | 2 -- poezio/windows/text_win.py | 10 +++------- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/poezio/text_buffer.py b/poezio/text_buffer.py index 15869676..121a797a 100644 --- a/poezio/text_buffer.py +++ b/poezio/text_buffer.py @@ -163,8 +163,7 @@ class TextBuffer: nick_size=nick_size) if ret_val == 0: ret_val = nb - top = isinstance(msg, Message) and msg.top - if window.pos != 0 and top is False: + if window.pos != 0: window.scroll_up(nb) return min(ret_val, 1) diff --git a/poezio/ui/render.py b/poezio/ui/render.py index a431b4e7..c85d3cc5 100644 --- a/poezio/ui/render.py +++ b/poezio/ui/render.py @@ -94,8 +94,6 @@ def build_message(msg: Message, width: int, timestamp: bool, nick_size: int = 10 offset = msg.compute_offset(timestamp, nick_size) lines = poopt.cut_text(txt, width - offset - 1) generated_lines = generate_lines(lines, msg, default_color='') - if msg.top: - generated_lines.reverse() return generated_lines diff --git a/poezio/windows/text_win.py b/poezio/windows/text_win.py index 9e6641f7..2cb75271 100644 --- a/poezio/windows/text_win.py +++ b/poezio/windows/text_win.py @@ -95,14 +95,10 @@ class TextWin(Win): lines = build_lines( message, self.width, timestamp=timestamp, nick_size=nick_size ) - if isinstance(message, Message) and message.top: - for line in lines: - self.built_lines.insert(0, line) + if self.lock: + self.lock_buffer.extend(lines) else: - if self.lock: - self.lock_buffer.extend(lines) - else: - self.built_lines.extend(lines) + self.built_lines.extend(lines) if not lines or not lines[0]: return 0 if isinstance(message, Message) and message.highlight: -- cgit v1.2.3 From fbec652efd4f0667f350de823b71d8ae91d98cae Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 22 May 2020 01:34:11 +0200 Subject: Core: dedup some code --- poezio/core/core.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/poezio/core/core.py b/poezio/core/core.py index 8ac88dd4..973c9103 100644 --- a/poezio/core/core.py +++ b/poezio/core/core.py @@ -2075,16 +2075,7 @@ class Core: # do not join rooms that do not have autojoin # but display them anyway if bm.autojoin: - muc.join_groupchat( - self, - bm.jid, - nick, - passwd=bm.password, - status=self.status.message, - show=self.status.show, - tab=tab) - if tab._text_buffer.last_message is None: - asyncio.ensure_future(mam.on_tab_open(tab)) + tab.join() def check_bookmark_storage(self, features): private = 'jabber:iq:private' in features -- cgit v1.2.3 From 54339ee7e070dd51229d7ad939d8d24963438694 Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 22 May 2020 01:35:07 +0200 Subject: Textbuffer: add "find last message" function --- poezio/tabs/muctab.py | 2 +- poezio/text_buffer.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/poezio/tabs/muctab.py b/poezio/tabs/muctab.py index f5d1c700..7b0f8a42 100644 --- a/poezio/tabs/muctab.py +++ b/poezio/tabs/muctab.py @@ -161,7 +161,7 @@ class MucTab(ChatTab): delta = datetime.now() - self.last_connection seconds = delta.seconds + delta.days * 24 * 3600 else: - seconds = None + seconds = self._text_buffer.find_last_message() muc.join_groupchat( self.core, self.jid.bare, diff --git a/poezio/text_buffer.py b/poezio/text_buffer.py index 121a797a..03ad2f1b 100644 --- a/poezio/text_buffer.py +++ b/poezio/text_buffer.py @@ -277,6 +277,13 @@ class TextBuffer: def del_window(self, win) -> None: self._windows.remove(win) + def find_last_message(self) -> Optional[Message]: + """Find the last real message received in this buffer""" + for message in reversed(self.messages): + if isinstance(message, Message): + return message + return None + def __del__(self): size = len(self.messages) log.debug('** Deleting %s messages from textbuffer', size) -- cgit v1.2.3 From d174e1fa352dd5b8c479a71123ab25a7371dd5bd Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 22 May 2020 01:36:13 +0200 Subject: MAM: many changes - Fix color & nicks in one to one chats - Make poezio-facing functions "schedules" to avoid races on tab query state - Rename functions - Use a different behavior when filling a history gap and populating a new tab in a MUC --- poezio/mam.py | 244 +++++++++++++++++++++++++++++------------------- poezio/tabs/basetabs.py | 4 +- poezio/tabs/muctab.py | 2 +- 3 files changed, 149 insertions(+), 101 deletions(-) diff --git a/poezio/mam.py b/poezio/mam.py index 50dad4a3..05275975 100644 --- a/poezio/mam.py +++ b/poezio/mam.py @@ -6,34 +6,43 @@ XEP-0313: Message Archive Management(MAM). """ +import asyncio +import logging import random from datetime import datetime, timedelta, timezone from hashlib import md5 -from typing import Optional, Callable +from typing import ( + AsyncIterable, + Callable, + Dict, + List, + Optional, +) -from slixmpp import JID +from slixmpp import JID, Message as SMessage from slixmpp.exceptions import IqError, IqTimeout from poezio.theming import get_theme from poezio import tabs from poezio import xhtml, colors from poezio.config import config -from poezio.text_buffer import TextBuffer -from poezio.ui.types import Message +from poezio.text_buffer import TextBuffer, HistoryGap +from poezio.ui.types import BaseMessage, Message +log = logging.getLogger(__name__) + class DiscoInfoException(Exception): pass class MAMQueryException(Exception): pass class NoMAMSupportException(Exception): pass -def add_line( - tab, - text_buffer: TextBuffer, +def make_line( + tab: tabs.Tab, text: str, time: datetime, nick: str, - top: bool, - ) -> None: + identifier: str = '', + ) -> Message: """Adds a textual entry in the TextBuffer""" # Convert to local timezone @@ -61,39 +70,40 @@ def add_line( color = xhtml.colors.get(color) color = (color, -1) else: - nick = nick.split('/')[0] - color = get_theme().COLOR_OWN_NICK - text_buffer.add_message( - Message( - txt=text, - time=time, - nickname=nick, - nick_color=color, - history=True, - user=None, - top=top, - ) + if nick.split('/')[0] == tab.core.xmpp.boundjid.bare: + color = get_theme().COLOR_OWN_NICK + else: + color = get_theme().COLOR_REMOTE_USER + nick = tab.get_nick() + return Message( + txt=text, + identifier=identifier, + time=time, + nickname=nick, + nick_color=color, + history=True, + user=None, ) -async def query( +async def get_mam_iterator( core, groupchat: bool, remote_jid: JID, amount: int, - reverse: bool, + reverse: bool = True, start: Optional[datetime] = None, end: Optional[datetime] = None, before: Optional[str] = None, - callback: Optional[Callable] = None, - ) -> None: + ) -> AsyncIterable[Message]: + """Get an async iterator for this mam query""" try: query_jid = remote_jid if groupchat else JID(core.xmpp.boundjid.bare) iq = await core.xmpp.plugin['xep_0030'].get_info(jid=query_jid) except (IqError, IqTimeout): - raise DiscoInfoException + raise DiscoInfoException() if 'urn:xmpp:mam:2' not in iq['disco_info'].get_features(): - raise NoMAMSupportException + raise NoMAMSupportException() args = { 'iterator': True, @@ -105,64 +115,66 @@ async def query( else: args['with_jid'] = remote_jid - args['rsm'] = {'max': amount} - if reverse: - if before is not None: - args['rsm']['before'] = before - else: - args['end'] = end - else: - args['rsm']['start'] = start - if before is not None: - args['rsm']['end'] = end - try: - results = core.xmpp['xep_0313'].retrieve(**args) - except (IqError, IqTimeout): - raise MAMQueryException - if callback is not None: - callback(results) + if amount > 0: + args['rsm'] = {'max': amount} + args['start'] = start + args['end'] = end + return core.xmpp['xep_0313'].retrieve(**args) - return results +def _parse_message(msg: SMessage) -> Dict: + """Parse info inside a MAM forwarded message""" + forwarded = msg['mam_result']['forwarded'] + message = forwarded['stanza'] + return { + 'time': forwarded['delay']['stamp'], + 'nick': str(message['from']), + 'text': message['body'], + 'identifier': message['origin-id'] + } -async def add_messages_to_buffer(tab, top: bool, results, amount: int) -> bool: - """Prepends or appends messages to the tab text_buffer""" +async def retrieve_messages(tab: tabs.Tab, + results: AsyncIterable[SMessage], + amount: int = 100) -> List[Message]: + """Run the MAM query and put messages in order""" text_buffer = tab._text_buffer msg_count = 0 msgs = [] - async for rsm in results: - if top: + to_add = [] + last_stanza_id = tab.last_stanza_id + try: + async for rsm in results: for msg in rsm['mam']['results']: if msg['mam_result']['forwarded']['stanza'] \ - .xml.find('{%s}%s' % ('jabber:client', 'body')) is not None: - msgs.append(msg) - if msg_count == amount: - tab.core.refresh_window() - return False + .xml.find('{%s}%s' % ('jabber:client', 'body')) is not None: + args = _parse_message(msg) + msgs.append(make_line(tab, **args)) + for msg in reversed(msgs): + to_add.append(msg) msg_count += 1 - msgs.reverse() - for msg in msgs: - forwarded = msg['mam_result']['forwarded'] - timestamp = forwarded['delay']['stamp'] - message = forwarded['stanza'] - tab.last_stanza_id = msg['mam_result']['id'] - nick = str(message['from']) - add_line(tab, text_buffer, message['body'], timestamp, nick, top) - else: - for msg in rsm['mam']['results']: - forwarded = msg['mam_result']['forwarded'] - timestamp = forwarded['delay']['stamp'] - message = forwarded['stanza'] - nick = str(message['from']) - add_line(tab, text_buffer, message['body'], timestamp, nick, top) - tab.core.refresh_window() - return False + if msg_count == amount: + to_add.reverse() + return to_add + msgs = [] + to_add.reverse() + return to_add + except (IqError, IqTimeout) as exc: + log.debug('Unable to complete MAM query: %s', exc, exc_info=True) + raise MAMQueryException('Query interrupted') -async def fetch_history(tab, end: Optional[datetime] = None, amount: Optional[int] = None): +async def fetch_history(tab: tabs.Tab, + start: Optional[datetime] = None, + end: Optional[datetime] = None, + amount: Optional[int] = None) -> None: remote_jid = tab.jid - before = tab.last_stanza_id + if not end: + for msg in tab._text_buffer.messages: + if isinstance(msg, Message): + end = msg.time + end -= timedelta(microseconds=1) + break if end is None: end = datetime.now() tzone = datetime.now().astimezone().tzinfo @@ -170,38 +182,74 @@ async def fetch_history(tab, end: Optional[datetime] = None, amount: Optional[in end = end.replace(tzinfo=None) end = datetime.strftime(end, '%Y-%m-%dT%H:%M:%SZ') - if amount >= 100: - amount = 99 - - groupchat = isinstance(tab, tabs.MucTab) + if start is not None: + start = start.replace(tzinfo=tzone).astimezone(tz=timezone.utc) + start = start.replace(tzinfo=None) + start = datetime.strftime(start, '%Y-%m-%dT%H:%M:%SZ') - results = await query( - tab.core, - groupchat, - remote_jid, - amount, - reverse=True, + mam_iterator = await get_mam_iterator( + core=tab.core, + groupchat=isinstance(tab, tabs.MucTab), + remote_jid=remote_jid, + amount=amount, end=end, - before=before, + start=start, + reverse=True, ) - query_status = await add_messages_to_buffer(tab, True, results, amount) - tab.query_status = query_status + return await retrieve_messages(tab, mam_iterator, amount) +async def fill_missing_history(tab: tabs.Tab, gap: HistoryGap) -> None: + start = gap.last_timestamp_before_leave + end = gap.first_timestamp_after_join + if start: + start = start + timedelta(seconds=1) + if end: + end = end - timedelta(seconds=1) + try: + messages = await fetch_history(tab, start=start, end=end, amount=999) + tab._text_buffer.add_history_messages(messages, gap=gap) + tab.core.refresh_window() + except (NoMAMSupportException, MAMQueryException, DiscoInfoException): + return + finally: + tab.query_status = False -async def on_tab_open(tab) -> None: +async def on_new_tab_open(tab: tabs.Tab) -> None: + """Called when opening a new tab""" amount = 2 * tab.text_win.height end = datetime.now() - tab.query_status = True for message in tab._text_buffer.messages: - time = message.time - if time < end: - end = time - end = end + timedelta(seconds=-1) + if isinstance(message, Message) and message.time < end: + end = message.time + break + end = end - timedelta(microseconds=1) try: - await fetch_history(tab, end=end, amount=amount) + messages = await fetch_history(tab, end=end, amount=amount) + tab._text_buffer.add_history_messages(messages) except (NoMAMSupportException, MAMQueryException, DiscoInfoException): - tab.query_status = False return None + finally: + tab.query_status = False + + +def schedule_tab_open(tab: tabs.Tab) -> None: + """Set the query status and schedule a MAM query""" + tab.query_status = True + asyncio.ensure_future(on_tab_open(tab)) + + +async def on_tab_open(tab: tabs.Tab) -> None: + gap = tab._text_buffer.find_last_gap_muc() + if gap is not None: + await fill_missing_history(tab, gap) + else: + await on_new_tab_open(tab) + + +def schedule_scroll_up(tab: tabs.Tab) -> None: + """Set query status and schedule a scroll up""" + tab.query_status = True + asyncio.ensure_future(on_scroll_up(tab)) async def on_scroll_up(tab) -> None: @@ -212,22 +260,22 @@ async def on_scroll_up(tab) -> None: # join if not already available. total, pos, height = len(tw.built_lines), tw.pos, tw.height rest = (total - pos) // height - # Not resetting the state of query_status here, it is changed only after the - # query is complete (in fetch_history) - # This is done to stop message repetition, eg: if the user presses PageUp continuously. - tab.query_status = True if rest > 1: + tab.query_status = False return None try: # XXX: Do we want to fetch a possibly variable number of messages? # (InfoTab changes height depending on the type of messages, see # `information_buffer_popup_on`). - await fetch_history(tab, amount=height) + messages = await fetch_history(tab, amount=height) + tab._text_buffer.add_history_messages(messages) except NoMAMSupportException: tab.core.information('MAM not supported for %r' % tab.jid, 'Info') return None except (MAMQueryException, DiscoInfoException): tab.core.information('An error occured when fetching MAM for %r' % tab.jid, 'Error') return None + finally: + tab.query_status = False diff --git a/poezio/tabs/basetabs.py b/poezio/tabs/basetabs.py index fbb0c4cf..490363f0 100644 --- a/poezio/tabs/basetabs.py +++ b/poezio/tabs/basetabs.py @@ -32,7 +32,6 @@ from typing import ( ) from poezio import ( - mam, poopt, timed_events, xhtml, @@ -926,7 +925,8 @@ class ChatTab(Tab): def on_scroll_up(self): if not self.query_status: - asyncio.ensure_future(mam.on_scroll_up(tab=self)) + from poezio import mam + mam.schedule_scroll_up(tab=self) return self.text_win.scroll_up(self.text_win.height - 1) def on_scroll_down(self): diff --git a/poezio/tabs/muctab.py b/poezio/tabs/muctab.py index 7b0f8a42..edf80bb6 100644 --- a/poezio/tabs/muctab.py +++ b/poezio/tabs/muctab.py @@ -170,7 +170,7 @@ class MucTab(ChatTab): status=status.message, show=status.show, seconds=seconds) - asyncio.ensure_future(mam.on_tab_open(self)) + mam.schedule_tab_open(self) def leave_room(self, message: str): if self.joined: -- cgit v1.2.3 From 36c85a5df4fc4650956bc3aa9d4e8474268a06a9 Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 22 May 2020 01:55:32 +0200 Subject: Add an "end of archive" message type --- poezio/mam.py | 13 ++++++++++++- poezio/ui/types.py | 5 +++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/poezio/mam.py b/poezio/mam.py index 05275975..6882ef7a 100644 --- a/poezio/mam.py +++ b/poezio/mam.py @@ -26,7 +26,11 @@ from poezio import tabs from poezio import xhtml, colors from poezio.config import config from poezio.text_buffer import TextBuffer, HistoryGap -from poezio.ui.types import BaseMessage, Message +from poezio.ui.types import ( + BaseMessage, + EndOfArchive, + Message, +) log = logging.getLogger(__name__) @@ -270,6 +274,13 @@ async def on_scroll_up(tab) -> None: # (InfoTab changes height depending on the type of messages, see # `information_buffer_popup_on`). messages = await fetch_history(tab, amount=height) + if tab._text_buffer.messages: + last_message = tab._text_buffer.messages[0] + else: + last_message = None + if not messages and not isinstance(last_message, EndOfArchive): + time = tab._text_buffer.messages[0].time + messages = [EndOfArchive('End of archive reached', time=time)] tab._text_buffer.add_history_messages(messages) except NoMAMSupportException: tab.core.information('MAM not supported for %r' % tab.jid, 'Info') diff --git a/poezio/ui/types.py b/poezio/ui/types.py index 34924112..15117275 100644 --- a/poezio/ui/types.py +++ b/poezio/ui/types.py @@ -12,6 +12,7 @@ from poezio.ui.consts import ( ) + class BaseMessage: __slots__ = ('txt', 'time', 'identifier') @@ -27,6 +28,10 @@ class BaseMessage: return SHORT_FORMAT_LENGTH + 1 +class EndOfArchive(BaseMessage): + """Marker added to a buffer when we reach the end of a MAM archive""" + + class InfoMessage(BaseMessage): def __init__(self, txt: str, identifier: str = '', time: Optional[datetime] = None): txt = ('\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT)) + txt -- cgit v1.2.3 From d3655c4c3520f1a86af5ccf7b816076ae1d18312 Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 22 May 2020 01:58:03 +0200 Subject: Fix some remaining refresh issues --- poezio/mam.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/poezio/mam.py b/poezio/mam.py index 6882ef7a..edd1e462 100644 --- a/poezio/mam.py +++ b/poezio/mam.py @@ -212,7 +212,8 @@ async def fill_missing_history(tab: tabs.Tab, gap: HistoryGap) -> None: try: messages = await fetch_history(tab, start=start, end=end, amount=999) tab._text_buffer.add_history_messages(messages, gap=gap) - tab.core.refresh_window() + if messages: + tab.core.refresh_window() except (NoMAMSupportException, MAMQueryException, DiscoInfoException): return finally: @@ -230,6 +231,8 @@ async def on_new_tab_open(tab: tabs.Tab) -> None: try: messages = await fetch_history(tab, end=end, amount=amount) tab._text_buffer.add_history_messages(messages) + if messages: + tab.core.refresh_window() except (NoMAMSupportException, MAMQueryException, DiscoInfoException): return None finally: @@ -282,6 +285,8 @@ async def on_scroll_up(tab) -> None: time = tab._text_buffer.messages[0].time messages = [EndOfArchive('End of archive reached', time=time)] tab._text_buffer.add_history_messages(messages) + if messages: + tab.core.refresh_window() except NoMAMSupportException: tab.core.information('MAM not supported for %r' % tab.jid, 'Info') return None -- cgit v1.2.3 From 29eef159d50c836fbd7a27770775d63700dc7f19 Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 22 May 2020 17:09:17 +0200 Subject: Fix some edge cases of MAM history fetch - Wait until we receive our own MUC presence to fetch history - Fix /reconnect weirdness --- poezio/mam.py | 6 +++--- poezio/tabs/muctab.py | 2 +- poezio/text_buffer.py | 52 +++++++++++++++++++++++++++++++++++------------- test/test_text_buffer.py | 41 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 18 deletions(-) diff --git a/poezio/mam.py b/poezio/mam.py index edd1e462..ee5b1be8 100644 --- a/poezio/mam.py +++ b/poezio/mam.py @@ -247,10 +247,10 @@ def schedule_tab_open(tab: tabs.Tab) -> None: async def on_tab_open(tab: tabs.Tab) -> None: gap = tab._text_buffer.find_last_gap_muc() - if gap is not None: - await fill_missing_history(tab, gap) - else: + if gap is None or not gap.leave_message: await on_new_tab_open(tab) + else: + await fill_missing_history(tab, gap) def schedule_scroll_up(tab: tabs.Tab) -> None: diff --git a/poezio/tabs/muctab.py b/poezio/tabs/muctab.py index edf80bb6..0cb3d859 100644 --- a/poezio/tabs/muctab.py +++ b/poezio/tabs/muctab.py @@ -170,7 +170,6 @@ class MucTab(ChatTab): status=status.message, show=status.show, seconds=seconds) - mam.schedule_tab_open(self) def leave_room(self, message: str): if self.joined: @@ -601,6 +600,7 @@ class MucTab(ChatTab): }, ), typ=0) + mam.schedule_tab_open(self) def handle_presence_joined(self, presence: Presence, status_codes) -> None: """ diff --git a/poezio/text_buffer.py b/poezio/text_buffer.py index 03ad2f1b..f5886ae0 100644 --- a/poezio/text_buffer.py +++ b/poezio/text_buffer.py @@ -44,8 +44,8 @@ class AckError(Exception): @dataclass class HistoryGap: """Class representing a period of non-presence inside a MUC""" - leave_message: Optional[MucOwnLeaveMessage] - join_message: Optional[MucOwnJoinMessage] + leave_message: Optional[BaseMessage] + join_message: Optional[BaseMessage] last_timestamp_before_leave: Optional[datetime] first_timestamp_after_join: Optional[datetime] @@ -78,33 +78,57 @@ class TextBuffer: leave, join = None, None for i, item in enumerate(reversed(self.messages)): if isinstance(item, MucOwnLeaveMessage): - leave = (i, item) + leave = (len(self.messages) - i - 1, item) + break + elif join and isinstance(item, MucOwnJoinMessage): + leave = (len(self.messages) - i - 1, item) break if isinstance(item, MucOwnJoinMessage): - join = (i, item) - if join and leave: # Skip if we find a message in the interval - real_leave = len(self.messages) - leave[0] - 1 - real_join = len(self.messages) - join[0] - 1 - for i in range(real_leave, real_join, 1): + join = (len(self.messages) - i - 1, item) + + last_timestamp = None + first_timestamp = datetime.now() + + # Identify the special case when we got disconnected from a chatroom + # without receiving or sending the relevant presence, therefore only + # having two joins with no leave, and messages in the middle. + if leave and isinstance(leave[1], MucOwnJoinMessage): + for i in range(join[0] - 1, leave[0], - 1): + if isinstance(self.messages[i], Message): + leave = ( + i, + self.messages[i] + ) + last_timestamp = self.messages[i].time + break + # If we have a normal gap but messages inbetween, it probably + # already has history, so abort there without returning it. + if join and leave: + for i in range(leave[0] + 1, join[0], 1): if isinstance(self.messages[i], Message): return None elif not (join or leave): return None + + # If a leave message is found, get the last Message timestamp + # before it. if leave is None: - last_timestamp, leave_msg = None, None - else: - last_timestamp = None + leave_msg = None + elif last_timestamp is None: leave_msg = leave[1] - for i in range(len(self.messages) - leave[0] - 1, 0, -1): + for i in range(leave[0], 0, -1): if isinstance(self.messages[i], Message): last_timestamp = self.messages[i].time break - first_timestamp = datetime.now() + else: + leave_msg = leave[1] + # If a join message is found, get the first Message timestamp + # after it, or the current time. if join is None: join_msg = None else: join_msg = join[1] - for i in range(len(self.messages) - join[0], len(self.messages)): + for i in range(join[0], len(self.messages)): msg = self.messages[i] if isinstance(msg, Message) and msg.time < first_timestamp: first_timestamp = msg.time diff --git a/test/test_text_buffer.py b/test/test_text_buffer.py index 8e9829f4..65c6d9bf 100644 --- a/test/test_text_buffer.py +++ b/test/test_text_buffer.py @@ -35,6 +35,13 @@ def msgs_noleave(): msg4 = Message('4', 'f') return [join, msg3, msg4] +@fixture(scope='function') +def msgs_doublejoin(): + join = MucOwnJoinMessage('join') + msg1 = Message('1', 'd') + msg2 = Message('2', 'f') + join2 = MucOwnJoinMessage('join') + return [join, msg1, msg2, join2] def test_last_message(buf2048): msg = BaseMessage('toto') @@ -67,6 +74,24 @@ def test_find_gap(buf2048, msgs_noleave): assert gap.first_timestamp_after_join == msg3.time +def test_find_gap_doublejoin(buf2048, msgs_doublejoin): + for msg in msgs_doublejoin: + buf2048.add_message(msg) + gap = buf2048.find_last_gap_muc() + assert gap.leave_message == msgs_doublejoin[2] + assert gap.join_message == msgs_doublejoin[3] + + +def test_find_gap_doublejoin_no_msg(buf2048): + join1 = MucOwnJoinMessage('join') + join2 = MucOwnJoinMessage('join') + for msg in [join1, join2]: + buf2048.add_message(msg) + gap = buf2048.find_last_gap_muc() + assert gap.leave_message is join1 + assert gap.join_message is join2 + + def test_find_gap_already_filled(buf2048): msg1 = Message('1', 'q') msg2 = Message('2', 's') @@ -115,6 +140,22 @@ def test_get_gap_index(buf2048): assert buf2048.get_gap_index(gap) == 3 +def test_get_gap_index_doublejoin(buf2048, msgs_doublejoin): + for msg in msgs_doublejoin: + buf2048.add_message(msg) + gap = buf2048.find_last_gap_muc() + assert buf2048.get_gap_index(gap) == 3 + + +def test_get_gap_index_doublejoin_no_msg(buf2048): + join1 = MucOwnJoinMessage('join') + join2 = MucOwnJoinMessage('join') + for msg in [join1, join2]: + buf2048.add_message(msg) + gap = buf2048.find_last_gap_muc() + assert buf2048.get_gap_index(gap) == 1 + + def test_get_gap_index_nojoin(buf2048, msgs_nojoin): for msg in msgs_nojoin: buf2048.add_message(msg) -- cgit v1.2.3 From 4210f5c776dab2439cecb1e19dc3997109cab400 Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 22 May 2020 17:13:11 +0200 Subject: Convert all datetimes to UTC when doing comparisons --- poezio/common.py | 17 ++++++++++++++++- poezio/mam.py | 11 ++++------- poezio/tabs/muctab.py | 4 ++-- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/poezio/common.py b/poezio/common.py index 7cddc306..98870dda 100644 --- a/poezio/common.py +++ b/poezio/common.py @@ -8,7 +8,11 @@ Various useful functions. """ -from datetime import datetime, timedelta +from datetime import ( + datetime, + timedelta, + timezone, +) from pathlib import Path from typing import Dict, List, Optional, Tuple, Union @@ -488,3 +492,14 @@ def unique_prefix_of(a: str, b: str) -> str: return a[:i+1] # both are equal, return a return a + + +def to_utc(time: datetime) -> datetime: + """Convert a datetime-aware time zone into raw UTC""" + tzone = datetime.now().astimezone().tzinfo + if time.tzinfo is not None: # Convert to UTC + time = time.astimezone(tz=timezone.utc) + else: # Assume local tz, convert to URC + time = time.replace(tzinfo=tzone).astimezone(tz=timezone.utc) + # Return an offset-naive datetime + return time.replace(tzinfo=None) diff --git a/poezio/mam.py b/poezio/mam.py index ee5b1be8..4171de5e 100644 --- a/poezio/mam.py +++ b/poezio/mam.py @@ -25,6 +25,7 @@ from poezio.theming import get_theme from poezio import tabs from poezio import xhtml, colors from poezio.config import config +from poezio.common import to_utc from poezio.text_buffer import TextBuffer, HistoryGap from poezio.ui.types import ( BaseMessage, @@ -89,7 +90,6 @@ def make_line( user=None, ) - async def get_mam_iterator( core, groupchat: bool, @@ -181,14 +181,11 @@ async def fetch_history(tab: tabs.Tab, break if end is None: end = datetime.now() - tzone = datetime.now().astimezone().tzinfo - end = end.replace(tzinfo=tzone).astimezone(tz=timezone.utc) - end = end.replace(tzinfo=None) + end = to_utc(end) end = datetime.strftime(end, '%Y-%m-%dT%H:%M:%SZ') if start is not None: - start = start.replace(tzinfo=tzone).astimezone(tz=timezone.utc) - start = start.replace(tzinfo=None) + start = to_utc(start) start = datetime.strftime(start, '%Y-%m-%dT%H:%M:%SZ') mam_iterator = await get_mam_iterator( @@ -224,7 +221,7 @@ async def on_new_tab_open(tab: tabs.Tab) -> None: amount = 2 * tab.text_win.height end = datetime.now() for message in tab._text_buffer.messages: - if isinstance(message, Message) and message.time < end: + if isinstance(message, Message) and to_utc(message.time) < to_utc(end): end = message.time break end = end - timedelta(microseconds=1) diff --git a/poezio/tabs/muctab.py b/poezio/tabs/muctab.py index 0cb3d859..5939db31 100644 --- a/poezio/tabs/muctab.py +++ b/poezio/tabs/muctab.py @@ -31,7 +31,7 @@ from poezio import multiuserchat as muc from poezio import timed_events from poezio import windows from poezio import xhtml -from poezio.common import safeJID +from poezio.common import safeJID, to_utc from poezio.config import config from poezio.core.structs import Command from poezio.decorators import refresh_wrapper, command_args_parser @@ -158,7 +158,7 @@ class MucTab(ChatTab): """ status = self.core.get_status() if self.last_connection: - delta = datetime.now() - self.last_connection + delta = to_utc(datetime.now()) - to_utc(self.last_connection) seconds = delta.seconds + delta.days * 24 * 3600 else: seconds = self._text_buffer.find_last_message() -- cgit v1.2.3 From d3863fad18cd7e6b3e764b663724718401c84bde Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 22 May 2020 18:01:32 +0200 Subject: ChatTab: make set text_win in constructor (typing) --- poezio/tabs/basetabs.py | 3 ++- poezio/tabs/conversationtab.py | 2 -- poezio/tabs/muctab.py | 2 -- poezio/tabs/privatetab.py | 2 -- 4 files changed, 2 insertions(+), 7 deletions(-) diff --git a/poezio/tabs/basetabs.py b/poezio/tabs/basetabs.py index 490363f0..df2387dd 100644 --- a/poezio/tabs/basetabs.py +++ b/poezio/tabs/basetabs.py @@ -495,9 +495,10 @@ class ChatTab(Tab): self.last_stanza_id = None self._name = jid.full # type: Optional[str] - self.text_win = None + self.text_win = windows.TextWin() self.directed_presence = None self._text_buffer = TextBuffer() + self._text_buffer.add_window(self.text_win) self.chatstate = None # can be "active", "composing", "paused", "gone", "inactive" # We keep a reference of the event that will set our chatstate to "paused", so that # we can delete it or change it if we need to diff --git a/poezio/tabs/conversationtab.py b/poezio/tabs/conversationtab.py index 70005f0f..5950e4cb 100644 --- a/poezio/tabs/conversationtab.py +++ b/poezio/tabs/conversationtab.py @@ -48,8 +48,6 @@ class ConversationTab(OneToOneTab): self.nick = None self.nick_sent = False self.state = 'normal' - self.text_win = windows.TextWin() - self._text_buffer.add_window(self.text_win) self.upper_bar = windows.ConversationStatusMessageWin() self.input = windows.MessageInput() # keys diff --git a/poezio/tabs/muctab.py b/poezio/tabs/muctab.py index 5939db31..751509a7 100644 --- a/poezio/tabs/muctab.py +++ b/poezio/tabs/muctab.py @@ -91,8 +91,6 @@ class MucTab(ChatTab): self.self_ping_event = None # UI stuff self.topic_win = windows.Topic() - self.text_win = windows.TextWin() - self._text_buffer.add_window(self.text_win) self.v_separator = windows.VerticalSeparator() self.user_win = windows.UserList() self.info_header = windows.MucInfoWin() diff --git a/poezio/tabs/privatetab.py b/poezio/tabs/privatetab.py index b43294a1..cd2123f3 100644 --- a/poezio/tabs/privatetab.py +++ b/poezio/tabs/privatetab.py @@ -46,8 +46,6 @@ class PrivateTab(OneToOneTab): def __init__(self, core, jid, nick): OneToOneTab.__init__(self, core, jid) self.own_nick = nick - self.text_win = windows.TextWin() - self._text_buffer.add_window(self.text_win) self.info_header = windows.PrivateInfoWin() self.input = windows.MessageInput() # keys -- cgit v1.2.3 From f282b14e8d8314b3ec95268571093c6e2295458c Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 22 May 2020 18:07:54 +0200 Subject: Fix typing in mam.py --- poezio/mam.py | 44 +++++++++++++++++++++++--------------------- poezio/text_buffer.py | 2 +- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/poezio/mam.py b/poezio/mam.py index 4171de5e..49ccf017 100644 --- a/poezio/mam.py +++ b/poezio/mam.py @@ -12,6 +12,7 @@ import random from datetime import datetime, timedelta, timezone from hashlib import md5 from typing import ( + Any, AsyncIterable, Callable, Dict, @@ -42,7 +43,7 @@ class NoMAMSupportException(Exception): pass def make_line( - tab: tabs.Tab, + tab: tabs.ChatTab, text: str, time: datetime, nick: str, @@ -96,8 +97,8 @@ async def get_mam_iterator( remote_jid: JID, amount: int, reverse: bool = True, - start: Optional[datetime] = None, - end: Optional[datetime] = None, + start: Optional[str] = None, + end: Optional[str] = None, before: Optional[str] = None, ) -> AsyncIterable[Message]: """Get an async iterator for this mam query""" @@ -112,7 +113,7 @@ async def get_mam_iterator( args = { 'iterator': True, 'reverse': reverse, - } + } # type: Dict[str, Any] if groupchat: args['jid'] = remote_jid @@ -138,9 +139,9 @@ def _parse_message(msg: SMessage) -> Dict: } -async def retrieve_messages(tab: tabs.Tab, +async def retrieve_messages(tab: tabs.ChatTab, results: AsyncIterable[SMessage], - amount: int = 100) -> List[Message]: + amount: int = 100) -> List[BaseMessage]: """Run the MAM query and put messages in order""" text_buffer = tab._text_buffer msg_count = 0 @@ -168,10 +169,10 @@ async def retrieve_messages(tab: tabs.Tab, raise MAMQueryException('Query interrupted') -async def fetch_history(tab: tabs.Tab, +async def fetch_history(tab: tabs.ChatTab, start: Optional[datetime] = None, end: Optional[datetime] = None, - amount: Optional[int] = None) -> None: + amount: int = 100) -> List[BaseMessage]: remote_jid = tab.jid if not end: for msg in tab._text_buffer.messages: @@ -182,24 +183,25 @@ async def fetch_history(tab: tabs.Tab, if end is None: end = datetime.now() end = to_utc(end) - end = datetime.strftime(end, '%Y-%m-%dT%H:%M:%SZ') + end_str = datetime.strftime(end, '%Y-%m-%dT%H:%M:%SZ') + start_str = None if start is not None: start = to_utc(start) - start = datetime.strftime(start, '%Y-%m-%dT%H:%M:%SZ') + start_str = datetime.strftime(start, '%Y-%m-%dT%H:%M:%SZ') mam_iterator = await get_mam_iterator( core=tab.core, groupchat=isinstance(tab, tabs.MucTab), remote_jid=remote_jid, amount=amount, - end=end, - start=start, + end=end_str, + start=start_str, reverse=True, ) return await retrieve_messages(tab, mam_iterator, amount) -async def fill_missing_history(tab: tabs.Tab, gap: HistoryGap) -> None: +async def fill_missing_history(tab: tabs.ChatTab, gap: HistoryGap) -> None: start = gap.last_timestamp_before_leave end = gap.first_timestamp_after_join if start: @@ -216,7 +218,7 @@ async def fill_missing_history(tab: tabs.Tab, gap: HistoryGap) -> None: finally: tab.query_status = False -async def on_new_tab_open(tab: tabs.Tab) -> None: +async def on_new_tab_open(tab: tabs.ChatTab) -> None: """Called when opening a new tab""" amount = 2 * tab.text_win.height end = datetime.now() @@ -236,13 +238,13 @@ async def on_new_tab_open(tab: tabs.Tab) -> None: tab.query_status = False -def schedule_tab_open(tab: tabs.Tab) -> None: +def schedule_tab_open(tab: tabs.ChatTab) -> None: """Set the query status and schedule a MAM query""" tab.query_status = True asyncio.ensure_future(on_tab_open(tab)) -async def on_tab_open(tab: tabs.Tab) -> None: +async def on_tab_open(tab: tabs.ChatTab) -> None: gap = tab._text_buffer.find_last_gap_muc() if gap is None or not gap.leave_message: await on_new_tab_open(tab) @@ -250,13 +252,13 @@ async def on_tab_open(tab: tabs.Tab) -> None: await fill_missing_history(tab, gap) -def schedule_scroll_up(tab: tabs.Tab) -> None: +def schedule_scroll_up(tab: tabs.ChatTab) -> None: """Set query status and schedule a scroll up""" tab.query_status = True asyncio.ensure_future(on_scroll_up(tab)) -async def on_scroll_up(tab) -> None: +async def on_scroll_up(tab: tabs.ChatTab) -> None: tw = tab.text_win # If position in the tab is < two screen pages, then fetch MAM, so that we @@ -274,11 +276,11 @@ async def on_scroll_up(tab) -> None: # (InfoTab changes height depending on the type of messages, see # `information_buffer_popup_on`). messages = await fetch_history(tab, amount=height) + last_message_exists = False if tab._text_buffer.messages: last_message = tab._text_buffer.messages[0] - else: - last_message = None - if not messages and not isinstance(last_message, EndOfArchive): + last_message_exists = True + if not messages and last_message_exists and not isinstance(last_message, EndOfArchive): time = tab._text_buffer.messages[0].time messages = [EndOfArchive('End of archive reached', time=time)] tab._text_buffer.add_history_messages(messages) diff --git a/poezio/text_buffer.py b/poezio/text_buffer.py index f5886ae0..c1fae54d 100644 --- a/poezio/text_buffer.py +++ b/poezio/text_buffer.py @@ -149,7 +149,7 @@ class TextBuffer: return i + 1 return None - def add_history_messages(self, messages: List[Message], gap: Optional[HistoryGap] = None) -> None: + def add_history_messages(self, messages: List[BaseMessage], gap: Optional[HistoryGap] = None) -> None: """Insert history messages at their correct place """ index = 0 if gap is not None: -- cgit v1.2.3 From 8e5cc9265dd82cab25ecff38921e49b738e6fd3c Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 22 May 2020 18:16:11 +0200 Subject: Fix types in text_buffer --- poezio/text_buffer.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/poezio/text_buffer.py b/poezio/text_buffer.py index c1fae54d..ff853a67 100644 --- a/poezio/text_buffer.py +++ b/poezio/text_buffer.py @@ -12,6 +12,7 @@ import logging log = logging.getLogger(__name__) from typing import ( + cast, Dict, List, Optional, @@ -59,7 +60,7 @@ class TextBuffer: def __init__(self, messages_nb_limit: Optional[int] = None) -> None: if messages_nb_limit is None: - messages_nb_limit = config.get('max_messages_in_memory') + messages_nb_limit = cast(int, config.get('max_messages_in_memory')) self._messages_nb_limit = messages_nb_limit # type: int # Message objects self.messages = [] # type: List[BaseMessage] @@ -75,7 +76,8 @@ class TextBuffer: def find_last_gap_muc(self) -> Optional[HistoryGap]: """Find the last known history gap contained in buffer""" - leave, join = None, None + leave = None # type:Optional[Tuple[int, BaseMessage]] + join = None # type:Optional[Tuple[int, BaseMessage]] for i, item in enumerate(reversed(self.messages)): if isinstance(item, MucOwnLeaveMessage): leave = (len(self.messages) - i - 1, item) @@ -92,7 +94,7 @@ class TextBuffer: # Identify the special case when we got disconnected from a chatroom # without receiving or sending the relevant presence, therefore only # having two joins with no leave, and messages in the middle. - if leave and isinstance(leave[1], MucOwnJoinMessage): + if leave and join and isinstance(leave[1], MucOwnJoinMessage): for i in range(join[0] - 1, leave[0], - 1): if isinstance(self.messages[i], Message): leave = ( @@ -152,10 +154,12 @@ class TextBuffer: def add_history_messages(self, messages: List[BaseMessage], gap: Optional[HistoryGap] = None) -> None: """Insert history messages at their correct place """ index = 0 + new_index = None if gap is not None: - index = self.get_gap_index(gap) - if index is None: # Not sure what happened, abort - return + new_index = self.get_gap_index(gap) + if new_index is None: # Not sure what happened, abort + return + index = new_index for message in messages: self.messages.insert(index, message) index += 1 @@ -177,8 +181,8 @@ class TextBuffer: self.messages.pop(0) ret_val = 0 - show_timestamps = config.get('show_timestamps') - nick_size = config.get('max_nick_length') + show_timestamps = cast(bool, config.get('show_timestamps')) + nick_size = cast(int, config.get('max_nick_length')) for window in self._windows: # make the associated windows # build the lines from the new message nb = window.build_new_message( -- cgit v1.2.3 From faeab78c7e3c9f125cfbfe3dce0fb18c9b8649c4 Mon Sep 17 00:00:00 2001 From: mathieui Date: Fri, 22 May 2020 18:23:51 +0200 Subject: Remove remaining occurences of tab.last_stanza_id --- poezio/mam.py | 1 - poezio/tabs/basetabs.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/poezio/mam.py b/poezio/mam.py index 49ccf017..371b34dd 100644 --- a/poezio/mam.py +++ b/poezio/mam.py @@ -147,7 +147,6 @@ async def retrieve_messages(tab: tabs.ChatTab, msg_count = 0 msgs = [] to_add = [] - last_stanza_id = tab.last_stanza_id try: async for rsm in results: for msg in rsm['mam']['results']: diff --git a/poezio/tabs/basetabs.py b/poezio/tabs/basetabs.py index df2387dd..a42ee41b 100644 --- a/poezio/tabs/basetabs.py +++ b/poezio/tabs/basetabs.py @@ -492,8 +492,6 @@ class ChatTab(Tab): self._jid = jid #: Is the tab currently requesting MAM data? self.query_status = False - self.last_stanza_id = None - self._name = jid.full # type: Optional[str] self.text_win = windows.TextWin() self.directed_presence = None -- cgit v1.2.3