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