capsConfig.py
# -*- coding: utf-8 -*-
config = {
'api': 'http://wiki.teamfortress.com/w/api.php',
'username': 'WindBOT',
'password': raw_input('Wiki password: '),
'logDir': 'logs',
'pages': {
'rules': 'User:WindBOT/WikiCapRules',
'results': 'User:WindBOT/WikiCapCandidates',
'log': 'User:WindBOT/WikiCapLog'
},
'mysql': {
'host': 'mysql.biringa.com',
'username': 'perot_irclog',
'password': raw_input('MySQL password: '),
'database': 'perot_irclog',
'table': 'tfwikirc'
}
}
countCaps.py
#!/usr/bin/python -OO
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import shutil
import re # Regular expressions
import wikitools # Wiki API
import datetime # Date manipulation and comparison
import MySQLdb # MySQL databases
import MySQLdb.cursors # Additional cursor classes for convenience
from capsConfig import config
config['runtime'] = {
'wiki': None,
'regexes': {}
}
config['rules'] = None
def pr(*args):
s = []
for i in args:
s.append(u(i))
s = u' '.join(s)
try:
print s
except:
pass
if config['output'] is not None:
try:
print >> config['output'], s
except:
pass
def u(s):
if type(s) is type(u''):
return s
if type(s) is type(''):
try:
return unicode(s)
except:
try:
return unicode(s.decode('utf8'))
except:
try:
return unicode(s.decode('windows-1252'))
except:
return unicode(s, errors='ignore')
try:
return unicode(s)
except:
try:
return u(str(s))
except:
return s
def wiki():
global config
if config['runtime']['wiki'] is None:
config['runtime']['wiki'] = wikitools.wiki.Wiki(config['api'])
pr('Logging in as', config['username'], '...')
config['runtime']['wiki'].login(config['username'], config['password'])
pr('Logged in.')
return config['runtime']['wiki']
def page(p):
if type(p) in (type(''), type(u'')):
p = wikitools.page.Page(wiki(), u(p), followRedir=False)
return p
def editPage(p, content, summary=u'', minor=True, bot=True, nocreate=True):
global config
summary = u(summary)
while len(summary) > 250:
if summary.find(u' ') == -1:
summary = summary[:summary.rfind(u' ')] + u'...'
else:
summary = summary[:247] + u'...'
result = page(p).edit(u(content), summary=summary, minor=minor, bot=bot, nocreate=nocreate)
return result
def compileRegex(regex, flags=re.IGNORECASE):
global config
regex = u(regex)
if regex in config['runtime']['regexes']:
return config['runtime']['regexes'][regex]
config['runtime']['regexes'][regex] = re.compile(regex, flags)
return config['runtime']['regexes'][regex]
def userSort(x, y):
global config
for k in config['rules']['sortKeys']:
reverse = False
if k[0] == '!':
k = k[1:]
reverse = True
if reverse:
r = cmp(y[k], x[k])
else:
r = cmp(x[k], y[k])
if r == 0:
continue
return r
return 0
def createUser(username):
global config
logFile = u(config['logDir']) + u(os.sep) + u(username).replace(u' ', u'_') + u'.log'
l = open(logFile, 'wb')
l.write('')
l.close()
return {
'name': username,
'score': 0.0,
'firstEdit': None,
'lastEdit': None,
'editDate': {},
'daysOff': 0,
'hasTalked': False,
'isBot': False,
'edits': 0,
'undone': 0,
'redirects': 0,
'newPages': 0,
'creationTime': None,
'log': logFile
}
def run():
global config, u
# Clear log directory
if os.path.exists(config['logDir']):
shutil.rmtree(config['logDir'])
os.mkdir(config['logDir'])
rulesText = u(page(config['pages']['rules']).getWikiText())[len('<p'+'re><now'+'iki>'):-len('</now'+'iki></pr'+'e>')]
pr('Rules:', rulesText)
config['rules'] = eval(rulesText)
rules = config['rules'] # Convenience
now = datetime.datetime.utcnow().replace(tzinfo=wikitools.utc.utc)
firstInspectedDay = now - datetime.timedelta(days = rules['inspectDuration'])
undoRegex = compileRegex(r'^(?:Undo edit by|Reverted edits by).*?\[\[Special:Contributions/([^]|]+)(?:.*?\((\d+)\))?')
mysql = MySQLdb.connect(host=config['mysql']['host'], user=config['mysql']['username'], passwd=config['mysql']['password'], db=config['mysql']['database'], cursorclass=MySQLdb.cursors.DictCursor)
cursor = mysql.cursor()
dbLimit = now - datetime.timedelta(days=rules['inspectDuration'] + rules['maxDaysOff'] + 1)
cursor.execute('SELECT * FROM `tfwikirc`')
stats = {}
editScores = {}
def log(user, *msg):
s = [u'<' + u(user) + u'>']
for i in msg:
s.append(u(i))
log = open(stats[user]['log'], 'ab')
log.write((u' '.join(s) + u'\n').encode('utf8'))
log.close()
pr(u'<' + u(user) + u'>', *msg)
while True:
row = cursor.fetchone()
if row == None:
break
row['timestamp'] = row['timestamp'].replace(tzinfo=wikitools.utc.utc)
#pr(row)
if not row['user'] in stats:
stats[row['user']] = createUser(row['user'])
rowScore = 0.0
for n in rules['namespaces']:
if rules['namespaces'][n]['id'] == row['namespace']:
if rules['namespaces'][n]['points'] > 0.0:
stats[row['user']]['edits'] += 1
# First, check if edit is not too old
if (now - row['timestamp']).days >= rules['inspectDuration']:
break
# Take care of daysOff
if stats[row['user']]['firstEdit'] is None:
stats[row['user']]['firstEdit'] = row['timestamp']
daysLost = max(0, (firstInspectedDay - row['timestamp']).days - 1)
if daysLost > 0:
log(row['user'], 'Days from beginning of inspected duration to first edit:', daysLost, '- Comparing', firstInspectedDay, '(inspected duration) to ', row['timestamp'], '(first edit)')
stats[row['user']]['daysOff'] += daysLost
else:
daysSinceLast = max(0, (row['timestamp'] - stats[row['user']]['lastEdit']).days - 1)
if daysSinceLast > 0:
log(row['user'], 'Days since last edit:', daysSinceLast, '- Comparing', stats[row['user']]['lastEdit'], '(previous edit) to', row['timestamp'], '(current edit)')
stats[row['user']]['daysOff'] += daysSinceLast
stats[row['user']]['lastEdit'] = row['timestamp']
# Handle consecutive edits
if row['title'] in stats[row['user']]['editDate']:
delta = row['timestamp'] - stats[row['user']]['editDate'][row['title']]
if delta.days * 86400 + delta.seconds < rules['consecutiveThreshold']:
stats[row['user']]['editDate'][row['title']] = row['timestamp']
break
stats[row['user']]['editDate'][row['title']] = row['timestamp']
# Now compute points
rowScore += rules['namespaces'][n]['points']
scoreBonus = rules['namespaces'][n]['points']
if row['flags'].find('N') != -1:
scoreBonus = scoreBonus * rules['newPageBonus'] + row['newsize'] * rules['newPageByteValue']
stats[row['user']]['newPages'] += 1
if row['flags'].find('R') != -1:
scoreBonus *= rules['redirect']
stats[row['user']]['redirects'] += 1
editScores[str(row['rcid'])] = scoreBonus
log(row['user'], 'got', scoreBonus, 'points for editing', row['title'], '(Flags:', row['flags']+')')
stats[row['user']]['score'] += scoreBonus
if row['flags'].find('b') != -1:
stats[row['user']]['isBot'] = True
# Handle undo's
undo = undoRegex.search(row['comment'])
if undo:
if undo.group(2) and undo.group(2) in editScores:
penalty = editScores[undo.group(2)] * rules['undoMultiplier']
else:
penalty = rules['undone']
if undo.group(1) not in stats:
stats[undo.group(1)] = createUser(undo.group(1))
log(undo.group(1), 'lost', abs(penalty), 'for edit undone on page', row['title'])
stats[undo.group(1)]['score'] += penalty
if row['comment'][:8] == 'Reverted': # Penalize users using rollback
log(row['user'], 'lost', abs(rules['rollbackPenalty']), 'points for using rollback on page', row['title'])
stats[row['user']]['score'] += rules['rollbackPenalty']
stats[undo.group(1)]['undone'] += 1
if rules['namespaces'][n]['isTalk']:
stats[row['user']]['hasTalked'] = True
break
users = []
pr('Building users table.')
ministatkeys = ('score', 'edits', 'daysOff', 'lastEdit')
for usr in stats:
ministats = {}
for k in ministatkeys:
ministats[k] = stats[usr][k]
log(usr, 'Info:', ministats)
if stats[usr]['lastEdit'] is None:
log(usr, 'Dropping - No edit')
continue
daysSinceLast = max(0, (now - stats[usr]['lastEdit']).days - 1) # Days from last edit to moment of counting
if daysSinceLast > 0:
log(usr, 'Dropping - Days since last edit until now:', daysSinceLast, '- Comparing', stats[usr]['lastEdit'], 'to', now)
stats[usr]['daysOff'] += daysSinceLast
if stats[usr]['isBot']: # Skip bots
log(usr, 'Dropping - It\'s a bot')
continue
if usr in rules['owners']:
log(usr, 'Dropping - Already has cap')
continue
if stats[usr]['score'] < rules['minScore']: # Skip too low scores
log(usr, 'Dropping - Score:', stats[usr]['score'], '<', rules['minScore'])
continue
if stats[usr]['edits'] < rules['minEdits']: # Skip too low edit count
log(usr, 'Dropping - Edits:', stats[usr]['edits'], '<', rules['minEdits'])
continue
if stats[usr]['daysOff'] > rules['maxDaysOff']: # Skip too many days off
log(usr, 'Dropping - Days off:', stats[usr]['daysOff'], '>', rules['maxDaysOff'])
continue
stats[usr]['creationTime'] = wikitools.user.User(wiki(), usr).registration
if (now - stats[usr]['creationTime']).days < rules['minAge']: # Skip too young accounts
pr(usr, 'Dropping - Age:', stats[usr]['creationTime'], '<', rules['minAge'])
continue
pr('Accepting candidate:', usr)
users.append(stats[usr])
config['users'] = users
pr('Sorting users.')
users = sorted(users, cmp=userSort, reverse=True)
pr('Building Wiki table.')
s = """{| class="wikitable grid sortable" align="center" style="text-align:center"
! class="header" style="font-size:90%;" | User
! class="header" style="font-size:90%;" | Score
! class="header" style="font-size:90%;" | Inactive days
! class="header" style="font-size:90%;" | Undone edits
! class="header" style="font-size:90%;" | Valid edits ratio
! class="header" style="font-size:90%;" | New pages
! class="header" style="font-size:90%;" | Redirects
! class="header" style="font-size:90%;" | Age
! class="header" style="font-size:90%;" | Edits (''all time'')
! class="header" style="font-size:90%;" | Has talked?
! class="header unsortable" style="font-size:90%;" | Contribs"""
s2 = u''
i = 0
for usr in users:
i += 1
if i > rules['maxCandidates']:
break
pr(usr['name'], usr['score'])
hasTalked = 'Yes'
if not usr['hasTalked']:
hasTalked = 'No'
s += '\n\
|-\n\
! {{subst:ul|' + usr['name'] + '}}\n\
| ' + str(round(usr['score'], 2)) + '\n\
| ' + str(usr['daysOff']) + ' (' + str(round(100.0 * (1.0-float(usr['daysOff'])/float(rules['inspectDuration'])), 2)) + '%)' + '\n\
| ' + str(usr['undone']) + '\n\
| ' + str(round((1.0-float(usr['undone'])/float(usr['edits']))*100.0, 2)) + '%\n\
| ' + str(usr['newPages']) + '\n\
| ' + str(usr['redirects']) + '\n\
| ' + str((now - usr['creationTime']).days) + ' days\n\
| ' + str(usr['edits']) + '\n\
| ' + hasTalked + '\n\
| [[Special:Contributions/' + usr['name'] + '|' + usr['name'] + '\'s Contribs]]'
s2 += u'=== ' + usr['name'] + u' ===\n<p'+'re><no'+'wiki>' + u(open(usr['log'], 'rb').read(-1)) + u'</no'+'wiki></pr'+'e>\n'
s+='\n\
|}'
editPage(config['pages']['results'], rules['template'].replace('%table%', s), summary=u'Updated Wiki Cap candidates list.', minor=False)
editPage(config['pages']['log'], s2, summary=u'Updated Wiki Cap log.', minor=False)
if __name__ == '__main__':
config['output'] = open('caps.txt', 'wb')
run()
config['output'].close()