<?php /** * Perforce Swarm * * @copyright 2012 Perforce Software. All rights reserved. * @license Please see LICENSE.txt in top-level folder of this distribution. * @version <release>/<patch> */ namespace Comments; use Activity\Model\Activity; use Application\Filter\Linkify; use Attachments\Model\Attachment; use Comments\Model\Comment as CommentModel; use P4\File\File; use Reviews\Model\Review; use P4\Spec\Change; use P4\Spec\Job; use Projects\Model\Project; use Users\Model\User; use Zend\Mvc\MvcEvent; class Module { /** * Connect to queue event manager to handle new comments. * * @param MvcEvent $event the bootstrap event * @return void */ public function onBootstrap(MvcEvent $event) { $application = $event->getApplication(); $services = $application->getServiceManager(); $events = $services->get('queue')->getEventManager(); // when a comment is created, fetch it and prepare notifications. $events->attach( 'task.comment', function ($event) use ($services) { $p4Admin = $services->get('p4_admin'); $keywords = $services->get('review_keywords'); $id = $event->getParam('id'); $data = $event->getParam('data') + array( 'user' => null, 'previous' => array(), 'current' => array(), ); try { // fetch comment record $comment = CommentModel::fetch($id, $p4Admin); $context = $comment->getFileContext(); $event->setParam('comment', $comment); // there are several types of comment activity - compare new against old to see what happened $data['current'] = $data['current'] ?: $comment->get(); $commentAction = CommentModel::deriveAction($data['current'], $data['previous']); // exit early if there's nothing to do if ($commentAction === CommentModel::ACTION_NONE) { return; } // determine the action to report $action = 'commented on'; $taskState = $data['current']['taskState']; if ($commentAction === CommentModel::ACTION_ADD && $taskState !== CommentModel::TASK_COMMENT) { $action = 'opened an issue on'; } elseif ($commentAction === CommentModel::ACTION_STATE_CHANGE) { $oldState = isset($data['previous']['taskState']) ? $data['previous']['taskState'] : null; $transitions = array( CommentModel::TASK_COMMENT => 'cleared', CommentModel::TASK_OPEN => $oldState === CommentModel::TASK_COMMENT ? 'opened' : 'reopened', CommentModel::TASK_ADDRESSED => 'addressed', CommentModel::TASK_VERIFIED => 'verified' ); $action = isset($transitions[$taskState]) ? $transitions[$taskState] . ' an issue on' : 'changed task state on'; } elseif ($commentAction === CommentModel::ACTION_EDIT) { $action = 'edited a comment on'; } // prepare comment info for activity streams $activity = new Activity; $activity->set( array( 'type' => 'comment', 'user' => $data['user'] ?: $comment->get('user'), 'action' => $action, 'target' => $comment->get('topic'), 'description' => $comment->get('body'), 'topic' => $comment->get('topic'), 'depotFile' => $context['file'], 'time' => $comment->get('updated') ) ); $event->setParam('activity', $activity); // prepare attachment info for comment notification emails if ($comment->get('attachments')) { $event->setParam( 'attachments', Attachment::fetchAll( array(Attachment::FETCH_BY_IDS => $comment->get('attachments')), $p4Admin ) ); } // default mail message subject is simply the topic name. $subject = $comment->get('topic'); // enhance activity and mail info if we recognize the topic type $to = array(); $topic = $comment->get('topic'); // start by priming mentions with valid users in this new comment // later we'll also add in @mentions from other locations $mentions = Linkify::getCallouts($comment->get('body')); // handle change comments if (strpos($topic, 'changes/') === 0) { $change = $context['change'] ?: end(explode('/', $topic)); $target = 'change ' . $change; $hash = 'comments'; if ($context['file']) { $line = isset($context['line']) ? ", line " . $context['line'] : ''; $target .= " (" . File::decodeFilespec($context['name']) . $line . ")"; $hash = $context['md5'] . ',c' . $comment->getId(); } $activity->set('target', $target); $activity->set('link', array('change', array('change' => $change, 'fragment' => $hash))); try { $change = Change::fetch($change, $p4Admin); // set 'change' field on activity, we want to ensure its the change id // in theory it might be different from $change in case the change was renumbered // and we got it from the topic as topics keep reference to the original id $activity->set('change', $change->getId()); // change author, @mentions and project(s) should be notified $to[] = $change->getUser(); $mentions = array_merge($mentions, Linkify::getCallouts($change->getDescription())); $activity->addFollowers($change->getUser()); $activity->addProjects(Project::getAffectedByChange($change, $p4Admin)); // enhance mail subject to use the change description (will be cropped) $subject = 'Change @' . $change->getId() . ' - ' . $keywords->filter($change->getDescription()); } catch (\Exception $e) { $services->get('logger')->err($e); } } // handle review comments if (strpos($topic, 'reviews/') === 0) { $review = $context['review'] ?: end(explode('/', $topic)); $target = 'review ' . $review; $hash = 'comments'; if ($context['file']) { $line = isset($context['line']) ? ", line " . $context['line'] : ''; $target .= " (" . File::decodeFilespec($context['name']) . $line . ")"; $hash = $context['md5'] . ',c' . $comment->getId(); } $activity->set('target', $target); $activity->set('link', array('review', array('review' => $review, 'fragment' => $hash))); try { $review = Review::fetch($review, $p4Admin); // associate activity with review's head change so we can filter for restricted changes $activity->set('change', $review->getHeadChange()); // add any folks that were @*mentioned as required reviewers $review->addRequired( User::filter(Linkify::getCallouts($comment->get('body'), true), $p4Admin) ); // comment author and, valid, @mentioned users should be participants $review->addParticipant($comment->get('user')) ->addParticipant(User::filter($mentions, $p4Admin)) ->save(); // review comments should appear on the review stream $activity->addStream('review-' . $review->getId()); // all review participants should be notified $to = $review->getParticipants(); $activity->addFollowers($review->getParticipants()); $activity->addProjects($review->getProjects()); // enhance mail subject to use the review description (will be cropped) $subject = 'Review @' . $review->getId() . ' - ' . $keywords->filter($review->get('description')); } catch (\Exception $e) { $services->get('logger')->err($e); } } // handle job comments if (strpos($topic, 'jobs/') === 0) { $job = end(explode('/', $topic)); $target = $job; $hash = 'comments'; $activity->set('target', $target); $activity->set('link', array('job', array('job' => $job, 'fragment' => $hash))); try { $job = Job::fetch($job, $p4Admin); // add author, modifier and possibly others to the email recipients list // we find users by looping through all job's defined fields and looking // for default value of '$user' $fields = $job->getSpecDefinition()->getFields(); foreach ($fields as $key => $field) { if (isset($field['default']) && $field['default'] === '$user') { $to[] = $job->get($key); } } // notify users mentioned in job's description $to = array_merge($to, Linkify::getCallouts($job->getDescription())); // associated change(s) users should also be notified if (count($job->getChanges())) { foreach ($job->getChangeObjects() as $change) { $to[] = $change->getUser(); } } // enhance mail subject to use the job description (will be cropped) $subject = $job->getId() . ' - ' . $job->getDescription(); } catch (\Exception $e) { $services->get('logger')->err($e); } } // every user that participates in this comment thread // should be notified of this activity (excluding author). try { // if the topic isn't a review we want to included any previous commenters and mentioned users. // if the topic is a review, we skip this step and simply rely on the review participants, // otherwise we might erroneously add back in removed reviewers if (strpos($topic, 'reviews/') !== 0) { // examine every comment on this topic to include: // - all users who posted a comment to the topic // - all users who were mentioned in a comment on this topic $comments = CommentModel::fetchAll(array('topic' => $topic), $p4Admin); $users = array(); foreach ($comments as $entry) { $users[] = $entry->get('user'); $mentions = array_merge($mentions, Linkify::getCallouts($entry->get('body'))); } $to = array_merge($to, $users); } // knock back the to list to only unique, valid ids $to = array_unique(array_merge($to, $mentions)); $to = User::filter($to, $p4Admin); // if we're emailing you its activity stream worthy, add em $activity->addFollowers($to); // don't email the person who carried out the action $to = array_diff($to, array($data['user'] ?: $comment->get('user'))); // configure mail notification - only email for adds and edits if (in_array($commentAction, array(CommentModel::ACTION_ADD, CommentModel::ACTION_EDIT))) { $event->setParam( 'mail', array( 'subject' => $subject, 'cropSubject' => 80, 'toUsers' => $to, 'fromUser' => $data['user'] ?: $comment->get('user'), 'messageId' => '<comment-' . $comment->getId() . '-' . time() . '@swarm>', 'inReplyTo' => '<topic-' . $topic . '@swarm>', 'htmlTemplate' => __DIR__ . '/view/mail/comment-html.phtml', 'textTemplate' => __DIR__ . '/view/mail/comment-text.phtml', ) ); } } catch (\Exception $e) { $services->get('logger')->err($e); } } catch (\Exception $e) { $services->get('logger')->err($e); } }, 100 ); } public function getConfig() { return include __DIR__ . '/config/module.config.php'; } public function getAutoloaderConfig() { return array( 'Zend\Loader\StandardAutoloader' => array( 'namespaces' => array( __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__, ), ), ); } }