Passed
Push — master ( 9f46c8...2432f8 )
by Dispositif
02:42
created

WikiBotConfig::checkStopOnTalkpageOrException()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 12
c 1
b 0
f 1
dl 0
loc 22
ccs 0
cts 14
cp 0
rs 9.8666
cc 3
nc 3
nop 1
crap 12
1
<?php
2
/*
3
 * This file is part of dispositif/wikibot application (@github)
4
 * 2019-2023 © Philippe M./Irønie  <[email protected]>
5
 * For the full copyright and MIT license information, view the license file.
6
 */
7
8
declare(strict_types=1);
9
10
namespace App\Application;
11
12
use App\Application\InfrastructurePorts\SMSInterface;
13
use App\Domain\Exceptions\ConfigException;
14
use App\Domain\Exceptions\StopActionException;
15
use App\Domain\Utils\WikiTextUtil;
16
use DateInterval;
17
use DateTime;
18
use DateTimeImmutable;
19
use DomainException;
20
use Mediawiki\Api\MediawikiFactory;
21
use Mediawiki\Api\UsageException;
22
use Psr\Log\LoggerInterface;
23
use Psr\Log\NullLogger;
24
use Throwable;
25
26
/**
27
 * Define wiki configuration of the bot.
28
 * See also .env file for parameters.
29
 * Page/Edit/Summary status are defined in Application/.../PageWorkStatus or Domain/OptiStatus
30
 */
31
class WikiBotConfig
32
{
33
    public const VERSION = '2.1';
34
    public const WATCHPAGE_FILENAME = __DIR__ . '/resources/watch_pages.json';
35
    public const EXIT_ON_CHECK_WATCHPAGE = false;
36
    // do not stop if they play with {stop} on bot talk page
37
    public const BLACKLIST_EDITOR = ['OrlodrimBot'];
38
    // Use that timers config instead of worker config ?
39
    public const BOT_FLAG = false;
40
    public const MODE_AUTO = false;
41
    public const EXIT_ON_WIKIMESSAGE = true;
42
    public const EDIT_LAPS = 20;
43
    public const EDIT_LAPS_MANUAL = 20;
44
    public const EDIT_LAPS_AUTOBOT = 60;
45
    public const EDIT_LAPS_FLAGBOT = 8;
46
    public const TALK_STOP_CHECK_INTERVAL = 'PT2M';
47
    public const TALK_PAGE_PREFIX = 'Discussion_utilisateur:';
48
    public const SLEEP_BEFORE_STOP_TALKPAGE = 30;
49
50
    public $taskName = 'Amélioration indéfinie';
51
    /**
52
     * @var LoggerInterface
53
     */
54
    protected $log;
55
    /**
56
     * @var DateTimeImmutable
57
     */
58
    protected $lastCheckStopDate;
59
    /**
60
     * @var SMSInterface|null
61
     */
62
    protected $SMSClient;
63
    protected $mediawikiFactory;
64
65
    public function __construct(MediawikiFactory $mediawikiFactory, ?LoggerInterface $logger = null, ?SMSInterface $SMSClient = null)
66
    {
67
        $this->log = $logger ?? new NullLogger();
68
        ini_set('user_agent', getenv('USER_AGENT'));
69
        $this->SMSClient = $SMSClient;
70
        $this->mediawikiFactory = $mediawikiFactory;
71
    }
72
73
    public function getLogger(): LoggerInterface
74
    {
75
        return $this->log;
76
    }
77
78
    /**
79
     * Detect wiki-templates restricting the edition on a frwiki page.
80
     */
81
    public static function isEditionTemporaryRestrictedOnWiki(?string $text, ?string $botName = null): bool
82
    {
83
        return empty($text)
84
            || preg_match('#{{Formation#i', $text) > 0
85
            || preg_match('#{{En travaux#i', $text) > 0
86
            || preg_match('#{{En cours#i', $text) > 0
87
            || preg_match('#{{Protection#i', $text) > 0
88
            || preg_match('#\{\{(R3R|Règle des 3 révocations|travaux|en travaux|en cours|formation)#i', $text) > 0
89
            || self::isNoBotTag($text, $botName);
90
    }
91
92 1
    /**
93
     * Detect {{nobots}}, {{bots|deny=all}}, {{bots|deny=MyBot,BobBot}}.
94 1
     * Relevant out of the "main" wiki-namespace (talk pages, etc).
95
     */
96
    protected static function isNoBotTag(string $text, ?string $botName = null): bool
97 1
    {
98 1
        $text = WikiTextUtil::removeHTMLcomments($text);
99 1
        $botName = $botName ?: self::getBotName();
100
        $denyReg = (empty($botName)) ? '' :
101
            '|\{\{bots ?\| ?(optout|deny)\=[^\}]*' . preg_quote($botName, '#') . '[^\}]*\}\}';
102
        return preg_match('#({{nobots}}|{{bots ?\| ?(optout|deny) ?= ?all ?}}' . $denyReg . ')#i', $text) > 0;
103
    }
104
105 1
    protected static function getBotOwner()
106
    {
107
        return getenv('BOT_OWNER');
108
    }
109
110
    /**
111
     * Throws Exception if "{{stop}}" or "STOP" on talk page.
112
     *
113
     * @throws StopActionException
114
     */
115
    public function checkStopOnTalkpageOrException(?bool $botTalk = false): void
116
    {
117
        if ($this->isLastCheckStopDateRecent()) {
118
            return;
119
        }
120
121
        // don't catch Exception (stop process if error)
122
        $pageAction = $this->getWikiBotPageAction();
123
        $text = $pageAction->getText() ?? '';
124
        $lastEditor = $pageAction->getLastEditor() ?? 'unknown';
125
126
        if (preg_match('#({{stop}}|{{Stop}}|STOP)#', $text) > 0) {
127
            echo date('Y-m-d H:i');
128
            echo sprintf("\n*** STOP ON TALK PAGE BY %s ***\n\n", $lastEditor);
129
            sleep(self::SLEEP_BEFORE_STOP_TALKPAGE);
130
131
            $this->sendSMSandFunnyTalk($lastEditor, $botTalk);
132
133
            throw new StopActionException();
134
        }
135
136
        $this->lastCheckStopDate = new DateTimeImmutable();
137
    }
138
139
    protected function isLastCheckStopDateRecent(): bool
140
    {
141
        $now = new DateTimeImmutable();
142
        $stopInterval = new DateInterval(self::TALK_STOP_CHECK_INTERVAL);
143
144
        return $this->lastCheckStopDate instanceof DateTimeImmutable
145
            && $now < DateTime::createFromImmutable($this->lastCheckStopDate)->add($stopInterval);
146
    }
147
148
    /**
149
     * @throws UsageException
150
     */
151
    protected function getWikiBotPageAction(): WikiPageAction
152
    {
153
        return new WikiPageAction($this->mediawikiFactory, $this->getBotTalkPageTitle());
154
    }
155
156
    protected function getBotTalkPageTitle(): string
157
    {
158
        return self::TALK_PAGE_PREFIX . $this::getBotName();
159
    }
160
161
    /**
162
     * @throws ConfigException
163
     */
164
    protected static function getBotName(): string
165
    {
166
        if (empty(getenv('BOT_NAME'))) {
167
            throw new ConfigException('BOT_NAME is not defined.');
168
        }
169
        return getenv('BOT_NAME') ?? '';
170
    }
171
172
    protected function sendSMSandFunnyTalk(string $lastEditor, ?bool $botTalk): void
173
    {
174
        $this->sendSMS($lastEditor);
175
176
        if ($botTalk) {
177
            $this->talkWithBot();
178
        }
179
    }
180
181
    protected function sendSMS(string $lastEditor): bool
182
    {
183
        if ($this->SMSClient instanceof SMSInterface) {
184
            try {
185
                return $this->SMSClient->send(sprintf('%s {stop} by %s', $this::getBotName(), $lastEditor));
186
            } catch (Throwable) {
187
                return false;
188
            }
189
        }
190
191
        return false;
192
    }
193
194
    protected function talkWithBot(): bool
195
    {
196
        if ($this instanceof TalkBotConfig) {
197
            try {
198
                return $this->botTalk();
199
            } catch (Throwable) {
200
                // do nothing
201
            }
202
        }
203
204
        return false;
205
    }
206
207
    /**
208
     * Is there a new message on the discussion page of the bot (or owner) ?
209
     * @throws ConfigException
210
     */
211
    public function checkWatchPages()
212
    {
213
        foreach ($this->getWatchPages() as $title => $lastTime) {
214
            $pageTime = $this->getTimestamp($title);
215
216
            // the page has been edited since last check ?
217
            if (!$pageTime || $pageTime !== $lastTime) {
218
                echo sprintf("WATCHPAGE '%s' has been edited since %s.\n", $title, $lastTime);
219
220
                // Ask? Mettre à jour $watchPages ?
221
                echo "Replace with $title => '$pageTime'";
222
223
                $this->checkExitOnWatchPage();
224
            }
225
        }
226
    }
227
228
    /**
229
     * @throws ConfigException
230
     */
231
    protected function getWatchPages(): array
232
    {
233
        if (!file_exists(static::WATCHPAGE_FILENAME)) {
234
            throw new ConfigException('No watchpage file found.');
235
        }
236
237
        try {
238
            $json = file_get_contents(static::WATCHPAGE_FILENAME);
239
            $array = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
240
        } catch (Throwable) {
241
            throw new ConfigException('Watchpage file malformed.');
242
        }
243
244
        return $array;
245
    }
246 4
247
    protected function getTimestamp(string $title): ?string
248 4
    {
249 4
        $page = new WikiPageAction($this->mediawikiFactory, $title);
250
251 4
        return $page->page->getRevisions()->getLatest()->getTimestamp();
252 2
    }
253
254
    protected function checkExitOnWatchPage(): void
255 2
    {
256
        if (static::EXIT_ON_CHECK_WATCHPAGE) {
257
            echo "EXIT_ON_CHECK_WATCHPAGE\n";
258
259
            throw new DomainException('exit from check watchpages');
260
        }
261
    }
262
263
    /**
264
     * How many minutes since last edit ? Do not to disturb human editors !
265 6
     * @return int minutes
266
     */
267 6
    public function minutesSinceLastEdit(string $title): int
268 5
    {
269 6
        $time = $this->getTimestamp($title);  // 2011-09-02T16:31:13Z
270
271 4
        return (int)round((time() - strtotime($time)) / 60);
272
    }
273
}
274