Passed
Push — master ( d76cad...d644e9 )
by Dispositif
14:09
created

WikiBotConfig::sleepAndExitOnBotDay()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 4
c 1
b 0
f 1
dl 0
loc 6
ccs 0
cts 2
cp 0
rs 10
cc 2
nc 2
nop 0
crap 6
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 App\Infrastructure\Monitor\NullLogger;
17
use DateInterval;
18
use DateTime;
19
use DateTimeImmutable;
20
use DomainException;
21
use Mediawiki\Api\MediawikiFactory;
22
use Mediawiki\Api\UsageException;
23
use Psr\Log\LoggerInterface;
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
    protected const BOT_DAY = '2306'; // 23 june
50
51
    protected string $taskName = 'Amélioration';
52
    /**
53
     * @var LoggerInterface
54
     */
55
    protected $log;
56
    /**
57
     * @var DateTimeImmutable
58
     */
59
    protected $lastCheckStopDate;
60
    protected SMSInterface|null $SMSClient;
61
    protected $mediawikiFactory;
62
    protected ?string $gitCommitHash = null;
63
64
    public function __construct(MediawikiFactory $mediawikiFactory, ?LoggerInterface $logger = null, ?SMSInterface $SMSClient = null)
65
    {
66
        $this->log = $logger ?? new NullLogger();
67
        ini_set('user_agent', getenv('USER_AGENT'));
68
        $this->SMSClient = $SMSClient;
69
        $this->mediawikiFactory = $mediawikiFactory;
70
    }
71
72
    /**
73
     * Detect wiki-templates restricting the edition on a frwiki page.
74
     */
75
    public static function isEditionTemporaryRestrictedOnWiki(?string $text, ?string $botName = null): bool
76
    {
77
        return empty($text)
78
            || preg_match('#{{Formation#i', $text) > 0
79
            || preg_match('#{{En travaux#i', $text) > 0
80
            || preg_match('#{{En cours#i', $text) > 0
81
            || preg_match('#{{Protection#i', $text) > 0
82
            || preg_match('#\{\{(R3R|Règle des 3 révocations|travaux|en travaux|en cours|formation)#i', $text) > 0
83
            || self::isNoBotTag($text, $botName);
84
    }
85
86
    /**
87
     * Detect {{nobots}}, {{bots|deny=all}}, {{bots|deny=MyBot,BobBot}}.
88
     * Relevant out of the "main" wiki-namespace (talk pages, etc).
89
     */
90
    protected static function isNoBotTag(string $text, ?string $botName = null): bool
91
    {
92 1
        $text = WikiTextUtil::removeHTMLcomments($text);
93
        $botName = $botName ?: self::getBotName();
94 1
        $denyReg = (empty($botName)) ? '' :
95
            '|\{\{bots ?\| ?(optout|deny)\=[^\}]*' . preg_quote($botName, '#') . '[^\}]*\}\}';
96
        return preg_match('#({{nobots}}|{{bots ?\| ?(optout|deny) ?= ?all ?}}' . $denyReg . ')#i', $text) > 0;
97 1
    }
98 1
99 1
    /**
100
     * @throws ConfigException
101
     */
102
    public static function getBotName(): string
103
    {
104
        if (empty(getenv('BOT_NAME'))) {
105 1
            throw new ConfigException('BOT_NAME is not defined.');
106
        }
107
        return getenv('BOT_NAME') ?? '';
108
    }
109
110
    protected static function getBotOwner()
111
    {
112
        return getenv('BOT_OWNER');
113
    }
114
115
    public function getTaskName(): string
116
    {
117
        return $this->taskName;
118
    }
119
120
    public function setTaskName(string $taskName): WikiBotConfig
121
    {
122
        $this->taskName = $taskName;
123
        return $this;
124
    }
125
126
    public function getCurrentGitCommitHash(): ?string
127
    {
128
        if ($this->gitCommitHash) {
129
            return $this->gitCommitHash;
130
        }
131
        $path = __DIR__ . '/../../.git/';
132
        if (!file_exists($path)) {
133
            return null;
134
        }
135
        $head = trim(substr(file_get_contents($path . 'HEAD'), 4));
136
        $hash = trim(file_get_contents(sprintf($path . $head)));
137
        $this->gitCommitHash = $hash; // cached
138
139
        return $hash;
140
    }
141
142
    public function getLogger(): LoggerInterface
143
    {
144
        return $this->log;
145
    }
146
147
    /**
148
     * Throws Exception if "{{stop}}" or "STOP" on talk page.
149
     * @throws StopActionException
150
     */
151
    public function checkStopOnTalkpageOrException(?bool $botTalk = false): void
152
    {
153
        if ($this->isLastCheckStopDateRecent()) {
154
            return;
155
        }
156
        $this->sleepAndExitOnBotDay();
157
158
        // don't catch Exception (stop process if error)
159
        $pageAction = $this->getWikiBotPageAction();
160
        $text = $pageAction->getText() ?? '';
161
        $lastEditor = $pageAction->getLastEditor() ?? 'unknown';
162
163
        if (preg_match('#({{stop}}|{{Stop}}|STOP)#', $text) > 0) {
164
            echo date('Y-m-d H:i');
165
            echo sprintf("\n*** STOP ON TALK PAGE BY %s ***\n\n", $lastEditor);
166
            sleep(self::SLEEP_BEFORE_STOP_TALKPAGE);
167
168
            $this->sendSMSandFunnyTalk($lastEditor, $botTalk);
169
170
            throw new StopActionException();
171
        }
172
173
        $this->lastCheckStopDate = new DateTimeImmutable();
174
    }
175
176
    /**
177
     * Exit script on annual bot day.
178
     */
179
    public function sleepAndExitOnBotDay(): void
180
    {
181
        if (date('dm') === self::BOT_DAY) {
182
            echo "BOT DAY ! Sleep before exit. \n";
183
            sleep(60 * 60);
184
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
185
        }
186
    }
187
188
    protected function isLastCheckStopDateRecent(): bool
189
    {
190
        $now = new DateTimeImmutable();
191
        $stopInterval = new DateInterval(self::TALK_STOP_CHECK_INTERVAL);
192
193
        return $this->lastCheckStopDate instanceof DateTimeImmutable
194
            && $now < DateTime::createFromImmutable($this->lastCheckStopDate)->add($stopInterval);
195
    }
196
197
    /**
198
     * @throws UsageException
199
     */
200
    protected function getWikiBotPageAction(): WikiPageAction
201
    {
202
        return new WikiPageAction($this->mediawikiFactory, $this->getBotTalkPageTitle());
203
    }
204
205
    protected function getBotTalkPageTitle(): string
206
    {
207
        return self::TALK_PAGE_PREFIX . $this::getBotName();
208
    }
209
210
    protected function sendSMSandFunnyTalk(string $lastEditor, ?bool $botTalk): void
211
    {
212
        $this->sendSMS($lastEditor);
213
214
        if ($botTalk) {
215
            $this->talkWithBot();
216
        }
217
    }
218
219
    protected function sendSMS(string $lastEditor): bool
220
    {
221
        if ($this->SMSClient instanceof SMSInterface) {
222
            try {
223
                return $this->SMSClient->send(sprintf('%s {stop} by %s', $this::getBotName(), $lastEditor));
224
            } catch (Throwable) {
225
                return false;
226
            }
227
        }
228
229
        return false;
230
    }
231
232
    protected function talkWithBot(): bool
233
    {
234
        if ($this instanceof TalkBotConfig) {
235
            try {
236
                return $this->botTalk();
237
            } catch (Throwable) {
238
                // do nothing
239
            }
240
        }
241
242
        return false;
243
    }
244
245
    /**
246 4
     * Is there a new message on the discussion page of the bot (or owner) ?
247
     * @throws ConfigException
248 4
     */
249 4
    public function checkWatchPages()
250
    {
251 4
        foreach ($this->getWatchPages() as $title => $lastTime) {
252 2
            $pageTime = $this->getTimestamp($title);
253
254
            // the page has been edited since last check ?
255 2
            if (!$pageTime || $pageTime !== $lastTime) {
256
                echo sprintf("WATCHPAGE '%s' has been edited since %s.\n", $title, $lastTime);
257
258
                // Ask? Mettre à jour $watchPages ?
259
                echo "Replace with $title => '$pageTime'";
260
261
                $this->checkExitOnWatchPage();
262
            }
263
        }
264
    }
265 6
266
    /**
267 6
     * @throws ConfigException
268 5
     */
269 6
    protected function getWatchPages(): array
270
    {
271 4
        if (!file_exists(static::WATCHPAGE_FILENAME)) {
272
            throw new ConfigException('No watchpage file found.');
273
        }
274 2
275
        try {
276
            $json = file_get_contents(static::WATCHPAGE_FILENAME);
277
            $array = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
278
        } catch (Throwable) {
279
            throw new ConfigException('Watchpage file malformed.');
280
        }
281
282
        return $array;
283
    }
284
285
    protected function getTimestamp(string $title): ?string
286
    {
287
        $page = new WikiPageAction($this->mediawikiFactory, $title);
288
289
        return $page->page->getRevisions()->getLatest()->getTimestamp();
290
    }
291
292
    protected function checkExitOnWatchPage(): void
293
    {
294
        if (static::EXIT_ON_CHECK_WATCHPAGE) {
295
            echo "EXIT_ON_CHECK_WATCHPAGE\n";
296
297
            throw new DomainException('exit from check watchpages');
298
        }
299
    }
300
301
    /**
302
     * How many minutes since last edit ? Do not to disturb human editors !
303
     * @return int minutes
304
     */
305
    public function minutesSinceLastEdit(string $title): int
306
    {
307
        $time = $this->getTimestamp($title);  // 2011-09-02T16:31:13Z
308
309
        return (int)round((time() - strtotime($time)) / 60);
310
    }
311
}
312