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 special chars to ascii from Darkplace's console.c.
17 _qfont_ascii_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', '{', '|', '}', '~', '<',
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', '{', '|', '}', '~', '<'
53 _qfont_unicode_glyphs = [
54 u'\u0020', u'\u0020', u'\u2014', u'\u0020',
55 u'\u005F', u'\u2747', u'\u2020', u'\u00B7',
56 u'\U0001F52B', u'\u0020', u'\u0020', u'\u25A0',
57 u'\u2022', u'\u2192', u'\u2748', u'\u2748',
58 u'\u005B', u'\u005D', u'\U0001F47D', u'\U0001F60F',
59 u'\U0001F61E', u'\U0001F635', u'\U0001F615', u'\U0001F60A',
60 u'\u00AB', u'\u00BB', u'\u2022', u'\u203E',
61 u'\u2748', u'\u25AC', u'\u25AC', u'\u25AC',
62 u'\u0020', u'\u0021', u'\u0022', u'\u0023',
63 u'\u0024', u'\u0025', u'\u0026', u'\u0027',
64 u'\u0028', u'\u0029', u'\u00D7', u'\u002B',
65 u'\u002C', u'\u002D', u'\u002E', u'\u002F',
66 u'\u0030', u'\u0031', u'\u0032', u'\u0033',
67 u'\u0034', u'\u0035', u'\u0036', u'\u0037',
68 u'\u0038', u'\u0039', u'\u003A', u'\u003B',
69 u'\u003C', u'\u003D', u'\u003E', u'\u003F',
70 u'\u0040', u'\u0041', u'\u0042', u'\u0043',
71 u'\u0044', u'\u0045', u'\u0046', u'\u0047',
72 u'\u0048', u'\u0049', u'\u004A', u'\u004B',
73 u'\u004C', u'\u004D', u'\u004E', u'\u004F',
74 u'\u0050', u'\u0051', u'\u0052', u'\u0053',
75 u'\u0054', u'\u0055', u'\u0056', u'\u0057',
76 u'\u0058', u'\u0059', u'\u005A', u'\u005B',
77 u'\u005C', u'\u005D', u'\u005E', u'\u005F',
78 u'\u0027', u'\u0061', u'\u0062', u'\u0063',
79 u'\u0064', u'\u0065', u'\u0066', u'\u0067',
80 u'\u0068', u'\u0069', u'\u006A', u'\u006B',
81 u'\u006C', u'\u006D', u'\u006E', u'\u006F',
82 u'\u0070', u'\u0071', u'\u0072', u'\u0073',
83 u'\u0074', u'\u0075', u'\u0076', u'\u0077',
84 u'\u0078', u'\u0079', u'\u007A', u'\u007B',
85 u'\u007C', u'\u007D', u'\u007E', u'\u2190',
86 u'\u003C', u'\u003D', u'\u003E', u'\U0001F680',
87 u'\u00A1', u'\u004F', u'\u0055', u'\u0049',
88 u'\u0043', u'\u00A9', u'\u00AE', u'\u25A0',
89 u'\u00BF', u'\u25B6', u'\u2748', u'\u2748',
90 u'\u2772', u'\u2773', u'\U0001F47D', u'\U0001F60F',
91 u'\U0001F61E', u'\U0001F635', u'\U0001F615', u'\U0001F60A',
92 u'\u00AB', u'\u00BB', u'\u2747', u'\u0078',
93 u'\u2748', u'\u2014', u'\u2014', u'\u2014',
94 u'\u0020', u'\u0021', u'\u0022', u'\u0023',
95 u'\u0024', u'\u0025', u'\u0026', u'\u0027',
96 u'\u0028', u'\u0029', u'\u002A', u'\u002B',
97 u'\u002C', u'\u002D', u'\u002E', u'\u002F',
98 u'\u0030', u'\u0031', u'\u0032', u'\u0033',
99 u'\u0034', u'\u0035', u'\u0036', u'\u0037',
100 u'\u0038', u'\u0039', u'\u003A', u'\u003B',
101 u'\u003C', u'\u003D', u'\u003E', u'\u003F',
102 u'\u0040', u'\u0041', u'\u0042', u'\u0043',
103 u'\u0044', u'\u0045', u'\u0046', u'\u0047',
104 u'\u0048', u'\u0049', u'\u004A', u'\u004B',
105 u'\u004C', u'\u004D', u'\u004E', u'\u004F',
106 u'\u0050', u'\u0051', u'\u0052', u'\u0053',
107 u'\u0054', u'\u0055', u'\u0056', u'\u0057',
108 u'\u0058', u'\u0059', u'\u005A', u'\u005B',
109 u'\u005C', u'\u005D', u'\u005E', u'\u005F',
110 u'\u0027', u'\u0041', u'\u0042', u'\u0043',
111 u'\u0044', u'\u0045', u'\u0046', u'\u0047',
112 u'\u0048', u'\u0049', u'\u004A', u'\u004B',
113 u'\u004C', u'\u004D', u'\u004E', u'\u004F',
114 u'\u0050', u'\u0051', u'\u0052', u'\u0053',
115 u'\u0054', u'\u0055', u'\u0056', u'\u0057',
116 u'\u0058', u'\u0059', u'\u005A', u'\u007B',
117 u'\u007C', u'\u007D', u'\u007E', u'\u25C0',
120 # Hex-colored spans for decimal color codes ^0 - ^9
122 "<span style='color:rgb(128,128,128)'>",
123 "<span style='color:rgb(255,0,0)'>",
124 "<span style='color:rgb(51,255,0)'>",
125 "<span style='color:rgb(255,255,0)'>",
126 "<span style='color:rgb(51,102,255)'>",
127 "<span style='color:rgb(51,255,255)'>",
128 "<span style='color:rgb(255,51,102)'>",
129 "<span style='color:rgb(255,255,255)'>",
130 "<span style='color:rgb(153,153,153)'>",
131 "<span style='color:rgb(128,128,128)'>"
134 # Color code patterns
135 _all_colors = re.compile(r'\^(\d|x[\dA-Fa-f]{3})')
136 _dec_colors = re.compile(r'\^(\d)')
137 _hex_colors = re.compile(r'\^x([\dA-Fa-f])([\dA-Fa-f])([\dA-Fa-f])')
139 # On a light scale of 0 (black) to 1.0 (white)
140 _contrast_threshold = 0.5
143 def qfont_decode(qstr='', glyph_translation=False):
144 """ Convert the qfont characters in a string to ascii.
146 glyph_translation - determines whether to convert the unicode characters to
147 their ascii counterparts (if False, the default) or to
148 the mapped glyph in the Xolonium font (if True).
154 if u'\ue000' <= c <= u'\ue0ff':
155 if glyph_translation:
156 c = _qfont_unicode_glyphs[ord(c) - 0xe000]
158 c = _qfont_ascii_table[ord(c) - 0xe000]
160 return ''.join(chars)
163 def strip_colors(qstr=''):
166 return _all_colors.sub('', qstr)
170 """Convert Darkplaces hex color codes to CSS rgb.
171 Brighten colors with HSL light value less than 50%"""
173 # Extend hex char to 8 bits and to 0.0-1.0 scale
174 r = int(match.group(1) * 2, 16) / 255.0
175 g = int(match.group(2) * 2, 16) / 255.0
176 b = int(match.group(3) * 2, 16) / 255.0
178 # Check if color is too dark
179 hue, light, satur = rgb_to_hls(r, g, b)
180 if light < _contrast_threshold:
181 light = _contrast_threshold
182 r, g, b = hls_to_rgb(hue, light, satur)
184 # Convert back to 0-255 scale for css
185 return '<span style="color:rgb(%d,%d,%d)">' % (255 * r, 255 * g, 255 * b)
188 def html_colors(qstr='', limit=None):
189 qstr = html_escape(qfont_decode(qstr, glyph_translation=True))
190 qstr = qstr.replace('^^', '^')
192 if limit is not None and limit > 0:
193 qstr = limit_printable_characters(qstr, limit)
195 html = _dec_colors.sub(lambda match: _dec_spans[int(match.group(1))], qstr)
196 html = _hex_colors.sub(hex_repl, html)
197 return html + "</span>" * len(_all_colors.findall(qstr))
200 def limit_printable_characters(qstr, limit):
201 # initialize assuming all printable characters
202 pc = [1 for i in range(len(qstr))]
204 groups = _all_colors.finditer(qstr)
206 pc[g.start():g.end()] = [0 for i in range(g.end() - g.start())]
208 # printable characters in the string is less than or equal to what was requested
209 if limit >= len(qstr) or sum(pc) <= limit:
213 for i,v in enumerate(pc):
220 return current_route_url(request, page=page, _query=request.GET)
223 def pretty_date(time=False):
224 '''Returns a human-readable relative date.'''
225 now = datetime.utcnow()
226 if type(time) is int:
227 diff = now - datetime.fromtimestamp(time)
228 elif isinstance(time,datetime):
231 print "not a time value"
234 dim = round(diff.seconds/60.0 + diff.days*1440.0)
237 return "less than a minute ago"
239 return "1 minute ago"
240 elif dim >= 2 and dim <= 44:
241 return "{0} minutes ago".format(int(dim))
242 elif dim >= 45 and dim <= 89:
243 return "about 1 hour ago"
244 elif dim >= 90 and dim <= 1439:
245 return "about {0} hours ago".format(int(round(dim/60.0)))
246 elif dim >= 1440 and dim <= 2519:
248 elif dim >= 2520 and dim <= 43199:
249 return "{0} days ago".format(int(round(dim/1440.0)))
250 elif dim >= 43200 and dim <= 86399:
251 return "about 1 month ago"
252 elif dim >= 86400 and dim <= 525599:
253 return "{0} months ago".format(int(round(dim/43200.0)))
254 elif dim >= 525600 and dim <= 655199:
255 return "about 1 year ago"
256 elif dim >= 655200 and dim <= 914399:
257 return "over 1 year ago"
258 elif dim >= 914400 and dim <= 1051199:
259 return "almost 2 years ago"
261 return "about {0} years ago".format(int(round(dim/525600.0)))
263 def datetime_seconds(td):
264 return float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6
267 if not type(data) == dict:
268 # assume it's a named tuple
269 data = data._asdict()
271 for key,value in data.items():
274 elif type(value) in [bool,int,long,float,complex,str]:
276 elif type(value) == unicode:
277 result[key] = value.encode('utf-8')
278 elif type(value) == Decimal:
279 result[key] = float(value)
280 elif type(value) == datetime:
281 result[key] = value.strftime('%Y-%m-%dT%H:%M:%SZ')
282 elif type(value) == timedelta:
283 result[key] = datetime_seconds(value)
285 result[key] = to_json(value.to_dict())
289 def is_leap_year(today_dt=None):
291 today_dt = datetime.utcnow()
293 if today_dt.year % 400 == 0:
295 elif today_dt.year % 100 == 0:
297 elif today_dt.year % 4 == 0:
305 def is_cake_day(create_dt, today_dt=None):
309 today_dt = datetime.utcnow()
311 # cakes are given on the first anniversary, not the actual create date!
312 if datetime.date(today_dt) != datetime.date(create_dt):
313 if today_dt.day == create_dt.day and today_dt.month == create_dt.month:
316 # leap year people get their cakes on March 1
317 if not is_leap_year(today_dt) and create_dt.month == 2 and create_dt.day == 29:
318 if today_dt.month == 3 and today_dt.day == 1:
324 def verify_request(request):
325 """Verify requests using the d0_blind_id library"""
327 # first determine if we should be verifying or not
328 val_verify_requests = request.registry.settings.get('xonstat.verify_requests', 'true')
329 if val_verify_requests == "true":
330 flg_verify_requests = True
332 flg_verify_requests = False
335 (idfp, status) = d0_blind_id_verify(
336 sig=request.headers['X-D0-Blind-Id-Detached-Signature'],
338 postdata=request.body)
340 log.debug('ERROR: Could not verify request: {0}'.format(sys.exc_info()))
344 if flg_verify_requests and not idfp:
345 log.debug("ERROR: Unverified request")
346 raise pyramid.httpexceptions.HTTPUnauthorized("Unverified request")
348 return (idfp, status)