diff options
-rw-r--r-- | actions/apistatusesretweet.php | 136 | ||||
-rw-r--r-- | actions/apistatusesretweets.php | 116 | ||||
-rw-r--r-- | actions/apitimelineretweetedbyme.php | 126 | ||||
-rw-r--r-- | actions/apitimelineretweetedtome.php | 125 | ||||
-rw-r--r-- | actions/apitimelineretweetsofme.php | 126 | ||||
-rw-r--r-- | actions/repeat.php | 122 | ||||
-rw-r--r-- | actions/showstream.php | 46 | ||||
-rw-r--r-- | classes/Notice.php | 147 | ||||
-rw-r--r-- | classes/Profile.php | 11 | ||||
-rw-r--r-- | classes/User.php | 159 | ||||
-rw-r--r-- | classes/statusnet.ini | 2 | ||||
-rw-r--r-- | db/08to09.sql | 4 | ||||
-rw-r--r-- | db/08to09_pg.sql | 1 | ||||
-rw-r--r-- | db/statusnet.sql | 2 | ||||
-rw-r--r-- | db/statusnet_pg.sql | 4 | ||||
-rw-r--r-- | js/util.js | 5 | ||||
-rw-r--r-- | lib/api.php | 23 | ||||
-rw-r--r-- | lib/noticelist.php | 81 | ||||
-rw-r--r-- | lib/repeatform.php | 145 | ||||
-rw-r--r-- | lib/router.php | 23 | ||||
-rw-r--r-- | theme/base/css/display.css | 4 |
21 files changed, 1394 insertions, 14 deletions
diff --git a/actions/apistatusesretweet.php b/actions/apistatusesretweet.php new file mode 100644 index 000000000..fc71d2274 --- /dev/null +++ b/actions/apistatusesretweet.php @@ -0,0 +1,136 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Repeat a notice through the API + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category API + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; +require_once INSTALLDIR . '/lib/mediafile.php'; + +/** + * Repeat a notice through the API + * + * @category API + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class ApiStatusesRetweetAction extends ApiAuthAction +{ + var $original = null; + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + * + */ + + function prepare($args) + { + parent::prepare($args); + + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + $this->clientError(_('This method requires a POST.'), + 400, $this->format); + return false; + } + + $id = $this->trimmed('id'); + + $this->original = Notice::staticGet('id', $id); + + if (empty($this->original)) { + $this->clientError(_('No such notice'), + 400, $this->format); + return false; + } + + $this->user = $this->auth_user; + + if ($this->user->id == $notice->profile_id) { + $this->clientError(_('Cannot repeat your own notice')); + 400, $this->format); + return false; + } + + $profile = $this->user->getProfile(); + + if ($profile->hasRepeated($id)) { + $this->clientError(_('Already repeated that notice'), + 400, $this->format); + return false; + } + + return true; + } + + /** + * Handle the request + * + * Make a new notice for the update, save it, and show it + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + + function handle($args) + { + parent::handle($args); + + $repeat = $this->original->repeat($this->user->id, $this->source); + + common_broadcast_notice($repeat); + + $this->showNotice($repeat); + } + + /** + * Show the resulting notice + * + * @return void + */ + + function showNotice($notice) + { + if (!empty($notice)) { + if ($this->format == 'xml') { + $this->showSingleXmlStatus($notice); + } elseif ($this->format == 'json') { + $this->show_single_json_status($notice); + } + } + } +} diff --git a/actions/apistatusesretweets.php b/actions/apistatusesretweets.php new file mode 100644 index 000000000..c54a374e2 --- /dev/null +++ b/actions/apistatusesretweets.php @@ -0,0 +1,116 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Show up to 100 repeats of a notice + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category API + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; +require_once INSTALLDIR . '/lib/mediafile.php'; + +/** + * Show up to 100 repeats of a notice + * + * @category API + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class ApiStatusesRetweetsAction extends ApiAuthAction +{ + const MAXCOUNT = 100; + + var $original = null; + var $cnt = self::MAXCOUNT; + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + * + */ + + function prepare($args) + { + parent::prepare($args); + + $id = $this->trimmed('id'); + + $this->original = Notice::staticGet('id', $id); + + if (empty($this->original)) { + $this->clientError(_('No such notice'), + 400, $this->format); + return false; + } + + $cnt = $this->trimmed('count'); + + if (empty($cnt) || !is_integer($cnt)) { + $cnt = 100; + } else { + $this->cnt = min((int)$cnt, self::MAXCOUNT); + } + + return true; + } + + /** + * Handle the request + * + * Make a new notice for the update, save it, and show it + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + + function handle($args) + { + parent::handle($args); + + $strm = $this->original->repeatStream($this->cnt); + + switch ($this->format) { + case 'xml': + $this->showXmlTimeline($strm); + break; + case 'json': + $this->showJsonTimeline($strm); + break; + default: + $this->clientError(_('API method not found!'), $code = 404); + break; + } + } +} diff --git a/actions/apitimelineretweetedbyme.php b/actions/apitimelineretweetedbyme.php new file mode 100644 index 000000000..1e65678ad --- /dev/null +++ b/actions/apitimelineretweetedbyme.php @@ -0,0 +1,126 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Show authenticating user's most recent repeats + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category API + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; +require_once INSTALLDIR . '/lib/mediafile.php'; + +/** + * Show authenticating user's most recent repeats + * + * @category API + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class ApiTimelineRetweetedByMeAction extends ApiAuthAction +{ + const DEFAULTCOUNT = 20; + const MAXCOUNT = 200; + const MAXNOTICES = 3200; + + var $repeats = null; + var $cnt = self::DEFAULTCOUNT; + var $page = 1; + var $since_id = null; + var $max_id = null; + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + * + */ + + function prepare($args) + { + parent::prepare($args); + + $cnt = $this->int('count', self::DEFAULTCOUNT, self::MAXCOUNT, 1); + + $page = $this->int('page', 1, (self::MAXNOTICES/$this->cnt)); + + $since_id = $this->int('since_id'); + + $max_id = $this->int('max_id'); + + return true; + } + + /** + * Handle the request + * + * show a timeline of the user's repeated notices + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + + function handle($args) + { + parent::handle($args); + + $offset = ($this->page-1) * $this->cnt; + $limit = $this->cnt; + + $strm = $this->auth_user->repeatedByMe($offset, $limit, $this->since_id, $this->max_id); + + switch ($this->format) { + case 'xml': + $this->showXmlTimeline($strm); + break; + case 'json': + $this->showJsonTimeline($strm); + break; + case 'atom': + $profile = $this->auth_user->getProfile(); + + $title = sprintf(_("Repeated by %s"), $this->auth_user->nickname); + $taguribase = common_config('integration', 'taguri'); + $id = "tag:$taguribase:RepeatedByMe:" . $this->auth_user->id; + $link = common_local_url('showstream', + array('nickname' => $this->auth_user->nickname)); + + $this->showAtomTimeline($strm, $title, $id, $link); + break; + + default: + $this->clientError(_('API method not found!'), $code = 404); + break; + } + } +} diff --git a/actions/apitimelineretweetedtome.php b/actions/apitimelineretweetedtome.php new file mode 100644 index 000000000..681b0b9e9 --- /dev/null +++ b/actions/apitimelineretweetedtome.php @@ -0,0 +1,125 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Show most recent notices that are repeats in user's inbox + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category API + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; + +/** + * Show most recent notices that are repeats in user's inbox + * + * @category API + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class ApiTimelineRetweetedToMeAction extends ApiAuthAction +{ + const DEFAULTCOUNT = 20; + const MAXCOUNT = 200; + const MAXNOTICES = 3200; + + var $repeats = null; + var $cnt = self::DEFAULTCOUNT; + var $page = 1; + var $since_id = null; + var $max_id = null; + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + * + */ + + function prepare($args) + { + parent::prepare($args); + + $cnt = $this->int('count', self::DEFAULTCOUNT, self::MAXCOUNT, 1); + + $page = $this->int('page', 1, (self::MAXNOTICES/$this->cnt)); + + $since_id = $this->int('since_id'); + + $max_id = $this->int('max_id'); + + return true; + } + + /** + * Handle the request + * + * show a timeline of the user's repeated notices + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + + function handle($args) + { + parent::handle($args); + + $offset = ($this->page-1) * $this->cnt; + $limit = $this->cnt; + + $strm = $this->auth_user->repeatedToMe($offset, $limit, $this->since_id, $this->max_id); + + switch ($this->format) { + case 'xml': + $this->showXmlTimeline($strm); + break; + case 'json': + $this->showJsonTimeline($strm); + break; + case 'atom': + $profile = $this->auth_user->getProfile(); + + $title = sprintf(_("Repeated to %s"), $this->auth_user->nickname); + $taguribase = common_config('integration', 'taguri'); + $id = "tag:$taguribase:RepeatedToMe:" . $this->auth_user->id; + $link = common_local_url('all', + array('nickname' => $this->auth_user->nickname)); + + $this->showAtomTimeline($strm, $title, $id, $link); + break; + + default: + $this->clientError(_('API method not found!'), $code = 404); + break; + } + } +} diff --git a/actions/apitimelineretweetsofme.php b/actions/apitimelineretweetsofme.php new file mode 100644 index 000000000..479bff431 --- /dev/null +++ b/actions/apitimelineretweetsofme.php @@ -0,0 +1,126 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Show authenticating user's most recent notices that have been repeated + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category API + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; +require_once INSTALLDIR . '/lib/mediafile.php'; + +/** + * Show authenticating user's most recent notices that have been repeated + * + * @category API + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class ApiTimelineRetweetsOfMeAction extends ApiAuthAction +{ + const DEFAULTCOUNT = 20; + const MAXCOUNT = 200; + const MAXNOTICES = 3200; + + var $repeats = null; + var $cnt = self::DEFAULTCOUNT; + var $page = 1; + var $since_id = null; + var $max_id = null; + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + * + */ + + function prepare($args) + { + parent::prepare($args); + + $cnt = $this->int('count', self::DEFAULTCOUNT, self::MAXCOUNT, 1); + + $page = $this->int('page', 1, (self::MAXNOTICES/$this->cnt)); + + $since_id = $this->int('since_id'); + + $max_id = $this->int('max_id'); + + return true; + } + + /** + * Handle the request + * + * show a timeline of the user's repeated notices + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + + function handle($args) + { + parent::handle($args); + + $offset = ($this->page-1) * $this->cnt; + $limit = $this->cnt; + + $strm = $this->auth_user->repeatsOfMe($offset, $limit, $this->since_id, $this->max_id); + + switch ($this->format) { + case 'xml': + $this->showXmlTimeline($strm); + break; + case 'json': + $this->showJsonTimeline($strm); + break; + case 'atom': + $profile = $this->auth_user->getProfile(); + + $title = sprintf(_("Repeats of %s"), $this->auth_user->nickname); + $taguribase = common_config('integration', 'taguri'); + $id = "tag:$taguribase:RepeatsOfMe:" . $this->auth_user->id; + $link = common_local_url('showstream', + array('nickname' => $this->auth_user->nickname)); + + $this->showAtomTimeline($strm, $title, $id, $link); + break; + + default: + $this->clientError(_('API method not found!'), $code = 404); + break; + } + } +} diff --git a/actions/repeat.php b/actions/repeat.php new file mode 100644 index 000000000..a1c5f443f --- /dev/null +++ b/actions/repeat.php @@ -0,0 +1,122 @@ +<?php + +/** + * Repeat action. + * + * PHP version 5 + * + * @category Action + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2008, 2009, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Repeat action + * + * @category Action + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + */ + +class RepeatAction extends Action +{ + var $user = null; + var $notice = null; + + function prepare($args) + { + parent::prepare($args); + + $this->user = common_current_user(); + + if (empty($this->user)) { + $this->clientError(_("Only logged-in users can repeat notices.")); + return false; + } + + $id = $this->trimmed('notice'); + + if (empty($id)) { + $this->clientError(_("No notice specified.")); + return false; + } + + $this->notice = Notice::staticGet('id', $id); + + if (empty($this->notice)) { + $this->clientError(_("No notice specified.")); + return false; + } + + if ($this->user->id == $this->notice->profile_id) { + $this->clientError(_("You can't repeat your own notice.")); + return false; + } + + $token = $this->trimmed('token-'.$id); + + if (empty($token) || $token != common_session_token()) { + $this->clientError(_("There was a problem with your session token. Try again, please.")); + return false; + } + + $profile = $this->user->getProfile(); + + if ($profile->hasRepeated($id)) { + $this->clientError(_("You already repeated that notice.")); + return false; + } + + return true; + } + + /** + * Class handler. + * + * @param array $args query arguments + * + * @return void + */ + + function handle($args) + { + $repeat = $this->notice->repeat($this->user->id, 'web'); + + if ($this->boolean('ajax')) { + $this->startHTML('text/xml;charset=utf-8'); + $this->elementStart('head'); + $this->element('title', null, _('Repeated')); + $this->elementEnd('head'); + $this->elementStart('body'); + $this->element('p', array('id' => 'repeat_response'), _('Repeated!')); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + // FIXME! + } + } +} diff --git a/actions/showstream.php b/actions/showstream.php index 663638c18..74b46cc95 100644 --- a/actions/showstream.php +++ b/actions/showstream.php @@ -269,4 +269,50 @@ class ProfileNoticeListItem extends NoticeListItem { return; } + + /** + * show a link to the author of repeat + * + * @return void + */ + + function showRepeat() + { + if (!empty($this->repeat)) { + + // FIXME: this code is almost identical to default; need to refactor + + $attrs = array('href' => $this->profile->profileurl, + 'class' => 'url'); + + if (!empty($this->profile->fullname)) { + $attrs['title'] = $this->profile->fullname . ' (' . $this->profile->nickname . ')'; + } + + $this->out->elementStart('span', 'repeat'); + + $this->out->elementStart('a', $attrs); + + $avatar = $this->profile->getAvatar(AVATAR_MINI_SIZE); + + $this->out->element('img', array('src' => ($avatar) ? + $avatar->displayUrl() : + Avatar::defaultImage(AVATAR_MINI_SIZE), + 'class' => 'avatar photo', + 'width' => AVATAR_MINI_SIZE, + 'height' => AVATAR_MINI_SIZE, + 'alt' => + ($this->profile->fullname) ? + $this->profile->fullname : + $this->profile->nickname)); + + $this->out->elementEnd('a'); + + $text_link = XMLStringer::estring('a', $attrs, $this->profile->nickname); + + $this->out->raw(sprintf(_('Repeat of %s'), $text_link)); + + $this->out->elementEnd('span'); + } + } } diff --git a/classes/Notice.php b/classes/Notice.php index 4422866fa..7d2b898d2 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -55,13 +55,13 @@ class Notice extends Memcached_DataObject public $__table = 'notice'; // table name public $id; // int(4) primary_key not_null - public $profile_id; // int(4) not_null + public $profile_id; // int(4) multiple_key not_null public $uri; // varchar(255) unique_key - public $content; // text() - public $rendered; // text() + public $content; // text + public $rendered; // text public $url; // varchar(255) - public $created; // datetime() not_null - public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP + public $created; // datetime multiple_key not_null default_0000-00-00%2000%3A00%3A00 + public $modified; // timestamp not_null default_CURRENT_TIMESTAMP public $reply_to; // int(4) public $is_local; // tinyint(1) public $source; // varchar(32) @@ -70,9 +70,11 @@ class Notice extends Memcached_DataObject public $lon; // decimal(10,7) public $location_id; // int(4) public $location_ns; // int(4) + public $repeat_of; // int(4) /* Static get */ - function staticGet($k,$v=NULL) { + function staticGet($k,$v=NULL) + { return Memcached_DataObject::staticGet('Notice',$k,$v); } @@ -113,6 +115,12 @@ class Notice extends Memcached_DataObject //Null any notices that are replies to this notice $this->query(sprintf("UPDATE notice set reply_to = null WHERE reply_to = %d", $this->id)); + + //Null any notices that are repeats of this notice + //XXX: probably need to uncache these, too + + $this->query(sprintf("UPDATE notice set repeat_of = null WHERE repeat_of = %d", $this->id)); + $related = array('Reply', 'Fave', 'Notice_tag', @@ -234,7 +242,14 @@ class Notice extends Memcached_DataObject $notice->source = $source; $notice->uri = $uri; - $notice->reply_to = self::getReplyTo($reply_to, $profile_id, $source, $final); + // Handle repeat case + + if (isset($repeat_of)) { + $notice->repeat_of = $repeat_of; + $notice->reply_to = $repeat_of; + } else { + $notice->reply_to = self::getReplyTo($reply_to, $profile_id, $source, $final); + } if (!empty($notice->reply_to)) { $reply = Notice::staticGet('id', $notice->reply_to); @@ -432,10 +447,60 @@ class Notice extends Memcached_DataObject $this->blowTagCache($blowLast); $this->blowGroupCache($blowLast); $this->blowConversationCache($blowLast); + $this->blowRepeatCache(); $profile = Profile::staticGet($this->profile_id); $profile->blowNoticeCount(); } + function blowRepeatCache() + { + if (!empty($this->repeat_of)) { + $cache = common_memcache(); + if (!empty($cache)) { + // XXX: only blow if <100 in cache + $ck = common_cache_key('notice:repeats:'.$this->repeat_of); + $result = $cache->delete($ck); + + $user = User::staticGet('id', $this->profile_id); + + if (!empty($user)) { + $uk = common_cache_key('user:repeated_by_me:'.$user->id); + $cache->delete($uk); + $user->free(); + unset($user); + } + + $original = Notice::staticGet('id', $this->repeat_of); + + if (!empty($original)) { + $originalUser = User::staticGet('id', $original->profile_id); + if (!empty($originalUser)) { + $ouk = common_cache_key('user:repeats_of_me:'.$originalUser->id); + $cache->delete($ouk); + $originalUser->free(); + unset($originalUser); + } + $original->free(); + unset($original); + } + + $ni = new Notice_inbox(); + + $ni->notice_id = $this->id; + + if ($ni->find()) { + while ($ni->fetch()) { + $tmk = common_cache_key('user:repeated_to_me:'.$ni->user_id); + $cache->delete($tmk); + } + } + + $ni->free(); + unset($ni); + } + } + } + function blowConversationCache($blowLast=false) { $cache = common_memcache(); @@ -1432,4 +1497,72 @@ class Notice extends Memcached_DataObject return $location; } + + function repeat($repeater_id, $source) + { + $author = Profile::staticGet('id', $this->profile_id); + + // FIXME: truncate on long repeats...? + + $content = sprintf(_('RT @%1$s %2$s'), + $author->nickname, + $this->content); + + return self::saveNew($repeater_id, $content, $source, + array('repeat_of' => $this->id)); + } + + // These are supposed to be in chron order! + + function repeatStream($limit=100) + { + $cache = common_memcache(); + + if (empty($cache)) { + $ids = $this->_repeatStreamDirect($limit); + } else { + $idstr = $cache->get(common_cache_key('notice:repeats:'.$this->id)); + if (!empty($idstr)) { + $ids = explode(',', $idstr); + } else { + $ids = $this->_repeatStreamDirect(100); + $cache->set(common_cache_key('notice:repeats:'.$this->id), implode(',', $ids)); + } + if ($limit < 100) { + // We do a max of 100, so slice down to limit + $ids = array_slice($ids, 0, $limit); + } + } + + return Notice::getStreamByIds($ids); + } + + function _repeatStreamDirect($limit) + { + $notice = new Notice(); + + $notice->selectAdd(); // clears it + $notice->selectAdd('id'); + + $notice->repeat_of = $this->id; + + $notice->orderBy('created'); // NB: asc! + + if (!is_null($offset)) { + $notice->limit($offset, $limit); + } + + $ids = array(); + + if ($notice->find()) { + while ($notice->fetch()) { + $ids[] = $notice->id; + } + } + + $notice->free(); + $notice = NULL; + + return $ids; + } } diff --git a/classes/Profile.php b/classes/Profile.php index 4b2e09006..03196447b 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -716,4 +716,15 @@ class Profile extends Memcached_DataObject } return $result; } + + function hasRepeated($notice_id) + { + // XXX: not really a pkey, but should work + + $notice = Memcached_DataObject::pkeyGet('Notice', + array('profile_id' => $this->id, + 'repeat_of' => $notice_id)); + + return !empty($notice); + } } diff --git a/classes/User.php b/classes/User.php index 2a4fab7d4..9c071e06b 100644 --- a/classes/User.php +++ b/classes/User.php @@ -741,4 +741,163 @@ class User extends Memcached_DataObject $profile = $this->getProfile(); return $profile->isSilenced(); } + + function repeatedByMe($offset=0, $limit=20, $since_id=null, $max_id=null) + { + $ids = Notice::stream(array($this, '_repeatedByMeDirect'), + array(), + 'user:repeated_by_me:'.$this->id, + $offset, $limit, $since_id, $max_id, null); + + return Notice::getStreamByIds($ids); + } + + function _repeatedByMeDirect($offset, $limit, $since_id, $max_id, $since) + { + $notice = new Notice(); + + $notice->selectAdd(); // clears it + $notice->selectAdd('id'); + + $notice->profile_id = $this->id; + $notice->whereAdd('repeat_of IS NOT NULL'); + + $notice->orderBy('id DESC'); + + if (!is_null($offset)) { + $notice->limit($offset, $limit); + } + + if ($since_id != 0) { + $notice->whereAdd('id > ' . $since_id); + } + + if ($max_id != 0) { + $notice->whereAdd('id <= ' . $max_id); + } + + if (!is_null($since)) { + $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); + } + + $ids = array(); + + if ($notice->find()) { + while ($notice->fetch()) { + $ids[] = $notice->id; + } + } + + $notice->free(); + $notice = NULL; + + return $ids; + } + + function repeatsOfMe($offset=0, $limit=20, $since_id=null, $max_id=null) + { + $ids = Notice::stream(array($this, '_repeatsOfMeDirect'), + array(), + 'user:repeats_of_me:'.$this->id, + $offset, $limit, $since_id, $max_id, null); + + return Notice::getStreamByIds($ids); + } + + function _repeatsOfMeDirect($offset, $limit, $since_id, $max_id, $since) + { + $qry = + 'SELECT DISTINCT original.id AS id ' . + 'FROM notice original JOIN notice rept ON original.id = rept.repeat_of ' . + 'WHERE original.profile_id = ' . $this->id . ' '; + + if ($since_id != 0) { + $qry .= 'AND original.id > ' . $since_id . ' '; + } + + if ($max_id != 0) { + $qry .= 'AND original.id <= ' . $max_id . ' '; + } + + if (!is_null($since)) { + $qry .= 'AND original.modified > \'' . date('Y-m-d H:i:s', $since) . '\' '; + } + + // NOTE: we sort by fave time, not by notice time! + + $qry .= 'ORDER BY original.id DESC '; + + if (!is_null($offset)) { + $qry .= "LIMIT $limit OFFSET $offset"; + } + + $ids = array(); + + $notice = new Notice(); + + $notice->query($qry); + + while ($notice->fetch()) { + $ids[] = $notice->id; + } + + $notice->free(); + $notice = NULL; + + return $ids; + } + + function repeatedToMe($offset=0, $limit=20, $since_id=null, $max_id=null) + { + $ids = Notice::stream(array($this, '_repeatedToMeDirect'), + array(), + 'user:repeated_to_me:'.$this->id, + $offset, $limit, $since_id, $max_id, null); + + return Notice::getStreamByIds($ids); + } + + function _repeatedToMeDirect($offset, $limit, $since_id, $max_id, $since) + { + $qry = + 'SELECT notice.id AS id ' . + 'FROM notice JOIN notice_inbox ON notice.id = notice_inbox.notice_id ' . + 'WHERE notice_inbox.user_id = ' . $this->id . ' ' . + 'AND notice.repeat_of IS NOT NULL '; + + if ($since_id != 0) { + $qry .= 'AND notice.id > ' . $since_id . ' '; + } + + if ($max_id != 0) { + $qry .= 'AND notice.id <= ' . $max_id . ' '; + } + + if (!is_null($since)) { + $qry .= 'AND notice.modified > \'' . date('Y-m-d H:i:s', $since) . '\' '; + } + + // NOTE: we sort by fave time, not by notice time! + + $qry .= 'ORDER BY notice.id DESC '; + + if (!is_null($offset)) { + $qry .= "LIMIT $limit OFFSET $offset"; + } + + $ids = array(); + + $notice = new Notice(); + + $notice->query($qry); + + while ($notice->fetch()) { + $ids[] = $notice->id; + } + + $notice->free(); + $notice = NULL; + + return $ids; + } } diff --git a/classes/statusnet.ini b/classes/statusnet.ini index 835faeb0b..ff4ef2c14 100644 --- a/classes/statusnet.ini +++ b/classes/statusnet.ini @@ -1,3 +1,4 @@ + [avatar] profile_id = 129 original = 17 @@ -306,6 +307,7 @@ lat = 1 lon = 1 location_id = 1 location_ns = 1 +repeat_of = 1 [notice__keys] id = N diff --git a/db/08to09.sql b/db/08to09.sql index 64640f4ce..28ec3ec16 100644 --- a/db/08to09.sql +++ b/db/08to09.sql @@ -4,8 +4,10 @@ alter table notice add column lon decimal(10,7) comment 'longitude', add column location_id integer comment 'location id if possible', add column location_ns integer comment 'namespace for location', + add column repeat_of integer comment 'notice this is a repeat of' references notice (id), drop index notice_profile_id_idx, - add index notice_profile_id_idx (profile_id,created,id); + add index notice_profile_id_idx (profile_id,created,id), + add index notice_repeatof_idx (repeat_of); alter table message modify column content text comment 'message content'; diff --git a/db/08to09_pg.sql b/db/08to09_pg.sql index 1df8c249b..0398952f6 100644 --- a/db/08to09_pg.sql +++ b/db/08to09_pg.sql @@ -74,6 +74,7 @@ ALTER TABLE notice ADD COLUMN lat decimal(10, 7) /* comment 'latitude'*/; ALTER TABLE notice ADD COLUMN lon decimal(10,7) /* comment 'longitude'*/; ALTER TABLE notice ADD COLUMN location_id integer /* comment 'location id if possible'*/ ; ALTER TABLE notice ADD COLUMN location_ns integer /* comment 'namespace for location'*/; +ALTER TABLE notice ADD COLUMN repeat_of integer / * comment 'notice this is a repeat of' */ references notice (id); ALTER TABLE profile ADD COLUMN lat decimal(10,7) /*comment 'latitude'*/ ; ALTER TABLE profile ADD COLUMN lon decimal(10,7) /*comment 'longitude'*/; diff --git a/db/statusnet.sql b/db/statusnet.sql index 18abcdfdb..6b3c2ca06 100644 --- a/db/statusnet.sql +++ b/db/statusnet.sql @@ -129,11 +129,13 @@ create table notice ( lon decimal(10,7) comment 'longitude', location_id integer comment 'location id if possible', location_ns integer comment 'namespace for location', + repeat_of integer comment 'notice this is a repeat of' references notice (id), index notice_profile_id_idx (profile_id,created,id), index notice_conversation_idx (conversation), index notice_created_idx (created), index notice_replyto_idx (reply_to), + index notice_repeatof_idx (repeat_of), FULLTEXT(content) ) ENGINE=MyISAM CHARACTER SET utf8 COLLATE utf8_general_ci; diff --git a/db/statusnet_pg.sql b/db/statusnet_pg.sql index c37fa81de..020bfd967 100644 --- a/db/statusnet_pg.sql +++ b/db/statusnet_pg.sql @@ -135,7 +135,9 @@ create table notice ( lat decimal(10,7) /* comment 'latitude'*/ , lon decimal(10,7) /* comment 'longitude'*/ , location_id integer /* comment 'location id if possible'*/ , - location_ns integer /* comment 'namespace for location'*/ + location_ns integer /* comment 'namespace for location'*/ , + repeat_of integer /* comment 'notice this is a repeat of' */ references notice (id) , + /* FULLTEXT(content) */ ); diff --git a/js/util.js b/js/util.js index 336cd8cfe..f60b5d313 100644 --- a/js/util.js +++ b/js/util.js @@ -315,6 +315,10 @@ var SN = { // StatusNet $('.form_disfavor').each(function() { SN.U.FormXHR($(this)); }); }, + NoticeRepeat: function() { + $('.form_repeat').each(function() { SN.U.FormXHR($(this)); }); + }, + NoticeAttachments: function() { $('.notice a.attachment').each(function() { SN.U.NoticeWithAttachment($(this).closest('.notice')); @@ -448,6 +452,7 @@ var SN = { // StatusNet Notices: function() { if ($('body.user_in').length > 0) { SN.U.NoticeFavor(); + SN.U.NoticeRepeat(); SN.U.NoticeReply(); } diff --git a/lib/api.php b/lib/api.php index 7ebe65dbb..b7ab407a1 100644 --- a/lib/api.php +++ b/lib/api.php @@ -216,6 +216,20 @@ class ApiAction extends Action function twitterStatusArray($notice, $include_user=true) { + $base = $this->twitterSimpleStatusArray($notice, $include_user); + + if (empty($notice->repeat_of)) { + return $base; + } else { + $original = Notice::staticGet('id', $notice->repeat_of); + $original_array = $this->twitterSimpleStatusArray($original, $include_user); + $original_array['retweeted_status'] = $base; + return $original_array; + } + } + + function twitterSimpleStatusArray($notice, $include_user=true) + { $profile = $notice->getProfile(); $twitter_status = array(); @@ -448,9 +462,9 @@ class ApiAction extends Action } } - function showTwitterXmlStatus($twitter_status) + function showTwitterXmlStatus($twitter_status, $tag='status') { - $this->elementStart('status'); + $this->elementStart($tag); foreach($twitter_status as $element => $value) { switch ($element) { case 'user': @@ -465,11 +479,14 @@ class ApiAction extends Action case 'geo': $this->showGeoRSS($value); break; + case 'retweeted_status': + $this->showTwitterXmlStatus($value, 'retweeted_status'); + break; default: $this->element($element, null, $value); } } - $this->elementEnd('status'); + $this->elementEnd($tag); } function showTwitterXmlGroup($twitter_group) diff --git a/lib/noticelist.php b/lib/noticelist.php index 21cec528f..7319a62ea 100644 --- a/lib/noticelist.php +++ b/lib/noticelist.php @@ -147,6 +147,10 @@ class NoticeListItem extends Widget var $notice = null; + /** The notice that was repeated. */ + + var $repeat = null; + /** The profile of the author of the notice, extracted once for convenience. */ var $profile = null; @@ -162,8 +166,13 @@ class NoticeListItem extends Widget function __construct($notice, $out=null) { parent::__construct($out); - $this->notice = $notice; - $this->profile = $notice->getProfile(); + if (!empty($notice->repeat_of)) { + $this->notice = Notice::staticGet('id', $notice->repeat_of); + $this->repeat = $notice; + } else { + $this->notice = $notice; + } + $this->profile = $this->notice->getProfile(); } /** @@ -202,6 +211,7 @@ class NoticeListItem extends Widget $this->showNoticeSource(); $this->showNoticeLocation(); $this->showContext(); + $this->showRepeat(); $this->out->elementEnd('div'); } @@ -212,6 +222,7 @@ class NoticeListItem extends Widget $this->out->elementStart('div', 'notice-options'); $this->showFaveForm(); $this->showReplyLink(); + $this->showRepeatForm(); $this->showDeleteLink(); $this->out->elementEnd('div'); } @@ -508,6 +519,52 @@ class NoticeListItem extends Widget } /** + * show a link to the author of repeat + * + * @return void + */ + + function showRepeat() + { + if (!empty($this->repeat)) { + + $repeater = Profile::staticGet('id', $this->repeat->profile_id); + + $attrs = array('href' => $repeater->profileurl, + 'class' => 'url'); + + if (!empty($repeater->fullname)) { + $attrs['title'] = $repeater->fullname . ' (' . $repeater->nickname . ')'; + } + + $this->out->elementStart('span', 'repeat'); + + $this->out->elementStart('a', $attrs); + + $avatar = $repeater->getAvatar(AVATAR_MINI_SIZE); + + $this->out->element('img', array('src' => ($avatar) ? + $avatar->displayUrl() : + Avatar::defaultImage(AVATAR_MINI_SIZE), + 'class' => 'avatar photo', + 'width' => AVATAR_MINI_SIZE, + 'height' => AVATAR_MINI_SIZE, + 'alt' => + ($repeater->fullname) ? + $repeater->fullname : + $repeater->nickname)); + + $this->out->elementEnd('a'); + + $text_link = XMLStringer::estring('a', $attrs, $repeater->nickname); + + $this->out->raw(sprintf(_('Repeated by %s'), $text_link)); + + $this->out->elementEnd('span'); + } + } + + /** * show a link to reply to the current notice * * Should either do the reply in the current notice form (if available), or @@ -552,6 +609,26 @@ class NoticeListItem extends Widget } /** + * show the form to repeat a notice + * + * @return void + */ + + function showRepeatForm() + { + $user = common_current_user(); + if ($user && $user->id != $this->notice->profile_id) { + $profile = $user->getProfile(); + if ($profile->hasRepeated($this->notice->id)) { + $this->out->text(_('Repeated')); + } else { + $rf = new RepeatForm($this->out, $this->notice); + $rf->show(); + } + } + } + + /** * finish the notice * * Close the last elements in the notice list item diff --git a/lib/repeatform.php b/lib/repeatform.php new file mode 100644 index 000000000..50e5d6dbe --- /dev/null +++ b/lib/repeatform.php @@ -0,0 +1,145 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Form for repeating a notice + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Form + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Form for repeating a notice + * + * @category Form + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class RepeatForm extends Form +{ + /** + * Notice to repeat + */ + + var $notice = null; + + /** + * Constructor + * + * @param HTMLOutputter $out output channel + * @param Notice $notice notice to repeat + */ + + function __construct($out=null, $notice=null) + { + parent::__construct($out); + + $this->notice = $notice; + } + + /** + * ID of the form + * + * @return int ID of the form + */ + + function id() + { + return 'repeat-' . $this->notice->id; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url('repeat'); + } + + /** + * Include a session token for CSRF protection + * + * @return void + */ + + function sessionToken() + { + $this->out->hidden('token-' . $this->notice->id, + common_session_token()); + } + + /** + * Legend of the Form + * + * @return void + */ + function formLegend() + { + $this->out->element('legend', null, _('Repeat this notice')); + } + + /** + * Data elements + * + * @return void + */ + + function formData() + { + $this->out->hidden('notice-n'.$this->notice->id, + $this->notice->id, + 'notice'); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + $this->out->submit('repeat-submit-' . $this->notice->id, + _('Repeat'), 'submit', null, _('Repeat this notice')); + } + + /** + * Class of the form. + * + * @return string the form's class + */ + + function formClass() + { + return 'form_repeat'; + } +} diff --git a/lib/router.php b/lib/router.php index 37525319f..8f68f86ac 100644 --- a/lib/router.php +++ b/lib/router.php @@ -99,6 +99,7 @@ class Router 'groupblock', 'groupunblock', 'sandbox', 'unsandbox', 'silence', 'unsilence', + 'repeat', 'deleteuser'); foreach ($main as $a) { @@ -318,6 +319,18 @@ class Router 'id' => '[a-zA-Z0-9]+', 'format' => '(xml|json|rss|atom)')); + $m->connect('api/statuses/retweeted_by_me.:format', + array('action' => 'ApiTimelineRetweetedByMe', + 'format' => '(xml|json|atom)')); + + $m->connect('api/statuses/retweeted_to_me.:format', + array('action' => 'ApiTimelineRetweetedToMe', + 'format' => '(xml|json|atom)')); + + $m->connect('api/statuses/retweets_of_me.:format', + array('action' => 'ApiTimelineRetweetsOfMe', + 'format' => '(xml|json|atom)')); + $m->connect('api/statuses/friends.:format', array('action' => 'ApiUserFriends', 'format' => '(xml|json)')); @@ -358,6 +371,16 @@ class Router 'id' => '[0-9]+', 'format' => '(xml|json)')); + $m->connect('api/statuses/retweet/:id.:format', + array('action' => 'ApiStatusesRetweet', + 'id' => '[0-9]+', + 'format' => '(xml|json)')); + + $m->connect('api/statuses/retweets/:id.:format', + array('action' => 'ApiStatusesRetweets', + 'id' => '[0-9]+', + 'format' => '(xml|json)')); + // users $m->connect('api/users/show.:format', diff --git a/theme/base/css/display.css b/theme/base/css/display.css index e88e1f222..7446a42cd 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -994,11 +994,13 @@ float:left; } .notice-options .notice_delete, .notice-options .notice_reply, +.notice-options .form_repeat, .notice-options .form_favor, .notice-options .form_disfavor { float:left; margin-left:20%; } +.notice-options .form_repeat, .notice-options .form_favor, .notice-options .form_disfavor { margin-left:0; @@ -1024,10 +1026,12 @@ border-radius:0; -moz-border-radius:0; -webkit-border-radius:0; } +.notice-options .form_repeat legend, .notice-options .form_favor legend, .notice-options .form_disfavor legend { display:none; } +.notice-options .form_repeat fieldset, .notice-options .form_favor fieldset, .notice-options .form_disfavor fieldset { border:0; |