Usuari:SignaBot/scripts/cawiki/signbot.py
Aparença
"""
(!C) 2023 Coet.
Copyleft: Feu el que vos convinga en sóc l'autor però decline tota responsabilitat i no m'importa el que feu amb
la integralitat o alguna part del codi. Si el conjunt o part d'eines, classes i mètodes, vos resulten útils i
podeu tornar a gastar-los no us perseguiré demanant-ne res i no m'importa si heu de canviar la llicència o autoria :)
OBJECTIU: obtenir darrera data a una secció sense signatures per a que [[Usuari:ArxivaBot|ArxivaBot]] puga arxivar la
secció.
Primera execució: [[Special:Diff/31301378]]
"""
import locale
import pytz
import re
import time
from datetime import datetime
from typing import Dict, List, Optional, Tuple, NoReturn
from pywikibot import Site, Page, Timestamp
# globals
site = Site('ca', 'wikipedia', 'SignaBot')
locale.setlocale(locale.LC_ALL, 'Catalan_Andorra.1250')
local_tz = pytz.timezone('Europe/Andorra')
class Thread:
"""
Classe per a una secció.
Obtenim títol, contingut i primera signatura. S'instància només amb el títol i el contingut.
"""
def __init__(self, title, body):
self.title: str = title
self.body: str = body
self.first_date: Optional[datetime] = None
class Unsigned:
"""
Classe per a una pàgina contenint alguna secció sense signar.
Retenim la pàgina on es troba el fil que no conté cap signatura i dos atributs més: el fil en qüestió i l'anterior.
El fet d'agafar l'anterior és sols orientatiu, per reduir les cerques, d'este fil ens interessa la darrera data,
l'utilitzarem per no eternitzar la recerca.
"""
def __init__(self, page, prev, cur):
self.page: Optional[Page] = page
self.prev_thread: Optional[Thread] = prev
self.cur_thread: Optional[Thread] = cur
class Months:
"""
Classe per a relacionar els mesos de la VP amb els del Python (del sitema operatiu en ús).
Tenim febrer que és `feb´ a VP i `febr.´ al Windows, també: `ago´ és `ag.´. I en este mòdul necesitem la
correspondència tant en un sentit com en l'altre.
"""
wiki_months = [short for long, short in site.months_names]
system_months = [time.strftime("%b", time.strptime(f"01-{_:>02}-2023", "%d-%m-%Y")) for _ in range(1, 13)]
@classmethod
def to_wiki(cls, sys_month: str) -> str:
index = cls.system_months.index(sys_month)
return cls.wiki_months[index]
@classmethod
def to_system(cls, wiki_month: str):
index = cls.wiki_months.index(wiki_month)
return cls.system_months[index]
class SignBot:
"""
L'objectiu d'esta classe és comprovar que els fils tinguen almenys una data en la qual el bot es puga basar per a
arxivar el fil.
Quan ens trobem amb un fil que no conté cap data, agafem l'historial de la pàgina i cerquem als resums d'edició
els que continguen el títol de la secció.
Coses que negligem:
- que un usuari esborre el títol en el qual ha fet un comentari.
- que la darrera data del comentari siga posterior, això faria que el bot no puguera accedir a la informació
"""
def __init__(self):
self.page: Optional[Page] = None
self.target_pages: List[Page] = []
self.undated: List[Unsigned] = []
@staticmethod
def get_datetime(dt: Tuple[str, str, str, str]) -> datetime:
"""
D'una tupla, resultat d'una expressió regular, obtenim la data de la signatura i hem de
processar el mes que no correspon al mes que Python té. Amb això retornem un objecte
de tipus datetime, que ens servirà per a calcular, ordenar, etc.
:param dt:
:return:
"""
time_str, day, month, year = dt
dt_str = f'{time_str}, {day} {Months.to_system(month)} {year}'
return datetime.strptime(dt_str, '%H:%M, %d %b %Y')
def get_first_date(self, body):
"""
Extraem la darrera data de les signatures que hi ha al fil.
:param body:
:return:
"""
dates = re.findall(r'(?P<time>\d{2}:\d{2}), (?P<day>\d{1,2}) (?P<month>\w{3,4}) (?P<year>\d{4})', body)
dates = [self.get_datetime(dt) for dt in dates]
dates.sort()
return dates[0] if dates else None
def get_dates(self, threads: Dict[str, Thread]) -> NoReturn:
"""
Retenim la darrera data del fil. Si el fil no té cap data, l'afegim a undated que serà una llista d'objectes
de la classe Unsigned.
:param threads:
:return:
"""
previous_thread = None
for thread in threads:
thread = threads[thread]
thread.first_date = self.get_first_date(thread.body)
if not thread.first_date:
self.undated.append(Unsigned(self.page, prev=previous_thread, cur=thread))
previous_thread = thread
@staticmethod
def get_threads(page: Page) -> Dict[str, Thread]:
"""
Convertim els fils de discussió en un diccionari de Threads
:param page: Page
:return: Dict[str, Thread]
"""
content = page.get(force=True)
split_text = re.split(r"\n== (.*?) ==\n*", content)
threads = {title: Thread(title, body) for title, body in zip(split_text[1::2], split_text[2::2])}
return threads
def survey_pages(self) -> NoReturn:
for page in self.target_pages:
self.page = page
threads = self.get_threads(page)
if threads:
self.get_dates(threads)
print(page.title(), len(threads))
@staticmethod
def get_local_timestamp(timestamp: Timestamp) -> str:
"""
El Timestamp que ens retorna el pywikibot és amb l'hora del servidor (UTC), li hem de sumar una o dues
hores segons el nostre horari.
:param timestamp: pywikibot.Timestamp
:return: str
"""
tz_name = local_tz.tzname(timestamp)
timestamp = timestamp + local_tz.utcoffset(timestamp)
dt_str = timestamp.strftime(f"%H:%M, %d %b %Y ({tz_name})")
syst_month = timestamp.strftime(f"%b")
wiki_month = Months.to_wiki(syst_month)
return dt_str.replace(syst_month, wiki_month)
def sign(self) -> NoReturn:
"""
Hem trobat un fil sense signatures, cerquem el primer resum d'edició que continga el títol de la secció
i inserim la plantilla {{sense signar}}
* Assumim que el primer resum d'edició trobat és el darrer comentari deixat (que deuria ser el de la plantilla
{{tancat}}, {{fet}} o {{no fet}}
"""
for undated in self.undated:
page = undated.page
title = undated.cur_thread.title
first_date = undated.prev_thread.first_date
for loops, rev in enumerate(page.revisions(endtime=first_date), 1):
if title in rev.comment:
old_content = undated.cur_thread.body
dt_str = self.get_local_timestamp(rev.timestamp)
new_content = f'{old_content}{{{{subst:sense signar|{rev.user}|{dt_str}}}}}\n'
old_text = page.get(force=True)
new_text = old_text.replace(old_content, new_content)
page.put(
new_text,
summary=f"Bot: afegint signatura de l'usuari {rev.user} a secció /* {title} */ - "
f"[[Special:Diff/{rev.revid}]]. S'ha hagut de remenejar {loops} revisions :P"
)
break
def run(self):
titles = (
"Canvi de nom d'usuari", "Bots/Sol·licituds", "Petició de marca de bot", "Petició als administradors",
"Sala dels administradors"
)
self.target_pages = [Page(site, f'Viquipèdia:{title}') for title in titles]
self.survey_pages()
self.sign()
if __name__ == '__main__':
signabot = SignBot()
signabot.run()