]> asedeno.scripts.mit.edu Git - vt_decor.git/blob - vt_decor.py
602f496f61d43aa64300f9628cd377430b9365aa
[vt_decor.git] / vt_decor.py
1 # -*- encoding: utf-8 -*-
2 # Copyright © 2022 Alejandro R. Sedeño <asedeno@mit.edu>
3 # All rights reserved.
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions
7 # are met:
8 #
9 # 1. Redistributions of source code must retain the above copyright
10 # notice, this list of conditions and the following disclaimer.
11 #
12 # 2. Redistributions in binary form must reproduce the above
13 # copyright notice, this list of conditions and the following
14 # disclaimer in the documentation and/or other materials provided
15 # with the distribution.
16 #
17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
18 # CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
19 # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
20 # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
22 # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
24 # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26 # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
27 # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
28 # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
29 # SUCH DAMAGE.
30
31
32 import base64
33 import inspect
34 import ipaddress
35 import re
36 import socket
37 import textwrap
38 import time
39 import weakref
40
41 from .. import chunks, messages, roost, util, zcode
42
43 ## Config
44 # I'm not sure why I can't just use the standard form of
45 # _enabled = util.Configurable(...) here, but this works.
46
47 SETTINGS = {
48     'enabled': True,
49     'min_width': 100,
50     'time': 'receiveTime',
51     'show_seconds': False,
52     'show_addr': False,
53     'show_signatures': False,
54 }
55
56 HOSTNAMES = {}
57
58 def _gen_setter(key):
59     def setter(_context, value):
60         SETTINGS[key] = value
61     return setter
62
63 util.Configurable(
64     'user.vt_decor.enabled',
65     True,
66     action=_gen_setter('enabled'),
67     oneof=('True', 'False'),
68     validate=lambda val: isinstance(val, bool),
69     coerce=util.coerce_bool,
70     )
71
72 util.Configurable(
73     'user.vt_decor.time',
74     'receiveTime',
75     action=_gen_setter('time'),
76     oneof=('time', 'receiveTime'),
77     )
78
79 util.Configurable(
80     'user.vt_decor.show_seconds',
81     False,
82     action=_gen_setter('show_seconds'),
83     oneof=('True', 'False'),
84     validate=lambda val: isinstance(val, bool),
85     coerce=util.coerce_bool,
86     )
87
88 util.Configurable(
89     'user.vt_decor.show_addr',
90     False,
91     action=_gen_setter('show_addr'),
92     oneof=('True', 'False'),
93     validate=lambda val: isinstance(val, bool),
94     coerce=util.coerce_bool,
95     )
96
97 util.Configurable(
98     'user.vt_decor.show_signatures',
99     False,
100     action=_gen_setter('show_signatures'),
101     oneof=('True', 'False'),
102     validate=lambda val: isinstance(val, bool),
103     coerce=util.coerce_bool,
104     )
105
106
107 ## Utils
108
109 UI = lambda: None
110
111 def _get_width():
112     global UI
113     try:
114         return UI().maxx
115     except AttributeError:
116         pass
117
118     frame = inspect.currentframe()
119     try:
120         while True:
121             frame = frame.f_back
122             try:
123                 ui = frame.f_locals['self'].ui
124                 UI = weakref.ref(ui)
125                 return ui.maxx
126             except (KeyError, AttributeError):
127                 pass
128     finally:
129         del frame
130
131
132 ## The fun part
133
134 MSG_NO_WRAP_PATTERNS = [re.compile(x, re.MULTILINE) for x in (
135     r'^(?: |>)',
136     r'\t',
137     r'[^.?!,]  ',
138 )]
139
140 MSG_LIST_WRAP_PATTERN = re.compile(r'(^\s*[-*]\s+)', re.MULTILINE)
141
142 REALM_MAP = {
143     'ANDREW.CMU.EDU': 'AN',
144     'CS.CMU.EDU': 'CS',
145     'IASTATE.EDU': 'IA',
146 }
147
148 class RoostVTDecor(roost.RoostMessage.Decor):
149
150
151     @classmethod
152     def decorate(cls, msg, decoration):
153         try:
154             if SETTINGS['enabled'] and _get_width() >= SETTINGS['min_width']:
155                 return cls.vt_decorate(msg, decoration)
156         except Exception:
157             pass
158         return super().decorate(msg, decoration)
159
160     @staticmethod
161     def rewrap_p(msg):
162         force_wrap_tuples = {
163             ('moira', 'incremental'),
164             ('scripts', 'nagios.multivalue-key.mysql-s'),
165             ('scripts', 'nagios.unique-key.mysql-s'),
166         }
167
168         msg_tuples = {
169             (msg.data['classKey'], '*'),
170             (msg.data['classKey'], msg.data['instanceKey']),
171         }
172
173         return (bool(force_wrap_tuples & msg_tuples) or
174                 not any(x.search(msg.body) for x in MSG_NO_WRAP_PATTERNS))
175
176     @staticmethod
177     def list_rewrap_p(msg):
178         return len(MSG_LIST_WRAP_PATTERN.findall(msg.body)) >= 2
179
180     @classmethod
181     def partial_rewrap(cls, body, width, indent):
182         try:
183             ret = ''
184             bindent = ''
185             for m in MSG_LIST_WRAP_PATTERN.split(body):
186                 if not m:
187                     continue
188                 if MSG_LIST_WRAP_PATTERN.match(m):
189                     # Bullet
190                     if ret:
191                         ret += indent
192                     ret += m.lstrip('\n')
193                     bindent = ' ' * util.glyphwidth(m)
194                 else:
195                     # Content
196                     # collapse whitespace and wrap.
197                     if '\n\n' in m:
198                         # list appears to end and have further content.
199                         # deal with final (?) bullet...
200                         bm, rest = m.split('\n\n', 1)
201                         ret += textwrap.fill(re.sub(r'\s+', ' ', bm), width,
202                                              initial_indent=indent+bindent,
203                                              subsequent_indent=indent+bindent,
204                                              break_long_words=False,
205                                              break_on_hyphens=False).lstrip()
206
207                         # ... and then indent and wrap following content normally.
208                         ret += '\n\n'
209                         ret += textwrap.fill(re.sub(r'\s+', ' ', rest), width,
210                                              initial_indent=indent,
211                                              subsequent_indent=indent,
212                                              break_long_words=False,
213                                              break_on_hyphens=False)
214                         ret += '\n'
215                     else:
216                         ret += textwrap.fill(re.sub(r'\s+', ' ', m), width,
217                                              initial_indent=indent+bindent,
218                                              subsequent_indent=indent+bindent,
219                                              break_long_words=False,
220                                              break_on_hyphens=False).lstrip()
221                         ret += '\n'
222
223             return ret.rstrip()
224         except Exception as e:
225             return str(e)
226
227
228     @classmethod
229     def vt_decorate(cls, msg, decoration):
230         width = _get_width()
231
232         tags = cls.decotags(decoration)
233
234         realm = msg.backend.realm
235         sender = msg.data['sender']
236         recipient = msg.data['recipient']
237         if msg.data['isPersonal'] and msg.data['isOutgoing']:
238             sender = f'➤{recipient}'
239         if sender.endswith(realm):
240             sender = sender[:sender.index('@')]
241         klass = msg.data['class'] if msg.data['classKey'] != 'message' else ''
242         inst = msg.data['instance'] or "''"
243         if klass and inst.lower() == 'personal':
244             dest = klass
245         else:
246             dest = f'{klass}[{inst}]'
247         auth = '+' if msg.data['auth'] else '-'
248         if not msg.data['auth'] and  msg.data['opcode'] == 'mattermost':
249             auth = '¤'
250         t = time.strftime(
251             '%H:%M:%S' if SETTINGS['show_seconds'] else '%H:%M',
252             time.localtime(msg.data[SETTINGS['time']] / 1000))
253
254         if recipient.startswith('@'):
255             mrealm = REALM_MAP.get(recipient[1:], '??')
256             dest = f'{mrealm} {dest}'
257
258         dest_width = 18 - (util.glyphwidth(dest) - len(dest))
259
260         prefix = f'{sender:10.10} {t} {dest:{dest_width}.{dest_width}} {auth} '
261         indent = ' ' * util.glyphwidth(prefix)
262
263         body = msg.body.rstrip()
264         if cls.list_rewrap_p(msg):
265             body = cls.partial_rewrap(body, width, indent)
266         elif cls.rewrap_p(msg):
267             body = textwrap.fill(body, width,
268                                  initial_indent=indent,
269                                  subsequent_indent=indent,
270                                  break_long_words=False,
271                                  break_on_hyphens=False).lstrip()
272         else:
273             body = textwrap.indent(body, indent).lstrip()
274
275         # body = f'{list(decoration.keys())}\n'
276         if msg.backend.format_body == 'format':
277             cbody = zcode.tag(body, frozenset(tags))
278         elif msg.backend.format_body == 'clear':
279             cbody = chunks.Chunk([(tags, '')])
280         else:
281             if msg.backend.format_body == 'strip':
282                 body = zcode.strip(body)
283             cbody = chunks.Chunk([(tags, body)])
284
285
286         chunk = chunks.Chunk([(tags, f'{prefix}')]) + cbody
287
288         if SETTINGS['show_signatures'] and msg.backend.format_zsig != 'clear':
289             zsig = '\n' + textwrap.fill(
290                 msg.data['signature'], width,
291                 initial_indent='                  -- ',
292                 subsequent_indent='                     ',
293                 break_long_words=False,
294                 break_on_hyphens=False)
295
296             if msg.backend.format_zsig == 'format':
297                 chunk += zcode.tag(zsig, frozenset(tags))
298             elif msg.backend.format_zsig == 'strip':
299                 chunk.append((tags, zcode.strip(zsig)))
300             else:
301                 chunk.append((tags, zsig))
302
303         if SETTINGS['show_addr']:
304             addr = str(ipaddress.ip_address(base64.b64decode(msg.data["uid"])[:4]))
305             # Hangs snipe while resolving; need to make async somehow.
306             #if addr not in HOSTNAMES:
307             #    try:
308             #        HOSTNAMES[addr] = socket.gethostbyaddr(addr)[0].upper()
309             #    except Exception:
310             #        HOSTNAMES[addr] = addr
311             #addr = HOSTNAMES[addr]
312             chunk.append((tags | {'right'}, f'## {addr}'))
313         else:
314             chunk.append((tags | {'right'}, ''))
315
316         return chunk