diff --git a/actions/apiconversation.php b/actions/apiconversation.php new file mode 100644 index 0000000000..fefb5b67a5 --- /dev/null +++ b/actions/apiconversation.php @@ -0,0 +1,242 @@ +. + * + * @category API + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; + +/** + * Show a stream of notices in a particular conversation + * + * @category API + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class ApiconversationAction extends ApiAuthAction +{ + protected $conversation = null; + protected $notices = null; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + + function prepare($argarray) + { + parent::prepare($argarray); + + $convId = $this->trimmed('id'); + + if (empty($convId)) { + throw new ClientException(_m('no conversation id')); + } + + $this->conversation = Conversation::staticGet('id', $convId); + + if (empty($this->conversation)) { + throw new ClientException(sprintf(_m('No conversation with id %d'), $convId), + 404); + } + + $profile = Profile::current(); + + $stream = new ConversationNoticeStream($convId, $profile); + + $notice = $stream->getNotices(($this->page-1) * $this->count, + $this->count, + $this->since_id, + $this->max_id); + + $this->notices = $notice->fetchAll(); + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + + function handle($argarray=null) + { + $sitename = common_config('site', 'name'); + // TRANS: Timeline title for user and friends. %s is a user nickname. + $title = _("Conversation"); + $id = common_local_url('apiconversation', array('id' => $this->conversation->id, 'format' => $this->format)); + $link = common_local_url('conversation', array('id' => $this->conversation->id)); + + $self = $id; + + switch($this->format) { + case 'xml': + $this->showXmlTimeline($this->notices); + break; + case 'rss': + $this->showRssTimeline( + $this->notices, + $title, + $link, + null, + null, + null, + $self + ); + break; + case 'atom': + + header('Content-Type: application/atom+xml; charset=utf-8'); + + $atom = new AtomNoticeFeed($this->auth_user); + + $atom->setId($id); + $atom->setTitle($title); + $atom->setUpdated('now'); + + $atom->addLink($link); + $atom->setSelfLink($self); + + $atom->addEntryFromNotices($this->notices); + $this->raw($atom->getString()); + + break; + case 'json': + $this->showJsonTimeline($this->notices); + break; + case 'as': + header('Content-Type: ' . ActivityStreamJSONDocument::CONTENT_TYPE); + $doc = new ActivityStreamJSONDocument($this->auth_user); + $doc->setTitle($title); + $doc->addLink($link, 'alternate', 'text/html'); + $doc->addItemsFromNotices($this->notices); + $this->raw($doc->asString()); + break; + default: + // TRANS: Client error displayed when coming across a non-supported API method. + $this->clientError(_('API method not found.'), $code = 404); + break; + } + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + + function isReadOnly($args) + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return true; + } else { + return false; + } + } + + /** + * Return last modified, if applicable. + * + * MAY override + * + * @return string last modified http header + */ + function lastModified() + { + if (!empty($this->notices) && (count($this->notices) > 0)) { + return strtotime($this->notices[0]->created); + } + + return null; + } + + /** + * Return etag, if applicable. + * + * MAY override + * + * @return string etag http header + */ + + function etag() + { + if (!empty($this->notices) && (count($this->notices) > 0)) { + + $last = count($this->notices) - 1; + + return '"' . implode( + ':', + array($this->arg('action'), + common_user_cache_hash($this->auth_user), + common_language(), + $this->user->id, + strtotime($this->notices[0]->created), + strtotime($this->notices[$last]->created)) + ) + . '"'; + } + + return null; + } + + /** + * Does this require authentication? + * + * @return boolean true if delete, else false + */ + + function requiresAuth() + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return false; + } else { + return true; + } + } +} \ No newline at end of file diff --git a/actions/conversation.php b/actions/conversation.php index f33d267d35..637e86e4b2 100644 --- a/actions/conversation.php +++ b/actions/conversation.php @@ -132,4 +132,33 @@ class ConversationAction extends Action { return true; } + + function getFeeds() + { + + return array(new Feed(Feed::JSON, + common_local_url('apiconversation', + array( + 'id' => $this->id, + 'format' => 'as')), + // TRANS: Title for link to notice feed. + // TRANS: %s is a user nickname. + _('Conversation feed (Activity Streams JSON)')), + new Feed(Feed::RSS2, + common_local_url('apiconversation', + array( + 'id' => $this->id, + 'format' => 'rss')), + // TRANS: Title for link to notice feed. + // TRANS: %s is a user nickname. + _('Conversation feed (RSS 2.0)')), + new Feed(Feed::ATOM, + common_local_url('apiconversation', + array( + 'id' => $this->id, + 'format' => 'atom')), + // TRANS: Title for link to notice feed. + // TRANS: %s is a user nickname. + _('Conversation feed (Activity Streams JSON)'))); + } } diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php index 0e60b7fed5..0eae9fb42a 100644 --- a/classes/Memcached_DataObject.php +++ b/classes/Memcached_DataObject.php @@ -63,7 +63,91 @@ class Memcached_DataObject extends Safe_DataObject } return $i; } + + /** + * Get multiple items from the database by key + * + * @param string $cls Class to fetch + * @param string $keyCol name of column for key + * @param array $keyVals key values to fetch + * @param boolean $skipNulls return only non-null results? + * + * @return array Array of objects, in order + */ + function multiGet($cls, $keyCol, $keyVals, $skipNulls=true) + { + $result = array_fill_keys($keyVals, null); + + $toFetch = array(); + + foreach ($keyVals as $keyVal) { + $i = self::getcached($cls, $keyCol, $keyVal); + if ($i !== false) { + $result[$keyVal] = $i; + } else if (!empty($keyVal)) { + $toFetch[] = $keyVal; + } + } + + if (count($toFetch) > 0) { + $i = DB_DataObject::factory($cls); + if (empty($i)) { + throw new Exception(_('Cannot instantiate class ' . $cls)); + } + $i->whereAddIn($keyCol, $toFetch, $i->columnType($keyCol)); + if ($i->find()) { + while ($i->fetch()) { + $copy = clone($i); + $copy->encache(); + $result[$i->$keyCol] = $copy; + } + } + + // Save state of DB misses + + foreach ($toFetch as $keyVal) { + if (empty($result[$keyVal])) { + // save the fact that no such row exists + $c = self::memcache(); + if (!empty($c)) { + $ck = self::cachekey($cls, $keyCol, $keyVal); + $c->set($ck, null); + } + } + } + } + + $values = array_values($result); + + if ($skipNulls) { + $tmp = array(); + foreach ($values as $value) { + if (!empty($value)) { + $tmp[] = $value; + } + } + $values = $tmp; + } + + return new ArrayWrapper($values); + } + function columnType($columnName) + { + $keys = $this->table(); + if (!array_key_exists($columnName, $keys)) { + throw new Exception('Unknown key column ' . $columnName . ' in ' . join(',', array_keys($keys))); + } + + $def = $keys[$columnName]; + + if ($def & DB_DATAOBJECT_INT) { + return 'integer'; + } else { + return 'string'; + } + } + /** * @fixme Should this return false on lookup fail to match staticGet? */ diff --git a/classes/Notice.php b/classes/Notice.php index 6eb4d9001e..650dca051b 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -84,6 +84,11 @@ class Notice extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE + function multiGet($kc, $kvs, $skipNulls=true) + { + return Memcached_DataObject::multiGet('Notice', $kc, $kvs, $skipNulls); + } + /* Notice types */ const LOCAL_PUBLIC = 1; const REMOTE = 0; @@ -1856,7 +1861,11 @@ class Notice extends Memcached_DataObject } else { $idstr = $cache->get(Cache::key('notice:repeats:'.$this->id)); if ($idstr !== false) { - $ids = explode(',', $idstr); + if (empty($idstr)) { + $ids = array(); + } else { + $ids = explode(',', $idstr); + } } else { $ids = $this->_repeatStreamDirect(100); $cache->set(Cache::key('notice:repeats:'.$this->id), implode(',', $ids)); @@ -1885,18 +1894,7 @@ class Notice extends Memcached_DataObject $notice->limit(0, $limit); } - $ids = array(); - - if ($notice->find()) { - while ($notice->fetch()) { - $ids[] = $notice->id; - } - } - - $notice->free(); - $notice = NULL; - - return $ids; + return $notice->fetchAll('id'); } function locationOptions($lat, $lon, $location_id, $location_ns, $profile = null) diff --git a/lib/arraywrapper.php b/lib/arraywrapper.php index f9d3c3cf94..30cb6cfdc7 100644 --- a/lib/arraywrapper.php +++ b/lib/arraywrapper.php @@ -48,6 +48,16 @@ class ArrayWrapper } } + function fetchAll($k= false, $v = false, $method = false) + { + if ($k !== false || $v !== false || $method !== false) + { + $item =& $this->_items[$this->_i]; + return $item->fetchAll($k, $v, $method); + } + return $this->_items; + } + function __set($name, $value) { $item =& $this->_items[$this->_i]; diff --git a/lib/conversationnoticestream.php b/lib/conversationnoticestream.php index 66075db84f..adf610ffe7 100644 --- a/lib/conversationnoticestream.php +++ b/lib/conversationnoticestream.php @@ -95,14 +95,6 @@ class RawConversationNoticeStream extends NoticeStream Notice::addWhereSinceId($notice, $since_id); Notice::addWhereMaxId($notice, $max_id); - $ids = array(); - - if ($notice->find()) { - while ($notice->fetch()) { - $ids[] = $notice->id; - } - } - - return $ids; + return $notice->fetchAll('id'); } } \ No newline at end of file diff --git a/lib/noticestream.php b/lib/noticestream.php index be28aa6186..e9ff47b68c 100644 --- a/lib/noticestream.php +++ b/lib/noticestream.php @@ -59,42 +59,6 @@ abstract class NoticeStream static function getStreamByIds($ids) { - $cache = Cache::instance(); - - if (!empty($cache)) { - $notices = array(); - foreach ($ids as $id) { - $n = Notice::staticGet('id', $id); - if (!empty($n)) { - $notices[] = $n; - } - } - return new ArrayWrapper($notices); - } else { - $notice = new Notice(); - if (empty($ids)) { - //if no IDs requested, just return the notice object - return $notice; - } - $notice->whereAdd('id in (' . implode(', ', $ids) . ')'); - - $notice->find(); - - $temp = array(); - - while ($notice->fetch()) { - $temp[$notice->id] = clone($notice); - } - - $wrapped = array(); - - foreach ($ids as $id) { - if (array_key_exists($id, $temp)) { - $wrapped[] = $temp[$id]; - } - } - - return new ArrayWrapper($wrapped); - } + return Notice::multiGet('id', $ids); } } diff --git a/lib/router.php b/lib/router.php index 536797dce1..180d8f791b 100644 --- a/lib/router.php +++ b/lib/router.php @@ -763,6 +763,11 @@ class Router array('action' => 'ApiGroupProfileUpdate', 'id' => '[a-zA-Z0-9]+', 'format' => '(xml|json)')); + + $m->connect('api/statusnet/conversation/:id.:format', + array('action' => 'apiconversation', + 'id' => '[0-9]+', + 'format' => '(xml|json|rss|atom|as)')); // Lists (people tags) diff --git a/plugins/Bookmark/BookmarkPlugin.php b/plugins/Bookmark/BookmarkPlugin.php index e383f80560..77b8a8483c 100644 --- a/plugins/Bookmark/BookmarkPlugin.php +++ b/plugins/Bookmark/BookmarkPlugin.php @@ -292,19 +292,27 @@ class BookmarkPlugin extends MicroAppPlugin function onStartOpenNoticeListItemElement($nli) { + if (!$this->isMyNotice($nli->notice)) { + return true; + } + $nb = Bookmark::getByNotice($nli->notice); - if (!empty($nb)) { - $id = (empty($nli->repeat)) ? $nli->notice->id : $nli->repeat->id; - $class = 'hentry notice bookmark'; - if ($nli->notice->scope != 0 && $nli->notice->scope != 1) { - $class .= ' limited-scope'; - } - $nli->out->elementStart('li', array('class' => $class, - 'id' => 'notice-' . $id)); - Event::handle('EndOpenNoticeListItemElement', array($nli)); - return false; + + if (empty($nb)) { + $this->log(LOG_INFO, "Notice {$nli->notice->id} has bookmark class but no matching Bookmark record."); + return true; } - return true; + + $id = (empty($nli->repeat)) ? $nli->notice->id : $nli->repeat->id; + $class = 'hentry notice bookmark'; + if ($nli->notice->scope != 0 && $nli->notice->scope != 1) { + $class .= ' limited-scope'; + } + $nli->out->elementStart('li', array('class' => $class, + 'id' => 'notice-' . $id)); + + Event::handle('EndOpenNoticeListItemElement', array($nli)); + return false; } /** @@ -355,12 +363,15 @@ class BookmarkPlugin extends MicroAppPlugin */ function deleteRelated($notice) { - $nb = Bookmark::getByNotice($notice); - - if (!empty($nb)) { - $nb->delete(); - } + if ($this->isMyNotice($notice)) { + + $nb = Bookmark::getByNotice($notice); + if (!empty($nb)) { + $nb->delete(); + } + } + return true; }