]> git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/util.py
0891765759356d3c1095985e64fe300a06d23fa0
[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_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 # Hex-colored spans for decimal color codes ^0 - ^9
54 _dec_spans = [
55  "<span style='color:rgb(128,128,128)'>",
56  "<span style='color:rgb(255,0,0)'>",
57  "<span style='color:rgb(51,255,0)'>",
58  "<span style='color:rgb(255,255,0)'>",
59  "<span style='color:rgb(51,102,255)'>",
60  "<span style='color:rgb(51,255,255)'>",
61  "<span style='color:rgb(255,51,102)'>",
62  "<span style='color:rgb(255,255,255)'>",
63  "<span style='color:rgb(153,153,153)'>",
64  "<span style='color:rgb(128,128,128)'>"
65 ]
66
67 # Color code patterns
68 _all_colors = re.compile(r'\^(\d|x[\dA-Fa-f]{3})')
69 _dec_colors = re.compile(r'\^(\d)')
70 _hex_colors = re.compile(r'\^x([\dA-Fa-f])([\dA-Fa-f])([\dA-Fa-f])')
71
72 # On a light scale of 0 (black) to 1.0 (white)
73 _contrast_threshold = 0.5
74
75
76 def qfont_decode(qstr=''):
77     """ Convert the qfont characters in a string to ascii.
78     """
79     if qstr == None:
80         qstr = ''
81     chars = []
82     for c in qstr:
83         if u'\ue000' <= c <= u'\ue0ff':
84             c = _qfont_table[ord(c) - 0xe000]
85         chars.append(c)
86     return ''.join(chars)
87
88
89 def strip_colors(qstr=''):
90     if qstr == None:
91         qstr = ''
92     return _all_colors.sub('', qstr)
93
94
95 def hex_repl(match):
96     """Convert Darkplaces hex color codes to CSS rgb.
97     Brighten colors with HSL light value less than 50%"""
98
99     # Extend hex char to 8 bits and to 0.0-1.0 scale
100     r = int(match.group(1) * 2, 16) / 255.0
101     g = int(match.group(2) * 2, 16) / 255.0
102     b = int(match.group(3) * 2, 16) / 255.0
103
104     # Check if color is too dark
105     hue, light, satur = rgb_to_hls(r, g, b)
106     if light < _contrast_threshold:
107         light = _contrast_threshold
108         r, g, b = hls_to_rgb(hue, light, satur)
109
110     # Convert back to 0-255 scale for css
111     return '<span style="color:rgb(%d,%d,%d)">' % (255 * r, 255 * g, 255 * b)
112
113
114 def html_colors(qstr='', limit=None):
115     qstr = html_escape(qfont_decode(qstr))
116     qstr = qstr.replace('^^', '^')
117
118     if limit is not None and limit > 0:
119         qstr = limit_printable_characters(qstr, limit)
120
121     html = _dec_colors.sub(lambda match: _dec_spans[int(match.group(1))], qstr)
122     html = _hex_colors.sub(hex_repl, html)
123     return html + "</span>" * len(_all_colors.findall(qstr))
124
125
126 def limit_printable_characters(qstr, limit):
127     # initialize assuming all printable characters
128     pc = [1 for i in range(len(qstr))]
129
130     groups = _all_colors.finditer(qstr)
131     for g in groups:
132         pc[g.start():g.end()] = [0 for i in range(g.end() - g.start())]
133
134     # printable characters in the string is less than or equal to what was requested
135     if limit >= len(qstr) or sum(pc) <= limit:
136         return qstr
137     else:
138         sumpc = 0
139         for i,v in enumerate(pc):
140             sumpc += v
141             if sumpc == limit:
142                 return qstr[0:i+1]
143
144
145 def page_url(page):
146     return current_route_url(request, page=page, _query=request.GET)
147
148
149 def pretty_date(time=False):
150     '''Returns a human-readable relative date.'''
151     now = datetime.utcnow()
152     if type(time) is int:
153         diff = now - datetime.fromtimestamp(time)
154     elif isinstance(time,datetime):
155         diff = now - time
156     elif not time:
157         print "not a time value"
158         diff = now - now
159
160     dim = round(diff.seconds/60.0 + diff.days*1440.0)
161
162     if dim == 0:
163         return "less than a minute ago"
164     elif dim == 1:
165         return "1 minute ago"
166     elif dim >= 2 and dim <= 44:
167         return "{0} minutes ago".format(int(dim))
168     elif dim >= 45 and dim <= 89:
169         return "about 1 hour ago"
170     elif dim >= 90 and dim <= 1439:
171         return "about {0} hours ago".format(int(round(dim/60.0)))
172     elif dim >= 1440 and dim <= 2519:
173         return "1 day ago"
174     elif dim >= 2520 and dim <= 43199:
175         return "{0} days ago".format(int(round(dim/1440.0)))
176     elif dim >= 43200 and dim <= 86399:
177         return "about 1 month ago"
178     elif dim >= 86400 and dim <= 525599:
179         return "{0} months ago".format(int(round(dim/43200.0)))
180     elif dim >= 525600 and dim <= 655199:
181         return "about 1 year ago"
182     elif dim >= 655200 and dim <= 914399:
183         return "over 1 year ago"
184     elif dim >= 914400 and dim <= 1051199:
185         return "almost 2 years ago"
186     else:
187         return "about {0} years ago".format(int(round(dim/525600.0)))
188
189 def datetime_seconds(td):
190     return float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6
191
192 def to_json(data):
193     if not type(data) == dict:
194         # assume it's a named tuple
195         data = data._asdict()
196     result = {}
197     for key,value in data.items():
198         if value == None:
199             result[key] = None
200         elif type(value) in [bool,int,long,float,complex,str]:
201             result[key] = value
202         elif type(value) == unicode:
203             result[key] = value.encode('utf-8')
204         elif type(value) == Decimal:
205             result[key] = float(value)
206         elif type(value) == datetime:
207             result[key] = value.strftime('%Y-%m-%dT%H:%M:%SZ')
208         elif type(value) == timedelta:
209             result[key] = datetime_seconds(value)
210         else:
211             result[key] = to_json(value.to_dict())
212     return result
213
214
215 def is_leap_year(today_dt=None):
216     if today_dt is None:
217         today_dt = datetime.utcnow()
218
219     if today_dt.year % 400 == 0:
220        leap_year = True
221     elif today_dt.year % 100 == 0:
222        leap_year = False
223     elif today_dt.year % 4 == 0:
224        leap_year = True
225     else:
226        leap_year = False
227
228     return leap_year
229
230
231 def is_cake_day(create_dt, today_dt=None):
232     cake_day = False
233
234     if today_dt is None:
235         today_dt = datetime.utcnow()
236
237     # cakes are given on the first anniversary, not the actual create date!
238     if datetime.date(today_dt) != datetime.date(create_dt):
239         if today_dt.day == create_dt.day and today_dt.month == create_dt.month:
240             cake_day = True
241
242         # leap year people get their cakes on March 1
243         if not is_leap_year(today_dt) and create_dt.month == 2 and create_dt.day == 29:
244             if today_dt.month == 3 and today_dt.day == 1:
245                 cake_day = True
246
247     return cake_day
248
249
250 def verify_request(request):
251     """Verify requests using the d0_blind_id library"""
252
253     # first determine if we should be verifying or not
254     val_verify_requests = request.registry.settings.get('xonstat.verify_requests', 'true')
255     if val_verify_requests == "true":
256         flg_verify_requests = True
257     else:
258         flg_verify_requests = False
259
260     try:
261         (idfp, status) = d0_blind_id_verify(
262                 sig=request.headers['X-D0-Blind-Id-Detached-Signature'],
263                 querystring='',
264                 postdata=request.body)
265     except:
266         log.debug('ERROR: Could not verify request: {0}'.format(sys.exc_info()))
267         idfp = None
268         status = None
269
270     if flg_verify_requests and not idfp:
271         log.debug("ERROR: Unverified request")
272         raise pyramid.httpexceptions.HTTPUnauthorized("Unverified request")
273
274     return (idfp, status)