Passed
Push — master ( 69b6a3...2b67eb )
by Dispositif
02:36
created

WikiBotConfig::getCurrentGitCommitHash()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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