├── .gitignore ├── .gitmodules ├── README.md ├── TODO ├── app.py ├── bin ├── _run_coverage_tests.py ├── _run_tests.py ├── redis2mongo.py ├── run_shell.py ├── run_tests.sh └── send_pickled_messages.py ├── celeryconfig.py ├── handlers.py ├── here.py ├── models.py ├── requirements.txt ├── settings.py ├── socketapp.py ├── static ├── bookmarklet.js ├── css │ └── style.css ├── fancybox │ ├── blank.gif │ ├── fancy_close.png │ ├── fancy_loading.png │ ├── fancy_nav_left.png │ ├── fancy_nav_right.png │ ├── fancy_shadow_e.png │ ├── fancy_shadow_n.png │ ├── fancy_shadow_ne.png │ ├── fancy_shadow_nw.png │ ├── fancy_shadow_s.png │ ├── fancy_shadow_se.png │ ├── fancy_shadow_sw.png │ ├── fancy_shadow_w.png │ ├── fancy_title_left.png │ ├── fancy_title_main.png │ ├── fancy_title_over.png │ ├── fancy_title_right.png │ ├── fancybox-x.png │ ├── fancybox-y.png │ ├── fancybox.png │ ├── jquery.easing-1.3.pack.js │ ├── jquery.fancybox-1.3.4.css │ ├── jquery.fancybox-1.3.4.js │ ├── jquery.fancybox-1.3.4.pack.js │ └── jquery.mousewheel-3.0.4.pack.js ├── images │ ├── chuck.jpg │ ├── favicon.ico │ ├── link.png │ ├── loading.gif │ ├── rock.jpg │ ├── screengrab.png │ ├── screengrab2.png │ ├── screenshots │ │ ├── bookmarklet-in-toolbar.png │ │ ├── bookmarklet-in-toolbar_small.png │ │ ├── everyone.png │ │ ├── everyone_small.png │ │ ├── follows-me.png │ │ ├── follows-me_small.png │ │ ├── lookups.png │ │ ├── lookups_small.png │ │ ├── on-twitter.png │ │ ├── on-twitter_small.png │ │ ├── too-cool.png │ │ └── too-cool_small.png │ ├── twitter-small-mirrored.png │ ├── twitter-small.png │ └── twitter.png └── js │ ├── chart.js │ ├── lookups.js │ ├── socket.io.js │ └── socket.io.min.js ├── tasks.py ├── templates ├── base.html ├── coolest.html ├── error.html ├── everyone.html ├── following.html ├── following_compared_missing.html ├── following_error.html ├── home.html ├── lookups.html ├── screenshots.html ├── test.html └── twitter_auth_failed.html ├── tests ├── __init__.py ├── base.py ├── test_handlers.py └── test_models.py ├── ui_modules.py ├── utils.py └── vendor └── vendor.pth /.gitignore: -------------------------------------------------------------------------------- 1 | local_settings.py 2 | utils/send_mail/pickles/ 3 | dump.rdb 4 | coverage_report/ 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/src/tornado-utils"] 2 | path = vendor/src/tornado-utils 3 | url = git://github.com/peterbe/tornado-utils.git 4 | [submodule "vendor/src/MongoLite"] 5 | path = vendor/src/MongoLite 6 | url = git://github.com/namlook/MongoLite.git 7 | [submodule "vendor/src/tornado"] 8 | path = vendor/src/tornado 9 | url = git://github.com/facebook/tornado.git 10 | [submodule "vendor/src/tornadio2"] 11 | path = vendor/src/tornadio2 12 | url = git://github.com/MrJoes/tornadio2.git 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | toocool 2 | ======= 3 | 4 | A web service for finding out who follows and does not follow you on 5 | Twitter. 6 | 7 | 8 | 9 | Running tests 10 | ------------- 11 | 12 | Run this: 13 | 14 | $ python bin/_run_tests.py --logging=error 15 | 16 | 17 | Running celeryd 18 | --------------- 19 | 20 | Run celeryd like this: 21 | 22 | $ celeryd --loglevel=INFO 23 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | * Do it for this page too 2 | https://twitter.com/#!/dotton/following 3 | 4 | * Override log_request in base handler so that only errors and 5 | warnings are actually logged. 6 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import re 4 | import here 5 | import tornado.httpserver 6 | import tornado.ioloop 7 | import tornado.options 8 | import tornado.web 9 | import redis.client 10 | from tornado.options import define, options 11 | from tornado_utils.routes import route 12 | import handlers 13 | import settings 14 | 15 | 16 | define("debug", default=False, help="run in debug mode", type=bool) 17 | define("database_name", default=settings.DATABASE_NAME, help="db name") 18 | define("port", default=8000, help="run on the given port", type=int) 19 | 20 | 21 | class Application(tornado.web.Application): 22 | def __init__(self, database_name=None): 23 | _ui_modules = __import__('ui_modules', globals(), locals(), ['ui_modules'], -1) 24 | ui_modules_map = {} 25 | for name in [x for x in dir(_ui_modules) if re.findall('[A-Z]\w+', x)]: 26 | thing = getattr(_ui_modules, name) 27 | try: 28 | if issubclass(thing, tornado.web.UIModule): 29 | ui_modules_map[name] = thing 30 | except TypeError: # pragma: no cover 31 | # most likely a builtin class or something 32 | pass 33 | 34 | routed_handlers = route.get_routes() 35 | app_settings = dict( 36 | title=settings.PROJECT_TITLE, 37 | template_path=os.path.join(os.path.dirname(__file__), "templates"), 38 | static_path=os.path.join(os.path.dirname(__file__), "static"), 39 | cookie_secret=settings.COOKIE_SECRET, 40 | debug=options.debug, 41 | email_backend=options.debug and \ 42 | 'tornado_utils.send_mail.backends.console.EmailBackend' \ 43 | or 'tornado_utils.send_mail.backends.pickle.EmailBackend', 44 | admin_emails=settings.ADMIN_EMAILS, 45 | ui_modules=ui_modules_map, 46 | twitter_consumer_key=settings.TWITTER_CONSUMER_KEY, 47 | twitter_consumer_secret=settings.TWITTER_CONSUMER_SECRET, 48 | ) 49 | if 1 or not options.debug: 50 | routed_handlers.append( 51 | tornado.web.url('/.*?', handlers.PageNotFoundHandler, 52 | name='page_not_found') 53 | ) 54 | super(Application, self).__init__(routed_handlers, **app_settings) 55 | 56 | self.redis = redis.client.Redis(settings.REDIS_HOST, 57 | settings.REDIS_PORT) 58 | 59 | from models import connection 60 | self.db = connection[database_name or settings.DATABASE_NAME] 61 | 62 | 63 | 64 | def main(): # pragma: no cover 65 | tornado.options.parse_command_line() 66 | http_server = tornado.httpserver.HTTPServer(Application()) 67 | print "Starting tornado on port", options.port 68 | http_server.listen(options.port) 69 | try: 70 | tornado.ioloop.IOLoop.instance().start() 71 | except KeyboardInterrupt: 72 | pass 73 | 74 | 75 | if __name__ == "__main__": # pragma: no cover 76 | main() 77 | -------------------------------------------------------------------------------- /bin/_run_coverage_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | import coverage 4 | 5 | from _run_tests import TEST_MODULES 6 | 7 | COVERAGE_MODULES = [ 8 | 'app', 9 | 'handlers', 10 | 'models', 11 | ] 12 | 13 | def all(): 14 | return unittest.defaultTestLoader.loadTestsFromNames(TEST_MODULES) 15 | 16 | if __name__ == '__main__': 17 | import tornado.testing 18 | 19 | cov = coverage.coverage() 20 | cov.use_cache(0) # Do not cache any of the coverage.py stuff 21 | cov.start() 22 | 23 | try: 24 | tornado.testing.main() 25 | except SystemExit, e: 26 | if e.code: 27 | # go ahead and raise the exit :( 28 | raise 29 | 30 | cov.stop() 31 | print '' 32 | print '----------------------------------------------------------------------' 33 | print ' Unit Test Code Coverage Results' 34 | print '----------------------------------------------------------------------' 35 | 36 | # Report code coverage metrics 37 | coverage_modules = [] 38 | for module in COVERAGE_MODULES: 39 | coverage_modules.append(__import__(module, globals(), locals(), [''])) 40 | cov.report(coverage_modules, show_missing=1) 41 | cov.html_report(coverage_modules, directory='coverage_report') 42 | # Print code metrics footer 43 | print '----------------------------------------------------------------------' 44 | -------------------------------------------------------------------------------- /bin/_run_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import unittest 4 | import site 5 | 6 | ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 7 | path = lambda *a: os.path.join(ROOT,*a) 8 | 9 | site.addsitedir(path('.')) 10 | site.addsitedir(path('vendor')) 11 | 12 | TEST_MODULES = [ 13 | 'tests.test_handlers', 14 | 'tests.test_models', 15 | ] 16 | 17 | 18 | def all(): 19 | try: 20 | return unittest.defaultTestLoader.loadTestsFromNames(TEST_MODULES) 21 | except AttributeError, e: 22 | if "'module' object has no attribute 'test_handlers'" in str(e): 23 | # most likely because of an import error 24 | for m in TEST_MODULES: 25 | __import__(m, globals(), locals()) 26 | raise 27 | 28 | 29 | if __name__ == '__main__': 30 | import tornado.testing 31 | #import cProfile, pstats 32 | #cProfile.run('tornado.testing.main()') 33 | try: 34 | tornado.testing.main() 35 | except KeyboardInterrupt: 36 | pass # exit 37 | -------------------------------------------------------------------------------- /bin/redis2mongo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import site 4 | 5 | ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 6 | path = lambda *a: os.path.join(ROOT,*a) 7 | 8 | site.addsitedir(path('.')) 9 | site.addsitedir(path('vendor')) 10 | 11 | import redis as redis_ 12 | 13 | def run(): 14 | import settings 15 | from models import connection 16 | db = connection[settings.DATABASE_NAME] 17 | redis = redis_.client.Redis(settings.REDIS_HOST, 18 | settings.REDIS_PORT) 19 | 20 | for username in redis.smembers('allusernames'): 21 | key = 'access_tokens:%s' % username 22 | access_token = redis.get(key) 23 | if access_token: 24 | user = db.User.find_one({'username': username}) 25 | if not user: 26 | user = db.User() 27 | user['username'] = username 28 | user['access_token'] = access_token 29 | user.save() 30 | 31 | if __name__ == '__main__': 32 | run() 33 | -------------------------------------------------------------------------------- /bin/run_shell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import code, re 4 | try: 5 | import here 6 | except ImportError: 7 | import sys 8 | import os.path as op 9 | sys.path.insert(0, op.abspath(op.join(op.dirname(__file__), '..'))) 10 | import here 11 | 12 | if __name__ == '__main__': 13 | 14 | 15 | from models import User, Tweeter, connection 16 | from bson.objectid import InvalidId, ObjectId 17 | 18 | import settings 19 | db = connection[settings.DATABASE_NAME] 20 | print "AVAILABLE:" 21 | print '\n'.join(['\t%s'%x for x in locals().keys() 22 | if re.findall('[A-Z]\w+|db|con', x)]) 23 | print "Database available as 'db'" 24 | code.interact(local=locals()) 25 | -------------------------------------------------------------------------------- /bin/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python bin/_run_tests.py --logging=error $@ 3 | -------------------------------------------------------------------------------- /bin/send_pickled_messages.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | try: 3 | import here 4 | except ImportError: 5 | import sys, os 6 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 7 | import here 8 | 9 | import time 10 | import cPickle 11 | import os.path 12 | import logging 13 | from glob import glob 14 | import tornado.options 15 | from tornado.options import define, options 16 | 17 | define("verbose", default=False, help="be louder", type=bool) 18 | define("debug", default=False, help="run in debug mode", type=bool) 19 | define("dry_run", default=False, help="print messages to send", type=bool) 20 | 21 | def main(): 22 | from tornado_utils.send_mail import config 23 | filenames = glob(os.path.join(config.PICKLE_LOCATION, '*.pickle')) 24 | filenames.sort() 25 | if not filenames: 26 | return 27 | 28 | t0 = time.time() 29 | tornado.options.parse_command_line() 30 | if options.debug or options.dry_run: 31 | from tornado_utils.send_mail.backends.console import EmailBackend 32 | else: 33 | from tornado_utils.send_mail.backends.smtp import EmailBackend 34 | max_count = 10 35 | filenames = filenames[:max_count] 36 | messages = [cPickle.load(open(x, 'rb')) for x in filenames] 37 | backend = EmailBackend() 38 | backend.send_messages(messages) 39 | if not options.dry_run: 40 | for filename in filenames: 41 | if options.verbose: 42 | print "SENT", filename 43 | os.remove(filename) 44 | t1 = time.time() 45 | if options.verbose: 46 | print ("Sent %s messages in %s seconds" % 47 | (len(filenames), t1 - t0)) 48 | 49 | if __name__ == "__main__": 50 | main() 51 | -------------------------------------------------------------------------------- /celeryconfig.py: -------------------------------------------------------------------------------- 1 | import here # so that the vendor lib is available later 2 | 3 | # http://docs.celeryproject.org/en/latest/tutorials/otherqueues.html#redis 4 | BROKER_TRANSPORT = "redis" 5 | 6 | import settings 7 | BROKER_HOST = settings.REDIS_HOST 8 | BROKER_PORT = settings.REDIS_PORT 9 | BROKER_VHOST = "0" # Maps to database number. 10 | 11 | CELERY_IGNORE_RESULT = True 12 | 13 | CELERY_IMPORTS = ("tasks", ) 14 | 15 | import os 16 | CELERY_ALWAYS_EAGER = bool(os.environ.get('ALWAYS_EAGER', False)) 17 | -------------------------------------------------------------------------------- /handlers.py: -------------------------------------------------------------------------------- 1 | import re 2 | import datetime 3 | import random 4 | import os 5 | import logging 6 | import time 7 | from pprint import pprint, pformat 8 | import tornado.auth 9 | import tornado.web 10 | import tornado.gen 11 | from tornado.web import HTTPError 12 | from tornado_utils.routes import route 13 | from tornado_utils.send_mail import send_email 14 | from tornado.escape import json_decode, json_encode 15 | from bson.objectid import InvalidId, ObjectId 16 | import utils 17 | import tasks 18 | from models import User, Tweeter 19 | 20 | 21 | class BaseHandler(tornado.web.RequestHandler): 22 | 23 | def write_json(self, struct, javascript=False): 24 | self.set_header("Content-Type", "application/json; charset=UTF-8") 25 | self.write(tornado.escape.json_encode(struct)) 26 | 27 | def write_jsonp(self, callback, struct): 28 | self.set_header("Content-Type", "text/javascript; charset=UTF-8") 29 | self.write('%s(%s)' % (callback, tornado.escape.json_encode(struct))) 30 | 31 | def get_current_user(self): 32 | _id = self.get_secure_cookie('user') 33 | if _id: 34 | try: 35 | return self.db.User.find_one({'_id': ObjectId(_id)}) 36 | except InvalidId: # pragma: no cover 37 | return self.db.User.find_one({'username': _id}) 38 | 39 | @property 40 | def redis(self): 41 | return self.application.redis 42 | 43 | @property 44 | def db(self): 45 | return self.application.db 46 | 47 | def save_following(self, source_username, dest_username, result): 48 | assert isinstance(result, bool) 49 | following = (self.db.Following 50 | .find_one({'user': source_username, 51 | 'follows': dest_username})) 52 | if not following: 53 | following = self.db.Following() 54 | following['user'] = source_username 55 | following['follows'] = dest_username 56 | 57 | if result != following['following']: 58 | following['following'] = result 59 | following.save() 60 | 61 | def save_tweeter_user(self, user): 62 | user_id = user['id'] 63 | tweeter = self.db.Tweeter.find_one({'user_id': user_id}) 64 | if not tweeter: 65 | tweeter = self.db.Tweeter() 66 | tweeter['user_id'] = user_id 67 | Tweeter.update_tweeter(tweeter, user) 68 | return tweeter 69 | 70 | def assert_tweeter_user(self, user): 71 | user_id = user['id'] 72 | tweeter = self.db.Tweeter.find_one({'user_id': user_id}) 73 | if not tweeter: 74 | return self.save_tweeter_user(user) 75 | return tweeter 76 | 77 | BACKGROUND_IMAGES = [ 78 | '/static/images/chuck.jpg', 79 | '/static/images/rock.jpg', 80 | ] 81 | 82 | def render(self, template, **options): 83 | background_image = self.redis.get('background_image') 84 | if not background_image: 85 | background_image = random.choice(self.BACKGROUND_IMAGES) 86 | self.redis.setex('background_image', background_image, 60) 87 | options['background_image'] = background_image 88 | return tornado.web.RequestHandler.render(self, template, **options) 89 | 90 | def write_error(self, status_code, **kwargs): 91 | if status_code >= 500 and not self.application.settings['debug']: 92 | if self.application.settings['admin_emails']: 93 | try: 94 | self._email_exception(status_code, *kwargs['exc_info']) 95 | except: 96 | logging.error("Failing to email exception", exc_info=True) 97 | else: 98 | logging.warn("No ADMIN_EMAILS set up in settings to " 99 | "email exception") 100 | if self.application.settings['debug']: 101 | super(BaseHandler, self).write_error(status_code, **kwargs) 102 | else: 103 | options = dict( 104 | status_code=status_code, 105 | err_type=kwargs['exc_info'][0], 106 | err_value=kwargs['exc_info'][1], 107 | err_traceback=kwargs['exc_info'][2], 108 | ) 109 | self.render("error.html", **options) 110 | 111 | def _email_exception(self, status_code, err_type, err_val, err_traceback): 112 | import traceback 113 | from cStringIO import StringIO 114 | from pprint import pprint 115 | out = StringIO() 116 | subject = "%r on %s" % (err_val, self.request.path) 117 | out.write("TRACEBACK:\n") 118 | traceback.print_exception(err_type, err_val, err_traceback, 500, out) 119 | traceback_formatted = out.getvalue() 120 | #print traceback_formatted 121 | out.write("\nREQUEST ARGUMENTS:\n") 122 | arguments = self.request.arguments 123 | if arguments.get('password') and arguments['password'][0]: 124 | password = arguments['password'][0] 125 | arguments['password'] = password[:2] + '*' * (len(password) -2) 126 | pprint(arguments, out) 127 | out.write("\nCOOKIES:\n") 128 | for cookie in self.cookies: 129 | out.write("\t%s: " % cookie) 130 | out.write("%r\n" % self.get_secure_cookie(cookie)) 131 | 132 | out.write("\nREQUEST:\n") 133 | for key in ('full_url', 'protocol', 'query', 'remote_ip', 134 | 'request_time', 'uri', 'version'): 135 | out.write(" %s: " % key) 136 | value = getattr(self.request, key) 137 | if callable(value): 138 | try: 139 | value = value() 140 | except: 141 | pass 142 | out.write("%r\n" % value) 143 | 144 | out.write("\nHEADERS:\n") 145 | pprint(dict(self.request.headers), out) 146 | try: 147 | send_email(self.application.settings['email_backend'], 148 | subject, 149 | out.getvalue(), 150 | self.application.settings['admin_emails'][0], 151 | self.application.settings['admin_emails'], 152 | ) 153 | except: 154 | logging.error("Failed to send email", 155 | exc_info=True) 156 | 157 | 158 | @route('/') 159 | class HomeHandler(BaseHandler): 160 | 161 | def get(self): 162 | options = { 163 | 'page_title': 'Too Cool for Me?', 164 | } 165 | user = self.get_current_user() 166 | if user: 167 | url = '/static/bookmarklet.js' 168 | url = '%s://%s%s' % (self.request.protocol, 169 | self.request.host, 170 | url) 171 | options['full_bookmarklet_url'] = url 172 | 173 | options['user'] = user 174 | self.render('home.html', **options) 175 | 176 | 177 | @route('/json', name='json') 178 | @route('/jsonp', name='jsonp') 179 | class FollowsHandler(BaseHandler, tornado.auth.TwitterMixin): 180 | 181 | def increment_lookup_count(self, username, usernames, jsonp=False): 182 | if jsonp: 183 | key = 'lookups:jsonp' 184 | else: 185 | key = 'lookups:json' 186 | if not isinstance(usernames, int): 187 | usernames = len(usernames) 188 | self.redis.publish('lookups', tornado.escape.json_encode({ 189 | key: int(self.redis.get(key) or 0) 190 | })) 191 | key = 'lookups:username:%s' % username 192 | assert username 193 | self.redis.incr(key) 194 | 195 | key = 'lookups:usernames' 196 | self.redis.incr(key, usernames) 197 | 198 | self.redis.publish('lookups', tornado.escape.json_encode({ 199 | key: int(self.redis.get(key)) 200 | 201 | })) 202 | 203 | @tornado.web.asynchronous 204 | @tornado.gen.engine 205 | def get(self): 206 | jsonp = 'jsonp' in self.request.path 207 | 208 | if (self.get_argument('username', None) and 209 | not self.get_argument('usernames', None)): 210 | usernames = self.get_arguments('username') 211 | else: 212 | usernames = self.get_arguments('usernames') 213 | if isinstance(usernames, basestring): 214 | usernames = [usernames] 215 | elif (isinstance(usernames, list) 216 | and len(usernames) == 1 217 | and ',' in usernames[0]): 218 | usernames = [x.strip() for x in 219 | usernames[0].split(',') 220 | if x.strip()] 221 | # make sure it's a unique list 222 | usernames = set(usernames) 223 | 224 | if jsonp: 225 | self.jsonp = self.get_argument('callback', 'callback') 226 | else: 227 | self.jsonp = False 228 | 229 | if not usernames: 230 | msg = {'ERROR': 'No usernames asked for'} 231 | if jsonp: 232 | self.write_jsonp(self.jsonp, msg) 233 | else: 234 | self.write_json(msg) 235 | self.finish() 236 | return 237 | 238 | # All of this is commented out until I can figure out why cookie 239 | # headers aren't sent from bookmarklet's AJAX code 240 | this_username = self.get_argument('you', None) 241 | access_token = None 242 | if this_username is not None: 243 | user = self.db.User.find_one({'username': this_username}) 244 | if user: 245 | access_token = user['access_token'] 246 | else: 247 | user = self.get_current_user() 248 | if user: 249 | this_username = user['username'] 250 | access_token = user['access_token'] 251 | 252 | if not access_token: 253 | msg = {'ERROR': ('Not authorized. Go to http://%s and sign in' % 254 | self.request.host)} 255 | if self.jsonp: 256 | self.write_jsonp(self.jsonp, msg) 257 | else: 258 | self.write_json(msg) 259 | self.finish() 260 | return 261 | 262 | self.increment_lookup_count(this_username, len(usernames), jsonp=jsonp) 263 | 264 | results = {} 265 | # pick some up already from the cache 266 | _drop = set() 267 | for username in usernames: 268 | key = 'follows:%s:%s' % (this_username, username) 269 | value = self.redis.get(key) 270 | if value is not None: 271 | results[username] = bool(int(value)) 272 | _drop.add(username) 273 | usernames -= _drop 274 | 275 | if len(usernames) == 1: 276 | username = list(usernames)[0] 277 | # See https://dev.twitter.com/docs/api/1/get/friendships/show 278 | 279 | result = yield tornado.gen.Task(self.twitter_request, 280 | "/friendships/show", 281 | source_screen_name=this_username, 282 | target_screen_name=username, 283 | access_token=access_token) 284 | self._on_show(result, this_username, username, results) 285 | elif usernames: 286 | if len(usernames) > 100: 287 | raise HTTPError(400, "Too many usernames to look up (max 100)") 288 | # See https://dev.twitter.com/docs/api/1/get/friendships/lookup 289 | result = None 290 | attempts = 0 291 | while result is None: 292 | result = yield tornado.gen.Task(self.twitter_request, 293 | "/friendships/lookup", 294 | screen_name=','.join(usernames), 295 | access_token=access_token) 296 | if result is not None: 297 | break 298 | else: 299 | attempts += 1 300 | time.sleep(1) 301 | if attempts > 2: 302 | result = { 303 | 'ERROR': 'Unable to look up friendships on Twitter' 304 | } 305 | if self.jsonp: 306 | self.write_jsonp(self.jsonp, result) 307 | else: 308 | self.write_json(result) 309 | self.finish() 310 | return 311 | 312 | self._on_lookup(result, this_username, results) 313 | else: 314 | # all usernames were lookup'able by cache 315 | if self.jsonp: 316 | self.write_jsonp(self.jsonp, results) 317 | else: 318 | self.write_json(results) 319 | self.finish() 320 | 321 | def _on_lookup(self, result, this_username, data): 322 | for each in result: 323 | if 'followed_by' in each['connections']: 324 | data[each['screen_name']] = True 325 | else: 326 | data[each['screen_name']] = False 327 | key = 'follows:%s:%s' % (this_username, each['screen_name']) 328 | self.redis.setex(key, int(data[each['screen_name']]), 60 * 5) 329 | self.save_following(each['screen_name'], this_username, 330 | bool(data[each['screen_name']])) 331 | 332 | if self.jsonp: 333 | self.write_jsonp(self.jsonp, data) 334 | else: 335 | self.write_json(data) 336 | self.finish() 337 | 338 | def _on_show(self, result, this_username, username, data): 339 | target_follows = None 340 | if result and 'relationship' in result: 341 | target_follows = result['relationship']['target']['following'] 342 | key = 'follows:%s:%s' % (this_username, username) 343 | if target_follows is not None: 344 | self.redis.setex(key, int(bool(target_follows)), 60) 345 | self.save_following(username, this_username, bool(target_follows)) 346 | data[username] = target_follows 347 | if self.jsonp: 348 | self.write_jsonp(self.jsonp, data) 349 | else: 350 | self.write_json(data) 351 | self.finish() 352 | 353 | 354 | class BaseAuthHandler(BaseHandler): 355 | 356 | def get_next_url(self): 357 | return '/' 358 | 359 | 360 | @route('/auth/twitter/', name='auth_twitter') 361 | class TwitterAuthHandler(BaseAuthHandler, tornado.auth.TwitterMixin): 362 | 363 | def increment_authentication_count(self, username): 364 | key = 'auths:username:%s' % username 365 | self.redis.incr(key) 366 | key = 'auths:total' 367 | self.redis.incr(key) 368 | self.redis.publish('lookups', tornado.escape.json_encode({ 369 | key: int(self.redis.get(key)) 370 | })) 371 | 372 | @tornado.web.asynchronous 373 | def get(self): 374 | if self.get_argument("oauth_token", None): 375 | self.get_authenticated_user(self.async_callback(self._on_auth)) 376 | return 377 | self.authenticate_redirect() 378 | 379 | def _on_auth(self, user_struct): 380 | if not user_struct: 381 | options = {} 382 | options['page_title'] = "Twitter authentication failed" 383 | self.render('twitter_auth_failed.html', **options) 384 | return 385 | 386 | username = user_struct.get('username', 387 | user_struct.get('screen_name')) 388 | access_token = user_struct['access_token'] 389 | assert access_token 390 | user = self.db.User.find_one({'username': username}) 391 | if user is None: 392 | user = self.db.User() 393 | user['username'] = username 394 | user['access_token'] = access_token 395 | user.save() 396 | 397 | self.increment_authentication_count(username) 398 | 399 | self.set_secure_cookie("user", 400 | str(user['_id']), 401 | expires_days=30, path='/') 402 | 403 | self.save_tweeter_user(user_struct) 404 | self.redirect('/') 405 | 406 | 407 | @route(r'/auth/logout/', name='logout') 408 | class AuthLogoutHandler(BaseAuthHandler): 409 | def get(self): 410 | self.clear_all_cookies() 411 | self.redirect(self.get_next_url()) 412 | 413 | 414 | @route(r'/test', name='test') 415 | class TestServiceHandler(BaseHandler): 416 | 417 | def get(self): 418 | options = {} 419 | user = self.get_current_user() 420 | if not user: 421 | self.redirect('/auth/twitter/') 422 | return 423 | options['user'] = user 424 | options['page_title'] = "Test the service" 425 | self.render('test.html', **options) 426 | 427 | 428 | @route('/following/(\w+)', name='following') 429 | class FollowingHandler(BaseHandler, tornado.auth.TwitterMixin): 430 | 431 | def get_following_perm_url(self, username, compared_to): 432 | base_url = self.request.host 433 | perm_url = self.reverse_url('following_compared', 434 | username, 435 | compared_to) 436 | return 'http://%s%s' % (base_url, perm_url) 437 | 438 | 439 | @tornado.web.asynchronous 440 | @tornado.gen.engine 441 | def get(self, username): 442 | options = { 443 | 'username': username, 444 | 'compared_to': None 445 | } 446 | current_user = self.get_current_user() 447 | if not current_user: 448 | self.redirect(self.reverse_url('auth_twitter')) 449 | return 450 | this_username = current_user['username'] 451 | options['this_username'] = this_username 452 | options['follows'] = None 453 | key = 'follows:%s:%s' % (this_username, username) 454 | value = self.redis.get(key) 455 | if value is None: 456 | access_token = current_user['access_token'] 457 | result = yield tornado.gen.Task(self.twitter_request, 458 | "/friendships/show", 459 | source_screen_name=this_username, 460 | target_screen_name=username, 461 | access_token=access_token) 462 | if result and 'relationship' in result: 463 | value = result['relationship']['target']['following'] 464 | self.save_following(username, this_username, value) 465 | else: 466 | result = bool(int(value)) 467 | key = None 468 | self._on_friendship(result, key, options) 469 | 470 | def _on_friendship(self, result, key, options): 471 | if result is None: 472 | options['error'] = ("Unable to look up friendship for %s" % 473 | options['username']) 474 | self._render(options) 475 | return 476 | 477 | if isinstance(result, bool): 478 | value = result 479 | else: 480 | if result and 'relationship' in result: 481 | value = result['relationship']['target']['following'] 482 | if key and value is not None: 483 | self.redis.setex(key, int(bool(value)), 60) 484 | options['follows'] = value 485 | self._fetch_info(options) 486 | 487 | @tornado.gen.engine 488 | def _fetch_info(self, options, username=None): 489 | if username is None: 490 | username = options['username'] 491 | 492 | def age(d): 493 | return (datetime.datetime.utcnow() - d).seconds 494 | 495 | tweeter = self.db.Tweeter.find_one({'username': username}) 496 | current_user = self.get_current_user() 497 | if not tweeter: 498 | access_token = current_user['access_token'] 499 | result = yield tornado.gen.Task(self.twitter_request, 500 | "/users/show", 501 | screen_name=username, 502 | access_token=access_token) 503 | if result: 504 | tweeter = self.save_tweeter_user(result) 505 | elif age(tweeter['modify_date']) > 3600: 506 | tasks.refresh_user_info.delay( 507 | username, current_user['access_token']) 508 | 509 | if not tweeter: 510 | options['error'] = "Unable to look up info for %s" % username 511 | self._render(options) 512 | return 513 | 514 | if 'info' not in options: 515 | options['info'] = {options['username']: tweeter} 516 | self._fetch_info(options, username=options['this_username']) 517 | else: 518 | options['info'][options['this_username']] = tweeter 519 | self._render(options) 520 | 521 | def _update_ratio_rank(self, tweeter): 522 | rank = tweeter.get('ratio_rank', None) 523 | # This should be re-calculated periodically 524 | if rank is None: 525 | rank = 0 526 | for each in (self.db.Tweeter 527 | .find(fields=('username',)) 528 | .sort('ratio', -1)): 529 | rank += 1 530 | if each['username'] == tweeter['username']: 531 | tweeter['ratio_rank'] = rank 532 | tweeter.save() 533 | break 534 | 535 | def _render(self, options): 536 | if 'error' in options: 537 | options['page_title'] = 'Error :(' 538 | self.render('following_error.html', **options) 539 | return 540 | 541 | if options['follows']: 542 | page_title = '%s follows me' 543 | else: 544 | page_title = '%s is too cool for me' 545 | options['page_title'] = page_title % options['username'] 546 | options['perm_url'] = self.get_following_perm_url( 547 | options['username'], 548 | options['this_username'] 549 | ) 550 | self.render('following.html', **options) 551 | 552 | def _set_ratio(self, options, key): 553 | value = options[key] 554 | followers = options['info'][value]['followers_count'] 555 | following = options['info'][value]['friends_count'] 556 | ratio = 1.0 * followers / max(following, 1) 557 | options['info'][value]['ratio'] = '%.1f' % ratio 558 | key = 'ratios' 559 | 560 | tweeter = self.db.Tweeter.find_by_username(self.db, value) 561 | assert tweeter 562 | rank = tweeter.get('ratio_rank', None) 563 | # This should be re-calculated periodically 564 | if rank is None: 565 | rank = 0 566 | for each in (self.db.Tweeter 567 | .find(fields=('username',)) 568 | .sort('ratio', -1)): 569 | rank += 1 570 | if each['username'] == value: 571 | tweeter['ratio_rank'] = rank 572 | tweeter.save() 573 | break 574 | options['info'][value]['rank'] = rank 575 | 576 | 577 | @route('/following/suggest_tweet.json', name='suggest_tweet') 578 | class SuggestTweetHandler(BaseHandler): 579 | 580 | def get(self): 581 | username = self.get_argument('username') 582 | 583 | if self.get_argument('compared_to', None): 584 | compared_to = self.get_argument('compared_to') 585 | else: 586 | current_user = self.get_current_user() 587 | if not current_user: 588 | raise HTTPError(403, "Not logged in") 589 | compared_to = current_user['username'] 590 | 591 | tweeter = self.db.Tweeter.find_by_username(self.db, username) 592 | if not tweeter: 593 | raise HTTPError(400, "Unknown tweeter %r" % username) 594 | compared_tweeter = self.db.Tweeter.find_by_username(self.db, compared_to) 595 | if not tweeter: 596 | raise HTTPError(400, "Unknown tweeter %r" % compared_to) 597 | 598 | if self.get_current_user() and self.get_current_user()['username'] == compared_to: 599 | different_user = False 600 | else: 601 | different_user = True 602 | 603 | def make_message(include_hashtag=False, include_fullname=False): 604 | if include_fullname: 605 | name = '@%s (%s)' % (username, fullname) 606 | else: 607 | name = '@%s' % username 608 | tweet = "Apparently " 609 | if abs(a - b) < 1.0: 610 | tweet += "%s is " % name 611 | tweet += "as cool as %s" % (compared_to if different_user else 'me') 612 | elif b > a: 613 | tweet += "%s am " % (compared_to if different_user else 'I') 614 | tweet += "%s times cooler than %s" % (get_times(a, b), name) 615 | elif a > b: 616 | tweet += "%s is " % name 617 | tweet += "%s times cooler than %s" % ( 618 | get_times(a, b), 619 | compared_to if different_user else 'me' 620 | ) 621 | 622 | hashtag = "#toocool" 623 | if include_hashtag: 624 | tweet += " %s" % hashtag 625 | 626 | return tweet 627 | 628 | def get_times(*numbers): 629 | small = min(numbers) 630 | big = max(numbers) 631 | bigger = round(big / small) 632 | if int(bigger) == 2: 633 | return "two" 634 | if int(bigger) == 3: 635 | return "three" 636 | return "about %s" % int(bigger) 637 | 638 | a, b = tweeter['ratio'], compared_tweeter['ratio'] 639 | fullname = tweeter['name'] 640 | 641 | tweet = make_message(include_hashtag=False, include_fullname=True) 642 | if len(tweet) > 140: 643 | tweet = make_message(include_hashtag=False, include_fullname=False) 644 | if len(tweet) > 140: 645 | tweet = make_message(include_hashtag=False, 646 | include_fullname=False) 647 | 648 | base_url = self.request.host 649 | perm_url = self.reverse_url('following_compared', 650 | username, 651 | compared_to) 652 | url = 'http://%s%s' % (base_url, perm_url) 653 | 654 | #self.write_json({'tweet': tweet}) 655 | self.write_json({'text': tweet, 'url': url}) 656 | 657 | 658 | @route('/following/(\w+)/vs/(\w+)', name='following_compared') 659 | class FollowingComparedtoHandler(FollowingHandler): 660 | 661 | @tornado.web.asynchronous 662 | @tornado.gen.engine 663 | def get(self, username, compared_to): 664 | options = {'compared_to': compared_to} 665 | tweeter = self.db.Tweeter.find_one({'username': username}) 666 | compared_tweeter = self.db.Tweeter.find_one({'username': compared_to}) 667 | 668 | def age(d): 669 | return (datetime.datetime.utcnow() - d).seconds 670 | 671 | 672 | current_user = self.get_current_user() 673 | if current_user: 674 | 675 | # if we don't have tweeter info on any of them, fetch it 676 | if not tweeter: 677 | # fetch it 678 | result = yield tornado.gen.Task(self.twitter_request, 679 | "/users/show", 680 | screen_name=username, 681 | access_token=current_user['access_token']) 682 | tweeter = self.save_tweeter_user(result) 683 | elif age(tweeter['modify_date']) > 3600: 684 | tasks.refresh_user_info.delay( 685 | username, current_user['access_token']) 686 | 687 | if not compared_tweeter: 688 | result = yield tornado.gen.Task(self.twitter_request, 689 | "/users/show", 690 | screen_name=compared_to, 691 | access_token=current_user['access_token']) 692 | compared_tweeter = self.save_tweeter_user(result) 693 | elif age(compared_tweeter['modify_date']) > 3600: 694 | tasks.refresh_user_info.delay( 695 | compared_to, current_user['access_token']) 696 | 697 | elif not tweeter or not compared_tweeter: 698 | options = { 699 | 'page_title': 'Comparing %s to %s' % (username, compared_to) 700 | } 701 | options['missing_info'] = [] 702 | if not tweeter: 703 | options['missing_info'].append(username) 704 | if not compared_tweeter: 705 | options['missing_info'].append(compared_to) 706 | options['next_url'] = self.request.path 707 | self.render('following_compared_missing.html', **options) 708 | return 709 | 710 | key = 'follows:%s:%s' % (compared_to, username) 711 | value = self.redis.get(key) 712 | if value is None: 713 | following = (self.db.Following 714 | .find_one({'user': tweeter['_id'], 715 | 'follows': compared_tweeter['_id']})) 716 | if following: 717 | options['follows'] = following['following'] 718 | else: 719 | options['follows'] = False 720 | else: 721 | value = bool(int(value)) 722 | options['follows'] = value 723 | 724 | if options['follows']: 725 | options['page_title'] = ('%s follows %s' % 726 | (username, compared_to)) 727 | else: 728 | options['page_title'] = ('%s is too cool for %s' % 729 | (username, compared_to)) 730 | 731 | if tweeter.get('ratio_rank', None) is None: 732 | self._update_ratio_rank(tweeter) 733 | if compared_tweeter.get('ratio_rank', None) is None: 734 | self._update_ratio_rank(compared_tweeter) 735 | 736 | options['info'] = { 737 | username: tweeter, 738 | compared_to: compared_tweeter 739 | } 740 | options['username'] = username 741 | options['this_username'] = compared_to 742 | options['compared_to'] = compared_to 743 | options['perm_url'] = self.get_following_perm_url( 744 | options['username'], options['this_username']) 745 | 746 | self.render('following.html', **options) 747 | 748 | 749 | @route(r'/coolest', name='coolest') 750 | class CoolestHandler(BaseHandler): # pragma: no cover (under development) 751 | 752 | def get(self): 753 | options = {} 754 | user = self.get_current_user() 755 | key = 'ratios' 756 | 757 | #ratios = self.redis.zrange(key, 0, -1, withscores=True) 758 | #ratios.reverse() 759 | 760 | options['ratios'] = self.db.Tweeter.find().sort('ratio', -1) 761 | options['user'] = user 762 | options['page_title'] = \ 763 | "Coolest in the world! ...on Twitter ...using this site" 764 | self.render('coolest.html', **options) 765 | 766 | @route(r'/screenshots', name='screenshots') 767 | class ScreenshotsHandler(BaseHandler): # pragma: no cover (under development) 768 | IMAGES = ( 769 | ('bookmarklet-in-toolbar.png', 770 | u"Bookmarklet in toolbar"), 771 | ('on-twitter.png', 772 | u"On Twitter"), 773 | ('follows-me.png', 774 | u"Someone who follows me"), 775 | ('too-cool.png', 776 | u"Someone who is too cool for me"), 777 | ('everyone.png', 778 | u"Complete list of all people you follow and if they follow you"), 779 | ('lookups.png', 780 | u"On /lookups you can see all Twitter traffic in near-real-time"), 781 | ) 782 | 783 | def get(self): 784 | options = {} 785 | options['page_title'] = "Screenshots" 786 | images = [] 787 | static_base_path = os.path.join( 788 | self.application.settings['static_path'], 789 | 'images', 790 | 'screenshots', 791 | ) 792 | for filename, title in self.IMAGES: 793 | file_path = os.path.join('images', 'screenshots', filename) 794 | file_path_small = file_path.replace('.png', '_small.png') 795 | images.append(( 796 | file_path, 797 | file_path_small, 798 | title 799 | )) 800 | 801 | options['images'] = images 802 | self.render('screenshots.html', **options) 803 | 804 | @route('/everyone', name='everyone') 805 | class EveryoneIFollowHandler(BaseHandler, tornado.auth.TwitterMixin): 806 | 807 | def get(self): 808 | current_user = self.get_current_user() 809 | if not current_user: 810 | self.redirect(self.reverse_url('auth_twitter')) 811 | return 812 | options = {} 813 | options['page_title'] = "Everyone I follow" 814 | self.render('everyone.html', **options) 815 | 816 | @route('/everyone.json', name='everyone_json') 817 | class EveryoneIFollowJSONHandler(BaseHandler, tornado.auth.TwitterMixin): 818 | 819 | @tornado.web.asynchronous 820 | @tornado.gen.engine 821 | def get(self): 822 | 823 | current_user = self.get_current_user() 824 | if not current_user: 825 | raise HTTPError(403, "Not logged in") 826 | 827 | this_username = current_user['username'] 828 | access_token = current_user['access_token'] 829 | key = 'friends:%s' % this_username 830 | result = self.redis.get(key) 831 | if result: 832 | if result == 'null': # pragma: no cover 833 | result = None 834 | elif 'next_cursor_str' in result: # pragma: no cover 835 | result = None 836 | 837 | if result is None: 838 | result = yield tornado.gen.Task(self.twitter_request, 839 | "/friends/ids", 840 | screen_name=this_username, 841 | access_token=access_token 842 | ) 843 | if 'ids' in result: 844 | result = result['ids'] 845 | if result: 846 | self.redis.setex(key, json_encode(result), 60 * 60) 847 | else: 848 | raise NotImplementedError 849 | else: 850 | result = json_decode(result) 851 | # now turn these IDs into real screen names 852 | unknown = [] 853 | screen_names = [] 854 | for id_ in result: 855 | user = self.db.User.find_one({'user_id': id_}) 856 | if user: 857 | screen_names.append(user['username']) 858 | else: 859 | key = 'screen_name:%s' % id_ 860 | screen_name = self.redis.get(key) 861 | if screen_name is None: 862 | unknown.append(id_) 863 | else: 864 | screen_names.append(screen_name) 865 | 866 | buckets = utils.bucketize(unknown, 100) 867 | 868 | for bucket in buckets: 869 | users = None 870 | attempts = 0 871 | while True: 872 | users = yield tornado.gen.Task(self.twitter_request, 873 | "/users/lookup", 874 | user_id=','.join(str(x) for x in bucket) 875 | ) 876 | if users is not None: 877 | break 878 | else: 879 | time.sleep(1) 880 | attempts += 1 881 | if attempts > 3: 882 | result = {'ERROR': 'Unable to connect to Twitter'} 883 | self.write_json(result) 884 | self.finish() 885 | return 886 | 887 | for user in users: 888 | username = user['screen_name'] 889 | key = 'screen_name:%s' % user['id'] 890 | self.save_tweeter_user(user) 891 | self.redis.setex(key, username, 7 * 24 * 60 * 60) 892 | screen_names.append(username) 893 | 894 | if len(result) != len(screen_names): 895 | logging.error('RESULT (%d): %r' % (len(result), result)) 896 | logging.error('SCREEN_NAMES (%d): %r' % (len(screen_names), screen_names)) 897 | raise ValueError('Invalid number of results') 898 | 899 | screen_names.sort() 900 | self.write_json(screen_names) 901 | self.finish() 902 | 903 | 904 | 905 | @route('/lookups', name='lookups') 906 | class LookupsHandler(BaseHandler): 907 | 908 | def get_lookups(self, username=None): 909 | data = {} 910 | data['lookups_json'] = self.redis.get('lookups:json') or 0 911 | data['lookups_jsonp'] = self.redis.get('lookups:jsonp') or 0 912 | data['auths'] = self.redis.get('auths:total') or 0 913 | data['lookups_usernames'] = self.redis.get('lookups:usernames') or 0 914 | if username: 915 | print "NotImplmented" 916 | for key, value in data.items(): 917 | data[key] = int(value) 918 | return data 919 | 920 | def get(self): 921 | options = {} 922 | options['page_title'] = "Lookups" 923 | options.update(self.get_lookups()) 924 | self.render('lookups.html', **options) 925 | 926 | 927 | @route('/lookups.json', name='lookups_json') 928 | class LookupsJSONHandler(LookupsHandler): 929 | 930 | def get(self): 931 | data = self.get_lookups() 932 | self.write_json(data) 933 | 934 | # this handler gets automatically appended last to all handlers inside app.py 935 | class PageNotFoundHandler(BaseHandler): 936 | 937 | def get(self): 938 | path = self.request.path 939 | if not path.endswith('/'): 940 | new_url = '%s/' % path 941 | if self.request.query: 942 | new_url += '?%s' % self.request.query 943 | self.redirect(new_url) 944 | return 945 | raise HTTPError(404, path) 946 | -------------------------------------------------------------------------------- /here.py: -------------------------------------------------------------------------------- 1 | import site, os.path as op 2 | ROOT = op.abspath(op.dirname(__file__)) 3 | path = lambda *a: op.join(ROOT, *a) 4 | site.addsitedir(path('vendor')) 5 | -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | import re 2 | import datetime 3 | from bson.objectid import ObjectId 4 | from mongolite import Connection, Document 5 | 6 | 7 | 8 | connection = Connection() 9 | 10 | class BaseDocument(Document): 11 | skeleton = { 12 | 'modify_date': datetime.datetime 13 | } 14 | 15 | default_values = {'modify_date': datetime.datetime.utcnow} 16 | 17 | def save(self, *args, **kwargs): 18 | if '_id' in self and kwargs.get('update_modify_date', True): 19 | m = datetime.datetime.utcnow() 20 | self['modify_date'] = m 21 | super(BaseDocument, self).save(*args, **kwargs) 22 | 23 | 24 | @connection.register 25 | class User(BaseDocument): 26 | __collection__ = 'users' 27 | skeleton = { 28 | 'username': unicode, 29 | 'access_token': dict, 30 | 'modify_date': datetime.datetime 31 | } 32 | optional = { 33 | 'user_id': int, 34 | } 35 | 36 | 37 | @connection.register 38 | class Tweeter(BaseDocument): 39 | __collection__ = 'tweeters' 40 | skeleton = { 41 | 'user_id': int, 42 | 'username': unicode, 43 | 'name': unicode, 44 | 'followers': int, 45 | 'following': int, 46 | 'ratio': float, 47 | 'last_tweet_date': datetime.datetime, 48 | } 49 | optional = { 50 | 'ratio_rank': int, 51 | } 52 | 53 | def set_ratio(self): 54 | self['ratio'] = 1.0 * self['followers'] / max(self['following'], 1) 55 | 56 | @staticmethod 57 | def find_by_username(db, username): 58 | tweeter = db.Tweeter.find_one({'username': username}) 59 | if not tweeter: 60 | tweeter = db.Tweeter.find_one({'username': re.compile(re.escape(username), re.I)}) 61 | return tweeter 62 | 63 | @staticmethod 64 | def update_tweeter(tweeter, user): 65 | if tweeter['name'] != user['name']: 66 | tweeter['name'] = user['name'] 67 | 68 | if tweeter['username'] != user['screen_name']: 69 | tweeter['username'] = user['screen_name'] 70 | 71 | if tweeter['followers'] != user['followers_count']: 72 | tweeter['followers'] = user['followers_count'] 73 | 74 | if tweeter['following'] != user['friends_count']: 75 | tweeter['following'] = user['friends_count'] 76 | 77 | def parse_status_date(dstr): 78 | dstr = re.sub('\+\d{1,4}', '', dstr) 79 | return datetime.datetime.strptime( 80 | dstr, 81 | '%a %b %d %H:%M:%S %Y' 82 | ) 83 | last_tweet_date = None 84 | if 'status' in user: 85 | last_tweet_date = user['status']['created_at'] 86 | last_tweet_date = parse_status_date(last_tweet_date) 87 | if tweeter['last_tweet_date'] != last_tweet_date: 88 | tweeter['last_tweet_date'] = last_tweet_date 89 | 90 | ratio_before = tweeter['ratio'] 91 | tweeter.set_ratio() 92 | tweeter.save() 93 | 94 | 95 | 96 | @connection.register 97 | class Following(BaseDocument): 98 | __collection__ = 'following' 99 | skeleton = { 100 | 'user': unicode, 101 | 'follows': unicode, 102 | 'following': bool, 103 | } 104 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | redis 2 | tornado 3 | mongolite 4 | mock 5 | tornado-utils 6 | Celery 7 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | PROJECT_TITLE = u"Too cool for me?" 2 | DATABASE_NAME = "toocool" 3 | 4 | COOKIE_SECRET = "92orTzK3XqaGUYdkL3gmUejIFuY37EQn92XsTo1v/Vi=" 5 | TWITTER_CONSUMER_KEY = None 6 | TWITTER_CONSUMER_SECRET = None 7 | 8 | REDIS_HOST = 'localhost' 9 | REDIS_PORT = 6379 10 | 11 | # complete this in your local_settings.py to get emails sent on errors 12 | ADMIN_EMAILS = ( 13 | ) 14 | 15 | try: 16 | from local_settings import * 17 | except ImportError: 18 | pass 19 | 20 | 21 | assert TWITTER_CONSUMER_KEY 22 | assert TWITTER_CONSUMER_SECRET 23 | -------------------------------------------------------------------------------- /socketapp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import re 4 | import here 5 | import logging 6 | from pprint import pprint 7 | import tornado.escape 8 | import redis.client 9 | import tornado.options 10 | from tornado.options import define, options 11 | import settings 12 | 13 | from tornadio2 import SocketConnection, TornadioRouter, SocketServer, event 14 | 15 | define("debug", default=False, help="run in debug mode", type=bool) 16 | define("port", default=8888, help="run on the given port (default 8888)", type=int) 17 | 18 | 19 | class LookupsConnection(SocketConnection): 20 | connected_clients = set() 21 | def on_open(self, request): 22 | #print "OPEN" 23 | for each in self.connected_clients: 24 | each.send({'message': "Someone connected!"}) 25 | self.connected_clients.add(self) 26 | 27 | def on_message(self, message): 28 | logging.debug("RECEIVED: %r" % message) 29 | for client in self.connected_clients: 30 | if client != self: 31 | #print "CLIENT", repr(client) 32 | #print "\t", client.is_closed 33 | if client.is_closed: 34 | print "DBG consider deleting", repr(client) 35 | else: 36 | client.send({'message': message.upper()}) 37 | 38 | def on_close(self): 39 | logging.debug("Closing client") 40 | if self in self.connected_clients: 41 | logging.debug("Removing %r" % self) 42 | self.connected_clients.remove(self) 43 | 44 | 45 | def redis_listener(): 46 | r = redis.Redis(settings.REDIS_HOST, settings.REDIS_PORT) 47 | ps = r.pubsub() 48 | ps.subscribe(['lookups']) 49 | for message in ps.listen(): 50 | try: 51 | data = tornado.escape.json_decode(message['data']) 52 | except ValueError: 53 | data = message['data'] 54 | 55 | #print "****MESSAGE" 56 | #pprint(data) 57 | #print "\t send this to", len(LookupsConnection.connected_clients), 'clients' 58 | to_send = {} 59 | for key, value in data.items(): 60 | new_key = { 61 | 'lookups:json': 'lookups_json', 62 | 'lookups:jsonp': 'lookups_jsonp', 63 | 'auths:total': 'auths', 64 | 'lookups:usernames': 'lookups_usernames' 65 | }.get(key) 66 | if new_key is None: 67 | print "Skipping", repr(key) 68 | continue 69 | #print new_key, repr(value) 70 | to_send[new_key] = value 71 | 72 | for client in LookupsConnection.connected_clients: 73 | client.send(to_send) 74 | 75 | 76 | 77 | def main(): 78 | import threading 79 | t = threading.Thread(target=redis_listener) 80 | t.setDaemon(True) 81 | t.start() 82 | 83 | LookupsServer = TornadioRouter(LookupsConnection) 84 | # Fill your routes here 85 | routes = [] 86 | # Extend list of routes with Tornadio2 URLs 87 | routes.extend(LookupsServer.urls) 88 | 89 | tornado.options.parse_command_line() 90 | if options.debug: 91 | logging.getLogger().setLevel(logging.DEBUG) 92 | else: 93 | logging.getLogger().setLevel(logging.INFO) 94 | 95 | application = tornado.web.Application(routes, 96 | socket_io_port=options.port) 97 | try: 98 | SocketServer(application) 99 | except KeyboardInterrupt: 100 | pass 101 | 102 | 103 | if __name__ == "__main__": 104 | main() 105 | -------------------------------------------------------------------------------- /static/bookmarklet.js: -------------------------------------------------------------------------------- 1 | function L() { 2 | if (window.console && window.console.log) 3 | console.log.apply(console, arguments); 4 | } 5 | 6 | var Lookup = (function() { 7 | var BASE_URL = 'http://toocoolfor.me'; 8 | //var BASE_URL = 'http://toocool'; 9 | var your_screen_name; 10 | var _default_interval = 3, _interval = _default_interval; 11 | 12 | var screennames = []; 13 | var known = {}; 14 | function findScreenNames() { 15 | var new_screennames = []; 16 | $('a.tweet-screen-name').each(function(i, e) { 17 | var v = $.trim($(this).text()); 18 | if (v && $.inArray(v, screennames) == -1) { 19 | screennames.push(v); 20 | new_screennames.push(v); 21 | } 22 | }); 23 | return new_screennames; 24 | } 25 | 26 | return { 27 | callback: function(response) { 28 | if (response.ERROR) { 29 | alert(response.ERROR); 30 | return; 31 | } 32 | var screen_name, tag, prefix; 33 | 34 | $('div.tweet-content').each(function() { 35 | // only display results once 36 | if ($('a.followsyou,a.followsyounot', this).size()) 37 | return; 38 | 39 | screen_name = $('a.tweet-screen-name', this).text(); 40 | // or if it's you 41 | if (screen_name == your_screen_name) 42 | return; 43 | 44 | var result = known[screen_name]; 45 | if (result === undefined) { 46 | result = response[screen_name]; 47 | known[screen_name] = result; 48 | } 49 | 50 | if (result) { 51 | tag = $('', {text: 'follows me'}) 52 | .addClass('followsyou') 53 | .css('color', 'green'); 54 | } else { 55 | tag = $('', {text: 'too cool for me'}) 56 | .addClass('followsyounot') 57 | .css('color', '#333'); 58 | } 59 | tag 60 | .attr('href', BASE_URL + '/following/' + screen_name) 61 | .attr('target', '_blank') 62 | .attr('title', 'According to follows.me') 63 | .css('float', 'right') 64 | .css('padding-right', '30px') 65 | .css('font-size', '11px') 66 | .appendTo($('span.tweet-user-name', this)); 67 | 68 | }); 69 | 70 | }, 71 | search: function() { 72 | setTimeout(function() { 73 | Lookup.search(); 74 | }, _interval * 1000); 75 | 76 | your_screen_name = $.trim($('#screen-name').text()); 77 | var names = findScreenNames(); 78 | names = $.grep(names, function(e) { 79 | return known[e] === undefined && e != your_screen_name; 80 | }); 81 | 82 | if (!names.length) { 83 | _interval++; // nothing has changed 84 | return; 85 | } 86 | _interval = _default_interval; 87 | var data = { 88 | you: your_screen_name, 89 | usernames: names.join(',') 90 | }; 91 | var s = document.createElement('script'); 92 | s.type = 'text/javascript'; 93 | s.defer = true; 94 | var url = BASE_URL + '/jsonp?callback=Lookup.callback' 95 | + '&usernames=' + data.usernames 96 | + '&you=' + data.you; 97 | s.src = url; 98 | document.getElementsByTagName('head')[0].appendChild(s); 99 | } 100 | } 101 | })(); 102 | 103 | 104 | (function() { 105 | Lookup.search(); 106 | })(); 107 | -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | background-attachment: fixed; 4 | background-repeat: no-repeat; 5 | background-position: bottom right; 6 | margin-left: 40px; 7 | } 8 | h1,h2,h3,h4 { 9 | font-family: Helvetica, sans-serif; 10 | } 11 | h3 { font-size:20px; } 12 | h1 a { color:black; text-decoration:none; } 13 | h1 a:hover { color:black; text-decoration:underline; } 14 | header h1 { font-family: 'Permanent Marker',cursive;margin-top:10px; 15 | font-size: 4.2em; letter-spacing: -3px; margin-bottom: 0; } 16 | /*footer { position: absolute; bottom: 0; }*/ 17 | footer { clear:left; margin-top:100px; } 18 | footer p { text-align:center; font-family: sans-serif; color:#666; font-size:0.8em; } 19 | .followsyou { color: green; } 20 | .followsyounot { color: #333; } 21 | h1 a .q { color:#999; } 22 | 23 | p.description { 24 | font-size: 14px; 25 | line-height: 1.6em; 26 | width: 600px; 27 | } 28 | 29 | p.login { 30 | display: block; 31 | float: left; 32 | width: 100%; 33 | } 34 | 35 | .twitter-login { 36 | /* Firefox 3.6+ */ 37 | background: -moz-linear-gradient(100% 100% 90deg, #e6e6e6, #f1f1f1); 38 | /* Safari 4-5, Chrome 1-9 */ /* -webkit-gradient(, [, ]?, [, ]? [, ]*) */ 39 | background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#f1f1f1), to(#e6e6e6)); 40 | /* Safari 5.1+, Chrome 10+ */ 41 | background: -webkit-linear-gradient(#e6e6e6, #f1f1f1); 42 | /* Opera 11.10+ */ 43 | background: -o-linear-gradient(#e6e6e6, #f1f1f1); 44 | border: 1px solid #FFFFFF; 45 | border-radius: 5px 5px 5px 5px; 46 | box-shadow: 0 1px 2px #999999; 47 | color: #333333; 48 | font-size: 17px; 49 | font-weight: bold; 50 | margin: 0 20px 10px 0; 51 | padding: 10px 15px; 52 | text-decoration: none; 53 | text-shadow: 0 1px 1px #FFFFFF; 54 | float: left; 55 | } 56 | 57 | a.twitter-login img, a.bookmarklet img { 58 | float: left; 59 | } 60 | 61 | a.twitter-login span.text { 62 | float: left; 63 | margin-left: 10px; 64 | position: relative; 65 | top: 4px; 66 | } 67 | 68 | span.readonly {font-size: 11px;} 69 | 70 | span.tick { 71 | color: green; 72 | font-size: 30px; 73 | font-weight: bold; 74 | padding: 3px 10px 3px 13px; 75 | } 76 | 77 | .useit { 78 | float: left; width: 340px; margin: 30px 40px 0pt 0pt; 79 | padding-right: 20px; 80 | /*height: 260px;*/ 81 | } 82 | .useit-first { 83 | border-right: 1px solid #e1e1e1; 84 | } 85 | span.title {font-weight: bold; letter-spacing: -1px; width: 100%; float: left; font-size: 30px; margin-bottom: 20px;} 86 | span.explain {font-size: 12px; margin-bottom: 12px;} 87 | a.bookmarklet { 88 | /* Firefox 3.6+ */ 89 | background: -moz-linear-gradient(100% 100% 90deg, #e6e6e6, #f1f1f1); 90 | /* Safari 4-5, Chrome 1-9 */ /* -webkit-gradient(, [, ]?, [, ]? [, ]*) */ 91 | background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#f1f1f1), to(#e6e6e6)); 92 | /* Safari 5.1+, Chrome 10+ */ 93 | background: -webkit-linear-gradient(#e6e6e6, #f1f1f1); 94 | /* Opera 11.10+ */ 95 | background: -o-linear-gradient(#e6e6e6, #f1f1f1); 96 | border: 1px solid #FFFFFF; 97 | border-radius: 5px 5px 5px 5px; 98 | box-shadow: 0 1px 3px #666666; 99 | color: #319FBF; 100 | font-weight: bold; 101 | padding: 10px 15px; 102 | text-decoration: none; 103 | text-shadow: 0 1px 1px #FFFFFF; 104 | margin: 0px 10px 20px 0; 105 | float: left; 106 | width: 200px; 107 | font-size: 20px; 108 | } 109 | 110 | a.bookmarklet span.text { 111 | float: left; 112 | margin-left: 10px; 113 | position: relative; 114 | top: 3px; 115 | } 116 | img.screengrab1 {float:left; margin-top: 20px; opacity:0.7;} 117 | img.screengrab {float: left; margin-right: 10px;} 118 | h3.confused {font-size:35px;} 119 | span.grey {color:#c1c1c1;} 120 | 121 | header h1 { } 122 | header p#home-link { font-size:0.8em; margin-top:0; } 123 | -------------------------------------------------------------------------------- /static/fancybox/blank.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/fancybox/blank.gif -------------------------------------------------------------------------------- /static/fancybox/fancy_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/fancybox/fancy_close.png -------------------------------------------------------------------------------- /static/fancybox/fancy_loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/fancybox/fancy_loading.png -------------------------------------------------------------------------------- /static/fancybox/fancy_nav_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/fancybox/fancy_nav_left.png -------------------------------------------------------------------------------- /static/fancybox/fancy_nav_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/fancybox/fancy_nav_right.png -------------------------------------------------------------------------------- /static/fancybox/fancy_shadow_e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/fancybox/fancy_shadow_e.png -------------------------------------------------------------------------------- /static/fancybox/fancy_shadow_n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/fancybox/fancy_shadow_n.png -------------------------------------------------------------------------------- /static/fancybox/fancy_shadow_ne.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/fancybox/fancy_shadow_ne.png -------------------------------------------------------------------------------- /static/fancybox/fancy_shadow_nw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/fancybox/fancy_shadow_nw.png -------------------------------------------------------------------------------- /static/fancybox/fancy_shadow_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/fancybox/fancy_shadow_s.png -------------------------------------------------------------------------------- /static/fancybox/fancy_shadow_se.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/fancybox/fancy_shadow_se.png -------------------------------------------------------------------------------- /static/fancybox/fancy_shadow_sw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/fancybox/fancy_shadow_sw.png -------------------------------------------------------------------------------- /static/fancybox/fancy_shadow_w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/fancybox/fancy_shadow_w.png -------------------------------------------------------------------------------- /static/fancybox/fancy_title_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/fancybox/fancy_title_left.png -------------------------------------------------------------------------------- /static/fancybox/fancy_title_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/fancybox/fancy_title_main.png -------------------------------------------------------------------------------- /static/fancybox/fancy_title_over.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/fancybox/fancy_title_over.png -------------------------------------------------------------------------------- /static/fancybox/fancy_title_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/fancybox/fancy_title_right.png -------------------------------------------------------------------------------- /static/fancybox/fancybox-x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/fancybox/fancybox-x.png -------------------------------------------------------------------------------- /static/fancybox/fancybox-y.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/fancybox/fancybox-y.png -------------------------------------------------------------------------------- /static/fancybox/fancybox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/fancybox/fancybox.png -------------------------------------------------------------------------------- /static/fancybox/jquery.easing-1.3.pack.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Easing v1.3 - http://gsgd.co.uk/sandbox/jquery/easing/ 3 | * 4 | * Uses the built in easing capabilities added In jQuery 1.1 5 | * to offer multiple easing options 6 | * 7 | * TERMS OF USE - jQuery Easing 8 | * 9 | * Open source under the BSD License. 10 | * 11 | * Copyright © 2008 George McGinley Smith 12 | * All rights reserved. 13 | * 14 | * Redistribution and use in source and binary forms, with or without modification, 15 | * are permitted provided that the following conditions are met: 16 | * 17 | * Redistributions of source code must retain the above copyright notice, this list of 18 | * conditions and the following disclaimer. 19 | * Redistributions in binary form must reproduce the above copyright notice, this list 20 | * of conditions and the following disclaimer in the documentation and/or other materials 21 | * provided with the distribution. 22 | * 23 | * Neither the name of the author nor the names of contributors may be used to endorse 24 | * or promote products derived from this software without specific prior written permission. 25 | * 26 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 27 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 28 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 29 | * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 30 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 31 | * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 32 | * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 33 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | * 36 | */ 37 | 38 | // t: current time, b: begInnIng value, c: change In value, d: duration 39 | eval(function(p,a,c,k,e,r){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('h.i[\'1a\']=h.i[\'z\'];h.O(h.i,{y:\'D\',z:9(x,t,b,c,d){6 h.i[h.i.y](x,t,b,c,d)},17:9(x,t,b,c,d){6 c*(t/=d)*t+b},D:9(x,t,b,c,d){6-c*(t/=d)*(t-2)+b},13:9(x,t,b,c,d){e((t/=d/2)<1)6 c/2*t*t+b;6-c/2*((--t)*(t-2)-1)+b},X:9(x,t,b,c,d){6 c*(t/=d)*t*t+b},U:9(x,t,b,c,d){6 c*((t=t/d-1)*t*t+1)+b},R:9(x,t,b,c,d){e((t/=d/2)<1)6 c/2*t*t*t+b;6 c/2*((t-=2)*t*t+2)+b},N:9(x,t,b,c,d){6 c*(t/=d)*t*t*t+b},M:9(x,t,b,c,d){6-c*((t=t/d-1)*t*t*t-1)+b},L:9(x,t,b,c,d){e((t/=d/2)<1)6 c/2*t*t*t*t+b;6-c/2*((t-=2)*t*t*t-2)+b},K:9(x,t,b,c,d){6 c*(t/=d)*t*t*t*t+b},J:9(x,t,b,c,d){6 c*((t=t/d-1)*t*t*t*t+1)+b},I:9(x,t,b,c,d){e((t/=d/2)<1)6 c/2*t*t*t*t*t+b;6 c/2*((t-=2)*t*t*t*t+2)+b},G:9(x,t,b,c,d){6-c*8.C(t/d*(8.g/2))+c+b},15:9(x,t,b,c,d){6 c*8.n(t/d*(8.g/2))+b},12:9(x,t,b,c,d){6-c/2*(8.C(8.g*t/d)-1)+b},Z:9(x,t,b,c,d){6(t==0)?b:c*8.j(2,10*(t/d-1))+b},Y:9(x,t,b,c,d){6(t==d)?b+c:c*(-8.j(2,-10*t/d)+1)+b},W:9(x,t,b,c,d){e(t==0)6 b;e(t==d)6 b+c;e((t/=d/2)<1)6 c/2*8.j(2,10*(t-1))+b;6 c/2*(-8.j(2,-10*--t)+2)+b},V:9(x,t,b,c,d){6-c*(8.o(1-(t/=d)*t)-1)+b},S:9(x,t,b,c,d){6 c*8.o(1-(t=t/d-1)*t)+b},Q:9(x,t,b,c,d){e((t/=d/2)<1)6-c/2*(8.o(1-t*t)-1)+b;6 c/2*(8.o(1-(t-=2)*t)+1)+b},P:9(x,t,b,c,d){f s=1.l;f p=0;f a=c;e(t==0)6 b;e((t/=d)==1)6 b+c;e(!p)p=d*.3;e(a<8.w(c)){a=c;f s=p/4}m f s=p/(2*8.g)*8.r(c/a);6-(a*8.j(2,10*(t-=1))*8.n((t*d-s)*(2*8.g)/p))+b},H:9(x,t,b,c,d){f s=1.l;f p=0;f a=c;e(t==0)6 b;e((t/=d)==1)6 b+c;e(!p)p=d*.3;e(a<8.w(c)){a=c;f s=p/4}m f s=p/(2*8.g)*8.r(c/a);6 a*8.j(2,-10*t)*8.n((t*d-s)*(2*8.g)/p)+c+b},T:9(x,t,b,c,d){f s=1.l;f p=0;f a=c;e(t==0)6 b;e((t/=d/2)==2)6 b+c;e(!p)p=d*(.3*1.5);e(a<8.w(c)){a=c;f s=p/4}m f s=p/(2*8.g)*8.r(c/a);e(t<1)6-.5*(a*8.j(2,10*(t-=1))*8.n((t*d-s)*(2*8.g)/p))+b;6 a*8.j(2,-10*(t-=1))*8.n((t*d-s)*(2*8.g)/p)*.5+c+b},F:9(x,t,b,c,d,s){e(s==u)s=1.l;6 c*(t/=d)*t*((s+1)*t-s)+b},E:9(x,t,b,c,d,s){e(s==u)s=1.l;6 c*((t=t/d-1)*t*((s+1)*t+s)+1)+b},16:9(x,t,b,c,d,s){e(s==u)s=1.l;e((t/=d/2)<1)6 c/2*(t*t*(((s*=(1.B))+1)*t-s))+b;6 c/2*((t-=2)*t*(((s*=(1.B))+1)*t+s)+2)+b},A:9(x,t,b,c,d){6 c-h.i.v(x,d-t,0,c,d)+b},v:9(x,t,b,c,d){e((t/=d)<(1/2.k)){6 c*(7.q*t*t)+b}m e(t<(2/2.k)){6 c*(7.q*(t-=(1.5/2.k))*t+.k)+b}m e(t<(2.5/2.k)){6 c*(7.q*(t-=(2.14/2.k))*t+.11)+b}m{6 c*(7.q*(t-=(2.18/2.k))*t+.19)+b}},1b:9(x,t,b,c,d){e(t')[0], { prop: 0 }), 28 | 29 | isIE6 = $.browser.msie && $.browser.version < 7 && !window.XMLHttpRequest, 30 | 31 | /* 32 | * Private methods 33 | */ 34 | 35 | _abort = function() { 36 | loading.hide(); 37 | 38 | imgPreloader.onerror = imgPreloader.onload = null; 39 | 40 | if (ajaxLoader) { 41 | ajaxLoader.abort(); 42 | } 43 | 44 | tmp.empty(); 45 | }, 46 | 47 | _error = function() { 48 | if (false === selectedOpts.onError(selectedArray, selectedIndex, selectedOpts)) { 49 | loading.hide(); 50 | busy = false; 51 | return; 52 | } 53 | 54 | selectedOpts.titleShow = false; 55 | 56 | selectedOpts.width = 'auto'; 57 | selectedOpts.height = 'auto'; 58 | 59 | tmp.html( '

The requested content cannot be loaded.
Please try again later.

' ); 60 | 61 | _process_inline(); 62 | }, 63 | 64 | _start = function() { 65 | var obj = selectedArray[ selectedIndex ], 66 | href, 67 | type, 68 | title, 69 | str, 70 | emb, 71 | ret; 72 | 73 | _abort(); 74 | 75 | selectedOpts = $.extend({}, $.fn.fancybox.defaults, (typeof $(obj).data('fancybox') == 'undefined' ? selectedOpts : $(obj).data('fancybox'))); 76 | 77 | ret = selectedOpts.onStart(selectedArray, selectedIndex, selectedOpts); 78 | 79 | if (ret === false) { 80 | busy = false; 81 | return; 82 | } else if (typeof ret == 'object') { 83 | selectedOpts = $.extend(selectedOpts, ret); 84 | } 85 | 86 | title = selectedOpts.title || (obj.nodeName ? $(obj).attr('title') : obj.title) || ''; 87 | 88 | if (obj.nodeName && !selectedOpts.orig) { 89 | selectedOpts.orig = $(obj).children("img:first").length ? $(obj).children("img:first") : $(obj); 90 | } 91 | 92 | if (title === '' && selectedOpts.orig && selectedOpts.titleFromAlt) { 93 | title = selectedOpts.orig.attr('alt'); 94 | } 95 | 96 | href = selectedOpts.href || (obj.nodeName ? $(obj).attr('href') : obj.href) || null; 97 | 98 | if ((/^(?:javascript)/i).test(href) || href == '#') { 99 | href = null; 100 | } 101 | 102 | if (selectedOpts.type) { 103 | type = selectedOpts.type; 104 | 105 | if (!href) { 106 | href = selectedOpts.content; 107 | } 108 | 109 | } else if (selectedOpts.content) { 110 | type = 'html'; 111 | 112 | } else if (href) { 113 | if (href.match(imgRegExp)) { 114 | type = 'image'; 115 | 116 | } else if (href.match(swfRegExp)) { 117 | type = 'swf'; 118 | 119 | } else if ($(obj).hasClass("iframe")) { 120 | type = 'iframe'; 121 | 122 | } else if (href.indexOf("#") === 0) { 123 | type = 'inline'; 124 | 125 | } else { 126 | type = 'ajax'; 127 | } 128 | } 129 | 130 | if (!type) { 131 | _error(); 132 | return; 133 | } 134 | 135 | if (type == 'inline') { 136 | obj = href.substr(href.indexOf("#")); 137 | type = $(obj).length > 0 ? 'inline' : 'ajax'; 138 | } 139 | 140 | selectedOpts.type = type; 141 | selectedOpts.href = href; 142 | selectedOpts.title = title; 143 | 144 | if (selectedOpts.autoDimensions) { 145 | if (selectedOpts.type == 'html' || selectedOpts.type == 'inline' || selectedOpts.type == 'ajax') { 146 | selectedOpts.width = 'auto'; 147 | selectedOpts.height = 'auto'; 148 | } else { 149 | selectedOpts.autoDimensions = false; 150 | } 151 | } 152 | 153 | if (selectedOpts.modal) { 154 | selectedOpts.overlayShow = true; 155 | selectedOpts.hideOnOverlayClick = false; 156 | selectedOpts.hideOnContentClick = false; 157 | selectedOpts.enableEscapeButton = false; 158 | selectedOpts.showCloseButton = false; 159 | } 160 | 161 | selectedOpts.padding = parseInt(selectedOpts.padding, 10); 162 | selectedOpts.margin = parseInt(selectedOpts.margin, 10); 163 | 164 | tmp.css('padding', (selectedOpts.padding + selectedOpts.margin)); 165 | 166 | $('.fancybox-inline-tmp').unbind('fancybox-cancel').bind('fancybox-change', function() { 167 | $(this).replaceWith(content.children()); 168 | }); 169 | 170 | switch (type) { 171 | case 'html' : 172 | tmp.html( selectedOpts.content ); 173 | _process_inline(); 174 | break; 175 | 176 | case 'inline' : 177 | if ( $(obj).parent().is('#fancybox-content') === true) { 178 | busy = false; 179 | return; 180 | } 181 | 182 | $('
') 183 | .hide() 184 | .insertBefore( $(obj) ) 185 | .bind('fancybox-cleanup', function() { 186 | $(this).replaceWith(content.children()); 187 | }).bind('fancybox-cancel', function() { 188 | $(this).replaceWith(tmp.children()); 189 | }); 190 | 191 | $(obj).appendTo(tmp); 192 | 193 | _process_inline(); 194 | break; 195 | 196 | case 'image': 197 | busy = false; 198 | 199 | $.fancybox.showActivity(); 200 | 201 | imgPreloader = new Image(); 202 | 203 | imgPreloader.onerror = function() { 204 | _error(); 205 | }; 206 | 207 | imgPreloader.onload = function() { 208 | busy = true; 209 | 210 | imgPreloader.onerror = imgPreloader.onload = null; 211 | 212 | _process_image(); 213 | }; 214 | 215 | imgPreloader.src = href; 216 | break; 217 | 218 | case 'swf': 219 | selectedOpts.scrolling = 'no'; 220 | 221 | str = ''; 222 | emb = ''; 223 | 224 | $.each(selectedOpts.swf, function(name, val) { 225 | str += ''; 226 | emb += ' ' + name + '="' + val + '"'; 227 | }); 228 | 229 | str += ''; 230 | 231 | tmp.html(str); 232 | 233 | _process_inline(); 234 | break; 235 | 236 | case 'ajax': 237 | busy = false; 238 | 239 | $.fancybox.showActivity(); 240 | 241 | selectedOpts.ajax.win = selectedOpts.ajax.success; 242 | 243 | ajaxLoader = $.ajax($.extend({}, selectedOpts.ajax, { 244 | url : href, 245 | data : selectedOpts.ajax.data || {}, 246 | error : function(XMLHttpRequest, textStatus, errorThrown) { 247 | if ( XMLHttpRequest.status > 0 ) { 248 | _error(); 249 | } 250 | }, 251 | success : function(data, textStatus, XMLHttpRequest) { 252 | var o = typeof XMLHttpRequest == 'object' ? XMLHttpRequest : ajaxLoader; 253 | if (o.status == 200) { 254 | if ( typeof selectedOpts.ajax.win == 'function' ) { 255 | ret = selectedOpts.ajax.win(href, data, textStatus, XMLHttpRequest); 256 | 257 | if (ret === false) { 258 | loading.hide(); 259 | return; 260 | } else if (typeof ret == 'string' || typeof ret == 'object') { 261 | data = ret; 262 | } 263 | } 264 | 265 | tmp.html( data ); 266 | _process_inline(); 267 | } 268 | } 269 | })); 270 | 271 | break; 272 | 273 | case 'iframe': 274 | _show(); 275 | break; 276 | } 277 | }, 278 | 279 | _process_inline = function() { 280 | var 281 | w = selectedOpts.width, 282 | h = selectedOpts.height; 283 | 284 | if (w.toString().indexOf('%') > -1) { 285 | w = parseInt( ($(window).width() - (selectedOpts.margin * 2)) * parseFloat(w) / 100, 10) + 'px'; 286 | 287 | } else { 288 | w = w == 'auto' ? 'auto' : w + 'px'; 289 | } 290 | 291 | if (h.toString().indexOf('%') > -1) { 292 | h = parseInt( ($(window).height() - (selectedOpts.margin * 2)) * parseFloat(h) / 100, 10) + 'px'; 293 | 294 | } else { 295 | h = h == 'auto' ? 'auto' : h + 'px'; 296 | } 297 | 298 | tmp.wrapInner('
'); 299 | 300 | selectedOpts.width = tmp.width(); 301 | selectedOpts.height = tmp.height(); 302 | 303 | _show(); 304 | }, 305 | 306 | _process_image = function() { 307 | selectedOpts.width = imgPreloader.width; 308 | selectedOpts.height = imgPreloader.height; 309 | 310 | $("").attr({ 311 | 'id' : 'fancybox-img', 312 | 'src' : imgPreloader.src, 313 | 'alt' : selectedOpts.title 314 | }).appendTo( tmp ); 315 | 316 | _show(); 317 | }, 318 | 319 | _show = function() { 320 | var pos, equal; 321 | 322 | loading.hide(); 323 | 324 | if (wrap.is(":visible") && false === currentOpts.onCleanup(currentArray, currentIndex, currentOpts)) { 325 | $.event.trigger('fancybox-cancel'); 326 | 327 | busy = false; 328 | return; 329 | } 330 | 331 | busy = true; 332 | 333 | $(content.add( overlay )).unbind(); 334 | 335 | $(window).unbind("resize.fb scroll.fb"); 336 | $(document).unbind('keydown.fb'); 337 | 338 | if (wrap.is(":visible") && currentOpts.titlePosition !== 'outside') { 339 | wrap.css('height', wrap.height()); 340 | } 341 | 342 | currentArray = selectedArray; 343 | currentIndex = selectedIndex; 344 | currentOpts = selectedOpts; 345 | 346 | if (currentOpts.overlayShow) { 347 | overlay.css({ 348 | 'background-color' : currentOpts.overlayColor, 349 | 'opacity' : currentOpts.overlayOpacity, 350 | 'cursor' : currentOpts.hideOnOverlayClick ? 'pointer' : 'auto', 351 | 'height' : $(document).height() 352 | }); 353 | 354 | if (!overlay.is(':visible')) { 355 | if (isIE6) { 356 | $('select:not(#fancybox-tmp select)').filter(function() { 357 | return this.style.visibility !== 'hidden'; 358 | }).css({'visibility' : 'hidden'}).one('fancybox-cleanup', function() { 359 | this.style.visibility = 'inherit'; 360 | }); 361 | } 362 | 363 | overlay.show(); 364 | } 365 | } else { 366 | overlay.hide(); 367 | } 368 | 369 | final_pos = _get_zoom_to(); 370 | 371 | _process_title(); 372 | 373 | if (wrap.is(":visible")) { 374 | $( close.add( nav_left ).add( nav_right ) ).hide(); 375 | 376 | pos = wrap.position(), 377 | 378 | start_pos = { 379 | top : pos.top, 380 | left : pos.left, 381 | width : wrap.width(), 382 | height : wrap.height() 383 | }; 384 | 385 | equal = (start_pos.width == final_pos.width && start_pos.height == final_pos.height); 386 | 387 | content.fadeTo(currentOpts.changeFade, 0.3, function() { 388 | var finish_resizing = function() { 389 | content.html( tmp.contents() ).fadeTo(currentOpts.changeFade, 1, _finish); 390 | }; 391 | 392 | $.event.trigger('fancybox-change'); 393 | 394 | content 395 | .empty() 396 | .removeAttr('filter') 397 | .css({ 398 | 'border-width' : currentOpts.padding, 399 | 'width' : final_pos.width - currentOpts.padding * 2, 400 | 'height' : selectedOpts.autoDimensions ? 'auto' : final_pos.height - titleHeight - currentOpts.padding * 2 401 | }); 402 | 403 | if (equal) { 404 | finish_resizing(); 405 | 406 | } else { 407 | fx.prop = 0; 408 | 409 | $(fx).animate({prop: 1}, { 410 | duration : currentOpts.changeSpeed, 411 | easing : currentOpts.easingChange, 412 | step : _draw, 413 | complete : finish_resizing 414 | }); 415 | } 416 | }); 417 | 418 | return; 419 | } 420 | 421 | wrap.removeAttr("style"); 422 | 423 | content.css('border-width', currentOpts.padding); 424 | 425 | if (currentOpts.transitionIn == 'elastic') { 426 | start_pos = _get_zoom_from(); 427 | 428 | content.html( tmp.contents() ); 429 | 430 | wrap.show(); 431 | 432 | if (currentOpts.opacity) { 433 | final_pos.opacity = 0; 434 | } 435 | 436 | fx.prop = 0; 437 | 438 | $(fx).animate({prop: 1}, { 439 | duration : currentOpts.speedIn, 440 | easing : currentOpts.easingIn, 441 | step : _draw, 442 | complete : _finish 443 | }); 444 | 445 | return; 446 | } 447 | 448 | if (currentOpts.titlePosition == 'inside' && titleHeight > 0) { 449 | title.show(); 450 | } 451 | 452 | content 453 | .css({ 454 | 'width' : final_pos.width - currentOpts.padding * 2, 455 | 'height' : selectedOpts.autoDimensions ? 'auto' : final_pos.height - titleHeight - currentOpts.padding * 2 456 | }) 457 | .html( tmp.contents() ); 458 | 459 | wrap 460 | .css(final_pos) 461 | .fadeIn( currentOpts.transitionIn == 'none' ? 0 : currentOpts.speedIn, _finish ); 462 | }, 463 | 464 | _format_title = function(title) { 465 | if (title && title.length) { 466 | if (currentOpts.titlePosition == 'float') { 467 | return '
' + title + '
'; 468 | } 469 | 470 | return '
' + title + '
'; 471 | } 472 | 473 | return false; 474 | }, 475 | 476 | _process_title = function() { 477 | titleStr = currentOpts.title || ''; 478 | titleHeight = 0; 479 | 480 | title 481 | .empty() 482 | .removeAttr('style') 483 | .removeClass(); 484 | 485 | if (currentOpts.titleShow === false) { 486 | title.hide(); 487 | return; 488 | } 489 | 490 | titleStr = $.isFunction(currentOpts.titleFormat) ? currentOpts.titleFormat(titleStr, currentArray, currentIndex, currentOpts) : _format_title(titleStr); 491 | 492 | if (!titleStr || titleStr === '') { 493 | title.hide(); 494 | return; 495 | } 496 | 497 | title 498 | .addClass('fancybox-title-' + currentOpts.titlePosition) 499 | .html( titleStr ) 500 | .appendTo( 'body' ) 501 | .show(); 502 | 503 | switch (currentOpts.titlePosition) { 504 | case 'inside': 505 | title 506 | .css({ 507 | 'width' : final_pos.width - (currentOpts.padding * 2), 508 | 'marginLeft' : currentOpts.padding, 509 | 'marginRight' : currentOpts.padding 510 | }); 511 | 512 | titleHeight = title.outerHeight(true); 513 | 514 | title.appendTo( outer ); 515 | 516 | final_pos.height += titleHeight; 517 | break; 518 | 519 | case 'over': 520 | title 521 | .css({ 522 | 'marginLeft' : currentOpts.padding, 523 | 'width' : final_pos.width - (currentOpts.padding * 2), 524 | 'bottom' : currentOpts.padding 525 | }) 526 | .appendTo( outer ); 527 | break; 528 | 529 | case 'float': 530 | title 531 | .css('left', parseInt((title.width() - final_pos.width - 40)/ 2, 10) * -1) 532 | .appendTo( wrap ); 533 | break; 534 | 535 | default: 536 | title 537 | .css({ 538 | 'width' : final_pos.width - (currentOpts.padding * 2), 539 | 'paddingLeft' : currentOpts.padding, 540 | 'paddingRight' : currentOpts.padding 541 | }) 542 | .appendTo( wrap ); 543 | break; 544 | } 545 | 546 | title.hide(); 547 | }, 548 | 549 | _set_navigation = function() { 550 | if (currentOpts.enableEscapeButton || currentOpts.enableKeyboardNav) { 551 | $(document).bind('keydown.fb', function(e) { 552 | if (e.keyCode == 27 && currentOpts.enableEscapeButton) { 553 | e.preventDefault(); 554 | $.fancybox.close(); 555 | 556 | } else if ((e.keyCode == 37 || e.keyCode == 39) && currentOpts.enableKeyboardNav && e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA' && e.target.tagName !== 'SELECT') { 557 | e.preventDefault(); 558 | $.fancybox[ e.keyCode == 37 ? 'prev' : 'next'](); 559 | } 560 | }); 561 | } 562 | 563 | if (!currentOpts.showNavArrows) { 564 | nav_left.hide(); 565 | nav_right.hide(); 566 | return; 567 | } 568 | 569 | if ((currentOpts.cyclic && currentArray.length > 1) || currentIndex !== 0) { 570 | nav_left.show(); 571 | } 572 | 573 | if ((currentOpts.cyclic && currentArray.length > 1) || currentIndex != (currentArray.length -1)) { 574 | nav_right.show(); 575 | } 576 | }, 577 | 578 | _finish = function () { 579 | if (!$.support.opacity) { 580 | content.get(0).style.removeAttribute('filter'); 581 | wrap.get(0).style.removeAttribute('filter'); 582 | } 583 | 584 | if (selectedOpts.autoDimensions) { 585 | content.css('height', 'auto'); 586 | } 587 | 588 | wrap.css('height', 'auto'); 589 | 590 | if (titleStr && titleStr.length) { 591 | title.show(); 592 | } 593 | 594 | if (currentOpts.showCloseButton) { 595 | close.show(); 596 | } 597 | 598 | _set_navigation(); 599 | 600 | if (currentOpts.hideOnContentClick) { 601 | content.bind('click', $.fancybox.close); 602 | } 603 | 604 | if (currentOpts.hideOnOverlayClick) { 605 | overlay.bind('click', $.fancybox.close); 606 | } 607 | 608 | $(window).bind("resize.fb", $.fancybox.resize); 609 | 610 | if (currentOpts.centerOnScroll) { 611 | $(window).bind("scroll.fb", $.fancybox.center); 612 | } 613 | 614 | if (currentOpts.type == 'iframe') { 615 | $('').appendTo(content); 616 | } 617 | 618 | wrap.show(); 619 | 620 | busy = false; 621 | 622 | $.fancybox.center(); 623 | 624 | currentOpts.onComplete(currentArray, currentIndex, currentOpts); 625 | 626 | _preload_images(); 627 | }, 628 | 629 | _preload_images = function() { 630 | var href, 631 | objNext; 632 | 633 | if ((currentArray.length -1) > currentIndex) { 634 | href = currentArray[ currentIndex + 1 ].href; 635 | 636 | if (typeof href !== 'undefined' && href.match(imgRegExp)) { 637 | objNext = new Image(); 638 | objNext.src = href; 639 | } 640 | } 641 | 642 | if (currentIndex > 0) { 643 | href = currentArray[ currentIndex - 1 ].href; 644 | 645 | if (typeof href !== 'undefined' && href.match(imgRegExp)) { 646 | objNext = new Image(); 647 | objNext.src = href; 648 | } 649 | } 650 | }, 651 | 652 | _draw = function(pos) { 653 | var dim = { 654 | width : parseInt(start_pos.width + (final_pos.width - start_pos.width) * pos, 10), 655 | height : parseInt(start_pos.height + (final_pos.height - start_pos.height) * pos, 10), 656 | 657 | top : parseInt(start_pos.top + (final_pos.top - start_pos.top) * pos, 10), 658 | left : parseInt(start_pos.left + (final_pos.left - start_pos.left) * pos, 10) 659 | }; 660 | 661 | if (typeof final_pos.opacity !== 'undefined') { 662 | dim.opacity = pos < 0.5 ? 0.5 : pos; 663 | } 664 | 665 | wrap.css(dim); 666 | 667 | content.css({ 668 | 'width' : dim.width - currentOpts.padding * 2, 669 | 'height' : dim.height - (titleHeight * pos) - currentOpts.padding * 2 670 | }); 671 | }, 672 | 673 | _get_viewport = function() { 674 | return [ 675 | $(window).width() - (currentOpts.margin * 2), 676 | $(window).height() - (currentOpts.margin * 2), 677 | $(document).scrollLeft() + currentOpts.margin, 678 | $(document).scrollTop() + currentOpts.margin 679 | ]; 680 | }, 681 | 682 | _get_zoom_to = function () { 683 | var view = _get_viewport(), 684 | to = {}, 685 | resize = currentOpts.autoScale, 686 | double_padding = currentOpts.padding * 2, 687 | ratio; 688 | 689 | if (currentOpts.width.toString().indexOf('%') > -1) { 690 | to.width = parseInt((view[0] * parseFloat(currentOpts.width)) / 100, 10); 691 | } else { 692 | to.width = currentOpts.width + double_padding; 693 | } 694 | 695 | if (currentOpts.height.toString().indexOf('%') > -1) { 696 | to.height = parseInt((view[1] * parseFloat(currentOpts.height)) / 100, 10); 697 | } else { 698 | to.height = currentOpts.height + double_padding; 699 | } 700 | 701 | if (resize && (to.width > view[0] || to.height > view[1])) { 702 | if (selectedOpts.type == 'image' || selectedOpts.type == 'swf') { 703 | ratio = (currentOpts.width ) / (currentOpts.height ); 704 | 705 | if ((to.width ) > view[0]) { 706 | to.width = view[0]; 707 | to.height = parseInt(((to.width - double_padding) / ratio) + double_padding, 10); 708 | } 709 | 710 | if ((to.height) > view[1]) { 711 | to.height = view[1]; 712 | to.width = parseInt(((to.height - double_padding) * ratio) + double_padding, 10); 713 | } 714 | 715 | } else { 716 | to.width = Math.min(to.width, view[0]); 717 | to.height = Math.min(to.height, view[1]); 718 | } 719 | } 720 | 721 | to.top = parseInt(Math.max(view[3] - 20, view[3] + ((view[1] - to.height - 40) * 0.5)), 10); 722 | to.left = parseInt(Math.max(view[2] - 20, view[2] + ((view[0] - to.width - 40) * 0.5)), 10); 723 | 724 | return to; 725 | }, 726 | 727 | _get_obj_pos = function(obj) { 728 | var pos = obj.offset(); 729 | 730 | pos.top += parseInt( obj.css('paddingTop'), 10 ) || 0; 731 | pos.left += parseInt( obj.css('paddingLeft'), 10 ) || 0; 732 | 733 | pos.top += parseInt( obj.css('border-top-width'), 10 ) || 0; 734 | pos.left += parseInt( obj.css('border-left-width'), 10 ) || 0; 735 | 736 | pos.width = obj.width(); 737 | pos.height = obj.height(); 738 | 739 | return pos; 740 | }, 741 | 742 | _get_zoom_from = function() { 743 | var orig = selectedOpts.orig ? $(selectedOpts.orig) : false, 744 | from = {}, 745 | pos, 746 | view; 747 | 748 | if (orig && orig.length) { 749 | pos = _get_obj_pos(orig); 750 | 751 | from = { 752 | width : pos.width + (currentOpts.padding * 2), 753 | height : pos.height + (currentOpts.padding * 2), 754 | top : pos.top - currentOpts.padding - 20, 755 | left : pos.left - currentOpts.padding - 20 756 | }; 757 | 758 | } else { 759 | view = _get_viewport(); 760 | 761 | from = { 762 | width : currentOpts.padding * 2, 763 | height : currentOpts.padding * 2, 764 | top : parseInt(view[3] + view[1] * 0.5, 10), 765 | left : parseInt(view[2] + view[0] * 0.5, 10) 766 | }; 767 | } 768 | 769 | return from; 770 | }, 771 | 772 | _animate_loading = function() { 773 | if (!loading.is(':visible')){ 774 | clearInterval(loadingTimer); 775 | return; 776 | } 777 | 778 | $('div', loading).css('top', (loadingFrame * -40) + 'px'); 779 | 780 | loadingFrame = (loadingFrame + 1) % 12; 781 | }; 782 | 783 | /* 784 | * Public methods 785 | */ 786 | 787 | $.fn.fancybox = function(options) { 788 | if (!$(this).length) { 789 | return this; 790 | } 791 | 792 | $(this) 793 | .data('fancybox', $.extend({}, options, ($.metadata ? $(this).metadata() : {}))) 794 | .unbind('click.fb') 795 | .bind('click.fb', function(e) { 796 | e.preventDefault(); 797 | 798 | if (busy) { 799 | return; 800 | } 801 | 802 | busy = true; 803 | 804 | $(this).blur(); 805 | 806 | selectedArray = []; 807 | selectedIndex = 0; 808 | 809 | var rel = $(this).attr('rel') || ''; 810 | 811 | if (!rel || rel == '' || rel === 'nofollow') { 812 | selectedArray.push(this); 813 | 814 | } else { 815 | selectedArray = $("a[rel=" + rel + "], area[rel=" + rel + "]"); 816 | selectedIndex = selectedArray.index( this ); 817 | } 818 | 819 | _start(); 820 | 821 | return; 822 | }); 823 | 824 | return this; 825 | }; 826 | 827 | $.fancybox = function(obj) { 828 | var opts; 829 | 830 | if (busy) { 831 | return; 832 | } 833 | 834 | busy = true; 835 | opts = typeof arguments[1] !== 'undefined' ? arguments[1] : {}; 836 | 837 | selectedArray = []; 838 | selectedIndex = parseInt(opts.index, 10) || 0; 839 | 840 | if ($.isArray(obj)) { 841 | for (var i = 0, j = obj.length; i < j; i++) { 842 | if (typeof obj[i] == 'object') { 843 | $(obj[i]).data('fancybox', $.extend({}, opts, obj[i])); 844 | } else { 845 | obj[i] = $({}).data('fancybox', $.extend({content : obj[i]}, opts)); 846 | } 847 | } 848 | 849 | selectedArray = jQuery.merge(selectedArray, obj); 850 | 851 | } else { 852 | if (typeof obj == 'object') { 853 | $(obj).data('fancybox', $.extend({}, opts, obj)); 854 | } else { 855 | obj = $({}).data('fancybox', $.extend({content : obj}, opts)); 856 | } 857 | 858 | selectedArray.push(obj); 859 | } 860 | 861 | if (selectedIndex > selectedArray.length || selectedIndex < 0) { 862 | selectedIndex = 0; 863 | } 864 | 865 | _start(); 866 | }; 867 | 868 | $.fancybox.showActivity = function() { 869 | clearInterval(loadingTimer); 870 | 871 | loading.show(); 872 | loadingTimer = setInterval(_animate_loading, 66); 873 | }; 874 | 875 | $.fancybox.hideActivity = function() { 876 | loading.hide(); 877 | }; 878 | 879 | $.fancybox.next = function() { 880 | return $.fancybox.pos( currentIndex + 1); 881 | }; 882 | 883 | $.fancybox.prev = function() { 884 | return $.fancybox.pos( currentIndex - 1); 885 | }; 886 | 887 | $.fancybox.pos = function(pos) { 888 | if (busy) { 889 | return; 890 | } 891 | 892 | pos = parseInt(pos); 893 | 894 | selectedArray = currentArray; 895 | 896 | if (pos > -1 && pos < currentArray.length) { 897 | selectedIndex = pos; 898 | _start(); 899 | 900 | } else if (currentOpts.cyclic && currentArray.length > 1) { 901 | selectedIndex = pos >= currentArray.length ? 0 : currentArray.length - 1; 902 | _start(); 903 | } 904 | 905 | return; 906 | }; 907 | 908 | $.fancybox.cancel = function() { 909 | if (busy) { 910 | return; 911 | } 912 | 913 | busy = true; 914 | 915 | $.event.trigger('fancybox-cancel'); 916 | 917 | _abort(); 918 | 919 | selectedOpts.onCancel(selectedArray, selectedIndex, selectedOpts); 920 | 921 | busy = false; 922 | }; 923 | 924 | // Note: within an iframe use - parent.$.fancybox.close(); 925 | $.fancybox.close = function() { 926 | if (busy || wrap.is(':hidden')) { 927 | return; 928 | } 929 | 930 | busy = true; 931 | 932 | if (currentOpts && false === currentOpts.onCleanup(currentArray, currentIndex, currentOpts)) { 933 | busy = false; 934 | return; 935 | } 936 | 937 | _abort(); 938 | 939 | $(close.add( nav_left ).add( nav_right )).hide(); 940 | 941 | $(content.add( overlay )).unbind(); 942 | 943 | $(window).unbind("resize.fb scroll.fb"); 944 | $(document).unbind('keydown.fb'); 945 | 946 | content.find('iframe').attr('src', isIE6 && /^https/i.test(window.location.href || '') ? 'javascript:void(false)' : 'about:blank'); 947 | 948 | if (currentOpts.titlePosition !== 'inside') { 949 | title.empty(); 950 | } 951 | 952 | wrap.stop(); 953 | 954 | function _cleanup() { 955 | overlay.fadeOut('fast'); 956 | 957 | title.empty().hide(); 958 | wrap.hide(); 959 | 960 | $.event.trigger('fancybox-cleanup'); 961 | 962 | content.empty(); 963 | 964 | currentOpts.onClosed(currentArray, currentIndex, currentOpts); 965 | 966 | currentArray = selectedOpts = []; 967 | currentIndex = selectedIndex = 0; 968 | currentOpts = selectedOpts = {}; 969 | 970 | busy = false; 971 | } 972 | 973 | if (currentOpts.transitionOut == 'elastic') { 974 | start_pos = _get_zoom_from(); 975 | 976 | var pos = wrap.position(); 977 | 978 | final_pos = { 979 | top : pos.top , 980 | left : pos.left, 981 | width : wrap.width(), 982 | height : wrap.height() 983 | }; 984 | 985 | if (currentOpts.opacity) { 986 | final_pos.opacity = 1; 987 | } 988 | 989 | title.empty().hide(); 990 | 991 | fx.prop = 1; 992 | 993 | $(fx).animate({ prop: 0 }, { 994 | duration : currentOpts.speedOut, 995 | easing : currentOpts.easingOut, 996 | step : _draw, 997 | complete : _cleanup 998 | }); 999 | 1000 | } else { 1001 | wrap.fadeOut( currentOpts.transitionOut == 'none' ? 0 : currentOpts.speedOut, _cleanup); 1002 | } 1003 | }; 1004 | 1005 | $.fancybox.resize = function() { 1006 | if (overlay.is(':visible')) { 1007 | overlay.css('height', $(document).height()); 1008 | } 1009 | 1010 | $.fancybox.center(true); 1011 | }; 1012 | 1013 | $.fancybox.center = function() { 1014 | var view, align; 1015 | 1016 | if (busy) { 1017 | return; 1018 | } 1019 | 1020 | align = arguments[0] === true ? 1 : 0; 1021 | view = _get_viewport(); 1022 | 1023 | if (!align && (wrap.width() > view[0] || wrap.height() > view[1])) { 1024 | return; 1025 | } 1026 | 1027 | wrap 1028 | .stop() 1029 | .animate({ 1030 | 'top' : parseInt(Math.max(view[3] - 20, view[3] + ((view[1] - content.height() - 40) * 0.5) - currentOpts.padding)), 1031 | 'left' : parseInt(Math.max(view[2] - 20, view[2] + ((view[0] - content.width() - 40) * 0.5) - currentOpts.padding)) 1032 | }, typeof arguments[0] == 'number' ? arguments[0] : 200); 1033 | }; 1034 | 1035 | $.fancybox.init = function() { 1036 | if ($("#fancybox-wrap").length) { 1037 | return; 1038 | } 1039 | 1040 | $('body').append( 1041 | tmp = $('
'), 1042 | loading = $('
'), 1043 | overlay = $('
'), 1044 | wrap = $('
') 1045 | ); 1046 | 1047 | outer = $('
') 1048 | .append('
') 1049 | .appendTo( wrap ); 1050 | 1051 | outer.append( 1052 | content = $('
'), 1053 | close = $('
'), 1054 | title = $('
'), 1055 | 1056 | nav_left = $(''), 1057 | nav_right = $('') 1058 | ); 1059 | 1060 | close.click($.fancybox.close); 1061 | loading.click($.fancybox.cancel); 1062 | 1063 | nav_left.click(function(e) { 1064 | e.preventDefault(); 1065 | $.fancybox.prev(); 1066 | }); 1067 | 1068 | nav_right.click(function(e) { 1069 | e.preventDefault(); 1070 | $.fancybox.next(); 1071 | }); 1072 | 1073 | if ($.fn.mousewheel) { 1074 | wrap.bind('mousewheel.fb', function(e, delta) { 1075 | if (busy) { 1076 | e.preventDefault(); 1077 | 1078 | } else if ($(e.target).get(0).clientHeight == 0 || $(e.target).get(0).scrollHeight === $(e.target).get(0).clientHeight) { 1079 | e.preventDefault(); 1080 | $.fancybox[ delta > 0 ? 'prev' : 'next'](); 1081 | } 1082 | }); 1083 | } 1084 | 1085 | if (!$.support.opacity) { 1086 | wrap.addClass('fancybox-ie'); 1087 | } 1088 | 1089 | if (isIE6) { 1090 | loading.addClass('fancybox-ie6'); 1091 | wrap.addClass('fancybox-ie6'); 1092 | 1093 | $('').prependTo(outer); 1094 | } 1095 | }; 1096 | 1097 | $.fn.fancybox.defaults = { 1098 | padding : 10, 1099 | margin : 40, 1100 | opacity : false, 1101 | modal : false, 1102 | cyclic : false, 1103 | scrolling : 'auto', // 'auto', 'yes' or 'no' 1104 | 1105 | width : 560, 1106 | height : 340, 1107 | 1108 | autoScale : true, 1109 | autoDimensions : true, 1110 | centerOnScroll : false, 1111 | 1112 | ajax : {}, 1113 | swf : { wmode: 'transparent' }, 1114 | 1115 | hideOnOverlayClick : true, 1116 | hideOnContentClick : false, 1117 | 1118 | overlayShow : true, 1119 | overlayOpacity : 0.7, 1120 | overlayColor : '#777', 1121 | 1122 | titleShow : true, 1123 | titlePosition : 'float', // 'float', 'outside', 'inside' or 'over' 1124 | titleFormat : null, 1125 | titleFromAlt : false, 1126 | 1127 | transitionIn : 'fade', // 'elastic', 'fade' or 'none' 1128 | transitionOut : 'fade', // 'elastic', 'fade' or 'none' 1129 | 1130 | speedIn : 300, 1131 | speedOut : 300, 1132 | 1133 | changeSpeed : 300, 1134 | changeFade : 'fast', 1135 | 1136 | easingIn : 'swing', 1137 | easingOut : 'swing', 1138 | 1139 | showCloseButton : true, 1140 | showNavArrows : true, 1141 | enableEscapeButton : true, 1142 | enableKeyboardNav : true, 1143 | 1144 | onStart : function(){}, 1145 | onCancel : function(){}, 1146 | onComplete : function(){}, 1147 | onCleanup : function(){}, 1148 | onClosed : function(){}, 1149 | onError : function(){} 1150 | }; 1151 | 1152 | $(document).ready(function() { 1153 | $.fancybox.init(); 1154 | }); 1155 | 1156 | })(jQuery); -------------------------------------------------------------------------------- /static/fancybox/jquery.fancybox-1.3.4.pack.js: -------------------------------------------------------------------------------- 1 | /* 2 | * FancyBox - jQuery Plugin 3 | * Simple and fancy lightbox alternative 4 | * 5 | * Examples and documentation at: http://fancybox.net 6 | * 7 | * Copyright (c) 2008 - 2010 Janis Skarnelis 8 | * That said, it is hardly a one-person project. Many people have submitted bugs, code, and offered their advice freely. Their support is greatly appreciated. 9 | * 10 | * Version: 1.3.4 (11/11/2010) 11 | * Requires: jQuery v1.3+ 12 | * 13 | * Dual licensed under the MIT and GPL licenses: 14 | * http://www.opensource.org/licenses/mit-license.php 15 | * http://www.gnu.org/licenses/gpl.html 16 | */ 17 | 18 | ;(function(b){var m,t,u,f,D,j,E,n,z,A,q=0,e={},o=[],p=0,d={},l=[],G=null,v=new Image,J=/\.(jpg|gif|png|bmp|jpeg)(.*)?$/i,W=/[^\.]\.(swf)\s*$/i,K,L=1,y=0,s="",r,i,h=false,B=b.extend(b("
")[0],{prop:0}),M=b.browser.msie&&b.browser.version<7&&!window.XMLHttpRequest,N=function(){t.hide();v.onerror=v.onload=null;G&&G.abort();m.empty()},O=function(){if(false===e.onError(o,q,e)){t.hide();h=false}else{e.titleShow=false;e.width="auto";e.height="auto";m.html('

The requested content cannot be loaded.
Please try again later.

'); 19 | F()}},I=function(){var a=o[q],c,g,k,C,P,w;N();e=b.extend({},b.fn.fancybox.defaults,typeof b(a).data("fancybox")=="undefined"?e:b(a).data("fancybox"));w=e.onStart(o,q,e);if(w===false)h=false;else{if(typeof w=="object")e=b.extend(e,w);k=e.title||(a.nodeName?b(a).attr("title"):a.title)||"";if(a.nodeName&&!e.orig)e.orig=b(a).children("img:first").length?b(a).children("img:first"):b(a);if(k===""&&e.orig&&e.titleFromAlt)k=e.orig.attr("alt");c=e.href||(a.nodeName?b(a).attr("href"):a.href)||null;if(/^(?:javascript)/i.test(c)|| 20 | c=="#")c=null;if(e.type){g=e.type;if(!c)c=e.content}else if(e.content)g="html";else if(c)g=c.match(J)?"image":c.match(W)?"swf":b(a).hasClass("iframe")?"iframe":c.indexOf("#")===0?"inline":"ajax";if(g){if(g=="inline"){a=c.substr(c.indexOf("#"));g=b(a).length>0?"inline":"ajax"}e.type=g;e.href=c;e.title=k;if(e.autoDimensions)if(e.type=="html"||e.type=="inline"||e.type=="ajax"){e.width="auto";e.height="auto"}else e.autoDimensions=false;if(e.modal){e.overlayShow=true;e.hideOnOverlayClick=false;e.hideOnContentClick= 21 | false;e.enableEscapeButton=false;e.showCloseButton=false}e.padding=parseInt(e.padding,10);e.margin=parseInt(e.margin,10);m.css("padding",e.padding+e.margin);b(".fancybox-inline-tmp").unbind("fancybox-cancel").bind("fancybox-change",function(){b(this).replaceWith(j.children())});switch(g){case "html":m.html(e.content);F();break;case "inline":if(b(a).parent().is("#fancybox-content")===true){h=false;break}b('
').hide().insertBefore(b(a)).bind("fancybox-cleanup",function(){b(this).replaceWith(j.children())}).bind("fancybox-cancel", 22 | function(){b(this).replaceWith(m.children())});b(a).appendTo(m);F();break;case "image":h=false;b.fancybox.showActivity();v=new Image;v.onerror=function(){O()};v.onload=function(){h=true;v.onerror=v.onload=null;e.width=v.width;e.height=v.height;b("").attr({id:"fancybox-img",src:v.src,alt:e.title}).appendTo(m);Q()};v.src=c;break;case "swf":e.scrolling="no";C='';P="";b.each(e.swf,function(x,H){C+='';P+=" "+x+'="'+H+'"'});C+='";m.html(C);F();break;case "ajax":h=false;b.fancybox.showActivity();e.ajax.win=e.ajax.success;G=b.ajax(b.extend({},e.ajax,{url:c,data:e.ajax.data||{},error:function(x){x.status>0&&O()},success:function(x,H,R){if((typeof R=="object"?R:G).status==200){if(typeof e.ajax.win== 24 | "function"){w=e.ajax.win(c,x,H,R);if(w===false){t.hide();return}else if(typeof w=="string"||typeof w=="object")x=w}m.html(x);F()}}}));break;case "iframe":Q()}}else O()}},F=function(){var a=e.width,c=e.height;a=a.toString().indexOf("%")>-1?parseInt((b(window).width()-e.margin*2)*parseFloat(a)/100,10)+"px":a=="auto"?"auto":a+"px";c=c.toString().indexOf("%")>-1?parseInt((b(window).height()-e.margin*2)*parseFloat(c)/100,10)+"px":c=="auto"?"auto":c+"px";m.wrapInner('
');e.width=m.width();e.height=m.height();Q()},Q=function(){var a,c;t.hide();if(f.is(":visible")&&false===d.onCleanup(l,p,d)){b.event.trigger("fancybox-cancel");h=false}else{h=true;b(j.add(u)).unbind();b(window).unbind("resize.fb scroll.fb");b(document).unbind("keydown.fb");f.is(":visible")&&d.titlePosition!=="outside"&&f.css("height",f.height());l=o;p=q;d=e;if(d.overlayShow){u.css({"background-color":d.overlayColor, 26 | opacity:d.overlayOpacity,cursor:d.hideOnOverlayClick?"pointer":"auto",height:b(document).height()});if(!u.is(":visible")){M&&b("select:not(#fancybox-tmp select)").filter(function(){return this.style.visibility!=="hidden"}).css({visibility:"hidden"}).one("fancybox-cleanup",function(){this.style.visibility="inherit"});u.show()}}else u.hide();i=X();s=d.title||"";y=0;n.empty().removeAttr("style").removeClass();if(d.titleShow!==false){if(b.isFunction(d.titleFormat))a=d.titleFormat(s,l,p,d);else a=s&&s.length? 27 | d.titlePosition=="float"?'
'+s+'
':'
'+s+"
":false;s=a;if(!(!s||s==="")){n.addClass("fancybox-title-"+d.titlePosition).html(s).appendTo("body").show();switch(d.titlePosition){case "inside":n.css({width:i.width-d.padding*2,marginLeft:d.padding,marginRight:d.padding}); 28 | y=n.outerHeight(true);n.appendTo(D);i.height+=y;break;case "over":n.css({marginLeft:d.padding,width:i.width-d.padding*2,bottom:d.padding}).appendTo(D);break;case "float":n.css("left",parseInt((n.width()-i.width-40)/2,10)*-1).appendTo(f);break;default:n.css({width:i.width-d.padding*2,paddingLeft:d.padding,paddingRight:d.padding}).appendTo(f)}}}n.hide();if(f.is(":visible")){b(E.add(z).add(A)).hide();a=f.position();r={top:a.top,left:a.left,width:f.width(),height:f.height()};c=r.width==i.width&&r.height== 29 | i.height;j.fadeTo(d.changeFade,0.3,function(){var g=function(){j.html(m.contents()).fadeTo(d.changeFade,1,S)};b.event.trigger("fancybox-change");j.empty().removeAttr("filter").css({"border-width":d.padding,width:i.width-d.padding*2,height:e.autoDimensions?"auto":i.height-y-d.padding*2});if(c)g();else{B.prop=0;b(B).animate({prop:1},{duration:d.changeSpeed,easing:d.easingChange,step:T,complete:g})}})}else{f.removeAttr("style");j.css("border-width",d.padding);if(d.transitionIn=="elastic"){r=V();j.html(m.contents()); 30 | f.show();if(d.opacity)i.opacity=0;B.prop=0;b(B).animate({prop:1},{duration:d.speedIn,easing:d.easingIn,step:T,complete:S})}else{d.titlePosition=="inside"&&y>0&&n.show();j.css({width:i.width-d.padding*2,height:e.autoDimensions?"auto":i.height-y-d.padding*2}).html(m.contents());f.css(i).fadeIn(d.transitionIn=="none"?0:d.speedIn,S)}}}},Y=function(){if(d.enableEscapeButton||d.enableKeyboardNav)b(document).bind("keydown.fb",function(a){if(a.keyCode==27&&d.enableEscapeButton){a.preventDefault();b.fancybox.close()}else if((a.keyCode== 31 | 37||a.keyCode==39)&&d.enableKeyboardNav&&a.target.tagName!=="INPUT"&&a.target.tagName!=="TEXTAREA"&&a.target.tagName!=="SELECT"){a.preventDefault();b.fancybox[a.keyCode==37?"prev":"next"]()}});if(d.showNavArrows){if(d.cyclic&&l.length>1||p!==0)z.show();if(d.cyclic&&l.length>1||p!=l.length-1)A.show()}else{z.hide();A.hide()}},S=function(){if(!b.support.opacity){j.get(0).style.removeAttribute("filter");f.get(0).style.removeAttribute("filter")}e.autoDimensions&&j.css("height","auto");f.css("height","auto"); 32 | s&&s.length&&n.show();d.showCloseButton&&E.show();Y();d.hideOnContentClick&&j.bind("click",b.fancybox.close);d.hideOnOverlayClick&&u.bind("click",b.fancybox.close);b(window).bind("resize.fb",b.fancybox.resize);d.centerOnScroll&&b(window).bind("scroll.fb",b.fancybox.center);if(d.type=="iframe")b('').appendTo(j); 33 | f.show();h=false;b.fancybox.center();d.onComplete(l,p,d);var a,c;if(l.length-1>p){a=l[p+1].href;if(typeof a!=="undefined"&&a.match(J)){c=new Image;c.src=a}}if(p>0){a=l[p-1].href;if(typeof a!=="undefined"&&a.match(J)){c=new Image;c.src=a}}},T=function(a){var c={width:parseInt(r.width+(i.width-r.width)*a,10),height:parseInt(r.height+(i.height-r.height)*a,10),top:parseInt(r.top+(i.top-r.top)*a,10),left:parseInt(r.left+(i.left-r.left)*a,10)};if(typeof i.opacity!=="undefined")c.opacity=a<0.5?0.5:a;f.css(c); 34 | j.css({width:c.width-d.padding*2,height:c.height-y*a-d.padding*2})},U=function(){return[b(window).width()-d.margin*2,b(window).height()-d.margin*2,b(document).scrollLeft()+d.margin,b(document).scrollTop()+d.margin]},X=function(){var a=U(),c={},g=d.autoScale,k=d.padding*2;c.width=d.width.toString().indexOf("%")>-1?parseInt(a[0]*parseFloat(d.width)/100,10):d.width+k;c.height=d.height.toString().indexOf("%")>-1?parseInt(a[1]*parseFloat(d.height)/100,10):d.height+k;if(g&&(c.width>a[0]||c.height>a[1]))if(e.type== 35 | "image"||e.type=="swf"){g=d.width/d.height;if(c.width>a[0]){c.width=a[0];c.height=parseInt((c.width-k)/g+k,10)}if(c.height>a[1]){c.height=a[1];c.width=parseInt((c.height-k)*g+k,10)}}else{c.width=Math.min(c.width,a[0]);c.height=Math.min(c.height,a[1])}c.top=parseInt(Math.max(a[3]-20,a[3]+(a[1]-c.height-40)*0.5),10);c.left=parseInt(Math.max(a[2]-20,a[2]+(a[0]-c.width-40)*0.5),10);return c},V=function(){var a=e.orig?b(e.orig):false,c={};if(a&&a.length){c=a.offset();c.top+=parseInt(a.css("paddingTop"), 36 | 10)||0;c.left+=parseInt(a.css("paddingLeft"),10)||0;c.top+=parseInt(a.css("border-top-width"),10)||0;c.left+=parseInt(a.css("border-left-width"),10)||0;c.width=a.width();c.height=a.height();c={width:c.width+d.padding*2,height:c.height+d.padding*2,top:c.top-d.padding-20,left:c.left-d.padding-20}}else{a=U();c={width:d.padding*2,height:d.padding*2,top:parseInt(a[3]+a[1]*0.5,10),left:parseInt(a[2]+a[0]*0.5,10)}}return c},Z=function(){if(t.is(":visible")){b("div",t).css("top",L*-40+"px");L=(L+1)%12}else clearInterval(K)}; 37 | b.fn.fancybox=function(a){if(!b(this).length)return this;b(this).data("fancybox",b.extend({},a,b.metadata?b(this).metadata():{})).unbind("click.fb").bind("click.fb",function(c){c.preventDefault();if(!h){h=true;b(this).blur();o=[];q=0;c=b(this).attr("rel")||"";if(!c||c==""||c==="nofollow")o.push(this);else{o=b("a[rel="+c+"], area[rel="+c+"]");q=o.index(this)}I()}});return this};b.fancybox=function(a,c){var g;if(!h){h=true;g=typeof c!=="undefined"?c:{};o=[];q=parseInt(g.index,10)||0;if(b.isArray(a)){for(var k= 38 | 0,C=a.length;ko.length||q<0)q=0;I()}};b.fancybox.showActivity=function(){clearInterval(K);t.show();K=setInterval(Z,66)};b.fancybox.hideActivity=function(){t.hide()};b.fancybox.next=function(){return b.fancybox.pos(p+ 39 | 1)};b.fancybox.prev=function(){return b.fancybox.pos(p-1)};b.fancybox.pos=function(a){if(!h){a=parseInt(a);o=l;if(a>-1&&a1){q=a>=l.length?0:l.length-1;I()}}};b.fancybox.cancel=function(){if(!h){h=true;b.event.trigger("fancybox-cancel");N();e.onCancel(o,q,e);h=false}};b.fancybox.close=function(){function a(){u.fadeOut("fast");n.empty().hide();f.hide();b.event.trigger("fancybox-cleanup");j.empty();d.onClosed(l,p,d);l=e=[];p=q=0;d=e={};h=false}if(!(h||f.is(":hidden"))){h= 40 | true;if(d&&false===d.onCleanup(l,p,d))h=false;else{N();b(E.add(z).add(A)).hide();b(j.add(u)).unbind();b(window).unbind("resize.fb scroll.fb");b(document).unbind("keydown.fb");j.find("iframe").attr("src",M&&/^https/i.test(window.location.href||"")?"javascript:void(false)":"about:blank");d.titlePosition!=="inside"&&n.empty();f.stop();if(d.transitionOut=="elastic"){r=V();var c=f.position();i={top:c.top,left:c.left,width:f.width(),height:f.height()};if(d.opacity)i.opacity=1;n.empty().hide();B.prop=1; 41 | b(B).animate({prop:0},{duration:d.speedOut,easing:d.easingOut,step:T,complete:a})}else f.fadeOut(d.transitionOut=="none"?0:d.speedOut,a)}}};b.fancybox.resize=function(){u.is(":visible")&&u.css("height",b(document).height());b.fancybox.center(true)};b.fancybox.center=function(a){var c,g;if(!h){g=a===true?1:0;c=U();!g&&(f.width()>c[0]||f.height()>c[1])||f.stop().animate({top:parseInt(Math.max(c[3]-20,c[3]+(c[1]-j.height()-40)*0.5-d.padding)),left:parseInt(Math.max(c[2]-20,c[2]+(c[0]-j.width()-40)*0.5- 42 | d.padding))},typeof a=="number"?a:200)}};b.fancybox.init=function(){if(!b("#fancybox-wrap").length){b("body").append(m=b('
'),t=b('
'),u=b('
'),f=b('
'));D=b('
').append('
').appendTo(f); 43 | D.append(j=b('
'),E=b(''),n=b('
'),z=b(''),A=b(''));E.click(b.fancybox.close);t.click(b.fancybox.cancel);z.click(function(a){a.preventDefault();b.fancybox.prev()});A.click(function(a){a.preventDefault();b.fancybox.next()}); 44 | b.fn.mousewheel&&f.bind("mousewheel.fb",function(a,c){if(h)a.preventDefault();else if(b(a.target).get(0).clientHeight==0||b(a.target).get(0).scrollHeight===b(a.target).get(0).clientHeight){a.preventDefault();b.fancybox[c>0?"prev":"next"]()}});b.support.opacity||f.addClass("fancybox-ie");if(M){t.addClass("fancybox-ie6");f.addClass("fancybox-ie6");b('').prependTo(D)}}}; 45 | b.fn.fancybox.defaults={padding:10,margin:40,opacity:false,modal:false,cyclic:false,scrolling:"auto",width:560,height:340,autoScale:true,autoDimensions:true,centerOnScroll:false,ajax:{},swf:{wmode:"transparent"},hideOnOverlayClick:true,hideOnContentClick:false,overlayShow:true,overlayOpacity:0.7,overlayColor:"#777",titleShow:true,titlePosition:"float",titleFormat:null,titleFromAlt:false,transitionIn:"fade",transitionOut:"fade",speedIn:300,speedOut:300,changeSpeed:300,changeFade:"fast",easingIn:"swing", 46 | easingOut:"swing",showCloseButton:true,showNavArrows:true,enableEscapeButton:true,enableKeyboardNav:true,onStart:function(){},onCancel:function(){},onComplete:function(){},onCleanup:function(){},onClosed:function(){},onError:function(){}};b(document).ready(function(){b.fancybox.init()})})(jQuery); -------------------------------------------------------------------------------- /static/fancybox/jquery.mousewheel-3.0.4.pack.js: -------------------------------------------------------------------------------- 1 | /*! Copyright (c) 2010 Brandon Aaron (http://brandonaaron.net) 2 | * Licensed under the MIT License (LICENSE.txt). 3 | * 4 | * Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers. 5 | * Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix. 6 | * Thanks to: Seamus Leahy for adding deltaX and deltaY 7 | * 8 | * Version: 3.0.4 9 | * 10 | * Requires: 1.2.2+ 11 | */ 12 | 13 | (function(d){function g(a){var b=a||window.event,i=[].slice.call(arguments,1),c=0,h=0,e=0;a=d.event.fix(b);a.type="mousewheel";if(a.wheelDelta)c=a.wheelDelta/120;if(a.detail)c=-a.detail/3;e=c;if(b.axis!==undefined&&b.axis===b.HORIZONTAL_AXIS){e=0;h=-1*c}if(b.wheelDeltaY!==undefined)e=b.wheelDeltaY/120;if(b.wheelDeltaX!==undefined)h=-1*b.wheelDeltaX/120;i.unshift(a,c,h,e);return d.event.handle.apply(this,i)}var f=["DOMMouseScroll","mousewheel"];d.event.special.mousewheel={setup:function(){if(this.addEventListener)for(var a= 14 | f.length;a;)this.addEventListener(f[--a],g,false);else this.onmousewheel=g},teardown:function(){if(this.removeEventListener)for(var a=f.length;a;)this.removeEventListener(f[--a],g,false);else this.onmousewheel=null}};d.fn.extend({mousewheel:function(a){return a?this.bind("mousewheel",a):this.trigger("mousewheel")},unmousewheel:function(a){return this.unbind("mousewheel",a)}})})(jQuery); -------------------------------------------------------------------------------- /static/images/chuck.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/chuck.jpg -------------------------------------------------------------------------------- /static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/favicon.ico -------------------------------------------------------------------------------- /static/images/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/link.png -------------------------------------------------------------------------------- /static/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/loading.gif -------------------------------------------------------------------------------- /static/images/rock.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/rock.jpg -------------------------------------------------------------------------------- /static/images/screengrab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/screengrab.png -------------------------------------------------------------------------------- /static/images/screengrab2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/screengrab2.png -------------------------------------------------------------------------------- /static/images/screenshots/bookmarklet-in-toolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/screenshots/bookmarklet-in-toolbar.png -------------------------------------------------------------------------------- /static/images/screenshots/bookmarklet-in-toolbar_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/screenshots/bookmarklet-in-toolbar_small.png -------------------------------------------------------------------------------- /static/images/screenshots/everyone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/screenshots/everyone.png -------------------------------------------------------------------------------- /static/images/screenshots/everyone_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/screenshots/everyone_small.png -------------------------------------------------------------------------------- /static/images/screenshots/follows-me.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/screenshots/follows-me.png -------------------------------------------------------------------------------- /static/images/screenshots/follows-me_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/screenshots/follows-me_small.png -------------------------------------------------------------------------------- /static/images/screenshots/lookups.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/screenshots/lookups.png -------------------------------------------------------------------------------- /static/images/screenshots/lookups_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/screenshots/lookups_small.png -------------------------------------------------------------------------------- /static/images/screenshots/on-twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/screenshots/on-twitter.png -------------------------------------------------------------------------------- /static/images/screenshots/on-twitter_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/screenshots/on-twitter_small.png -------------------------------------------------------------------------------- /static/images/screenshots/too-cool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/screenshots/too-cool.png -------------------------------------------------------------------------------- /static/images/screenshots/too-cool_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/screenshots/too-cool_small.png -------------------------------------------------------------------------------- /static/images/twitter-small-mirrored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/twitter-small-mirrored.png -------------------------------------------------------------------------------- /static/images/twitter-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/twitter-small.png -------------------------------------------------------------------------------- /static/images/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/static/images/twitter.png -------------------------------------------------------------------------------- /static/js/chart.js: -------------------------------------------------------------------------------- 1 | function Chart(div_id, options) { 2 | this.div_id = div_id; 3 | this.options = options; 4 | this.data = new google.visualization.DataTable(); 5 | this.data.addColumn('string', '#'); 6 | this.data.addColumn('number', options.title); 7 | this.chart = new google.visualization.LineChart(document.getElementById(div_id)); 8 | this.draw = function() { 9 | this.chart.draw(this.data, this.options); 10 | }; 11 | } 12 | 13 | 14 | Chart.prototype.add_value = function(n) { 15 | // now we have at least 2 values 16 | this.data.addRow(); 17 | var i = this.data.getNumberOfRows() - 1; 18 | this.data.setValue(i, 0, '' + (i + 1)); 19 | this.data.setValue(i, 1, n); 20 | if (i > 0) { // second iteration 21 | this.draw(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /static/js/lookups.js: -------------------------------------------------------------------------------- 1 | function L() { 2 | if (window.console && window.console.log) 3 | console.log.apply(console, arguments); 4 | } 5 | 6 | function compareAssociativeArrays(a, b) { 7 | function nrKeys(a) { 8 | var i = 0; 9 | for (key in a) { 10 | i++; 11 | } 12 | return i; 13 | } 14 | if (a == b) { 15 | return true; 16 | } 17 | if (nrKeys(a) != nrKeys(b)) { 18 | return false; 19 | } 20 | for (key in a) { 21 | if (a[key] != b[key]) { 22 | return false; 23 | } 24 | } 25 | return true; 26 | } 27 | 28 | function tsep(n,swap) { 29 | var ts=",", ds="."; // thousands and decimal separators 30 | if (swap) { ts=","; ts="."; } // swap if requested 31 | 32 | var ns = String(n),ps=ns,ss=""; // numString, prefixString, suffixString 33 | var i = ns.indexOf("."); 34 | if (i!=-1) { // if ".", then split: 35 | ps = ns.substring(0,i); 36 | ss = ds+ns.substring(i+1); 37 | } 38 | return ps.replace(/(\d)(?=(\d{3})+([.]|$))/g,"$1"+ts)+ss; 39 | } 40 | 41 | 42 | // globals 43 | var previous = {} 44 | , incr = 0 45 | , chart_jsons = null 46 | , chart_jsonps = null 47 | , chart_usernames = null 48 | , chart_auths = null 49 | ; 50 | 51 | function cloneObject(source) { 52 | for (i in source) { 53 | if (typeof source[i] == 'source') { 54 | this[i] = new cloneObject(source[i]); 55 | } 56 | else{ 57 | this[i] = source[i]; 58 | } 59 | } 60 | } 61 | function _set_up_charts(numbers) { 62 | var options = { 63 | curveType: 'function', 64 | legend: 'none', 65 | width:400, 66 | height:300, 67 | lineWidth:3 68 | }; 69 | if (chart_jsons === null && numbers.lookups_json) { 70 | var p = new cloneObject(options); 71 | p.title = 'Twitter requests by JSON'; 72 | chart_jsons = new Chart('chart-jsons', p); 73 | } 74 | if (chart_jsonps === null && numbers.lookups_jsonp) { 75 | var p = new cloneObject(options); 76 | p.title = 'Twitter requests by JSONP'; 77 | chart_jsonps = new Chart('chart-jsonps', p); 78 | } 79 | if (chart_usernames === null && numbers.lookups_usernames) { 80 | var p = new cloneObject(options); 81 | p.title = 'Total number of usernames looked up'; 82 | p.series = [{color: 'green'}]; 83 | chart_usernames = new Chart('chart-usernames', p); 84 | } 85 | if (chart_auths === null && numbers.auths) { 86 | var p = new cloneObject(options); 87 | p.title = 'Authentications'; 88 | p.series = [{color: 'red'}]; 89 | chart_auths = new Chart('chart-auths', p); 90 | } 91 | 92 | if (numbers.lookups_json) 93 | chart_jsons.add_value(numbers.lookups_json); 94 | if (numbers.lookups_jsonp) 95 | chart_jsonps.add_value(numbers.lookups_jsonp); 96 | if (numbers.lookups_usernames) 97 | chart_usernames.add_value(numbers.lookups_usernames); 98 | if (numbers.auths) 99 | chart_auths.add_value(numbers.auths); 100 | 101 | } 102 | 103 | function incr_number(key, num) { 104 | var before = $(key).text(); 105 | if (before !== '' + tsep(num)) { 106 | // there's a change! 107 | $(key).fadeTo(200, 0.1, function() { 108 | $(this).text(tsep(num)).fadeTo(300, 1.0); 109 | }); 110 | } 111 | } 112 | 113 | function process_response(response) { 114 | if (response.lookups_json && response.lookups_jsonp) 115 | incr_number('#lookups-total', response.lookups_json + response.lookups_jsonp); 116 | if (response.lookups_json) 117 | incr_number('#lookups-json', response.lookups_json); 118 | if (response.lookups_jsonp) 119 | incr_number('#lookups-jsonp', response.lookups_jsonp); 120 | if (response.lookups_usernames) 121 | incr_number('#lookups-usernames', response.lookups_usernames); 122 | if (response.auths) 123 | incr_number('#auths', response.auths); 124 | _set_up_charts(response); 125 | } 126 | 127 | /* 128 | function update() { 129 | $.getJSON(JSON_URL, function(response) { 130 | process_response(response); 131 | var change = !compareAssociativeArrays(response, previous); 132 | previous = response; 133 | 134 | var t; 135 | if (change) { 136 | t = 1; 137 | incr = 0; 138 | _set_up_charts(response); 139 | } else { 140 | t = Math.min(3 + incr, 10); 141 | incr += 0.1; 142 | } 143 | setTimeout(update, Math.ceil(t * 1000)); 144 | }); 145 | } 146 | */ 147 | 148 | window.WEB_SOCKET_DEBUG = true; 149 | function setupSocket() { 150 | var socket = new io.connect('http://' + window.location.host, { 151 | port: 8888 152 | }); 153 | 154 | socket.on('connect', function() { 155 | socket.on('message', function(msg) { 156 | process_response(msg); 157 | }); 158 | 159 | }); 160 | } 161 | 162 | $(function() { 163 | setupSocket(); 164 | }); 165 | -------------------------------------------------------------------------------- /static/js/socket.io.min.js: -------------------------------------------------------------------------------- 1 | /*! Socket.IO.min.js build:0.8.7, production. Copyright(c) 2011 LearnBoost MIT Licensed */ 2 | (function(a,b){var c=a;c.version="0.8.7",c.protocol=1,c.transports=[],c.j=[],c.sockets={},c.connect=function(a,d){var e=c.util.parseUri(a),f,g;b&&b.location&&(e.protocol=e.protocol||b.location.protocol.slice(0,-1),e.host=e.host||(b.document?b.document.domain:b.location.hostname),e.port=e.port||b.location.port),f=c.util.uniqueUri(e);var h={host:e.host,secure:"https"==e.protocol,port:e.port||("https"==e.protocol?443:80),query:e.query||""};c.util.merge(h,d);if(h["force new connection"]||!c.sockets[f])g=new c.Socket(h);!h["force new connection"]&&g&&(c.sockets[f]=g),g=g||c.sockets[f];return g.of(e.path.length>1?e.path:"")}})("object"==typeof module?module.exports:this.io={},this),function(a,b){var c=a.util={},d=/^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/,e=["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"];c.parseUri=function(a){var b=d.exec(a||""),c={},f=14;while(f--)c[e[f]]=b[f]||"";return c},c.uniqueUri=function(a){var c=a.protocol,d=a.host,e=a.port;"document"in b?(d=d||document.domain,e=e||(c=="https"&&document.location.protocol!=="https:"?443:document.location.port)):(d=d||"localhost",!e&&c=="https"&&(e=443));return(c||"http")+"://"+d+":"+(e||80)},c.query=function(a,b){var d=c.chunkQuery(a||""),e=[];c.merge(d,c.chunkQuery(b||""));for(var f in d)d.hasOwnProperty(f)&&e.push(f+"="+d[f]);return e.length?"?"+e.join("&"):""},c.chunkQuery=function(a){var b={},c=a.split("&"),d=0,e=c.length,f;for(;db.length?a:b,f=a.length>b.length?b:a;for(var g=0,h=f.length;g0&&a.splice(0,1)[0]!=c.transport.name);a.length?h(a):c.publish("connect_failed")}}},c.options["connect timeout"]))})}c.sessionid=d,c.closeTimeout=f*1e3,c.heartbeatTimeout=e*1e3,c.transports=b.util.intersect(g.split(","),c.options.transports),h(),c.once("connect",function(){clearTimeout(c.connectTimeoutTimer),a&&typeof a=="function"&&a()})});return this},d.prototype.packet=function(a){this.connected&&!this.doBuffer?this.transport.packet(a):this.buffer.push(a);return this},d.prototype.setBuffer=function(a){this.doBuffer=a,!a&&this.connected&&this.buffer.length&&(this.transport.payload(this.buffer),this.buffer=[])},d.prototype.disconnect=function(){this.connected&&(this.open&&this.of("").packet({type:"disconnect"}),this.onDisconnect("booted"));return this},d.prototype.disconnectSync=function(){var a=b.util.request(),c=this.resource+"/"+b.protocol+"/"+this.sessionid;a.open("GET",c,!0),this.onDisconnect("booted")},d.prototype.isXDomain=function(){var a=c.location.port||("https:"==c.location.protocol?443:80);return this.options.host!==c.location.hostname||this.options.port!=a},d.prototype.onConnect=function(){this.connected||(this.connected=!0,this.connecting=!1,this.doBuffer||this.setBuffer(!1),this.emit("connect"))},d.prototype.onOpen=function(){this.open=!0},d.prototype.onClose=function(){this.open=!1},d.prototype.onPacket=function(a){this.of(a.endpoint).onPacket(a)},d.prototype.onError=function(a){a&&a.advice&&a.advice==="reconnect"&&this.connected&&(this.disconnect(),this.reconnect()),this.publish("error",a&&a.reason?a.reason:a)},d.prototype.onDisconnect=function(a){var b=this.connected;this.connected=!1,this.connecting=!1,this.open=!1,b&&(this.transport.close(),this.transport.clearTimeouts(),this.publish("disconnect",a),"booted"!=a&&this.options.reconnect&&!this.reconnecting&&this.reconnect())},d.prototype.reconnect=function(){function f(){if(!!a.reconnecting){if(a.connected)return e();if(a.connecting&&a.reconnecting)return a.reconnectionTimer=setTimeout(f,1e3);a.reconnectionAttempts++>=b?a.redoTransports?(a.publish("reconnect_failed"),e()):(a.on("connect_failed",f),a.options["try multiple transports"]=!0,a.transport=a.getTransport(),a.redoTransports=!0,a.connect()):(a.reconnectionDelay=10:!1},c.xdomainCheck=function(){return!0},typeof window!="undefined"&&(WEB_SOCKET_DISABLE_AUTO_INITIALIZATION=!0),b.transports.push("flashsocket")}("undefined"!=typeof io?io.Transport:module.exports,"undefined"!=typeof io?io:module.parent.exports);if("undefined"!=typeof window)var swfobject=function(){function V(b){var c=/[\\\"<>\.;]/,d=c.exec(b)!=null;return d&&typeof encodeURIComponent!=a?encodeURIComponent(b):b}function U(a,b){if(!!x){var c=b?"visible":"hidden";t&&P(a)?P(a).style.visibility=c:T("#"+a,"visibility:"+c)}}function T(c,d,e,f){if(!y.ie||!y.mac){var g=i.getElementsByTagName("head")[0];if(!g)return;var h=e&&typeof e=="string"?e:"screen";f&&(v=null,w=null);if(!v||w!=h){var j=Q("style");j.setAttribute("type","text/css"),j.setAttribute("media",h),v=g.appendChild(j),y.ie&&y.win&&typeof i.styleSheets!=a&&i.styleSheets.length>0&&(v=i.styleSheets[i.styleSheets.length-1]),w=h}y.ie&&y.win?v&&typeof v.addRule==b&&v.addRule(c,d):v&&typeof i.createTextNode!=a&&v.appendChild(i.createTextNode(c+" {"+d+"}"))}}function S(a){var b=y.pv,c=a.split(".");c[0]=parseInt(c[0],10),c[1]=parseInt(c[1],10)||0,c[2]=parseInt(c[2],10)||0;return b[0]>c[0]||b[0]==c[0]&&b[1]>c[1]||b[0]==c[0]&&b[1]==c[1]&&b[2]>=c[2]?!0:!1}function R(a,b,c){a.attachEvent(b,c),o[o.length]=[a,b,c]}function Q(a){return i.createElement(a)}function P(a){var b=null;try{b=i.getElementById(a)}catch(c){}return b}function O(a){var b=P(a);if(b){for(var c in b)typeof b[c]=="function"&&(b[c]=null);b.parentNode.removeChild(b)}}function N(a){var b=P(a);b&&b.nodeName=="OBJECT"&&(y.ie&&y.win?(b.style.display="none",function(){b.readyState==4?O(a):setTimeout(arguments.callee,10)}()):b.parentNode.removeChild(b))}function M(a,b,c){var d=Q("param");d.setAttribute("name",b),d.setAttribute("value",c),a.appendChild(d)}function L(c,d,f){var g,h=P(f);if(y.wk&&y.wk<312)return g;if(h){typeof c.id==a&&(c.id=f);if(y.ie&&y.win){var i="";for(var j in c)c[j]!=Object.prototype[j]&&(j.toLowerCase()=="data"?d.movie=c[j]:j.toLowerCase()=="styleclass"?i+=' class="'+c[j]+'"':j.toLowerCase()!="classid"&&(i+=" "+j+'="'+c[j]+'"'));var k="";for(var l in d)d[l]!=Object.prototype[l]&&(k+='');h.outerHTML='"+k+"",n[n.length]=c.id,g=P(c.id)}else{var m=Q(b);m.setAttribute("type",e);for(var o in c)c[o]!=Object.prototype[o]&&(o.toLowerCase()=="styleclass"?m.setAttribute("class",c[o]):o.toLowerCase()!="classid"&&m.setAttribute(o,c[o]));for(var p in d)d[p]!=Object.prototype[p]&&p.toLowerCase()!="movie"&&M(m,p,d[p]);h.parentNode.replaceChild(m,h),g=m}}return g}function K(a){var c=Q("div");if(y.win&&y.ie)c.innerHTML=a.innerHTML;else{var d=a.getElementsByTagName(b)[0];if(d){var e=d.childNodes;if(e){var f=e.length;for(var g=0;g0)for(var c=0;c0){var g=P(d);if(g)if(S(m[c].swfVersion)&&!(y.wk&&y.wk<312))U(d,!0),e&&(f.success=!0,f.ref=G(d),e(f));else if(m[c].expressInstall&&H()){var h={};h.data=m[c].expressInstall,h.width=g.getAttribute("width")||"0",h.height=g.getAttribute("height")||"0",g.getAttribute("class")&&(h.styleclass=g.getAttribute("class")),g.getAttribute("align")&&(h.align=g.getAttribute("align"));var i={},j=g.getElementsByTagName("param"),k=j.length;for(var l=0;l= 10.0.0 is required.");return}location.protocol=="file:"&&a.error("WARNING: web-socket-js doesn't work in file:///... URL unless you set Flash Security Settings properly. Open the page via Web server i.e. http://..."),WebSocket=function(a,b,c,d,e){var f=this;f.__id=WebSocket.__nextId++,WebSocket.__instances[f.__id]=f,f.readyState=WebSocket.CONNECTING,f.bufferedAmount=0,f.__events={},b?typeof b=="string"&&(b=[b]):b=[],setTimeout(function(){WebSocket.__addTask(function(){WebSocket.__flash.create(f.__id,a,b,c||null,d||0,e||null)})},0)},WebSocket.prototype.send=function(a){if(this.readyState==WebSocket.CONNECTING)throw"INVALID_STATE_ERR: Web Socket connection has not been established";var b=WebSocket.__flash.send(this.__id,encodeURIComponent(a));if(b<0)return!0;this.bufferedAmount+=b;return!1},WebSocket.prototype.close=function(){this.readyState!=WebSocket.CLOSED&&this.readyState!=WebSocket.CLOSING&&(this.readyState=WebSocket.CLOSING,WebSocket.__flash.close(this.__id))},WebSocket.prototype.addEventListener=function(a,b,c){a in this.__events||(this.__events[a]=[]),this.__events[a].push(b)},WebSocket.prototype.removeEventListener=function(a,b,c){if(a in this.__events){var d=this.__events[a];for(var e=d.length-1;e>=0;--e)if(d[e]===b){d.splice(e,1);break}}},WebSocket.prototype.dispatchEvent=function(a){var b=this.__events[a.type]||[];for(var c=0;c"),this.doc.close(),this.doc.parentWindow.s=this;var a=this.doc.createElement("div");a.className="socketio",this.doc.body.appendChild(a),this.iframe=this.doc.createElement("iframe"),a.appendChild(this.iframe);var c=this,d=b.util.query(this.socket.options.query,"t="+ +(new Date));this.iframe.src=this.prepareUrl()+d,b.util.on(window,"unload",function(){c.destroy()})},c.prototype._=function(a,b){this.onData(a);try{var c=b.getElementsByTagName("script")[0];c.parentNode.removeChild(c)}catch(d){}},c.prototype.destroy=function(){if(this.iframe){try{this.iframe.src="about:blank"}catch(a){}this.doc=null,this.iframe.parentNode.removeChild(this.iframe),this.iframe=null,CollectGarbage()}},c.prototype.close=function(){this.destroy();return b.Transport.XHR.prototype.close.call(this)},c.check=function(){if("ActiveXObject"in window)try{var a=new ActiveXObject("htmlfile");return a&&b.Transport.XHR.check()}catch(c){}return!1},c.xdomainCheck=function(){return!1},b.transports.push("htmlfile")}("undefined"!=typeof io?io.Transport:module.exports,"undefined"!=typeof io?io:module.parent.exports),function(a,b,c){function e(){}function d(){b.Transport.XHR.apply(this,arguments)}a["xhr-polling"]=d,b.util.inherit(d,b.Transport.XHR),b.util.merge(d,b.Transport.XHR),d.prototype.name="xhr-polling",d.prototype.open=function(){var a=this;b.Transport.XHR.prototype.open.call(a);return!1},d.prototype.get=function(){function d(){this.onload=e,a.onData(this.responseText),a.get()}function b(){this.readyState==4&&(this.onreadystatechange=e,this.status==200?(a.onData(this.responseText),a.get()):a.onClose())}if(!!this.open){var a=this;this.xhr=this.request(),c.XDomainRequest&&this.xhr instanceof XDomainRequest?this.xhr.onload=this.xhr.onerror=d:this.xhr.onreadystatechange=b,this.xhr.send(null)}},d.prototype.onClose=function(){b.Transport.XHR.prototype.onClose.call(this);if(this.xhr){this.xhr.onreadystatechange=this.xhr.onload=e;try{this.xhr.abort()}catch(a){}this.xhr=null}},d.prototype.ready=function(a,c){var d=this;b.util.defer(function(){c.call(d)})},b.transports.push("xhr-polling")}("undefined"!=typeof io?io.Transport:module.exports,"undefined"!=typeof io?io:module.parent.exports,this),function(a,b,c){function e(a){b.Transport["xhr-polling"].apply(this,arguments),this.index=b.j.length;var c=this;b.j.push(function(a){c._(a)})}var d=c.document&&"MozAppearance"in c.document.documentElement.style;a["jsonp-polling"]=e,b.util.inherit(e,b.Transport["xhr-polling"]),e.prototype.name="jsonp-polling",e.prototype.post=function(a){function j(){c.iframe&&c.form.removeChild(c.iframe);try{h=document.createElement(' 80 |

Or, watch it on YouTube

81 |

Or if you think it helps, check out the screenshots

82 |
83 | 84 | {% end %} 85 | -------------------------------------------------------------------------------- /templates/lookups.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block extra_head %} 4 | 10 | {% end %} 11 | 12 | 13 | {% block content %} 14 |

Lookups

15 | 16 |

In other words, is this app being actively used?

17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
Total number of usernames looked up:{% module Thousands(lookups_usernames) %}
Twitter requests total:{% module Thousands(lookups_json + lookups_jsonp) %}
Twitter requests by JSON:{% module Thousands(lookups_json) %}
Twitter requests by JSONP:{% module Thousands(lookups_jsonp) %}
Authentications:{% module Thousands(auths) %}
41 |
42 |
43 |
44 |
45 |
46 |
47 |
 
48 | {% end %} 49 | 50 | {% block extra_js %} 51 | 52 | 55 | 56 | 57 | 58 | 59 | 62 | {% end %} 63 | -------------------------------------------------------------------------------- /templates/screenshots.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block extra_head %} 4 | 5 | 8 | {% end %} 9 | 10 | 11 | {% block content %} 12 | 13 |

14 | 15 | {% for file_path, file_path_small, title in images %} 16 | {{ title }} 18 | {% end %} 19 | 20 |

21 | 22 | {% end %} 23 | 24 | {% block extra_js %} 25 | 26 | 27 | 28 | 29 | 72 | {% end %} 73 | -------------------------------------------------------------------------------- /templates/test.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 | 5 |
6 |

Type some Twitter user names below to see who's too cool for you.
7 | Also include some user names you know follow you:

8 |
9 | 13 |
14 |
15 | 16 |
17 |
18 |
 
19 | {% end %} 20 | 21 | {% block extra_js %} 22 | 23 | 55 | {% end %} 56 | -------------------------------------------------------------------------------- /templates/twitter_auth_failed.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 |

Sorry, Twitter's authentication link failed

4 | 5 |

Unfortunately, sometimes Twitter's server run over capacity and just simply don't work as 6 | expected. Most of them time, the solution is very simple...

7 | 8 |

Try again →

9 | 10 | {% end %} 11 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/toocool/d6e24e55728585addbad945122e7293e8903d14a/tests/__init__.py -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import time 3 | import datetime 4 | import mimetypes 5 | import os 6 | import re 7 | import hmac 8 | import hashlib 9 | import unittest 10 | 11 | 12 | from tornado.testing import LogTrapTestCase, AsyncHTTPTestCase 13 | os.environ['ALWAYS_EAGER'] = 'true' 14 | import celery 15 | import settings 16 | 17 | import app 18 | from tornado_utils.http_test_client import TestClient, HTTPClientMixin 19 | 20 | 21 | class DatabaseTestCaseMixin(object): 22 | _once = False 23 | 24 | def setup_connection(self): 25 | if not self._once: 26 | self._once = True 27 | assert 'test' in self.db.name 28 | self._emptyCollections() 29 | 30 | def teardown_connection(self): 31 | self._emptyCollections() 32 | 33 | def _emptyCollections(self): 34 | [self.db.drop_collection(x) for x 35 | in self.db.collection_names() 36 | if x not in ('system.indexes',)] 37 | 38 | 39 | class BaseAsyncTestCase(AsyncHTTPTestCase, DatabaseTestCaseMixin): 40 | 41 | def setUp(self): 42 | super(BaseAsyncTestCase, self).setUp() 43 | self.setup_connection() 44 | 45 | def tearDown(self): 46 | super(BaseAsyncTestCase, self).tearDown() 47 | self.teardown_connection() 48 | 49 | 50 | class BaseHTTPTestCase(BaseAsyncTestCase, HTTPClientMixin): 51 | 52 | def setUp(self): 53 | super(BaseHTTPTestCase, self).setUp() 54 | 55 | self._app.settings['email_backend'] = \ 56 | 'tornado_utils.send_mail.backends.locmem.EmailBackend' 57 | self._app.settings['email_exceptions'] = False 58 | self.client = TestClient(self) 59 | celery.conf.ALWAYS_EAGER = True 60 | settings.DATABASE_NAME = 'test' 61 | 62 | def tearDown(self): 63 | super(BaseHTTPTestCase, self).tearDown() 64 | self.redis.flushall() 65 | 66 | def get_app(self): 67 | return app.Application(database_name='test') 68 | 69 | @property 70 | def redis(self): 71 | return self.get_app().redis 72 | 73 | @property 74 | def db(self): 75 | return self.get_app().db 76 | 77 | def decode_cookie_value(self, key, cookie_value): 78 | try: 79 | return re.findall('%s=([\w=\|]+);' % key, cookie_value)[0] 80 | except IndexError: 81 | raise ValueError("couldn't find %r in %r" % (key, cookie_value)) 82 | 83 | def reverse_url(self, *args, **kwargs): 84 | return self._app.reverse_url(*args, **kwargs) 85 | 86 | 87 | ## these two are shamelessly copied from tornado.web.RequestHandler 88 | ## because in the _login() we have no access to a request and 89 | ## we need to be able to set a cookie 90 | def create_signed_value(self, name, value): 91 | """Signs and timestamps a string so it cannot be forged. 92 | 93 | Normally used via set_secure_cookie, but provided as a separate 94 | method for non-cookie uses. To decode a value not stored 95 | as a cookie use the optional value argument to get_secure_cookie. 96 | """ 97 | timestamp = str(int(time.time())) 98 | value = base64.b64encode(value) 99 | signature = self._cookie_signature(name, value, timestamp) 100 | value = "|".join([value, timestamp, signature]) 101 | return value 102 | 103 | def _cookie_signature(self, *parts): 104 | hash = hmac.new(self._app.settings["cookie_secret"], 105 | digestmod=hashlib.sha1) 106 | for part in parts: hash.update(part) 107 | return hash.hexdigest() 108 | 109 | def _get_html_attributes(self, tag, html): 110 | _elem_regex = re.compile('<%s (.*?)>' % tag, re.M | re.DOTALL) 111 | _attrs_regex = re.compile('(\w+)="([^"]+)"') 112 | all_attrs = [] 113 | for input in _elem_regex.findall(html): 114 | all_attrs.append(dict(_attrs_regex.findall(input))) 115 | return all_attrs 116 | -------------------------------------------------------------------------------- /tests/test_models.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from models import User, connection 3 | from .base import DatabaseTestCaseMixin 4 | 5 | class ModelsTestCase(TestCase, DatabaseTestCaseMixin): 6 | 7 | def setUp(self): 8 | self.db = connection.test 9 | super(ModelsTestCase, self).setUp() 10 | self.setup_connection() 11 | 12 | def tearDown(self): 13 | self.teardown_connection() 14 | 15 | def test_create_user(self): 16 | assert not self.db.User.find().count() 17 | user = self.db.User() 18 | user.username = u'bob' 19 | user.access_token = {'key': 'xxx', 'secret': 'yyy'} 20 | user.save() 21 | 22 | b = user['modify_date'] 23 | self.assertTrue(b) 24 | from time import sleep 25 | # mongodb is a bit weird when it comes to actual saving time 26 | # so introduce a realistic phsyical waiting time. 27 | sleep(0.001) 28 | user.username = u'bobby' 29 | user.save() 30 | 31 | a = user['modify_date'] 32 | self.assertTrue(a.microsecond > b.microsecond) 33 | 34 | def test_find_by_username(self): 35 | tweeter = self.db.Tweeter() 36 | tweeter['user_id'] = 123 37 | tweeter['username'] = u'TheRock' 38 | tweeter['name'] = u'The Rock' 39 | tweeter['followers'] = 100 40 | tweeter['following'] = 100 41 | tweeter.save() 42 | 43 | self.assertTrue(self.db.Tweeter.find_by_username(self.db, 'THEROCK')) 44 | self.assertTrue(self.db.Tweeter.find_by_username(self.db, 'TheRock')) 45 | self.assertTrue(not self.db.Tweeter.find_by_username(self.db, 'RockThe')) 46 | -------------------------------------------------------------------------------- /ui_modules.py: -------------------------------------------------------------------------------- 1 | import re 2 | import tornado.web 3 | 4 | def thousands_commas(v): 5 | thou=re.compile(r"([0-9])([0-9][0-9][0-9]([,.]|$))").search 6 | v=str(v) 7 | vl=v.split('.') 8 | if not vl: return v 9 | v=vl[0] 10 | del vl[0] 11 | if vl: s='.'+'.'.join(vl) 12 | else: s='' 13 | mo=thou(v) 14 | while mo is not None: 15 | l = mo.start(0) 16 | v=v[:l+1]+','+v[l+1:] 17 | mo=thou(v) 18 | return v+s 19 | 20 | class Thousands(tornado.web.UIModule): 21 | 22 | def render(self, number): 23 | return thousands_commas(number) 24 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | def bucketize(sequence, max_size): 2 | buckets = [] 3 | if not sequence: 4 | return buckets 5 | for i, each in enumerate(sequence): 6 | if not i: 7 | bucket = [] 8 | elif not i % max_size: 9 | buckets.append(bucket) 10 | bucket = [] 11 | bucket.append(each) 12 | if bucket: 13 | buckets.append(bucket) 14 | return buckets 15 | 16 | 17 | def test_bucketize(): 18 | buckets = bucketize(range(1, 12), 5) 19 | assert buckets == [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11]] 20 | 21 | buckets = bucketize(range(1, 11), 5) 22 | assert buckets == [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]] 23 | 24 | 25 | if __name__ == '__main__': 26 | test_bucketize() 27 | -------------------------------------------------------------------------------- /vendor/vendor.pth: -------------------------------------------------------------------------------- 1 | src/tornado-utils 2 | src/MongoLite 3 | src/tornado 4 | src/tornadio2 5 | 6 | 7 | --------------------------------------------------------------------------------