Passed
Branch dev3 (ae391d)
by Dispositif
02:29
created

isEditionTemporaryRestrictedOnWiki()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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