Issues (106)

src/Application/WikiBotConfig.php (1 issue)

Severity
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.2';
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
    public const DELAY_MINUTES_AFTER_HUMAN_EDIT = 15;
50
51
    protected const BOT_DAY = '2306'; // 23 june
52
53
    protected string $taskName = 'Amélioration';
54
    /**
55
     * @var LoggerInterface
56
     */
57
    protected $log;
58
    /**
59
     * @var DateTimeImmutable
60
     */
61
    protected $lastCheckStopDate;
62
    protected SMSInterface|null $SMSClient;
63
    protected $mediawikiFactory;
64
    protected ?string $gitCommitHash = null;
65
66
    public function __construct(MediawikiFactory $mediawikiFactory, ?LoggerInterface $logger = null, ?SMSInterface $SMSClient = null)
67
    {
68
        $this->log = $logger ?? new NullLogger();
69
        ini_set('user_agent', getenv('USER_AGENT'));
70
        $this->SMSClient = $SMSClient;
71
        $this->mediawikiFactory = $mediawikiFactory;
72
    }
73
74
    /**
75
     * Detect wiki-templates restricting the edition on a frwiki page.
76
     */
77
    public static function isEditionTemporaryRestrictedOnWiki(?string $text, ?string $botName = null): bool
78
    {
79
        return empty($text)
80
            || preg_match('#{{Formation#i', $text) > 0
81
            || preg_match('#{{En travaux#i', $text) > 0
82
            || preg_match('#{{En cours#i', $text) > 0
83
            || preg_match('#{{Protection#i', $text) > 0
84
            || preg_match('#\{\{(R3R|Règle des 3 révocations|travaux|en travaux|en cours|formation)#i', $text) > 0
85
            || self::isNoBotTag($text, $botName);
86
    }
87
88
    /**
89
     * Detect {{nobots}}, {{bots|deny=all}}, {{bots|deny=MyBot,BobBot}}.
90
     * Relevant out of the "main" wiki-namespace (talk pages, etc).
91
     */
92 1
    protected static function isNoBotTag(string $text, ?string $botName = null): bool
93
    {
94 1
        $text = WikiTextUtil::removeHTMLcomments($text);
95
        $botName = $botName ?: self::getBotName();
96
        $denyReg = (empty($botName)) ? '' :
97 1
            '|\{\{bots ?\| ?(optout|deny)\=[^\}]*' . preg_quote($botName, '#') . '[^\}]*\}\}';
98 1
        return preg_match('#({{nobots}}|{{bots ?\| ?(optout|deny) ?= ?all ?}}' . $denyReg . ')#i', $text) > 0;
99 1
    }
100
101
    /**
102
     * @throws ConfigException
103
     */
104
    public static function getBotName(): string
105 1
    {
106
        if (empty(getenv('BOT_NAME'))) {
107
            throw new ConfigException('BOT_NAME is not defined.');
108
        }
109
        return getenv('BOT_NAME') ?? '';
110
    }
111
112
    protected static function getBotOwner()
113
    {
114
        return getenv('BOT_OWNER');
115
    }
116
117
    public function getTaskName(): string
118
    {
119
        return $this->taskName;
120
    }
121
122
    public function setTaskName(string $taskName): WikiBotConfig
123
    {
124
        $this->taskName = $taskName;
125
        return $this;
126
    }
127
128
    public function getCurrentGitCommitHash(): ?string
129
    {
130
        if ($this->gitCommitHash) {
131
            return $this->gitCommitHash;
132
        }
133
        $path = __DIR__ . '/../../.git/';
134
        if (!file_exists($path)) {
135
            return null;
136
        }
137
        $head = trim(substr(file_get_contents($path . 'HEAD'), 4));
138
        $hash = trim(file_get_contents(sprintf($path . $head)));
139
        $this->gitCommitHash = $hash; // cached
140
141
        return $hash;
142
    }
143
144
    public function getLogger(): LoggerInterface
145
    {
146
        return $this->log;
147
    }
148
149
    /**
150
     * Throws Exception if "{{stop}}" or "STOP" on talk page.
151
     * @throws StopActionException
152
     */
153
    public function checkStopOnTalkpageOrException(?bool $botTalk = false): void
154
    {
155
        if ($this->isLastCheckStopDateRecent()) {
156
            return;
157
        }
158
        $this->sleepAndExitOnBotDay();
159
160
        // don't catch Exception (stop process if error)
161
        $pageAction = $this->getWikiBotPageAction();
162
        $text = $pageAction->getText() ?? '';
163
        $lastEditor = $pageAction->getLastEditor() ?? 'unknown';
164
165
        if (preg_match('#({{stop}}|{{Stop}}|STOP)#', $text) > 0) {
166
            echo date('Y-m-d H:i');
167
            echo sprintf("\n*** STOP ON TALK PAGE BY %s ***\n\n", $lastEditor);
168
            sleep(self::SLEEP_BEFORE_STOP_TALKPAGE);
169
170
            $this->sendSMSandFunnyTalk($lastEditor, $botTalk);
171
172
            throw new StopActionException();
173
        }
174
175
        $this->lastCheckStopDate = new DateTimeImmutable();
176
    }
177
178
    /**
179
     * Exit script on annual bot day.
180
     */
181
    public function sleepAndExitOnBotDay(): void
182
    {
183
        if (date('dm') === self::BOT_DAY) {
184
            echo "BOT DAY ! Sleep before exit. \n";
185
            sleep(60 * 60);
186
            exit;
0 ignored issues
show
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...
187
        }
188
    }
189
190
    protected function isLastCheckStopDateRecent(): bool
191
    {
192
        $now = new DateTimeImmutable();
193
        $stopInterval = new DateInterval(self::TALK_STOP_CHECK_INTERVAL);
194
195
        return $this->lastCheckStopDate instanceof DateTimeImmutable
196
            && $now < DateTime::createFromImmutable($this->lastCheckStopDate)->add($stopInterval);
197
    }
198
199
    /**
200
     * @throws UsageException
201
     */
202
    protected function getWikiBotPageAction(): WikiPageAction
203
    {
204
        return new WikiPageAction($this->mediawikiFactory, $this->getBotTalkPageTitle());
205
    }
206
207
    protected function getBotTalkPageTitle(): string
208
    {
209
        return self::TALK_PAGE_PREFIX . $this::getBotName();
210
    }
211
212
    protected function sendSMSandFunnyTalk(string $lastEditor, ?bool $botTalk): void
213
    {
214
        $this->sendSMS($lastEditor);
215
216
        if ($botTalk) {
217
            $this->talkWithBot();
218
        }
219
    }
220
221
    protected function sendSMS(string $lastEditor): bool
222
    {
223
        if ($this->SMSClient instanceof SMSInterface) {
224
            try {
225
                return $this->SMSClient->send(sprintf('%s {stop} by %s', $this::getBotName(), $lastEditor));
226
            } catch (Throwable) {
227
                return false;
228
            }
229
        }
230
231
        return false;
232
    }
233
234
    protected function talkWithBot(): bool
235
    {
236
        if ($this instanceof TalkBotConfig) {
237
            try {
238
                return $this->botTalk();
239
            } catch (Throwable) {
240
                // do nothing
241
            }
242
        }
243
244
        return false;
245
    }
246 4
247
    /**
248 4
     * Is there a new message on the discussion page of the bot (or owner) ?
249 4
     * @throws ConfigException
250
     */
251 4
    public function checkWatchPages()
252 2
    {
253
        foreach ($this->getWatchPages() as $title => $lastTime) {
254
            $pageTime = $this->getTimestamp($title);
255 2
256
            // the page has been edited since last check ?
257
            if (!$pageTime || $pageTime !== $lastTime) {
258
                echo sprintf("WATCHPAGE '%s' has been edited since %s.\n", $title, $lastTime);
259
260
                // Ask? Mettre à jour $watchPages ?
261
                echo "Replace with $title => '$pageTime'";
262
263
                $this->checkExitOnWatchPage();
264
            }
265 6
        }
266
    }
267 6
268 5
    /**
269 6
     * @throws ConfigException
270
     */
271 4
    protected function getWatchPages(): array
272
    {
273
        if (!file_exists(static::WATCHPAGE_FILENAME)) {
274 2
            throw new ConfigException('No watchpage file found.');
275
        }
276
277
        try {
278
            $json = file_get_contents(static::WATCHPAGE_FILENAME);
279
            $array = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
280
        } catch (Throwable) {
281
            throw new ConfigException('Watchpage file malformed.');
282
        }
283
284
        return $array;
285
    }
286
287
    protected function getTimestamp(string $title): ?string
288
    {
289
        $page = new WikiPageAction($this->mediawikiFactory, $title);
290
291
        return $page->page->getRevisions()->getLatest()->getTimestamp();
292
    }
293
294
    protected function checkExitOnWatchPage(): void
295
    {
296
        if (static::EXIT_ON_CHECK_WATCHPAGE) {
297
            echo "EXIT_ON_CHECK_WATCHPAGE\n";
298
299
            throw new DomainException('exit from check watchpages');
300
        }
301
    }
302
303
    /**
304
     * How many minutes since last edit ? Do not to disturb human editors !
305
     * @return int minutes
306
     */
307
    public function minutesSinceLastEdit(string $title): int
308
    {
309
        $time = $this->getTimestamp($title);  // 2011-09-02T16:31:13Z
310
311
        return (int)round((time() - strtotime($time)) / 60);
312
    }
313
}
314