├── public ├── favicon.ico ├── images │ ├── designo.ico │ ├── designo16.png │ ├── designo32.png │ ├── designo64new4.png │ └── sign-in-with-twitter-l.png ├── stylesheets │ └── style.less └── javascripts │ └── client.js ├── views ├── layout.jade ├── login.jade └── index.jade ├── package.json ├── README.md ├── app.js └── twitter.js /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazuyukitanimura/designo/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/images/designo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazuyukitanimura/designo/HEAD/public/images/designo.ico -------------------------------------------------------------------------------- /public/images/designo16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazuyukitanimura/designo/HEAD/public/images/designo16.png -------------------------------------------------------------------------------- /public/images/designo32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazuyukitanimura/designo/HEAD/public/images/designo32.png -------------------------------------------------------------------------------- /public/images/designo64new4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazuyukitanimura/designo/HEAD/public/images/designo64new4.png -------------------------------------------------------------------------------- /public/images/sign-in-with-twitter-l.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazuyukitanimura/designo/HEAD/public/images/sign-in-with-twitter-l.png -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | script(type='text/javascript', src='http://code.jquery.com/jquery-1.7.min.js') 7 | script(type='text/javascript', src='/socket.io/socket.io.js') 8 | script(type='text/javascript', src='/javascripts/client.js') 9 | body!= body 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "kazuyukitanimura", 3 | "name": "designo", 4 | "description": "Realtime Twitter client exploiting Node.js+Websockets+HTML5", 5 | "version": "0.1", 6 | "repository": { 7 | "url": "https://github.com/kazuyukitanimura/designo" 8 | }, 9 | "main": "app.js", 10 | "engines": { 11 | "node": ">= v0.4.12" 12 | }, 13 | "dependencies": { 14 | "connect": ">= 1.7.1" 15 | , "connect-redis": ">= 1.1.0" 16 | , "express": ">= 2.4.7" 17 | , "jade": ">= 0.16.2" 18 | , "less": ">= 1.1.4" 19 | , "oauth": ">= 0.9.5" 20 | , "socket.io": ">= 0.8.5" 21 | }, 22 | "devDependencies": {} 23 | } 24 | -------------------------------------------------------------------------------- /views/login.jade: -------------------------------------------------------------------------------- 1 | div(style='position:relative;width: 100%;height: 100%;text-align: center;-moz-box-shadow: 5px 5px 5px #888; -webkit-box-shadow: 5px 5px 5px #888; box-shadow: 5px 5px 5px #888;border: 5px solid #ddd;background: #F6F6F6;') 2 | a#logo(href= loginto, style='display: block;width: 100%; height: 100%;padding: 20% 0%;') 3 | h1= title 4 | img(src='images/designo64new4.png') 5 | p(style='font-size: 11px;color: #AEAEAE;margin-top: -15px;margin-bottom: 20px;') The Lightweight Social Client 6 | img(src='images/sign-in-with-twitter-l.png') 7 | div(style='position: absolute; bottom: 0px; right: 0px;padding: 3px 5px;') 8 | span Online users: 9 | span#count 10 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | a#logo(onclick='select("all");return false;', href='#') 2 | h1= title 3 | img(src='images/designo64new4.png') 4 | p(style='color: #AEAEAE;margin-top: 2px;margin-bottom: 20px;') The Lightweight Social Client 5 | #account 6 | a(href='https://github.com/kazuyukitanimura/designo') Fork me on GitHub 7 | span \ |\ 8 | a(href= loginto)= loginm 9 | #main 10 | #sidebar 11 | div#mentions(class='sidebar') 12 | a(onclick='select("mentions");return false;', href='#') ( 13 | span.badge 0 14 | ) @Mentions 15 | div#all(class='sidebar', style='background: #ddd') 16 | a(onclick='select("all");return false;', href='#') ( 17 | span.badge 0 18 | ) All 19 | #right 20 | form#form(enctype="multipart/form-data", onsubmit='send(); return false;') 21 | div#name= screen_name 22 | div#text(contenteditable='true') 23 | div#countChar 24 | input(type='submit', value='send') 25 | #chat 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Designo 2 | This is a Twitter client demo with Node.js+Websocket+HTML5. Enjoy realtime tweeting. 3 | 4 | ## Requrements: 5 | - [Express](https://github.com/visionmedia/express) 6 | - [Connect](https://github.com/senchalabs/Connect) 7 | - [connect-redis](https://github.com/visionmedia/connect-redis) 8 | - [Jade](https://github.com/visionmedia/jade) 9 | - [less.js](https://github.com/cloudhead/less.js) 10 | - [socket.io](https://github.com/learnboost/socket.io) 11 | - [node-oauth](https://github.com/ciaranj/node-oauth) 12 | - - Designo does not use session-web-sockets nor Socket.IO-connect anymore 13 | 14 | ## Demo 15 | Demo is available at [http://designo.pictshare.me/](http://designo.pictshare.me/) 16 | 17 | More details will be explained at my blog, [http://hobbycoding.posterous.com/](http://hobbycoding.posterous.com/) 18 | 19 | ## Tested Environment 20 | ### Browsers 21 | - Google Chrome (works best) 22 | - Firefox 23 | - Safari 24 | 25 | ### Node.js version 26 | - v0.4.7 27 | 28 | ## License 29 | 30 | The MIT License 31 | 32 | Copyright (c) 2011 Kazuyuki Tanimura 33 | 34 | Permission is hereby granted, free of charge, to any person obtaining a copy 35 | of this software and associated documentation files (the "Software"), to deal 36 | in the Software without restriction, including without limitation the rights 37 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 38 | copies of the Software, and to permit persons to whom the Software is 39 | furnished to do so, subject to the following conditions: 40 | 41 | The above copyright notice and this permission notice shall be included in 42 | all copies or substantial portions of the Software. 43 | 44 | THE SOFTWARE IS PROVIDED `AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 45 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 46 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 47 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 48 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 49 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 50 | THE SOFTWARE. 51 | 52 | -------------------------------------------------------------------------------- /public/stylesheets/style.less: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 10px 30px 5px 30px; 3 | font: 14px "Lucida Grande", "Helvetica Nueue", Arial, sans-serif; 4 | background: #fafafa; 5 | #logo { 6 | text-decoration: none; 7 | color: #000; 8 | display: inline-block; 9 | height: 56px; 10 | h1 { 11 | text-shadow: 1px 1px 1px #888; 12 | -webkit-mask-image: -webkit-gradient(linear, left top, left bottom, from(rgba(0,0,0,0.2)), to(rgba(0,0,0,1))); 13 | img { 14 | margin-left: 2px; 15 | margin-bottom: -4px; 16 | height: 32px; 17 | width: 32px; 18 | -moz-box-shadow: 1px 1px 1px #888; 19 | -webkit-box-shadow: 1px 1px 1px #888; 20 | box-shadow: 1px 1px 1px #888; 21 | } 22 | } 23 | } 24 | } 25 | #account { 26 | background-image: -webkit-gradient(linear, left top, left bottom, from(#969696), to(#333)); 27 | background-image: -moz-linear-gradient(center bottom, #969696, #333); 28 | border-radius: 0px 0px 8px 8px; 29 | -moz-border-radius: 0px 0px 8px 8px; 30 | -webkit-border-radius: 0px 0px 8px 8px; 31 | position: absolute; 32 | top: 0px; 33 | right: 50%; 34 | margin-right: -120px; 35 | padding: 3px 5px; 36 | -moz-box-shadow: 0px 3px 3px #888; 37 | -webkit-box-shadow: 0px 3px 3px #888; 38 | box-shadow: 0px 3px 3px #888; 39 | a { 40 | text-decoration: none; 41 | color: #AEAEAE; 42 | text-shadow: -1px -1px 1px #000; 43 | font-size: 17px; 44 | font-weight: bold; 45 | } 46 | a:hover { 47 | color: #fff; 48 | } 49 | } 50 | #main{ 51 | width: 100%; 52 | min-width: 420px; 53 | height: 100%; 54 | .bio { 55 | position: absolute; 56 | display: none; 57 | background: #fff; 58 | border: 5px solid #eee; 59 | -moz-box-shadow: 5px 5px 5px #888; 60 | -webkit-box-shadow: 5px 5px 5px #888; 61 | box-shadow: 5px 5px 5px #888; 62 | padding: 5px; 63 | font-size: 11px; 64 | width: 400px; 65 | .fullname { 66 | font-size: 14px; 67 | } 68 | img { 69 | border-style: none; 70 | height: 64px; 71 | width: 64px; 72 | float: left; 73 | margin-right: 0.5em; 74 | vertical-align: bottom; 75 | } 76 | a { 77 | display: inline; 78 | color: #00f; 79 | } 80 | span { 81 | width: 320px; 82 | float: left; 83 | span { 84 | float: none; 85 | color: #AEAEAE; 86 | } 87 | } 88 | } 89 | } 90 | #sidebar { 91 | height: auto; 92 | overflow-x: hidden; 93 | overflow-y: auto; 94 | width: 15%; 95 | float: left; 96 | margin: 0px; 97 | border: none; 98 | background: #fff; 99 | font-size: 13px; 100 | div { 101 | padding: 3px; 102 | margin: 0px; 103 | border: none; 104 | a { 105 | display: block; 106 | width: 100%; 107 | height: 100%; 108 | color: #000; 109 | text-decoration: none; 110 | } 111 | } 112 | div:hover { 113 | background: #f1f1f1; 114 | } 115 | img { 116 | border-style: none; 117 | height: 16px; 118 | width: 16px; 119 | float: left; 120 | margin-right: 0.5em; 121 | } 122 | } 123 | #right { 124 | width: 84%; 125 | float: left; 126 | margin: 0px; 127 | -moz-box-shadow: 5px 5px 5px #888; 128 | -webkit-box-shadow: 5px 5px 5px #888; 129 | box-shadow: 5px 5px 5px #888; 130 | } 131 | #chat { 132 | overflow: auto; 133 | width: auto; 134 | border-top: none; 135 | border-left: 5px solid #ddd; 136 | border-right: 5px solid #ddd; 137 | border-bottom: 5px solid #ddd; 138 | p { 139 | padding: 0px; 140 | margin: 0px; 141 | } 142 | div { 143 | padding: 5px; 144 | margin: 0px; 145 | border-top: 1px solid #eee; 146 | .twitpic { 147 | position: absolute; 148 | display: none; 149 | border: 5px solid #eee; 150 | -moz-box-shadow: 5px 5px 5px #888; 151 | -webkit-box-shadow: 5px 5px 5px #888; 152 | box-shadow: 5px 5px 5px #888; 153 | } 154 | } 155 | div:hover { 156 | background: #f1f1f1; 157 | } 158 | a img.profile { 159 | border-style: none; 160 | height: 32px; 161 | width: 32px; 162 | float: left; 163 | margin-right: 0.5em; 164 | vertical-align: bottom; 165 | } 166 | .permalink { 167 | font-size: 12px; 168 | span { 169 | color: #AEAEAE; 170 | } 171 | a { 172 | text-decoration: none; 173 | color: #AEAEAE; 174 | } 175 | a:hover { 176 | color: #f00; 177 | } 178 | } 179 | } 180 | #form { 181 | width: 100%; 182 | background: #333; 183 | padding: 5px 10px; 184 | border-left: 5px solid #ddd; 185 | border-top: 5px solid #ddd; 186 | border-right: 5px solid #ddd; 187 | border-bottom: none; 188 | margin-top: -5px; 189 | display: table; 190 | #name { 191 | width: 64px; 192 | padding: 5px; 193 | border: 1px solid #eee; 194 | background: #eee; 195 | display: table-cell; 196 | } 197 | #text { 198 | width: 100%; 199 | padding: 5px; 200 | background: #fff; 201 | border: 1px solid #eee; 202 | display: table-cell; 203 | white-space: pre-wrap; 204 | } 205 | #countChar{ 206 | color: #eee; 207 | padding: 5px; 208 | display: table-cell; 209 | } 210 | input[type=submit] { 211 | cursor: pointer; 212 | background: #999; 213 | border: none; 214 | padding: 6px 8px; 215 | border-radius: 8px; 216 | -moz-border-radius: 8px; 217 | -webkit-border-radius: 8px; 218 | margin-left: 5px; 219 | text-shadow: 0 1px 0 #fff; 220 | display: table-cell; 221 | } 222 | input[type=submit]:hover { 223 | background: #A2A2A2; 224 | } 225 | input[type=submit]:active { 226 | position: relative; 227 | top: 2px; 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var express = require('express'); 7 | var RedisStore = require('connect-redis')(express); 8 | var Twitter = require('./twitter'); 9 | var connect = require('connect'); 10 | var parseCookie = connect.utils.parseCookie; 11 | var Session = connect.middleware.session.Session; 12 | 13 | var app = module.exports = express.createServer(); 14 | var sessionStore = new RedisStore; 15 | 16 | var consumerKey = 'your consumer key', 17 | consumerSecret = 'your consumer secret'; 18 | 19 | // Configuration 20 | 21 | app.configure(function(){ 22 | app.set('views', __dirname + '/views'); 23 | app.use(express.cookieParser()); 24 | app.use(express.session({secret: 'himitsu!', fingerprint: function(req){return req.socket.remoteAddress;}, store: sessionStore, key: 'express.sid'})); 25 | app.use(express.bodyParser()); 26 | app.use(express.methodOverride()); 27 | app.use(express.compiler({ src: __dirname + '/public', enable: ['less'] })); 28 | app.use(app.router); 29 | app.use(express['static'](__dirname + '/public')); 30 | app.use(express.logger({ format: ':method :url' })); 31 | }); 32 | 33 | app.configure('development', function(){ 34 | express.logger('development node'); 35 | app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 36 | }); 37 | 38 | app.configure('production', function(){ 39 | express.logger('production node'); 40 | app.use(express.errorHandler()); 41 | }); 42 | 43 | // Routes 44 | 45 | app.get('/', function(req, res){ 46 | var jadeFile = 'login.jade', 47 | loginMessage = 'Login with Twitter!', 48 | loginTo = '/login', 49 | screenName = 'Twitter ID'; 50 | if(req.session.oauth){ 51 | jadeFile = 'index.jade'; 52 | loginMessage = 'Logout'; 53 | loginTo = '/logout'; 54 | try{ 55 | screenName = req.session.oauth._results.screen_name; 56 | }catch(e){ 57 | console.error('screen_name ERROR: ' + e); 58 | setTimeout(res.redirect, 500, '/'); 59 | } 60 | } 61 | res.render(jadeFile, { 62 | title: 'designo', 63 | loginm: loginMessage, 64 | loginto: loginTo, 65 | screen_name: screenName 66 | }); 67 | }); 68 | 69 | app.get('/login', function(req, res){ 70 | var tw = new Twitter(consumerKey, consumerSecret); 71 | tw.getRequestToken(function(error, url){ 72 | if(error){ 73 | req.session.destroy(function(){ 74 | console.error(error); 75 | res.writeHead(500, {'Content-Type': 'text/html'}); 76 | res.send('ERROR :' + error); 77 | }); 78 | }else{ 79 | req.session.oauth = tw; 80 | res.redirect(url); 81 | } 82 | }); 83 | }); 84 | 85 | app.get('/logout', function(req, res){ 86 | req.session.destroy(function(){ 87 | res.redirect('/'); 88 | }); 89 | }); 90 | 91 | // authorized callback from twitter.com 92 | app.get('/authorized', function(req, res){ 93 | if( !req.session.oauth ){ 94 | res.redirect('/'); // invalid callback url access; 95 | }else{ 96 | var tw = new Twitter(consumerKey, consumerSecret, req.session.oauth); 97 | tw.getAccessToken(req.query.oauth_verifier, function(error){ 98 | if(error){ 99 | req.session.destroy(function(){ 100 | console.error(error); 101 | res.send(error); 102 | }); 103 | }else{ 104 | req.session.oauth = tw; 105 | res.redirect('/'); 106 | } 107 | }); 108 | } 109 | }); 110 | 111 | // Only listen on $ node app.js 112 | 113 | if (!module.parent) { 114 | app.listen(8080); 115 | console.log('Express server listening on port '+app.address().port); 116 | } 117 | 118 | var io = require('socket.io').listen(app); 119 | // Based on http://www.danielbaulig.de/socket-ioexpress/ 120 | io.set('authorization', function (data, accept){ 121 | if(data.headers.cookie){ 122 | data.cookie = parseCookie(data.headers.cookie); 123 | data.sessionID = data.cookie['express.sid']; 124 | // save the session store to the data object 125 | // (as required by the Session constructor) 126 | data.sessionStore = sessionStore; 127 | sessionStore.get(data.sessionID, function (err, session){ 128 | if(err){ 129 | accept(err.message, false); 130 | }else{ 131 | // create a session object, passing data as request and our 132 | // just acquired session data 133 | data.session = new Session(data, session); 134 | accept(null, true); 135 | } 136 | }); 137 | } else { 138 | return accept('No cookie transmitted.', false); 139 | } 140 | }); 141 | 142 | io.sockets.tid2clt = {}; 143 | io.sockets.broadcastTo = function(to, message){ //to has to be an Array 144 | try{ 145 | for(var i=to.length; i--;){ 146 | var clt = this.tid2clt[to[i]]; 147 | if(clt){ 148 | if(this.flags.json){ 149 | clt.json.send(message); 150 | }else{ 151 | clt.send(message); 152 | } 153 | } 154 | } 155 | }catch(e){ 156 | console.error('broadcastTo ERROR: '+e); 157 | } 158 | return this; 159 | }; 160 | var count = 0, 161 | maxcount = 0; 162 | io.sockets.on('connection', function(client){ 163 | count++; 164 | client.json.broadcast.send({count: count}); 165 | client.json.send({count: count}); 166 | if(count>maxcount){ 167 | console.log('maxcount: '+(maxcount=count)); 168 | } 169 | // Based on http://www.danielbaulig.de/socket-ioexpress/ 170 | var hs = client.handshake; 171 | var session = hs.session; 172 | var sessionID = hs.sessionID; 173 | console.log('A client with sessionID '+sessionID+' connected!'); 174 | // setup an inteval that will keep our session fresh 175 | 176 | if(session.oauth){ 177 | var tw = new Twitter(consumerKey, consumerSecret, session.oauth); 178 | try{ 179 | io.sockets.tid2clt[tw._results.user_id] = client; 180 | }catch(e){ 181 | console.error('io.sockets.tid2sid ERROR: ' + e); 182 | } 183 | //view home 184 | var scroll = function(params){ 185 | tw.getTimeline(params, function(error, data, response){ 186 | if(error){ 187 | console.error('TIMELLINE ERROR: ' + error); 188 | }else{ 189 | client.json.send(data); 190 | //req.session.page.push(data); 191 | } 192 | }); 193 | }; 194 | scroll({page: 1, include_entities: true}); 195 | //manage followers 196 | if(!client.followers){ 197 | tw.followers(function(error, data, response){ 198 | if(error){ 199 | console.error('FOLLOWERS ERROR: ' + error); 200 | }else{ 201 | client.followers = data; 202 | } 203 | }); 204 | } 205 | //user streams 206 | var usParams = {include_entities: true}, 207 | stream = tw.openUserStream(usParams); 208 | stream.on('data', function(data){ 209 | try{ 210 | if(data.friends){ 211 | }else{ 212 | client.json.send(data); 213 | } 214 | }catch(e){ 215 | console.error('dispatch event ERROR: ' + e); 216 | } 217 | }); 218 | stream.on('error', function(err){ 219 | session.destroy(function(){ 220 | console.error('UserStream ERROR: ' + err); 221 | }); 222 | }); 223 | stream.on('end', function(){ 224 | session.destroy(function(){ 225 | console.log('UserStream ends successfully'); 226 | }); 227 | }); 228 | } 229 | 230 | client.on('update', function(message){ 231 | tw.update(message, function(error, data, response){ 232 | if(error){ 233 | console.error("UPDATE ERROR\ndata: "+data+'response: '+response+'oauth: '+tw+'message: '+message); 234 | }else{ 235 | client.json.send(data); 236 | io.sockets.json.broadcastTo(client.followers, data); 237 | } 238 | }); 239 | }); 240 | client.on('retweet', function(message){ 241 | tw.retweet(message.id_str, function(error, data, response){ 242 | if(error){ 243 | console.error("RETWEET ERROR\ndata: "+data+'response: '+response+'oauth: '+tw+'message: '+message); 244 | }else{ 245 | client.json.send(data); 246 | io.sockets.json.broadcastTo(client.followers, data); 247 | } 248 | }); 249 | }); 250 | client.on('destroy', function(message){ 251 | tw.destroy(message.id_str, function(error, data, response){ 252 | if(error){ 253 | console.error("DELETE ERROR\ndata: "+data+'response: '+response+'oauth: '+tw+'message: '+message); 254 | } 255 | }); 256 | }); 257 | client.on('scroll', function(message){ 258 | scroll(message); 259 | }); 260 | client.on('disconnect', function(){ 261 | count--; 262 | client.json.broadcast.send({count: count}); 263 | }); 264 | // Based on http://www.danielbaulig.de/socket-ioexpress/ 265 | var intervalID = setInterval(function(){ 266 | // reload the session (just in case something changed, 267 | // we don't want to override anything, but the age) 268 | // reloading will also ensure we keep an up2date copy 269 | // of the session with our connection. 270 | session.reload(function(){ 271 | // "touch" it (resetting maxAge and lastAccess) 272 | // and save it back again. 273 | session.touch().save(); 274 | }); 275 | }, 60*1000); 276 | client.on('disconnect', function(){ 277 | console.log('A client with sessionID '+sessionID+' disconnected!'); 278 | // clear the client interval to stop refreshing the session 279 | clearInterval(intervalID); 280 | }); 281 | }); 282 | -------------------------------------------------------------------------------- /public/javascripts/client.js: -------------------------------------------------------------------------------- 1 | var SECOND = 1000, 2 | MINUTE = SECOND * 60, 3 | HOUR = MINUTE * 60, 4 | DAY = HOUR * 24, 5 | MONTH = DAY * 31; 6 | 7 | if(!$.fn.hoverBio){ 8 | $.fn.hoverBio = function(target){ 9 | var thisObj = $(this[0]), 10 | targetObj = (target||thisObj).children('.bio'); 11 | return thisObj.hover( 12 | function(e){ 13 | targetObj.css('left',e.pageX+10).css('top',e.pageY).show(); 14 | },function(){ 15 | targetObj.hide(); 16 | } 17 | ); 18 | }; 19 | } 20 | 21 | if(!$.fn.hoverPic){ 22 | $.fn.hoverPic = function(){ 23 | return $(this[0]).find('a').each(function(){ 24 | var thisObj = $(this), 25 | thisImg = thisObj.children('img.twitpic'); 26 | if(thisImg){ 27 | thisObj.hover( 28 | function(e){ 29 | thisImg.css('left',e.pageX+10).css('top',e.pageY).show(); 30 | },function(){ 31 | thisImg.hide(); 32 | } 33 | ); 34 | } 35 | }); 36 | }; 37 | } 38 | 39 | if(!$.fn.ago){ 40 | $.fn.ago = function(){ 41 | for(var i=this.length; i--;){ 42 | var thisObj = $(this[i]), 43 | timestamp = Date.parse(thisObj.attr('datetime')), 44 | duration = new Date() - timestamp; 45 | if(durationdiv').css('margin-left','0px')); 87 | idObj.remove(); 88 | }; 89 | socket.on('message', function(message){ 90 | if(message.count){ 91 | $('#count').text(message.count); 92 | }else if(message['delete']){ 93 | deleteOutDiv(message['delete'].status.id_str); 94 | }else{ 95 | var messageArray = Array.isArray(message) ? message.reverse() : [message]; 96 | for(var k=messageArray.length; k--;){ 97 | var message=messageArray[k]; 98 | if(message.text){ 99 | var id = message.id_str, 100 | scroll = false, 101 | jQid = '#'+id, 102 | idObj = $(jQid); 103 | if((id.length===oldestId.length && id=0 && urls[i].indices[0]>user_mentions[j].indices[0])){// check j and i first 124 | var href = urls[i].expanded_url, 125 | src = href.match(/http:\/\/twitpic\.com\/(\w+)/) ? 'http://twitpic.com/show/thumb/'+RegExp.$1 : 126 | href.match(/http:\/\/yfrog\.com\/(\w+)/) ? 'http://yfrog.com/'+RegExp.$1+':small' : 127 | href.match(/http:\/\/instagr\.am\/p\/(\w+)/) ? 'http://instagr.am/p/'+RegExp.$1+'/media/?size=t' : 128 | href.match(/http:\/\/i\.imgur\.com\/(\w+)\.jpg/) ? 'http://i.imgur.com/'+RegExp.$1+'s.jpg' : 129 | href.match(/http:\/\/plixi\.com\/p\//) ? 'http://api.plixi.com/api/tpapi.svc/imagefromurl?size=thumbnail&url='+href : null, 130 | img = src ? ' style="font-weight:bold">'+href+'' : '>'+href; 131 | text = text.slice(0, urls[i].indices[0])+''+text.slice(urls[i].indices[1]); 132 | ++j;// put back the other one 133 | }else{ 134 | mentioned = (user_mentions[j].screen_name === name);// true or false 135 | text = text.slice(0, user_mentions[j].indices[0]) + 136 | '@' + 137 | user_mentions[j].screen_name + '' + text.slice(user_mentions[j].indices[1]); 138 | ++i;// put back the other one 139 | } 140 | } 141 | var p_str = '

'+text+'

from '+message.source+' | ', 142 | rp_str = 'Reply'; 143 | if(screen_name===name){ 144 | p_str += ''+rp_str+' - Delete'; 145 | }else{ 146 | p_str += 'Retweet - '+rp_str+''; 147 | } 148 | if(idObj.length){ 149 | idObj.html(p_str).addClass(uid); 150 | }else{ 151 | var d_str = '
'+p_str+'
'; 152 | if(scroll){ 153 | chatObj.append(d_str); 154 | }else{ 155 | chatObj.prepend(d_str); 156 | } 157 | } 158 | $(jQid).hoverPic(); 159 | if(in_reply_to_status_id_str){ 160 | if(scroll){ 161 | $(jQid).append('
'); 162 | }else{ 163 | $(jQid).append($('#'+in_reply_to_status_id_str).css('margin-left', '14px')); 164 | } 165 | } 166 | if(!(uidObj.length)){ 167 | var bioObj = $('').hoverBio(); 168 | if(scroll){ 169 | sidebarObj.append(bioObj); 170 | }else{ 171 | allObj.after(bioObj); 172 | } 173 | uidObj = $(jQuid); 174 | }else if(!scroll){ 175 | allObj.after(uidObj); 176 | } 177 | $(jQuid+'.sidebar a span.badge').text(increaseBadge); 178 | $('#all.sidebar a span.badge').text(increaseBadge); 179 | if(mentioned){ 180 | $('#mentions.sidebar a span.badge').text(increaseBadge); 181 | $(jQid).addClass('mentions'); 182 | } 183 | if(uid!==selected_id && selected_id !== 'all'){ 184 | $(jQid+' :not(:has(.'+selected_id+'))').parentsUntil('#chat').hide(); 185 | } 186 | $(jQid+' p a img.profile').hoverBio(uidObj); 187 | $(jQid).data('user_mentions', user_mentions); 188 | } 189 | }else{ 190 | console.log(message); 191 | } 192 | } 193 | } 194 | }); 195 | 196 | setInterval(function(){ $('time.sec').ago(); }, SECOND); 197 | setInterval(function(){ $('time.min').ago(); }, MINUTE/6); 198 | setInterval(function(){ $('time.hr').ago(); }, HOUR/4); 199 | setInterval(function(){ $('time.day').ago(); }, DAY/3); 200 | 201 | textObj.keydown(function(e){ 202 | if((e.keyCode||e.which)===13){ // return key 203 | document.send(); 204 | e.preventDefault(); 205 | } 206 | }); 207 | textObj.keyup(function(e){ 208 | $('#countChar').text(140-$(this).text().length); 209 | if(textObj.text()===''){ 210 | reply_id = ''; 211 | } 212 | }); 213 | 214 | this.retweet = function(id_str){ 215 | socket.emit('retweet', {id_str: id_str}); 216 | textObj.focus(); 217 | return false; 218 | }; 219 | 220 | this.reply = function(id_str, screen_name){ 221 | var rms = ['@'+screen_name, ''], 222 | ums = $('#'+id_str).data('user_mentions'); 223 | for(var i=ums.length; i--;){ 224 | if(ums[i].screen_name !== screen_name && ums[i].screen_name !== name){ 225 | rms.splice(-1, 0, '@'+ums[i].screen_name); 226 | } 227 | } 228 | textObj.text(rms.join(' ')); 229 | 230 | textObj.focus(); 231 | reply_id = id_str; 232 | selected_id = 'reply'; 233 | $('#chat> :not(:has(#'+id_str+'), #'+id_str+')').hide(); 234 | return false; 235 | }; 236 | 237 | this.destroy = function(id_str){ 238 | deleteOutDiv(id_str); 239 | socket.emit('destroy', {id_str: id_str}); 240 | textObj.focus(); 241 | return false; 242 | }; 243 | 244 | this.send = function(){ 245 | var text = textObj.text(); 246 | 247 | if(text && name){ 248 | socket.emit('update', {text: text, in_reply_to_status_id: reply_id, include_entities: true}); 249 | textObj.text(''); 250 | reply_id = ''; 251 | textObj.focus(); 252 | this.select('all'); 253 | }else{ 254 | alert('Oops, blank message...'); 255 | } 256 | return false; 257 | }; 258 | 259 | this.select = function(id_str){ 260 | selected_id = id_str; 261 | $('#'+id_str+' .bio').hide(); 262 | $('.sidebar').css('background', '#fff'); 263 | $('#'+id_str).css('background', '#ddd'); 264 | $('#chat').find(':hidden:not(.twitpic)').show(); 265 | if(id_str!=='all'){ 266 | $('#chat> :not(:has(.'+id_str+'), .'+id_str+')').hide(); 267 | } 268 | return false; 269 | }; 270 | 271 | windowObj.scroll(function(){ 272 | if (documentObj.height() - windowObj.height() - windowObj.scrollTop() <= 0){ 273 | socket.emit('scroll', {page: ++page, include_entities: true}); 274 | } 275 | }); 276 | }); 277 | -------------------------------------------------------------------------------- /twitter.js: -------------------------------------------------------------------------------- 1 | //This is a modified version of twitter.js that is originally from 2 | //https://github.com/yssk22/node-twbot/ 3 | var util = require('util'), 4 | url = require('url'), 5 | querystring = require('querystring'), 6 | crypto = require('crypto'), 7 | http = require('http'), 8 | EventEmitter = require('events').EventEmitter, 9 | OAuth = require('oauth').OAuth; 10 | 11 | // for debugging 12 | var debug, 13 | debugLevel = parseInt(process.env.NODE_DEBUG, 16); 14 | if (debugLevel & 0x4) { 15 | debug = function (x) { util.error('[twbot/Twitter]: ' + x); }; 16 | } else { 17 | debug = function () { }; 18 | } 19 | 20 | /** 21 | * OAuth Configuration constants 22 | */ 23 | var OAUTH_CONFIG = { 24 | RequestTokenUrl : 'https://api.twitter.com/oauth/request_token', 25 | AccessTokenUrl : 'https://api.twitter.com/oauth/access_token', 26 | Version : '1.0', 27 | Method : 'HMAC-SHA1' 28 | }; 29 | 30 | /** 31 | * Twitter API endpoint URL 32 | */ 33 | var API_URL = 'https://api.twitter.com/1', 34 | STREAM_URL = 'https://userstream.twitter.com/2', 35 | AUTHORIZE_URL = 'https://twitter.com/oauth/authorize?oauth_token='; 36 | 37 | /** 38 | * Twitter API Client 39 | * 40 | * @param consumerKey {String} consumerKey OAuth Consumer Key 41 | * @param consumerSecret {String} consumerSecret OAuth Consumer Secret 42 | * @param options {Object} API behavior options 43 | * 44 | */ 45 | function Twitter(consumerKey, consumerSecret, options){ 46 | if( !options ){ 47 | options = {}; 48 | } 49 | if(!(this instanceof Twitter)){ // enforcing new 50 | return new Twitter(consumerKey, consumerSecret, options); 51 | } 52 | 53 | this._oa = new OAuth( 54 | OAUTH_CONFIG.RequestTokenUrl, 55 | OAUTH_CONFIG.AccessTokenUrl, 56 | consumerKey, 57 | consumerSecret, 58 | OAUTH_CONFIG.Version, 59 | null, 60 | OAUTH_CONFIG.Method 61 | ); 62 | this.accessKey = options.accessKey; 63 | this.accessSecret = options.accessSecret; 64 | this._token = options._token; 65 | this._token_secret = options._token_secret; 66 | this._results = options._results; 67 | 68 | this._apiUrl = options._apiUrl || API_URL; 69 | this._streamUrl = options._streamUrl || STREAM_URL; 70 | } 71 | /** 72 | * Normalize the error as an Error object. 73 | * 74 | * @param err {Object} An object to be normalized 75 | * 76 | */ 77 | function normalizeError(err){ 78 | if( err instanceof Error ){ 79 | return err; 80 | }else if( err.statusCode ){ 81 | // for 4XX/5XX error 82 | var e = new Error(err.statusCode + ': ' + err.data); 83 | e.statusCode = err.statusCode; 84 | try{ 85 | e.data = JSON.parse(err.data); 86 | }catch(er){ 87 | e.data = err.data; 88 | } 89 | return e; 90 | }else{ 91 | // unknown error 92 | return new Error(err); 93 | } 94 | } 95 | 96 | /** 97 | * build the url with the specified path and params. 98 | * 99 | * @param path {String} the path string. 100 | * @param params {Object} (optional) the query parameter object. 101 | */ 102 | function buildUrl(path, params){ 103 | var qs; 104 | if( typeof params == 'object' ){ 105 | qs = querystring.stringify(params); 106 | } 107 | return qs ? path + '?' + qs : path; 108 | } 109 | 110 | Twitter.prototype.getRequestToken = function(callback){ 111 | var self = this; 112 | this._oa.getOAuthRequestToken(function(err, token, token_secret, results){ 113 | if(err){ 114 | if(callback){callback(normalizeError(err));} 115 | }else{ 116 | self._token = token; 117 | self._token_secret = token_secret; 118 | self._token_results = results; 119 | if(callback){callback(null, AUTHORIZE_URL + token);} 120 | } 121 | }); 122 | }; 123 | 124 | Twitter.prototype.getAccessToken = function(verifier, callback){ 125 | var self = this; 126 | this._oa.getOAuthAccessToken( 127 | self._token, self._token_secret, verifier, 128 | function(error, akey, asecret, results2){ 129 | if(error){ 130 | if(callback){callback(normalizeError(error));} 131 | }else{ 132 | self.accessKey = akey; 133 | self.accessSecret = asecret; 134 | self._results = results2; 135 | if(callback){callback(null, akey, asecret);} 136 | } 137 | }); 138 | }; 139 | 140 | // ----------------------------------------------------------------------------- 141 | // Tweets Resources 142 | // ----------------------------------------------------------------------------- 143 | Twitter.prototype.show = function(id, params, callback){ 144 | if( typeof params == 'function' ){ 145 | callback = params; 146 | params = {}; 147 | } 148 | var path = '/statuses/' + id + '.json'; 149 | return this._doGet(path, params, callback); 150 | }; 151 | 152 | Twitter.prototype.update = function(params, callback){ 153 | if( typeof params == 'string' ){ 154 | params = { 155 | status: params 156 | }; 157 | } else{ 158 | params = {status: params.text, in_reply_to_status_id: params.in_reply_to_status_id}; 159 | } 160 | return this._doPost('/statuses/update.json', params, callback); 161 | }; 162 | 163 | Twitter.prototype.destroy = function(id, callback){ 164 | return this._doPost('/statuses/destroy/' + id + '.json', {}, callback); 165 | }; 166 | 167 | Twitter.prototype.retweet = function(id, callback){ 168 | return this._doPost('/statuses/retweet/' + id + '.json', {}, callback); 169 | }; 170 | Twitter.prototype.retweets = function(id, callback){ 171 | return this._doGet('/statuses/retweets/' + id + '.json', {}, callback); 172 | }; 173 | 174 | // ----------------------------------------------------------------------------- 175 | // Friendship Resources 176 | // ----------------------------------------------------------------------------- 177 | Twitter.prototype.follow = function(params, callback){ 178 | if( typeof params == "string" ){ 179 | params = { 180 | user_id: params 181 | }; 182 | } 183 | this._doPost('/friendships/create.json', params, callback); 184 | }; 185 | 186 | Twitter.prototype.unfollow = function(params, callback){ 187 | if( typeof params == "string" ){ 188 | params = { 189 | user_id: params 190 | }; 191 | } 192 | this._doPost('/friendships/delete.json', params, callback); 193 | }; 194 | 195 | Twitter.prototype.followers = function(params, callback){ 196 | if( typeof params == 'function' ){ 197 | callback = params; 198 | params = { 199 | user_id: this._results.user_id 200 | }; 201 | } 202 | var path = '/followers/ids.json'; 203 | this._doGet(path, params, callback); 204 | }; 205 | 206 | // ----------------------------------------------------------------------------- 207 | // Tweets Resources 208 | // ----------------------------------------------------------------------------- 209 | var supportedTLTypes = ['public_timeline', 'home_timeline', 'friends_timeline', 'user_timeline','mentions','retweeted_by_me','retweeted_to_me','retweeted_of_me']; 210 | Twitter.prototype.getTimeline = function(params, callback){ 211 | if( typeof params == 'function' ){ 212 | callback = params; 213 | params = { 214 | type: 'home_timeline' 215 | }; 216 | }else if( typeof params == 'string' ){ 217 | params = { 218 | type: params 219 | }; 220 | }else if( typeof params == 'object') { 221 | params.type = params.type || 'home_timeline'; 222 | }else { 223 | throw new TypeError('params must be string or object'); 224 | } 225 | if( supportedTLTypes.indexOf(params.type) == -1 ){ 226 | throw new Error('timeline type must be one of (' + supportedTLTypes.join(',') + ') but ' + params.type + '.'); 227 | } 228 | var path = '/statuses/' + params.type + '.json'; 229 | delete(params.type); 230 | this._doGet(path, params, callback); 231 | }; 232 | 233 | Twitter.prototype.getListStatuses = function(user, id, params, callback){ 234 | if( typeof(params) == 'function' ){ 235 | callback = params; 236 | params = {}; 237 | } 238 | var path = ['', user, 'lists', id, 'statuses'].join('/') + '.json'; 239 | this._doGet(path, params, callback); 240 | }; 241 | 242 | // ----------------------------------------------------------------------------- 243 | // Account Resources 244 | // ----------------------------------------------------------------------------- 245 | Twitter.prototype.getAccount = function(params, callback){ 246 | if( typeof params == 'boolean' ){ 247 | params = {include_entities: params}; 248 | }else if(typeof params == 'function'){ 249 | callback = params; 250 | params = {}; 251 | } 252 | var path = '/account/verify_credentials.json'; 253 | this._doGet(path, params, callback); 254 | }; 255 | 256 | 257 | // ----------------------------------------------------------------------------- 258 | // Streaming Support 259 | // ----------------------------------------------------------------------------- 260 | /** 261 | * UserStream event class 262 | */ 263 | function UserStream(client, request){ 264 | this._client = client; 265 | this._request = request; 266 | this._request.end(); 267 | // Event Binding 268 | var self = this; 269 | function _dataReceived(jsonStr, response){ 270 | if( jsonStr ){ 271 | var obj = undefined; 272 | try{ 273 | obj = JSON.parse(jsonStr); 274 | }catch(e){ 275 | // ignore invalid JSON string from twitter. 276 | } 277 | if( obj ){ 278 | debug('onData: ' + util.inspect(obj)); 279 | self.emit('data', obj); 280 | } 281 | } 282 | } 283 | self._request.on('error', function(err){ 284 | this.emit('error', err, undefined); 285 | }); 286 | self._request.on('response', function(response){ 287 | self._response = response; 288 | response.setEncoding('utf8'); 289 | var isError = response.statusCode != 200; 290 | var buff = ''; 291 | response.on('data', function(chunk){ 292 | if( isError ){ 293 | buff += chunk; 294 | }else{ 295 | // valid stream started 296 | if( chunk.match(/\n/) ){ 297 | // a line seperatator implies a data seperator. 298 | var chunks = chunk.split(/\r?\n/); 299 | var jsonStr = buff + chunks.shift(); // first chunk 300 | _dataReceived(jsonStr, response); 301 | if( chunks ){ 302 | buff = chunks.pop(); // last chunk move back to buffer because it may be incomplete. 303 | } 304 | // all chunks are passed 305 | for(var i=0, len=chunks.length; i> ' + util.inspect(body)); 375 | var url = [this._apiUrl, path].join(''); 376 | this._oa.post(url, this.accessKey, this.accessSecret, 377 | body, 378 | this._createResponseHandler(callback)); 379 | }; 380 | 381 | Twitter.prototype._createResponseHandler = function(callback){ 382 | return function(error, data, response){ 383 | if( error ){ 384 | return callback && callback(normalizeError(error), data, response); 385 | }else{ 386 | var obj = undefined; 387 | if( data ){ 388 | debug('<< ' + data); 389 | try{ 390 | obj = JSON.parse(data); 391 | }catch(e){ 392 | obj = data; 393 | return callback(e, data, reponse); 394 | } 395 | return callback && callback(undefined, obj, response); 396 | }else{ 397 | return callback && callback(undefined, data, response); 398 | } 399 | } 400 | }; 401 | }; 402 | 403 | module.exports = Twitter; 404 | 405 | --------------------------------------------------------------------------------