Skip to content

Commit b5f9a22

Browse files
committed
t3: Support multiblocks
Change-Id: I61d46860482bb7b8341d94bedb616e3f154eea99
1 parent 987e4d3 commit b5f9a22

File tree

2 files changed

+121
-81
lines changed

2 files changed

+121
-81
lines changed

majavahbot/tasks/task_3_bot_status.py

Lines changed: 100 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
2+
from dataclasses import dataclass
23
from datetime import datetime
3-
from typing import List, Optional
4+
from typing import Any, List, Optional
45

56
import mwparserfromhell
67
from pywikibot.data.api import QueryGenerator
@@ -52,18 +53,81 @@
5253
EMPTY_COLUMN = "{{center|—}}"
5354

5455

56+
def parse_date(string: str | None) -> datetime | None:
57+
if string is None:
58+
return None
59+
return datetime.strptime(string, MEDIAWIKI_DATE_FORMAT)
60+
61+
62+
def format_date(date: datetime | None, sortkey: bool = True) -> str:
63+
if date is None:
64+
return EMPTY_COLUMN
65+
66+
if sortkey:
67+
return 'class="nowrap" data-sort-value={} | {}'.format(
68+
date.strftime(MEDIAWIKI_DATE_FORMAT), date.strftime(HUMAN_DATE_FORMAT)
69+
)
70+
71+
return date.strftime(HUMAN_DATE_FORMAT)
72+
73+
74+
@dataclass
75+
class Block:
76+
id: int
77+
by: str
78+
reason: str
79+
at: str
80+
expiry: str
81+
partial: bool
82+
83+
def format(self) -> str:
84+
return "%s by {{no ping|%s}} on %s%s.<br/>Block reason is '%s{{'}}" % (
85+
"Partially blocked" if self.partial else "Blocked",
86+
self.by,
87+
format_date(parse_date(self.at), sortkey=False),
88+
(
89+
"to expire at {}".format(
90+
format_date(parse_date(self.expiry), sortkey=False)
91+
)
92+
if self.expiry != "infinite"
93+
else ""
94+
),
95+
self.format_reason(),
96+
)
97+
98+
def format_reason(self) -> str:
99+
return (
100+
self.reason.replace("[[Category:", "[[:Category:")
101+
.replace("[[category:", "[[:category:")
102+
.replace("{", "&#123;")
103+
.replace("<", "&lt;")
104+
.replace(">", "&gt;")
105+
)
106+
107+
@classmethod
108+
def parse(cls, data: dict[str, Any]) -> "Block":
109+
return cls(
110+
id=data["blockid"],
111+
by=data["blockedby"],
112+
reason=data["blockreason"],
113+
at=data["blockedtimestamp"],
114+
expiry=data["blockexpiry"],
115+
partial=data.get("blockpartial", False) is not False,
116+
)
117+
118+
55119
class BotStatusData:
56120
def __init__(
57121
self,
58122
*,
59123
name: str,
60-
operators: List[str],
61-
last_edit_timestamp: Optional[str],
62-
last_log_timestamp: Optional[str],
63-
last_operator_activity_timestamp: Optional[str],
64-
edit_count: Optional[int],
65-
groups: Optional[List[str]],
66-
block_data: Optional[dict],
124+
operators: list[str],
125+
last_edit_timestamp: str | None,
126+
last_log_timestamp: str | None,
127+
last_operator_activity_timestamp: datetime | None,
128+
edit_count: int | None,
129+
groups: list[str],
130+
blocks: list[Block],
67131
):
68132
self.name = name
69133
self.operators = set(operators)
@@ -74,16 +138,16 @@ def __init__(
74138
self.last_operator_activity_timestamp = None
75139

76140
if last_edit_timestamp is not None:
77-
self.last_edit_timestamp = self.parse_date(last_edit_timestamp)
141+
self.last_edit_timestamp = parse_date(last_edit_timestamp)
78142
self.last_activity_timestamp = self.last_edit_timestamp
79143

80144
if last_log_timestamp is not None:
81-
self.last_log_timestamp = self.parse_date(last_log_timestamp)
145+
self.last_log_timestamp = parse_date(last_log_timestamp)
82146
if self.last_edit_timestamp is None:
83147
self.last_activity_timestamp = self.last_log_timestamp
84148
else:
85149
self.last_activity_timestamp = max(
86-
self.last_log_timestamp, self.last_edit_timestamp
150+
self.last_log_timestamp, self.last_edit_timestamp # type: ignore
87151
)
88152

89153
if last_operator_activity_timestamp:
@@ -96,7 +160,7 @@ def __init__(
96160
else:
97161
self.groups = []
98162

99-
self.block_data = block_data
163+
self.blocks = blocks
100164

101165
@staticmethod
102166
def new_for_unknown(name: str) -> "BotStatusData":
@@ -107,8 +171,8 @@ def new_for_unknown(name: str) -> "BotStatusData":
107171
last_log_timestamp=None,
108172
last_operator_activity_timestamp=None,
109173
edit_count=None,
110-
groups=None,
111-
block_data=None,
174+
groups=[],
175+
blocks=[],
112176
)
113177

114178
def format_number(self, number: Optional[int], sortkey=True):
@@ -119,76 +183,34 @@ def format_number(self, number: Optional[int], sortkey=True):
119183
return 'class="nowrap" data-sort-value={} | {:,}'.format(number, number)
120184
return "{:,}".format(number)
121185

122-
def parse_date(self, string):
123-
if string is None:
124-
return None
125-
return datetime.strptime(string, MEDIAWIKI_DATE_FORMAT)
126-
127-
def format_date(self, date, sortkey=True):
128-
if date is None:
129-
return EMPTY_COLUMN
130-
131-
if sortkey:
132-
return 'class="nowrap" data-sort-value={} | {}'.format(
133-
date.strftime(MEDIAWIKI_DATE_FORMAT), date.strftime(HUMAN_DATE_FORMAT)
134-
)
135-
136-
return date.strftime(HUMAN_DATE_FORMAT)
137-
138-
def format_block_reason(self):
139-
return (
140-
self.block_data["reason"]
141-
.replace("[[Category:", "[[:Category:")
142-
.replace("[[category:", "[[:category:")
143-
.replace("{", "&#123;")
144-
.replace("<", "&lt;")
145-
.replace(">", "&gt;")
146-
)
147-
148-
def format_block(self):
149-
date = self.parse_date(self.block_data["at"])
150-
151-
return (
152-
"data-sort-value=%s | %s by {{no ping|%s}} on %s%s.<br/>Block reason is '%s{{'}}"
153-
% (
154-
date.strftime(MEDIAWIKI_DATE_FORMAT),
155-
"Partially blocked" if self.block_data["partial"] else "Blocked",
156-
self.block_data["by"],
157-
self.format_date(date, sortkey=False),
158-
(
159-
"to expire at {}".format(
160-
self.format_date(self.block_data["expiry"], sortkey=False)
161-
)
162-
if self.block_data["expiry"] != "infinite"
163-
else ""
164-
),
165-
self.format_block_reason(),
166-
)
167-
)
168-
169-
def format_extra_details(self):
186+
def format_extra_details(self) -> str:
170187
details = []
171188

172189
if len(self.groups) > 0:
173190
details.append("Extra groups: " + ", ".join(self.groups))
174-
if self.block_data is not None:
175-
details.append(self.format_block())
191+
if self.blocks:
192+
if len(self.blocks) == 1:
193+
details.append(self.blocks[0].format())
194+
else:
195+
details.append(
196+
"\n".join([f"* {block.format()}" for block in self.blocks])
197+
)
176198

177199
return "\n----\n".join(details)
178200

179-
def to_table_row(self):
201+
def to_table_row(self) -> str:
180202
return TABLE_ROW_FORMAT % (
181203
self.name,
182204
self.format_operators(),
183205
self.format_number(self.edit_count),
184-
self.format_date(self.last_activity_timestamp),
185-
self.format_date(self.last_edit_timestamp),
186-
self.format_date(self.last_log_timestamp),
187-
self.format_date(self.last_operator_activity_timestamp),
206+
format_date(self.last_activity_timestamp),
207+
format_date(self.last_edit_timestamp),
208+
format_date(self.last_log_timestamp),
209+
format_date(self.last_operator_activity_timestamp),
188210
self.format_extra_details(),
189211
)
190212

191-
def format_operators(self):
213+
def format_operators(self) -> str:
192214
if len(self.operators) == 0:
193215
return EMPTY_COLUMN
194216
return "{{no ping|" + "}}, {{no ping|".join(sorted(self.operators)) + "}}"
@@ -224,16 +246,13 @@ def get_bot_data(self, username):
224246
if "query" in data:
225247
data = data["query"]
226248

227-
block = None
228-
if "blockid" in data["users"][0]:
229-
block = {
230-
"id": data["users"][0]["blockid"],
231-
"by": data["users"][0]["blockedby"],
232-
"reason": data["users"][0]["blockreason"],
233-
"at": data["users"][0]["blockedtimestamp"],
234-
"expiry": data["users"][0]["blockexpiry"],
235-
"partial": "blockpartial" in data["users"][0],
236-
}
249+
blocks = []
250+
if "blockcomponents" in data["users"][0]:
251+
blocks = [
252+
Block.parse(block) for block in data["users"][0]["blockcomponents"]
253+
]
254+
elif "blockid" in data["users"][0]:
255+
blocks.append(Block.parse(data["users"][0]))
237256

238257
operators = []
239258
for page_id in data["pages"]:
@@ -322,7 +341,7 @@ def get_bot_data(self, username):
322341
last_operator_activity_timestamp=operator_activity,
323342
edit_count=data["users"][0]["editcount"],
324343
groups=data["users"][0]["groups"],
325-
block_data=block,
344+
blocks=blocks,
326345
)
327346

328347
raise Exception("Failed loading bot data for " + username + ": " + str(data))
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from majavahbot.tasks.task_3_bot_status import Block
2+
3+
4+
def test_Block_parse_format() -> None:
5+
block = Block.parse(
6+
{
7+
"blockid": 25197918,
8+
"blockedby": "Theleekycauldron",
9+
"blockedbyid": 32403560,
10+
"blockreason": "WugBot task 2 being replaced, [[Special:Diff/1298191373|permission given by operator]] to effect handoff via pblock",
11+
"blockedtimestamp": "2025-07-12T20:18:29Z",
12+
"blockexpiry": "infinite",
13+
"blockpartial": "",
14+
"blockedtimestampformatted": "23:18, 12 July 2025",
15+
}
16+
)
17+
18+
assert (
19+
block.format()
20+
== "Partially blocked by {{no ping|Theleekycauldron}} on 12 Jul 2025.<br/>Block reason is 'WugBot task 2 being replaced, [[Special:Diff/1298191373|permission given by operator]] to effect handoff via pblock{{'}}"
21+
)

0 commit comments

Comments
 (0)