Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
LqtNotifications
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 7
132
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 overrideUsersToNotify
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 locateUsersWithPendingLqtNotifications
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 afterTopicImported
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 importAborted
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 afterHeaderImported
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 afterPostImported
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Flow\Import\Postprocessor;
4
5use BatchRowIterator;
6use Flow\Import\IImportHeader;
7use Flow\Import\IImportPost;
8use Flow\Import\IImportTopic;
9use Flow\Import\ImportException;
10use Flow\Import\LiquidThreadsApi\ImportTopic;
11use Flow\Import\PageImportState;
12use Flow\Import\TopicImportState;
13use Flow\Model\PostRevision;
14use Flow\Notifications\Controller;
15use MediaWiki\Extension\Notifications\Iterator\CallbackIterator;
16use MediaWiki\Extension\Notifications\Model\Event;
17use MediaWiki\User\User;
18use RecursiveIteratorIterator;
19use Wikimedia\Rdbms\IReadableDatabase;
20
21/**
22 * Converts LQT unread notifications into Echo notifications after a topic is imported
23 */
24class LqtNotifications implements Postprocessor {
25
26    /**
27     * @var Controller
28     */
29    protected $controller;
30
31    protected IReadableDatabase $dbr;
32
33    /**
34     * @var PostRevision[] Array of imported replies
35     */
36    protected $postsImported = [];
37
38    public function __construct( Controller $controller, IReadableDatabase $dbr ) {
39        $this->controller = $controller;
40        $this->dbr = $dbr;
41        $this->overrideUsersToNotify();
42    }
43
44    /**
45     * evil, but what should we do instead? This basically removes the default methods
46     * of determining users to notify so they can be replaced with this class during imports.
47     */
48    protected function overrideUsersToNotify() {
49        global $wgEchoNotifications;
50
51        // Remove the user-locators that choose on a per-notification basis who
52        // should be notified.
53        $notifications = require dirname( dirname( dirname( __DIR__ ) ) ) . '/Notifications.php';
54        foreach ( array_keys( $notifications ) as $type ) {
55            unset( $wgEchoNotifications[$type]['user-locators'] );
56
57            // The job queue causes our overrides to be lost since it
58            // has a separate execution context.
59            $wgEchoNotifications[$type]['immediate'] = true;
60        }
61
62        // Insert our own user locator to decide who should be notified.
63        // Note this has to be a closure rather than direct callback due to how
64        // echo considers an array to be extra parameters.
65        // Overrides existing user-locators, because we don't want unintended
66        // notifications to go out here.
67        $wgEchoNotifications['flow-post-reply']['user-locators'] = [
68            function ( Event $event ) {
69                return $this->locateUsersWithPendingLqtNotifications( $event );
70            }
71        ];
72    }
73
74    /**
75     * @param Event $event
76     * @param int $batchSize
77     * @throws ImportException
78     * @return CallbackIterator
79     */
80    public function locateUsersWithPendingLqtNotifications( Event $event, $batchSize = 500 ) {
81        $activeThreadId = $event->getExtraParam( 'lqtThreadId' );
82        if ( $activeThreadId === null ) {
83            throw new ImportException( 'No active thread!' );
84        }
85
86        $it = new BatchRowIterator(
87            $this->dbr,
88            /* table = */ 'user_message_state',
89            /* primary keys */ [ 'ums_user' ],
90            $batchSize
91        );
92        $it->addConditions( [
93            'ums_conversation' => $activeThreadId,
94            'ums_read_timestamp' => null,
95        ] );
96        $it->setCaller( __METHOD__ );
97
98        // flatten result into a stream of rows
99        $it = new RecursiveIteratorIterator( $it );
100
101        // add callback to convert user id to user objects
102        $it = new CallbackIterator( $it, static function ( $row ) {
103            return User::newFromId( $row->ums_user );
104        } );
105
106        return $it;
107    }
108
109    public function afterTopicImported( TopicImportState $state, IImportTopic $topic ) {
110        if ( !$topic instanceof ImportTopic ) {
111            return;
112        }
113        if ( !$this->postsImported ) {
114            // nothing was imported in this topic
115            return;
116        }
117
118        $this->controller->notifyPostChange( 'flow-post-reply', [
119            'revision' => $this->postsImported[0],
120            'topic-title' => $state->topicTitle,
121            'topic-workflow' => $state->topicWorkflow,
122            'title' => $state->topicWorkflow->getOwnerTitle(),
123            'reply-to' => $state->topicTitle,
124            'extra-data' => [
125                'lqtThreadId' => $topic->getLqtThreadId(),
126                'notifyAgent' => true,
127            ],
128            'timestamp' => $topic->getTimestamp(),
129        ] );
130
131        $this->postsImported = [];
132    }
133
134    public function importAborted() {
135        $this->postsImported = [];
136    }
137
138    public function afterHeaderImported( PageImportState $state, IImportHeader $header ) {
139        // not a thing to do, yet
140    }
141
142    public function afterPostImported( TopicImportState $state, IImportPost $post, PostRevision $newPost ) {
143        $this->postsImported[] = $newPost;
144    }
145}