diff --git a/README b/README index 989fcb7f0a..67dc9a66b3 100644 --- a/README +++ b/README @@ -507,7 +507,7 @@ server is probably a good idea for high-volume sites. needs as a parameter the install path; if you run it from the Laconica dir, "." should suffice. -This will run six (for now) queue handlers: +This will run eight (for now) queue handlers: * xmppdaemon.php - listens for new XMPP messages from users and stores them as notices in the database. @@ -521,6 +521,10 @@ This will run six (for now) queue handlers: of registered users. * xmppconfirmhandler.php - sends confirmation messages to registered users. +* twitterqueuehandler.php - sends queued notices to Twitter for user + who have opted to set up Twitter bridging. +* facebookqueuehandler.php - sends queued notices to Facebook for users + of the built-in Facebook application. Note that these queue daemons are pretty raw, and need your care. In particular, they leak memory, and you may want to restart them on a @@ -553,6 +557,53 @@ Sample cron job: # Update Twitter friends subscriptions every half hour 0,30 * * * * /path/to/php /path/to/laconica/scripts/synctwitterfriends.php>&/dev/null +Built-in Facebook Application +----------------------------- + +Laconica's Facebook application allows your users to automatically +update their Facebook statuses with their latest notices, invite +their friends to use the app (and thus your site), view their notice +timelines, and post notices -- all from within Facebook. The application +is built into Laconica and runs on your host. For automatic Facebook +status updating to work you will need to enable queuing and run the +facebookqueuehandler.php daemon (see the "Queues and daemons" section +above). + +Quick setup instructions*: + +Install the Facebook Developer application on Facebook: + + http://www.facebook.com/developers/ + +Use it to create a new application and generate an API key and secret. +Uncomment the Facebook app section of your config.php and copy in the +key and secret, e.g.: + + # Config section for the built-in Facebook application + $config['facebook']['apikey'] = 'APIKEY'; + $config['facebook']['secret'] = 'SECRET'; + +In Facebook's application editor, specify the following URLs for your app: + +- Callback URL: http://example.net/mublog/facebook/ +- Post-Remove URL: http://example.net/mublog/facebook/remove +- Post-Add Redirect URL: http://apps.facebook.com/yourapp/ +- Canvas URL: http://apps.facebook.com/yourapp/ + +(Replace 'example.net' with your host's URL, 'mublog' with the path +to your Laconica installation, and 'yourapp' with the name of the +Facebook application you created.) + +Additionally, Choose "Web" for Application type in the Advanced tab. +In the "Canvas setting" section, choose the "FBML" for Render Method, +"Smart Size" for IFrame size, and "Full width (760px)" for Canvas Width. +Everything else can be left with default values. + +*For more detailed instructions please see the installation guide on the +Laconica wiki: + + http://laconi.ca/trac/wiki/FacebookApplication + Sitemaps -------- diff --git a/actions/emailsettings.php b/actions/emailsettings.php index b84acb2141..634388fddd 100644 --- a/actions/emailsettings.php +++ b/actions/emailsettings.php @@ -164,6 +164,11 @@ class EmailsettingsAction extends AccountSettingsAction $user->emailnotifymsg); $this->elementEnd('li'); $this->elementStart('li'); + $this->checkbox('emailnotifyattn', + _('Send me email when someone sends me an "@-reply".'), + $user->emailnotifyattn); + $this->elementEnd('li'); + $this->elementStart('li'); $this->checkbox('emailnotifynudge', _('Allow friends to nudge me and send me an email.'), $user->emailnotifynudge); @@ -255,6 +260,7 @@ class EmailsettingsAction extends AccountSettingsAction $emailnotifyfav = $this->boolean('emailnotifyfav'); $emailnotifymsg = $this->boolean('emailnotifymsg'); $emailnotifynudge = $this->boolean('emailnotifynudge'); + $emailnotifyattn = $this->boolean('emailnotifyattn'); $emailmicroid = $this->boolean('emailmicroid'); $emailpost = $this->boolean('emailpost'); @@ -270,6 +276,7 @@ class EmailsettingsAction extends AccountSettingsAction $user->emailnotifyfav = $emailnotifyfav; $user->emailnotifymsg = $emailnotifymsg; $user->emailnotifynudge = $emailnotifynudge; + $user->emailnotifyattn = $emailnotifyattn; $user->emailmicroid = $emailmicroid; $user->emailpost = $emailpost; diff --git a/actions/showstream.php b/actions/showstream.php index c736c99b5d..65482167e1 100644 --- a/actions/showstream.php +++ b/actions/showstream.php @@ -292,11 +292,11 @@ class ShowstreamAction extends Action $this->elementStart('ul', 'tags xoxo'); foreach ($tags as $tag) { $this->elementStart('li'); - $this->element('span', 'mark_hash', '#'); - $this->element('a', array('rel' => 'tag', - 'href' => common_local_url('peopletag', - array('tag' => $tag))), - $tag); + // Avoid space by using raw output. + $pt = '#'; + $this->raw($pt); $this->elementEnd('li'); } $this->elementEnd('ul'); diff --git a/bin/flowplayer-3.0.5.swf b/bin/flowplayer-3.0.5.swf new file mode 100644 index 0000000000..05b64a032b Binary files /dev/null and b/bin/flowplayer-3.0.5.swf differ diff --git a/bin/flowplayer.audio-3.0.3.swf b/bin/flowplayer.audio-3.0.3.swf new file mode 100644 index 0000000000..ef85f1bff0 Binary files /dev/null and b/bin/flowplayer.audio-3.0.3.swf differ diff --git a/bin/flowplayer.controls-3.0.3.swf b/bin/flowplayer.controls-3.0.3.swf new file mode 100644 index 0000000000..09a27e8a98 Binary files /dev/null and b/bin/flowplayer.controls-3.0.3.swf differ diff --git a/classes/Notice.php b/classes/Notice.php index 6db59c96ef..8300667fa4 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -34,22 +34,23 @@ class Notice extends Memcached_DataObject ###START_AUTOCODE /* the code below is auto generated do not remove the above tag */ - public $__table = 'notice'; // table name - public $id; // int(4) primary_key not_null - public $profile_id; // int(4) not_null + public $__table = 'notice'; // table name + public $id; // int(4) primary_key not_null + public $profile_id; // int(4) not_null public $uri; // varchar(255) unique_key public $content; // varchar(140) - public $rendered; // text() + public $rendered; // text() public $url; // varchar(255) - public $created; // datetime() not_null - public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP - public $reply_to; // int(4) - public $is_local; // tinyint(1) - public $source; // varchar(32) + public $created; // datetime() not_null + public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP + public $reply_to; // int(4) + public $is_local; // tinyint(1) + public $source; // varchar(32) /* Static get */ - function staticGet($k,$v=null) - { return Memcached_DataObject::staticGet('Notice',$k,$v); } + function staticGet($k,$v=NULL) { + return Memcached_DataObject::staticGet('Notice',$k,$v); + } /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE @@ -94,23 +95,28 @@ class Notice extends Memcached_DataObject /* Add them to the database */ foreach(array_unique($match[1]) as $hashtag) { /* elide characters we don't want in the tag */ - $hashtag = common_canonical_tag($hashtag); - - $tag = DB_DataObject::factory('Notice_tag'); - $tag->notice_id = $this->id; - $tag->tag = $hashtag; - $tag->created = $this->created; - $id = $tag->insert(); - if (!$id) { - $last_error = PEAR::getStaticProperty('DB_DataObject','lastError'); - common_log(LOG_ERR, 'DB error inserting hashtag: ' . $last_error->message); - common_server_error(sprintf(_('DB error inserting hashtag: %s'), $last_error->message)); - return; - } + $this->saveTag($hashtag); } return true; } + function saveTag($hashtag) + { + $hashtag = common_canonical_tag($hashtag); + + $tag = new Notice_tag(); + $tag->notice_id = $this->id; + $tag->tag = $hashtag; + $tag->created = $this->created; + $id = $tag->insert(); + + if (!$id) { + throw new ServerException(sprintf(_('DB error inserting hashtag: %s'), + $last_error->message)); + return; + } + } + static function saveNew($profile_id, $content, $source=null, $is_local=1, $reply_to=null, $uri=null) { $profile = Profile::staticGet($profile_id); @@ -136,10 +142,12 @@ class Notice extends Memcached_DataObject $notice->profile_id = $profile_id; $blacklist = common_config('public', 'blacklist'); + $autosource = common_config('public', 'autosource'); # Blacklisted are non-false, but not 1, either - if ($blacklist && in_array($profile_id, $blacklist)) { + if (($blacklist && in_array($profile_id, $blacklist)) || + ($source && $autosource && in_array($source, $autosource))) { $notice->is_local = -1; } else { $notice->is_local = $is_local; @@ -619,6 +627,15 @@ class Notice extends Memcached_DataObject continue; } + // we automatically add a tag for every group name, too + + $tag = Notice_tag::pkeyGet(array('tag' => common_canonical_tag($nickname), + 'notice_id' => $this->id)); + + if (is_null($tag)) { + $this->saveTag($nickname); + } + if ($profile->isMember($group)) { $gi = new Group_inbox(); @@ -730,10 +747,19 @@ class Notice extends Memcached_DataObject if (!$id) { common_log_db_error($reply, 'INSERT', __FILE__); return; + } else { + $replied[$recipient->id] = 1; } } } } } + + foreach (array_keys($replied) as $recipient) { + $user = User::staticGet('id', $recipient); + if ($user) { + mail_notify_attn($user, $this); + } + } } } diff --git a/classes/Notice_tag.php b/classes/Notice_tag.php index 94f9296d60..0365973f56 100644 --- a/classes/Notice_tag.php +++ b/classes/Notice_tag.php @@ -19,7 +19,7 @@ require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; -class Notice_tag extends Memcached_DataObject +class Notice_tag extends Memcached_DataObject { ###START_AUTOCODE /* the code below is auto generated do not remove the above tag */ @@ -35,9 +35,9 @@ class Notice_tag extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE - + static function getStream($tag, $offset=0, $limit=20) { - $qry = + $qry = 'SELECT notice.* ' . 'FROM notice JOIN notice_tag ON notice.id = notice_tag.notice_id ' . 'WHERE notice_tag.tag = "%s" '; @@ -46,7 +46,7 @@ class Notice_tag extends Memcached_DataObject 'notice_tag:notice_stream:' . common_keyize($tag), $offset, $limit); } - + function blowCache() { $cache = common_memcache(); @@ -54,4 +54,9 @@ class Notice_tag extends Memcached_DataObject $cache->delete(common_cache_key('notice_tag:notice_stream:' . $this->tag)); } } + + function &pkeyGet($kv) + { + return Memcached_DataObject::pkeyGet('Notice_tag', $kv); + } } diff --git a/classes/User.php b/classes/User.php index a6a1b11b9f..495a982360 100644 --- a/classes/User.php +++ b/classes/User.php @@ -40,6 +40,7 @@ class User extends Memcached_DataObject public $emailnotifyfav; // tinyint(1) default_1 public $emailnotifynudge; // tinyint(1) default_1 public $emailnotifymsg; // tinyint(1) default_1 + public $emailnotifyattn; // tinyint(1) default_1 public $emailmicroid; // tinyint(1) default_1 public $language; // varchar(50) public $timezone; // varchar(50) @@ -62,8 +63,10 @@ class User extends Memcached_DataObject public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP /* Static get */ - function staticGet($k,$v=null) - { return Memcached_DataObject::staticGet('User',$k,$v); } + function staticGet($k,$v=NULL) + { + return Memcached_DataObject::staticGet('User',$k,$v); + } /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE diff --git a/classes/laconica.ini b/classes/laconica.ini index 19267f2688..5fd2cd1f86 100755 --- a/classes/laconica.ini +++ b/classes/laconica.ini @@ -332,6 +332,7 @@ emailnotifysub = 17 emailnotifyfav = 17 emailnotifynudge = 17 emailnotifymsg = 17 +emailnotifyattn = 17 emailmicroid = 17 language = 2 timezone = 2 diff --git a/config.php.sample b/config.php.sample index a2c5801f45..6e55eaffc8 100644 --- a/config.php.sample +++ b/config.php.sample @@ -18,6 +18,8 @@ $config['site']['server'] = 'localhost'; $config['site']['path'] = 'laconica'; #$config['site']['fancy'] = false; #$config['site']['theme'] = 'default'; +#To enable the built-in mobile style sheet, defaults to false. +#$config['site']['mobile'] = true; #For contact email, defaults to $_SERVER["SERVER_ADMIN"] #$config['site']['email'] = 'admin@example.net'; #Brought by... @@ -107,6 +109,14 @@ $config['sphinx']['port'] = 3312; #$config['public']['blacklist'][] = 123; #$config['public']['blacklist'][] = 2307; +#Mark certain notice sources as automatic and thus not +#appropriate for public feed +#$config['public]['autosource'][] = 'twitterfeed'; +#$config['public]['autosource'][] = 'rssdent'; +#$config['public]['autosource'][] = 'Ping.Fm'; +#$config['public]['autosource'][] = 'HelloTxt'; +#$config['public]['autosource'][] = 'Updating.Me'; + #Do notice broadcasts offline #If you use this, you must run the six offline daemons in the #background. See the README for details. @@ -139,7 +149,7 @@ $config['sphinx']['port'] = 3312; #$config['profile']['banned'][] = 'hacker'; #$config['profile']['banned'][] = 12345; -# config section for the built-in Facebook application +# Config section for the built-in Facebook application #$config['facebook']['apikey'] = 'APIKEY'; #$config['facebook']['secret'] = 'SECRET'; diff --git a/db/laconica.sql b/db/laconica.sql index 15f03a978f..dd93a727b7 100644 --- a/db/laconica.sql +++ b/db/laconica.sql @@ -50,6 +50,7 @@ create table user ( emailnotifyfav tinyint default 1 comment 'Notify by email of favorites', emailnotifynudge tinyint default 1 comment 'Notify by email of nudges', emailnotifymsg tinyint default 1 comment 'Notify by email of direct messages', + emailnotifyattn tinyint default 1 comment 'Notify by email of @-replies', emailmicroid tinyint default 1 comment 'whether to publish email microid', language varchar(50) comment 'preferred language', timezone varchar(50) comment 'timezone', diff --git a/js/flowplayer-3.0.5.min.js b/js/flowplayer-3.0.5.min.js new file mode 100644 index 0000000000..b1c33150ac --- /dev/null +++ b/js/flowplayer-3.0.5.min.js @@ -0,0 +1,24 @@ +/** + * flowplayer.js 3.0.5. The Flowplayer API + * + * Copyright 2009 Flowplayer Oy + * + * This file is part of Flowplayer. + * + * Flowplayer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Flowplayer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Flowplayer. If not, see . + * + * Version: 3.0.5 - Tue Feb 03 2009 13:14:17 GMT-0000 (GMT+00:00) + */ +(function(){function log(args){console.log("$f.fireEvent",[].slice.call(args));}function clone(obj){if(!obj||typeof obj!='object'){return obj;}var temp=new obj.constructor();for(var key in obj){if(obj.hasOwnProperty(key)){temp[key]=clone(obj[key]);}}return temp;}function each(obj,fn){if(!obj){return;}var name,i=0,length=obj.length;if(length===undefined){for(name in obj){if(fn.call(obj[name],name,obj[name])===false){break;}}}else{for(var value=obj[0];i1){var swf=arguments[1];var conf=(arguments.length==3)?arguments[2]:{};if(typeof arg=='string'){if(arg.indexOf(".")!=-1){var instances=[];each(select(arg),function(){instances.push(new Player(this,clone(swf),clone(conf)));});return new Iterator(instances);}else{var node=el(arg);return new Player(node!==null?node:arg,swf,conf);}}else if(arg){return new Player(arg,swf,conf);}}return null;};extend(window.$f,{fireEvent:function(id,evt,a0,a1,a2){var p=$f(id);return p?p._fireEvent(evt,a0,a1,a2):null;},addPlugin:function(name,fn){Player.prototype[name]=fn;return $f;},each:each,extend:extend});if(document.all){window.onbeforeunload=function(){$f("*").each(function(){if(this.isLoaded()){this.close();}});};}if(typeof jQuery=='function'){jQuery.prototype.flowplayer=function(params,conf){if(!arguments.length||typeof arguments[0]=='number'){var arr=[];this.each(function(){var p=$f(this);if(p){arr.push(p);}});return arguments.length?arr[arguments[0]]:new Iterator(arr);}return this.each(function(){$f(this,clone(params),conf?clone(conf):{});});};}})();(function(){var jQ=typeof jQuery=='function';function isDomReady(){if(domReady.done){return false;}var d=document;if(d&&d.getElementsByTagName&&d.getElementById&&d.body){clearInterval(domReady.timer);domReady.timer=null;for(var i=0;i';}var e=extend({},p);e.width=e.height=e.id=e.w3c=e.src=null;for(var k in e){if(e[k]!==null){html+='';}}var vars="";if(c){for(var key in c){if(c[key]!==null){vars+=key+'='+(typeof c[key]=='object'?asString(c[key]):c[key])+'&';}}vars=vars.substring(0,vars.length-1);html+='';}html+="";return html;}function Flash(root,opts,flashvars){var version=flashembed.getVersion();extend(this,{getContainer:function(){return root;},getConf:function(){return conf;},getVersion:function(){return version;},getFlashvars:function(){return flashvars;},getApi:function(){return root.firstChild;},getHTML:function(){return getHTML(opts,flashvars);}});var required=opts.version;var express=opts.expressInstall;var ok=!required||flashembed.isSupported(required);if(ok){opts.onFail=opts.version=opts.expressInstall=null;root.innerHTML=getHTML(opts,flashvars);}else if(required&&express&&flashembed.isSupported([6,65])){extend(opts,{src:express});flashvars={MMredirectURL:location.href,MMplayerType:'PlugIn',MMdoctitle:document.title};root.innerHTML=getHTML(opts,flashvars);}else{if(root.innerHTML.replace(/\s/g,'')!==''){}else{root.innerHTML="

Flash version "+required+" or greater is required

"+"

"+(version[0]>0?"Your version is "+version:"You have no flash plugin installed")+"

"+"

Download latest version from here

";}}if(!ok&&opts.onFail){var ret=opts.onFail.call(this);if(typeof ret=='string'){root.innerHTML=ret;}}}window.flashembed=function(root,conf,flashvars){if(typeof root=='string'){var el=document.getElementById(root);if(el){root=el;}else{domReady(function(){flashembed(root,conf,flashvars);});return;}}if(!root){return;}var opts={width:'100%',height:'100%',allowfullscreen:true,allowscriptaccess:'always',quality:'high',version:null,onFail:null,expressInstall:null,w3c:false};if(typeof conf=='string'){conf={src:conf};}extend(opts,conf);return new Flash(root,opts,flashvars);};extend(window.flashembed,{getVersion:function(){var version=[0,0];if(navigator.plugins&&typeof navigator.plugins["Shockwave Flash"]=="object"){var _d=navigator.plugins["Shockwave Flash"].description;if(typeof _d!="undefined"){_d=_d.replace(/^.*\s+(\S+\s+\S+$)/,"$1");var _m=parseInt(_d.replace(/^(.*)\..*$/,"$1"),10);var _r=/r/.test(_d)?parseInt(_d.replace(/^.*r(.*)$/,"$1"),10):0;version=[_m,_r];}}else if(window.ActiveXObject){try{var _a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");}catch(e){try{_a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");version=[6,0];_a.AllowScriptAccess="always";}catch(ee){if(version[0]==6){return;}}try{_a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash");}catch(eee){}}if(typeof _a=="object"){_d=_a.GetVariable("$version");if(typeof _d!="undefined"){_d=_d.replace(/^\S+\s+(.*)$/,"$1").split(",");version=[parseInt(_d[0],10),parseInt(_d[2],10)];}}}return version;},isSupported:function(version){var now=flashembed.getVersion();var ret=(now[0]>version[0])||(now[0]==version[0]&&now[1]>=version[1]);return ret;},domReady:domReady,asString:asString,getHTML:getHTML});if(jQ){jQuery.prototype.flashembed=function(conf,flashvars){return this.each(function(){flashembed(this,conf,flashvars);});};}})(); \ No newline at end of file diff --git a/js/jquery.simplemodal-1.2.2.pack.js b/js/jquery.simplemodal-1.2.2.pack.js new file mode 100644 index 0000000000..b5ad5c23a3 --- /dev/null +++ b/js/jquery.simplemodal-1.2.2.pack.js @@ -0,0 +1,8 @@ +/* + * SimpleModal 1.2.2 - jQuery Plugin + * http://www.ericmmartin.com/projects/simplemodal/ + * Copyright (c) 2008 Eric Martin + * Dual licensed under the MIT and GPL licenses + * Revision: $Id: jquery.simplemodal.js 181 2008-12-16 16:51:44Z emartin24 $ + */ +eval(function(p,a,c,k,e,r){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('(g($){m f=$.Q.1Q&&1a($.Q.1D)==6&&!10[\'2g\'],1f=$.Q.1Q&&!$.2a,w=[];$.y=g(a,b){I $.y.12.1n(a,b)};$.y.D=g(){$.y.12.D()};$.1P.y=g(a){I $.y.12.1n(3,a)};$.y.1O={V:29,1J:\'r-H\',1B:{},1z:\'r-n\',20:{},1Z:{},v:2t,D:1o,1T:\'\',X:\'r-D\',l:F,1g:K,1e:F,1d:F,1c:F};$.y.12={7:F,4:{},1n:g(a,b){8(3.4.j){I K}3.7=$.U({},$.y.1O,b);3.v=3.7.v;3.1w=K;8(J a==\'27\'){a=a 25 1A?a:$(a);8(a.1v().1v().23()>0){3.4.T=a.1v();8(!3.7.1g){3.4.21=a.2x(1o)}}}q 8(J a==\'2w\'||J a==\'1r\'){a=$(\'<1q/>\').2s(a)}q{2r(\'2q 2p: 2o j 2l: \'+J a);I K}3.4.j=a.11(\'r-j\').E(3.7.1Z);a=F;3.1S();3.1R();8($.1m(3.7.1d)){3.7.1d.1l(3,[3.4])}I 3},1S:g(){w=3.1k();8(f){3.4.x=$(\'\').E($.U(3.7.2b,{1j:\'1i\',V:0,l:\'1h\',A:w[0],z:w[1],v:3.7.v,L:0,B:0})).O(\'u\')}3.4.H=$(\'<1q/>\').1N(\'1M\',3.7.1J).11(\'r-H\').E($.U(3.7.1B,{1j:\'1i\',V:3.7.V/1b,A:w[0],z:w[1],l:\'1h\',B:0,L:0,v:3.7.v+1})).O(\'u\');3.4.n=$(\'<1q/>\').1N(\'1M\',3.7.1z).11(\'r-n\').E($.U(3.7.20,{1j:\'1i\',l:\'1h\',v:3.7.v+2})).1K(3.7.D?$(3.7.1T).11(3.7.X):\'\').O(\'u\');3.19();8(f||1f){3.18()}3.4.n.1K(3.4.j.1I())},1H:g(){m a=3;$(\'.\'+3.7.X).1G(\'1L.r\',g(e){e.28();a.D()});$(10).1G(\'1F.r\',g(){w=a.1k();a.19();8(f||1f){a.18()}q{a.4.x&&a.4.x.E({A:w[0],z:w[1]});a.4.H.E({A:w[0],z:w[1]})}})},1E:g(){$(\'.\'+3.7.X).1C(\'1L.r\');$(10).1C(\'1F.r\')},18:g(){m p=3.7.l;$.26([3.4.x||F,3.4.H,3.4.n],g(i,e){8(e){m a=\'k.u.17\',N=\'k.u.1W\',16=\'k.u.24\',S=\'k.u.1y\',R=\'k.u.1x\',15=\'k.u.22\',1t=\'k.P.17\',1s=\'k.P.1W\',C=\'k.P.1y\',G=\'k.P.1x\',s=e[0].2v;s.l=\'2u\';8(i<2){s.14(\'A\');s.14(\'z\');s.Z(\'A\',\'\'+16+\' > \'+a+\' ? \'+16+\' : \'+a+\' + "o"\');s.Z(\'z\',\'\'+15+\' > \'+N+\' ? \'+15+\' : \'+N+\' + "o"\')}q{m b,W;8(p&&p.1Y==1X){8(p[0]){m c=J p[0]==\'1r\'?p[0].1V():p[0].13(/o/,\'\');b=c.1U(\'%\')==-1?c+\' + (t = \'+G+\' ? \'+G+\' : \'+R+\') + "o"\':1a(c.13(/%/,\'\'))+\' * ((\'+1t+\' || \'+a+\') / 1b) + (t = \'+G+\' ? \'+G+\' : \'+R+\') + "o"\'}8(p[1]){m d=J p[1]==\'1r\'?p[1].1V():p[1].13(/o/,\'\');W=d.1U(\'%\')==-1?d+\' + (t = \'+C+\' ? \'+C+\' : \'+S+\') + "o"\':1a(d.13(/%/,\'\'))+\' * ((\'+1s+\' || \'+N+\') / 1b) + (t = \'+C+\' ? \'+C+\' : \'+S+\') + "o"\'}}q{b=\'(\'+1t+\' || \'+a+\') / 2 - (3.2n / 2) + (t = \'+G+\' ? \'+G+\' : \'+R+\') + "o"\';W=\'(\'+1s+\' || \'+N+\') / 2 - (3.2m / 2) + (t = \'+C+\' ? \'+C+\' : \'+S+\') + "o"\'}s.14(\'L\');s.14(\'B\');s.Z(\'L\',b);s.Z(\'B\',W)}}})},1k:g(){m a=$(10);m h=$.Q.2k&&$.Q.1D>\'9.5\'&&$.1P.2i<=\'1.2.6\'?k.P[\'17\']:a.A();I[h,a.z()]},19:g(){m a,B,1u=(w[0]/2)-((3.4.n.A()||3.4.j.A())/2),1p=(w[1]/2)-((3.4.n.z()||3.4.j.z())/2);8(3.7.l&&3.7.l.1Y==1X){a=3.7.l[0]||1u;B=3.7.l[1]||1p}q{a=1u;B=1p}3.4.n.E({B:B,L:a})},1R:g(){3.4.x&&3.4.x.Y();8($.1m(3.7.1e)){3.7.1e.1l(3,[3.4])}q{3.4.H.Y();3.4.n.Y();3.4.j.Y()}3.1H()},D:g(){8(!3.4.j){I K}8($.1m(3.7.1c)&&!3.1w){3.1w=1o;3.7.1c.1l(3,[3.4])}q{8(3.4.T){8(3.7.1g){3.4.j.1I().O(3.4.T)}q{3.4.j.M();3.4.21.O(3.4.T)}}q{3.4.j.M()}3.4.n.M();3.4.H.M();3.4.x&&3.4.x.M();3.4={}}3.1E()}}})(1A);',62,158,'|||this|dialog|||opts|if||||||||function|||data|document|position|var|container|px||else|simplemodal|||body|zIndex||iframe|modal|width|height|left|sl|close|css|null|st|overlay|return|typeof|false|top|remove|bcw|appendTo|documentElement|browser|bst|bsl|parentNode|extend|opacity|le|closeClass|show|setExpression|window|addClass|impl|replace|removeExpression|bsw|bsh|clientHeight|fixIE|setPosition|parseInt|100|onClose|onShow|onOpen|ieQuirks|persist|fixed|none|display|getDimensions|apply|isFunction|init|true|vCenter|div|number|cw|ch|hCenter|parent|occb|scrollTop|scrollLeft|containerId|jQuery|overlayCss|unbind|version|unbindEvents|resize|bind|bindEvents|hide|overlayId|append|click|id|attr|defaults|fn|msie|open|create|closeHTML|indexOf|toString|clientWidth|Array|constructor|dataCss|containerCss|orig|scrollWidth|size|scrollHeight|instanceof|each|object|preventDefault|50|boxModel|iframeCss|javascript|src|Close|title|XMLHttpRequest|modalCloseImg|jquery|class|opera|type|offsetWidth|offsetHeight|Unsupported|Error|SimpleModal|alert|html|1000|absolute|style|string|clone'.split('|'),0,{})) \ No newline at end of file diff --git a/js/video.js b/js/video.js new file mode 100644 index 0000000000..936a6312e3 --- /dev/null +++ b/js/video.js @@ -0,0 +1,9 @@ +$('document').ready(function() { + $('a.media, a.mediamp3').append(' [PLAY]'); + $('a.mediamp3').html('').css('display', 'block').css('width', '224px').css('height','24px').flowplayer('../bin/flowplayer-3.0.5.swf'); + $('a.media').click(function() { + $('').attr('href', $(this).attr('href')).flowplayer('../bin/flowplayer-3.0.5.swf').modal({'closeHTML':''}); + return false; + }); +}); + diff --git a/lib/action.php b/lib/action.php index 48d5821a17..e2d09ace2b 100644 --- a/lib/action.php +++ b/lib/action.php @@ -153,14 +153,26 @@ class Action extends HTMLOutputter // lawsuit { if (Event::handle('StartShowStyles', array($this))) { if (Event::handle('StartShowLaconicaStyles', array($this))) { + $this->element('link', array('rel' => 'stylesheet', 'type' => 'text/css', 'href' => theme_path('css/display.css', 'base') . '?version=' . LACONICA_VERSION, 'media' => 'screen, projection, tv')); + $this->element('link', array('rel' => 'stylesheet', + 'type' => 'text/css', + 'href' => theme_path('css/modal.css', 'base') . '?version=' . LACONICA_VERSION, + 'media' => 'screen, projection, tv')); $this->element('link', array('rel' => 'stylesheet', 'type' => 'text/css', 'href' => theme_path('css/display.css', null) . '?version=' . LACONICA_VERSION, 'media' => 'screen, projection, tv')); + if (common_config('site', 'mobile')) { + $this->element('link', array('rel' => 'stylesheet', + 'type' => 'text/css', + 'href' => theme_path('css/mobile.css', 'base') . '?version=' . LACONICA_VERSION, + // TODO: "handheld" CSS for other mobile devices + 'media' => 'only screen and (max-device-width: 480px)')); // Mobile WebKit + } Event::handle('EndShowLaconicaStyles', array($this)); } if (Event::handle('StartShowUAStyles', array($this))) { @@ -196,6 +208,13 @@ class Action extends HTMLOutputter // lawsuit $this->element('script', array('type' => 'text/javascript', 'src' => common_path('js/jquery.form.js')), ' '); + + + $this->element('script', array('type' => 'text/javascript', + 'src' => common_path('js/jquery.simplemodal-1.2.2.pack.js')), + ' '); + + Event::handle('EndShowJQueryScripts', array($this)); } if (Event::handle('StartShowLaconicaScripts', array($this))) { @@ -208,6 +227,14 @@ class Action extends HTMLOutputter // lawsuit // Frame-busting code to avoid clickjacking attacks. $this->element('script', array('type' => 'text/javascript'), 'if (window.top !== window.self) { window.top.location.href = window.self.location.href; }'); + + $this->element('script', array('type' => 'text/javascript', + 'src' => common_path('js/flowplayer-3.0.5.min.js')), + ' '); + + $this->element('script', array('type' => 'text/javascript', + 'src' => common_path('js/video.js')), + ' '); Event::handle('EndShowLaconicaScripts', array($this)); } Event::handle('EndShowScripts', array($this)); diff --git a/lib/common.php b/lib/common.php index 7bfd14c429..4fc749ca06 100644 --- a/lib/common.php +++ b/lib/common.php @@ -106,7 +106,8 @@ $config = array('server' => null), 'public' => array('localonly' => true, - 'blacklist' => array()), + 'blacklist' => array(), + 'autosource' => array()), 'theme' => array('server' => null), 'throttle' => diff --git a/lib/mail.php b/lib/mail.php index a1faefc806..9fa86de5cc 100644 --- a/lib/mail.php +++ b/lib/mail.php @@ -573,3 +573,53 @@ function mail_notify_fave($other, $user, $notice) common_init_locale(); mail_to_user($other, $subject, $body); } + +/** + * notify a user that they have received an "attn:" message AKA "@-reply" + * + * @param User $user The user who recevied the notice + * @param Notice $notice The notice that was sent + * + * @return void + */ + +function mail_notify_attn($user, $notice) +{ + if (!$user->email || !$user->emailnotifyattn) { + return; + } + + $sender = $notice->getProfile(); + + $bestname = $sender->getBestName(); + + common_init_locale($user->language); + + $subject = sprintf(_('%s sent a notice to your attention'), $bestname); + + $body = sprintf(_("%1\$s just sent a notice to your attention (an '@-reply') on %2\$s.\n\n". + "The notice is here:\n\n". + "\t%3\$s\n\n" . + "It reads:\n\n". + "\t%4\$s\n\n" . + "You can reply back here:\n\n". + "\t%5\$s\n\n" . + "The list of all @-replies for you here:\n\n" . + "%6\$s\n\n" . + "Faithfully yours,\n" . + "%2\$s\n\n" . + "P.S. You can turn off these email notifications here: %7\$s\n"), + $bestname, + common_config('site', 'name'), + common_local_url('shownotice', + array('notice' => $notice->id)), + $notice->content, + common_local_url('newnotice', + array('replyto' => $sender->nickname)), + common_local_url('replies', + array('nickname' => $user->nickname)), + common_local_url('emailsettings')); + + common_init_locale(); + mail_to_user($user, $subject, $body); +} diff --git a/lib/util.php b/lib/util.php index b065c2d748..46aa7b9df9 100644 --- a/lib/util.php +++ b/lib/util.php @@ -474,12 +474,18 @@ function common_replace_urls_callback($text, $callback) { function common_linkify($url) { // It comes in special'd, so we unspecial it before passing to the stringifying // functions + $ext = pathinfo($url, PATHINFO_EXTENSION); $url = htmlspecialchars_decode($url); + $video_ext = array('mp4', 'flv', 'avi', 'mpg', 'mp3', 'ogg'); $display = $url; $url = (!preg_match('#^([a-z]+://|(mailto|aim|tel):)#i', $url)) ? 'http://'.$url : $url; $attrs = array('href' => $url, 'rel' => 'external'); + if (in_array($ext, $video_ext)) { + $attrs['class'] = 'media'; + } + if ($longurl = common_longurl($url)) { $attrs['title'] = $longurl; } @@ -590,7 +596,7 @@ function common_tag_link($tag) $xs->element('a', array('href' => $url, 'rel' => 'tag'), $tag); - $xs->elementEnd(); + $xs->elementEnd('span'); return $xs->getString(); } diff --git a/plugins/BlogspamNetPlugin.php b/plugins/BlogspamNetPlugin.php new file mode 100644 index 0000000000..d9372bcd56 --- /dev/null +++ b/plugins/BlogspamNetPlugin.php @@ -0,0 +1,144 @@ +. + * + * @category Plugin + * @package Laconica + * @author Evan Prodromou + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +define('BLOGSPAMNETPLUGIN_VERSION', '0.1'); + +/** + * Plugin to check submitted notices with blogspam.net + * + * When new notices are saved, we check their text with blogspam.net (or + * a compatible service). + * + * Blogspam.net is supposed to catch blog comment spam, and I found that + * some of its tests (min/max size, bayesian match) gave a lot of false positives. + * So, I've turned those tests off by default. This may not get as many + * hits, but it's better than nothing. + * + * @category Plugin + * @package Laconica + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + * + * @see Event + */ + +class BlogspamNetPlugin extends Plugin +{ + var $baseUrl = 'http://test.blogspam.net:8888/'; + + function __construct($url=null) + { + parent::__construct(); + if ($url) { + $this->baseUrl = $url; + } + } + + function onStartNoticeSave($notice) + { + $args = $this->testArgs($notice); + common_debug("Blogspamnet args = " . print_r($args, TRUE)); + $request = xmlrpc_encode_request('testComment', array($args)); + $context = stream_context_create(array('http' => array('method' => "POST", + 'header' => + "Content-Type: text/xml\r\n". + "User-Agent: " . $this->userAgent(), + 'content' => $request))); + $file = file_get_contents($this->baseUrl, false, $context); + $response = xmlrpc_decode($file); + if (xmlrpc_is_fault($response)) { + throw new ServerException("$response[faultString] ($response[faultCode])", 500); + } else { + common_debug("Blogspamnet results = " . $response); + if (preg_match('/^ERROR(:(.*))?$/', $response, $match)) { + throw new ServerException(sprintf(_("Error from %s: %s"), $this->baseUrl, $match[2]), 500); + } else if (preg_match('/^SPAM(:(.*))?$/', $response, $match)) { + throw new ClientException(sprintf(_("Spam checker results: %s"), $match[2]), 400); + } else if (preg_match('/^OK$/', $response)) { + // don't do anything + } else { + throw new ServerException(sprintf(_("Unexpected response from %s: %s"), $this->baseUrl, $response), 500); + } + } + return true; + } + + function testArgs($notice) + { + $args = array(); + $args['comment'] = $notice->content; + $args['ip'] = $this->getClientIP(); + + if (isset($_SERVER) && array_key_exists('HTTP_USER_AGENT', $_SERVER)) { + $args['agent'] = $_SERVER['HTTP_USER_AGENT']; + } + + $profile = $notice->getProfile(); + + if ($profile && $profile->homepage) { + $args['link'] = $profile->homepage; + } + + if ($profile && $profile->fullname) { + $args['name'] = $profile->fullname; + } else { + $args['name'] = $profile->nickname; + } + + $args['site'] = common_root_url(); + $args['version'] = $this->userAgent(); + + $args['options'] = "max-size=140,min-size=0,min-words=0,exclude=bayasian"; + + return $args; + } + + function getClientIP() + { + if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) { + // Note: order matters here; use proxy-forwarded stuff first + foreach (array('HTTP_X_FORWARDED_FOR', 'CLIENT-IP', 'REMOTE_ADDR') as $k) { + if (isset($_SERVER[$k])) { + return $_SERVER[$k]; + } + } + } + return '127.0.0.1'; + } + + function userAgent() + { + return 'BlogspamNetPlugin/'.BLOGSPAMNETPLUGIN_VERSION . ' Laconica/' . LACONICA_VERSION; + } +} diff --git a/scripts/facebookqueuehandler.php b/scripts/facebookqueuehandler.php new file mode 100755 index 0000000000..c6859cb219 --- /dev/null +++ b/scripts/facebookqueuehandler.php @@ -0,0 +1,71 @@ +#!/usr/bin/env php +. + */ + +# Abort if called from a web server +if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) { + print "This script must be run from the command line\n"; + exit(); +} + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); +define('LACONICA', true); + +require_once(INSTALLDIR . '/lib/common.php'); +require_once(INSTALLDIR . '/lib/facebookutil.php'); +require_once(INSTALLDIR . '/lib/queuehandler.php'); + +set_error_handler('common_error_handler'); + +class FacebookQueueHandler extends QueueHandler +{ + + function transport() + { + return 'facebook'; + } + + function start() + { + $this->log(LOG_INFO, "INITIALIZE"); + return true; + } + + function handle_notice($notice) + { + return facebookBroadcastNotice($notice); + } + + function finish() + { + } + +} + +ini_set("max_execution_time", "0"); +ini_set("max_input_time", "0"); +set_time_limit(0); + +mb_internal_encoding('UTF-8'); + +$id = ($argc > 1) ? $argv[1] : null; + +$handler = new FacebookQueueHandler($id); + +$handler->runOnce(); diff --git a/theme/base/css/display.css b/theme/base/css/display.css index 1ac63927d2..5ce5ac884d 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -22,7 +22,6 @@ line-height:1.65; position:relative; } h1,h2,h3,h4,h5,h6 { -text-transform:capitalize; margin-bottom:7px; overflow:hidden; } diff --git a/theme/base/css/mobile.css b/theme/base/css/mobile.css new file mode 100644 index 0000000000..3d0455a673 --- /dev/null +++ b/theme/base/css/mobile.css @@ -0,0 +1,72 @@ +/** theme: base + * + * @package Laconica + * @author Meitar Moscovitz + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +/* Go linear. */ +#header, +#header address, +#site_nav_global_primary, +#anon_notice, +#site_nav_local_views .nav, +#form_notice, +#form_notice .form_data li, +#core, +#content_inner, +#notices_primary, +.notice, +.notice .entry-title, +.notice div.entry-content, +.notice-options, +.notice .notice-options a, +.pagination, +.pagination .nav, +.aside .section { float: none; } + +.notice-options .notice_reply, +.notice-options .notice_delete, +.notice-options .form_favor, +.notice-options .form_disfavor { position: static; } + +#form_notice, +#anon_notice, +#content_inner, +#footer { width: auto; } + +/* And liquid. */ +#wrap { width: 95%; } + +/* Make things bigger on smaller screens. */ +body { font-size: 2em; } +.notices { font-size: 1.5em; } + +#site_nav_global_primary, #site_nav_global_secondary { text-align: center; } + +.notice div.entry-content { margin-left: 0; } +address { margin: 0; } + +#anon_notice, #footer { clear: left; font-size: .5em; } + +#form_notice textarea { width: 80%; height: 5em; } +#form_notice .form_note { right: 20%; top: 6em; } +#form_notice .form_actions input.submit { width: auto; } + +#content { padding: 18px 0; width: 100%; } +#content h1, #page_notice, #content_inner { padding: 0 18px; } +.notices .entry-title, .notices div.entry-content { width: 90%; } +.notice .author .photo { height: 4.5em; width: 4.5em; } /* about double physical size; TODO: do this scaling better */ +.notice-options { position: absolute; top: 0; right: 0; padding-left: 7%; width: 3%; } +.notice-options .notice_delete a { float: left; } /* Works, but feels like it shouldn't. */ +/* TODO: Make the icons of the notice options bigger. Probably with mobile-specific images. */ +.pagination .nav { overflow: auto; } + +#aside_primary { margin: 10px 0 0 0; border: none; padding: 0; width: 100%; } +#popular_notices { float: none; width: auto; } +/* Columns for supplemental info. */ +.aside .section { clear: none; padding: 9px; width: 45%; } +#top_groups_by_post { float: left; } +#featured_users { float: right; } +#export_data { display: none; } diff --git a/theme/base/css/modal.css b/theme/base/css/modal.css new file mode 100644 index 0000000000..985e4adfa5 --- /dev/null +++ b/theme/base/css/modal.css @@ -0,0 +1,22 @@ +/* + * SimpleModal Basic Modal Dialog + * http://www.ericmmartin.com/projects/simplemodal/ + * http://code.google.com/p/simplemodal/ + * + * Copyright (c) 2008 Eric Martin - http://ericmmartin.com + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * Revision: $Id: basic.css 162 2008-12-01 23:36:58Z emartin24 $ + * + */ + + +/* Overlay */ +#simplemodal-overlay {background-color:#000; cursor:wait;} + +/* Container */ +#simplemodal-container {height:240px; width:320px; background-color:#fff; border:3px solid #ccc;} +#simplemodal-container a.modalCloseImg {background:url(../images/x.png) no-repeat; width:25px; height:29px; display:inline; z-index:3200; position:absolute; top:-15px; right:-18px; cursor:pointer;} +#simplemodal-container #basicModalContent {padding:8px;} diff --git a/theme/base/css/modal_ie.css b/theme/base/css/modal_ie.css new file mode 100644 index 0000000000..eab4637c0f --- /dev/null +++ b/theme/base/css/modal_ie.css @@ -0,0 +1,16 @@ +/* + * SimpleModal Basic Modal Dialog + * http://www.ericmmartin.com/projects/simplemodal/ + * http://code.google.com/p/simplemodal/ + * + * Copyright (c) 2008 Eric Martin - http://ericmmartin.com + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * Revision: $Id: basic_ie.css 162 2008-12-01 23:36:58Z emartin24 $ + * + */ + +/* IE 6 hacks*/ +#simplemodal-container a.modalCloseImg {background:none; right:-14px; width:22px; height:26px; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../images/x.png',sizingMethod='scale');} diff --git a/theme/base/images/x.png b/theme/base/images/x.png new file mode 100644 index 0000000000..c11f7af69f Binary files /dev/null and b/theme/base/images/x.png differ