├── twitsocket ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── top_tweets.py │ │ ├── lister.py │ │ └── websockets.py ├── templatetags │ ├── __init__.py │ └── twitsocket_tags.py ├── templates │ └── twitsocket │ │ ├── count.html │ │ ├── switch.html │ │ ├── top_users.html │ │ ├── flash_hack.html │ │ ├── top_tweets.html │ │ ├── tweets.html │ │ └── websocket.html ├── static │ └── twitsocket │ │ ├── WebSocketMain.swf │ │ ├── swfobject.js │ │ ├── web_socket.js │ │ └── FABridge.js └── models.py ├── MANIFEST.in ├── setup.py ├── LICENCE └── README.rst /twitsocket/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /twitsocket/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /twitsocket/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /twitsocket/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /twitsocket/templates/twitsocket/count.html: -------------------------------------------------------------------------------- 1 | {{ count }} tweet{{ count|pluralize }} 2 | -------------------------------------------------------------------------------- /twitsocket/static/twitsocket/WebSocketMain.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brutasse-archive/django-twitsocket/HEAD/twitsocket/static/twitsocket/WebSocketMain.swf -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENCE README.rst 2 | include bin/websockets.py 3 | recursive-include twitsocket/media * 4 | recursive-include twitsocket/bin * 5 | recursive-include twitsocket/templates * 6 | -------------------------------------------------------------------------------- /twitsocket/templates/twitsocket/switch.html: -------------------------------------------------------------------------------- 1 | showhide retweets 2 | 3 | 14 | -------------------------------------------------------------------------------- /twitsocket/templates/twitsocket/top_users.html: -------------------------------------------------------------------------------- 1 |
{% for t in top_users %} 2 |

3 | {{ t.username }} 4 |
5 | {{ t.total_count }} tweet{{ t.total_count|pluralize }} 6 |

7 | {% endfor %}
8 | -------------------------------------------------------------------------------- /twitsocket/templates/twitsocket/flash_hack.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /twitsocket/templates/twitsocket/top_tweets.html: -------------------------------------------------------------------------------- 1 |
{% for t in top_tweets %} 2 | {% with t.get_content.retweeted_status as rt %} 3 |

4 | {{ rt.user.screen_name }}: {{ rt.text|safe }} 5 |
6 | {{ t.rt_count }} retweet{{ t.rt_count|pluralize }} 7 |

8 | {% endwith %} 9 | {% endfor %}
10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | from distutils.core import setup 4 | from setuptools import find_packages 5 | 6 | def read(fname): 7 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 8 | 9 | setup( 10 | name='django-twitsocket', 11 | version='0.1', 12 | author=u'Bruno Renie, Gautier Hayoun', 13 | author_email='bruno@renie.fr', 14 | packages=find_packages(), 15 | include_package_data=True, 16 | url='http://github.com/brutasse/django-twitsocket', 17 | license='BSD', 18 | description='A twitter wall / live stream for your conference / event / topic of interest, as a Django reusable app.', 19 | long_description=read('README.rst'), 20 | zip_safe=False, 21 | ) 22 | 23 | -------------------------------------------------------------------------------- /twitsocket/templatetags/twitsocket_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.conf import settings 3 | 4 | from twitsocket.models import Tweet, Top, Flooder 5 | 6 | register = template.Library() 7 | 8 | 9 | @register.inclusion_tag('twitsocket/websocket.html') 10 | def websocket_client(): 11 | return {'websocket_server': settings.WEBSOCKET_SERVER} 12 | 13 | 14 | @register.inclusion_tag('twitsocket/tweets.html') 15 | def render_tweets(count): 16 | count = int(count) 17 | return {'tweets': Tweet.objects.all()[:count]} 18 | 19 | 20 | @register.inclusion_tag('twitsocket/flash_hack.html') 21 | def flash_hack(): 22 | return {'STATIC_URL': settings.STATIC_URL} 23 | 24 | 25 | @register.inclusion_tag('twitsocket/top_tweets.html') 26 | def top_tweets(count): 27 | count = int(count) 28 | return {'top_tweets': Top.objects.all()[:count]} 29 | 30 | 31 | @register.inclusion_tag('twitsocket/top_users.html') 32 | def top_users(count): 33 | count = int(count) 34 | return {'top_users': Flooder.objects.all()[:count]} 35 | 36 | 37 | @register.inclusion_tag('twitsocket/count.html') 38 | def count(): 39 | return {'count': Tweet.objects.count()} 40 | 41 | 42 | @register.inclusion_tag('twitsocket/switch.html') 43 | def retweet_switch(): 44 | return {} 45 | -------------------------------------------------------------------------------- /twitsocket/templates/twitsocket/tweets.html: -------------------------------------------------------------------------------- 1 | 4 | 7 | 10 |
11 | {% for tweet in tweets %} 12 | {% with tweet.get_content as j %} 13 |
14 | {% if j.retweeted_status %} 15 | 16 |

{{ j.retweeted_status.user.screen_name }}: {{ j.retweeted_status.text|safe }}

17 | {% else %} 18 | 19 |

{{ j.user.screen_name }}: {{ j.text|safe }}

20 | {% endif %} 21 |
22 |
{% if j.retweeted_status %}Retweeted by {{ j.user.screen_name }} {% endif %} {{ j.created_at }}
23 | {% endwith %} 24 | {% endfor %} 25 |
26 | -------------------------------------------------------------------------------- /twitsocket/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils import simplejson as json 3 | 4 | 5 | class Tweet(models.Model): 6 | status_id = models.BigIntegerField() 7 | content = models.TextField(blank=True) 8 | 9 | class Meta: 10 | ordering = ('-status_id',) 11 | 12 | def get_content(self): 13 | return json.loads(self.content) 14 | 15 | 16 | class Top(models.Model): 17 | """Top tweets: tweets that are retweeted the most""" 18 | status_id = models.BigIntegerField() 19 | content = models.TextField(null=True) 20 | rt_count = models.IntegerField(default=0, db_index=True) 21 | 22 | def __unicode__(self): 23 | return u'%s, %s rts' % (self.status_id, self.rt_count) 24 | 25 | class Meta: 26 | ordering = ['-rt_count'] 27 | 28 | def get_content(self): 29 | return json.loads(self.content) 30 | 31 | 32 | class Flooder(models.Model): 33 | """People who tweet a lot (too much?) about the topic""" 34 | username = models.CharField(max_length=255) 35 | profile_picture = models.CharField(max_length=1023, null=True) 36 | tweet_count = models.IntegerField(default=0) 37 | rt_count = models.IntegerField(default=0) 38 | total_count = models.IntegerField(default=0, db_index=True) 39 | 40 | def __unicode__(self): 41 | return u'%s, %s, %s, %s' % (self.username, self.tweet_count, 42 | self.rt_count, self.total_count) 43 | 44 | class Meta: 45 | ordering = ['-total_count'] 46 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Bruno Renié and Gautier Hayoun 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of this project nor the names of its contributors may be 15 | used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /twitsocket/management/commands/top_tweets.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import NoArgsCommand 2 | 3 | from twitsocket.models import Tweet, Top, Flooder 4 | 5 | 6 | class Command(NoArgsCommand): 7 | 8 | def handle_noargs(self, **options): 9 | top_tw = {} 10 | top_users = {} 11 | 12 | for tw in Tweet.objects.order_by('id'): 13 | content = tw.get_content() 14 | screen_name = content['user']['screen_name'] 15 | 16 | if not screen_name in top_users: 17 | top_users[screen_name], created = Flooder.objects.get_or_create(username=screen_name) 18 | top_users[screen_name].tweet_count = 0 19 | top_users[screen_name].rt_count = 0 20 | top_users[screen_name].profile_picture = content['user']['profile_image_url'] 21 | 22 | if 'retweeted_status' in content: 23 | top_users[screen_name].rt_count += 1 24 | 25 | # Top tweets 26 | rt_id = int(content['retweeted_status']['id']) 27 | if not rt_id in top_tw: 28 | top_tw[rt_id], created = Top.objects.get_or_create(status_id=rt_id) 29 | top_tw[rt_id].rt_count = 0 30 | top_tw[rt_id].rt_count = top_tw[rt_id].rt_count + 1 31 | if not top_tw[rt_id].content: 32 | top_tw[rt_id].content = tw.content 33 | 34 | else: 35 | top_users[screen_name].tweet_count += 1 36 | 37 | for u in top_users.values(): 38 | u.total_count = u.tweet_count + u.rt_count 39 | u.save() 40 | 41 | for u in top_tw.values(): 42 | u.save() 43 | -------------------------------------------------------------------------------- /twitsocket/management/commands/lister.py: -------------------------------------------------------------------------------- 1 | import urllib2 2 | import urllib 3 | import time 4 | 5 | from django.conf import settings 6 | from django.core.management.base import NoArgsCommand 7 | from django.utils import simplejson as json 8 | 9 | import oauth2 as oauth 10 | 11 | from twitsocket.models import Tweet 12 | 13 | LIST_MEMBERS = 'https://api.twitter.com/1/%s/members.json' % settings.TWITTER_LIST 14 | 15 | def oauth_request(url, method='GET', params={}, data=None): 16 | qs = '' 17 | if method == 'GET': 18 | qs = '&'.join(['%s=%s' % (key, value) for key, value in params.items()]) 19 | if qs: 20 | url += '?%s' % qs 21 | consumer = oauth.Consumer(secret=settings.CONSUMER_SECRET, 22 | key=settings.CONSUMER_KEY) 23 | token = oauth.Token(secret=settings.TOKEN_SECRET, 24 | key=settings.TOKEN_KEY) 25 | oparams = { 26 | 'oauth_version': '1.0', 27 | 'oauth_nonce': oauth.generate_nonce(), 28 | 'oauth_timestamp': int(time.time()), 29 | 'oauth_token': token.key, 30 | 'oauth_consumer_key': consumer.key, 31 | } 32 | if method == 'POST': 33 | oparams.update(params) 34 | req = oauth.Request(method=method, url=url, parameters=oparams) 35 | signature_method = oauth.SignatureMethod_HMAC_SHA1() 36 | req.sign_request(signature_method, consumer, token) 37 | if method == 'POST': 38 | return urllib2.Request(url, data, headers=req.to_header()) 39 | return urllib2.Request(url, headers=req.to_header()) 40 | 41 | 42 | class Command(NoArgsCommand): 43 | """Adds all the twitter users to the list""" 44 | 45 | def handle_noargs(self, **options): 46 | members = self.get_list_members() 47 | users = self.get_users(Tweet.objects.all()) 48 | for u in users: 49 | if u not in members: 50 | print "Adding %s to list" % u 51 | self.add_to_list(u) 52 | time.sleep(1) 53 | 54 | def get_list_members(self): 55 | more_pages = True 56 | members = [] 57 | cursor = -1 58 | while more_pages: 59 | request = oauth_request(LIST_MEMBERS, params={'cursor': cursor}) 60 | data = urllib2.urlopen(request).read() 61 | 62 | payload = json.loads(data) 63 | cursor = payload['next_cursor'] 64 | for user in payload['users']: 65 | members.append(user['id']) 66 | more_pages = len(payload['users']) == 20 67 | return members 68 | 69 | def get_users(self, queryset): 70 | users = [] 71 | for tweet in queryset: 72 | content = tweet.get_content() 73 | if 'retweeted_status' in content: 74 | continue 75 | user_id = content['user']['id'] 76 | if user_id not in users: 77 | users.append(user_id) 78 | return users 79 | 80 | def add_to_list(self, user_id): 81 | data = urllib.urlencode({'id': user_id}) 82 | request = oauth_request(LIST_MEMBERS, method='POST', 83 | params={'id': user_id}, data=data) 84 | response = urllib2.urlopen(request).read() 85 | -------------------------------------------------------------------------------- /twitsocket/templates/twitsocket/websocket.html: -------------------------------------------------------------------------------- 1 | 93 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Twitsocket, twitter + WebSockets = ♥ 2 | ==================================== 3 | 4 | A twitter wall / live stream for your conference / event / topic of interest, 5 | integrated in your Django website. 6 | 7 | How it works 8 | ------------ 9 | 10 | Twitsocket has 3 components: 11 | 12 | * A client for twitter's streaming API which you can use to track keywords and 13 | specific accounts. 14 | 15 | * A websocket server to broadcast new tweets to every connected client. 16 | 17 | * A tiny Django app to store the tweets in a DB and render them. 18 | 19 | Installation 20 | ------------ 21 | 22 | :: 23 | 24 | pip install https://github.com/brutasse/django-twitsocket/tarball/master 25 | 26 | **Requirements**: 27 | 28 | * Python >=2.4 29 | * Django >= 1.0 30 | * python-oauth2 31 | * Jquery >= 1.4 enabled in your templates. 32 | 33 | Configuration 34 | ------------- 35 | 36 | You need to register an OAuth consumer on twitter.com (read AND write if you 37 | want to maintain the Twitter list) and get a token for it (using ``tweepy`` 38 | and `this script`_ for instance). 39 | 40 | .. _this script: https://gist.github.com/545143 41 | 42 | Add ``twitsocket`` to your ``INSTALLED_APPS``, run ``syncdb`` and add a few 43 | settings:: 44 | 45 | CONSUMER_SECRET = 'your consumer secret' 46 | CONSUMER_KEY = 'your consumer key' 47 | 48 | TOKEN_SECRET = 'your token secret' 49 | TOKEN_KEY = 'your token key' 50 | 51 | # Optional, in case of spam 52 | BANNED_USERS = ('list', 'of', 'banned', 'users') 53 | 54 | TRACK_KEYWORDS = ('europython', 'python', 'java') 55 | TRACK_USERS = (15324940, 15389140) # Only IDs here 56 | 57 | # The WebSocket port is mandatory, no trailing slash. 58 | WEBSOCKET_SERVER = 'ws://ws.example.com:8888' 59 | WEB_SERVER = 'http://example.com' 60 | 61 | # The log file tells you when new clients and new tweets arrive. 62 | LOGFILE = '/path/to/logfile.log' 63 | 64 | Create a view somewhere that renders a template and add this to the content:: 65 | 66 | {% load twitsocket_tags %} 67 | 68 | {% render_tweets 30 %} 69 | 70 | {% websocket_client %} 71 | 72 | Extra template tags 73 | ------------------- 74 | 75 | The following template tags are available. They are not required but may be 76 | used to display extra information about the stream. 77 | 78 | * **count**: ``{% count %}`` will output the number of tweets saved in the 79 | database. The counter is automatically incremented when new tweets are 80 | received. 81 | 82 | * **top_users**: ``{% top_users %}`` will display the top ```` 83 | users, people who are tweeting the most about the topic. 84 | 85 | * **top_tweets**: ``{% top_tweets %}`` will display the top ```` 86 | tweets, ordered by number of retweets. 87 | 88 | * **retweet_switch**: ``{% retweet_switch %}`` will display a "show retweets" / 89 | "hide retweets" button if users want to see only original tweets. The user's 90 | preference is stored in the browser's localStorage and restored when the 91 | page is reloaded. 92 | 93 | .. note:: 94 | 95 | For the ``top_users`` and ``top_tweets`` tags, you need to run the 96 | ``top_tweets`` management command every once a while to refreshed the 97 | cached values. It's up to you to decide on the frequency since the 98 | operation can be rather intensive with a large number of tweets. 99 | 100 | Running the websocket server 101 | ---------------------------- 102 | 103 | :: 104 | 105 | ./manage.py websockets 106 | 107 | Ideally, you should put this line in a bash script and run it with supervisord 108 | to restart it automatically when Twitter cuts the connection. 109 | 110 | Bonus 111 | ----- 112 | 113 | There is a ``lister`` management command which you can use to maintain a 114 | Twitter list of people tweeting about the subject you're tracking. 115 | 116 | If you want to use the command, create the list on twitter and add it to your 117 | settings (prepend you twitter username):: 118 | 119 | TWITTER_LIST = 'brutasse/europython2010' 120 | 121 | Running ``manage.py lister`` will add everyone to the list. Run it every once 122 | a while, Twitter has a rate-limit (and a max. of 500 ppl on a list). Note that 123 | only people who send an 'original' tweet (not a retweet) will appear on the 124 | list. 125 | 126 | Styling 127 | ------- 128 | 129 | This example CSS code will add a nice look to your tweets (WTFPL, modify it to 130 | fit your needs):: 131 | 132 | #tweets { 133 | font: normal 18px Georgia, Times, serif; width: 600px; 134 | margin: 10px auto; padding: 10px; 135 | border: 1px solid #ccc; background-color: #eee; 136 | } 137 | #tweets a { text-decoration: none; color: #4096ee; } 138 | #tweets a.username { color: #73880a; font-weight: bold; } 139 | #tweets a:hover { text-decoration: underline; } 140 | #tweets .tweet { color: #444; } 141 | #tweets .tweet img { 142 | display: block; float: left; 143 | background-color: #fff; border: 1px solid #bbb; 144 | padding: 3px; margin-right: 10px; 145 | } 146 | #tweets .tweet p { margin: 0; padding: 0; float: left; width: 500px; } 147 | #tweets .clear { 148 | clear: both; border-bottom: 1px solid #ccc; 149 | margin-bottom: 10px; padding-bottom: 10px; 150 | font-size: 0.8em; color: #aaa; 151 | text-align: right; text-shadow: 0 1px 0 #fff; 152 | } 153 | #tweets .rt { color: #d01f3c; font-weight: bold; padding-right: 15px; } 154 | .notice { 155 | width: 610px; text-shadow: 0 1px 0 #fff; 156 | margin: 10px auto; background-color: #FFFFaa; 157 | padding: 5px; border: 1px solid #eecc55; 158 | color: #555; font-size: 0.8em; 159 | } 160 | 161 | The flash hack 162 | -------------- 163 | 164 | As you may know, not all browsers support WebSockets. They are implemented in 165 | Safari, Chrome and Firefox 4. There is a clever hack involving Flash that 166 | implements WebSockets for older browsers. To enable it, simply use 167 | contrib.staticfiles (or django-staticfiles with django 1.2). Add it to your 168 | ``INSTALLED_APPS``, configure ``STATIC_ROOT`` and ``STATIC_URL`` and run 169 | ``manage.py collectstatic``. 170 | 171 | Then add to your ```` block (assuming you've loaded 172 | ``twitsocket_tags``):: 173 | 174 | 175 | ... whatever you have 176 | ... 177 | {% flash_hack %} 178 | 179 | 180 | Note that because of some cross-domain security concerns, the flash hack will 181 | only if the media files are served on the same domain name as the website 182 | itself. No media.example.com for serving static files. 183 | 184 | TODO 185 | ---- 186 | 187 | * i18n for websocket error messages. 188 | 189 | * Try to decouple the Twitter consumer and the WebSocket server. Maybe with 190 | Redis and its Pub/Sub mechanism. 191 | -------------------------------------------------------------------------------- /twitsocket/static/twitsocket/swfobject.js: -------------------------------------------------------------------------------- 1 | /* SWFObject v2.2 2 | is released under the MIT License 3 | */ 4 | var swfobject=function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O.ActiveXObject!=D){try{var ad=new ActiveXObject(W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y0){for(var af=0;af0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad'}}aa.outerHTML='"+af+"";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;ab.?) #(?P[A-Za-z0-9_]+)(?P.?)") 19 | HASH_RE2 = re.compile(r"^#(?P[A-Za-z0-9_]+)(?P.?)") 20 | USERNAME_RE = re.compile(r"(?P.?)@(?P[A-Za-z0-9_]+)(?P.?)") 21 | 22 | logger = logging.getLogger('twitstream') 23 | handler = logging.FileHandler(settings.LOGFILE) 24 | formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') 25 | handler.setFormatter(formatter) 26 | logger.addHandler(handler) 27 | logger.setLevel(logging.INFO) 28 | 29 | BANNED_USERS = getattr(settings, 'BANNED_USERS', ()) 30 | 31 | POLICY_FILE = """ 32 | 33 | 34 | """ 35 | 36 | 37 | def get_oauth_request(url, consumer, token, extra_params): 38 | oparams = { 39 | 'oauth_version': '1.0', 40 | 'oauth_nonce': oauth.generate_nonce(), 41 | 'oauth_timestamp': int(time.time()), 42 | 'oauth_token': token.key, 43 | 'oauth_consumer_key': consumer.key, 44 | } 45 | oparams.update(extra_params) 46 | 47 | req = oauth.Request(method="POST", url=url, parameters=oparams) 48 | signature_method = oauth.SignatureMethod_HMAC_SHA1() 49 | req.sign_request(signature_method, consumer, token) 50 | return req 51 | 52 | 53 | class StreamClient(asyncore.dispatcher): 54 | 55 | def __init__(self, host, headers, body, server): 56 | asyncore.dispatcher.__init__(self) 57 | self.create_socket(socket.AF_INET, socket.SOCK_STREAM) 58 | self.connect((host, 80)) 59 | self.buffer = '\r\n'.join(headers) + '\r\n\r\n' + body + '\r\n\r\n' 60 | self.data = '' 61 | self.server = server 62 | logger.info("Twitter StreamClient listening.") 63 | 64 | def handle_connect(self): 65 | pass 66 | 67 | def handle_close(self): 68 | self.close() 69 | raise ValueError("Connection closed by remote host") 70 | 71 | def handle_read(self): 72 | buf = self.recv(8192) 73 | logger.info('Twitter: %s bytes' % len(buf)) 74 | self.data += buf 75 | if not self.data.endswith('\r\n\r\n'): 76 | return 77 | 78 | data = self.data.split('\r\n') 79 | 80 | if len(data) > 4: 81 | data = data[-2:] 82 | content = data[1] 83 | if not content: 84 | self.data = '' 85 | return 86 | 87 | payload = json.loads(content) 88 | 89 | if type(payload) == int: 90 | self.data = '' 91 | return 92 | 93 | self.handle_json(payload) 94 | self.data = '' 95 | 96 | def writable(self): 97 | return len(self.buffer) > 0 98 | 99 | def write(self, data): 100 | self.buffer += data 101 | 102 | def handle_write(self): 103 | sent = self.send(self.buffer) 104 | self.buffer = self.buffer[sent:] 105 | 106 | def handle_json(self, payload): 107 | if payload['user']['screen_name'] in BANNED_USERS: 108 | return 109 | 110 | dt = datetime.datetime.strptime(payload['created_at'], 111 | '%a %b %d %H:%M:%S +0000 %Y') 112 | dt = dt + datetime.timedelta(hours=1) 113 | payload['created_at'] = dt.strftime('%a %b %d %H:%M:%S +0100 %Y') 114 | 115 | dumped = json.dumps(payload) 116 | tw = Tweet(status_id=payload['id'], content=dumped).save() 117 | self.server.send_to_clients(dumped) 118 | 119 | 120 | class WebSocket(asyncore.dispatcher): 121 | handshake_75 = """HTTP/1.1 101 Web Socket Protocol Handshake\r 122 | Upgrade: WebSocket\r 123 | Connection: Upgrade\r 124 | WebSocket-Origin: %(web_server)s\r 125 | WebSocket-Location: %(websocket_server)s/\r 126 | WebSocket-Protocol: sample""" + '\r\n\r\n' 127 | 128 | handshake_76 = """HTTP/1.1 101 Web Socket Protocol Handshake\r 129 | Upgrade: WebSocket\r 130 | Connection: Upgrade\r 131 | Sec-WebSocket-Origin: %(web_server)s\r 132 | Sec-WebSocket-Location: %(websocket_server)s/\r 133 | Sec-WebSocket-Protocol: sample""" + '\r\n\r\n' 134 | 135 | def __init__(self, web_server, websocket_server): 136 | params = {'web_server': web_server, 137 | 'websocket_server': websocket_server} 138 | self.handshake_75 = self.handshake_75 % params 139 | self.handshake_76 = self.handshake_76 % params 140 | port = int(websocket_server.split(':')[-1]) 141 | 142 | asyncore.dispatcher.__init__(self) 143 | self.create_socket(socket.AF_INET, socket.SOCK_STREAM) 144 | self.set_reuse_addr() 145 | self.bind(('0.0.0.0', port)) 146 | self.listen(5) 147 | 148 | self.clients = set() 149 | logger.info("Websocket server ready") 150 | 151 | def handle_accept(self): 152 | sock, addr = self.accept() 153 | handshaken = False 154 | header = '' 155 | while handshaken == False: 156 | header += sock.recv(8192) 157 | 158 | if (not header.startswith(''): 164 | sock.send(POLICY_FILE) 165 | sock.close() 166 | return 167 | 168 | if len(header) - header.find('\r\n\r\n') == 12: 169 | # WebSockets 76 170 | handshake = self.handshake_76 + self.get_challenge(header) 171 | handshaken = True 172 | sock.send(handshake) 173 | 174 | elif header.find('\r\n\r\n') != -1: 175 | # WebSockets 75 176 | handshaken = True 177 | sock.send(self.handshake_75) 178 | handler = WebSocketHandler(self, sock) 179 | 180 | def send_to_clients(self, payload): 181 | if not self.clients: 182 | logger.info("Could have send something but no client") 183 | else: 184 | logger.info("Sending payload to %s clients" % len(self.clients)) 185 | for client in self.clients: 186 | client.queue.append(payload) 187 | 188 | def get_challenge(self, header_string): 189 | headers = header_string.split('\r\n') 190 | header_dict = {} 191 | for h in [head for head in headers[1:] if ': ' in head]: 192 | key, value = h.split(': ', 1) 193 | header_dict[key] = value 194 | 195 | key_1 = header_dict['Sec-WebSocket-Key1'] 196 | key_2 = header_dict['Sec-WebSocket-Key2'] 197 | key_3 = header_string[-8:] 198 | 199 | def key_challenge(key): 200 | key_number = int(''.join([i for i in key if 47 < ord(i) < 58])) 201 | spaces = len([i for i in key if i == ' ']) 202 | part = key_number / spaces 203 | return struct.pack('!I', part) 204 | 205 | challenge = key_challenge(key_1) + key_challenge(key_2) + key_3 206 | return md5.md5(challenge).digest() 207 | 208 | 209 | class WebSocketHandler(asyncore.dispatcher): 210 | 211 | def __init__(self, server, sock): 212 | asyncore.dispatcher.__init__(self, sock=sock) 213 | self.server = server 214 | self.server.clients.add(self) 215 | self.queue = [] 216 | logger.info("New client connected, count: %s" % len(self.server.clients)) 217 | 218 | def handle_read(self): 219 | data = self.recv(4096) 220 | logger.debug("Got some data: %s" % data) 221 | 222 | def handle_write(self): 223 | logger.debug("Handling write") 224 | if self.queue: 225 | message = self.queue.pop(0) 226 | self.send('\x00%s\xff' % message) 227 | else: 228 | logger.info("Nothing to write") 229 | 230 | def writable(self): 231 | return len(self.queue) > 0 232 | 233 | def handle_close(self): 234 | self.server.clients.remove(self) 235 | logger.info("Client quitting, count: %s" % len(self.server.clients)) 236 | self.close() 237 | 238 | 239 | def dict_to_postdata(data_dict): 240 | body = '' 241 | for key, value in data_dict.items(): 242 | data = '%s=%s' % (key, value) 243 | if body: 244 | body += '&' + data 245 | else: 246 | body = data 247 | return body 248 | 249 | 250 | def all_in_one_handler(): 251 | keywords = getattr(settings, 'TRACK_KEYWORDS', ()) 252 | user_ids = map(str, getattr(settings, 'TRACK_USERS', ())) 253 | if not keywords and not user_ids: 254 | raise ValueError("Set at least TRACK_KEYWORDS or TRACK_USERS " 255 | "in your settings") 256 | post_params = {} 257 | if keywords: 258 | post_params['track'] = ','.join(keywords) 259 | if user_ids: 260 | post_params['follow'] = ','.join(user_ids) 261 | 262 | body = dict_to_postdata(post_params) 263 | 264 | token = oauth.Token(secret=settings.TOKEN_SECRET, 265 | key=settings.TOKEN_KEY) 266 | consumer = oauth.Consumer(secret=settings.CONSUMER_SECRET, 267 | key=settings.CONSUMER_KEY) 268 | host = 'stream.twitter.com' 269 | path = '/1/statuses/filter.json' 270 | url = 'http://%s%s' % (host, path) 271 | request = get_oauth_request(url, consumer, token, post_params) 272 | 273 | headers = [ 274 | 'POST %s HTTP/1.1' % path, 275 | 'Host: %s' % host, 276 | 'Authorization: %s' % request.to_header()['Authorization'], 277 | 'Content-Type: application/x-www-form-urlencoded', 278 | 'Content-Length: %s' % len(body), 279 | ] 280 | 281 | websocket_server = WebSocket(settings.WEB_SERVER, 282 | settings.WEBSOCKET_SERVER) 283 | twitter_client = StreamClient(host, headers, body, websocket_server) 284 | asyncore.loop() 285 | 286 | 287 | class Command(NoArgsCommand): 288 | 289 | def handle_noargs(self, **options): 290 | all_in_one_handler() 291 | -------------------------------------------------------------------------------- /twitsocket/static/twitsocket/web_socket.js: -------------------------------------------------------------------------------- 1 | // Copyright: Hiroshi Ichikawa 2 | // License: New BSD License 3 | // Reference: http://dev.w3.org/html5/websockets/ 4 | // Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol 5 | 6 | (function() { 7 | 8 | if (window.WebSocket) return; 9 | 10 | var console = window.console; 11 | if (!console) console = {log: function(){ }, error: function(){ }}; 12 | 13 | function hasFlash() { 14 | if ('navigator' in window && 'plugins' in navigator && navigator.plugins['Shockwave Flash']) { 15 | return !!navigator.plugins['Shockwave Flash'].description; 16 | } 17 | if ('ActiveXObject' in window) { 18 | try { 19 | return !!new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version'); 20 | } catch (e) {} 21 | } 22 | return false; 23 | } 24 | 25 | if (!hasFlash()) { 26 | console.error("Flash Player is not installed."); 27 | return; 28 | } 29 | console.log(location.protocol); 30 | if (location.protocol == "file:") { 31 | console.error( 32 | "web-socket-js doesn't work in file:///... URL (without special configuration). " + 33 | "Open the page via Web server i.e. http://..."); 34 | } 35 | 36 | WebSocket = function(url, protocol, proxyHost, proxyPort, headers) { 37 | var self = this; 38 | self.readyState = WebSocket.CONNECTING; 39 | self.bufferedAmount = 0; 40 | // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc. 41 | // Otherwise, when onopen fires immediately, onopen is called before it is set. 42 | setTimeout(function() { 43 | WebSocket.__addTask(function() { 44 | self.__createFlash(url, protocol, proxyHost, proxyPort, headers); 45 | }); 46 | }, 1); 47 | } 48 | 49 | WebSocket.prototype.__createFlash = function(url, protocol, proxyHost, proxyPort, headers) { 50 | var self = this; 51 | self.__flash = 52 | WebSocket.__flash.create(url, protocol, proxyHost || null, proxyPort || 0, headers || null); 53 | 54 | self.__flash.addEventListener("open", function(fe) { 55 | try { 56 | self.readyState = self.__flash.getReadyState(); 57 | if (self.__timer) clearInterval(self.__timer); 58 | if (window.opera) { 59 | // Workaround for weird behavior of Opera which sometimes drops events. 60 | self.__timer = setInterval(function () { 61 | self.__handleMessages(); 62 | }, 500); 63 | } 64 | if (self.onopen) self.onopen(); 65 | } catch (e) { 66 | console.error(e.toString()); 67 | } 68 | }); 69 | 70 | self.__flash.addEventListener("close", function(fe) { 71 | try { 72 | self.readyState = self.__flash.getReadyState(); 73 | if (self.__timer) clearInterval(self.__timer); 74 | if (self.onclose) self.onclose(); 75 | } catch (e) { 76 | console.error(e.toString()); 77 | } 78 | }); 79 | 80 | self.__flash.addEventListener("message", function() { 81 | try { 82 | self.__handleMessages(); 83 | } catch (e) { 84 | console.error(e.toString()); 85 | } 86 | }); 87 | 88 | self.__flash.addEventListener("error", function(fe) { 89 | try { 90 | if (self.__timer) clearInterval(self.__timer); 91 | if (self.onerror) self.onerror(); 92 | } catch (e) { 93 | console.error(e.toString()); 94 | } 95 | }); 96 | 97 | self.__flash.addEventListener("stateChange", function(fe) { 98 | try { 99 | self.readyState = self.__flash.getReadyState(); 100 | self.bufferedAmount = fe.getBufferedAmount(); 101 | } catch (e) { 102 | console.error(e.toString()); 103 | } 104 | }); 105 | 106 | //console.log("[WebSocket] Flash object is ready"); 107 | }; 108 | 109 | WebSocket.prototype.send = function(data) { 110 | if (this.__flash) { 111 | this.readyState = this.__flash.getReadyState(); 112 | } 113 | if (!this.__flash || this.readyState == WebSocket.CONNECTING) { 114 | throw "INVALID_STATE_ERR: Web Socket connection has not been established"; 115 | } 116 | var result = this.__flash.send(encodeURIComponent(data)); 117 | if (result < 0) { // success 118 | return true; 119 | } else { 120 | this.bufferedAmount = result; 121 | return false; 122 | } 123 | }; 124 | 125 | WebSocket.prototype.close = function() { 126 | if (!this.__flash) return; 127 | this.readyState = this.__flash.getReadyState(); 128 | if (this.readyState != WebSocket.OPEN) return; 129 | this.__flash.close(); 130 | // Sets/calls them manually here because Flash WebSocketConnection.close cannot fire events 131 | // which causes weird error: 132 | // > You are trying to call recursively into the Flash Player which is not allowed. 133 | this.readyState = WebSocket.CLOSED; 134 | if (this.__timer) clearInterval(this.__timer); 135 | if (this.onclose) this.onclose(); 136 | }; 137 | 138 | /** 139 | * Implementation of {@link DOM 2 EventTarget Interface} 140 | * 141 | * @param {string} type 142 | * @param {function} listener 143 | * @param {boolean} useCapture !NB Not implemented yet 144 | * @return void 145 | */ 146 | WebSocket.prototype.addEventListener = function(type, listener, useCapture) { 147 | if (!('__events' in this)) { 148 | this.__events = {}; 149 | } 150 | if (!(type in this.__events)) { 151 | this.__events[type] = []; 152 | if ('function' == typeof this['on' + type]) { 153 | this.__events[type].defaultHandler = this['on' + type]; 154 | this['on' + type] = this.__createEventHandler(this, type); 155 | } 156 | } 157 | this.__events[type].push(listener); 158 | }; 159 | 160 | /** 161 | * Implementation of {@link DOM 2 EventTarget Interface} 162 | * 163 | * @param {string} type 164 | * @param {function} listener 165 | * @param {boolean} useCapture NB! Not implemented yet 166 | * @return void 167 | */ 168 | WebSocket.prototype.removeEventListener = function(type, listener, useCapture) { 169 | if (!('__events' in this)) { 170 | this.__events = {}; 171 | } 172 | if (!(type in this.__events)) return; 173 | for (var i = this.__events.length; i > -1; --i) { 174 | if (listener === this.__events[type][i]) { 175 | this.__events[type].splice(i, 1); 176 | break; 177 | } 178 | } 179 | }; 180 | 181 | /** 182 | * Implementation of {@link DOM 2 EventTarget Interface} 183 | * 184 | * @param {WebSocketEvent} event 185 | * @return void 186 | */ 187 | WebSocket.prototype.dispatchEvent = function(event) { 188 | if (!('__events' in this)) throw 'UNSPECIFIED_EVENT_TYPE_ERR'; 189 | if (!(event.type in this.__events)) throw 'UNSPECIFIED_EVENT_TYPE_ERR'; 190 | 191 | for (var i = 0, l = this.__events[event.type].length; i < l; ++ i) { 192 | this.__events[event.type][i](event); 193 | if (event.cancelBubble) break; 194 | } 195 | 196 | if (false !== event.returnValue && 197 | 'function' == typeof this.__events[event.type].defaultHandler) 198 | { 199 | this.__events[event.type].defaultHandler(event); 200 | } 201 | }; 202 | 203 | WebSocket.prototype.__handleMessages = function() { 204 | // Gets data using readSocketData() instead of getting it from event object 205 | // of Flash event. This is to make sure to keep message order. 206 | // It seems sometimes Flash events don't arrive in the same order as they are sent. 207 | var arr = this.__flash.readSocketData(); 208 | for (var i = 0; i < arr.length; i++) { 209 | var data = decodeURIComponent(arr[i]); 210 | try { 211 | if (this.onmessage) { 212 | var e; 213 | if (window.MessageEvent) { 214 | e = document.createEvent("MessageEvent"); 215 | e.initMessageEvent("message", false, false, data, null, null, window, null); 216 | } else { // IE 217 | e = {data: data}; 218 | } 219 | this.onmessage(e); 220 | } 221 | } catch (e) { 222 | console.error(e.toString()); 223 | } 224 | } 225 | }; 226 | 227 | /** 228 | * @param {object} object 229 | * @param {string} type 230 | */ 231 | WebSocket.prototype.__createEventHandler = function(object, type) { 232 | return function(data) { 233 | var event = new WebSocketEvent(); 234 | event.initEvent(type, true, true); 235 | event.target = event.currentTarget = object; 236 | for (var key in data) { 237 | event[key] = data[key]; 238 | } 239 | object.dispatchEvent(event, arguments); 240 | }; 241 | } 242 | 243 | /** 244 | * Basic implementation of {@link DOM 2 EventInterface} 245 | * 246 | * @class 247 | * @constructor 248 | */ 249 | function WebSocketEvent(){} 250 | 251 | /** 252 | * 253 | * @type boolean 254 | */ 255 | WebSocketEvent.prototype.cancelable = true; 256 | 257 | /** 258 | * 259 | * @type boolean 260 | */ 261 | WebSocketEvent.prototype.cancelBubble = false; 262 | 263 | /** 264 | * 265 | * @return void 266 | */ 267 | WebSocketEvent.prototype.preventDefault = function() { 268 | if (this.cancelable) { 269 | this.returnValue = false; 270 | } 271 | }; 272 | 273 | /** 274 | * 275 | * @return void 276 | */ 277 | WebSocketEvent.prototype.stopPropagation = function() { 278 | this.cancelBubble = true; 279 | }; 280 | 281 | /** 282 | * 283 | * @param {string} eventTypeArg 284 | * @param {boolean} canBubbleArg 285 | * @param {boolean} cancelableArg 286 | * @return void 287 | */ 288 | WebSocketEvent.prototype.initEvent = function(eventTypeArg, canBubbleArg, cancelableArg) { 289 | this.type = eventTypeArg; 290 | this.cancelable = cancelableArg; 291 | this.timeStamp = new Date(); 292 | }; 293 | 294 | 295 | WebSocket.CONNECTING = 0; 296 | WebSocket.OPEN = 1; 297 | WebSocket.CLOSING = 2; 298 | WebSocket.CLOSED = 3; 299 | 300 | WebSocket.__tasks = []; 301 | 302 | WebSocket.__initialize = function() { 303 | if (WebSocket.__swfLocation) { 304 | // For backword compatibility. 305 | window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation; 306 | } 307 | if (!window.WEB_SOCKET_SWF_LOCATION) { 308 | console.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf"); 309 | return; 310 | } 311 | var container = document.createElement("div"); 312 | container.id = "webSocketContainer"; 313 | // Puts the Flash out of the window. Note that we cannot use display: none or visibility: hidden 314 | // here because it prevents Flash from loading at least in IE. 315 | container.style.position = "absolute"; 316 | container.style.left = "-100px"; 317 | container.style.top = "-100px"; 318 | var holder = document.createElement("div"); 319 | holder.id = "webSocketFlash"; 320 | container.appendChild(holder); 321 | document.body.appendChild(container); 322 | swfobject.embedSWF( 323 | WEB_SOCKET_SWF_LOCATION, "webSocketFlash", "8", "8", "9.0.0", 324 | null, {bridgeName: "webSocket"}, null, null, 325 | function(e) { 326 | if (!e.success) console.error("[WebSocket] swfobject.embedSWF failed"); 327 | } 328 | ); 329 | FABridge.addInitializationCallback("webSocket", function() { 330 | try { 331 | //console.log("[WebSocket] FABridge initializad"); 332 | WebSocket.__flash = FABridge.webSocket.root(); 333 | WebSocket.__flash.setCallerUrl(location.href); 334 | WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG); 335 | for (var i = 0; i < WebSocket.__tasks.length; ++i) { 336 | WebSocket.__tasks[i](); 337 | } 338 | WebSocket.__tasks = []; 339 | } catch (e) { 340 | console.error("[WebSocket] " + e.toString()); 341 | } 342 | }); 343 | }; 344 | 345 | WebSocket.__addTask = function(task) { 346 | if (WebSocket.__flash) { 347 | task(); 348 | } else { 349 | WebSocket.__tasks.push(task); 350 | } 351 | } 352 | 353 | // called from Flash 354 | window.webSocketLog = function(message) { 355 | console.log(decodeURIComponent(message)); 356 | }; 357 | 358 | // called from Flash 359 | window.webSocketError = function(message) { 360 | console.error(decodeURIComponent(message)); 361 | }; 362 | 363 | if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) { 364 | if (window.addEventListener) { 365 | window.addEventListener("load", WebSocket.__initialize, false); 366 | } else { 367 | window.attachEvent("onload", WebSocket.__initialize); 368 | } 369 | } 370 | 371 | })(); 372 | -------------------------------------------------------------------------------- /twitsocket/static/twitsocket/FABridge.js: -------------------------------------------------------------------------------- 1 | /* 2 | /* 3 | Copyright 2006 Adobe Systems Incorporated 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 6 | to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 14 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 15 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | */ 18 | 19 | 20 | /* 21 | * The Bridge class, responsible for navigating AS instances 22 | */ 23 | function FABridge(target,bridgeName) 24 | { 25 | this.target = target; 26 | this.remoteTypeCache = {}; 27 | this.remoteInstanceCache = {}; 28 | this.remoteFunctionCache = {}; 29 | this.localFunctionCache = {}; 30 | this.bridgeID = FABridge.nextBridgeID++; 31 | this.name = bridgeName; 32 | this.nextLocalFuncID = 0; 33 | FABridge.instances[this.name] = this; 34 | FABridge.idMap[this.bridgeID] = this; 35 | 36 | return this; 37 | } 38 | 39 | // type codes for packed values 40 | FABridge.TYPE_ASINSTANCE = 1; 41 | FABridge.TYPE_ASFUNCTION = 2; 42 | 43 | FABridge.TYPE_JSFUNCTION = 3; 44 | FABridge.TYPE_ANONYMOUS = 4; 45 | 46 | FABridge.initCallbacks = {}; 47 | FABridge.userTypes = {}; 48 | 49 | FABridge.addToUserTypes = function() 50 | { 51 | for (var i = 0; i < arguments.length; i++) 52 | { 53 | FABridge.userTypes[arguments[i]] = { 54 | 'typeName': arguments[i], 55 | 'enriched': false 56 | }; 57 | } 58 | } 59 | 60 | FABridge.argsToArray = function(args) 61 | { 62 | var result = []; 63 | for (var i = 0; i < args.length; i++) 64 | { 65 | result[i] = args[i]; 66 | } 67 | return result; 68 | } 69 | 70 | function instanceFactory(objID) 71 | { 72 | this.fb_instance_id = objID; 73 | return this; 74 | } 75 | 76 | function FABridge__invokeJSFunction(args) 77 | { 78 | var funcID = args[0]; 79 | var throughArgs = args.concat();//FABridge.argsToArray(arguments); 80 | throughArgs.shift(); 81 | 82 | var bridge = FABridge.extractBridgeFromID(funcID); 83 | return bridge.invokeLocalFunction(funcID, throughArgs); 84 | } 85 | 86 | FABridge.addInitializationCallback = function(bridgeName, callback) 87 | { 88 | var inst = FABridge.instances[bridgeName]; 89 | if (inst != undefined) 90 | { 91 | callback.call(inst); 92 | return; 93 | } 94 | 95 | var callbackList = FABridge.initCallbacks[bridgeName]; 96 | if(callbackList == null) 97 | { 98 | FABridge.initCallbacks[bridgeName] = callbackList = []; 99 | } 100 | 101 | callbackList.push(callback); 102 | } 103 | 104 | // updated for changes to SWFObject2 105 | function FABridge__bridgeInitialized(bridgeName) { 106 | var objects = document.getElementsByTagName("object"); 107 | var ol = objects.length; 108 | var activeObjects = []; 109 | if (ol > 0) { 110 | for (var i = 0; i < ol; i++) { 111 | if (typeof objects[i].SetVariable != "undefined") { 112 | activeObjects[activeObjects.length] = objects[i]; 113 | } 114 | } 115 | } 116 | var embeds = document.getElementsByTagName("embed"); 117 | var el = embeds.length; 118 | var activeEmbeds = []; 119 | if (el > 0) { 120 | for (var j = 0; j < el; j++) { 121 | if (typeof embeds[j].SetVariable != "undefined") { 122 | activeEmbeds[activeEmbeds.length] = embeds[j]; 123 | } 124 | } 125 | } 126 | var aol = activeObjects.length; 127 | var ael = activeEmbeds.length; 128 | var searchStr = "bridgeName="+ bridgeName; 129 | if ((aol == 1 && !ael) || (aol == 1 && ael == 1)) { 130 | FABridge.attachBridge(activeObjects[0], bridgeName); 131 | } 132 | else if (ael == 1 && !aol) { 133 | FABridge.attachBridge(activeEmbeds[0], bridgeName); 134 | } 135 | else { 136 | var flash_found = false; 137 | if (aol > 1) { 138 | for (var k = 0; k < aol; k++) { 139 | var params = activeObjects[k].childNodes; 140 | for (var l = 0; l < params.length; l++) { 141 | var param = params[l]; 142 | if (param.nodeType == 1 && param.tagName.toLowerCase() == "param" && param["name"].toLowerCase() == "flashvars" && param["value"].indexOf(searchStr) >= 0) { 143 | FABridge.attachBridge(activeObjects[k], bridgeName); 144 | flash_found = true; 145 | break; 146 | } 147 | } 148 | if (flash_found) { 149 | break; 150 | } 151 | } 152 | } 153 | if (!flash_found && ael > 1) { 154 | for (var m = 0; m < ael; m++) { 155 | var flashVars = activeEmbeds[m].attributes.getNamedItem("flashVars").nodeValue; 156 | if (flashVars.indexOf(searchStr) >= 0) { 157 | FABridge.attachBridge(activeEmbeds[m], bridgeName); 158 | break; 159 | } 160 | } 161 | } 162 | } 163 | return true; 164 | } 165 | 166 | // used to track multiple bridge instances, since callbacks from AS are global across the page. 167 | 168 | FABridge.nextBridgeID = 0; 169 | FABridge.instances = {}; 170 | FABridge.idMap = {}; 171 | FABridge.refCount = 0; 172 | 173 | FABridge.extractBridgeFromID = function(id) 174 | { 175 | var bridgeID = (id >> 16); 176 | return FABridge.idMap[bridgeID]; 177 | } 178 | 179 | FABridge.attachBridge = function(instance, bridgeName) 180 | { 181 | var newBridgeInstance = new FABridge(instance, bridgeName); 182 | 183 | FABridge[bridgeName] = newBridgeInstance; 184 | 185 | /* FABridge[bridgeName] = function() { 186 | return newBridgeInstance.root(); 187 | } 188 | */ 189 | var callbacks = FABridge.initCallbacks[bridgeName]; 190 | if (callbacks == null) 191 | { 192 | return; 193 | } 194 | for (var i = 0; i < callbacks.length; i++) 195 | { 196 | callbacks[i].call(newBridgeInstance); 197 | } 198 | delete FABridge.initCallbacks[bridgeName] 199 | } 200 | 201 | // some methods can't be proxied. You can use the explicit get,set, and call methods if necessary. 202 | 203 | FABridge.blockedMethods = 204 | { 205 | toString: true, 206 | get: true, 207 | set: true, 208 | call: true 209 | }; 210 | 211 | FABridge.prototype = 212 | { 213 | 214 | 215 | // bootstrapping 216 | 217 | root: function() 218 | { 219 | return this.deserialize(this.target.getRoot()); 220 | }, 221 | //clears all of the AS objects in the cache maps 222 | releaseASObjects: function() 223 | { 224 | return this.target.releaseASObjects(); 225 | }, 226 | //clears a specific object in AS from the type maps 227 | releaseNamedASObject: function(value) 228 | { 229 | if(typeof(value) != "object") 230 | { 231 | return false; 232 | } 233 | else 234 | { 235 | var ret = this.target.releaseNamedASObject(value.fb_instance_id); 236 | return ret; 237 | } 238 | }, 239 | //create a new AS Object 240 | create: function(className) 241 | { 242 | return this.deserialize(this.target.create(className)); 243 | }, 244 | 245 | 246 | // utilities 247 | 248 | makeID: function(token) 249 | { 250 | return (this.bridgeID << 16) + token; 251 | }, 252 | 253 | 254 | // low level access to the flash object 255 | 256 | //get a named property from an AS object 257 | getPropertyFromAS: function(objRef, propName) 258 | { 259 | if (FABridge.refCount > 0) 260 | { 261 | throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); 262 | } 263 | else 264 | { 265 | FABridge.refCount++; 266 | retVal = this.target.getPropFromAS(objRef, propName); 267 | retVal = this.handleError(retVal); 268 | FABridge.refCount--; 269 | return retVal; 270 | } 271 | }, 272 | //set a named property on an AS object 273 | setPropertyInAS: function(objRef,propName, value) 274 | { 275 | if (FABridge.refCount > 0) 276 | { 277 | throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); 278 | } 279 | else 280 | { 281 | FABridge.refCount++; 282 | retVal = this.target.setPropInAS(objRef,propName, this.serialize(value)); 283 | retVal = this.handleError(retVal); 284 | FABridge.refCount--; 285 | return retVal; 286 | } 287 | }, 288 | 289 | //call an AS function 290 | callASFunction: function(funcID, args) 291 | { 292 | if (FABridge.refCount > 0) 293 | { 294 | throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); 295 | } 296 | else 297 | { 298 | FABridge.refCount++; 299 | retVal = this.target.invokeASFunction(funcID, this.serialize(args)); 300 | retVal = this.handleError(retVal); 301 | FABridge.refCount--; 302 | return retVal; 303 | } 304 | }, 305 | //call a method on an AS object 306 | callASMethod: function(objID, funcName, args) 307 | { 308 | if (FABridge.refCount > 0) 309 | { 310 | throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); 311 | } 312 | else 313 | { 314 | FABridge.refCount++; 315 | args = this.serialize(args); 316 | retVal = this.target.invokeASMethod(objID, funcName, args); 317 | retVal = this.handleError(retVal); 318 | FABridge.refCount--; 319 | return retVal; 320 | } 321 | }, 322 | 323 | // responders to remote calls from flash 324 | 325 | //callback from flash that executes a local JS function 326 | //used mostly when setting js functions as callbacks on events 327 | invokeLocalFunction: function(funcID, args) 328 | { 329 | var result; 330 | var func = this.localFunctionCache[funcID]; 331 | 332 | if(func != undefined) 333 | { 334 | result = this.serialize(func.apply(null, this.deserialize(args))); 335 | } 336 | 337 | return result; 338 | }, 339 | 340 | // Object Types and Proxies 341 | 342 | // accepts an object reference, returns a type object matching the obj reference. 343 | getTypeFromName: function(objTypeName) 344 | { 345 | return this.remoteTypeCache[objTypeName]; 346 | }, 347 | //create an AS proxy for the given object ID and type 348 | createProxy: function(objID, typeName) 349 | { 350 | var objType = this.getTypeFromName(typeName); 351 | instanceFactory.prototype = objType; 352 | var instance = new instanceFactory(objID); 353 | this.remoteInstanceCache[objID] = instance; 354 | return instance; 355 | }, 356 | //return the proxy associated with the given object ID 357 | getProxy: function(objID) 358 | { 359 | return this.remoteInstanceCache[objID]; 360 | }, 361 | 362 | // accepts a type structure, returns a constructed type 363 | addTypeDataToCache: function(typeData) 364 | { 365 | newType = new ASProxy(this, typeData.name); 366 | var accessors = typeData.accessors; 367 | for (var i = 0; i < accessors.length; i++) 368 | { 369 | this.addPropertyToType(newType, accessors[i]); 370 | } 371 | 372 | var methods = typeData.methods; 373 | for (var i = 0; i < methods.length; i++) 374 | { 375 | if (FABridge.blockedMethods[methods[i]] == undefined) 376 | { 377 | this.addMethodToType(newType, methods[i]); 378 | } 379 | } 380 | 381 | 382 | this.remoteTypeCache[newType.typeName] = newType; 383 | return newType; 384 | }, 385 | 386 | //add a property to a typename; used to define the properties that can be called on an AS proxied object 387 | addPropertyToType: function(ty, propName) 388 | { 389 | var c = propName.charAt(0); 390 | var setterName; 391 | var getterName; 392 | if(c >= "a" && c <= "z") 393 | { 394 | getterName = "get" + c.toUpperCase() + propName.substr(1); 395 | setterName = "set" + c.toUpperCase() + propName.substr(1); 396 | } 397 | else 398 | { 399 | getterName = "get" + propName; 400 | setterName = "set" + propName; 401 | } 402 | ty[setterName] = function(val) 403 | { 404 | this.bridge.setPropertyInAS(this.fb_instance_id, propName, val); 405 | } 406 | ty[getterName] = function() 407 | { 408 | return this.bridge.deserialize(this.bridge.getPropertyFromAS(this.fb_instance_id, propName)); 409 | } 410 | }, 411 | 412 | //add a method to a typename; used to define the methods that can be callefd on an AS proxied object 413 | addMethodToType: function(ty, methodName) 414 | { 415 | ty[methodName] = function() 416 | { 417 | return this.bridge.deserialize(this.bridge.callASMethod(this.fb_instance_id, methodName, FABridge.argsToArray(arguments))); 418 | } 419 | }, 420 | 421 | // Function Proxies 422 | 423 | //returns the AS proxy for the specified function ID 424 | getFunctionProxy: function(funcID) 425 | { 426 | var bridge = this; 427 | if (this.remoteFunctionCache[funcID] == null) 428 | { 429 | this.remoteFunctionCache[funcID] = function() 430 | { 431 | bridge.callASFunction(funcID, FABridge.argsToArray(arguments)); 432 | } 433 | } 434 | return this.remoteFunctionCache[funcID]; 435 | }, 436 | 437 | //reutrns the ID of the given function; if it doesnt exist it is created and added to the local cache 438 | getFunctionID: function(func) 439 | { 440 | if (func.__bridge_id__ == undefined) 441 | { 442 | func.__bridge_id__ = this.makeID(this.nextLocalFuncID++); 443 | this.localFunctionCache[func.__bridge_id__] = func; 444 | } 445 | return func.__bridge_id__; 446 | }, 447 | 448 | // serialization / deserialization 449 | 450 | serialize: function(value) 451 | { 452 | var result = {}; 453 | 454 | var t = typeof(value); 455 | //primitives are kept as such 456 | if (t == "number" || t == "string" || t == "boolean" || t == null || t == undefined) 457 | { 458 | result = value; 459 | } 460 | else if (value instanceof Array) 461 | { 462 | //arrays are serializesd recursively 463 | result = []; 464 | for (var i = 0; i < value.length; i++) 465 | { 466 | result[i] = this.serialize(value[i]); 467 | } 468 | } 469 | else if (t == "function") 470 | { 471 | //js functions are assigned an ID and stored in the local cache 472 | result.type = FABridge.TYPE_JSFUNCTION; 473 | result.value = this.getFunctionID(value); 474 | } 475 | else if (value instanceof ASProxy) 476 | { 477 | result.type = FABridge.TYPE_ASINSTANCE; 478 | result.value = value.fb_instance_id; 479 | } 480 | else 481 | { 482 | result.type = FABridge.TYPE_ANONYMOUS; 483 | result.value = value; 484 | } 485 | 486 | return result; 487 | }, 488 | 489 | //on deserialization we always check the return for the specific error code that is used to marshall NPE's into JS errors 490 | // the unpacking is done by returning the value on each pachet for objects/arrays 491 | deserialize: function(packedValue) 492 | { 493 | 494 | var result; 495 | 496 | var t = typeof(packedValue); 497 | if (t == "number" || t == "string" || t == "boolean" || packedValue == null || packedValue == undefined) 498 | { 499 | result = this.handleError(packedValue); 500 | } 501 | else if (packedValue instanceof Array) 502 | { 503 | result = []; 504 | for (var i = 0; i < packedValue.length; i++) 505 | { 506 | result[i] = this.deserialize(packedValue[i]); 507 | } 508 | } 509 | else if (t == "object") 510 | { 511 | for(var i = 0; i < packedValue.newTypes.length; i++) 512 | { 513 | this.addTypeDataToCache(packedValue.newTypes[i]); 514 | } 515 | for (var aRefID in packedValue.newRefs) 516 | { 517 | this.createProxy(aRefID, packedValue.newRefs[aRefID]); 518 | } 519 | if (packedValue.type == FABridge.TYPE_PRIMITIVE) 520 | { 521 | result = packedValue.value; 522 | } 523 | else if (packedValue.type == FABridge.TYPE_ASFUNCTION) 524 | { 525 | result = this.getFunctionProxy(packedValue.value); 526 | } 527 | else if (packedValue.type == FABridge.TYPE_ASINSTANCE) 528 | { 529 | result = this.getProxy(packedValue.value); 530 | } 531 | else if (packedValue.type == FABridge.TYPE_ANONYMOUS) 532 | { 533 | result = packedValue.value; 534 | } 535 | } 536 | return result; 537 | }, 538 | //increases the reference count for the given object 539 | addRef: function(obj) 540 | { 541 | this.target.incRef(obj.fb_instance_id); 542 | }, 543 | //decrease the reference count for the given object and release it if needed 544 | release:function(obj) 545 | { 546 | this.target.releaseRef(obj.fb_instance_id); 547 | }, 548 | 549 | // check the given value for the components of the hard-coded error code : __FLASHERROR 550 | // used to marshall NPE's into flash 551 | 552 | handleError: function(value) 553 | { 554 | if (typeof(value)=="string" && value.indexOf("__FLASHERROR")==0) 555 | { 556 | var myErrorMessage = value.split("||"); 557 | if(FABridge.refCount > 0 ) 558 | { 559 | FABridge.refCount--; 560 | } 561 | throw new Error(myErrorMessage[1]); 562 | return value; 563 | } 564 | else 565 | { 566 | return value; 567 | } 568 | } 569 | }; 570 | 571 | // The root ASProxy class that facades a flash object 572 | 573 | ASProxy = function(bridge, typeName) 574 | { 575 | this.bridge = bridge; 576 | this.typeName = typeName; 577 | return this; 578 | }; 579 | //methods available on each ASProxy object 580 | ASProxy.prototype = 581 | { 582 | get: function(propName) 583 | { 584 | return this.bridge.deserialize(this.bridge.getPropertyFromAS(this.fb_instance_id, propName)); 585 | }, 586 | 587 | set: function(propName, value) 588 | { 589 | this.bridge.setPropertyInAS(this.fb_instance_id, propName, value); 590 | }, 591 | 592 | call: function(funcName, args) 593 | { 594 | this.bridge.callASMethod(this.fb_instance_id, funcName, args); 595 | }, 596 | 597 | addRef: function() { 598 | this.bridge.addRef(this); 599 | }, 600 | 601 | release: function() { 602 | this.bridge.release(this); 603 | } 604 | }; 605 | --------------------------------------------------------------------------------