2021-02-09 21:51:09 +00:00
|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* DiscussionTools event dispatcher
|
|
|
|
*
|
|
|
|
* @file
|
|
|
|
* @ingroup Extensions
|
|
|
|
* @license MIT
|
|
|
|
*/
|
|
|
|
|
|
|
|
namespace MediaWiki\Extension\DiscussionTools\Notifications;
|
|
|
|
|
2021-11-08 18:00:25 +00:00
|
|
|
use DateInterval;
|
|
|
|
use DateTimeImmutable;
|
2021-02-09 21:51:09 +00:00
|
|
|
use Iterator;
|
2024-06-08 22:02:35 +00:00
|
|
|
use MediaWiki\Context\RequestContext;
|
2023-12-11 15:38:02 +00:00
|
|
|
use MediaWiki\Deferred\DeferredUpdates;
|
2023-01-31 15:50:34 +00:00
|
|
|
use MediaWiki\Extension\DiscussionTools\CommentUtils;
|
2022-03-18 03:28:06 +00:00
|
|
|
use MediaWiki\Extension\DiscussionTools\ContentThreadItemSet;
|
2021-04-08 12:12:07 +00:00
|
|
|
use MediaWiki\Extension\DiscussionTools\Hooks\HookUtils;
|
2021-02-09 21:51:09 +00:00
|
|
|
use MediaWiki\Extension\DiscussionTools\SubscriptionItem;
|
2021-08-16 16:13:12 +00:00
|
|
|
use MediaWiki\Extension\DiscussionTools\SubscriptionStore;
|
2022-03-18 03:28:06 +00:00
|
|
|
use MediaWiki\Extension\DiscussionTools\ThreadItem\CommentItem;
|
|
|
|
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentCommentItem;
|
2022-07-30 21:59:55 +00:00
|
|
|
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentHeadingItem;
|
2022-03-18 03:28:06 +00:00
|
|
|
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentThreadItem;
|
|
|
|
use MediaWiki\Extension\DiscussionTools\ThreadItem\HeadingItem;
|
2022-03-06 16:10:40 +00:00
|
|
|
use MediaWiki\Extension\EventLogging\EventLogging;
|
2024-06-08 22:02:35 +00:00
|
|
|
use MediaWiki\Extension\EventLogging\Libs\UserBucketProvider\UserBucketProvider;
|
2023-06-08 08:16:15 +00:00
|
|
|
use MediaWiki\Extension\Notifications\Model\Event;
|
2021-02-09 21:51:09 +00:00
|
|
|
use MediaWiki\MediaWikiServices;
|
2021-07-15 13:56:13 +00:00
|
|
|
use MediaWiki\Page\PageIdentity;
|
2024-10-19 21:39:18 +00:00
|
|
|
use MediaWiki\Registration\ExtensionRegistry;
|
2021-02-09 21:51:09 +00:00
|
|
|
use MediaWiki\Revision\RevisionRecord;
|
2023-08-19 18:16:15 +00:00
|
|
|
use MediaWiki\Title\Title;
|
2021-02-09 21:51:09 +00:00
|
|
|
use MediaWiki\User\UserIdentity;
|
2021-06-30 20:24:29 +00:00
|
|
|
use Wikimedia\Assert\Assert;
|
2021-08-02 13:45:39 +00:00
|
|
|
use Wikimedia\Parsoid\Utils\DOMCompat;
|
2021-02-09 21:51:09 +00:00
|
|
|
use Wikimedia\Parsoid\Utils\DOMUtils;
|
2024-10-19 21:39:18 +00:00
|
|
|
use Wikimedia\Rdbms\IDBAccessObject;
|
2021-02-09 21:51:09 +00:00
|
|
|
|
|
|
|
class EventDispatcher {
|
2022-03-18 03:28:06 +00:00
|
|
|
private static function getParsedRevision( RevisionRecord $revRecord ): ContentThreadItemSet {
|
2024-11-05 01:05:17 +00:00
|
|
|
return HookUtils::parseRevisionParsoidHtml( $revRecord, __METHOD__ )->getValueOrThrow();
|
2021-02-09 21:51:09 +00:00
|
|
|
}
|
|
|
|
|
2021-12-01 14:53:20 +00:00
|
|
|
public static function generateEventsForRevision( array &$events, RevisionRecord $newRevRecord ): void {
|
2021-02-09 21:51:09 +00:00
|
|
|
$services = MediaWikiServices::getInstance();
|
2021-04-08 12:16:54 +00:00
|
|
|
|
2021-04-08 12:12:07 +00:00
|
|
|
$title = Title::newFromLinkTarget(
|
|
|
|
$newRevRecord->getPageAsLinkTarget()
|
|
|
|
);
|
|
|
|
if ( !HookUtils::isAvailableForTitle( $title ) ) {
|
|
|
|
// Not a talk page
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-04-21 12:30:12 +00:00
|
|
|
$user = $newRevRecord->getUser();
|
|
|
|
if ( !$user ) {
|
|
|
|
// User can be null if the user is deleted, but this is unlikely
|
|
|
|
// to be the case if the user just made an edit
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-08-14 16:18:02 +00:00
|
|
|
$revisionStore = $services->getRevisionStore();
|
|
|
|
$oldRevRecord = $revisionStore->getPreviousRevision( $newRevRecord, IDBAccessObject::READ_LATEST );
|
|
|
|
|
2021-07-22 18:05:34 +00:00
|
|
|
if ( $oldRevRecord !== null ) {
|
2022-06-09 13:51:33 +00:00
|
|
|
$oldItemSet = static::getParsedRevision( $oldRevRecord );
|
2021-07-22 18:05:34 +00:00
|
|
|
} else {
|
|
|
|
// Page creation
|
|
|
|
$doc = DOMUtils::parseHTML( '' );
|
|
|
|
$container = DOMCompat::getBody( $doc );
|
2022-02-19 06:31:34 +00:00
|
|
|
$oldItemSet = $services->getService( 'DiscussionTools.CommentParser' )
|
2022-02-21 22:07:38 +00:00
|
|
|
->parse( $container, $title->getTitleValue() );
|
2021-07-22 18:05:34 +00:00
|
|
|
}
|
2022-06-09 13:51:33 +00:00
|
|
|
$newItemSet = static::getParsedRevision( $newRevRecord );
|
2021-02-09 21:51:09 +00:00
|
|
|
|
2022-06-09 13:51:33 +00:00
|
|
|
static::generateEventsFromItemSets( $events, $oldItemSet, $newItemSet, $newRevRecord, $title, $user );
|
2021-07-15 13:56:13 +00:00
|
|
|
}
|
|
|
|
|
2021-07-16 16:01:18 +00:00
|
|
|
/**
|
2021-08-14 15:56:41 +00:00
|
|
|
* For each level 2 heading, get a list of comments in the thread grouped by names, then IDs.
|
2021-07-16 16:01:18 +00:00
|
|
|
* (Compare by name first, as ID could be changed by a parent comment being moved/deleted.)
|
2021-08-14 15:56:41 +00:00
|
|
|
* Comments in level 3+ sub-threads are grouped together with the parent thread.
|
|
|
|
*
|
|
|
|
* For any other headings (including level 3+ before the first level 2 heading, level 1, and
|
|
|
|
* section zero placeholder headings), ignore comments in those threads.
|
2021-07-16 16:01:18 +00:00
|
|
|
*
|
2022-03-18 03:28:06 +00:00
|
|
|
* @param ContentThreadItem[] $items
|
|
|
|
* @return ContentCommentItem[][][]
|
2021-07-16 16:01:18 +00:00
|
|
|
*/
|
|
|
|
private static function groupCommentsByThreadAndName( array $items ): array {
|
|
|
|
$comments = [];
|
|
|
|
$threadName = null;
|
|
|
|
foreach ( $items as $item ) {
|
2021-08-14 15:56:41 +00:00
|
|
|
if ( $item instanceof HeadingItem && ( $item->getHeadingLevel() < 2 || $item->isPlaceholderHeading() ) ) {
|
|
|
|
$threadName = null;
|
|
|
|
} elseif ( $item instanceof HeadingItem && $item->getHeadingLevel() === 2 ) {
|
2021-07-16 16:01:18 +00:00
|
|
|
$threadName = $item->getName();
|
2021-08-12 20:37:51 +00:00
|
|
|
} elseif ( $item instanceof CommentItem && $threadName !== null ) {
|
2021-07-16 16:01:18 +00:00
|
|
|
$comments[ $threadName ][ $item->getName() ][ $item->getId() ] = $item;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $comments;
|
|
|
|
}
|
|
|
|
|
2022-07-30 21:59:55 +00:00
|
|
|
/**
|
|
|
|
* Get a list of all subscribable headings, grouped by name in case there are duplicates.
|
|
|
|
*
|
|
|
|
* @param ContentHeadingItem[] $items
|
|
|
|
* @return ContentHeadingItem[][]
|
|
|
|
*/
|
|
|
|
private static function groupSubscribableHeadings( array $items ): array {
|
|
|
|
$headings = [];
|
|
|
|
foreach ( $items as $item ) {
|
|
|
|
if ( $item->isSubscribable() ) {
|
|
|
|
$headings[ $item->getName() ][ $item->getId() ] = $item;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $headings;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Compare two lists of thread items, return those in $new but not $old.
|
|
|
|
*
|
|
|
|
* @param ContentThreadItem[][] $old
|
|
|
|
* @param ContentThreadItem[][] $new
|
|
|
|
* @return iterable<ContentThreadItem>
|
|
|
|
*/
|
|
|
|
private static function findAddedItems( array $old, array $new ) {
|
|
|
|
foreach ( $new as $itemName => $nameNewItems ) {
|
|
|
|
// Usually, there will be 0 or 1 $nameNewItems, and 0 $nameOldItems,
|
|
|
|
// and $addedCount will be 0 or 1.
|
|
|
|
//
|
|
|
|
// But when multiple replies are added in one edit, or in multiple edits within the same
|
|
|
|
// minute, there may be more, and the complex logic below tries to make the best guess
|
|
|
|
// as to which items are actually new. See the 'multiple' and 'sametime' test cases.
|
|
|
|
//
|
|
|
|
$nameOldItems = $old[ $itemName ] ?? [];
|
|
|
|
$addedCount = count( $nameNewItems ) - count( $nameOldItems );
|
|
|
|
|
|
|
|
if ( $addedCount > 0 ) {
|
|
|
|
// For any name that occurs more times in new than old, report that many new items,
|
|
|
|
// preferring IDs that did not occur in old, then preferring items lower on the page.
|
|
|
|
foreach ( array_reverse( $nameNewItems ) as $itemId => $newItem ) {
|
|
|
|
if ( $addedCount > 0 && !isset( $nameOldItems[ $itemId ] ) ) {
|
|
|
|
yield $newItem;
|
|
|
|
$addedCount--;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
foreach ( array_reverse( $nameNewItems ) as $itemId => $newItem ) {
|
|
|
|
if ( $addedCount > 0 ) {
|
|
|
|
yield $newItem;
|
|
|
|
$addedCount--;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Assert::postcondition( $addedCount === 0, 'Reported expected number of items' );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-15 13:56:13 +00:00
|
|
|
/**
|
|
|
|
* Helper for generateEventsForRevision(), separated out for easier testing.
|
|
|
|
*/
|
2022-02-19 06:31:34 +00:00
|
|
|
protected static function generateEventsFromItemSets(
|
2021-07-15 13:56:13 +00:00
|
|
|
array &$events,
|
2022-03-18 03:28:06 +00:00
|
|
|
ContentThreadItemSet $oldItemSet,
|
|
|
|
ContentThreadItemSet $newItemSet,
|
2021-07-15 13:56:13 +00:00
|
|
|
RevisionRecord $newRevRecord,
|
|
|
|
PageIdentity $title,
|
|
|
|
UserIdentity $user
|
2021-12-01 14:53:20 +00:00
|
|
|
): void {
|
2022-06-09 13:51:33 +00:00
|
|
|
$newComments = static::groupCommentsByThreadAndName( $newItemSet->getThreadItems() );
|
|
|
|
$oldComments = static::groupCommentsByThreadAndName( $oldItemSet->getThreadItems() );
|
2021-07-16 16:01:18 +00:00
|
|
|
$addedComments = [];
|
|
|
|
foreach ( $newComments as $threadName => $threadNewComments ) {
|
2022-07-30 21:59:55 +00:00
|
|
|
$threadOldComments = $oldComments[ $threadName ] ?? [];
|
|
|
|
foreach ( static::findAddedItems( $threadOldComments, $threadNewComments ) as $newComment ) {
|
|
|
|
Assert::precondition( $newComment instanceof ContentCommentItem, 'Must be ContentCommentItem' );
|
|
|
|
$addedComments[] = $newComment;
|
2021-02-09 21:51:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-30 21:59:55 +00:00
|
|
|
$newHeadings = static::groupSubscribableHeadings( $newItemSet->getThreads() );
|
|
|
|
$oldHeadings = static::groupSubscribableHeadings( $oldItemSet->getThreads() );
|
2023-01-31 15:50:34 +00:00
|
|
|
|
|
|
|
$addedHeadings = [];
|
|
|
|
foreach ( static::findAddedItems( $oldHeadings, $newHeadings ) as $newHeading ) {
|
|
|
|
Assert::precondition( $newHeading instanceof ContentHeadingItem, 'Must be ContentHeadingItem' );
|
|
|
|
$addedHeadings[] = $newHeading;
|
|
|
|
}
|
|
|
|
|
2022-07-30 21:59:55 +00:00
|
|
|
$removedHeadings = [];
|
|
|
|
// Pass swapped parameters to findAddedItems() to find *removed* items
|
|
|
|
foreach ( static::findAddedItems( $newHeadings, $oldHeadings ) as $oldHeading ) {
|
|
|
|
Assert::precondition( $oldHeading instanceof ContentHeadingItem, 'Must be ContentHeadingItem' );
|
|
|
|
$removedHeadings[] = $oldHeading;
|
|
|
|
}
|
|
|
|
|
2021-02-09 21:51:09 +00:00
|
|
|
$mentionedUsers = [];
|
2021-04-30 16:07:31 +00:00
|
|
|
foreach ( $events as &$event ) {
|
2021-02-09 21:51:09 +00:00
|
|
|
if ( $event['type'] === 'mention' || $event['type'] === 'mention-summary' ) {
|
2021-04-30 16:07:31 +00:00
|
|
|
// Save mentioned users in our events, so that we can exclude them from our notification,
|
|
|
|
// to avoid duplicate notifications for a single comment.
|
|
|
|
// Array is keyed by user id so we can do a simple array merge.
|
2021-02-09 21:51:09 +00:00
|
|
|
$mentionedUsers += $event['extra']['mentioned-users'];
|
|
|
|
}
|
2021-04-30 16:07:31 +00:00
|
|
|
|
|
|
|
if ( count( $addedComments ) === 1 ) {
|
|
|
|
// If this edit was a new user talk message according to Echo,
|
|
|
|
// and we also found exactly one new comment,
|
|
|
|
// add some extra information to the edit-user-talk event.
|
|
|
|
if ( $event['type'] === 'edit-user-talk' ) {
|
|
|
|
$event['extra'] += [
|
|
|
|
'comment-id' => $addedComments[0]->getId(),
|
|
|
|
'comment-name' => $addedComments[0]->getName(),
|
|
|
|
'content' => $addedComments[0]->getBodyText( true ),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Similarly for mentions.
|
|
|
|
// We don't handle 'content' in this case, as Echo makes its own snippets.
|
|
|
|
if ( $event['type'] === 'mention' ) {
|
|
|
|
$event['extra'] += [
|
|
|
|
'comment-id' => $addedComments[0]->getId(),
|
|
|
|
'comment-name' => $addedComments[0]->getName(),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
}
|
2021-02-09 21:51:09 +00:00
|
|
|
}
|
|
|
|
|
2021-07-22 18:05:34 +00:00
|
|
|
if ( $addedComments ) {
|
|
|
|
// It's a bit weird to do this here, in the middle of the hook handler for Echo. However:
|
|
|
|
// * Echo calls this from a PageSaveComplete hook handler as a DeferredUpdate,
|
|
|
|
// which is exactly how we would do this otherwise
|
|
|
|
// * It allows us to reuse the generated comment trees without any annoying caching
|
|
|
|
static::addCommentChangeTag( $newRevRecord );
|
2021-10-18 08:32:17 +00:00
|
|
|
// For very similar reasons, we do logging here
|
|
|
|
static::logAddedComments( $addedComments, $newRevRecord, $title, $user );
|
2021-07-22 18:05:34 +00:00
|
|
|
}
|
|
|
|
|
2021-07-16 16:01:18 +00:00
|
|
|
foreach ( $addedComments as $newComment ) {
|
|
|
|
// Ignore comments by other users, e.g. in case of reverts or a discussion being moved.
|
|
|
|
// TODO: But what about someone signing another's comment?
|
|
|
|
if ( $newComment->getAuthor() !== $user->getName() ) {
|
|
|
|
continue;
|
|
|
|
}
|
2021-11-08 18:00:25 +00:00
|
|
|
// Ignore comments which are more than 10 minutes old, as this may be a user archiving
|
|
|
|
// their own comment. (T290803)
|
|
|
|
$revTimestamp = new DateTimeImmutable( $newRevRecord->getTimestamp() );
|
|
|
|
$threshold = $revTimestamp->sub( new DateInterval( 'PT10M' ) );
|
|
|
|
if ( $newComment->getTimestamp() <= $threshold ) {
|
|
|
|
continue;
|
|
|
|
}
|
2021-09-01 22:05:00 +00:00
|
|
|
$heading = $newComment->getSubscribableHeading();
|
|
|
|
if ( !$heading ) {
|
2021-06-25 13:24:46 +00:00
|
|
|
continue;
|
|
|
|
}
|
2021-02-09 21:51:09 +00:00
|
|
|
$events[] = [
|
2023-01-31 15:50:34 +00:00
|
|
|
// This probably should've been called "dt-new-comment": this code is
|
|
|
|
// unaware if there are any subscriptions to the containing topic and
|
|
|
|
// an event is generated for every comment posted.
|
|
|
|
// However, changing this would require a complex migration.
|
2021-02-09 21:51:09 +00:00
|
|
|
'type' => 'dt-subscribed-new-comment',
|
2021-04-08 12:12:07 +00:00
|
|
|
'title' => $title,
|
2021-02-09 21:51:09 +00:00
|
|
|
'extra' => [
|
|
|
|
'subscribed-comment-name' => $heading->getName(),
|
|
|
|
'comment-id' => $newComment->getId(),
|
2021-04-21 12:36:29 +00:00
|
|
|
'comment-name' => $newComment->getName(),
|
2021-02-09 21:51:09 +00:00
|
|
|
'content' => $newComment->getBodyText( true ),
|
Handle highlighting and scrolling to comments for bundled notifications
Notifications are bundled by section, so instead of linking to the
comment, link to the section.
Additionally, add a parameter to the URL listing all the comment IDs
from the bundle, and highlight them all and scroll to the topmost one.
Having to handle both URL fragments and URL query parameters makes
this code kind of a mess :(
Also, some unexpected changes became necessary:
* EventDispatcher.php: Store the section title in events using
HeadingItem::getLinkableTitle() instead of ThreadItem::getText().
The result is mostly the same, except in case of wacky markup like
images or extension tags. We can more reliably use it to link to the
section on the page, and we already use getLinkableTitle() when
generating edit summaries in the reply tool for this reason.
* dt.init.less: Change the mix-blend-mode for the highlights from
'multiply' to 'darken', so that multiple overlapping highlights do
not look more opaque. This affects how the highlights look on
non-white backgrounds and images (they're less blue, and on darker
backgrounds entirely invisible), but it seems worth it.
Bug: T286620
Change-Id: I21bb5c003abc2747f0350d3f3af558dfb55693e9
2021-08-05 20:04:17 +00:00
|
|
|
'section-title' => $heading->getLinkableTitle(),
|
2021-02-09 21:51:09 +00:00
|
|
|
'revid' => $newRevRecord->getId(),
|
|
|
|
'mentioned-users' => $mentionedUsers,
|
|
|
|
],
|
2021-04-21 12:30:12 +00:00
|
|
|
'agent' => $user,
|
2021-02-09 21:51:09 +00:00
|
|
|
];
|
2021-08-17 20:23:27 +00:00
|
|
|
|
|
|
|
$titleForSubscriptions = Title::castFromPageIdentity( $title )->createFragmentTarget( $heading->getText() );
|
|
|
|
static::addAutoSubscription( $user, $titleForSubscriptions, $heading->getName() );
|
2021-02-09 21:51:09 +00:00
|
|
|
}
|
2022-07-30 21:59:55 +00:00
|
|
|
|
|
|
|
foreach ( $removedHeadings as $oldHeading ) {
|
|
|
|
$events[] = [
|
|
|
|
'type' => 'dt-removed-topic',
|
|
|
|
'title' => $title,
|
|
|
|
'extra' => [
|
|
|
|
'subscribed-comment-name' => $oldHeading->getName(),
|
|
|
|
'heading-id' => $oldHeading->getId(),
|
|
|
|
'heading-name' => $oldHeading->getName(),
|
|
|
|
'section-title' => $oldHeading->getLinkableTitle(),
|
|
|
|
'revid' => $newRevRecord->getId(),
|
|
|
|
],
|
|
|
|
'agent' => $user,
|
|
|
|
];
|
|
|
|
}
|
2023-01-31 15:50:34 +00:00
|
|
|
|
|
|
|
$titleObj = Title::castFromPageIdentity( $title );
|
|
|
|
if ( $titleObj ) {
|
|
|
|
foreach ( $addedHeadings as $newHeading ) {
|
|
|
|
// Don't use $event here as that already exists as a reference from above
|
|
|
|
$addTopicEvent = [
|
|
|
|
'type' => 'dt-added-topic',
|
|
|
|
'title' => $title,
|
|
|
|
'extra' => [
|
|
|
|
// As no one can be subscribed to a topic before it has been created,
|
|
|
|
// we will notify users who have subscribed to the whole page.
|
|
|
|
'subscribed-comment-name' => CommentUtils::getNewTopicsSubscriptionId( $titleObj ),
|
|
|
|
'heading-id' => $newHeading->getId(),
|
|
|
|
'heading-name' => $newHeading->getName(),
|
|
|
|
'section-title' => $newHeading->getLinkableTitle(),
|
|
|
|
'revid' => $newRevRecord->getId(),
|
|
|
|
],
|
|
|
|
'agent' => $user,
|
|
|
|
];
|
|
|
|
// Add metadata about the accompanying comment
|
|
|
|
$firstComment = $newHeading->getOldestReply();
|
|
|
|
if ( $firstComment ) {
|
|
|
|
$addTopicEvent['extra']['comment-id'] = $firstComment->getId();
|
|
|
|
$addTopicEvent['extra']['comment-name'] = $firstComment->getName();
|
|
|
|
$addTopicEvent['extra']['content'] = $firstComment->getBodyText( true );
|
|
|
|
}
|
|
|
|
$events[] = $addTopicEvent;
|
|
|
|
}
|
|
|
|
}
|
2021-02-09 21:51:09 +00:00
|
|
|
}
|
|
|
|
|
2021-07-22 18:05:34 +00:00
|
|
|
/**
|
|
|
|
* Add our change tag for a revision that adds new comments.
|
|
|
|
*/
|
2021-12-01 14:53:20 +00:00
|
|
|
protected static function addCommentChangeTag( RevisionRecord $newRevRecord ): void {
|
2021-07-22 18:05:34 +00:00
|
|
|
// Unclear if DeferredUpdates::addCallableUpdate() is needed,
|
|
|
|
// but every extension does it that way.
|
|
|
|
DeferredUpdates::addCallableUpdate( static function () use ( $newRevRecord ) {
|
2024-04-01 01:00:39 +00:00
|
|
|
MediaWikiServices::getInstance()->getChangeTagsStore()
|
|
|
|
->addTags( [ 'discussiontools-added-comment' ], null, $newRevRecord->getId() );
|
2021-07-22 18:05:34 +00:00
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
2021-08-17 20:23:27 +00:00
|
|
|
/**
|
|
|
|
* Add an automatic subscription to the given item, assuming the user has automatic subscriptions
|
|
|
|
* enabled.
|
|
|
|
*/
|
2021-12-01 14:53:20 +00:00
|
|
|
protected static function addAutoSubscription( UserIdentity $user, Title $title, string $itemName ): void {
|
2021-08-17 20:23:27 +00:00
|
|
|
$dtConfig = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'discussiontools' );
|
|
|
|
|
|
|
|
if (
|
2021-12-15 16:21:43 +00:00
|
|
|
$dtConfig->get( 'DiscussionToolsAutoTopicSubEditor' ) === 'any' &&
|
2021-08-17 20:23:27 +00:00
|
|
|
HookUtils::shouldAddAutoSubscription( $user, $title )
|
|
|
|
) {
|
2024-11-20 07:11:59 +00:00
|
|
|
/** @var SubscriptionStore $subscriptionStore */
|
2021-08-17 20:23:27 +00:00
|
|
|
$subscriptionStore = MediaWikiServices::getInstance()->getService( 'DiscussionTools.SubscriptionStore' );
|
|
|
|
$subscriptionStore->addAutoSubscriptionForUser( $user, $title, $itemName );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-09 21:51:09 +00:00
|
|
|
/**
|
|
|
|
* Return all users subscribed to a comment
|
|
|
|
*
|
2023-06-08 08:16:15 +00:00
|
|
|
* @param Event $event
|
2021-02-09 21:51:09 +00:00
|
|
|
* @param int $batchSize
|
|
|
|
* @return UserIdentity[]|Iterator<UserIdentity>
|
|
|
|
*/
|
2023-06-08 08:16:15 +00:00
|
|
|
public static function locateSubscribedUsers( Event $event, $batchSize = 500 ) {
|
2021-02-09 21:51:09 +00:00
|
|
|
$commentName = $event->getExtraParam( 'subscribed-comment-name' );
|
|
|
|
|
2023-05-19 07:44:23 +00:00
|
|
|
/** @var SubscriptionStore $subscriptionStore */
|
2021-02-09 21:51:09 +00:00
|
|
|
$subscriptionStore = MediaWikiServices::getInstance()->getService( 'DiscussionTools.SubscriptionStore' );
|
|
|
|
$subscriptionItems = $subscriptionStore->getSubscriptionItemsForTopic(
|
|
|
|
$commentName,
|
2021-08-17 20:23:27 +00:00
|
|
|
[ SubscriptionStore::STATE_SUBSCRIBED, SubscriptionStore::STATE_AUTOSUBSCRIBED ]
|
2021-02-09 21:51:09 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
// Update notified timestamps
|
|
|
|
$subscriptionStore->updateSubscriptionNotifiedTimestamp(
|
|
|
|
null,
|
|
|
|
$commentName
|
|
|
|
);
|
|
|
|
|
|
|
|
// TODD: Have this return an Iterator instead?
|
2021-05-05 06:59:38 +00:00
|
|
|
$users = array_map( static function ( SubscriptionItem $item ) {
|
2021-02-09 21:51:09 +00:00
|
|
|
return $item->getUserIdentity();
|
|
|
|
}, $subscriptionItems );
|
|
|
|
|
|
|
|
return $users;
|
|
|
|
}
|
|
|
|
|
2021-10-18 08:32:17 +00:00
|
|
|
/**
|
|
|
|
* Log stuff to EventLogging's Schema:TalkPageEvent
|
|
|
|
* If you don't have EventLogging installed, does nothing.
|
|
|
|
*
|
|
|
|
* @param array $addedComments
|
|
|
|
* @param RevisionRecord $newRevRecord
|
|
|
|
* @param PageIdentity $title
|
|
|
|
* @param UserIdentity $identity
|
|
|
|
* @return bool Whether events were logged
|
|
|
|
*/
|
2021-12-01 14:53:20 +00:00
|
|
|
protected static function logAddedComments(
|
|
|
|
array $addedComments,
|
|
|
|
RevisionRecord $newRevRecord,
|
|
|
|
PageIdentity $title,
|
|
|
|
UserIdentity $identity
|
|
|
|
): bool {
|
2022-07-27 11:39:50 +00:00
|
|
|
global $wgDTSchemaEditAttemptStepOversample, $wgDBname;
|
2021-11-18 17:27:44 +00:00
|
|
|
$context = RequestContext::getMain();
|
|
|
|
$request = $context->getRequest();
|
2021-10-18 08:32:17 +00:00
|
|
|
// We've reached here through Echo's post-save deferredupdate, which
|
|
|
|
// might either be after an API request from DiscussionTools or a
|
|
|
|
// regular POST from WikiEditor. Both should have this value snuck
|
|
|
|
// into their request if their session is being logged.
|
2022-08-26 10:07:15 +00:00
|
|
|
if ( !$request->getCheck( 'editingStatsId' ) ) {
|
2021-10-18 08:32:17 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
$editingStatsId = $request->getVal( 'editingStatsId' );
|
2022-08-26 10:07:15 +00:00
|
|
|
$isDiscussionTools = $request->getCheck( 'dttags' );
|
2021-10-18 08:32:17 +00:00
|
|
|
|
|
|
|
$extensionRegistry = ExtensionRegistry::getInstance();
|
|
|
|
if ( !$extensionRegistry->isLoaded( 'EventLogging' ) ) {
|
|
|
|
return false;
|
|
|
|
}
|
2023-03-18 02:29:27 +00:00
|
|
|
if ( !$extensionRegistry->isLoaded( 'WikimediaEvents' ) ) {
|
|
|
|
return false;
|
|
|
|
}
|
2022-06-09 13:51:33 +00:00
|
|
|
$inSample = static::inEventSample( $editingStatsId );
|
2022-07-27 11:39:50 +00:00
|
|
|
$shouldOversample = ( $isDiscussionTools && $wgDTSchemaEditAttemptStepOversample ) || (
|
2021-11-18 17:27:44 +00:00
|
|
|
// @phan-suppress-next-line PhanUndeclaredClassMethod
|
|
|
|
\WikimediaEvents\WikimediaEventsHooks::shouldSchemaEditAttemptStepOversample( $context )
|
|
|
|
);
|
2021-10-18 08:32:17 +00:00
|
|
|
if ( !$inSample && !$shouldOversample ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-04-11 19:52:05 +00:00
|
|
|
$services = MediaWikiServices::getInstance();
|
|
|
|
$editTracker = $services->getUserEditTracker();
|
2023-07-26 10:32:27 +00:00
|
|
|
$userIdentityUtils = $services->getUserIdentityUtils();
|
2021-10-18 08:32:17 +00:00
|
|
|
|
2022-02-11 21:06:00 +00:00
|
|
|
$commonData = [
|
2023-04-11 19:52:05 +00:00
|
|
|
'$schema' => '/analytics/mediawiki/talk_page_edit/1.2.0',
|
2022-02-11 21:06:00 +00:00
|
|
|
'action' => 'publish',
|
|
|
|
'session_id' => $editingStatsId,
|
|
|
|
'page_id' => $newRevRecord->getPageId(),
|
|
|
|
'page_namespace' => $title->getNamespace(),
|
|
|
|
'revision_id' => $newRevRecord->getId() ?: 0,
|
|
|
|
'performer' => [
|
|
|
|
// Note: we're logging the user who made the edit, not the user who's signed on the comment
|
2022-08-04 23:43:16 +00:00
|
|
|
'user_id' => $identity->getId(),
|
|
|
|
'user_edit_count' => $editTracker->getUserEditCount( $identity ) ?: 0,
|
2022-02-11 21:06:00 +00:00
|
|
|
// Retention-safe values:
|
2022-08-04 23:43:16 +00:00
|
|
|
'user_is_anonymous' => !$identity->isRegistered(),
|
2023-07-26 10:32:27 +00:00
|
|
|
'user_is_temp' => $userIdentityUtils->isTemp( $identity ),
|
2024-06-08 22:02:35 +00:00
|
|
|
'user_edit_count_bucket' => UserBucketProvider::getUserEditCountBucket( $identity ) ?: 'N/A',
|
2022-02-11 21:06:00 +00:00
|
|
|
],
|
|
|
|
'database' => $wgDBname,
|
|
|
|
// This is unreliable, but sufficient for our purposes; we
|
|
|
|
// mostly just want to see the difference between DT and
|
|
|
|
// everything-else:
|
|
|
|
'integration' => $isDiscussionTools ? 'discussiontools' : 'page',
|
|
|
|
];
|
|
|
|
|
2021-10-18 08:32:17 +00:00
|
|
|
foreach ( $addedComments as $comment ) {
|
|
|
|
$heading = $comment->getSubscribableHeading();
|
|
|
|
$parent = $comment->getParent();
|
|
|
|
if ( !$heading || !$parent ) {
|
|
|
|
continue;
|
|
|
|
}
|
2022-02-11 21:06:00 +00:00
|
|
|
if ( $parent->getType() === 'heading' ) {
|
|
|
|
if ( count( $heading->getReplies() ) === 1 ) {
|
|
|
|
// A new heading was added when this comment was created
|
|
|
|
$component_type = 'topic';
|
|
|
|
} else {
|
|
|
|
$component_type = 'comment';
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$component_type = 'response';
|
|
|
|
}
|
2022-03-06 16:10:40 +00:00
|
|
|
EventLogging::submit( 'mediawiki.talk_page_edit', array_merge( $commonData, [
|
2022-02-11 21:06:00 +00:00
|
|
|
'component_type' => $component_type,
|
2021-10-18 08:32:17 +00:00
|
|
|
'topic_id' => $heading->getId(),
|
|
|
|
'comment_id' => $comment->getId(),
|
|
|
|
'comment_parent_id' => $parent->getId(),
|
2022-02-11 21:06:00 +00:00
|
|
|
] ) );
|
2021-10-18 08:32:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Should the current session be sampled for EventLogging?
|
|
|
|
*
|
|
|
|
* @param string $sessionId
|
|
|
|
* @return bool Whether to sample the session
|
|
|
|
*/
|
2021-12-01 14:53:20 +00:00
|
|
|
protected static function inEventSample( string $sessionId ): bool {
|
2021-10-18 08:32:17 +00:00
|
|
|
global $wgDTSchemaEditAttemptStepSamplingRate, $wgWMESchemaEditAttemptStepSamplingRate;
|
|
|
|
// Sample 6.25%
|
|
|
|
$samplingRate = 0.0625;
|
2024-12-16 15:41:02 +00:00
|
|
|
if ( $wgDTSchemaEditAttemptStepSamplingRate !== null ) {
|
2021-10-18 08:32:17 +00:00
|
|
|
$samplingRate = $wgDTSchemaEditAttemptStepSamplingRate;
|
|
|
|
}
|
2024-12-16 15:41:02 +00:00
|
|
|
if ( $wgWMESchemaEditAttemptStepSamplingRate !== null ) {
|
2021-10-18 08:32:17 +00:00
|
|
|
$samplingRate = $wgWMESchemaEditAttemptStepSamplingRate;
|
|
|
|
}
|
|
|
|
if ( $samplingRate === 0 ) {
|
|
|
|
return false;
|
|
|
|
}
|
2022-03-06 16:10:40 +00:00
|
|
|
$inSample = EventLogging::sessionInSample(
|
2021-10-18 08:32:17 +00:00
|
|
|
(int)( 1 / $samplingRate ), $sessionId
|
|
|
|
);
|
|
|
|
return $inSample;
|
|
|
|
}
|
|
|
|
|
2021-02-09 21:51:09 +00:00
|
|
|
}
|