text_buffer.py 9.97 KB
Newer Older
1
2
"""
Define the TextBuffer class
3
4
5
6
7

A text buffer contains a list of intermediate representations of messages
(not xml stanzas, but neither the Lines used in windows.py.

Each text buffer can be linked to multiple windows, that will be rendered
Link Mauve's avatar
Link Mauve committed
8
independently by their TextWins.
9
10
"""

11
12
13
import logging
log = logging.getLogger(__name__)

Maxime Buquet's avatar
Maxime Buquet committed
14
from typing import Dict, Union, Optional, List, Tuple
15
from datetime import datetime
mathieui's avatar
mathieui committed
16
17
from poezio.config import config
from poezio.theming import get_theme, dump_tuple
18

mathieui's avatar
mathieui committed
19

20
21
class Message:
    __slots__ = ('txt', 'nick_color', 'time', 'str_time', 'nickname', 'user',
22
                 'identifier', 'top', 'highlight', 'me', 'old_message', 'revisions',
23
24
                 'jid', 'ack')

mathieui's avatar
mathieui committed
25
    def __init__(self,
26
27
28
29
30
31
32
                 txt: str,
                 time: Optional[datetime],
                 nickname: Optional[str],
                 nick_color: Optional[Tuple],
                 history: bool,
                 user: Optional[str],
                 identifier: Optional[str],
33
                 top: Optional[bool] = False,
34
35
                 str_time: Optional[str] = None,
                 highlight: bool = False,
36
                 old_message: Optional['Message'] = None,
37
38
39
                 revisions: int = 0,
                 jid: Optional[str] = None,
                 ack: int = 0) -> None:
40
41
42
43
        """
        Create a new Message object with parameters, check for /me messages,
        and delayed messages
        """
44
        time = time if time is not None else datetime.now()
45
46
47
48
49
50
        if txt.startswith('/me '):
            me = True
            txt = '\x19%s}%s\x19o' % (dump_tuple(get_theme().COLOR_ME_MESSAGE),
                                      txt[4:])
        else:
            me = False
51
        str_time = time.strftime("%H:%M:%S")
52
        if history:
mathieui's avatar
mathieui committed
53
54
55
            txt = txt.replace(
                '\x19o',
                '\x19o\x19%s}' % dump_tuple(get_theme().COLOR_LOG_MSG))
56
57
58
59
60
61
62
63
64
            str_time = time.strftime("%Y-%m-%d %H:%M:%S")

        self.txt = txt.replace('\t', '    ') + '\x19o'
        self.nick_color = nick_color
        self.time = time
        self.str_time = str_time
        self.nickname = nickname
        self.user = user
        self.identifier = identifier
65
        self.top = top
66
67
68
69
70
71
72
        self.highlight = highlight
        self.me = me
        self.old_message = old_message
        self.revisions = revisions
        self.jid = jid
        self.ack = ack

73
    def _other_elems(self) -> str:
74
        "Helper for the repr_message function"
75
        acc = []
76
77
78
79
        fields = list(self.__slots__)
        fields.remove('old_message')
        for field in fields:
            acc.append('%s=%s' % (field, repr(getattr(self, field))))
80
        return 'Message(%s, %s' % (', '.join(acc), 'old_message=')
81

82
    def __repr__(self) -> str:
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
        """
        repr() for the Message class, for debug purposes, since the default
        repr() is recursive, so it can stack overflow given too many revisions
        of a message
        """
        init = self._other_elems()
        acc = [init]
        next_message = self.old_message
        rev = 1
        while next_message is not None:
            acc.append(next_message._other_elems())
            next_message = next_message.old_message
            rev += 1
        acc.append('None')
        while rev:
            acc.append(')')
            rev -= 1
        return ''.join(acc)
101

mathieui's avatar
mathieui committed
102

mathieui's avatar
mathieui committed
103
104
class CorrectionError(Exception):
    pass
mathieui's avatar
mathieui committed
105

mathieui's avatar
mathieui committed
106

107
108
109
class AckError(Exception):
    pass

mathieui's avatar
mathieui committed
110

111
class TextBuffer:
112
113
    """
    This class just keep trace of messages, in a list with various
114
    information and attributes.
115
    """
mathieui's avatar
mathieui committed
116

117
    def __init__(self, messages_nb_limit: Optional[int] = None) -> None:
118
119

        if messages_nb_limit is None:
120
            messages_nb_limit = config.get('max_messages_in_memory')
121
        self._messages_nb_limit = messages_nb_limit  # type: int
122
        # Message objects
123
        self.messages = []  # type: List[Message]
124
        # COMPAT: Correction id -> Original message id.
Maxime Buquet's avatar
Maxime Buquet committed
125
        self.correction_ids = {}  # type: Dict[str, str]
126
        # we keep track of one or more windows
127
        # so we can pass the new messages to them, as they are added, so
128
        # they (the windows) can build the lines from the new message
129
        self._windows = []
130

131
    def add_window(self, win) -> None:
132
        self._windows.append(win)
133

134
    @property
135
    def last_message(self) -> Optional[Message]:
136
137
        return self.messages[-1] if self.messages else None

mathieui's avatar
mathieui committed
138
    def add_message(self,
139
140
141
142
143
144
145
                    txt: str,
                    time: Optional[datetime] = None,
                    nickname: Optional[str] = None,
                    nick_color: Optional[Tuple] = None,
                    history: bool = False,
                    user: Optional[str] = None,
                    highlight: bool = False,
146
                    top: Optional[bool] = False,
147
148
149
150
                    identifier: Optional[str] = None,
                    str_time: Optional[str] = None,
                    jid: Optional[str] = None,
                    ack: int = 0) -> int:
151
152
153
        """
        Create a message and add it to the text buffer
        """
mathieui's avatar
mathieui committed
154
155
156
157
158
159
160
161
        msg = Message(
            txt,
            time,
            nickname,
            nick_color,
            history,
            user,
            identifier,
162
            top,
mathieui's avatar
mathieui committed
163
164
165
166
            str_time=str_time,
            highlight=highlight,
            jid=jid,
            ack=ack)
167
        self.messages.append(msg)
168

169
        while len(self.messages) > self._messages_nb_limit:
170
            self.messages.pop(0)
171

172
        ret_val = 0
173
        show_timestamps = config.get('show_timestamps')
174
        nick_size = config.get('max_nick_length')
mathieui's avatar
mathieui committed
175
176
        for window in self._windows:  # make the associated windows
            # build the lines from the new message
Madhur Garg's avatar
Madhur Garg committed
177
178
179
180
181
182
183
184
185
            nb = window.build_new_message(
                msg,
                history=history,
                highlight=highlight,
                timestamp=show_timestamps,
                top=top,
                nick_size=nick_size)
            if ret_val == 0:
                ret_val = nb
186
            if window.pos != 0 and top is False:
Madhur Garg's avatar
Madhur Garg committed
187
                window.scroll_up(nb)
188

189
        return min(ret_val, 1)
190

191
    def _find_message(self, orig_id: str) -> Tuple[str, int]:
192
193
194
        """
        Find a message in the text buffer from its message id
        """
Maxime Buquet's avatar
Maxime Buquet committed
195
196
197
        # When looking for a message, ensure the id doesn't appear in a
        # message we've removed from our message list. If so return the index
        # of the corresponding id for the original message instead.
198
        orig_id = self.correction_ids.get(orig_id, orig_id)
Maxime Buquet's avatar
Maxime Buquet committed
199

mathieui's avatar
mathieui committed
200
        for i in range(len(self.messages) - 1, -1, -1):
201
            msg = self.messages[i]
202
203
204
            if msg.identifier == orig_id:
                return (orig_id, i)
        return (orig_id, -1)
205

206
    def ack_message(self, old_id: str, jid: str) -> Union[None, bool, Message]:
207
        """Mark a message as acked"""
208
        return self._edit_ack(1, old_id, jid)
209

Link Mauve's avatar
Link Mauve committed
210
211
    def nack_message(self, error: str, old_id: str,
                     jid: str) -> Union[None, bool, Message]:
212
        """Mark a message as errored"""
213
        return self._edit_ack(-1, old_id, jid, append=error)
214

215
    def _edit_ack(self, value: int, old_id: str, jid: str,
Link Mauve's avatar
Link Mauve committed
216
                  append: str = '') -> Union[None, bool, Message]:
217
        """
218
219
        Edit the ack status of a message, and optionally
        append some text.
220
        """
Maxime Buquet's avatar
Maxime Buquet committed
221
        _, i = self._find_message(old_id)
222
        if i == -1:
223
            return None
224
        msg = self.messages[i]
225
226
        if msg.ack == 1:  # Message was already acked
            return False
227
        if msg.jid != jid:
mathieui's avatar
mathieui committed
228
229
            raise AckError('Wrong JID for message id %s (was %s, expected %s)'
                           % (old_id, msg.jid, jid))
230

231
        msg.ack = value
232
        if append:
233
234
            msg.txt += append
        return msg
235

mathieui's avatar
mathieui committed
236
    def modify_message(self,
237
                       txt: str,
238
                       orig_id: str,
239
240
241
242
243
                       new_id: str,
                       highlight: bool = False,
                       time: Optional[datetime] = None,
                       user: Optional[str] = None,
                       jid: Optional[str] = None):
244
245
        """
        Correct a message in a text buffer.
Maxime Buquet's avatar
Maxime Buquet committed
246
247
248
249
250

        Version 1.1.0 of Last Message Correction (0308) added clarifications
        that break the way poezio handles corrections. Instead of linking
        corrections to the previous correction/message as we were doing, we
        are now required to link all corrections to the original messages.
251
252
        """

253
        orig_id, i = self._find_message(orig_id)
254
255

        if i == -1:
mathieui's avatar
mathieui committed
256
257
            log.debug(
                'Message %s not found in text_buffer, abort replacement.',
258
                orig_id)
259
260
261
262
263
264
            raise CorrectionError("nothing to replace")

        msg = self.messages[i]

        if msg.user and msg.user is not user:
            raise CorrectionError("Different users")
mathieui's avatar
mathieui committed
265
        elif len(msg.str_time) > 8:  # ugly
266
267
268
269
270
            raise CorrectionError("Delayed message")
        elif not msg.user and (msg.jid is None or jid is None):
            raise CorrectionError('Could not check the '
                                  'identity of the sender')
        elif not msg.user and msg.jid != jid:
mathieui's avatar
mathieui committed
271
272
            raise CorrectionError(
                'Messages %s and %s have not been '
273
                'sent by the same fullJID' % (orig_id, new_id))
274
275
276

        if not time:
            time = msg.time
Maxime Buquet's avatar
Maxime Buquet committed
277

278
        self.correction_ids[new_id] = orig_id
mathieui's avatar
mathieui committed
279
280
281
282
283
        message = Message(
            txt,
            time,
            msg.nickname,
            msg.nick_color,
284
            False,
mathieui's avatar
mathieui committed
285
            msg.user,
286
            orig_id,
mathieui's avatar
mathieui committed
287
288
289
290
            highlight=highlight,
            old_message=msg,
            revisions=msg.revisions + 1,
            jid=jid)
291
        self.messages[i] = message
292
        log.debug('Replacing message %s with %s.', orig_id, new_id)
293
        return message
294

295
    def del_window(self, win) -> None:
296
        self._windows.remove(win)
297
298

    def __del__(self):
299
300
        size = len(self.messages)
        log.debug('** Deleting %s messages from textbuffer', size)