User:WindBOT/Filters

From Team Fortress Wiki
< User:WindBOT
Revision as of 23:54, 15 August 2012 by Moussekateer (talk | contribs) (Implement {{tl|Dictionary}}: +mann vs. machievements)
Jump to: navigation, search

How to disable a filter

If the bot is malfunctioning, chances are that the problem lies in one of these blocks of code. Thus, instead of shutting down the whole bot, it would be wiser to disable only the chunk of code that is misbehaving. To make the bot ignore a certain line, add a "#" in front of it:

# This line will be ignored

If there are multiple lines, wrap them inside triple-quotes (you still need to put the two spaces at the beginning of the line):

"""This line will be ignored
and this one as well
and this one is cake
and the previous one was a lie but it was still ignored"""

If all else fails, you can simply delete the block from the page. The bot can't come up with code by itself yet, so it won't run anything. Or, if the problem really is elsewhere, block the bot.

Global filters

File categorization (Add {{No category}})

def fileCategorization(content, **kwargs):
	badLoneCategories = [u'Category:Screenshot images', u'Category:Artwork images', u'Category:Multimedia files', u'Category:Extracted media', u'Category:Images without license tags', u'Category:Images that need improving', u'Category:Images using ((Another license)) incorrectly']
	extraStuff = u'{{No category}}'
	regLang = compileRegex('/[^/]+$')
	if 'article' not in kwargs or content.find(u'#REDIRECT ') != -1:
		return content
	title = u(kwargs['article'].title)
	if title[:5].lower() != u'file:':
		return content
	for c in kwargs['article'].getCategories():
		if u(regLang.sub(u'', c)) not in badLoneCategories:
			return content
	if not len(content):
		return extraStuff
	if content.find(extraStuff) != -1:
		return content
	return content.strip() + u'\n' + extraStuff
addFilter(fileCategorization)

Page filters

addPageFilter(r'^user:', r'(?:talk|help|wiki|template):')

Semantic filters

Item names

categories = [ # These are categories that contain that name of things that should be capitalized
    'Weapons',
    'Hats',
    'Miscellaneous items',
    'Buildings'
]
exceptions = [ # Those are page names that are in the above categories but should not count as capitalized weapon names
    'Direct Hit',
    'Hats', 'Weapons', 'Miscellaneous items', 'Buildings',
    'Item levels',
    'Developer weapons',
    'Self-made items',
    'Unused content',
    'Promotional items',
    'Mercenary', 'Taunts', 'Decapitation', 'Fists', 'Cloak', # Too common to be reliably replaced
    'Club', 'Bat', 'Knife', 'Syringe', 'Bottle', 'Original', # Ditto (weapons)
    'Classified', # Not capitalized even when referring to the hat
    'Attendant', # Avoids issue with French
    'Buff Banner', 'Medi Gun', 'Bonk Helm', # Avoids issue with German
    'Reskins' # Not capitalized
]
for c in categories:
    c = wikitools.category.Category(wiki(), u(c))
    for p in pageFilter(c.getAllMembers(titleonly=True)):
        if p not in exceptions:
            enforceCapitalization(p)
# Special case: Direct Hit
addSafeFilter(wordFilter('Direct Hit', r'(?<!a )(?<!one )(?<!on )(?<!\()(?<!placed )(?<!single )\bDirect Hit'))
# Special case: Attendant
enforceCapitalization('Attendant', languageBlacklist=['fr'])
# Special cases: "Buff-Banner"/"Medigun" in German
enforceCapitalization('Buff Banner', 'Medi Gun', languageBlacklist=['de'])
enforceCapitalization('Buff-Banner', 'Medigun', language='de')
# Put "an" in front of Electro Sapper
addSafeFilter(
    dumbReplace('an Sapper', 'a Sapper'),
    dumbReplace('An Sapper', 'A Sapper')
)
# Flare Gun
addSafeFilter(wordFilter('Flare Gun', 'flaregun'))

Classes

# Not "Heavy" because the word can be used as an adjective:
classes = ['Scout', 'Soldier', 'Pyro', 'Demoman', 'Heavy Weapons Guy', 'Engineer', 'Medic', 'Sniper'] # Spy added later
classesPlurals = ['Scouts', 'Pyros', 'Demomen', 'Heavy Weapons Guys', 'Heavies', 'Engineers', 'Snipers', 'Spies']
enforceCapitalization(*classes)
enforceCapitalization(*classesPlurals)
# Special Spy case:
addSafeFilter(wordFilter('Spy', '(?<!I )Spy'))
classes.append('Spy') # Used in later functions with Spy included

BLU/RED

DISABLED because of TFC's team names

"""addSafeFilter(wordFilter('BLU', 'BLU(?![- ]+ray)'))
# Can't force simply "red" because it would ruin things like "this red hat" or something. However, the following can probably be assumed to be correct:
for c in classes + classesPlurals + ['Soldiers']:
    enforceCapitalization('RED ' + c) # Capitalize "RED Scout", "RED Demoman", etc
enforceCapitalization('RED team', 'RED Heavy', 'RED Intelligence')
# Similarly, "blue" can be converted to "BLU" in the same cases:
for c in classes + classesPlurals + ['Soldiers']:
    addSafeFilter(wordFilter('BLU ' + c, 'blue? ' + c)) # Convert "blue scout" -> "BLU Scout", "blue demoman" -> "BLU Demoman", etc.
addSafeFilter(
    wordFilter('BLU team', 'blue? team'),
    wordFilter('BLU Heavy', 'blue? Heavy'),
    wordFilter('BLU Intelligence', 'blue? intelligence')
)"""

Other capitalized words

enforceCapitalization('Team Fortress 2', 'TF2')
enforceCapitalization('Payload', 'Cloak', 'Overtime', 'Dispensers', 'Sappers')
enforceCapitalization('PlayStation', 'Xbox')
enforceCapitalization('iPod', 'iPhone')

Word aliases

addSafeFilter(
    wordFilter(u'Übersaw', u'[üu]bersaw'), # Ubersaw
    wordFilter(u'Force-a-Nature', u'Force of Nature'), # Force-A-Nature
    wordFilter(u'ÜberCharges', u'[Üüu]ber(?!säge)(?!sage)(?! Entertainment)(?: ?charge)?s'), # ÜberCharges
    wordFilter(u'Intelligence', u'Intel(?!\\s*(?:CPU|processor))'), # Intelligence (maybe adding "flag" in there would be too agressive)
    wordFilter(u'Sapper', u'(?:Ele[ck]tro |Ultra )+Sapper', u'Electro-?Sapper'),
    wordFilter(u'Heavy Weapons Guy', u'Heavy Guy'),
    wordFilter(u'Intelligence room', u'Intel(?:ligence)? room'),
    wordFilter(u'Intelligence briefcase', u'Intel(?:ligence)? briefcase'),
    wordFilter(u'K.G.B.', u'KGB'),
    wordFilter(u'Chieftain', u'Chieftian', u'Chieftan'),
    wordFilter(u'Batallion\'s Backup', u'Batallion\'?s? Back-?up'),
    wordFilter(u'Mann-Conomy', u'Mann?-?Conomy'),
    wordFilter(u'First-person view', u'1(?:st)? person view'),
    wordFilter(u'Über Update', u'[UÜü]ber update')
    #wordFilter(u'Stickybomb', u'Stickybomb', u'Sticky bomb'),
    #wordFilter(u'Stickybombs', u'Stickybombs', u'Sticky bombs')
)
addSafeFilter(
    wordFilter(u'Medi Gun', u'Medi-?Gun'),
    languageBlacklist=['fr', 'de', 'pl']
)

Sentry Gun

addSafeFilter(
    wordFilter(u'Sentry Gun', r"(?<!Mini.)(?<!Mah.)(?<!My.)Sentry(?![-\s]*(?:Gun|here|ahead|forward|up there|right up|, right up|'{2,}|jump)|-)"),
    wordFilter(u'Sentry Guns', r'Sentry Guns', r'Sentries(?!\s+Gun)'),
    wordFilter(u'Combat Mini-Sentry Gun', r'Combat Mini-Sentry Gun', r'(?:(?:Mini|Combat)\s+)+Sentry(?!\s+Gun|-)'),
    wordFilter(u'Combat Mini-Sentry Guns', r'(?:(?:Mini|Combat)[-\s]+)+Sentr(?:y[-\s]+Guns|ies)')
)
addFilter(regex(r'\[\[([^][|]+\|)?(?:Sentry Guns|Sentries)\]\]', '$1Sentry Guns')) # Put the "s" out of the link

Common misspellings

addSafeFilter(
    wordFilter('Huntsman', 'Hunstman'),
    wordFilter('Dispenser', 'Dis?pen[sc][eo]r'),
    wordFilter('Heavy', 'Hevy'),
    wordFilter('Engineer', 'Enginer'),
    wordFilter('Soldier', 'Solider'),
    wordFilter('Mini-Crit', 'Minicrit'),
    wordFilter('Mini-Crits', 'Minicrits'),
    wordFilter('Chargin\' Targe', 'charg[ei][-\'n\\s]*targe?'),
    wordFilter('Kritzkrieg', 'Kritzkreig'),
    wordFilter('screenshot', 'screen shot'),
    wordFilter('screenshots', 'screen shots'),
    wordFilter('in-game', 'ingame'),
    wordFilter('team-colored', 'team colou?red', keepcapitalization=True)
    # wordFilter('color', '(?<!Rustic )colour')
)
addSafeFilter(
    wordFilter('Natascha', 'Natas?c?ha'),
    language='en'
)
addSafeFilter(
    wordFilter('Spies', 'Spys'),
    languageBlacklist=['de', 'es']
)

Map names

addSafeFilter(
    wordFilter('Gravel Pit', '(?<!cp_)Gravelpit', 'Gravel pit'),
    wordFilter('Badwater Basin', '(?<!pl_)Badwater Basin', '(?<!pl_)Badwater(?!\s+Basin)'),
    wordFilter('Gold Rush', '(?<!pl_)Goldrush')
)
# Capitalisation rules for map names are whitelist-based rather than Category:Maps+blacklist based, because lots of maps are common nouns.
# As such, if a map with a non-common name is added, please add it to this list.
enforceCapitalization(
    '2Fort', 'Badlands', 'Coldfront', 'Dustbowl', 'Egypt', 'Fastlane', 'Granary', 'Gullywash',
    'Hightower', 'Hoodoo', 'Landfall', 'Offblast', 'Thunder Mountain',
    'Viaduct', 'Wildfire', 'Yukon'
)

Section headers

addSafeFilter(
    wordFilter(u'== Update history ==', u'==+ ?(?:Update history|Previous changes) ?==+'),
    wordFilter(u'== See also ==', u'==+ ?See also ?==+'),
    wordFilter(u'== External links ==', u'==+ ?External links ?==+'),
    wordFilter(u'== Painted variants ==', u'==+ ?Painted variants ?==+'),
    wordFilter(u'== Item set ==', u'==+ ?Item set ?==+'),
    wordFilter(u'== Damage and function times ==', u'==+ ?Damage and function times ?==+'),
    wordFilter(u'=== As a crafting ingredient ===', u'==+ ?As a crafting ingredient ?==+'),
    wordFilter(u'== Unused content ==', u'==+ ?Unused content ?==+'),
    wordFilter(u'== See also ==', u'==+ ?See also ?==+'),
    wordFilter(u'== Related achievements ==', u'==+ ?Related achievements ?==+'),
    wordFilter(u'== Strange variant ==', u'==+ ?Strange variant ?==+')
)

Language-specific filters

Language-agnostic

# Language-agnostic achievements auto-translate
def translateAchievements(language, pageSuffix):
    try:
        tf = readLocaleFile(urllib2.urlopen(page('File:Tf_' + language + '.txt').getDownloadUrl()).read(-1))
    except:
        print 'Downloading failed.'
        return
    try:
        languages = parseLocaleFile(tf, language=language)
    except:
        print 'Error while parsing tf_' + language + '.txt'
        return
    acceptPrefix = ['TF_SCOUT_', 'TF_SOLDIER_', 'TF_PYRO_', 'TF_DEMOMAN_', 'TF_HEAVY_', 'TF_ENGINEER_', 'TF_MEDIC_', 'TF_SNIPER_', 'TF_SPY_', 'TF_GET_', 'TF_KILL_', 'TF_BURN_', 'TF_WIN_', 'TF_PLAY_']
    acceptSuffix = ['_NAME', '_DESC']
    filteredAchievements = languagesFilter(languages, commonto=[language, 'english'], prefix=acceptPrefix, suffix=acceptSuffix, exceptions=['TF_SOLDIER_ASSIST_MEDIC_UBER_NAME'])
    associateLocaleWordFilters(filteredAchievements, 'english', language, pageSuffix)

Romanian filters

# Romanian characters
addSafeFilter(
    dumbReplaces({
        u'ş': u'ș',
        u'ţ': u'ț'
    }), language='ro'
)
# Romanian achievements (disabled for now)
#translateAchievements('romanian', 'ro')
# Fix User:Vulturas's stoopid:
addSafeFilter(
    wordFilter(u'batjocură', u'batjocoră'),
    wordFilter(u'batjocura', u'batjocora'),
    wordFilter(u'batjocureşte', u'batjocoreşte', u'batjocorește'),
    wordFilter(u'batjocuri', u'batjocori'),
    wordFilter(u'batjocurile', u'batjocorile'),
    wordFilter(u'batjocurii', u'batjocorii'),
    language='ro'
)

German filters

addSafeFilter( # Requested by Picard
    wordFilter(u'Krit-\'n-Cola', u'[CK]rit-\'?[an]\'?-Cola'),
    wordFilter(u'Krit', u'Crit'),
    wordFilter(u'Krits', u'Crits'),
    wordFilter(u'Sonstiger Gegenstand', u'Diverser Gegenstand', u'diverser Gegenstand', u'sonstiger Gegenstand'),
    wordFilter(u'Sonstige Gegenstände', u'Diverse Gegenstände', 'diverse Gegenstände', 'sonstige Gegenstände'),
    language='de'
)
# German achievements (disabled for now)
#translateAchievements('german', 'de')
addSafeFilter(
    wordFilter(u'== Update-Verlauf ==', u'==+ *(?:Update Verlauf|Letzte [Ääa]nderungen) *==+'), language='de'
)

Spanish filters

addSafeFilter(
    wordFilter(u'Forajido', u'Gunslinger'),
    wordFilter(u'Pistolón', u'Gran Masacre'),
    wordFilter(u'Medic', u'M[eé]dico(?! Medieval)'),
    wordFilter(u'Medics', u'M[eé]dicos(?! Medieval)'),
    wordFilter(u'Sniper', u'(?<!Rifle de )Francotirador'),
    wordFilter(u'Snipers', u'(?<!Rifle de )Francotiradore?s'),
    wordFilter(u'Spy', u'Esp[ií]a'),
    wordFilter(u'Spies', u'Spys', u'Esp[ií]as'),
    wordFilter(u'Soldier', u'Soldado(?! de Fortuna)'),
    wordFilter(u'Soldiers', u'Soldados'),
    wordFilter(u'Engineer', u'(?<!Gorra de )Ingeniero'),
    wordFilter(u'Engineers', u'Ingenieros'),
    wordFilter(u'Curiosidades', u'Trivia'),
    wordFilter(u'Actualizacion Mann-Conomy', u'Mann-Conomy Update'),
    language='es'
)
class spanishDateFilter: # Requested by BiBi
    def __init__(self):
        self.reg = compileRegex(r'(?:Parche|Patch) del? (\d{1,2}) (?:del?)? (Enero|Febrero|Marzo|Abril|Mayo|Ju[nl]io|Agosto|Septiembre|Oct[ou]bre|Noviembre|Diciembre) de (\d{4})')
        self.filterName = u'Spanish date consistency filter'
    def replace(self, match):
        return u'Parche del ' + u(match.group(1)) + u' de ' + u(match.group(2)).title() + u' de ' + u(match.group(3))
    def __call__(self, content, **kwargs):
        return self.reg.sub(self.replace, content)
addSafeFilter(spanishDateFilter(), language='es')
addSafeFilter( # Requested by Dio
    wordFilter(u'== Variaciones de Colores ==', u'==+ *Variantes +pintadas *==+'), language='es'
)
addSafeFilter( # Requested by Dio
    wordFilter(u'La trampa', u'El cheto', keepcapitalization=True),
    wordFilter(u'Las trampas', u'Los chetos', keepcapitalization=True),
    wordFilter(u'Trampas', u'Chetos', keepcapitalization=True),
    wordFilter(u'Trampa', u'Cheto', keepcapitalization=True),
    language='es'
)
addSafeFilter( # Requested by Jagoba RL, then flipped by request of Dio
    wordFilter(u'SuperActualización', u'Actualizaci[oó]n [Üüu]ber'),
    language='es'
)

French filters

enforceCapitalization(
    u'janvier', u'février', u'mars', u'avril', u'mai', u'juin', u'juillet', u'août', u'septembre', u'octobre', u'novembre', u'décembre', language='fr'
) # Do not capitalize months
addSafeFilter(wordFilter('janvier', 'janviers'), language='fr')
addSafeFilter(
    wordFilter(u'== Historique des mises à jour ==', u'==+ *(?:Historique des? mises? [aà] jour|Changements? pr.c.dents?) *==+'), language='fr'
)

Brazilian Portuguese filters

def portugueseInfoboxAvailabilityFilter(t, **kwargs): # Requested by Cructo
    if t.getName().lower() == u'item infobox':
        return t # Skip
    if t.getParam('availability'):
        t.setParam('availability', t.getParam('availability').replace(u'craft', u'fabricação').replace(u'Craft', u'Fabricação'))
    return t
addTemplateFilter(portugueseInfoboxAvailabilityFilter, language='pt-br')
addSafeFilter( # Requested by Cructo
    wordFilter(u'Fabricação', u'Crafting', keepcapitalization=True),
    wordFilter(u'Conquista', u'Achievement', keepcapitalization=True),
    wordFilter(u'Conquistas', u'Achievements', keepcapitalization=True),
    language='pt-br'
)
addSafeFilter( # Requested by EpicEric
    wordFilter(u'Demomen', u'Demomans'),
    wordFilter(u'Heavies', u'Heavys'),
    wordFilter(u'N/D', u'N/A'),
    wordFilter(u'Atualização', u'Patch', keepcapitalization=True),
    wordFilter(u'Atualizações', u'Patches', keepcapitalization=True),
    wordFilter(u'Desempenho', u'Performance', keepcapitalization=True),
    wordFilter(u'Motor Source', u'Source Engine', u'Engine Source'),
    wordFilter(u'Spies', u'Spys'),
    wordFilter(u'ÜberCarga', u'Sobrecarga', u'Übercharge'),
    wordFilter(u'Mochila', u'Backpack', keepcapitalization=True),
    wordFilter(u'Comunidade Steam', u'Steam Community'),
    wordFilter(u'Corrigido um bug', u'Corrigido um defeito', keepcapitalization=True),
    wordFilter(u'== Bugs ==', u'==+ *Defeitos *==+'),
    wordFilter(u'Sala de renascimento', u'Respawn room', u'Sala de respawn', keepcapitalization=True),
    wordFilter(u'Área de renascimento', u'Respawn area', u'Área de respawn',  keepcapitalization=True),
    wordFilter(u'Über Atualização', u'Atualização Über', u'Atualização do Über', u'Über Update'),
    wordFilter(u'Sistema de obtenção de itens', u'Sistema de drop de itens', u'Item drop system', u'Sistema de queda de itens', keepcapitalization=True),
    wordFilter(u'Rajada de ar', u'Compression blast', u'Airblast', keepcapitalization=True),
    wordFilter(u'minicrits', u'Mini-Crits', u'Mini-Críticos', u'Minicríticos'),
    wordFilter(u'Blog Oficial do TF2', u'TF2 Official Blog', u'Blog Oficial de TF2'),
    wordFilter(u'Personalizada', u'Customizada', keepcapitalization=True),
    wordFilter(u'Personalizado', u'Customizado', keepcapitalization=True),
    wordFilter(u'Genuína', u'Genuine', keepcapitalization=True),
    wordFilter(u'Feito por mim', u'Self-Made', keepcapitalization=True),
    wordFilter(u'Padrão', u'Stock', keepcapitalization=True),
    wordFilter(u'Estranha', u'Strange', keepcapitalization=True),
    wordFilter(u'Única', u'Unique', keepcapitalization=True),
    wordFilter(u'Incomum', u'Unusual', keepcapitalization=True),
    wordFilter(u'Compartimento', u'Slot', keepcapitalization=True),
    wordFilter(u'Jogabilidade', u'Gameplay', keepcapitalization=True),
    wordFilter(u'Rei do Pedaço', u'King of the Hill'),
    wordFilter(u'Disparo Alternativo', u'Disparo-Alt', u'Alt-Fire', u'Fogo Alternativo', u'Disparo Alternativo', keepcapitalization=True),
    wordFilter(u'Botão de Disparo-Alt', u'Botão Direito do Mouse', keepcapitalization=True),
    wordFilter(u'Golden Wrench', u'Golden Chave Inglesa'),
    wordFilter(u'Habilidades', u'Abilidades', keepcapitalization=True),
    wordFilter(u'Austrálio', u'Australium', keepcapitalization=True),
    wordFilter(u'Cavaleiro Carente de Cavalo e Cabeça', u'Horseless Headless Horsemann'),
    wordFilter(u'Festiva', u'Festive', keepcapitalization=True),
    wordFilter(u'Natal Australiano', u'Australian Christmas', keepcapitalization=True),
    wordFilter(u'Natal Australiano de 2011', u'Natal Australiano 2011', u'Australian Christmas 2011', keepcapitalization=True),
    wordFilter(u'== Variantes pintadas ==', u'==+ *Variantes pintados *==+', u'==+ *Variações pintadas *==+'),
    wordFilter(u'== Curiosidades ==', u'==+ *Trivias? *==+'),
    wordFilter(u'Projeto', u'Planta', keepcapitalization=True),
    wordFilter(u'Projetos', u'Plantas', keepcapitalization=True),
    language='pt-br'
)
enforceCapitalization( # Do not capitalize languages, nationalities, months or days of the week (will not include abbrevs. of weekdays, as they are also ordinal numbers)
  u'inglês', u'russo', u'russa', u'francês', u'francesa', u'alemão', u'alemã', u'polonês', u'polonesa', u'brasileiro', u'brasileira', u'finlandês', u'finlandesa', u'castelhano', u'espanhol', u'espanhola', u'holandês', u'holandesa', u'chinês', u'chinesa', u'árabe', u'tcheco', u'tcheca', u'dinamarquês', u'dinamarquesa', u'húngaro', u'húngara', u'italiano', u'italiana', u'japonês', u'japonesa', u'coreano', u'coreana', u'norueguês', u'norueguesa', u'português', u'portuguesa', u'romeno', u'romena', u'sueco', u'sueca', u'turco', u'egípcio', u'egípcia', u'janeiro', u'fevereiro', u'março', u'abril', u'maio', u'junho', u'julho', u'agosto', u'setembro', u'outubro', u'novembro', u'dezembro', u'segunda-feira', u'terça-feira', u'quarta-feira', u'quinta-feira', u'sexta-feira', u'sábado', u'domingo', language='pt-br'
)
addSafeFilter( # Requested by Cructo
   wordFilter(u'Espingarda', u'Scattergun'),
   wordFilter(u'Pistola', u'Pistol'),
   wordFilter(u'Taco', u'Bat'),
   wordFilter(u'Lança-Foguetes', u'Rocket Launcher'),
   wordFilter(u'Escopeta', u'Shotgun'),
   wordFilter(u'Pá', u'Shovel'),
   wordFilter(u'Lança-Chamas', u'Flamethrower'),
   wordFilter(u'Machado de Incêndio', u'Fire Axe'),
   wordFilter(u'Lança-Granadas', u'Grenade Launcher'),
   wordFilter(u'Lança-Stickybombs', u'Stickybomb Launcher'),
   wordFilter(u'Garrafa', u'Bottle'),
   wordFilter(u'Metralhadora Giratória', u'Minigun'),
   wordFilter(u'Punhos', u'Fists'),
   wordFilter(u'Ferramenta de Construção', u'Build PDA'),
   wordFilter(u'Ferramenta de Demolição', u'Destroy PDA'),
   wordFilter(u'Arma de Seringas', u'Syringe Gun'),
   wordFilter(u'Arma Médica', u'Medi Gun'),
   wordFilter(u'Serra de Ossos', u'Bone Saw'),
   wordFilter(u'Rifle de Sniper', u'Sniper Rifle'),
   wordFilter(u'Submetralhadora', u'SMG'),
   wordFilter(u'Revólver', u'Revolver'),
   wordFilter(u'Faca', u'Knife'),
   wordFilter(u'Relógio de Invisibilidade', u'Invis Watch', u'Invisibility Watch'),
   wordFilter(u'Kit de Disfarce', u'Disguise Kit'),
    language='pt-br'
)

Russian filters

addSafeFilter(
    regexes({
        (u'^([^<>]*)(?<!=)([""])((?:(?!\\2|[=<>]).)+)\\2([^<>]*)$', re.MULTILINE): u'$1«$3»$4',
        u'«([^»]*)(?<!\')\'([^»\']+)\'(?!\')': u'«$1„$2“'
    }),
    language='ru'
)
addSafeFilter( # Requested by FreeXMan
    wordFilter(u'Зефенайя', u'Зефанайя', u'Зефинайя'),
    wordFilter(u'Зефенайи', u'Зефанайи', u'Зефинайи'),
    wordFilter(u'Зефенайей', u'Зефанайей', u'Зефинайей'),
    wordFilter(u'Зефенай', u'Зефанай', u'Зефинай'),
    wordFilter(u'Нет', u'N/A'),
    wordFilter(u'Хеллоуин', u'Хэллоуин', keepcapitalization=True),
    wordFilter(u'Хеллоуинский', u'Хэллоуинский', keepcapitalization=True),
    wordFilter(u'Хеллоуинского', u'Хэллоуинского', keepcapitalization=True),
    wordFilter(u'Хеллоуинскому', u'Хэллоуинскому', keepcapitalization=True),
    wordFilter(u'Хеллоуинским', u'Хэллоуинским', keepcapitalization=True),
    wordFilter(u'Хеллоуинском', u'Хэллоуинском', keepcapitalization=True),
    language='ru'
)

Korean filters

addSafeFilter(dumbReplace(u'솔져', u'솔저'), language='ko') # Requested by Cyrus H.

Dutch filters

#addSafeFilter(wordFilter(u'Niveau', u'Level', keepcapitalization=True), language='nl') # Requested by Apparition; too broad
addSafeFilter( # Requested by Warlike and Heifastus
    wordFilter(u'Updateverleden', u'Updategeschiedenis', u'Update verleden', u'Update geschiedenis', keepcapitalization=True),
    wordFilter(u'Galerij', u'Gallerij', keepcapitalization=True),
    wordFilter(u'community', u'communitie', keepcapitalization=True),
    language='nl'
)

Polish filters

addSafeFilter( # Requested by real_alien
    wordFilter(u'Exploit', u'Nadużycie', keepcapitalization=True),
    wordFilter(u'Exploity', u'Nadużycia', keepcapitalization=True),
    wordFilter(u'Exploitem', u'Nadużyciem', keepcapitalization=True),
    wordFilter(u'Exploitu', u'Nadużycia', keepcapitalization=True),
    language='pl'
)

Link filters

Moved links

def movedLinks(link, **kwargs):
    movedLinks = {
        u'Quality#Normal': u'Normal',
        u'Quality#Unique': u'Unique',
        u'Quality#Vintage': u'Vintage',
        u'Quality#Genuine': u'Genuine',
        u'Quality#Strange': u'Strange',
        u'Quality#Unusual': u'Unusual',
        u'Quality#Community': u'Community (quality)',
        u'Quality#Self-Made': u'Self-Made',
        u'Quality#Valve': u'Valve (quality)'
    }
    if link.getType() == u'internal' and link.getLink() in movedLinks:
        link.setLink(movedLinks[link.getLink()])
    return link
addLinkFilter(movedLinks)

Wikipedia links filter

def wikipediaLinks(link, **kwargs):
    wikipediaRegex = compileRegex(r'^https?://(?:(\w+)\.)?wikipedia\.org/wiki/(\S+)')
    if link.getType() == u'external':
        linkInfo = wikipediaRegex.search(link.getLink())
        if linkInfo:
            link.setType(u'internal')
            try:
                wikiPage = urllib2.unquote(str(linkInfo.group(2))).decode('utf8', 'ignore').replace(u'_', ' ')
            except:
                wikiPage = u(linkInfo.group(2)).replace(u'_', ' ')
            if not linkInfo.group(1) or linkInfo.group(1).lower() == u'en':
                link.setLink(u'Wikipedia:' + wikiPage) # English Wikipedia
            else:
                link.setLink(u'Wikipedia:' + linkInfo.group(1).lower() + u':' + wikiPage) # Non-english Wikipedia
            if link.getLabel() is None:
                link.setLabel(u'(Wikipedia)')
    return link
addLinkFilter(wikipediaLinks)

TF2Wiki links filter

def tf2wikiLinks(link, **kwargs):
    tf2wikiRegex1 = compileRegex(r'^https?://[-.\w]*tf2wiki\.net/wiki/(\S+)$')
    tf2wikiRegex2 = compileRegex(r'^https?://[-.\w]*tf2wiki\.net/w[-_/\w]+?/([^/\s]+)$')
    if link.getType() == 'external':
        linkInfo = tf2wikiRegex1.search(link.getLink())
        isMedia = False
        if not linkInfo:
            linkInfo = tf2wikiRegex2.search(link.getLink())
            isMedia = True
        if linkInfo:
            link.setType('internal')
            try:
                wikiPage = u(urllib2.unquote(str(linkInfo.group(1))).decode('utf8', 'ignore').replace(u'_', ' '))
            except:
                wikiPage = u(linkInfo.group(1)).replace(u'_', ' ')
            label = wikiPage
            if isMedia:
                if wikiPage[-4:].lower() == '.wav':
                    wikiPage = 'Media:' + wikiPage
                else:
                    wikiPage = ':File:' + wikiPage
            link.setLink(wikiPage)
            if link.getLabel() is None:
                link.setLabel(label)
    return link
addLinkFilter(tf2wikiLinks)

Category removal on pages using {{Item infobox}}

def removeCategory(l, **kwargs):
    catsToRemove = [u'Category:Weapons', u'Category:Hats', u'Category:Primary weapons', u'Category:Secondary weapons', u'Category:Melee weapons', u'Category:PDA1 weapons', u'Category:PDA2 weapons', u'Category:Miscellaneous items', u'Category:Tools', u'Category:Action items', u'Category:Taunts', u'Community-contributed items']
    regLang = compileRegex('/[^/]+$')
    if 'article' not in kwargs or regLang.sub(u, l.getLink()) not in catsToRemove:
        return l
    if u'Category:Item infobox usage' not in kwargs['article'].getCategories():
        return l
    return None
addLinkFilter(removeCategory)

FPSBanana to GameBanana

addLinkFilter(linkDomainFilter('fpsbanana.com', 'gamebanana.com'))

Remove trailing slashes from internal links

def removeTrailingSlash(l, **kwargs):
    if l.getType() != u'internal' or not len(l.getLink()):
        return l
    if l.getLink()[-1] == '/':
        l.setLink(l.getLink()[:-1])
    return l
addLinkFilter(removeTrailingSlash)

Convert patch links to {{Patch name}}

def patchNameLinkFilter(l, **kwargs):
    if l.getType() != u'internal':
        return l
    regPatchName = compileRegex(u'(January|February|March|April|May|June|July|August|September|October|November|December)\\s+(\\d+),\\s+(\\d{4,})\\s+Patch(?:/\\w+)?')
    result = regPatchName.match(l.getLink())
    if result is None or l.getLabel().find(result.group(2)) == -1 or l.getLabel().find(result.group(3)) == -1:
        return l
    monthNames = ('january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december')
    patchType = u
    if l.getLink().lower().find(u'beta') != -1:
        patchType = u'|beta'
    elif l.getLink().lower().find(u'xbox') != -1:
        patchType = u'|xbox'
    elif l.getLink().lower().find(u'classic') != -1:
        patchType = u'|classic'
    return template(u'{{Patch name|' + u(monthNames.index(result.group(1).lower()) + 1) + u'|' + u(result.group(2)) +  u'|' + u(result.group(3)) + patchType + u'}}')
addLinkFilter(patchNameLinkFilter)

Template filters

Template renaming

def templateRenameMapping(t, **kwargs):
    templateMap = {
        # Format goes like this (without the "#" in front obviously):
        #'Good template name': ['Bad template lowercase name 1', 'Bad template lowercase name 2', 'Bad template lowercase name 3'],
        # Last line has no comma at the end
        'Scout Nav': ['scout nav/ro'],
        'Soldier Nav': ['soldier nav/ro'],
        'Pyro Nav': ['pyro nav/ro'],
        'Heavy Nav': ['heavy nav/ro'],
        'Demoman Nav': ['demoman nav/ro'],
        'Engineer Nav': ['engineer nav/ro'],
        'Medic Nav': ['medic nav/ro'],
        'Sniper Nav': ['sniper nav/ro'],
        'Spy Nav': ['spy nav/ro'],
        'Promo nav': ['mncnav', 'pokernightnav', 'l4dnav', 'kfnav'],
        'Crush': ['pngcrush']
    }
    for n in templateMap:
        if t.getName().lower() in templateMap[n]:
            t.setName(n)
    return t
addTemplateFilter(templateRenameMapping)

Manage all {{Item infobox}}es

def infoboxFilter(t, **kwargs):
    filteredTemplates = ('weapon infobox', 'hat infobox', 'tool infobox', 'item infobox') # Only do stuff to these templates
    if t.getName().lower() not in filteredTemplates:
        return t # Skip
    t.setName('Item infobox')
    t.indentationMatters(True) # Reindents every time, not only when modifying values
    paramAliases = { # Parameter alias 'goodParam': 'badParam', or 'goodParam': [list of bad params].
        'name': ['weapon-name-override', 'hat-name-override', 'tool-name-override', 'NAME', 'name-override'],
        'image': ['weapon-image', 'hat-image', 'tool-image'],
        'kill-text-1': 'kill-text',
        'team-colors': 'has-team-colors',
        'two-models': 'has-two-models',
        'slot': ['weapon-slot', 'hat-slot', 'tool-slot'],
        'trade': 'tradable',
        'gift': 'giftable',
        'craft': 'craftable',
        'paint': ['paintable', 'Paint'],
        'rename': ['name-tag', 'nametag'],
        'loadout': 'display-loadout-stats',
        'level': 'level-and-type',
        'loadout-prefix': 'hide-loadout-prefix'
    }
    catstoCheck = { # Mapping 'templateAttribute': [List of 'Category|templateAttributeValue']
        'type': ['Taunts|taunt', 'Action items|action', 'Taunts|action taunt', 'Hats|hat', 'Miscellaneous items|misc item', 'Tools|tools', 'Weapons|weapon'],
        'slot': ['Primary weapons|primary', 'Secondary weapons|secondary', 'Melee weapons|melee', 'PDA1 weapons|pda 1', 'PDA2 weapons|pda 2']
    }
    catsDontCheck = [u'Category:Beta and unused content', 'Category:Taunts'] # Type and slot won't be modified on these pages
    preferedOrder = [ # Prefered order of keys inside template
        'name', 'game', 'type', 'beta', 'unused', 'image', 'imagewidth', '3d-team', '3d-alt', '3d-team-alt', 'number-of-3d-images', 'number-of-3d-team-images', 'number-of-3d-alt-images', 'view#', 'view#name', 'team-colors', 'team-colors-class#', 'two-models', 'skin-image-red', 'skin-image-blu', 'TFC-model', 'QTF-model', 'hide-kill-icon', 'kill-icon-#', 'kill-text-#', 'used-by', 'slot', 'crafting-slot', 'custom-slot', 'equip-region', 'equip-region-2', 'contributed-by', 'released', 'availability', 'trade', 'gift', 'craft', 'paint', 'rename', 'numbered', 'medieval', 'ammo-loaded', 'ammo-carried', 'ammo-type', 'show-ammo', 'reload', 'loadout', 'loadout-prefix', 'quality', 'level', '%ATTRIBUTES%', 'item-description', 'item-uses', 'item-flags', 'item-expiration'
    ]
    checkEnglish = ['trade', 'gift', 'craft', 'paint', 'rename', 'numbered', 'medieval'] # If these attributes aren't yes or no, check the values on the english page
    attributeTypes = ['neutral', 'positive', 'negative'] # Possible loadout attribute types
    regLang = compileRegex('/[^/]+$')
    # Step 0 - Check categories:
    tCats = None
    isTFC = False # Assume false by default
    if 'article' in kwargs:
        if kwargs['article'] is not None:
            cats2 = kwargs['article'].getCategories()
            tCats = []
            for c in cats2:
                tCats.append(u(regLang.sub(u, u(c))))
            isTFC = u'Category:Weapons (Classic)' in tCats
    # Step 1 - Rename obsolete attributes
    for p in paramAliases:
        if type(paramAliases[p]) is type([]):
            for a in paramAliases[p]:
                t.renameParam(a, p)
        else:
            t.renameParam(paramAliases[p], p)
    # Step 2 - Fix ammo stuff, delete pricing attributes
    t.delParam('ammo', 'price', 'show-price', 'purchasable', 'backpack-image')
    # Step 3 - Fix reload stuff
    if t.getParam('reload') is None:
        t.renameParam('reload-type', 'reload')
    # Step 4 - Count, split, order and fix loadout attributes
    attrNumber = 1
    regexAttrSplit = compileRegex(r'\s*<br[^<>]*>\s*')
    for a in attributeTypes:
        if t.getParam(a + '-attributes') is not None:
            attrs = regexAttrSplit.split(t.getParam(a + '-attributes'))
            for attr in attrs:
                t.setParam('att-' + str(attrNumber) + '-' + a, attr)
                attrNumber += 1
            t.delParam(a + '-attributes')
    if tCats is not None:
        # Step 5 - Lookup english fallback on certain attributes
        fetchEnglish = False
        values = {}
        for attr in checkEnglish:
            if t.getParam(attr) is not None and t.getParam(attr).lower() not in (u'yes', u'no'):
                fetchEnglish = True
            elif t.getParam(attr) is not None:
                values[attr] = t.getParam(attr).lower()
        if regLang.search(kwargs['article'].title):
            englishArticle = page(regLang.sub(, kwargs['article'].title))
            try:
                englishContent = englishArticle.getWikiText()
            except:
                englishContent = u
            englishContent, englishTemplates, englishKeys = templateExtract(englishContent)
            for englishT in englishTemplates.values():
                if englishT.getName().lower() in filteredTemplates:
                    for p in paramAliases:
                        if type(paramAliases[p]) is type([]):
                            for a in paramAliases[p]:
                                englishT.renameParam(a, p)
                        else:
                            englishT.renameParam(paramAliases[p], p)
                    for attr in checkEnglish:
                        if englishT.getParam(attr) is not None and englishT.getParam(attr).lower() in (u'yes', u'no'):
                            values[attr] = englishT.getParam(attr).lower()
                    break
        for attr in values:
            t.setParam(attr, values[attr])
        checkCats = True
        for c in catsDontCheck:
            if c in tCats:
                checkCats = False
                break
        if not isTFC and not t.getParam('custom-slot') and checkCats:
            # Step 6 - Set certains attributes based on page categories
            for cname in catstoCheck:
                cname = u(cname)
                found = None
                foundmultiple = False
                for c in catstoCheck[cname]:
                    cat, val = u(c).split(u'|')
                    cat = u'Category:' + u(regLang.sub(u, cat))
                    if cat in tCats:
                        if found is not None:
                            foundmultiple = True
                        found = val
                if not foundmultiple:
                    pass#t.setParam(cname, found) # Temporarily disabled for Made Man
            if u'Category:Weapons' not in tCats:
                t.delParam('slot')
            # Step 7 - Convert neutral attributes - Disabled
            """desc = []
            if t.getParam('item-description') is not None:
                desc = regexAttrSplit.split(t.getParam('item-description'))
            for i in range(1, 10):
                if t.getParam('att-' + str(i) + '-neutral') is not None:
                    desc.append(t.getParam('att-' + str(i) + '-neutral'))
                    t.delParam('att-' + str(i) + '-neutral')
            desc2 = []
            for d in desc:
                if d.strip():
                    desc2.append(d.strip())
            if len(desc2):
                t.setParam('item-description', u'
'.join(desc2))""" # Step 7.5 TEMPORARY: Add numbered = no if not isTFC and not t.getParam('numbered'): t.setParam('numbered', 'no') # Step 8 - Do TFC stuff if isTFC: t.setParam('type', 'weapon') t.setParam('game', 'tfc') # Step 9 - Set correct preferred indentation for k in ['quality', 'level', 'item-description', 'item-uses', 'item-flags', 'level-and-type', 'loadout-name', 'loadout-prefix', 'hide-loadout-prefix']: t.setPreferedIndentation(k, 2) for i in range(1, 10): for a in attributeTypes: t.setPreferedIndentation('att-' + str(i) + '-' + a, 2) # Step 10 - Build correct attribute order newOrder = [] for o in preferedOrder: if o.find('#') == -1: newOrder.append(o) continue if o == '%ATTRIBUTES%': for i in range(1, 10): for a in attributeTypes: newOrder.append('att-' + str(i) + '-' + a) for i in range(1, 10): newOrder.append(o.replace('#', str(i))) t.setPreferedOrder(newOrder) # Step 11 - There is no step 11 return t addTemplateFilter(infoboxFilter)

Remove useless templates

def removeUselessTemplate(t, **kwargs):
    if t.getName().lower() in (u'targeted', u'languages', u'auto lang cat'):
        return None # Delete template
    return t
addTemplateFilter(removeUselessTemplate)

Remove manual video IDs from {{Weapon Demonstration}}

def weaponDemonstrationFilter(t, **kwargs):
    if t.getName().lower() != 'weapon demonstration':
        return t # Skip
    t.delParam('1')
    return t
addTemplateFilter(weaponDemonstrationFilter)

Filter parameters of certain templates

def templateParamFilter(t, **kwargs):
    params = { # Map: 'lowercase template name': ['list', 'of', 'params', 'to', 'filter']
        'patch layout': ['before', 'after', 'current'],
        'item infobox': ['released'],
        'update history': [1]
    }
    if t.getName().lower() not in params:
        return t
    for p in params[t.getName().lower()]:
        if t.getParam(p):
            t.setParam(p, fixContent(t.getParam(p), **kwargs))
    return t
addTemplateFilter(templateParamFilter)

Remove obsolete parameters

def obsoleteParameterFilter(t, **kwargs):
    params = { # Map: 'lowercase template name': ['list', 'of', 'params', 'to', 'delete']
        'blueprint': ['ingredient-#n-local', 'result-local', 'result-#n-local'],
        'taunt': ['weapon-#n-local'],
    }
    if t.getName().lower() not in params:
        return t
    for p in params[t.getName().lower()]:
        p = u(p)
        if p.find(u'#n') != -1:
            for i in range(10):
                t.delParam(p.replace(u'#n', str(i)))
        else:
            t.delParam(p)
    return t
addTemplateFilter(obsoleteParameterFilter)

Fix map infobox attribute

def mapTypeFix(t, **kwargs):
    mapTypeDict = {
         'Arena': ['Badlands (Arena)', 'Granary (Arena)', 'Hardhat', 'Lumberyard', 'Nucleus', 'Offblast', 'Ravine', 'Sawmill', 'Watchtower', 'Well (Arena)'],
         'Capture the Flag': ['2Fort', 'Atrophy (Capture the Flag)', 'Bedrock', 'Cloudburst', 'Converge', 'Convoy', 'Deliverance', 'Double Cross', 'Fusion', 'HAARP', 'Landfall', 'Mercy', 'Overlook', 'Premuda', 'Sawmill (Capture the Flag)', 'Slate (Capture the Flag)', 'Stockpile', 'Turbine', 'Vector', 'Well (Capture the Flag)', 'Wildfire', 'Wiretap'],
         'Control Point': ['5Gorge', 'Badlands', 'Coldfront', 'DeGroot Keep', 'Dustbowl', 'Egypt', 'Fastlane', 'Freight', 'Furnace Creek', 'Glacier', 'Gorge', 'Granary', 'Gravel Pit', 'Gullywash', 'Junction', 'Mann Manor', 'Mountain Lab', 'Obscure', 'Steel', 'Well', 'Yukon'],
         'King of the Hill': ['Harvest', 'Harvest Event', 'Lakeside', 'Moonshine', 'Nucleus (King of the Hill)', 'Sawmill (King of the Hill)', 'Viaduct'],
         'Payload': ['Badwater Basin', 'Cashworks', 'Cranetop', 'Frontier', 'Gold Rush', 'Hoodoo', 'Swiftwater', 'Thunder Mountain', 'Upward', 'Waste'],
         'Payload Race': ['Animus', 'Cornfield', 'Hightower', 'Highwind', 'Nightfall', 'Panic', 'Pipeline', 'Scoville', 'Solitude'],
         'Territorial Control': ['Hydro', 'Meridian'],
         'Training mode': ['Dustbowl (Training)', 'Target', 'Walkway']
         }
    if 'article' not in kwargs:
        return t
    basetitle = u(kwargs['article'].title.split('/')[0])
    if t.getName().lower() != 'map infobox':
        return t
    for n in mapTypeDict:
         if basetitle in mapTypeDict[n]:
             t.setParam('game-type', n)
    return t
addTemplateFilter(mapTypeFix)

Implement Backpack Item Link template

def backpackLink(t, **kwargs):
    if t.getName().lower() != 'item infobox':
        return t
    if t.getParam('contributed-by') is None:
        return t
    contributor = t.getParam('contributed-by')
    optf2LinkReg = compileRegex(r'^\[http://optf2.com/item/(\S+)\s(.[^\[\]]+)]$')
    result = optf2LinkReg.search(contributor)
    if result:
        t.setParam('contributed-by', '{{Backpack Item Link|' + result.group(1) + '|' + result.group(2) + '}}')
    return t
addTemplateFilter(backpackLink)

Add day/month/year to {{Patch layout}}

def patchLayoutFilter(t, **kwargs):
    if t.getName().lower() != 'patch layout' or 'article' not in kwargs:
        return t
    t.setPreferedOrder(['game', 'before', 'day', 'month', 'year', 'after', 'source-title', 'source', 'source-lang'] + [['source-' + str(n) + '-title', 'source-' + str(n), 'source-' + str(n) + '-lang'] for n in xrange(10)] + ['updatelink', 'update', 'update-link', 'update-lang', 'hide-diff'] + [['diff-' + str(n)] for n in xrange(10)] + ['notes'])
    t.delParam('current')
    regPatchName = compileRegex(u'^(January|February|March|April|May|June|July|August|September|October|November|December)\\s+(\\d+),\\s+(\\d{4,})\\s+Patch(?:/\\w+)?')
    result = regPatchName.match(u(kwargs['article'].title))
    if result is not None:
        t.setParam('day', result.group(2))
        t.setParam('month', result.group(1).lower())
        t.setParam('year', result.group(3))
    return t
addTemplateFilter(patchLayoutFilter)

Implement {{Dictionary}}

class DictionaryUpdater:
    def __init__(self):
        self.subpageTemplateLang = """{{#switch:{{{lang|{{SUBPAGENAME}}}}}|%options%}}"""
        self.subpageTemplateParam = """{{#switch:{{{1|}}}|%options%}}"""
        self.invalidParamError = """<span class="error">Error: invalid param.</span>[[Category:ERROR]]"""
        self.subpageTemplateID = """%string%"""
        self.dictionaries = {
            u'Template:Dictionary/items': { # Dictionary page
                'name': 'items', # Dictionary name (used for categorizing)
                'sync': 'Template:Dictionary/items/Special:SyncData' # Page holding last sync data
            },
            u'Template:Dictionary/common strings': { # Warning: no underscore
                'name': 'common strings',
                'sync': 'Template:Dictionary/common strings/Special:SyncData'
            },
            u'Template:Dictionary/classes': {
                'name': 'classes',
                'sync': 'Template:Dictionary/classes/Special:SyncData'
            },
            u'Template:Dictionary/demonstrations': {
                'name': 'demonstrations',
                'sync': 'Template:Dictionary/demonstrations/Special:SyncData'
            },
            u'Template:Dictionary/price': {
                'name': 'price',
                'sync': 'Template:Dictionary/price/Special:SyncData',
                'allTemplate': '{{{{{template|item price/fmt}}}|%options%|tt={{{tt|yes}}}}}'
            },
            u'Template:Dictionary/merchandise': {
                'name': 'merchandise',
                'sync': 'Template:Dictionary/merchandise/Special:SyncData'
            },
            u'Template:Dictionary/steam ids': {
                'name': 'steam ids',
                'sync': 'Template:Dictionary/steam ids/Special:SyncData'
            },
            u'Template:Dictionary/achievements/scout': {
                'name': 'achievements/scout',
                'sync': 'Template:Dictionary/achievements/scout/Special:SyncData'
            },
            u'Template:Dictionary/achievements/soldier': {
                'name': 'achievements/soldier',
                'sync': 'Template:Dictionary/achievements/soldier/Special:SyncData'
            },
            u'Template:Dictionary/achievements/pyro': {
                'name': 'achievements/pyro',
                'sync': 'Template:Dictionary/achievements/pyro/Special:SyncData'
            },
            u'Template:Dictionary/achievements/demoman': {
                'name': 'achievements/demoman',
                'sync': 'Template:Dictionary/achievements/demoman/Special:SyncData'
            },
            u'Template:Dictionary/achievements/heavy': {
                'name': 'achievements/heavy',
                'sync': 'Template:Dictionary/achievements/heavy/Special:SyncData'
            },
            u'Template:Dictionary/achievements/engineer': {
                'name': 'achievements/engineer',
                'sync': 'Template:Dictionary/achievements/engineer/Special:SyncData'
            },
            u'Template:Dictionary/achievements/medic': {
                'name': 'achievements/medic',
                'sync': 'Template:Dictionary/achievements/medic/Special:SyncData'
            },
            u'Template:Dictionary/achievements/sniper': {
                'name': 'achievements/sniper',
                'sync': 'Template:Dictionary/achievements/sniper/Special:SyncData'
            },
            u'Template:Dictionary/achievements/spy': {
                'name': 'achievements/spy',
                'sync': 'Template:Dictionary/achievements/spy/Special:SyncData'
            },
            u'Template:Dictionary/achievements/general': {
                'name': 'achievements/general',
                'sync': 'Template:Dictionary/achievements/general/Special:SyncData'
            },
            u'Template:Dictionary/achievements/halloween': {
                'name': 'achievements/halloween',
                'sync': 'Template:Dictionary/achievements/halloween/Special:SyncData'
            },
            u'Template:Dictionary/achievements/treasure hunt': {
                'name': 'achievements/treasure hunt',
                'sync': 'Template:Dictionary/achievements/treasure hunt/Special:SyncData'
            },
            u'Template:Dictionary/achievements/replay': {
                'name': 'achievements/replay',
                'sync': 'Template:Dictionary/achievements/replay/Special:SyncData'
            },
            u'Template:Dictionary/achievements/summer camp': {
                'name': 'achievements/summer camp',
                'sync': 'Template:Dictionary/achievements/summer camp/Special:SyncData'
            },
            u'Template:Dictionary/achievements/foundry': {
                'name': 'achievements/foundry',
                'sync': 'Template:Dictionary/achievements/foundry/Special:SyncData'
            },
            u'Template:Dictionary/achievements/christmas': {
                'name': 'achievements/christmas',
                'sync': 'Template:Dictionary/achievements/christmas/Special:SyncData'
            },
            u'Template:Dictionary/achievements/astro-chievements': {
                'name': 'achievements/astro-chievements',
                'sync': 'Template:Dictionary/achievements/astro-chievements/Special:SyncData'
            },
            u'Template:Dictionary/achievements/mann vs. machievements': {
                'name': 'achievements/mann vs. machievements',
                'sync': 'Template:Dictionary/achievements/mann vs. machievements��/Special:SyncData'
            },
            u'Template:Dictionary/blueprints': {
                'name': 'blueprints',
                'sync': 'Template:Dictionary/blueprints/Special:SyncData'
            },
            u'Template:Dictionary/templatecore': {
                'name': 'templatecore',
                'sync': 'Template:Dictionary/templatecore/Special:SyncData'
            },
            u'Template:Dictionary/defindex': {
                'name': 'defindex',
                'sync': 'Template:Dictionary/defindex/Special:SyncData'
            },
            u'Template:Dictionary/quad': {
                'name': 'quad',
                'sync': 'Template:Dictionary/quad/Special:SyncData',
                'blankString': '-'
            },
            u'Template:Dictionary/item set weapons': {
                'name': 'item set weapons',
                'sync': 'Template:Dictionary/item set weapons/Special:SyncData'
            }
        }
        self.subpageSeparator = u'/'
        # List of supported languages, in prefered order
        self.languages = [u'en', u'ar', u'cs', u'da', u'de', u'es', u'fi', u'fr', u'hu', u'it', u'ja', u'ko', u'nl', u'no', u'pl', u'pt', u'pt-br', u'ro', u'ru', u'sv', u'tr', u'zh-hans', u'zh-hant']
        self.defaultLang = u'en'
        self.allKeyName = u'_all_'
        self.filterName = u'Your friendly neighborhood dictionary updater'
        self.commentsExtract = compileRegex(r)
        self.stringsExtract = compileRegex(r'(?:^[ \t]*#[ \t]*([^\r\n]*?)[ \t]*$\s*)?^[ \t]*([^\r\n]+?[ \t]*(?:\|[ \t]*[^\r\n]+?[ \t]*)*):[ \t]*([^\r\n]+?[ \t]*$|\s*[\r\n]+(?:\s*[ \t]+[-\w]+[ \t]*:[ \t]*[^\r\n]+[ \t]*$)+)', re.IGNORECASE | re.MULTILINE)
        self.translationExtract = compileRegex(r'^[ \t]+([-\w]+)[ \t]*:[ \t]*([^\r\n]+)[ \t]*$', re.IGNORECASE | re.MULTILINE)
        self.scheduler = BatchScheduler(16)
        addWhitelistPage(self.dictionaries.keys())
    def generateSubpage(self, keyName, data, currentDict, syncData):
        h = hashlib.md5()
        if type(data) is type({}): # Subkeys (translations or not)
            isTranslation = True
            subpage = u(self.subpageTemplateLang)
            for k in data:
                if 'blankString' in self.dictionaries[currentDict] and data[k] == self.dictionaries[currentDict]['blankString']:
                    data[k] = u
                if isTranslation and k not in self.languages:
                    isTranslation = False
                    subpage = u(self.subpageTemplateParam)
            ordered = []
            unordered = {}
            if isTranslation:
                missing = []
                for lang in self.languages:
                    if lang in data:
                        ordered.append(lang + u'=' + data[lang])
                        unordered[lang] = data[lang]
                        h.update((lang + u'=' + data[lang]).encode('utf8'))
                    else:
                        missing.append(lang)
                        h.update((u'null-' + lang).encode('utf8'))
                if self.defaultLang in data:
                    ordered.insert(0, u'#default=' + data[self.defaultLang])
                if len(missing):
                    subpage = subpage.replace(u'%missing%', u"Languages missing: " + u', '.join(missing))
                else:
                    subpage = subpage.replace(u'%missing%', u"Supported languages: all")
            else: # Not a translation
                h.update('Any-')
                subkeys = data.keys()
                subkeys.sort()
                for k in subkeys:
                    ordered.append(k + u'=' + data[k])
                    unordered[k] = data[k]
                    h.update((k + u'=' + data[k]).encode('utf8'))
            if 'allTemplate' in self.dictionaries[currentDict] and (len(unordered) or len(self.dictionaries[currentDict]['allTemplate']['params'])):
                allKey = []
                keys = unordered.keys()
                keys.sort()
                for k in keys:
                    allKey.append(k + u'=' + unordered[k])
                insertIndex = 0
                if isTranslation and self.defaultLang in data:
                    insertIndex = 1
                ordered.insert(insertIndex, u(self.allKeyName) + u'=' + u(self.dictionaries[currentDict]['allTemplate'].replace(u'%options%', u'|'.join(allKey))))
            subpage = subpage.replace(u'%options%', u'|'.join(ordered))
        else: # No subkeys
            data = u(data)
            subpage = self.subpageTemplateID
            h.update(u(u'ID-' + data).encode('utf8'))
            subpage = subpage.replace(u'%string%', data)
        h = u(h.hexdigest())
        if keyName in syncData and syncData[keyName] == h:
            return # Same hash
        syncData[keyName] = h # Update sync data
        subpage = subpage.replace(u'%dictionary%', currentDict)
        subpage = subpage.replace(u'%dictionaryname%', self.dictionaries[currentDict]['name'])
        subpage = subpage.replace(u'%keyname%', keyName)
        self.scheduler.schedule(editPage, currentDict + self.subpageSeparator + keyName, subpage, summary=u'Pushed changes from [[:' + currentDict + u']] for string "' + keyName + u'".', minor=True, nocreate=False)
    def processComment(self, commentString, currentDict, definedStrings, syncData):
        commentContents = []
        for extractedStr in self.stringsExtract.finditer(commentString):
            comment = u
            if extractedStr.group(1):
                comment = u'# ' + u(extractedStr.group(1)) + u'\n'
            dataString = u(extractedStr.group(3))
            if dataString.find(u'\r') == -1 and dataString.find(u'\n') == -1: # Assume no subkeys
                data = dataString.strip()
                dataWriteback = u' ' + data
            else: # There's subkeys; detect whether this is a translation or not
                data = {}
                isTranslation = True
                for translation in self.translationExtract.finditer(dataString.rstrip()):
                    data[u(translation.group(1))] = u(translation.group(2))
                    if u(translation.group(1)) not in self.languages:
                        isTranslation = False
                ordered = []
                if isTranslation:
                    for lang in self.languages:
                        if lang in data:
                            ordered.append(u'  ' + lang + u': ' + data[lang])
                else: # Not a translation, so order in alphabetical order
                    subkeys = data.keys()
                    subkeys.sort()
                    for subk in subkeys:
                        ordered.append(u'  ' + subk + u': ' + data[subk])
                dataWriteback = u'\n' + u'\n'.join(ordered)
            keyNames = u(extractedStr.group(2)).lower().split(u'|')
            validKeyNames = []
            for keyName in keyNames:
                keyName = keyName.replace(u'_', u' ').strip()
                if keyName in definedStrings:
                    continue # Duplicate key
                definedStrings.append(keyName)
                validKeyNames.append(keyName)
                self.generateSubpage(keyName, data, currentDict, syncData)
            if len(validKeyNames):
                commentContents.append(comment + u' | '.join(validKeyNames) + u':' + dataWriteback)
        self.scheduler.execute()
        return u'\n\n'.join(commentContents)
    def __call__(self, content, **kwargs):
        if 'article' not in kwargs:
            return content
        if u(kwargs['article'].title) not in self.dictionaries:
            return content
        currentDict = u(kwargs['article'].title)
        syncPage = page(self.dictionaries[currentDict]['sync'])
        try:
            syncDataText = u(syncPage.getWikiText()).split(u'\n')
        except: # Page probably doesn't exist
            syncDataText = u
        syncData = {}
        for sync in syncDataText:
            sync = u(sync.strip())
            if not sync:
                continue
            sync = sync.split(u':', 2)
            if len(sync) == 2:
                syncData[sync[0]] = sync[1]
        oldSyncData = syncData.copy()
        newContent = u
        previousIndex = 0
        definedStrings = []
        for comment in self.commentsExtract.finditer(content):
            newContent += content[previousIndex:comment.start()]
            previousIndex = comment.end()
            # Process current comment
            newContent += u
        newContent += content[previousIndex:]
        # Check if we need to update sync data
        needUpdate = False
        for k in syncData:
            if k not in oldSyncData or oldSyncData[k] != syncData[k]:
                needUpdate = True
                break
        # Check for deleted strings
        for k in oldSyncData:
            if k not in definedStrings:
                try:
                    deletePage(currentDict + self.subpageSeparator + k, 'Removed deleted string "' + k + u'" from ' + currentDict + u'.')
                except:
                    pass
                if k in syncData:
                    del syncData[k]
                needUpdate = True
        if needUpdate:
            # Build syncdata string representation
            syncKeys = syncData.keys()
            syncKeys.sort()
            syncLines = []
            for k in syncKeys:
                syncLines.append(k + u':' + syncData[k])
            editPage(syncPage, u'\n'.join(syncLines), summary=u'Updated synchronization information for [[:' + currentDict + u']].', minor=True, nocreate=False)
        return newContent
    def scheduledRun(self):
        for d in self.dictionaries:
            fixPage(d)
dictUpdater = DictionaryUpdater()
addFilter(dictUpdater)
scheduleTask(dictUpdater.scheduledRun, 3)

File filters

Crush all PNG/JPG images

Delegated to CrushBOT.

Additional tasks

Update {{Blog link}}

def updateTemplateBlogLink():
    fConfig = {
        'page': 'Template:Blog link',
        'begin': u,
        'end': u,
        'firstElement': u'[http://www.teamfortress.com/post.php?id={{#switch:{{{1|ERROR}}}',
        'lastElement': u'#default = ERROR}} {{{2|{{{1|blog post}}}}}}]',
        'rss': 'http://www.teamfortress.com/rss.xml',
        'linkRegex': compileRegex(r'id=(\d+)'),
        'timeoffset': -7
    }
    bl = page(fConfig['page'])
    content = u(bl.getWikiText())
    originalContent = content
    begin, end = content.find(fConfig['begin']), content.find(fConfig['end'])
    if begin == -1 or end == -1:
        return # Couldn't find begin/end
    try:
        feed = feedparser.parse(fConfig['rss'])
    except:
        return
    results = [fConfig['firstElement']]
    entries = 0
    for i in feed.entries:
        res = fConfig['linkRegex'].search(i.link)
        date = datetime.datetime.fromtimestamp(time.mktime(i.updated_parsed)) + datetime.timedelta(hours=fConfig['timeoffset'])
        if res:
            entries += 1
            day = date.strftime('%d')
            if day[0] == '0':
                day = day[1:]
            results.append(u(date.strftime('%B ') + day + date.strftime(', %Y') + ' = ' + res.group(1)))
    results.append(fConfig['lastElement'])
    results = u'\n  | '.join(results)
    content = content[:begin+len(fConfig['begin'])] + results + content[end:]
    if content != originalContent and entries:
        editPage(bl, content, summary=u'Updated blog post list.')
# scheduleTask(updateTemplateBlogLink, 10)

Update {{List of item attributes}}

def updateItemAttributes():
    try:
        attributes = urllib2.urlopen('http://optf2.com/wiki/attributes').read(-1)
    except:
        return
    if not attributes or attributes.find('<!DOCTYPE') != -1:
        return
    editPage('Template:List of item attributes', '\n' + attributes, summary=u'Updated item attributes', minor=True)
#scheduleTask(updateItemAttributes, 20)

Update {{Item checklist}} on list of subscribers

def itemChecklists():
    def updateItemChecklist(checklist, schema, support):
        if not checklist.getParam('steamid'):
            checklist.setParam('error', 'Unspecified Steam ID.')
            return True
        supportedItems = {}
        for i in support:
            supportedItems[i] = 0
        try:
            steamUser = steam.user.profile(checklist.getParam('steamid'))
            backpack = steam.tf2.backpack(steamUser, schema=schema)
        except steam.user.ProfileError as e:
            checklist.setParam('error', u(e))
            return True
        except steam.tf2.TF2Error as e:
            if u(e) in (u'Bad SteamID64 given', u'Profile set to private'):
                checklist.setParam('error', u(e))
                return True
            return False
        for item in backpack:
            itemName = u(item.get_name()).lower()
            if itemName in supportedItems:
                supportedItems[itemName] += 1
        for item in supportedItems:
            if supportedItems[item] > 1:
                checklist.setParam(item, supportedItems[item])
            elif supportedItems[item] == 1:
                checklist.setParam(item, 'yes')
            else:
                p = checklist.getParam(item)
                if p is not None:
                    p = p.lower()
                if p in (None, 'no', '0'):
                    checklist.setParam(item, 'no')
                elif p not in ('wanted', 'want', 'do not', 'anti', 'do not want'):
                    checklist.setParam(item, 'had')
        return True
    try:
        schema = steamGetGameSchema(steam.tf2)
        allItems = []
        for item in schema:
            allItems.append(u(item.get_name()).lower())
    except:
        return # No schema means no fancy
    support = []
    templateParams = compileRegex(r'\{\{\{\s*([^{}|]+?)\s*\|')
    templateCode = page('Template:Item checklist').getWikiText()
    res = templateParams.search(templateCode)
    while res:
        item = u(res.group(1)).lower()
        if item not in support and item in allItems:
            support.append(item)
        templateCode = templateCode[res.end():]
        res = templateParams.search(templateCode)
    checkPage, checkLinks, checkKeys = linkExtract(page('User:WindBOT/Item_checklists').getWikiText())
    linksLeft = checkLinks.values()[:]
    for i in range(12):
        randLink = random.choice(linksLeft)
        linksLeft.remove(randLink)
        checklist = page(randLink.getLink())
        print 'Updating', checklist
        update = False
        oldContent = u(checklist.getWikiText())
        content, templatelist, templatekeys = templateExtract(oldContent)
        for t in templatelist.values():
            if t.getName().lower().find(u'item checklist') != -1:
                update = updateItemChecklist(t, schema, support)
                break
        content = templateRestore(content, templatelist, templatekeys)
        if update and oldContent != content:
            editPage(checklist, content, summary=u'Updated Item checklist [[:' + u(checklist.title) + u']]', minor=True)
scheduleTask(itemChecklists, 2)

Update {{Dictionary/defindex}} and {{Dictionary/price}}

def updateGameDictionaries():
	if steam is None:
		return
	# Begin configurable section
	game = steam.tf2
	priceDictionaryPage = u'Template:Dictionary/price'
	indexDictionaryPage = u'Template:Dictionary/defindex'
	itemEquivalences = {
		u'taunt: the high five!': u'high five!',
		u'taunt: the director\'s vision': u'director\'s vision',
		u'taunt: the schadenfreude': u'schadenfreude',
		u'taunt: the meet the medic': u'meet the medic (taunt)'
	}
	forceDefindex = {
		u'red-tape recorder': 810,
		u'huo-long heater': 811,
		u'flying guillotine': 812,
		u'neon annihilator': 813,
		u'triad trinket': 814,
		u'champ stamp': 815,
		u'marxman': 816,
		u'human cannonball': 817
	}
	illegalItemCharacters = compileRegex(r'[:%\r\n\t]')
	priceDictionaryHeader = u'{{dictionary/header}}\n== /price ==\n\'\'\'This dictionary is automatically updated by WindBOT\'\'\'. Edits made to this page will be overwritten.\n<!--\n'
 	indexDictionaryHeader = u'{{dictionary/header}}\n== /defindex ==\n\'\'\'This dictionary is automatically updated by WindBOT\'\'\'. Edits made to this page will be overwritten.\n<!--\n'
 	dictionaryFooter = u'\n-->'
	# End configurable section
	schema = steamGetGameSchema(game)
	assets = steamGetGameAssets(game)
	dictionary = {}
	defindex = {}
	for item in schema:
		try:
			itemName = u(item.get_name()).lower().replace(u'_', u' ')
			keys = [itemName]
			if itemName in itemEquivalences:
				itemName = itemEquivalences[itemName]
				keys.append(itemName)
			defindex[itemName] = u' | '.join([illegalItemCharacters.sub(u, x) for x in keys]) + u': ' + u(item.get_schema_id())
			prices = assets.get_price(item.get_schema_id())
			if not len(prices):
				continue
			dictionary[itemName] = itemName + u':'
			for currency in sorted(prices):
				price = prices[currency]
				if price.is_integer():
					price = u(int(price))
				else:
					price = u(round(price, 2))
					dot = price.find(u'.')
					if dot == len(price) - 2:
						price += u'0'
				dictionary[itemName] += u'\n  ' + u(currency).lower() + u': ' + price
		except:
			continue
	for item in forceDefindex:
		defindex[u(item)] = illegalItemCharacters.sub(u, u(item)) + u': ' + u(forceDefindex[item])
	if len(dictionary):
		# Is updating time doktor
		finalPage = priceDictionaryHeader
		for item in sorted(dictionary):
			finalPage += u'\n' + dictionary[item] + u'\n'
		finalPage += dictionaryFooter
		priceDictionaryPage = page(priceDictionaryPage)
		editPage(priceDictionaryPage, finalPage, summary=u'Updated item prices from WebAPI.', minor=True, bot=True, nocreate=True)
		# Run other filters on the new page
		fixPage(priceDictionaryPage)
	if len(defindex):
		# Is also updating time doktor
		finalPage = indexDictionaryHeader
		for item in sorted(defindex):
			finalPage += u'\n' + defindex[item] + u'\n'
		finalPage += dictionaryFooter
		indexDictionaryPage = page(indexDictionaryPage)
		editPage(indexDictionaryPage, finalPage, summary=u'Updated item indexes from WebAPI.', minor=True, bot=True, nocreate=True)
		# Run other filters on the new page
		fixPage(indexDictionaryPage)
scheduleTask(updateGameDictionaries, 2)