Passed
Push — master ( a1929c...27d925 )
by Dispositif
02:28
created

WikiBotConfig::getBotName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 6
ccs 0
cts 1
cp 0
crap 6
rs 10
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\Domain\Exceptions\ConfigException;
13
use App\Domain\Exceptions\StopActionException;
14
use App\Domain\Utils\WikiTextUtil;
15
use App\Infrastructure\Logger;
16
use App\Infrastructure\ServiceFactory;
17
use App\Infrastructure\SMS;
18
use DateInterval;
19
use DateTime;
20
use DateTimeImmutable;
21
use DomainException;
22
use Exception;
23
use Mediawiki\Api\UsageException;
24
use Psr\Log\LoggerInterface;
25
use Throwable;
26
27
/**
28
 * Define wiki configuration of the bot.
29
 * See also .env file for parameters.
30
 * Class WikiBotConfig.
31
 */
32
class WikiBotConfig
33
{
34
    public const VERSION = '1.0';
35
36
    public const WATCHPAGE_FILENAME = __DIR__ . '/resources/watch_pages.json';
37
38
    public const EXIT_ON_CHECK_WATCHPAGE = false;
39
40
    // do not stop if they play with {stop} on bot talk page
41
    public const BLACKLIST_EDITOR = ['OrlodrimBot'];
42
43
    public const BOT_FLAG = false;
44
45
    public const MODE_AUTO = false;
46
47
    public const EXIT_ON_WIKIMESSAGE = true;
48
49
    public const EDIT_LAPS = 20;
50
51
    public const EDIT_LAPS_MANUAL = 20;
52
53
    public const EDIT_LAPS_AUTOBOT = 60;
54
55
    public const EDIT_LAPS_FLAGBOT = 8;
56
57
    public const TALK_STOP_CHECK_INTERVAL = 'PT2M';
58
59
    public const TALK_PAGE_PREFIX = 'Discussion_utilisateur:';
60
61
    public $taskName = 'Améliorations indéfinie';
62
63
    /**
64
     * @var DateTimeImmutable
65
     */
66
    protected $lastCheckStopDate;
67
    /**
68
     * @var LoggerInterface
69
     */
70
    public $log;
71
72
    public function __construct(?LoggerInterface $logger = null)
73
    {
74
        $this->log = $logger ?: new Logger();
75
        ini_set('user_agent', getenv('USER_AGENT'));
76
    }
77
78
    /**
79
     * Throws Exception if "{{stop}}" or "STOP" on talk page.
80
     * @throws StopActionException|UsageException
81
     */
82
    public function checkStopOnTalkpage(?bool $botTalk = false): void
83
    {
84
        if ($this->isLastCheckStopDateRecent()) {
85
            return;
86
        }
87
88
        // don't catch Exception (stop process if error)
89
        $pageAction = $this->getWikiBotPageAction();
90
        $text = $pageAction->getText() ?? '';
91
        $lastEditor = $pageAction->getLastEditor() ?? 'unknown';
92 1
93
        if (preg_match('#({{stop}}|{{Stop}}|STOP)#', $text) > 0) {
94 1
            echo date('Y-m-d H:i');
95
            echo sprintf("\n*** STOP ON TALK PAGE BY %s ***\n\n", $lastEditor);
96
97 1
            $this->sendSMSandFunnyTalk($lastEditor, $botTalk);
98 1
99 1
            throw new StopActionException();
100
        }
101
102
        $this->lastCheckStopDate = new DateTimeImmutable;
103
    }
104
105 1
    /**
106
     * Is there a new message on the discussion page of the bot (or owner) ?
107
     * @throws ConfigException
108
     */
109
    public function checkWatchPages()
110
    {
111
        foreach ($this->getWatchPages() as $title => $lastTime) {
112
            $pageTime = $this->getTimestamp($title);
113
114
            // the page has been edited since last check ?
115
            if (!$pageTime || $pageTime !== $lastTime) {
116
                echo sprintf("WATCHPAGE '%s' has been edited since %s.\n", $title, $lastTime);
117
118
                // Ask? Mettre à jour $watchPages ?
119
                echo "Replace with $title => '$pageTime'";
120
121
                $this->checkExitOnWatchPage();
122
            }
123
        }
124
    }
125
126
    /**
127
     * @throws ConfigException
128
     */
129
    protected function getWatchPages(): array
130
    {
131
        if (!file_exists(static::WATCHPAGE_FILENAME)) {
132
            throw new ConfigException('No watchpage file found.');
133
        }
134
135
        try {
136
            $json = file_get_contents(static::WATCHPAGE_FILENAME);
137
            $array = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
138
        } catch (Throwable $e) {
139
            throw new ConfigException('Watchpage file malformed.');
140
        }
141
142
        return $array;
143
    }
144
145
    protected function getTimestamp(string $title): ?string
146
    {
147
        $wiki = ServiceFactory::wikiApi();
148
        $page = new WikiPageAction($wiki, $title);
149
150
        return $page->page->getRevisions()->getLatest()->getTimestamp();
151
    }
152
153
    /**
154
     * How many minutes since last edit ? Do not to disturb human editors !
155
     * @return int minutes
156
     */
157
    public function minutesSinceLastEdit(string $title): int
158
    {
159
        $time = $this->getTimestamp($title);  // 2011-09-02T16:31:13Z
160
161
        return (int)round((time() - strtotime($time)) / 60);
162
    }
163
164
    /**
165
     * Detect {{nobots}}, {{bots|deny=all}}, {{bots|deny=MyBot,BobBot}}.
166
     * Relevant out of the "main" wiki-namespace (talk pages, etc).
167
     */
168
    protected static function isNoBotTag(string $text, ?string $botName = null): bool
169
    {
170
        $text = WikiTextUtil::removeHTMLcomments($text);
171
        $botName = $botName ?: self::getBotName();
172
        $denyReg = (empty($botName)) ? '' :
173
            '|\{\{bots ?\| ?(optout|deny)\=[^\}]*' . preg_quote($botName, '#') . '[^\}]*\}\}';
174
        return preg_match('#({{nobots}}|{{bots ?\| ?(optout|deny) ?= ?all ?}}' . $denyReg . ')#i', $text) > 0;
175
    }
176
177
    /**
178
     * Detect wiki-templates restricting the edition on a frwiki page.
179
     */
180
    public static function isEditionTemporaryRestrictedOnWiki(string $text, ?string $botName = null): bool
181
    {
182
        // travaux|en travaux| ??
183
        return preg_match('#{{Protection#i', $text) > 0
184
            || preg_match('#\{\{(R3R|Règle des 3 révocations|travaux|en travaux|en cours|formation)#i', $text) > 0
185
            || self::isNoBotTag($text, $botName);
186
    }
187
188
    /**
189
     * @throws ConfigException
190
     */
191
    protected static function getBotName(): string
192
    {
193
        if (empty(getenv('BOT_NAME'))) {
194
            throw new ConfigException('BOT_NAME is not defined.');
195
        }
196
        return getenv('BOT_NAME') ?? '';
197
    }
198
199
    protected static function getBotOwner()
200
    {
201
        return getenv('BOT_OWNER');
202
    }
203
204
    protected function isLastCheckStopDateRecent(): bool
205
    {
206
        $now = new DateTimeImmutable();
207
        $stopInterval = new DateInterval(self::TALK_STOP_CHECK_INTERVAL);
208
        if (
209
            $this->lastCheckStopDate instanceof DateTimeImmutable
210
            && $now < DateTime::createFromImmutable($this->lastCheckStopDate)->add($stopInterval)
211
        ) {
212
            return true;
213
        }
214
215
        return false;
216
    }
217
218
    /**
219
     * @throws UsageException
220
     */
221
    protected function getWikiBotPageAction(): WikiPageAction
222
    {
223
        $wiki = ServiceFactory::wikiApi();
224
225
        return new WikiPageAction($wiki, $this->getBotTalkPageTitle());
226
    }
227
228
    protected function getBotTalkPageTitle(): string
229
    {
230
        return self::TALK_PAGE_PREFIX . $this::getBotName();
231
    }
232
233
    protected function sendSMSandFunnyTalk(string $lastEditor, ?bool $botTalk): void
234
    {
235
        if (class_exists(SMS::class)) {
236
            try {
237
                new SMS($this::getBotName() . ' {stop} by ' . $lastEditor);
238
            } catch (Exception $smsException) {
239
                unset($smsException);
240
            }
241
        }
242
        if ($botTalk && class_exists(TalkBotConfig::class)) {
243
            try {
244
                (new TalkBotConfig())->botTalk();
245
            } catch (Throwable $botTalkException) {
246 4
                unset($botTalkException);
247
            }
248 4
        }
249 4
    }
250
251 4
    protected function checkExitOnWatchPage(): void
252 2
    {
253
        if (static::EXIT_ON_CHECK_WATCHPAGE) {
254
            echo "EXIT_ON_CHECK_WATCHPAGE\n";
255 2
256
            throw new DomainException('exit from check watchpages');
257
        }
258
    }
259
}
260