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