Service to interact with topic subscriptions

Bug: T264885
Change-Id: Ie9592de655f50e1d0cf02a7f795b5203398a9696
This commit is contained in:
David Lynch 2021-02-16 01:51:49 -06:00 committed by Bartosz Dziewoński
parent 5b8646f73f
commit 86be6f83da
4 changed files with 424 additions and 0 deletions

View file

@ -383,6 +383,9 @@
"class": "MediaWiki\\Extension\\DiscussionTools\\Hooks\\TagHooks"
}
},
"ServiceWiringFiles": [
"includes/ServiceWiring.php"
],
"DefaultUserOptions": {
"discussiontools-editmode": "",
"discussiontools-newtopictool": 1,

View file

@ -0,0 +1,15 @@
<?php
namespace MediaWiki\Extension\DiscussionTools;
use MediaWiki\MediaWikiServices;
return [
'DiscussionTools.SubscriptionStore' => static function ( MediaWikiServices $services ) : SubscriptionStore {
return new SubscriptionStore(
$services->getDBLoadBalancerFactory(),
$services->getReadOnlyMode(),
$services->getUserFactory()
);
}
];

View file

@ -0,0 +1,91 @@
<?php
namespace MediaWiki\Extension\DiscussionTools;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\User\UserIdentity;
/**
* Representation of a subscription to a given topic.
*/
class SubscriptionItem {
private $itemName;
private $linkTarget;
private $user;
private $state;
private $createdTimestamp;
private $notifiedTimestamp;
/**
* @param UserIdentity $user
* @param string $itemName
* @param LinkTarget $linkTarget
* @param int $state 1/0 for watched/muted
* @param string|null $createdTimestamp When the subscription was created
* @param string|null $notifiedTimestamp When the item subscribed to last tried to trigger
* a notification (even if muted).
*/
public function __construct(
UserIdentity $user,
string $itemName,
linkTarget $linkTarget,
int $state,
?string $createdTimestamp,
?string $notifiedTimestamp
) {
$this->user = $user;
$this->itemName = $itemName;
$this->linkTarget = $linkTarget;
$this->state = $state;
$this->createdTimestamp = $createdTimestamp;
$this->notifiedTimestamp = $notifiedTimestamp;
}
/**
* @return UserIdentity
*/
public function getUserIdentity() : UserIdentity {
return $this->user;
}
/**
* @return string
*/
public function getItemName() : string {
return $this->itemName;
}
/**
* @return LinkTarget
*/
public function getLinkTarget() : LinkTarget {
return $this->linkTarget;
}
/**
* Get the creation timestamp of this entry.
*
* @return string|null
*/
public function getCreatedTimestamp() {
return $this->createdTimestamp;
}
/**
* Get the notification timestamp of this entry.
*
* @return string|null
*/
public function getNotificationTimestamp() {
return $this->notifiedTimestamp;
}
/**
* Check if the notification is muted
*
* @return bool
*/
public function isMuted() : bool {
return $this->state === 0;
}
}

View file

@ -0,0 +1,315 @@
<?php
namespace MediaWiki\Extension\DiscussionTools;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\User\UserFactory;
use MediaWiki\User\UserIdentity;
use ReadOnlyMode;
use stdClass;
use TitleValue;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\ILBFactory;
use Wikimedia\Rdbms\ILoadBalancer;
use Wikimedia\Rdbms\IResultWrapper;
// use Wikimedia\ParamValidator\TypeDef\ExpiryDef;
// use Wikimedia\Timestamp\ConvertibleTimestamp;
class SubscriptionStore {
/** @var ILBFactory */
private $lbFactory;
/** @var ILoadBalancer */
private $loadBalancer;
/** @var ReadOnlyMode */
private $readOnlyMode;
/** @var UserFactory */
private $userFactory;
/**
* @param ILBFactory $lbFactory
* @param ReadOnlyMode $readOnlyMode
* @param UserFactory $userFactory
*/
public function __construct(
ILBFactory $lbFactory,
ReadOnlyMode $readOnlyMode,
UserFactory $userFactory
) {
$this->lbFactory = $lbFactory;
$this->loadBalancer = $lbFactory->getMainLB();
$this->userFactory = $userFactory;
$this->readOnlyMode = $readOnlyMode;
}
/**
* @param int $dbIndex DB_MASTER or DB_REPLICA
*
* @return IDatabase
*/
private function getConnectionRef( $dbIndex ) : IDatabase {
return $this->loadBalancer->getConnectionRef( $dbIndex, [ 'watchlist' ] );
}
/**
* @param IDatabase $db
* @param UserIdentity|null $user
* @param string|null $itemName
* @param int|null $state
* @return IResultWrapper|false
*/
private function fetchSubscriptions(
IDatabase $db,
?UserIdentity $user = null,
?string $itemName = null,
?int $state = null
) {
$conditions = [];
if ( $user ) {
$conditions[ 'sub_user' ] = $user->getId();
}
if ( $itemName ) {
$conditions[ 'sub_item' ] = $itemName;
}
if ( $state !== null ) {
$conditions[ 'sub_state' ] = $state;
}
return $db->select(
'discussiontools_subscription',
[
'sub_user', 'sub_item', 'sub_namespace', 'sub_title', 'sub_section', 'sub_state',
'sub_created', 'sub_notified'
],
$conditions,
__METHOD__
);
}
/**
* @param UserIdentity $user
* @param string|null $itemName
* @param int|null $state
* @param array $options
* @return SubscriptionItem[]
*/
public function getSubscriptionItemsForUser(
UserIdentity $user,
?string $itemName = null,
?int $state = null,
array $options = []
) : array {
// Only a registered user can be subscribed
if ( !$user->isRegistered() ) {
return [];
}
$options += [ 'forWrite' => false ];
$db = $this->getConnectionRef( $options['forWrite'] ? DB_MASTER : DB_REPLICA );
$rows = $this->fetchSubscriptions(
$db,
$user,
$itemName,
$state
);
if ( !$rows ) {
return [];
}
$items = [];
foreach ( $rows as $row ) {
$target = new TitleValue( (int)$row->sub_namespace, $row->sub_title, $row->sub_section );
$items[] = $this->getSubscriptionItemFromRow( $user, $target, $row );
}
return $items;
}
/**
* @param string $itemName
* @param int|null $state
* @param array $options
* @return array
*/
public function getSubscriptionItemsForTopic(
string $itemName,
?int $state = null,
array $options = []
) : array {
$options += [ 'forWrite' => false ];
$db = $this->getConnectionRef( $options['forWrite'] ? DB_MASTER : DB_REPLICA );
$rows = $this->fetchSubscriptions(
$db,
null,
$itemName,
$state
);
if ( !$rows ) {
return [];
}
$items = [];
foreach ( $rows as $row ) {
$target = new TitleValue( (int)$row->sub_namespace, $row->sub_title, $row->sub_section );
$user = $this->userFactory->newFromId( $row->sub_user );
$items[] = $this->getSubscriptionItemFromRow( $user, $target, $row );
}
return $items;
}
/**
* @param UserIdentity $user
* @param LinkTarget $target
* @param stdClass $row
* @return SubscriptionItem
*/
private function getSubscriptionItemFromRow(
UserIdentity $user,
LinkTarget $target,
stdClass $row
) : SubscriptionItem {
return new SubscriptionItem(
$user,
$row->sub_item,
$target,
$row->sub_state,
$row->sub_created,
$row->sub_notified
);
}
/**
* @param UserIdentity $user
* @param LinkTarget $target
* @param string $itemName
* @return bool
*/
public function addSubscriptionForUser(
UserIdentity $user,
LinkTarget $target,
string $itemName
) : bool {
if ( $this->readOnlyMode->isReadOnly() ) {
return false;
}
// Only a registered user can subscribe
if ( !$user->isRegistered() ) {
return false;
}
$dbw = $this->getConnectionRef( DB_MASTER );
$dbw->upsert(
'discussiontools_subscription',
[
'sub_user' => $user->getId(),
'sub_namespace' => $target->getNamespace(),
'sub_title' => $target->getDBkey(),
'sub_section' => $target->getFragment(),
'sub_item' => $itemName,
'sub_state' => 1,
'sub_created' => $dbw->timestamp(),
],
[ [ 'sub_user', 'sub_item' ] ],
[
'sub_state' => 1,
],
__METHOD__
);
return (bool)$dbw->affectedRows();
}
/**
* @param UserIdentity $user
* @param string $itemName
* @return bool
*/
public function removeSubscriptionForUser(
UserIdentity $user,
string $itemName
) : bool {
if ( $this->readOnlyMode->isReadOnly() ) {
return false;
}
// Only a registered user can subscribe
if ( !$user->isRegistered() ) {
return false;
}
$dbw = $this->getConnectionRef( DB_MASTER );
$dbw->update(
'discussiontools_subscription',
[ 'sub_state' => 0 ],
[
'sub_user' => $user->getId(),
'sub_item' => $itemName,
],
__METHOD__
);
return (bool)$dbw->affectedRows();
}
/**
* @param string $field Timestamp field name
* @param UserIdentity|null $user
* @param string $itemName
* @return bool
*/
private function updateSubscriptionTimestamp(
string $field,
?UserIdentity $user,
string $itemName
) : bool {
if ( $this->readOnlyMode->isReadOnly() ) {
return false;
}
$dbw = $this->getConnectionRef( DB_MASTER );
$conditions = [
'sub_item' => $itemName,
];
if ( $user ) {
$conditions[ 'sub_user' ] = $user->getId();
}
$dbw->update(
'discussiontools_subscription',
[ $field => $dbw->timestamp() ],
$conditions,
__METHOD__
);
return (bool)$dbw->affectedRows();
}
/**
* Update the notified timestamp on a subscription
*
* This field could be used in future to cleanup notifications
* that are no longer needed (e.g. because the conversation has
* been archived), so should be set for muted notifications too.
*
* @param UserIdentity|null $user
* @param string $itemName
* @return bool
*/
public function updateSubscriptionNotifiedTimestamp(
?UserIdentity $user,
string $itemName
) : bool {
return $this->updateSubscriptionTimestamp(
'sub_notified',
$user,
$itemName
);
}
}