3 import pyramid.httpexceptions
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
14 log = logging.getLogger(__name__)
17 # Map of old weapons codes to new ones
19 "grenadelauncher": "mortar",
21 "minstanex": "vaporizer",
23 "rocketlauncher": "devastator",
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', '{', '|', '}', '~', '<',
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', '{', '|', '}', '~', '<'
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',
132 # Hex-colored spans for decimal color codes ^0 - ^9
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)'>"
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])')
151 # On a light scale of 0 (black) to 1.0 (white)
152 _contrast_threshold = 0.5
155 def qfont_decode(qstr='', glyph_translation=False):
156 """ Convert the qfont characters in a string to ascii.
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).
166 if u'\ue000' <= c <= u'\ue0ff':
167 if glyph_translation:
168 c = _qfont_unicode_glyphs[ord(c) - 0xe000]
170 c = _qfont_ascii_table[ord(c) - 0xe000]
172 return ''.join(chars)
175 def strip_colors(qstr=''):
178 return _all_colors.sub('', qstr)
182 """Convert Darkplaces hex color codes to CSS rgb.
183 Brighten colors with HSL light value less than 50%"""
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
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)
196 # Convert back to 0-255 scale for css
197 return '<span style="color:rgb(%d,%d,%d)">' % (255 * r, 255 * g, 255 * b)
200 def html_colors(qstr='', limit=None):
201 qstr = html_escape(qfont_decode(qstr, glyph_translation=True))
202 qstr = qstr.replace('^^', '^')
204 if limit is not None and limit > 0:
205 qstr = limit_printable_characters(qstr, limit)
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))
212 def limit_printable_characters(qstr, limit):
213 # initialize assuming all printable characters
214 pc = [1 for i in range(len(qstr))]
216 groups = _all_colors.finditer(qstr)
218 pc[g.start():g.end()] = [0 for i in range(g.end() - g.start())]
220 # printable characters in the string is less than or equal to what was requested
221 if limit >= len(qstr) or sum(pc) <= limit:
225 for i,v in enumerate(pc):
232 return pyramid.url.current_route_url(request, page=page, _query=request.GET)
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):
243 print "not a time value"
246 dim = round(diff.seconds/60.0 + diff.days*1440.0)
249 return "less than a minute ago"
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:
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"
273 return "about {0} years ago".format(int(round(dim/525600.0)))
275 def datetime_seconds(td):
276 return float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6
279 if not type(data) == dict:
280 # assume it's a named tuple
281 data = data._asdict()
283 for key,value in data.items():
286 elif type(value) in [bool,int,long,float,complex,str]:
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)
297 result[key] = to_json(value.to_dict())
301 def is_leap_year(today_dt=None):
303 today_dt = datetime.utcnow()
305 if today_dt.year % 400 == 0:
307 elif today_dt.year % 100 == 0:
309 elif today_dt.year % 4 == 0:
317 def is_cake_day(create_dt, today_dt=None):
321 today_dt = datetime.utcnow()
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:
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:
336 def verify_request(request):
337 """Verify requests using the d0_blind_id library"""
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
344 flg_verify_requests = False
347 (idfp, status) = d0_blind_id_verify(
348 sig=request.headers['X-D0-Blind-Id-Detached-Signature'],
350 postdata=request.body)
352 log.debug('ERROR: Could not verify request: {0}'.format(sys.exc_info()))
356 if flg_verify_requests and not idfp:
357 log.debug("ERROR: Unverified request")
358 raise pyramid.httpexceptions.HTTPUnauthorized("Unverified request")
360 return (idfp, status)