#!/usr/bin/python # -*- coding: utf-8 -*- # # AfdlBot - Bot to maintain Wikipedia Afdl pages, see http://en.wikipedia.org/wiki/User:AfdlBot # Written by an Anon E. Mouse Wikipedia editor, http://en.wikipedia.org/wiki/User:AnonEMouse # June 2007 # # Intended potential users of this bot: # Wikipedia:WikiProject Macintosh/Deletion, # Wikipedia:WikiProject Georgia Tech/Deletion # Wikipedia:WikiProject Pornography/Deletion # Wikipedia:WikiProject Video games/Deletion # Wikipedia:WikiProject Warcraft/Deletion # Wikipedia:WikiProject Webcomics/Deletion # TODO: Templates, (and tfdl), mfdl? # TODO: actually read categories for discussion, rather than just maintaining the already listed ones # TODO: Deletion review as for categories # TODO: follow down config-input categories, get all subcategories # TODO: WikiProject Deletion Sorting and subst:ed Afds # TODO: mark Afd when listing import sys,re,wikipedia,codecs,time,config from datetime import date, timedelta site = wikipedia.getSite() logfile = open("afd.log", 'w') afdlBotPage = 'User:AfdlBot/Configuration' afdlProjects = [] def followRedirects(title): """ Gets the page content, and as long as it's a redirect, follows those redirects. """ page = wikipedia.Page(site, title); try: page.get() return page except wikipedia.NoPage: return page except wikipedia.SectionError: return page except wikipedia.IsRedirectPage, arg: return followRedirects(arg.args[0]) def parseDate(string): """ Accept YYYY-MM-DD or 17:55, June 8, 2007 formats, return a date object""" if string == None: return None # print 'parsing "' + string + '"' try: return date(*(time.strptime(string, '%Y-%m-%d')[0:3])) except ValueError: try: return date(*(time.strptime(string, '%H:%M, %B %d, %Y')[0:3])) except ValueError: try: return date(*(time.strptime(string, '%H:%M, %d %B %Y')[0:3])) except ValueError: return None # Retular sub-expression to match 4 kinds of wikilinked dates # [[2007]]-[[03-15]] # [[2007-03-15]] # [[July 3]], [[2007]] # [[3 July]], [[2007]] # Note {len(May)=3, len(September)=9} datexpr = r'(\[\[\d{4}\]\]-\[\[\d\d\-\d\d\]\])|' + \ r'(\[\[\d{4}-\d\d-\d\d\]\])|' + \ r'(\[\[[A-Za-z]{3,9} \d\d\]\],?\s*\[\[\d{4}\]\])|' + \ r'(\[\[\d\d [A-Za-z]{3,9}\]\],?\s*\[\[\d{4}\]\])' def parseWikidate(string): """ Parses the above 4 formats into a date object. """ if string == None: return None string = re.sub('[\]\[,]', '', string) # print 'newstring=', string # print 'parsing "' + string + '"' try: return date(*(time.strptime(string, '%Y-%m-%d')[0:3])) except ValueError: try: return date(*(time.strptime(string, '%B %d %Y')[0:3])) except ValueError: try: return date(*(time.strptime(string, '%d %B %Y')[0:3])) except ValueError: return None def dateToPageName(d): """Need to produce single digits for days < 10. For example, 2007 June 9.""" return d.strftime('%Y %B ') + str(d.day) # The result was '''Speedy delete per G3'''. [[User talk:PeaceNT|'''''Peacent''''']] 02:29, 9 June 2007 (UTC) Rresult = re.compile(r".*\<!--Template:Afd top.*The result was[^']*'''(?P<result>[^']*)'''.*" + r"(?P<date>[0-9][0-9]:[0-9][0-9], [0-9][0-9] [A-Za-z]+ [0-9]{4}) \(UTC\)", re.DOTALL) # Don't keep afdls more than a month old archivedate = date.today() + timedelta(days=-31) class AfdlProject(object): """A project (or other party) maintaining a list of Afds with Afdls.""" # Regular expression matching an Afdl template Rafdl = re.compile(r'\s*(\*\s*)?{{\s*Afdl(\|(?P<params>.+?))?}}\s*', re.DOTALL | re.IGNORECASE) def parseAfdls(self, text): """ Parses Afdls from text, returns unparsed text.""" last = 0 rest = '' for m in AfdlProject.Rafdl.finditer(text): paramString = m.group('params') params = [] if paramString: params = paramString.split('|') aa = AfdArticleFromAfdlParams(params) if aa: if m.start() > last: rest += text[last:m.start()] + '\n' last = m.end() if aa.result: if aa.closedate > archivedate: self.closedAfds.append(aa) else: self.newArchived += 1 else: self.openAfds.append(aa) if last < len(text): rest += text[last:] + '\n' return rest # Matching a categories for deletion line is tough since it's not a template, but free form text. # * [[:Category:Anime and manga inspired webcomics]] to [[:Category:Anime and manga webcomics]] at [[Wikipedia:Categories for deletion/Log/2006 July 11#Category:Anime and manga inspired webcomics to Category:Anime and manga webcomics]] '''Kept''' ''([[July 10]] [[2006]] – [[July 20]] [[2006]])'' # *[[:Category:Keentoons]] at [[Wikipedia:Categories for discussion/Log/2007 April 30#Category:Keentoons]] # * [[:Category:WikiProject Webcomics members]] at [[Wikipedia:Categories for deletion/Log/2006 July 20#WikiProject participants]] ''[[July 20]] [[2006]] – [[July 31]] [[2006]]'' # * [[:Category:Big-bust models and performers]] ([[2007]]-[[03-15]] – [[2007]]-[[03-21]]) '''No consensus''' # *[[:Category:Naturally busty porn stars]] at [[Wikipedia:Categories for discussion/Log/2007 March 8#Category:Naturally busty porn stars]] ([[2007]]-[[03-08]] - ([[2007]]-[[03-14]]) '''Delete''' # * [[:Category:Adult video games]] at [[Wikipedia:Categories for deletion/Log/2006 May 12#Category:Adult video games to Category:Erotic computer and video games]] '''renamed to''' [[:Category:Erotic computer and video games]] # * [[:Category:Artifex]] at [[Wikipedia:Categories for discussion/Log/2007 April 17#Category:Artifex]] '''Speedy Delete''' Rcfd = re.compile(r'\s*(\*\s*)?\[\[:(?P<category>Category:[^\]]+)\]\](?P<optional>.*?)' + r'(\s+at\s+\[\[(?P<cfd>Wikipedia:Categories[ _]for[ _]d[a-z]+/Log/[^\]]+)\]\])?\s*' + r"((?P<optional2>.*?)'''(?P<result>[^'\n]+)''')?(?P<rest>.*)", re.IGNORECASE) # Similarly for deletion review. # * [[Air Force Amy]] at [[Wikipedia:Deletion_review/Log/2007 May 5#Air Force Amy]] ([[2007-04-05]]—[[2007-04-06]]) '''Keep rewritten article.''' #* [[List of male performers in gay porn films]] at [[Wikipedia:Deletion review/Log/2007 April 18#List of male performers in gay porn films]] ([[2007-04-18]]—[[23 April]] [[2007]]) '''Deletion overturned''' Rdrv = re.compile(r'\s*(\*\s*)?\[\[:?(?P<target>[^\]]+)\]\](?P<optional>.*?)\s+at\s+' + r'\[\[(?P<cfd>Wikipedia:Deletion[ _]review[ _]deletion/Log/[^\]]+)\]\]\s*' + r"((?P<optional2>.*?)'''(?P<result>[^'\n]+)''')?(?P<rest>.*)", re.IGNORECASE) Rdatespan = re.compile(r'\s*\(\s*(?P<fromdate>' +datexpr+ r')\s*[^\[\)]*\s*(?P<todate>' +datexpr+ r')\s*\)\s*') # Rdatespan = re.compile(r'\s*\(\s*(?P<fromdate>' +datexpr+ u')\s*-|(–)\s*(?P<todate>' +datexpr+ r')\s*\)\s*') # Rdatespan = re.compile(r'\s*\(\s*(?P<fromdate>\[\[\S+\]\])\s*(?P<dash>-|(–))\s*(?P<todate>\[\[\S+\]\])\)\s*') # Rdatespan = re.compile(r'\s*\(\s*(?P<fromdate>' +datexpr+ ')\s*(-|(—)|(–))\s*(?P<todate>' +datexpr+ ')\s*\)\s*') def parseCfds(self, text): """ Parses Cfd listings from text, returns unparsed text.""" last = 0 rest = '' for m in AfdlProject.Rcfd.finditer(text): # print 'match=', m.group() # print 'category=', m.group('category') # print 'cfd=', m.group('cfd') # print 'optional=', m.group('optional') # print 'optional2=', m.group('optional2') # print 'result=', m.group('result') # print 'rest=', m.group('rest') cfdname = m.group('cfd') if cfdname: cfd = wikipedia.Page(site, m.group('cfd')) else: cfd = None category = wikipedia.Page(site, m.group('category')) cfdrest = '' if m.group('optional'): cfdrest += ' ' + m.group('optional') cfdrest = cfdrest.strip() if m.group('optional2'): cfdrest += ' ' + m.group('optional2') cfdrest = cfdrest.strip() if m.group('rest'): cfdrest += ' ' + m.group('rest') cfdrest = cfdrest.strip() datespan = AfdlProject.Rdatespan.search(cfdrest) fromdate = None todate = None if datespan: # print 'datespan=', datespan.group() # print 'fromdate=', datespan.group('fromdate') # print 'dash=', datespan.group('dash') # print 'todate=', datespan.group('todate') cfdrest = AfdlProject.Rdatespan.sub('', cfdrest) fromdate=parseWikidate(datespan.group('fromdate')) todate=parseWikidate(datespan.group('todate')) if fromdate and not cfd : cfd = wikipedia.Page(site, 'Wikipedia:Categories for discussion/Log/' + dateToPageName(fromdate)) # Todo: check if cfd page links to category? c = CfdCategory(cfd, category, fromdate, todate, m.group('result'), cfdrest) if c.startdate: # in other words, if it's legitimate if m.start() > last: rest += text[last:m.start()] + '\n' last = m.end() if c.result: if not c.closedate or c.closedate > archivedate: self.closedAfds.append(aa) else: self.newArchived += 1 else: self.openAfds.append(c) if last < len(text): rest += text[last:] + '\n' print 'rest after cfds=', rest return rest # Regular expression that matches an Afdl list page RafdlPage = re.compile(r'(?P<header>.*)' + r'==\s*Open\s*==\s*(?P<open>.*)' + r'==\s*Closed\s*==\s*(?P<closed>.*)', re.DOTALL) #Todo: separate footer? def __init__(self, listpage, articleCategories, articleTemplates, talkCategories, talkTemplates): # print listpage, articleTemplates, articleCategories, talkTemplates, talkCategories self.listpage = listpage self.articleTemplates = articleTemplates self.articleCategories = articleCategories self.talkTemplates = talkTemplates self.talkCategories = talkCategories self.openAfds = [] self.closedAfds = [] # Count the number of useful changes that would be made to list page when writing #- if none, don't write anything self.newOpen = 0 self.newClosed = 0 self.newArchived = 0 # Todo: self.archivedAfds = [] match = AfdlProject.RafdlPage.match(listpage.get()) if not match: print 'Could not parse', listpage, '!!' logfile.write('Could not parse ' + str(listpage) + '!!\n') return self.header = match.group('header') openmatch = match.group('open') openmatch = AfdlProject.Rdateheader.sub('', openmatch) closedmatch = match.group('closed') closedmatch = AfdlProject.Rdateheader.sub('', closedmatch) self.opentext = self.parseAfdls(openmatch) self.opentext = self.parseCfds(self.opentext) # Some of the formerly open Afds will have just been closed, count them self.newClosed = len(self.closedAfds) self.closedtext = self.parseAfdls(closedmatch) self.closedtext = self.parseCfds(self.closedtext) def __str__(self): """A console representation of the AfdlProject""" return str(self.listpage) def logAfd(self, page, afd, reason, spec): """ Add an article and its afd to the project lists. Log this in a file for fun.""" # print self.listpage, page.title(), afd.title(), reason, spec aa = AfdArticle(afd, page) # print aa # Consider if article has been deleted or redirected if aa.result: # Todo: should we check archivedate? Or should we put it on the page at least once? self.closedAfds.append(aa) self.newClosed += 1 else: self.openAfds.append(aa) self.newOpen += 1 logfile.write(self.listpage.title() + '\t' + page.title() + '\t' + afd.title() + '\t' + reason + ':' + spec + '\n') logfile.flush() def checkAfdArticle(self, afd, article, talkpage): """ Check if an Afd for an article qualifies to be added to the project lists. Returns True if qualifies (and has been added), False if not. """ # check for articles already in Afd list, those don't even need to be "gotten" for open in self.openAfds: if open.afd == afd and open.article == article: # print afd, 'matches', open if Rresult.match(afd.get()): # afd has a result, in other words, was closed self.openAfds.remove(aa) self.logAfd(article, afd, 'listed as open on', sortPageName) return True for closed in self.closedAfds: if closed.afd == afd and closed.article == article: return True if len(self.articleCategories)>0: for cat in article.categories(): if cat.title().capitalize() in self.articleCategories: self.logAfd(article, afd, 'article category', cat.title()) return True if len(self.articleTemplates)>0: for template in article.templates(): if template.title().capitalize() in self.articleTemplates: self.logAfd(article, afd, 'article template', template) return True # Do we need to check talk page? if len(self.talkCategories) + len(self.talkTemplates) <= 0: return False if not talkpage.exists(): return False if len(self.talkCategories) > 0: for cat in talkpage.categories(): if cat.capitalize() in self.talkCategories: self.logAfd(article, afd, 'talk category', cat.title()) return True if len(self.talkTemplates) > 0: for template in talkpage.templates(): if template.capitalize() in self.talkTemplates: self.logAfd(article, afd, 'talk template', template) return True return False # Regular expression that matches the date header generated below Rdateheader = re.compile(r"^\s*'''\d\d? [A-Za-z]{3,9}'''\s+\(\[\[.*\|AfD*\]\].*\)[ \t]*\n", re.MULTILINE) def afdsByTime(self, list): list.sort() lastdate = None result = '' for afd in list: if afd.startdate != lastdate: # print 'changing lastdate', lastdate, 'to', afd.startdate lastdate = afd.startdate # '''19 June''' ([[Wikipedia:Articles for deletion/Log/2007 June 19|AfD]], # [[Wikipedia:Categories for discussion/Log/2007 June 19|CfD]]) datename = dateToPageName(afd.startdate) result += afd.startdate.strftime("'''%d %B'''") \ + ' ([[Wikipedia:Articles for deletion/Log/' + datename + '|AfD]],' \ + ' [[Wikipedia:Categories for discussion/Log/' + datename + '|CfD]])' + '\n' result += '* ' + str(afd) + '\n' logfile.write('* ' + str(afd).encode(config.console_encoding, 'replace') + '\n') return result def afdlsText(self): """Returns the AfdArticle lists in this project, also to logfile.""" logfile.write(str(self) + '\n') text = self.header + '== Open ==\n' + self.opentext text += self.afdsByTime(self.openAfds) text += '== Closed ==\n' + self.closedtext text += self.afdsByTime(self.closedAfds) # Todo: archived Afds by alphabetical order? logfile.write(text.encode(config.console_encoding, 'replace')) return text # end class AfdlProject class AfdArticle(object): """An article for deletion, with its article (usually but not always 1-1).""" def __init__(self, afd, article, startdate=None, closedate=None, result=None): # print afd, article, startdate, closedate, result self.article = article self.afd = afd if startdate: self.startdate = startdate else: # An approximation - assuming first edit created # print 'getting version history' edits = afd.getVersionHistory(reverseOrder = True, getAll = True) if not edits: return # an AfD must have a startdate self.startdate = parseDate(edits[0][1]) if result and closedate: self.result = result self.closedate = closedate else: # print 'getting afd' afdtext = afd.get() match = Rresult.match(afdtext) if match: if result and len(result) > 0: self.result = result else: self.result = match.group('result') # print self.result if closedate: self.closedate = closedate else: self.closedate = parseDate(match.group('date')) else: self.result = self.closedate = None # print self def __str__(self): """A console representation of the AfdArticle""" return self.afdl() def __cmp__(self, other): """Allows sorting AfdArticles. Descending order by startdate. """ return cmp(other.startdate, self.startdate) def afdl(self): """{{afdl|Rebecca Cummings|Rebecca Cummings (2nd nomination)|2007-05-30|2007-06-04|Delete}}""" retval = '{{afdl|' + self.article.title() + '|' if self.afd.title() != 'Wikipedia:Articles for deletion/' + self.article.title(): retval += self.afd.title() retval += '|' + self.startdate.strftime('%Y-%m-%d') + '|' if self.result: # 2007-03-16 retval += self.closedate.strftime('%Y-%m-%d') + '|' + self.result else: retval += '|' retval += '}}' return retval # end class AfdArticle def AfdArticleFromAfdlParams(afdlp): """Reads an AfdArticle from ['article', 'AfD name', 'open YYYY-MM-DD', 'close YYYY-MM-DD', 'result']. Last 3 params optional. """ # print afdlp if not afdlp or len(afdlp) < 1: return None if len(afdlp) > 1 and len(afdlp[1]) > 0: afdname = afdlp[1] else: afdname = 'Wikipedia:Articles for deletion/' + afdlp[0] afd = wikipedia.Page(site, afdname) # if not afd.exists(): return article = wikipedia.Page(site, afdlp[0]) # Any missing params will be read from the afd if len(afdlp) > 4: aa = AfdArticle(afd, article, parseDate(afdlp[2]), parseDate(afdlp[3]), afdlp[4]) elif len(afdlp) > 2: aa = AfdArticle(afd, article, parseDate(afdlp[2]), None, None) else: aa = AfdArticle(afd, article, None, None, None) # No AFD if not hasattr(aa, 'startdate'): return None return aa class CfdCategory(AfdArticle): """Some special treatment for Categories for discussion/deletion debates.""" # Parse date and subsection out of a cfd link Rcfdlink = re.compile(r'Wikipedia:Categories for d[a-z]+/Log/(?P<date>[A-Za-z0-9_ ]+)(#.*)?') def __init__(self, cfd, category, startdate, closedate, result, rest): # print cfd, category, startdate, closedate, result, rest self.article = category self.afd = cfd self.startdate = startdate self.closedate = closedate self.result = result self.rest = rest # any unparsed stuff if not startdate: match = CfdCategory.Rcfdlink.match(cfd.title()) if match: #If not, should throw error self.startdate = parseDate(match.group('date')) else: # Throw error? return # Todo: parse result and closedate from cfd? # if result and not closedate: # self.closedate = self.startdate + timedelta(10) # Nasty hack # print self def __str__(self): """A console representation of the CfdCategory""" # *[[:Category:Naturally busty porn stars]] at [[Wikipedia:Categories for discussion/Log/2007 March 8#Category:Naturally busty porn stars]] ([[2007]]-[[03-08]] - ([[2007]]-[[03-14]]) '''Delete''' result = '[[:' + self.article.title() + ']] at [[' + self.afd.title() + ']] ([[' + str(self.startdate) + ']] -' if self.closedate: result += ' [[' + str(self.closedate) + ']]' result += ')' if self.result: result += " '''" + self.result + "'''" result += self.rest return result # end class CfdCategory(AfdArticle) def readAfdlProjects(projpagename): """ Reads specifications of all AfdlProjects on input page. """ projPage = followRedirects(projpagename) # Afd List:, article templates:, article categories:, talk templates: # The Afd List one is mandatory, the rest are optional Rspec = re.compile(r'==[^=]*' + r'^\*\s*Afd List:[^\[]*\[\[(?P<listpage>[^\]]+)\]\][^\*=$]*$' +'[^=]*', re.IGNORECASE | re.MULTILINE) # Note that the category includes the word 'Category:' but the template doesn't include the # word 'Template:'. This is to match the results of the Page methods. Rtemplate = re.compile(r'\[\[Template:(?P<template>[^\]]+)\]\]', re.IGNORECASE) Rcategory = re.compile(r'\[\[:(?P<category>Category:[^\]]+)\]\]', re.IGNORECASE) RartCat = re.compile(r'(^\*\s*Article categories:[^\*$]*$)', re.IGNORECASE) RartTem = re.compile(r'(^\*\s*Article templates:[^\*$]*$)', re.IGNORECASE) RtalkCat = re.compile(r'(^\*\s*Talk categories:[^\*$]*$)', re.IGNORECASE) RtalkTem = re.compile(r'(^\*\s*Talk templates:[^\*$]*$)', re.IGNORECASE) for match in Rspec.finditer(projPage.get()): # print match listpagename = match.group('listpage') listPage = followRedirects(listpagename) if not listPage.exists(): continue articleTemplates = [] articleCategories = [] talkTemplates = [] talkCategories = [] # print 'listpage=', listpage for line in match.group().splitlines(): # print line if RartCat.match(line): for cat in Rcategory.finditer(line): articleCategories.append(cat.group('category').capitalize()) # print articleCategories if RartTem.match(line): for template in Rtemplate.finditer(line): articleTemplates.append(template.group('template').capitalize()) # print articleTemplates if RtalkCat.match(line): for cat in Rcategory.finditer(line): talkCategories.append(cat.group('category').capitalize()) # print talkCategories if RtalkTem.match(line): for template in Rtemplate.finditer(line): talkTemplates.append(template.group('template').capitalize()) # print talkTemplates afdlProjects.append(AfdlProject(listPage, articleCategories, articleTemplates, talkCategories, talkTemplates)) # Regular expression that matches a "subst"ed Afd debate # {{Wikipedia:Articles for deletion/Dr. John E. Douglas}} Rafd = re.compile(r'{{\s*(?P<afd>Wikipedia:Articles for deletion/(?P<title>[^}]*))}}') def processAfdList(afdListName): """ Searches input page name for Afds of pages matching project categories and templates. """ print 'Processing', afdListName listpage = followRedirects(afdListName) if not listpage.exists(): return listtext = listpage.get() # print listtext for match in Rafd.finditer(listtext): # print 'match=', match.group() afdname = match.group('afd') # print afdname afdtitle = match.group('title') afd = followRedirects(afdname) # print afd.linkedPages() # need to follow every link, to deal with multiple nominations in one AFD checked = [] # only follow a link once per Afd for article in afd.linkedPages(): # print 'article', article, 'section', article.section() if article.section() != None: continue if ':' in article.title(): continue # mainspace pages only if article in checked: continue checked.append(article) article = followRedirects(article.title()) if not article.exists(): continue # print 'considering ', article # print article.templatesWithParams() for (template, params) in article.templatesWithParams(): if template == 'AfDM' and 'page='+afdtitle in params: talkpage = wikipedia.Page(site, 'Talk:' + article.title()) for proj in afdlProjects: # check them all: even if listed in one, may be listed in many proj.checkAfdArticle(afd, article, talkpage) break # assume only one AfDM template per article def main(): args = wikipedia.handleArgs() # take out the global params readAfdlProjects(afdlBotPage) # for proj in afdlProjects: # print proj # print proj.afdlsText() # return if len(args) > 0: for arg in args: processAfdList(arg) else: checkdate = date.today() + timedelta(days=+2) lastdate = date.today() + timedelta(days=-12) while checkdate > lastdate : checkdate = checkdate + timedelta(days=-1) # Wikipedia:Articles_for_deletion/Log/2007_June_9 checkpagename = 'Wikipedia:Articles_for_deletion/Log/' + dateToPageName(checkdate) processAfdList(checkpagename) for proj in afdlProjects: print proj, 'newOpen', proj.newOpen, 'newClosed', proj.newClosed, 'newArchived', proj.newArchived if proj.newOpen + proj.newClosed + proj.newArchived > 0: comment = '' if proj.newOpen > 0: comment += '+' + str(proj.newOpen) + ' open' if proj.newClosed > 0: if len(comment) > 0: comment += ', ' comment += '+' + str(proj.newClosed) + ' closed' if proj.newArchived > 0: if len(comment) > 0: comment += ', ' comment += '-' + str(proj.newArchived) + ' archived' comment += ' deletion discussions' print comment text = proj.afdlsText() print text proj.listpage.put(text, comment, watchArticle = True, minorEdit = False) if __name__ == "__main__": try: main() finally: wikipedia.stopme() logfile.close()