]> de.git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/util.py
Rename a parameter to better represent what it does.
[xonotic/xonstat.git] / xonstat / util.py
1 import sys
2 import logging
3 import pyramid.httpexceptions
4 import re
5 from colorsys import rgb_to_hls, hls_to_rgb
6 from cgi import escape as html_escape
7 from datetime import datetime, timedelta
8 from decimal import Decimal
9 from collections import namedtuple
10 from xonstat.d0_blind_id import d0_blind_id_verify
11
12
13 log = logging.getLogger(__name__)
14
15
16 # Map of special chars to ascii from Darkplace's console.c.
17 _qfont_ascii_table = [
18  '\0', '#',  '#',  '#',  '#',  '.',  '#',  '#',
19  '#',  '\t', '\n', '#',  ' ',  '\r', '.',  '.',
20  '[',  ']',  '0',  '1',  '2',  '3',  '4',  '5',
21  '6',  '7',  '8',  '9',  '.',  '<',  '=',  '>',
22  ' ',  '!',  '"',  '#',  '$',  '%',  '&',  '\'',
23  '(',  ')',  '*',  '+',  ',',  '-',  '.',  '/',
24  '0',  '1',  '2',  '3',  '4',  '5',  '6',  '7',
25  '8',  '9',  ':',  ';',  '<',  '=',  '>',  '?',
26  '@',  'A',  'B',  'C',  'D',  'E',  'F',  'G',
27  'H',  'I',  'J',  'K',  'L',  'M',  'N',  'O',
28  'P',  'Q',  'R',  'S',  'T',  'U',  'V',  'W',
29  'X',  'Y',  'Z',  '[',  '\\', ']',  '^',  '_',
30  '`',  'a',  'b',  'c',  'd',  'e',  'f',  'g',
31  'h',  'i',  'j',  'k',  'l',  'm',  'n',  'o',
32  'p',  'q',  'r',  's',  't',  'u',  'v',  'w',
33  'x',  'y',  'z',  '{',  '|',  '}',  '~',  '<',
34
35  '<',  '=',  '>',  '#',  '#',  '.',  '#',  '#',
36  '#',  '#',  ' ',  '#',  ' ',  '>',  '.',  '.',
37  '[',  ']',  '0',  '1',  '2',  '3',  '4',  '5',
38  '6',  '7',  '8',  '9',  '.',  '<',  '=',  '>',
39  ' ',  '!',  '"',  '#',  '$',  '%',  '&',  '\'',
40  '(',  ')',  '*',  '+',  ',',  '-',  '.',  '/',
41  '0',  '1',  '2',  '3',  '4',  '5',  '6',  '7',
42  '8',  '9',  ':',  ';',  '<',  '=',  '>',  '?',
43  '@',  'A',  'B',  'C',  'D',  'E',  'F',  'G',
44  'H',  'I',  'J',  'K',  'L',  'M',  'N',  'O',
45  'P',  'Q',  'R',  'S',  'T',  'U',  'V',  'W',
46  'X',  'Y',  'Z',  '[',  '\\', ']',  '^',  '_',
47  '`',  'a',  'b',  'c',  'd',  'e',  'f',  'g',
48  'h',  'i',  'j',  'k',  'l',  'm',  'n',  'o',
49  'p',  'q',  'r',  's',  't',  'u',  'v',  'w',
50  'x',  'y',  'z',  '{',  '|',  '}',  '~',  '<'
51 ]
52
53 _qfont_unicode_glyphs = [
54    u'\u0020',       u'\u0020',       u'\u2014',       u'\u0020',
55    u'\u005F',       u'\u2747',       u'\u2020',       u'\u00B7',
56    u'\U0001F52B',   u'\u0020',       u'\u0020',       u'\u25A0',
57    u'\u2022',       u'\u2192',       u'\u2748',       u'\u2748',
58    u'\u005B',       u'\u005D',       u'\U0001F47D',   u'\U0001F60F',
59    u'\U0001F61E',   u'\U0001F635',   u'\U0001F615',   u'\U0001F60A',
60    u'\u00AB',       u'\u00BB',       u'\u2022',       u'\u203E',
61    u'\u2748',       u'\u25AC',       u'\u25AC',       u'\u25AC',
62    u'\u0020',       u'\u0021',       u'\u0022',       u'\u0023',
63    u'\u0024',       u'\u0025',       u'\u0026',       u'\u0027',
64    u'\u0028',       u'\u0029',       u'\u00D7',       u'\u002B',
65    u'\u002C',       u'\u002D',       u'\u002E',       u'\u002F',
66    u'\u0030',       u'\u0031',       u'\u0032',       u'\u0033',
67    u'\u0034',       u'\u0035',       u'\u0036',       u'\u0037',
68    u'\u0038',       u'\u0039',       u'\u003A',       u'\u003B',
69    u'\u003C',       u'\u003D',       u'\u003E',       u'\u003F',
70    u'\u0040',       u'\u0041',       u'\u0042',       u'\u0043',
71    u'\u0044',       u'\u0045',       u'\u0046',       u'\u0047',
72    u'\u0048',       u'\u0049',       u'\u004A',       u'\u004B',
73    u'\u004C',       u'\u004D',       u'\u004E',       u'\u004F',
74    u'\u0050',       u'\u0051',       u'\u0052',       u'\u0053',
75    u'\u0054',       u'\u0055',       u'\u0056',       u'\u0057',
76    u'\u0058',       u'\u0059',       u'\u005A',       u'\u005B',
77    u'\u005C',       u'\u005D',       u'\u005E',       u'\u005F',
78    u'\u0027',       u'\u0061',       u'\u0062',       u'\u0063',
79    u'\u0064',       u'\u0065',       u'\u0066',       u'\u0067',
80    u'\u0068',       u'\u0069',       u'\u006A',       u'\u006B',
81    u'\u006C',       u'\u006D',       u'\u006E',       u'\u006F',
82    u'\u0070',       u'\u0071',       u'\u0072',       u'\u0073',
83    u'\u0074',       u'\u0075',       u'\u0076',       u'\u0077',
84    u'\u0078',       u'\u0079',       u'\u007A',       u'\u007B',
85    u'\u007C',       u'\u007D',       u'\u007E',       u'\u2190',
86    u'\u003C',       u'\u003D',       u'\u003E',       u'\U0001F680',
87    u'\u00A1',       u'\u004F',       u'\u0055',       u'\u0049',
88    u'\u0043',       u'\u00A9',       u'\u00AE',       u'\u25A0',
89    u'\u00BF',       u'\u25B6',       u'\u2748',       u'\u2748',
90    u'\u2772',       u'\u2773',       u'\U0001F47D',   u'\U0001F60F',
91    u'\U0001F61E',   u'\U0001F635',   u'\U0001F615',   u'\U0001F60A',
92    u'\u00AB',       u'\u00BB',       u'\u2747',       u'\u0078',
93    u'\u2748',       u'\u2014',       u'\u2014',       u'\u2014',
94    u'\u0020',       u'\u0021',       u'\u0022',       u'\u0023',
95    u'\u0024',       u'\u0025',       u'\u0026',       u'\u0027',
96    u'\u0028',       u'\u0029',       u'\u002A',       u'\u002B',
97    u'\u002C',       u'\u002D',       u'\u002E',       u'\u002F',
98    u'\u0030',       u'\u0031',       u'\u0032',       u'\u0033',
99    u'\u0034',       u'\u0035',       u'\u0036',       u'\u0037',
100    u'\u0038',       u'\u0039',       u'\u003A',       u'\u003B',
101    u'\u003C',       u'\u003D',       u'\u003E',       u'\u003F',
102    u'\u0040',       u'\u0041',       u'\u0042',       u'\u0043',
103    u'\u0044',       u'\u0045',       u'\u0046',       u'\u0047',
104    u'\u0048',       u'\u0049',       u'\u004A',       u'\u004B',
105    u'\u004C',       u'\u004D',       u'\u004E',       u'\u004F',
106    u'\u0050',       u'\u0051',       u'\u0052',       u'\u0053',
107    u'\u0054',       u'\u0055',       u'\u0056',       u'\u0057',
108    u'\u0058',       u'\u0059',       u'\u005A',       u'\u005B',
109    u'\u005C',       u'\u005D',       u'\u005E',       u'\u005F',
110    u'\u0027',       u'\u0041',       u'\u0042',       u'\u0043',
111    u'\u0044',       u'\u0045',       u'\u0046',       u'\u0047',
112    u'\u0048',       u'\u0049',       u'\u004A',       u'\u004B',
113    u'\u004C',       u'\u004D',       u'\u004E',       u'\u004F',
114    u'\u0050',       u'\u0051',       u'\u0052',       u'\u0053',
115    u'\u0054',       u'\u0055',       u'\u0056',       u'\u0057',
116    u'\u0058',       u'\u0059',       u'\u005A',       u'\u007B',
117    u'\u007C',       u'\u007D',       u'\u007E',       u'\u25C0',
118 ]
119
120 # Hex-colored spans for decimal color codes ^0 - ^9
121 _dec_spans = [
122  "<span style='color:rgb(128,128,128)'>",
123  "<span style='color:rgb(255,0,0)'>",
124  "<span style='color:rgb(51,255,0)'>",
125  "<span style='color:rgb(255,255,0)'>",
126  "<span style='color:rgb(51,102,255)'>",
127  "<span style='color:rgb(51,255,255)'>",
128  "<span style='color:rgb(255,51,102)'>",
129  "<span style='color:rgb(255,255,255)'>",
130  "<span style='color:rgb(153,153,153)'>",
131  "<span style='color:rgb(128,128,128)'>"
132 ]
133
134 # Color code patterns
135 _all_colors = re.compile(r'\^(\d|x[\dA-Fa-f]{3})')
136 _dec_colors = re.compile(r'\^(\d)')
137 _hex_colors = re.compile(r'\^x([\dA-Fa-f])([\dA-Fa-f])([\dA-Fa-f])')
138
139 # On a light scale of 0 (black) to 1.0 (white)
140 _contrast_threshold = 0.5
141
142
143 def qfont_decode(qstr='', glyph_translation=False):
144     """ Convert the qfont characters in a string to ascii.
145
146         glyph_translation - determines whether to convert the unicode characters to
147                    their ascii counterparts (if False, the default) or to
148                    the mapped glyph in the Xolonium font (if True).
149     """
150     if qstr == None:
151         qstr = ''
152     chars = []
153     for c in qstr:
154         if u'\ue000' <= c <= u'\ue0ff':
155             if glyph_translation:
156                 c = _qfont_unicode_glyphs[ord(c) - 0xe000]
157             else:
158                 c = _qfont_ascii_table[ord(c) - 0xe000]
159         chars.append(c)
160     return ''.join(chars)
161
162
163 def strip_colors(qstr=''):
164     if qstr == None:
165         qstr = ''
166     return _all_colors.sub('', qstr)
167
168
169 def hex_repl(match):
170     """Convert Darkplaces hex color codes to CSS rgb.
171     Brighten colors with HSL light value less than 50%"""
172
173     # Extend hex char to 8 bits and to 0.0-1.0 scale
174     r = int(match.group(1) * 2, 16) / 255.0
175     g = int(match.group(2) * 2, 16) / 255.0
176     b = int(match.group(3) * 2, 16) / 255.0
177
178     # Check if color is too dark
179     hue, light, satur = rgb_to_hls(r, g, b)
180     if light < _contrast_threshold:
181         light = _contrast_threshold
182         r, g, b = hls_to_rgb(hue, light, satur)
183
184     # Convert back to 0-255 scale for css
185     return '<span style="color:rgb(%d,%d,%d)">' % (255 * r, 255 * g, 255 * b)
186
187
188 def html_colors(qstr='', limit=None):
189     qstr = html_escape(qfont_decode(qstr, glyph_translation=True))
190     qstr = qstr.replace('^^', '^')
191
192     if limit is not None and limit > 0:
193         qstr = limit_printable_characters(qstr, limit)
194
195     html = _dec_colors.sub(lambda match: _dec_spans[int(match.group(1))], qstr)
196     html = _hex_colors.sub(hex_repl, html)
197     return html + "</span>" * len(_all_colors.findall(qstr))
198
199
200 def limit_printable_characters(qstr, limit):
201     # initialize assuming all printable characters
202     pc = [1 for i in range(len(qstr))]
203
204     groups = _all_colors.finditer(qstr)
205     for g in groups:
206         pc[g.start():g.end()] = [0 for i in range(g.end() - g.start())]
207
208     # printable characters in the string is less than or equal to what was requested
209     if limit >= len(qstr) or sum(pc) <= limit:
210         return qstr
211     else:
212         sumpc = 0
213         for i,v in enumerate(pc):
214             sumpc += v
215             if sumpc == limit:
216                 return qstr[0:i+1]
217
218
219 def page_url(page):
220     return current_route_url(request, page=page, _query=request.GET)
221
222
223 def pretty_date(time=False):
224     '''Returns a human-readable relative date.'''
225     now = datetime.utcnow()
226     if type(time) is int:
227         diff = now - datetime.fromtimestamp(time)
228     elif isinstance(time,datetime):
229         diff = now - time
230     elif not time:
231         print "not a time value"
232         diff = now - now
233
234     dim = round(diff.seconds/60.0 + diff.days*1440.0)
235
236     if dim == 0:
237         return "less than a minute ago"
238     elif dim == 1:
239         return "1 minute ago"
240     elif dim >= 2 and dim <= 44:
241         return "{0} minutes ago".format(int(dim))
242     elif dim >= 45 and dim <= 89:
243         return "about 1 hour ago"
244     elif dim >= 90 and dim <= 1439:
245         return "about {0} hours ago".format(int(round(dim/60.0)))
246     elif dim >= 1440 and dim <= 2519:
247         return "1 day ago"
248     elif dim >= 2520 and dim <= 43199:
249         return "{0} days ago".format(int(round(dim/1440.0)))
250     elif dim >= 43200 and dim <= 86399:
251         return "about 1 month ago"
252     elif dim >= 86400 and dim <= 525599:
253         return "{0} months ago".format(int(round(dim/43200.0)))
254     elif dim >= 525600 and dim <= 655199:
255         return "about 1 year ago"
256     elif dim >= 655200 and dim <= 914399:
257         return "over 1 year ago"
258     elif dim >= 914400 and dim <= 1051199:
259         return "almost 2 years ago"
260     else:
261         return "about {0} years ago".format(int(round(dim/525600.0)))
262
263 def datetime_seconds(td):
264     return float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6
265
266 def to_json(data):
267     if not type(data) == dict:
268         # assume it's a named tuple
269         data = data._asdict()
270     result = {}
271     for key,value in data.items():
272         if value == None:
273             result[key] = None
274         elif type(value) in [bool,int,long,float,complex,str]:
275             result[key] = value
276         elif type(value) == unicode:
277             result[key] = value.encode('utf-8')
278         elif type(value) == Decimal:
279             result[key] = float(value)
280         elif type(value) == datetime:
281             result[key] = value.strftime('%Y-%m-%dT%H:%M:%SZ')
282         elif type(value) == timedelta:
283             result[key] = datetime_seconds(value)
284         else:
285             result[key] = to_json(value.to_dict())
286     return result
287
288
289 def is_leap_year(today_dt=None):
290     if today_dt is None:
291         today_dt = datetime.utcnow()
292
293     if today_dt.year % 400 == 0:
294        leap_year = True
295     elif today_dt.year % 100 == 0:
296        leap_year = False
297     elif today_dt.year % 4 == 0:
298        leap_year = True
299     else:
300        leap_year = False
301
302     return leap_year
303
304
305 def is_cake_day(create_dt, today_dt=None):
306     cake_day = False
307
308     if today_dt is None:
309         today_dt = datetime.utcnow()
310
311     # cakes are given on the first anniversary, not the actual create date!
312     if datetime.date(today_dt) != datetime.date(create_dt):
313         if today_dt.day == create_dt.day and today_dt.month == create_dt.month:
314             cake_day = True
315
316         # leap year people get their cakes on March 1
317         if not is_leap_year(today_dt) and create_dt.month == 2 and create_dt.day == 29:
318             if today_dt.month == 3 and today_dt.day == 1:
319                 cake_day = True
320
321     return cake_day
322
323
324 def verify_request(request):
325     """Verify requests using the d0_blind_id library"""
326
327     # first determine if we should be verifying or not
328     val_verify_requests = request.registry.settings.get('xonstat.verify_requests', 'true')
329     if val_verify_requests == "true":
330         flg_verify_requests = True
331     else:
332         flg_verify_requests = False
333
334     try:
335         (idfp, status) = d0_blind_id_verify(
336                 sig=request.headers['X-D0-Blind-Id-Detached-Signature'],
337                 querystring='',
338                 postdata=request.body)
339     except:
340         log.debug('ERROR: Could not verify request: {0}'.format(sys.exc_info()))
341         idfp = None
342         status = None
343
344     if flg_verify_requests and not idfp:
345         log.debug("ERROR: Unverified request")
346         raise pyramid.httpexceptions.HTTPUnauthorized("Unverified request")
347
348     return (idfp, status)