From a12680e8d52cc0da3a03bf396728d40f0ddcca64 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 22 Mar 2011 14:20:21 -0700 Subject: [PATCH 1/5] Fix typo in cf45c978 --- lib/threadednoticelist.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/threadednoticelist.php b/lib/threadednoticelist.php index 97087ba03a..1a20075cce 100644 --- a/lib/threadednoticelist.php +++ b/lib/threadednoticelist.php @@ -391,7 +391,7 @@ abstract class NoticeListActorsItem extends NoticeListItem $first = array_slice($items, 0, -1); $last = array_slice($items, -1, 1); // TRANS: Separator in list of user names like "You, Bob, Mary". - $sepataror = _(', '); + $separator = _(', '); // TRANS: For building a list such as "You, bob, mary and 5 others have favored this notice". // TRANS: %1$s is a list of users, separated by a separator (default: ", "), %2$s is the last user in the list. return sprintf(_m('FAVELIST', '%1$s and %2$s'), implode($separator, $first), implode($separator, $last)); From 14a6ab2b0433d89f23857bf1bbaec64e65ee718c Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 22 Mar 2011 16:26:26 -0700 Subject: [PATCH 2/5] Refactoring on notification mail generation: common profile & footer chunks pulled out, notifications added for group joins. --- classes/Group_join_queue.php | 11 ++ classes/Group_member.php | 9 ++ classes/Profile.php | 21 ++- lib/mail.php | 250 +++++++++++++++++++++++++---------- 4 files changed, 216 insertions(+), 75 deletions(-) diff --git a/classes/Group_join_queue.php b/classes/Group_join_queue.php index ee47b4932d..48b36cae2d 100644 --- a/classes/Group_join_queue.php +++ b/classes/Group_join_queue.php @@ -55,4 +55,15 @@ class Group_join_queue extends Managed_DataObject $rq->insert(); return $rq; } + + /** + * Send notifications via email etc to group administrators about + * this exciting new pending moderation queue item! + */ + public function notify() + { + $joiner = Profile::staticGet('id', $this->profile_id); + $group = User_group::staticGet('id', $this->group_id); + mail_notify_group_join_pending($group, $joiner); + } } diff --git a/classes/Group_member.php b/classes/Group_member.php index 30b79bb931..5385e0f487 100644 --- a/classes/Group_member.php +++ b/classes/Group_member.php @@ -162,4 +162,13 @@ class Group_member extends Memcached_DataObject return $act; } + + /** + * Send notifications via email etc to group administrators about + * this exciting new membership! + */ + public function notify() + { + mail_notify_group_join($this->getGroup(), $this->getMember()); + } } diff --git a/classes/Profile.php b/classes/Profile.php index 9566226951..d84d5da290 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -355,16 +355,20 @@ class Profile extends Memcached_DataObject */ function joinGroup(User_group $group) { - $ok = null; + $join = null; if ($group->join_policy == User_group::JOIN_POLICY_MODERATE) { - $ok = Group_join_queue::saveNew($this, $group); + $join = Group_join_queue::saveNew($this, $group); } else { if (Event::handle('StartJoinGroup', array($group, $this))) { - $ok = Group_member::join($group->id, $this->id); + $join = Group_member::join($group->id, $this->id); Event::handle('EndJoinGroup', array($group, $this)); } } - return $ok; + if ($join) { + // Send any applicable notifications... + $join->notify(); + } + return $join; } /** @@ -391,19 +395,22 @@ class Profile extends Memcached_DataObject */ function completeJoinGroup(User_group $group) { - $ok = null; + $join = null; $request = Group_join_queue::pkeyGet(array('profile_id' => $this->id, 'group_id' => $group->id)); if ($request) { if (Event::handle('StartJoinGroup', array($group, $this))) { - $ok = Group_member::join($group->id, $this->id); + $join = Group_member::join($group->id, $this->id); $request->delete(); Event::handle('EndJoinGroup', array($group, $this)); } } else { throw new Exception(_m('Invalid group join approval: not pending.')); } - return $ok; + if ($join) { + $join->notify(); + } + return $join; } /** diff --git a/lib/mail.php b/lib/mail.php index ab22de404c..d90fe6e8f2 100644 --- a/lib/mail.php +++ b/lib/mail.php @@ -245,44 +245,13 @@ function mail_subscribe_notify_profile($listenee, $other) $other->getBestName(), common_config('site', 'name')); - // TRANS: This is a paragraph in a new-subscriber e-mail. - // TRANS: %s is a URL where the subscriber can be reported as abusive. - $blocklink = sprintf(_("If you believe this account is being used abusively, " . - "you can block them from your subscribers list and " . - "report as spam to site administrators at %s"), - common_local_url('block', array('profileid' => $other->id))); - // TRANS: Main body of new-subscriber notification e-mail. - // TRANS: %1$s is the subscriber's long name, %2$s is the StatusNet sitename, - // TRANS: %3$s is the subscriber's profile URL, %4$s is the subscriber's location (or empty) - // TRANS: %5$s is the subscriber's homepage URL (or empty), %6%s is the subscriber's bio (or empty) - // TRANS: %7$s is a link to the addressed user's e-mail settings. - $body = sprintf(_('%1$s is now listening to your notices on %2$s.'."\n\n". - "\t".'%3$s'."\n\n". - '%4$s'. - '%5$s'. - '%6$s'. - "\n".'Faithfully yours,'."\n".'%2$s.'."\n\n". - "----\n". - "Change your email address or ". - "notification options at ".'%7$s' ."\n"), + // TRANS: %1$s is the subscriber's long name, %2$s is the StatusNet sitename. + $body = sprintf(_('%1$s is now listening to your notices on %2$s.'), $long_name, - common_config('site', 'name'), - $other->profileurl, - ($other->location) ? - // TRANS: Profile info line in new-subscriber notification e-mail. - // TRANS: %s is a location. - sprintf(_("Location: %s"), $other->location) . "\n" : '', - ($other->homepage) ? - // TRANS: Profile info line in new-subscriber notification e-mail. - // TRANS: %s is a homepage. - sprintf(_("Homepage: %s"), $other->homepage) . "\n" : '', - (($other->bio) ? - // TRANS: Profile info line in new-subscriber notification e-mail. - // TRANS: %s is biographical information. - sprintf(_("Bio: %s"), $other->bio) . "\n" : '') . - "\n\n" . $blocklink . "\n", - common_local_url('emailsettings')); + common_config('site', 'name')) . + mail_profile_block($other) . + mail_footer_block(); // reset localization common_switch_locale(); @@ -290,6 +259,69 @@ function mail_subscribe_notify_profile($listenee, $other) } } +function mail_footer_block() +{ + // TRANS: Common footer block for StatusNet notification emails. + // TRANS: %1$s is the StatusNet sitename, + // TRANS: %2$s is a link to the addressed user's e-mail settings. + return "\n\n" . sprintf(_('Faithfully yours,'. + "\n".'%1$s.'."\n\n". + "----\n". + "Change your email address or ". + "notification options at ".'%2$s'), + common_config('site', 'name'), + common_local_url('emailsettings')) . "\n"; +} + +/** + * Format a block of profile info for a plaintext notification email. + * + * @param Profile $profile + * @return string + */ +function mail_profile_block($profile) +{ + // TRANS: Layout for + // TRANS: %1$s is the subscriber's profile URL, %2$s is the subscriber's location (or empty) + // TRANS: %3$s is the subscriber's homepage URL (or empty), %4%s is the subscriber's bio (or empty) + $out = array(); + $out[] = ""; + $out[] = ""; + // TRANS: Profile info line in notification e-mail. + // TRANS: %s is a URL. + $out[] = sprintf(_("Profile: %s"), $profile->profileurl); + if ($profile->location) { + // TRANS: Profile info line in notification e-mail. + // TRANS: %s is a location. + $out[] = sprintf(_("Location: %s"), $profile->location); + } + if ($profile->homepage) { + // TRANS: Profile info line in notification e-mail. + // TRANS: %s is a homepage. + $out[] = sprintf(_("Homepage: %s"), $profile->homepage); + } + if ($profile->bio) { + // TRANS: Profile info line in notification e-mail. + // TRANS: %s is biographical information. + $out[] = sprintf(_("Bio: %s"), $profile->bio); + } + + $blocklink = common_local_url('block', array('profileid' => $profile->id)); + // This'll let ModPlus add the remote profile info so it's possible + // to block remote users directly... + Event::handle('MailProfileInfoBlockLink', array($profile, &$blocklink)); + + // TRANS: This is a paragraph in a new-subscriber e-mail. + // TRANS: %s is a URL where the subscriber can be reported as abusive. + $out[] = sprintf(_("If you believe this account is being used abusively, " . + "you can block them from your subscribers list and " . + "report as spam to site administrators at %s"), + $blocklink); + $out[] = ""; + + return implode("\n", $out); +} + /** * notify a user of their new incoming email address * @@ -317,11 +349,11 @@ function mail_new_incoming_notify($user) // TRANS: to to post by e-mail, %3$s is a URL to more instructions. $body = sprintf(_("You have a new posting address on %1\$s.\n\n". "Send email to %2\$s to post new messages.\n\n". - "More email instructions at %3\$s.\n\n". - "Faithfully yours,\n%1\$s"), + "More email instructions at %3\$s."), common_config('site', 'name'), $user->incomingemail, - common_local_url('doc', array('title' => 'email'))); + common_local_url('doc', array('title' => 'email'))) . + mail_footer_block(); mail_send($user->email, $headers, $body); } @@ -493,18 +525,16 @@ function mail_notify_nudge($from, $to) // TRANS: Body for 'nudge' notification email. // TRANS: %1$s is the nuding user's long name, $2$s is the nudging user's nickname, - // TRANS: %3$s is a URL to post notices at, %4$s is the StatusNet sitename. + // TRANS: %3$s is a URL to post notices at. $body = sprintf(_("%1\$s (%2\$s) is wondering what you are up to ". "these days and is inviting you to post some news.\n\n". "So let's hear from you :)\n\n". "%3\$s\n\n". - "Don't reply to this email; it won't get to them.\n\n". - "With kind regards,\n". - "%4\$s\n"), + "Don't reply to this email; it won't get to them."), $from_profile->getBestName(), $from->nickname, - common_local_url('all', array('nickname' => $to->nickname)), - common_config('site', 'name')); + common_local_url('all', array('nickname' => $to->nickname))) . + mail_footer_block(); common_switch_locale(); $headers = _mail_prepare_headers('nudge', $to->nickname, $from->nickname); @@ -548,21 +578,18 @@ function mail_notify_message($message, $from=null, $to=null) // TRANS: Body for direct-message notification email. // TRANS: %1$s is the sending user's long name, %2$s is the sending user's nickname, // TRANS: %3$s is the message content, %4$s a URL to the message, - // TRANS: %5$s is the StatusNet sitename. $body = sprintf(_("%1\$s (%2\$s) sent you a private message:\n\n". "------------------------------------------------------\n". "%3\$s\n". "------------------------------------------------------\n\n". "You can reply to their message here:\n\n". "%4\$s\n\n". - "Don't reply to this email; it won't get to them.\n\n". - "With kind regards,\n". - "%5\$s\n"), + "Don't reply to this email; it won't get to them."), $from_profile->getBestName(), $from->nickname, $message->content, - common_local_url('newmessage', array('to' => $from->id)), - common_config('site', 'name')); + common_local_url('newmessage', array('to' => $from->id))) . + mail_footer_block(); $headers = _mail_prepare_headers('message', $to->nickname, $from->nickname); @@ -615,9 +642,7 @@ function mail_notify_fave($other, $user, $notice) "The text of your notice is:\n\n" . "%4\$s\n\n" . "You can see the list of %1\$s's favorites here:\n\n" . - "%5\$s\n\n" . - "Faithfully yours,\n" . - "%6\$s\n"), + "%5\$s"), $bestname, common_exact_date($notice->created), common_local_url('shownotice', @@ -626,7 +651,8 @@ function mail_notify_fave($other, $user, $notice) common_local_url('showfavorites', array('nickname' => $user->nickname)), common_config('site', 'name'), - $user->nickname); + $user->nickname) . + mail_footer_block(); $headers = _mail_prepare_headers('fave', $other->nickname, $user->nickname); @@ -677,12 +703,11 @@ function mail_notify_attn($user, $notice) $subject = sprintf(_('%1$s (@%2$s) sent a notice to your attention'), $bestname, $sender->nickname); // TRANS: Body of @-reply notification e-mail. - // TRANS: %1$s is the sending user's long name, $2$s is the StatusNet sitename, + // TRANS: %1$s is the sending user's name, $2$s is the StatusNet sitename, // TRANS: %3$s is a URL to the notice, %4$s is the notice text, // TRANS: %5$s is a URL to the full conversion if it exists (otherwise empty), - // TRANS: %6$s is a URL to reply to the notice, %7$s is a URL to all @-replied for the addressed user, - // TRANS: %8$s is a URL to the addressed user's e-mail settings, %9$s is the sender's nickname. - $body = sprintf(_("%1\$s (@%9\$s) just sent a notice to your attention (an '@-reply') on %2\$s.\n\n". + // TRANS: %6$s is a URL to reply to the notice, %7$s is a URL to all @-replies for the addressed user, + $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". @@ -691,11 +716,8 @@ function mail_notify_attn($user, $notice) "You can reply back here:\n\n". "\t%6\$s\n\n" . "The list of all @-replies for you here:\n\n" . - "%7\$s\n\n" . - "Faithfully yours,\n" . - "%2\$s\n\n" . - "P.S. You can turn off these email notifications here: %8\$s\n"), - $bestname,//%1 + "%7\$s"), + $sender->getFancyName(),//%1 common_config('site', 'name'),//%2 common_local_url('shownotice', array('notice' => $notice->id)),//%3 @@ -704,10 +726,8 @@ function mail_notify_attn($user, $notice) common_local_url('newnotice', array('replyto' => $sender->nickname, 'inreplyto' => $notice->id)),//%6 common_local_url('replies', - array('nickname' => $user->nickname)),//%7 - common_local_url('emailsettings'), //%8 - $sender->nickname); //%9 - + array('nickname' => $user->nickname))) . //%7 + mail_footer_block(); $headers = _mail_prepare_headers('mention', $user->nickname, $sender->nickname); common_switch_locale(); @@ -734,3 +754,97 @@ function _mail_prepare_headers($msg_type, $to, $from) return $headers; } + +/** + * Send notification emails to group administrator. + * + * @param User_group $group + * @param Profile $joiner + */ +function mail_notify_group_join($group, $joiner) +{ + // This returns a Profile query... + $admin = $group->getAdmins(); + while ($admin->fetch()) { + // We need a local user for email notifications... + $adminUser = User::staticGet('id', $admin->id); + // @fixme check for email preference? + if ($adminUser && $adminUser->email) { + // use the recipient's localization + common_switch_locale($adminUser->language); + + $headers = _mail_prepare_headers('join', $admin->nickname, $joiner->nickname); + $headers['From'] = mail_notify_from(); + $headers['To'] = $admin->getBestName() . ' <' . $adminUser->email . '>'; + // TRANS: Subject of group join notification e-mail. + // TRANS: %1$s is the joining user's nickname, %2$s is the group name, and %3$s is the StatusNet sitename. + $headers['Subject'] = sprintf(_('%1$s has joined '. + 'your group %2$s on %3$s.'), + $joiner->getBestName(), + $group->getBestName(), + common_config('site', 'name')); + + // TRANS: Main body of group join notification e-mail. + // TRANS: %1$s is the subscriber's long name, %2$s is the group name, and %3$s is the StatusNet sitename, + // TRANS: %4$s is a block of profile info about the subscriber. + // TRANS: %5$s is a link to the addressed user's e-mail settings. + $body = sprintf(_('%1$s has joined your group %2$s on %3$s.'), + $joiner->getFancyName(), + $group->getFancyName(), + common_config('site', 'name')) . + mail_profile_block($joiner) . + mail_footer_block(); + + // reset localization + common_switch_locale(); + mail_send($adminUser->email, $headers, $body); + } + } +} + + +/** + * Send notification emails to group administrator. + * + * @param User_group $group + * @param Profile $joiner + */ +function mail_notify_group_join_pending($group, $joiner) +{ + $admin = $group->getAdmins(); + while ($admin->fetch()) { + // We need a local user for email notifications... + $adminUser = User::staticGet('id', $admin->id); + // @fixme check for email preference? + if ($adminUser && $adminUser->email) { + // use the recipient's localization + common_switch_locale($adminUser->language); + + $headers = _mail_prepare_headers('join', $admin->nickname, $joiner->nickname); + $headers['From'] = mail_notify_from(); + $headers['To'] = $admin->getBestName() . ' <' . $adminUser->email . '>'; + // TRANS: Subject of pending group join request notification e-mail. + // TRANS: %1$s is the joining user's nickname, %2$s is the group name, and %3$s is the StatusNet sitename. + $headers['Subject'] = sprintf(_('%1$s wants to join your group %2$s on %3$s.'), + $joiner->getBestName(), + $group->getBestName(), + common_config('site', 'name')); + + // TRANS: Main body of pending group join request notification e-mail. + // TRANS: %1$s is the subscriber's long name, %2$s is the group name, and %3$s is the StatusNet sitename, + // TRANS: %3$s is the URL to the moderation queue page. + $body = sprintf(_('%1$s would like to join your group %2$s on %3$s. ' . + 'You may approve or reject their group membership at %4$s'), + $joiner->getFancyName(), + $group->getFancyName(), + common_config('site', 'name'), + common_local_url('groupqueue', array('nickname' => $group->nickname))) . + mail_profile_block($joiner) . + mail_footer_block(); + + // reset localization + common_switch_locale(); + mail_send($adminUser->email, $headers, $body); + } + } +} From 0b35ce7c370bbb6cb9d55bb2a4256f58cb1158f1 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 23 Mar 2011 11:29:55 -0400 Subject: [PATCH 3/5] New NoticeStream class to reify streams of notices We've been muddling through with 6- or 8-argument functions for managing streams. I'd like to start thinking of streams as their own thing, and give them some more value. So, the new NoticeStream class takes over the Notice::stream() function and Notice::getStreamByIds(). There's probably some fine-tuning to do on the object interface. --- classes/Fave.php | 12 +-- classes/File.php | 9 +- classes/Notice.php | 114 ++----------------------- classes/Notice_tag.php | 15 ++-- classes/Profile.php | 20 ++--- classes/Reply.php | 10 +-- classes/User.php | 24 +++--- classes/User_group.php | 9 +- lib/noticestream.php | 189 +++++++++++++++++++++++++++++++++++++++++ 9 files changed, 243 insertions(+), 159 deletions(-) create mode 100644 lib/noticestream.php diff --git a/classes/Fave.php b/classes/Fave.php index efbceee6a8..a61f35d190 100644 --- a/classes/Fave.php +++ b/classes/Fave.php @@ -79,12 +79,12 @@ class Fave extends Memcached_DataObject function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $own=false, $since_id=0, $max_id=0) { - $ids = Notice::stream(array('Fave', '_streamDirect'), - array($user_id, $own), - ($own) ? 'fave:ids_by_user_own:'.$user_id : - 'fave:ids_by_user:'.$user_id, - $offset, $limit, $since_id, $max_id); - return $ids; + $stream = new NoticeStream(array('Fave', '_streamDirect'), + array($user_id, $own), + ($own) ? 'fave:ids_by_user_own:'.$user_id : + 'fave:ids_by_user:'.$user_id); + + return $stream->getNotices($offset, $limit, $since_id, $max_id); } /** diff --git a/classes/File.php b/classes/File.php index e9a0131c4e..681c33f9cd 100644 --- a/classes/File.php +++ b/classes/File.php @@ -449,12 +449,11 @@ class File extends Memcached_DataObject function stream($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0) { - $ids = Notice::stream(array($this, '_streamDirect'), - array(), - 'file:notice-ids:'.$this->url, - $offset, $limit, $since_id, $max_id); + $stream = new NoticeStream(array($this, '_streamDirect'), + array(), + 'file:notice-ids:'.$this->url); - return Notice::getStreamByIds($ids); + return $stream->getNotices($offset, $limit, $since_id, $max_id); } /** diff --git a/classes/Notice.php b/classes/Notice.php index b228a49c7c..8200e4554f 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -629,54 +629,14 @@ class Notice extends Memcached_DataObject return $att; } - 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); - } - } function publicStream($offset=0, $limit=20, $since_id=0, $max_id=0) { - $ids = Notice::stream(array('Notice', '_publicStreamDirect'), - array(), - 'public', - $offset, $limit, $since_id, $max_id); - return Notice::getStreamByIds($ids); + $stream = new NoticeStream(array('Notice', '_publicStreamDirect'), + array(), + 'public'); + + return $stream->getNotices($offset, $limit, $since_id, $max_id); } function _publicStreamDirect($offset=0, $limit=20, $since_id=0, $max_id=0) @@ -719,12 +679,11 @@ class Notice extends Memcached_DataObject function conversationStream($id, $offset=0, $limit=20, $since_id=0, $max_id=0) { - $ids = Notice::stream(array('Notice', '_conversationStreamDirect'), - array($id), - 'notice:conversation_ids:'.$id, - $offset, $limit, $since_id, $max_id); + $stream = new NoticeStream(array('Notice', '_conversationStreamDirect'), + array($id), + 'notice:conversation_ids:'.$id); - return Notice::getStreamByIds($ids); + return $stream->getNotices($offset, $limit, $since_id, $max_id); } function _conversationStreamDirect($id, $offset=0, $limit=20, $since_id=0, $max_id=0) @@ -1540,61 +1499,6 @@ class Notice extends Memcached_DataObject } } - function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $max_id=0) - { - $cache = Cache::instance(); - - if (empty($cache) || - $since_id != 0 || $max_id != 0 || - is_null($limit) || - ($offset + $limit) > NOTICE_CACHE_WINDOW) { - return call_user_func_array($fn, array_merge($args, array($offset, $limit, $since_id, - $max_id))); - } - - $idkey = Cache::key($cachekey); - - $idstr = $cache->get($idkey); - - if ($idstr !== false) { - // Cache hit! Woohoo! - $window = explode(',', $idstr); - $ids = array_slice($window, $offset, $limit); - return $ids; - } - - $laststr = $cache->get($idkey.';last'); - - if ($laststr !== false) { - $window = explode(',', $laststr); - $last_id = $window[0]; - $new_ids = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW, - $last_id, 0, null))); - - $new_window = array_merge($new_ids, $window); - - $new_windowstr = implode(',', $new_window); - - $result = $cache->set($idkey, $new_windowstr); - $result = $cache->set($idkey . ';last', $new_windowstr); - - $ids = array_slice($new_window, $offset, $limit); - - return $ids; - } - - $window = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW, - 0, 0, null))); - - $windowstr = implode(',', $window); - - $result = $cache->set($idkey, $windowstr); - $result = $cache->set($idkey . ';last', $windowstr); - - $ids = array_slice($window, $offset, $limit); - - return $ids; - } /** * Determine which notice, if any, a new notice is in reply to. diff --git a/classes/Notice_tag.php b/classes/Notice_tag.php index 81d346c5d3..813242253d 100644 --- a/classes/Notice_tag.php +++ b/classes/Notice_tag.php @@ -36,14 +36,13 @@ 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) { - - $ids = Notice::stream(array('Notice_tag', '_streamDirect'), - array($tag), - 'notice_tag:notice_ids:' . Cache::keyize($tag), - $offset, $limit); - - return Notice::getStreamByIds($ids); + static function getStream($tag, $offset=0, $limit=20, $sinceId=0, $maxId=0) + { + $stream = new NoticeStream(array('Notice_tag', '_streamDirect'), + array($tag), + 'notice_tag:notice_ids:' . Cache::keyize($tag)); + + return $stream->getNotices($offset, $limit, $sinceId, $maxId); } function _streamDirect($tag, $offset, $limit, $since_id, $max_id) diff --git a/classes/Profile.php b/classes/Profile.php index d84d5da290..209e5ef84a 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -198,22 +198,20 @@ class Profile extends Memcached_DataObject function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0) { - $ids = Notice::stream(array($this, '_streamTaggedDirect'), - array($tag), - 'profile:notice_ids_tagged:' . $this->id . ':' . $tag, - $offset, $limit, $since_id, $max_id); - return Notice::getStreamByIds($ids); + $stream = new NoticeStream(array($this, '_streamTaggedDirect'), + array($tag), + 'profile:notice_ids_tagged:'.$this->id.':'.$tag); + + return $stream->getNotices($offset, $limit, $since_id, $max_id); } function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0) { - // XXX: I'm not sure this is going to be any faster. It probably isn't. - $ids = Notice::stream(array($this, '_streamDirect'), - array(), - 'profile:notice_ids:' . $this->id, - $offset, $limit, $since_id, $max_id); + $stream = new NoticeStream(array($this, '_streamDirect'), + array(), + 'profile:notice_ids:' . $this->id); - return Notice::getStreamByIds($ids); + return $stream->getNotices($offset, $limit, $since_id, $max_id); } function _streamTaggedDirect($tag, $offset, $limit, $since_id, $max_id) diff --git a/classes/Reply.php b/classes/Reply.php index 371c16cf48..d5341b9a05 100644 --- a/classes/Reply.php +++ b/classes/Reply.php @@ -38,11 +38,11 @@ class Reply extends Memcached_DataObject function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0) { - $ids = Notice::stream(array('Reply', '_streamDirect'), - array($user_id), - 'reply:stream:' . $user_id, - $offset, $limit, $since_id, $max_id); - return $ids; + $stream = new NoticeStream(array('Reply', '_streamDirect'), + array($user_id), + 'reply:stream:' . $user_id); + + return $stream->getNotices($offset, $limit, $since_id, $max_id); } function _streamDirect($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0) diff --git a/classes/User.php b/classes/User.php index 31b132d0f3..4bd7b039df 100644 --- a/classes/User.php +++ b/classes/User.php @@ -448,8 +448,7 @@ class User extends Memcached_DataObject function getReplies($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) { - $ids = Reply::stream($this->id, $offset, $limit, $since_id, $before_id); - return Notice::getStreamByIds($ids); + return Reply::stream($this->id, $offset, $limit, $since_id, $before_id); } function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) { @@ -465,8 +464,7 @@ class User extends Memcached_DataObject function favoriteNotices($own=false, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0) { - $ids = Fave::stream($this->id, $offset, $limit, $own, $since_id, $max_id); - return Notice::getStreamByIds($ids); + return Fave::stream($this->id, $offset, $limit, $own, $since_id, $max_id); } function noticesWithFriends($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) @@ -769,12 +767,11 @@ class User extends Memcached_DataObject 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); + $stream = new NoticeStream(array($this, '_repeatedByMeDirect'), + array(), + 'user:repeated_by_me:'.$this->id); - return Notice::getStreamByIds($ids); + return $stream->getNotices($offset, $limit, $since_id, $max_id); } function _repeatedByMeDirect($offset, $limit, $since_id, $max_id) @@ -812,12 +809,11 @@ class User extends Memcached_DataObject 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); + $stream = new NoticeStream(array($this, '_repeatsOfMeDirect'), + array(), + 'user:repeats_of_me:'.$this->id); - return Notice::getStreamByIds($ids); + return $stream->getNotices($offset, $limit, $since_id, $max_id); } function _repeatsOfMeDirect($offset, $limit, $since_id, $max_id) diff --git a/classes/User_group.php b/classes/User_group.php index 707acbd13c..4d6dcfab68 100644 --- a/classes/User_group.php +++ b/classes/User_group.php @@ -87,12 +87,11 @@ class User_group extends Memcached_DataObject function getNotices($offset, $limit, $since_id=null, $max_id=null) { - $ids = Notice::stream(array($this, '_streamDirect'), - array(), - 'user_group:notice_ids:' . $this->id, - $offset, $limit, $since_id, $max_id); + $stream = new NoticeStream(array($this, '_streamDirect'), + array(), + 'user_group:notice_ids:' . $this->id); - return Notice::getStreamByIds($ids); + return $stream->getNotices($offset, $limit, $since_id, $max_id); } function _streamDirect($offset, $limit, $since_id, $max_id) diff --git a/lib/noticestream.php b/lib/noticestream.php new file mode 100644 index 0000000000..2b6e10f7b9 --- /dev/null +++ b/lib/noticestream.php @@ -0,0 +1,189 @@ +. + * + * @category Stream + * @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); +} + +/** + * Class for notice streams + * + * @category Stream + * @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 NoticeStream +{ + public $generator = null; + public $args = null; + public $cachekey = null; + + function __construct($generator, $args, $cachekey) + { + $this->generator = $generator; + $this->args = $args; + $this->cachekey = $cachekey; + } + + function getNotices($offset=0, $limit=20, $sinceId=0, $maxId=0) + { + $ids = $this->getNoticeIds($offset, $limit, $sinceId, $maxId); + + $notices = $this->getStreamByIds($ids); + + return $notices; + } + + function getNoticeIds($offset=0, $limit=20, $sinceId=0, $maxId=0) + { + $cache = Cache::instance(); + + // We cache NOTICE_CACHE_WINDOW elements at the tip of the stream. + // If the cache won't be hit, just generate directly. + + if (empty($cache) || + $sinceId != 0 || $maxId != 0 || + is_null($limit) || + ($offset + $limit) > NOTICE_CACHE_WINDOW) { + return $this->generate($offset, $limit, $sinceId, $maxId); + } + + // Check the cache to see if we have the stream. + + $idkey = Cache::key($this->cachekey); + + $idstr = $cache->get($idkey); + + if ($idstr !== false) { + // Cache hit! Woohoo! + $window = explode(',', $idstr); + $ids = array_slice($window, $offset, $limit); + return $ids; + } + + // Check the cache to see if we have a "last-known-good" version. + // The actual cache gets blown away when new notices are added, but + // the "last" value holds a lot of info. We might need to only generate + // a few at the "tip", which can bound our queries and save lots + // of time. + + $laststr = $cache->get($idkey.';last'); + + if ($laststr !== false) { + $window = explode(',', $laststr); + $last_id = $window[0]; + $new_ids = $this->generate(0, NOTICE_CACHE_WINDOW, $last_id, 0); + + $new_window = array_merge($new_ids, $window); + + $new_windowstr = implode(',', $new_window); + + $result = $cache->set($idkey, $new_windowstr); + $result = $cache->set($idkey . ';last', $new_windowstr); + + $ids = array_slice($new_window, $offset, $limit); + + return $ids; + } + + // No cache hits :( Generate directly and stick the results + // into the cache. Note we generate the full cache window. + + $window = $this->generate(0, NOTICE_CACHE_WINDOW, 0, 0); + + $windowstr = implode(',', $window); + + $result = $cache->set($idkey, $windowstr); + $result = $cache->set($idkey . ';last', $windowstr); + + // Return just the slice that was requested + + $ids = array_slice($window, $offset, $limit); + + return $ids; + } + + 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); + } + } + + function generate($offset, $limit, $sinceId, $maxId) + { + $args = array_merge($this->args, array($offset, + $limit, + $sinceId, + $maxId)); + + return call_user_func_array($this->generator, $args); + } +} From efb6a7b441dc14227a30b83454e8b47e79274997 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 23 Mar 2011 11:42:52 -0400 Subject: [PATCH 4/5] let Inbox class go fingerpokin' in streams --- classes/Inbox.php | 4 ++-- lib/noticestream.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/classes/Inbox.php b/classes/Inbox.php index f0f626a24e..feaead249b 100644 --- a/classes/Inbox.php +++ b/classes/Inbox.php @@ -233,7 +233,7 @@ class Inbox extends Memcached_DataObject // Do a bulk lookup for the first $limit items // Fast path when nothing's deleted. $firstChunk = array_slice($ids, 0, $offset + $limit); - $notices = Notice::getStreamByIds($firstChunk); + $notices = NoticeStream::getStreamByIds($firstChunk); assert($notices instanceof ArrayWrapper); $items = $notices->_items; @@ -292,7 +292,7 @@ class Inbox extends Memcached_DataObject // Do a bulk lookup for the first $limit items // Fast path when nothing's deleted. $firstChunk = array_slice($ids, 0, $limit); - $notices = Notice::getStreamByIds($firstChunk); + $notices = NoticeStream::getStreamByIds($firstChunk); $wanted = count($firstChunk); // raw entry count in the inbox up to our $limit if ($notices->N >= $wanted) { diff --git a/lib/noticestream.php b/lib/noticestream.php index 2b6e10f7b9..a96eb53da6 100644 --- a/lib/noticestream.php +++ b/lib/noticestream.php @@ -62,7 +62,7 @@ class NoticeStream { $ids = $this->getNoticeIds($offset, $limit, $sinceId, $maxId); - $notices = $this->getStreamByIds($ids); + $notices = self::getStreamByIds($ids); return $notices; } @@ -136,7 +136,7 @@ class NoticeStream return $ids; } - function getStreamByIds($ids) + static function getStreamByIds($ids) { $cache = Cache::instance(); From 2b901894c2c14f3e7e1c3eae8960f8a3c09310a0 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 23 Mar 2011 11:59:01 -0400 Subject: [PATCH 5/5] Some fixes to make the notice stream class work --- classes/Fave.php | 10 ++++++++++ classes/Notice.php | 6 +++--- classes/Profile.php | 4 ++-- lib/noticestream.php | 10 ++++++---- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/classes/Fave.php b/classes/Fave.php index a61f35d190..7cd64982cd 100644 --- a/classes/Fave.php +++ b/classes/Fave.php @@ -87,6 +87,16 @@ class Fave extends Memcached_DataObject return $stream->getNotices($offset, $limit, $since_id, $max_id); } + function idStream($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $own=false, $since_id=0, $max_id=0) + { + $stream = new NoticeStream(array('Fave', '_streamDirect'), + array($user_id, $own), + ($own) ? 'fave:ids_by_user_own:'.$user_id : + 'fave:ids_by_user:'.$user_id); + + return $stream->getNoticeIds($offset, $limit, $since_id, $max_id); + } + /** * Note that the sorting for this is by order of *fave* not order of *notice*. * diff --git a/classes/Notice.php b/classes/Notice.php index 8200e4554f..1201dd902b 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -45,7 +45,7 @@ require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; /* We keep 200 notices, the max number of notices available per API request, * in the memcached cache. */ -define('NOTICE_CACHE_WINDOW', 200); +define('NOTICE_CACHE_WINDOW', NoticeStream::CACHE_WINDOW); define('MAX_BOXCARS', 128); @@ -548,7 +548,7 @@ class Notice extends Memcached_DataObject if (empty($profile)) { return false; } - $notice = $profile->getNotices(0, NOTICE_CACHE_WINDOW); + $notice = $profile->getNotices(0, NoticeStream::CACHE_WINDOW); if (!empty($notice)) { $last = 0; while ($notice->fetch()) { @@ -1656,7 +1656,7 @@ class Notice extends Memcached_DataObject } } - return Notice::getStreamByIds($ids); + return NoticeStream::getStreamByIds($ids); } function _repeatStreamDirect($limit) diff --git a/classes/Profile.php b/classes/Profile.php index 209e5ef84a..c5dd2dfda9 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -550,7 +550,7 @@ class Profile extends Memcached_DataObject // This is the stream of favorite notices, in rev chron // order. This forces it into cache. - $ids = Fave::stream($this->id, 0, NOTICE_CACHE_WINDOW); + $ids = Fave::idStream($this->id, 0, NoticeStream::CACHE_WINDOW); // If it's in the list, then it's a fave @@ -562,7 +562,7 @@ class Profile extends Memcached_DataObject // then the cache has all available faves, so this one // is not a fave. - if (count($ids) < NOTICE_CACHE_WINDOW) { + if (count($ids) < NoticeStream::CACHE_WINDOW) { return false; } diff --git a/lib/noticestream.php b/lib/noticestream.php index a96eb53da6..d1ed203a67 100644 --- a/lib/noticestream.php +++ b/lib/noticestream.php @@ -47,6 +47,8 @@ if (!defined('STATUSNET')) { class NoticeStream { + const CACHE_WINDOW = 200; + public $generator = null; public $args = null; public $cachekey = null; @@ -71,13 +73,13 @@ class NoticeStream { $cache = Cache::instance(); - // We cache NOTICE_CACHE_WINDOW elements at the tip of the stream. + // We cache self::CACHE_WINDOW elements at the tip of the stream. // If the cache won't be hit, just generate directly. if (empty($cache) || $sinceId != 0 || $maxId != 0 || is_null($limit) || - ($offset + $limit) > NOTICE_CACHE_WINDOW) { + ($offset + $limit) > self::CACHE_WINDOW) { return $this->generate($offset, $limit, $sinceId, $maxId); } @@ -105,7 +107,7 @@ class NoticeStream if ($laststr !== false) { $window = explode(',', $laststr); $last_id = $window[0]; - $new_ids = $this->generate(0, NOTICE_CACHE_WINDOW, $last_id, 0); + $new_ids = $this->generate(0, self::CACHE_WINDOW, $last_id, 0); $new_window = array_merge($new_ids, $window); @@ -122,7 +124,7 @@ class NoticeStream // No cache hits :( Generate directly and stick the results // into the cache. Note we generate the full cache window. - $window = $this->generate(0, NOTICE_CACHE_WINDOW, 0, 0); + $window = $this->generate(0, self::CACHE_WINDOW, 0, 0); $windowstr = implode(',', $window);