3 import pyramid.httpexceptions
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
13 log = logging.getLogger(__name__)
16 # Map of old weapons codes to new ones
18 "grenadelauncher": "mortar",
20 "minstanex": "vaporizer",
22 "rocketlauncher": "devastator",
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', '{', '|', '}', '~', '<',
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', '{', '|', '}', '~', '<'
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'\U0001F60F',
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'\U0001F60F',
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',
131 # Hex-colored spans for decimal color codes ^0 - ^9
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)'>"
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])')
150 # On a light scale of 0 (black) to 1.0 (white)
151 _contrast_threshold = 0.5
154 def qfont_decode(qstr='', glyph_translation=False):
155 """ Convert the qfont characters in a string to ascii.
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).
165 if u'\ue000' <= c <= u'\ue0ff':
166 if glyph_translation:
167 c = _qfont_unicode_glyphs[ord(c) - 0xe000]
169 c = _qfont_ascii_table[ord(c) - 0xe000]
171 return ''.join(chars)
174 def strip_colors(qstr=''):
177 return _all_colors.sub('', qstr)
181 """Convert Darkplaces hex color codes to CSS rgb.
182 Brighten colors with HSL light value less than 50%"""
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
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)
195 # Convert back to 0-255 scale for css
196 return '<span style="color:rgb(%d,%d,%d)">' % (255 * r, 255 * g, 255 * b)
199 def html_colors(qstr='', limit=None):
200 qstr = html_escape(qfont_decode(qstr, glyph_translation=True))
201 qstr = qstr.replace('^^', '^')
203 if limit is not None and limit > 0:
204 qstr = limit_printable_characters(qstr, limit)
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))
211 def limit_printable_characters(qstr, limit):
212 # initialize assuming all printable characters
213 pc = [1 for i in range(len(qstr))]
215 groups = _all_colors.finditer(qstr)
217 pc[g.start():g.end()] = [0 for i in range(g.end() - g.start())]
219 # printable characters in the string is less than or equal to what was requested
220 if limit >= len(qstr) or sum(pc) <= limit:
224 for i,v in enumerate(pc):
231 return current_route_url(request, page=page, _query=request.GET)
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):
242 print "not a time value"
245 dim = round(diff.seconds/60.0 + diff.days*1440.0)
248 return "less than a minute ago"
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:
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"
272 return "about {0} years ago".format(int(round(dim/525600.0)))
274 def datetime_seconds(td):
275 return float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6
278 if not type(data) == dict:
279 # assume it's a named tuple
280 data = data._asdict()
282 for key,value in data.items():
285 elif type(value) in [bool,int,long,float,complex,str]:
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)
296 result[key] = to_json(value.to_dict())
300 def is_leap_year(today_dt=None):
302 today_dt = datetime.utcnow()
304 if today_dt.year % 400 == 0:
306 elif today_dt.year % 100 == 0:
308 elif today_dt.year % 4 == 0:
316 def is_cake_day(create_dt, today_dt=None):
320 today_dt = datetime.utcnow()
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:
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:
335 def verify_request(request):
336 """Verify requests using the d0_blind_id library"""
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
343 flg_verify_requests = False
346 (idfp, status) = d0_blind_id_verify(
347 sig=request.headers['X-D0-Blind-Id-Detached-Signature'],
349 postdata=request.body)
351 log.debug('ERROR: Could not verify request: {0}'.format(sys.exc_info()))
355 if flg_verify_requests and not idfp:
356 log.debug("ERROR: Unverified request")
357 raise pyramid.httpexceptions.HTTPUnauthorized("Unverified request")
359 return (idfp, status)