Passed
Branch dev3 (71ef0d)
by Dispositif
03:09
created

WikiBotConfig::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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