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