1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 14:52:41 +01:00

Hook Ponder to Subscriptions

Summary: Adding email aggro email aggro

Test Plan: Added some content; annoyed some coworkers.

Reviewers: epriestley, nh, vrana

Reviewed By: epriestley

CC: aran, Korvin

Maniphest Tasks: T1808

Differential Revision: https://secure.phabricator.com/D3643
This commit is contained in:
Pieter Hooimeijer 2012-10-08 14:47:21 -07:00
parent cd9d78c107
commit 3440839c99
14 changed files with 587 additions and 6 deletions

View file

@ -1200,15 +1200,19 @@ phutil_register_library_map(array(
'PonderAnswerQuery' => 'applications/ponder/query/PonderAnswerQuery.php',
'PonderAnswerSaveController' => 'applications/ponder/controller/PonderAnswerSaveController.php',
'PonderAnswerViewController' => 'applications/ponder/controller/PonderAnswerViewController.php',
'PonderAnsweredMail' => 'applications/ponder/mail/PonderAnsweredMail.php',
'PonderComment' => 'applications/ponder/storage/PonderComment.php',
'PonderCommentEditor' => 'applications/ponder/editor/PonderCommentEditor.php',
'PonderCommentListView' => 'applications/ponder/view/PonderCommentListView.php',
'PonderCommentMail' => 'applications/ponder/mail/PonderCommentMail.php',
'PonderCommentQuery' => 'applications/ponder/query/PonderCommentQuery.php',
'PonderCommentSaveController' => 'applications/ponder/controller/PonderCommentSaveController.php',
'PonderConstants' => 'applications/ponder/PonderConstants.php',
'PonderController' => 'applications/ponder/controller/PonderController.php',
'PonderDAO' => 'applications/ponder/storage/PonderDAO.php',
'PonderFeedController' => 'applications/ponder/controller/PonderFeedController.php',
'PonderMail' => 'applications/ponder/mail/PonderMail.php',
'PonderMentionMail' => 'applications/ponder/mail/PonderMentionMail.php',
'PonderPostBodyView' => 'applications/ponder/view/PonderPostBodyView.php',
'PonderQuestion' => 'applications/ponder/storage/PonderQuestion.php',
'PonderQuestionAskController' => 'applications/ponder/controller/PonderQuestionAskController.php',
@ -1218,6 +1222,7 @@ phutil_register_library_map(array(
'PonderQuestionQuery' => 'applications/ponder/query/PonderQuestionQuery.php',
'PonderQuestionSummaryView' => 'applications/ponder/view/PonderQuestionSummaryView.php',
'PonderQuestionViewController' => 'applications/ponder/controller/PonderQuestionViewController.php',
'PonderReplyHandler' => 'applications/ponder/PonderReplyHandler.php',
'PonderRuleQuestion' => 'infrastructure/markup/rule/PonderRuleQuestion.php',
'PonderUserProfileView' => 'applications/ponder/view/PonderUserProfileView.php',
'PonderVotableInterface' => 'applications/ponder/storage/PonderVotableInterface.php',
@ -2318,23 +2323,27 @@ phutil_register_library_map(array(
'PonderAnswerQuery' => 'PhabricatorOffsetPagedQuery',
'PonderAnswerSaveController' => 'PonderController',
'PonderAnswerViewController' => 'PonderController',
'PonderAnsweredMail' => 'PonderMail',
'PonderComment' =>
array(
0 => 'PonderDAO',
1 => 'PhabricatorMarkupInterface',
),
'PonderCommentListView' => 'AphrontView',
'PonderCommentMail' => 'PonderMail',
'PonderCommentQuery' => 'PhabricatorQuery',
'PonderCommentSaveController' => 'PonderController',
'PonderController' => 'PhabricatorController',
'PonderDAO' => 'PhabricatorLiskDAO',
'PonderFeedController' => 'PonderController',
'PonderMentionMail' => 'PonderMail',
'PonderPostBodyView' => 'AphrontView',
'PonderQuestion' =>
array(
0 => 'PonderDAO',
1 => 'PhabricatorMarkupInterface',
2 => 'PonderVotableInterface',
3 => 'PhabricatorSubscribableInterface',
),
'PonderQuestionAskController' => 'PonderController',
'PonderQuestionDetailView' => 'AphrontView',
@ -2342,6 +2351,7 @@ phutil_register_library_map(array(
'PonderQuestionQuery' => 'PhabricatorOffsetPagedQuery',
'PonderQuestionSummaryView' => 'AphrontView',
'PonderQuestionViewController' => 'PonderController',
'PonderReplyHandler' => 'PhabricatorMailReplyHandler',
'PonderRuleQuestion' => 'PhabricatorRemarkupRuleObjectName',
'PonderUserProfileView' => 'AphrontView',
'PonderVotableView' => 'AphrontView',

View file

@ -0,0 +1,48 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PonderReplyHandler extends PhabricatorMailReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PonderQuestion)) {
throw new Exception("Mail receiver is not a PonderQuestion!");
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'Q');
}
public function getPublicReplyHandlerEmailAddress() {
return $this->getDefaultPublicReplyHandlerEmailAddress('Q');
}
public function getReplyHandlerDomain() {
return PhabricatorEnv::getEnvConfig(
'metamta.maniphest.reply-handler-domain');
}
public function getReplyHandlerInstructions() {
return null;
}
protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
// ignore this entirely for now
}
}

View file

@ -60,6 +60,7 @@ final class PonderAnswerSaveController extends PonderController {
->setContentSource($content_source);
id(new PonderAnswerEditor())
->setUser($user)
->setQuestion($question)
->setAnswer($res)
->saveAnswer();

View file

@ -38,6 +38,7 @@ final class PonderCommentSaveController extends PonderController {
if (!$objects) {
return new Aphront404Response();
}
$content = $request->getStr('content');
if (!strlen(trim($content))) {
@ -59,6 +60,8 @@ final class PonderCommentSaveController extends PonderController {
id(new PonderCommentEditor())
->setQuestion($question)
->setComment($res)
->setTargetPHID($target)
->setUser($user)
->save();
return id(new AphrontRedirectResponse())

View file

@ -53,6 +53,7 @@ final class PonderQuestionAskController extends PonderController {
id(new PonderQuestionEditor())
->setQuestion($question)
->setUser($user)
->save();
return id(new AphrontRedirectResponse())

View file

@ -52,6 +52,11 @@ final class PonderQuestionViewController extends PonderController {
}
}
$subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID(
$question->getPHID());
$object_phids = array_merge($object_phids, $subscribers);
$handles = $this->loadViewerHandles($object_phids);
$this->loadHandles($object_phids);
@ -79,7 +84,7 @@ final class PonderQuestionViewController extends PonderController {
->setHeader($question->getTitle());
$actions = $this->buildActionListView($question);
$properties = $this->buildPropertyListView($question);
$properties = $this->buildPropertyListView($question, $subscribers);
$nav = $this->buildSideNavView($question);
$nav->appendChild(
@ -112,7 +117,10 @@ final class PonderQuestionViewController extends PonderController {
return $view;
}
private function buildPropertyListView(PonderQuestion $question) {
private function buildPropertyListView(
PonderQuestion $question,
array $subscribers) {
$viewer = $this->getRequest()->getUser();
$view = new PhabricatorPropertyListView();
@ -124,6 +132,17 @@ final class PonderQuestionViewController extends PonderController {
pht('Created'),
phabricator_datetime($question->getDateCreated(), $viewer));
if ($subscribers) {
foreach ($subscribers as $key => $subscriber) {
$subscribers[$key] = $this->getHandle($subscriber)->renderLink();
}
$subscribers = implode(', ', $subscribers);
}
$view->addProperty(
pht('Subscribers'),
nonempty($subscribers, '<em>'.pht('None').'</em>'));
return $view;
}
}

View file

@ -16,11 +16,11 @@
* limitations under the License.
*/
final class PonderAnswerEditor {
private $question;
private $answer;
private $viewer;
private $shouldEmail = true;
public function setQuestion($question) {
$this->question = $question;
@ -32,7 +32,15 @@ final class PonderAnswerEditor {
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->viewer = $user;
return $this;
}
public function saveAnswer() {
if (!$this->viewer) {
throw new Exception("Must set user before saving question");
}
if (!$this->question) {
throw new Exception("Must set question before saving answer");
}
@ -42,6 +50,7 @@ final class PonderAnswerEditor {
$question = $this->question;
$answer = $this->answer;
$viewer = $this->viewer;
$conn = $answer->establishConnection('w');
$trans = $conn->openTransaction();
$trans->beginReadLocking();
@ -63,5 +72,53 @@ final class PonderAnswerEditor {
$question->attachRelated();
PhabricatorSearchPonderIndexer::indexQuestion($question);
// subscribe author and @mentions
$subeditor = id(new PhabricatorSubscriptionsEditor())
->setObject($question)
->setUser($viewer);
$subeditor->subscribeExplicit(array($answer->getAuthorPHID()));
$content = $answer->getContent();
$at_mention_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(
array($content)
);
$subeditor->subscribeImplicit($at_mention_phids);
$subeditor->save();
if ($this->shouldEmail) {
// now load subscribers, including implicitly-added @mention victims
$subscribers = PhabricatorSubscribersQuery
::loadSubscribersForPHID($question->getPHID());
// @mention emails (but not for anyone who has explicitly unsubscribed)
if (array_intersect($at_mention_phids, $subscribers)) {
id(new PonderMentionMail(
$question,
$answer,
$viewer))
->setToPHIDs($at_mention_phids)
->send();
}
$other_subs =
array_diff(
$subscribers,
$at_mention_phids
);
// 'Answered' emails for subscribers who are not @mentiond (and excluding
// author depending on their MetaMTA settings).
if ($other_subs) {
id(new PonderAnsweredMail(
$question,
$answer,
$viewer))
->setToPHIDs($other_subs)
->send();
}
}
}
}

View file

@ -21,6 +21,9 @@ final class PonderCommentEditor {
private $question;
private $comment;
private $targetPHID;
private $viewer;
private $shouldEmail = true;
public function setComment(PonderComment $comment) {
$this->comment = $comment;
@ -32,6 +35,16 @@ final class PonderCommentEditor {
return $this;
}
public function setTargetPHID($target) {
$this->targetPHID = $target;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->viewer = $user;
return $this;
}
public function save() {
if (!$this->comment) {
throw new Exception("Must set comment before saving it");
@ -39,13 +52,82 @@ final class PonderCommentEditor {
if (!$this->question) {
throw new Exception("Must set question before saving comment");
}
if (!$this->targetPHID) {
throw new Exception("Must set target before saving comment");
}
if (!$this->viewer) {
throw new Exception("Must set viewer before saving comment");
}
$comment = $this->comment;
$question = $this->question;
$target = $this->targetPHID;
$viewer = $this->viewer;
$comment->save();
$question->attachRelated();
PhabricatorSearchPonderIndexer::indexQuestion($question);
// subscribe author and @mentions
$subeditor = id(new PhabricatorSubscriptionsEditor())
->setObject($question)
->setUser($viewer);
$subeditor->subscribeExplicit(array($comment->getAuthorPHID()));
$content = $comment->getContent();
$at_mention_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(
array($content)
);
$subeditor->subscribeImplicit($at_mention_phids);
$subeditor->save();
if ($this->shouldEmail) {
// now load subscribers, including implicitly-added @mention victims
$subscribers = PhabricatorSubscribersQuery
::loadSubscribersForPHID($question->getPHID());
// @mention emails (but not for anyone who has explicitly unsubscribed)
if (array_intersect($at_mention_phids, $subscribers)) {
id(new PonderMentionMail(
$question,
$comment,
$viewer))
->setToPHIDs($at_mention_phids)
->send();
}
if ($target === $question->getPHID()) {
$target = $question;
}
else {
$answers_by_phid = mgroup($question->getAnswers(), 'getPHID');
$target = head($answers_by_phid[$target]);
}
// only send emails to others in the same thread
$thread = mpull($target->getComments(), 'getAuthorPHID');
$thread[] = $target->getAuthorPHID();
$thread[] = $question->getAuthorPHID();
$other_subs =
array_diff(
array_intersect($thread, $subscribers),
$at_mention_phids
);
// 'Comment' emails for subscribers who are in the same comment thread,
// including the author of the parent question and/or answer, excluding
// @mentions (and excluding the author, depending on their MetaMTA
// settings).
if ($other_subs) {
id(new PonderCommentMail(
$question,
$comment,
$viewer))
->setToPHIDs($other_subs)
->send();
}
}
}
}

View file

@ -21,21 +21,60 @@ final class PonderQuestionEditor {
private $question;
private $viewer;
private $shouldEmail = true;
public function setQuestion(PonderQuestion $question) {
$this->question = $question;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->viewer = $user;
return $this;
}
public function setShouldEmail($se) {
$this->shouldEmail = $se;
return $this;
}
public function save() {
if (!$this->viewer) {
throw new Exception("Must set user before saving question");
}
if (!$this->question) {
throw new Exception("Must set question before saving it");
}
$viewer = $this->viewer;
$question = $this->question;
$question->save();
// search index
$question->attachRelated();
PhabricatorSearchPonderIndexer::indexQuestion($question);
// subscribe author and @mentions
$subeditor = id(new PhabricatorSubscriptionsEditor())
->setObject($question)
->setUser($viewer);
$subeditor->subscribeExplicit(array($question->getAuthorPHID()));
$content = $question->getContent();
$at_mention_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(
array($content)
);
$subeditor->subscribeImplicit($at_mention_phids);
$subeditor->save();
if ($this->shouldEmail && $at_mention_phids) {
id(new PonderMentionMail(
$question,
$question,
$viewer))
->setToPHIDs($at_mention_phids)
->send();
}
}
}

View file

@ -0,0 +1,53 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PonderAnsweredMail extends PonderMail {
public function __construct(
PonderQuestion $question,
PonderAnswer $target,
PhabricatorUser $actor) {
$this->setQuestion($question);
$this->setTarget($target);
$this->setActorHandle($actor);
}
protected function renderVaryPrefix() {
return "[Answered]";
}
protected function renderBody() {
$question = $this->getQuestion();
$target = $this->getTarget();
$actor = $this->getActorName();
$name = $question->getTitle();
$body = array();
$body[] = "{$actor} answered a question that you are subscribed to.";
$body[] = null;
$content = $target->getContent();
if (strlen($content)) {
$body[] = $this->formatText($content);
$body[] = null;
}
return implode("\n", $body);
}
}

View file

@ -0,0 +1,53 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PonderCommentMail extends PonderMail {
public function __construct(
PonderQuestion $question,
PonderComment $target,
PhabricatorUser $actor) {
$this->setQuestion($question);
$this->setTarget($target);
$this->setActorHandle($actor);
}
protected function renderVaryPrefix() {
return "[Commented]";
}
protected function renderBody() {
$question = $this->getQuestion();
$target = $this->getTarget();
$actor = $this->getActorName();
$name = $question->getTitle();
$body = array();
$body[] = "{$actor} commented on a question that you are subscribed to.";
$body[] = null;
$content = $target->getContent();
if (strlen($content)) {
$body[] = $this->formatText($content);
$body[] = null;
}
return implode("\n", $body);
}
}

View file

@ -0,0 +1,144 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
abstract class PonderMail {
protected $to = array();
protected $actorHandle;
protected $question;
protected $target;
protected $isFirstMailAboutQuestion;
// protected $replyHandler;
protected $parentMessageID;
protected function renderSubject() {
$question = $this->getQuestion();
$title = $question->getTitle();
$id = $question->getID();
return "Q{$id}: {$title}";
}
abstract protected function renderVaryPrefix();
abstract protected function renderBody();
public function setActorHandle($actor_handle) {
$this->actorHandle = $actor_handle;
return $this;
}
public function getActorHandle() {
return $this->actorHandle;
}
protected function getActorName() {
return $this->actorHandle->getRealName();
}
protected function getSubjectPrefix() {
return "[Ponder]";
}
public function setToPHIDs(array $to) {
$this->to = $to;
return $this;
}
protected function getToPHIDs() {
return $this->to;
}
public function setQuestion($question) {
$this->question = $question;
return $this;
}
public function getQuestion() {
return $this->question;
}
public function setTarget($target) {
$this->target = $target;
return $this;
}
public function getTarget() {
return $this->target;
}
protected function getThreadID() {
$phid = $this->getQuestion()->getPHID();
return "ponder-ques-{$phid}";
}
protected function getThreadTopic() {
$id = $this->getQuestion()->getID();
$title = $this->getQuestion()->getTitle();
return "Q{$id}: {$title}";
}
public function send() {
$email_to = array_filter(array_unique($this->to));
$question = $this->getQuestion();
$target = $this->getTarget();
$uri = PhabricatorEnv::getURI('/Q'. $question->getID());
$thread_id = $this->getThreadID();
$handles = id(new PhabricatorObjectHandleData($email_to))
->loadHandles();
$reply_handler = new PonderReplyHandler();
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection($this->renderBody());
$body->addTextSection(pht('QUESTION DETAIL'), $uri);
$template = id(new PhabricatorMetaMTAMail())
->setSubject($this->getThreadTopic())
->setSubjectPrefix($this->getSubjectPrefix())
->setVarySubjectPrefix($this->renderVaryPrefix())
->setFrom($target->getAuthorPHID())
->setParentMessageID($this->parentMessageID)
->addHeader('Thread-Topic', $this->getThreadTopic())
->setThreadID($this->getThreadID(), false)
->setRelatedPHID($question->getPHID())
->setIsBulk(true)
->setBody($body->render());
$mails = $reply_handler->multiplexMail(
$template,
array_select_keys($handles, $email_to),
array());
foreach ($mails as $mail) {
$mail->saveAndSend();
}
}
protected function formatText($text) {
$text = explode("\n", rtrim($text));
foreach ($text as &$line) {
$line = rtrim(' '.$line);
}
unset($line);
return implode("\n", $text);
}
}

View file

@ -0,0 +1,64 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PonderMentionMail extends PonderMail {
public function __construct(
PonderQuestion $question,
$target,
PhabricatorUser $actor) {
$this->setQuestion($question);
$this->setTarget($target);
$this->setActorHandle($actor);
}
protected function renderVaryPrefix() {
return "[Mentioned]";
}
protected function renderBody() {
$question = $this->getQuestion();
$target = $this->getTarget();
$actor = $this->getActorName();
$name = $question->getTitle();
$targetkind = "somewhere";
if ($target instanceof PonderQuestion) {
$targetkind = "in a question";
}
else if ($target instanceof PonderAnswer) {
$targetkind = "in an answer";
}
else if ($target instanceof PonderComment) {
$targetkind = "in a comment";
}
$body = array();
$body[] = "{$actor} mentioned you {$targetkind}.";
$body[] = null;
$content = $target->getContent();
if (strlen($content)) {
$body[] = $this->formatText($content);
$body[] = null;
}
return implode("\n", $body);
}
}

View file

@ -17,7 +17,10 @@
*/
final class PonderQuestion extends PonderDAO
implements PhabricatorMarkupInterface, PonderVotableInterface {
implements
PhabricatorMarkupInterface,
PonderVotableInterface,
PhabricatorSubscribableInterface {
const MARKUP_FIELD_CONTENT = 'markup:content';
@ -168,4 +171,8 @@ final class PonderQuestion extends PonderDAO
public function getVotablePHID() {
return $this->getPHID();
}
public function isAutomaticallySubscribed($phid) {
return false;
}
}