Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
main.py
Go to the documentation of this file.
1# Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
2# for details. All rights reserved. Use of this source code is governed by a
3# BSD-style license that can be found in the LICENSE file.
4
5#!/usr/bin/env python3
6#
7import re, base64, logging, pickle, httplib2, time, urlparse, urllib2, urllib, StringIO, gzip, zipfile
8
9from google.appengine.ext import webapp, db
10
11from google.appengine.api import taskqueue, urlfetch, memcache, images, users
12from google.appengine.ext.webapp.util import login_required
13from google.appengine.ext.webapp import template
14
15from django.utils import simplejson as json
16from django.utils.html import strip_tags
17
18from oauth2client.appengine import CredentialsProperty
19from oauth2client.client import OAuth2WebServerFlow
20
21import encoder
22
23# TODO(jimhug): Allow client to request desired thumb size.
24THUMB_SIZE = (57, 57)
25READER_API = 'http://www.google.com/reader/api/0'
26
27MAX_SECTIONS = 5
28MAX_ARTICLES = 20
29
30
31class UserData(db.Model):
32 credentials = CredentialsProperty()
33 sections = db.ListProperty(db.Key)
34
35 def getEncodedData(self, articleKeys=None):
36 enc = encoder.Encoder()
37 # TODO(jimhug): Only return initially visible section in first reply.
38 maxSections = min(MAX_SECTIONS, len(self.sections))
39 enc.writeInt(maxSections)
40 for section in db.get(self.sections[:maxSections]):
41 section.encode(enc, articleKeys)
42 return enc.getRaw()
43
44
45class Section(db.Model):
46 title = db.TextProperty()
47 feeds = db.ListProperty(db.Key)
48
49 def fixedTitle(self):
50 return self.title.split('_')[0]
51
52 def encode(self, enc, articleKeys=None):
53 # TODO(jimhug): Need to optimize format and support incremental updates.
54 enc.writeString(self.key().name())
55 enc.writeString(self.fixedTitle())
56 enc.writeInt(len(self.feeds))
57 for feed in db.get(self.feeds):
58 feed.ensureEncodedFeed()
59 enc.writeRaw(feed.encodedFeed3)
60 if articleKeys is not None:
61 articleKeys.extend(feed.topArticles)
62
63
64class Feed(db.Model):
65 title = db.TextProperty()
66 iconUrl = db.TextProperty()
67 lastUpdated = db.IntegerProperty()
68
69 encodedFeed3 = db.TextProperty()
70 topArticles = db.ListProperty(db.Key)
71
72 def ensureEncodedFeed(self, force=False):
73 if force or self.encodedFeed3 is None:
74 enc = encoder.Encoder()
75 articleSet = []
76 self.encode(enc, MAX_ARTICLES, articleSet)
77 logging.info('articleSet length is %s' % len(articleSet))
78 self.topArticles = articleSet
79 self.encodedFeed3 = enc.getRaw()
80 self.put()
81
82 def encode(self, enc, maxArticles, articleSet):
83 enc.writeString(self.key().name())
84 enc.writeString(self.title)
85 enc.writeString(self.iconUrl)
86
87 logging.info('encoding feed: %s' % self.title)
88 encodedArts = []
89
90 for article in self.article_set.order('-date').fetch(limit=maxArticles):
91 encodedArts.append(article.encodeHeader())
92 articleSet.append(article.key())
93
94 enc.writeInt(len(encodedArts))
95 enc.writeRaw(''.join(encodedArts))
96
97
98class Article(db.Model):
99 feed = db.ReferenceProperty(Feed)
100
101 title = db.TextProperty()
102 author = db.TextProperty()
103 content = db.TextProperty()
104 snippet = db.TextProperty()
105 thumbnail = db.BlobProperty()
106 thumbnailSize = db.TextProperty()
107 srcurl = db.TextProperty()
108 date = db.IntegerProperty()
109
111 # If our desired thumbnail size has changed, regenerate it and cache.
112 if self.thumbnailSize != str(THUMB_SIZE):
113 self.thumbnail = makeThumbnail(self.content)
114 self.thumbnailSize = str(THUMB_SIZE)
115 self.put()
116
117 def encodeHeader(self):
118 # TODO(jmesserly): for now always unescape until the crawler catches up
119 enc = encoder.Encoder()
120 enc.writeString(self.key().name())
121 enc.writeString(unescape(self.title))
122 enc.writeString(self.srcurl)
123 enc.writeBool(self.thumbnail is not None)
124 enc.writeString(self.author)
125 enc.writeInt(self.date)
126 enc.writeString(unescape(self.snippet))
127 return enc.getRaw()
128
129
130class HtmlFile(db.Model):
131 content = db.BlobProperty()
132 compressed = db.BooleanProperty()
133 filename = db.StringProperty()
134 author = db.UserProperty(auto_current_user=True)
135 date = db.DateTimeProperty(auto_now_add=True)
136
137
138class UpdateHtml(webapp.RequestHandler):
139
140 def post(self):
141 upload_files = self.request.POST.multi.__dict__['_items']
142 version = self.request.get('version')
143 logging.info('files: %r' % upload_files)
144 for data in upload_files:
145 if data[0] != 'files': continue
146 file = data[1]
147 filename = file.filename
148 if version:
149 filename = '%s-%s' % (version, filename)
150 logging.info('upload: %r' % filename)
151
152 htmlFile = HtmlFile.get_or_insert(filename)
153 htmlFile.filename = filename
154
155 # If text > (1MB - 1KB) then gzip text to fit in 1MB space
156 text = file.value
157 if len(text) > 1024 * 1023:
158 data = StringIO.StringIO()
159 gz = gzip.GzipFile(str(filename), 'wb', fileobj=data)
160 gz.write(text)
161 gz.close()
162 htmlFile.content = data.getvalue()
163 htmlFile.compressed = True
164 else:
165 htmlFile.content = text
166 htmlFile.compressed = False
167
168 htmlFile.put()
169
170 self.redirect('/')
171
172
173class TopHandler(webapp.RequestHandler):
174
175 @login_required
176 def get(self):
177 user = users.get_current_user()
178 prefs = UserData.get_by_key_name(user.user_id())
179 if prefs is None:
180 self.redirect('/update/user')
181 return
182
183 params = {'files': HtmlFile.all().order('-date').fetch(limit=30)}
184 self.response.out.write(template.render('top.html', params))
185
186
187class MainHandler(webapp.RequestHandler):
188
189 @login_required
190 def get(self, name):
191 if name == 'dev':
192 return self.handleDev()
193
194 elif name == 'login':
195 return self.handleLogin()
196
197 elif name == 'upload':
198 return self.handleUpload()
199
200 user = users.get_current_user()
201 prefs = UserData.get_by_key_name(user.user_id())
202 if prefs is None:
203 return self.handleLogin()
204
205 html = HtmlFile.get_by_key_name(name)
206 if html is None:
207 self.error(404)
208 return
209
210 self.response.headers['Content-Type'] = 'text/html'
211
212 if html.compressed:
213 # TODO(jimhug): This slightly sucks ;-)
214 # Can we write directly to the response.out?
215 gz = gzip.GzipFile(name,
216 'rb',
217 fileobj=StringIO.StringIO(html.content))
218 self.response.out.write(gz.read())
219 gz.close()
220 else:
221 self.response.out.write(html.content)
222
223 # TODO(jimhug): Include first data packet with html.
224
225 def handleLogin(self):
226 user = users.get_current_user()
227 # TODO(jimhug): Manage secrets for dart.googleplex.com better.
228 # TODO(jimhug): Confirm that we need client_secret.
229 flow = OAuth2WebServerFlow(
230 client_id='267793340506.apps.googleusercontent.com',
231 client_secret='5m8H-zyamfTYg5vnpYu1uGMU',
232 scope=READER_API,
233 user_agent='swarm')
234
235 callback = self.request.relative_url('/oauth2callback')
236 authorize_url = flow.step1_get_authorize_url(callback)
237
238 memcache.set(user.user_id(), pickle.dumps(flow))
239
240 content = template.render('login.html', {'authorize': authorize_url})
241 self.response.out.write(content)
242
243 def handleDev(self):
244 user = users.get_current_user()
245 content = template.render('dev.html', {'user': user})
246 self.response.out.write(content)
247
248 def handleUpload(self):
249 user = users.get_current_user()
250 content = template.render('upload.html', {'user': user})
251 self.response.out.write(content)
252
253
254class UploadFeed(webapp.RequestHandler):
255
256 def post(self):
257 upload_files = self.request.POST.multi.__dict__['_items']
258 version = self.request.get('version')
259 logging.info('files: %r' % upload_files)
260 for data in upload_files:
261 if data[0] != 'files': continue
262 file = data[1]
263 logging.info('upload feed: %r' % file.filename)
264
265 data = json.loads(file.value)
266
267 feedId = file.filename
268 feed = Feed.get_or_insert(feedId)
269
270 # Find the section to add it to.
271 sectionTitle = data['section']
272 section = findSectionByTitle(sectionTitle)
273 if section != None:
274 if feed.key() in section.feeds:
275 logging.warn('Already contains feed %s, replacing' % feedId)
276 section.feeds.remove(feed.key())
277
278 # Add the feed to the section.
279 section.feeds.insert(0, feed.key())
280 section.put()
281
282 # Add the articles.
283 collectFeed(feed, data)
284
285 else:
286 logging.error('Could not find section %s to add the feed to' %
287 sectionTitle)
288
289 self.redirect('/')
290
291
292# TODO(jimhug): Batch these up and request them more aggressively.
293class DataHandler(webapp.RequestHandler):
294
295 def get(self, name):
296 if name.endswith('.jpg'):
297 # Must be a thumbnail
298 key = urllib2.unquote(name[:-len('.jpg')])
299 article = Article.get_by_key_name(key)
300 self.response.headers['Content-Type'] = 'image/jpeg'
301 # cache images for 10 hours
302 self.response.headers['Cache-Control'] = 'public,max-age=36000'
303 article.ensureThumbnail()
304 self.response.out.write(article.thumbnail)
305 elif name.endswith('.html'):
306 # Must be article content
307 key = urllib2.unquote(name[:-len('.html')])
308 article = Article.get_by_key_name(key)
309 self.response.headers['Content-Type'] = 'text/html'
310 if article is None:
311 content = '<h2>Missing article</h2>'
312 else:
313 content = article.content
314 # cache article content for 10 hours
315 self.response.headers['Cache-Control'] = 'public,max-age=36000'
316 self.response.out.write(content)
317 elif name == 'user.data':
318 self.response.out.write(self.getUserData())
319 elif name == 'CannedData.dart':
320 self.canData()
321 elif name == 'CannedData.zip':
322 self.canDataZip()
323 else:
324 self.error(404)
325
326 def getUserData(self, articleKeys=None):
327 user = users.get_current_user()
328 user_id = user.user_id()
329
330 key = 'data_' + user_id
331 # need to flush memcache fairly frequently...
332 data = memcache.get(key)
333 if data is None:
334 prefs = UserData.get_or_insert(user_id)
335 if prefs is None:
336 # TODO(jimhug): Graceful failure for unknown users.
337 pass
338 data = prefs.getEncodedData(articleKeys)
339 # TODO(jimhug): memcache.set(key, data)
340
341 return data
342
343 def canData(self):
344
345 def makeDartSafe(data):
346 return repr(unicode(data))[1:].replace('$', '\\$')
347
348 lines = [
349 '// TODO(jimhug): Work out correct copyright for this file.',
350 'class CannedData {'
351 ]
352
353 user = users.get_current_user()
354 prefs = UserData.get_by_key_name(user.user_id())
355 articleKeys = []
356 data = prefs.getEncodedData(articleKeys)
357 lines.append(' static const Map<String,String> data = const {')
358 for article in db.get(articleKeys):
359 key = makeDartSafe(urllib.quote(article.key().name()) + '.html')
360 lines.append(' %s:%s, ' % (key, makeDartSafe(article.content)))
361
362 lines.append(' "user.data":%s' % makeDartSafe(data))
363
364 lines.append(' };')
365
366 lines.append('}')
367 self.response.headers['Content-Type'] = 'application/dart'
368 self.response.out.write('\n'.join(lines))
369
370 # Get canned static data
371 def canDataZip(self):
372 # We need to zip into an in-memory buffer to get the right string encoding
373 # behavior.
374 data = StringIO.StringIO()
375 result = zipfile.ZipFile(data, 'w')
376
377 articleKeys = []
378 result.writestr('data/user.data',
379 self.getUserData(articleKeys).encode('utf-8'))
380 logging.info(' adding articles %s' % len(articleKeys))
381 images = []
382 for article in db.get(articleKeys):
383 article.ensureThumbnail()
384 path = 'data/' + article.key().name() + '.html'
385 result.writestr(path.encode('utf-8'),
386 article.content.encode('utf-8'))
387 if article.thumbnail:
388 path = 'data/' + article.key().name() + '.jpg'
389 result.writestr(path.encode('utf-8'), article.thumbnail)
390
391 result.close()
392 logging.info('writing CannedData.zip')
393 self.response.headers['Content-Type'] = 'multipart/x-zip'
394 disposition = 'attachment; filename=CannedData.zip'
395 self.response.headers['Content-Disposition'] = disposition
396 self.response.out.write(data.getvalue())
397 data.close()
398
399
400class SetDefaultFeeds(webapp.RequestHandler):
401
402 @login_required
403 def get(self):
404 user = users.get_current_user()
405 prefs = UserData.get_or_insert(user.user_id())
406
407 prefs.sections = [
408 db.Key.from_path('Section', 'user/17857667084667353155/label/Top'),
409 db.Key.from_path('Section',
410 'user/17857667084667353155/label/Design'),
411 db.Key.from_path('Section', 'user/17857667084667353155/label/Eco'),
412 db.Key.from_path('Section', 'user/17857667084667353155/label/Geek'),
413 db.Key.from_path('Section',
414 'user/17857667084667353155/label/Google'),
415 db.Key.from_path('Section',
416 'user/17857667084667353155/label/Seattle'),
417 db.Key.from_path('Section', 'user/17857667084667353155/label/Tech'),
418 db.Key.from_path('Section', 'user/17857667084667353155/label/Web')
419 ]
420
421 prefs.put()
422
423 self.redirect('/')
424
425
426class SetTestFeeds(webapp.RequestHandler):
427
428 @login_required
429 def get(self):
430 user = users.get_current_user()
431 prefs = UserData.get_or_insert(user.user_id())
432
433 sections = []
434 for i in range(3):
435 s1 = Section.get_or_insert('Test%d' % i)
436 s1.title = 'Section %d' % (i + 1)
437
438 feeds = []
439 for j in range(4):
440 label = '%d_%d' % (i, j)
441 f1 = Feed.get_or_insert('Test%s' % label)
442 f1.title = 'Feed %s' % label
443 f1.iconUrl = getFeedIcon('http://google.com')
444 f1.lastUpdated = 0
445 f1.put()
446 feeds.append(f1.key())
447
448 for k in range(8):
449 label = '%d_%d_%d' % (i, j, k)
450 a1 = Article.get_or_insert('Test%s' % label)
451 if a1.title is None:
452 a1.feed = f1
453 a1.title = 'Article %s' % label
454 a1.author = 'anon'
455 a1.content = 'Lorem ipsum something or other...'
456 a1.snippet = 'Lorem ipsum something or other...'
457 a1.thumbnail = None
458 a1.srcurl = ''
459 a1.date = 0
460
461 s1.feeds = feeds
462 s1.put()
463 sections.append(s1.key())
464
465 prefs.sections = sections
466 prefs.put()
467
468 self.redirect('/')
469
470
471class UserLoginHandler(webapp.RequestHandler):
472
473 @login_required
474 def get(self):
475 user = users.get_current_user()
476 prefs = UserData.get_or_insert(user.user_id())
477 if prefs.credentials:
478 http = prefs.credentials.authorize(httplib2.Http())
479
480 response, content = http.request(
481 '%s/subscription/list?output=json' % READER_API)
482 self.collectFeeds(prefs, content)
483 self.redirect('/')
484 else:
485 self.redirect('/login')
486
487 def collectFeeds(self, prefs, content):
488 data = json.loads(content)
489
490 queue_name = self.request.get('queue_name', 'priority-queue')
491 sections = {}
492 for feedData in data['subscriptions']:
493 feed = Feed.get_or_insert(feedData['id'])
494 feed.put()
495 category = feedData['categories'][0]
496 categoryId = category['id']
497 if not sections.has_key(categoryId):
498 sections[categoryId] = (category['label'], [])
499
500 # TODO(jimhug): Use Reader preferences to sort feeds in a section.
501 sections[categoryId][1].append(feed.key())
502
503 # Kick off a high priority feed update
504 taskqueue.add(url='/update/feed',
505 queue_name=queue_name,
506 params={'id': feed.key().name()})
507
508 sectionKeys = []
509 for name, (title, feeds) in sections.items():
510 section = Section.get_or_insert(name)
511 section.feeds = feeds
512 section.title = title
513 section.put()
514 # Forces Top to be the first section
515 if title == 'Top': title = '0Top'
516 sectionKeys.append((title, section.key()))
517
518 # TODO(jimhug): Use Reader preferences API to get users true sort order.
519 prefs.sections = [key for t, key in sorted(sectionKeys)]
520 prefs.put()
521
522
523class AllFeedsCollector(webapp.RequestHandler):
524 '''Ensures that a given feed object is locally up to date.'''
525
526 def post(self):
527 return self.get()
528
529 def get(self):
530 queue_name = self.request.get('queue_name', 'background')
531 for feed in Feed.all():
532 taskqueue.add(url='/update/feed',
533 queue_name=queue_name,
534 params={'id': feed.key().name()})
535
536
537UPDATE_COUNT = 4 # The number of articles to request on periodic updates.
538INITIAL_COUNT = 40 # The number of articles to get first for a new queue.
539SNIPPET_SIZE = 180 # The length of plain-text snippet to extract.
540
541
542class FeedCollector(webapp.RequestHandler):
543
544 def post(self):
545 return self.get()
546
547 def get(self):
548 feedId = self.request.get('id')
549 feed = Feed.get_or_insert(feedId)
550
551 if feed.lastUpdated is None:
552 self.fetchn(feed, feedId, INITIAL_COUNT)
553 else:
554 self.fetchn(feed, feedId, UPDATE_COUNT)
555
556 self.response.headers['Content-Type'] = "text/plain"
557
558 def fetchn(self, feed, feedId, n, continuation=None):
559 # basic pattern is to read by ARTICLE_COUNT until we hit existing.
560 if continuation is None:
561 apiUrl = '%s/stream/contents/%s?n=%d' % (READER_API, feedId, n)
562 else:
563 apiUrl = '%s/stream/contents/%s?n=%d&c=%s' % (READER_API, feedId, n,
564 continuation)
565
566 logging.info('fetching: %s' % apiUrl)
567 result = urlfetch.fetch(apiUrl)
568
569 if result.status_code == 200:
570 data = json.loads(result.content)
571 collectFeed(feed, data, continuation)
572 elif result.status_code == 401:
573 self.response.out.write('<pre>%s</pre>' % result.content)
574 else:
575 self.response.out.write(result.status_code)
576
577
579 for section in Section.all():
580 if section.fixedTitle() == title:
581 return section
582 return None
583
584
585def collectFeed(feed, data, continuation=None):
586 '''
587 Reads a feed from the given JSON object and populates the given feed object
588 in the datastore with its data.
589 '''
590 if continuation is None:
591 if 'alternate' in data:
592 feed.iconUrl = getFeedIcon(data['alternate'][0]['href'])
593 feed.title = data['title']
594 feed.lastUpdated = data['updated']
595
596 articles = data['items']
597 logging.info('%d new articles for %s' % (len(articles), feed.title))
598
599 for articleData in articles:
600 if not collectArticle(feed, articleData):
601 feed.put()
602 return False
603
604 if len(articles) > 0 and data.has_key('continuation'):
605 logging.info('would have looked for more articles')
606 # TODO(jimhug): Enable this continuation check when more robust
607 #self.fetchn(feed, feedId, data['continuation'])
608
609 feed.ensureEncodedFeed(force=True)
610 feed.put()
611 return True
612
613
614def collectArticle(feed, data):
615 '''
616 Reads an article from the given JSON object and populates the datastore with
617 it.
618 '''
619 if not 'title' in data:
620 # Skip this articles without titles
621 return True
622
623 articleId = data['id']
624 article = Article.get_or_insert(articleId)
625 # TODO(jimhug): This aborts too early - at lease for one adafruit case.
626 if article.date == data['published']:
627 logging.info('found existing, aborting: %r, %r' %
628 (articleId, article.date))
629 return False
630
631 if data.has_key('content'):
632 content = data['content']['content']
633 elif data.has_key('summary'):
634 content = data['summary']['content']
635 else:
636 content = ''
637 #TODO(jimhug): better summary?
638 article.content = content
639 article.date = data['published']
640 article.title = unescape(data['title'])
641 article.snippet = unescape(strip_tags(content)[:SNIPPET_SIZE])
642
643 article.feed = feed
644
645 # TODO(jimhug): make this canonical so UX can change for this state
646 article.author = data.get('author', 'anonymous')
647
648 article.ensureThumbnail()
649
650 article.srcurl = ''
651 if data.has_key('alternate'):
652 for alt in data['alternate']:
653 if alt.has_key('href'):
654 article.srcurl = alt['href']
655 return True
656
657
658def unescape(html):
659 "Inverse of Django's utils.html.escape function"
660 if not isinstance(html, basestring):
661 html = str(html)
662 html = html.replace('&#39;', "'").replace('&quot;', '"')
663 return html.replace('&gt;', '>').replace('&lt;', '<').replace('&amp;', '&')
664
665
666def getFeedIcon(url):
667 url = urlparse.urlparse(url).netloc
668 return 'http://s2.googleusercontent.com/s2/favicons?domain=%s&alt=feed' % url
669
670
671def findImage(text):
672 img = findImgTag(text, 'jpg|jpeg|png')
673 if img is not None:
674 return img
675
676 img = findVideoTag(text)
677 if img is not None:
678 return img
679
680 img = findImgTag(text, 'gif')
681 return img
682
683
684def findImgTag(text, extensions):
685 m = re.search(r'src="(http://\S+\.(%s))(\?.*)?"' % extensions, text)
686 if m is None:
687 return None
688 return m.group(1)
689
690
691def findVideoTag(text):
692 # TODO(jimhug): Add other videos beyond youtube.
693 m = re.search(r'src="http://www.youtube.com/(\S+)/(\S+)[/|"]', text)
694 if m is None:
695 return None
696
697 return 'http://img.youtube.com/vi/%s/0.jpg' % m.group(2)
698
699
701 url = None
702 try:
703 url = findImage(text)
704 if url is None:
705 return None
706 return generateThumbnail(url)
707 except:
708 logging.info('error decoding: %s' % (url or text))
709 return None
710
711
713 logging.info('generating thumbnail: %s' % url)
714 thumbWidth, thumbHeight = THUMB_SIZE
715
716 result = urlfetch.fetch(url)
717 img = images.Image(result.content)
718
719 w, h = img.width, img.height
720
721 aspect = float(w) / h
722 thumbAspect = float(thumbWidth) / thumbHeight
723
724 if aspect > thumbAspect:
725 # Too wide, so crop on the sides.
726 normalizedCrop = (w - h * thumbAspect) / (2.0 * w)
727 img.crop(normalizedCrop, 0., 1. - normalizedCrop, 1.)
728 elif aspect < thumbAspect:
729 # Too tall, so crop out the bottom.
730 normalizedCrop = (h - w / thumbAspect) / h
731 img.crop(0., 0., 1., 1. - normalizedCrop)
732
733 img.resize(thumbWidth, thumbHeight)
734
735 # Chose JPEG encoding because informal experiments showed it generated
736 # the best size to quality ratio for thumbnail images.
737 nimg = img.execute_transforms(output_encoding=images.JPEG)
738 logging.info(' finished thumbnail: %s' % url)
739
740 return nimg
741
742
743class OAuthHandler(webapp.RequestHandler):
744
745 @login_required
746 def get(self):
747 user = users.get_current_user()
748 flow = pickle.loads(memcache.get(user.user_id()))
749 if flow:
750 prefs = UserData.get_or_insert(user.user_id())
751 prefs.credentials = flow.step2_exchange(self.request.params)
752 prefs.put()
753 self.redirect('/update/user')
754 else:
755 pass
756
757
758def main():
759 application = webapp.WSGIApplication(
760 [
761 ('/data/(.*)', DataHandler),
762
763 # This is called periodically from cron.yaml.
764 ('/update/allFeeds', AllFeedsCollector),
765 ('/update/feed', FeedCollector),
766 ('/update/user', UserLoginHandler),
767 ('/update/defaultFeeds', SetDefaultFeeds),
768 ('/update/testFeeds', SetTestFeeds),
769 ('/update/html', UpdateHtml),
770 ('/update/upload', UploadFeed),
771 ('/oauth2callback', OAuthHandler),
772 ('/', TopHandler),
773 ('/(.*)', MainHandler),
774 ],
775 debug=True)
776 webapp.util.run_wsgi_app(application)
777
778
779if __name__ == '__main__':
780 main()
static void encode(uint8_t output[16], const uint32_t input[4])
Definition SkMD5.cpp:240
ensureThumbnail(self)
Definition main.py:110
encodeHeader(self)
Definition main.py:117
getUserData(self, articleKeys=None)
Definition main.py:326
canDataZip(self)
Definition main.py:371
canData(self)
Definition main.py:343
get(self, name)
Definition main.py:295
fetchn(self, feed, feedId, n, continuation=None)
Definition main.py:558
ensureEncodedFeed(self, force=False)
Definition main.py:72
iconUrl
Definition main.py:66
topArticles
Definition main.py:70
encode(self, enc, maxArticles, articleSet)
Definition main.py:82
encodedFeed3
Definition main.py:69
handleUpload(self)
Definition main.py:248
handleDev(self)
Definition main.py:243
get(self, name)
Definition main.py:190
handleLogin(self)
Definition main.py:225
fixedTitle(self)
Definition main.py:49
encode(self, enc, articleKeys=None)
Definition main.py:52
get(self)
Definition main.py:176
post(self)
Definition main.py:140
post(self)
Definition main.py:256
getEncodedData(self, articleKeys=None)
Definition main.py:35
collectFeeds(self, prefs, content)
Definition main.py:487
static void append(char **dst, size_t *count, const char *src, size_t n)
Definition editor.cpp:211
const char * name
Definition fuchsia.cc:50
static float min(float r, float g, float b)
Definition hsl.cpp:48
Definition main.py:1
unescape(html)
Definition main.py:658
makeThumbnail(text)
Definition main.py:700
findImgTag(text, extensions)
Definition main.py:684
main()
Definition main.py:758
findVideoTag(text)
Definition main.py:691
findImage(text)
Definition main.py:671
findSectionByTitle(title)
Definition main.py:578
collectArticle(feed, data)
Definition main.py:614
collectFeed(feed, data, continuation=None)
Definition main.py:585
generateThumbnail(url)
Definition main.py:712
getFeedIcon(url)
Definition main.py:666