MaxSem has uploaded a new change for review. (
https://gerrit.wikimedia.org/r/350500 )
Change subject: WIP: Add user right expiry notifications
......................................................................
WIP: Add user right expiry notifications
TODO: links
TODO: configurability
More fundamental problem is that without remembering which user rights have
already been echoed, you can't reliably rerun the script or miss a scheduled
run and hope to recover from it later. Users will just receive duplicate
messages.
Bug: T153817
Change-Id: I697ba9524b9798ae35dcd6565089e0727f7c1b2f
---
M extension.json
M i18n/en.json
A includes/formatters/UserRightsExpiryPresentationModel.php
A includes/jobs/UserRightsExpiryNotificationJob.php
A maintenance/generateRightExpiryNotifications.php
5 files changed, 334 insertions(+), 1 deletion(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Echo
refs/changes/00/350500/1
diff --git a/extension.json b/extension.json
index 2dec0f1..d4393eb 100644
--- a/extension.json
+++ b/extension.json
@@ -35,7 +35,8 @@
],
"JobClasses": {
"EchoNotificationJob": "EchoNotificationJob",
- "EchoNotificationDeleteJob": "EchoNotificationDeleteJob"
+ "EchoNotificationDeleteJob": "EchoNotificationDeleteJob",
+ "EchoUserRightExpiryNotification":
"EchoUserRightsExpiryNotificationJob"
},
"SpecialPages": {
"Notifications": "SpecialNotifications",
@@ -143,6 +144,8 @@
"EchoUserLocatorTest": "tests/phpunit/UserLocatorTest.php",
"EchoUserNotificationGateway":
"includes/gateway/UserNotificationGateway.php",
"EchoUserNotificationGatewayTest":
"tests/phpunit/gateway/UserNotificationGatewayTest.php",
+ "EchoUserRightsExpiryNotificationJob":
"includes/jobs/UserRightsExpiryNotificationJob.php",
+ "EchoUserRightsExpiryPresentationModel":
"includes/formatters/UserRightsExpiryPresentationModel.php",
"EchoUserRightsPresentationModel":
"includes/formatters/UserRightsPresentationModel.php",
"EchoWelcomePresentationModel":
"includes/formatters/WelcomePresentationModel.php",
"FilteredSequentialIteratorTest":
"tests/phpunit/iterator/FilteredSequentialIteratorTest.php",
@@ -928,6 +931,15 @@
"section": "alert",
"presentation-model":
"EchoUserRightsPresentationModel"
},
+ "user-rights-expiry": {
+ "user-locators": [
+
"EchoUserLocator::locateEventAgent"
+ ],
+ "category": "user-rights",
+ "group": "neutral",
+ "section": "alert",
+ "presentation-model":
"EchoUserRightsExpiryPresentationModel"
+ },
"emailuser": {
"presentation-model":
"EchoEmailUserPresentationModel",
"user-locators": [
diff --git a/i18n/en.json b/i18n/en.json
index 28d626f..0fe2861 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -167,6 +167,8 @@
"notification-header-user-rights-remove-only": "{{GENDER:$4|Your}} user
rights were {{GENDER:$1|changed}}. You are no longer a member of: $2.",
"notification-header-user-rights-add-and-remove": "{{GENDER:$6|Your}}
user rights were {{GENDER:$1|changed}}. You have been added to: $2. You are no
longer a member of: $4.",
"notification-header-user-rights-expiry-change": "The expiry of
{{GENDER:$4|your}} membership in the following {{PLURAL:$3|group|groups}} has
been {{GENDER:$1|changed}}: $2.",
+ "notification-header-user-rights-future-expiry": "{{GENDER:$3|Your}}
membership in the following {{PLURAL:$2|group|groups}}: $1 expires on $4.",
+ "notification-header-user-rights-past-expiry": "{{GENDER:$3|Your}}
membership in the following {{PLURAL:$2|group|groups}}: $1 expired.",
"notification-body-user-rights": "$1",
"notification-header-welcome": "{{GENDER:$2|Welcome}} to {{SITENAME}},
$1! We're glad {{GENDER:$2|you're}} here.",
"notification-welcome-link": "",
diff --git a/includes/formatters/UserRightsExpiryPresentationModel.php
b/includes/formatters/UserRightsExpiryPresentationModel.php
new file mode 100644
index 0000000..0b38a28
--- /dev/null
+++ b/includes/formatters/UserRightsExpiryPresentationModel.php
@@ -0,0 +1,76 @@
+<?php
+
+class EchoUserRightsExpiryPresentationModel extends EchoEventPresentationModel
{
+ const TYPE_FUTURE = 'future';
+ const TYPE_PAST = 'past';
+
+ public function getIconType() {
+ return 'user-rights';
+ }
+
+ public function getHeaderMessage() {
+ $groups = $this->event->getExtraParam( 'groups' );
+ if ( !$groups ) {
+ throw new Exception( __CLASS__ . ' received an invalid
list of groups: '
+ . json_encode( $groups )
+ );
+ }
+ $groupNames = array_map(
+ [ $this->language, 'embedBidi' ],
+ $this->getLocalizedGroupNames( $groups )
+ );
+
+ $type = $this->event->getExtraParam( 'type' );
+ if ( !$type ) {
+ throw new Exception( __CLASS__ . ' received no
notification type' );
+ }
+
+ switch ( $type ) {
+ case self::TYPE_FUTURE:
+ $messageKey =
'notification-header-user-rights-future-expiry';
+ break;
+ case self::TYPE_PAST:
+ $messageKey =
'notification-header-user-rights-past-expiry';
+ break;
+ default:
+ throw new Exception( "Unrecognized notification
type '{$type}'" );
+ }
+
+ $msg = $this->msg( $messageKey )
+ ->params(
+ $this->language->commaList( $groupNames ),
+ count( $groupNames ),
+ $this->getViewingUserForGender()
+ );
+
+ if ( $type === self::TYPE_FUTURE ) {
+ $expiry = $this->event->getExtraParam( 'expiry' );
+ if ( !$expiry ) {
+ throw new Exception( __CLASS__ . ' received an
event without expiry time' );
+ }
+
+ $dateTime = new MWTimestamp( $expiry );
+ $msg->params( $this->language->date( $dateTime ) );
+ }
+
+ return $msg;
+ }
+
+ private function getLocalizedGroupNames( $names ) {
+ return array_map( function( $name ) {
+ $msg = $this->msg( 'group-' . $name );
+ return $msg->isBlank() ? $name : $msg->text();
+ }, $names );
+ }
+
+ /**
+ * Array of primary link details, with possibly-relative URL & label.
+ *
+ * @return array|bool Array of link data, or false for no link:
+ * ['url' => (string) url, 'label' => (string) link
text (non-escaped)]
+ */
+ public function getPrimaryLink() {
+ // @TODO:
+ return false;
+ }
+}
diff --git a/includes/jobs/UserRightsExpiryNotificationJob.php
b/includes/jobs/UserRightsExpiryNotificationJob.php
new file mode 100644
index 0000000..162e0f3
--- /dev/null
+++ b/includes/jobs/UserRightsExpiryNotificationJob.php
@@ -0,0 +1,108 @@
+<?php
+
+use Wikimedia\Assert\Assert;
+
+/**
+ * Job that sends notifications about user rights expiry at the moment these
happen
+ *
+ * These jobs are enqueued by maintenance/generateRightExpiryNotifications.php
+ */
+class EchoUserRightsExpiryNotificationJob extends Job {
+ /** @var User */
+ private $user;
+
+ /** @var string[] */
+ private $groups;
+
+ /** @var string */
+ private $expiry;
+
+ /**
+ * @param Title $title
+ * @param array|bool $params
+ */
+ public function __construct( Title $title, $params = false ) {
+ parent::__construct( 'EchoUserRightExpiryNotification', $title,
$params );
+
+ Assert::parameter( isset( $params['user'] ),
+ "params['user']",
+ __CLASS__ . ' needs a user'
+ );
+ Assert::parameterType( 'User', $params['user'],
"params['user']" );
+ $this->user = $params['user'];
+
+ Assert::parameter( isset( $params['groups'] ),
+ "params['groups']",
+ __CLASS__ . ' needs groups'
+ );
+ Assert::parameterElementType( 'string', $params['groups'],
"params['groups']" );
+ $this->groups = $params['groups'];
+
+ Assert::parameter( isset( $params['expiry'] ),
+ "params['expiry']",
+ __CLASS__ . ' needs expiry time'
+ );
+ $this->expiry = $params['expiry'];
+ }
+
+ /**
+ * Run the job
+ *
+ * @return bool Success
+ */
+ public function run() {
+ $groups = $this->getStillExpiringGroups();
+ if ( !$groups ) {
+ return true;
+ }
+
+ return (bool)EchoEvent::create( [
+ 'type' => 'user-rights-expiry',
+ 'agent' => $this->user,
+ 'extra' => [
+ 'type' =>
EchoUserRightsExpiryPresentationModel::TYPE_PAST,
+ 'groups' => $groups,
+ 'expiry' => $this->expiry,
+ 'notifyAgent' => true,
+ ],
+ ] );
+ }
+
+ /**
+ * This job should be executed in the future, when user rights expire
+ *
+ * @return string
+ */
+ public function getReleaseTimestamp() {
+ return wfTimestamp( TS_UNIX, $this->expiry );
+ }
+
+ /**
+ * Returns which of the groups are still expiring at the expected time
+ *
+ * @return string[]
+ */
+ private function getStillExpiringGroups() {
+ $dbr = wfGetDB( DB_REPLICA );
+
+ $res = $dbr->select( 'user_groups',
+ '*',
+ [
+ 'ug_user' => $this->user->getId(),
+ 'ug_group' => $this->groups,
+ ],
+ __METHOD__
+ );
+
+ // If a group is still there but its expiry is different, don't
notify about it
+ // If a group is missing from database, assume it had expired
and was garbage collected
+ $groups = array_flip( $this->groups );
+ foreach ( $res as $row ) {
+ if ( $row->ug_expiry !== $this->expiry ) {
+ unset( $groups[$row->ug_group] );
+ }
+ }
+
+ return array_keys( $groups );
+ }
+}
diff --git a/maintenance/generateRightExpiryNotifications.php
b/maintenance/generateRightExpiryNotifications.php
new file mode 100644
index 0000000..046434d
--- /dev/null
+++ b/maintenance/generateRightExpiryNotifications.php
@@ -0,0 +1,135 @@
+<?php
+
+$IP = getenv( 'MW_INSTALL_PATH' );
+if ( $IP === false ) {
+ $IP = __DIR__ . '/../../..';
+}
+require_once ( "$IP/maintenance/Maintenance.php" );
+
+class GenerateRightExpiryNotifications extends Maintenance {
+ private $startTimestamp;
+
+ public function __construct() {
+ parent::__construct();
+
+ $this->requireExtension( 'Echo' );
+ $this->addDescription( 'Generates notifications for expiring
user rights' );
+ $this->setBatchSize( 500 );
+ }
+
+ public function execute() {
+ $this->startTimestamp = wfTimestampNow();
+
+ $this->output( "Looking up rights expiring in the future...\n"
);
+ $this->scanExpiringRights( '1 days', '14 days', function(
$userId, array $groups, $expiry ) {
+ $this->notifyAboutExpiry(
EchoUserRightsExpiryPresentationModel::TYPE_FUTURE,
+ $userId, $groups, $expiry
+ );
+ } );
+
+ $this->output( "Looking up rights expiring soon...\n" );
+ $this->scanExpiringRights( false, '1 day', function( $userId,
array $groups, $expiry ) {
+ $user = User::newFromId( $userId );
+ echo "Past: {$user} " . json_encode( $groups ) . "
expires {$expiry}\n";
+ $job = new EchoUserRightsExpiryNotificationJob(
$user->getTalkPage(),
+ [
+ 'user' => $user,
+ 'groups' => $groups,
+ 'expiry' => $expiry,
+ ]
+ );
+
+ JobQueueGroup::singleton()->push( $job );
+ } );
+ }
+
+ /**
+ * @param string|bool $offset
+ * @return mixed
+ */
+ private function timeFromNow( $offset = false ) {
+ $ts = new MWTimestamp( $this->startTimestamp );
+
+ if ( $offset !== false ) {
+ $ts->timestamp->modify( $offset );
+ }
+
+ return $ts;
+ }
+
+ private function notifyAboutExpiry( $type, $userId, array $groups,
$expiry ) {
+ $user = User::newFromId( $userId );
+ if ( !$user ) {
+ $this->error( "User with id={$userId} does not exist\n"
);
+ }
+ sort( $groups );
+
+ $event = EchoEvent::create( [
+ 'type' => 'user-rights-expiry',
+ 'agent' => $user,
+ 'extra' => [
+ 'type' => $type,
+ 'groups' => $groups,
+ 'expiry' => $expiry,
+ 'notifyAgent' => true,
+ ],
+ ] );
+
+ if ( !$event ) {
+ $this->error( "Error sending notification\n" );
+ }
+ }
+
+ private function scanExpiringRights( $from, $to, callable $callback ) {
+ $dbr = $this->getDB( DB_REPLICA );
+
+ $from = $dbr->timestamp( $this->timeFromNow( $from ) );
+ $to = $dbr->timestamp( $this->timeFromNow( $to ) );
+
+ $count = 0;
+ $lastUser = null;
+ do {
+ $conds = [
+ 'ug_expiry BETWEEN ' . $dbr->addQuotes( $from )
. ' AND ' . $dbr->addQuotes( $to ),
+ ];
+ if ( $lastUser !== null ) {
+ $conds[] = 'ug_user >= ' . $dbr->addQuotes(
$lastUser );
+ }
+
+ $res = $dbr->select( 'user_groups', '*', $conds,
__METHOD__,
+ [ 'LIMIT' => $this->mBatchSize, 'ORDER BY' => [
'ug_expiry', 'ug_user' ] ]
+ );
+
+ // This omits the last user in the batch because we
might miss some of their groups.
+ // Start our next select with them.
+ $user = null;
+ $groups = [];
+ $expiry = null;
+ foreach ( $res as $row ) {
+ if ( $user && $user != $row->ug_user && $expiry
&& $expiry != $row->ug_expiry ) {
+ if ( !$groups ) {
+ throw new Exception( "Trying to
notify user {$user} with an empty group list" );
+ }
+
+ $callback( $user, $groups, $expiry );
+ $groups = [];
+ }
+ $user = $row->ug_user;
+ $expiry = $row->ug_expiry;
+ $groups[] = $row->ug_group;
+ }
+ $lastUser = $user;
+
+ $count += $res->numRows();
+ $this->output( " {$count}\n" );
+ } while ( $res->numRows() == $this->mBatchSize );
+
+ // Flush last user
+ if ( $user && $expiry && $groups ) {
+ $callback( $user, $groups, $expiry );
+ }
+ }
+}
+
+$maintClass = 'GenerateRightExpiryNotifications';
+require_once ( DO_MAINTENANCE );
--
To view, visit https://gerrit.wikimedia.org/r/350500
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I697ba9524b9798ae35dcd6565089e0727f7c1b2f
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Echo
Gerrit-Branch: master
Gerrit-Owner: MaxSem <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits