{{ title }}
Tool Labs – wmopbot, IRC bot for Wikimedia channels operators
{%- endif %} -{% block content %}{% endblock %} +{% block content %}{{ content|safe }}{% endblock %} {% if warn %}
{{ warn|safe }}
{% endif %}diff --git a/templates/base.html b/templates/base.html index cd4c6bf..93a9fd6 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,56 +1,55 @@
{{ warn|safe }}
{% endif %}{{ message }}
{% else %} {% endif %}To request a cloak use in IRC the command: /msg wmopbot cloak
Filter staus: go
Last {{ limit }} {{ queuestatus }} cloak requests:
# | Requester user | Acounts | Cloak requested | Status | History | +
---|---|---|---|---|---|
# | Requester user | Accounts | Cloak requested | Status | History | {% for id, requser, irc, wiki, bot, _type, cloak, editcloak, status, ts, data in requests -%}
{{ id }} | {{ requser.replace('!', ' !').replace('@', ' @')|safe }} |
irc: {{ irc }} wiki: {{ wiki }}{%- if bot %} bot: {{ bot }}{% endif %} |
{{ _type }} cloak request: {{ cloak }} {%- if usergroup == 'gc' %} {{ editcloak and "edited: " + editcloak or "" }}{# #} edit{%- else %}{%- if editcloak -%} edited: {{ editcloak }}{% endif %}{% endif %} |
{{ status }} |
{% for line in data.split('\n')[0].split(',') %}{% set line = line.split() -%}
{{ line[:-2]|join(' ') }} {{ line[-2:]|join(' ') }}{% if not loop.last %}
{{ user -}}:
save{% endif %} {%- endfor %} |
{%- for line in data.split('\n')[1:] %}
{{ line }}{% if not loop.last %} {% endif %} {%- endfor -%}{% if status == 'approved' %} Command to freenode staffs apply the cloak: /msg NickServ VHOST {{ irc }} ON {{ editcloak or cloak }}{% endif %} |
The cloak queue is restricted to GCs and Freenode staff
', + **userparams()) limit = 'limit' in request.args and request.args['limit'].isdigit() and int(request.args['limit']) or 20 args = [limit > 200 and 200 or limit] status = request.args.get('status', '') if status: args[0:0] = [status] queue = query('SELECT * FROM cloak WHERE cl_status = ? ORDER BY cl_id DESC LIMIT ?', tuple(args)) else: queue = query('SELECT * FROM cloak ORDER BY cl_id DESC LIMIT ?', tuple(args)) - usergroup = 'op' - if 'test' in request.args: - usergroup = request.args['test'] - elif chanList['#wikimedia-ops']['access'].get(session['user'], {}).get('template', '') == 'Contact': - usergorup = 'gc' - elif query("SELECT 1 FROM user WHERE us_account = ? and us_user LIKE '%@freenode/staff/%'", (session['user'],)): - usergroup = 'staff' for req in queue: data = req[-1].split('\n') if len(data) < 2 or not data[1]: checkuser(req[3]) return render_template('cloakqueue.html', title='Cloaks Queue', limit=limit, queuestatus=status, requests=queue[::-1], usergroup=usergroup, **userparams()) @app.route('/setcloak', methods=['POST']) def setcloak(): if not 'user' in session: return 'Error: not logged' with open('web.log', 'a') as f: f.write('setcloak: %r\n' % request.form) _id = request.form['id'] status, cloak = request.form.get('status'), request.form.get('cloak') r = query('SELECT cl_data FROM cloak WHERE cl_id = ?', (_id,)) if not r: return 'Error: id not found' data = r[0][0].split('\n', 1) history = data[0].split(',') new = '' if chanList['#wikimedia-ops']['access'].get(session['user'], {}).get('template', '') == 'Contact': if cloak: new = '%s: cloak edit %s' % (session['user'], time.strftime('%Y-%m-%d %H:%M:%S')) + if history and history[-1].startswith('%s: cloak edit ' % session['user']): + history = history[0:-1] data = ','.join(history + [new]) + '\n' + data[1] execute('UPDATE cloak SET cl_cloak_edit = ?, cl_data = ? WHERE cl_id = ?', (cloak, data, _id)) return '(ok)%s\n%s' % (cloak, new) - elif not status in ('+1', '-1', 'approved', 'rejected', 'cloaked'): + elif not status in ('approved', 'rejected', 'cloaked'): return 'Error: You can not set that status' elif not status: return 'Error: No status to set' elif query("SELECT 1 FROM user WHERE us_account = ? and us_user LIKE '%@freenode/staff/%'", (session['user'],)): - if not status in ('+1', '-1', 'cloaked'): + if status != 'cloaked': return 'Error: can not set that status' - elif history[-1].split()[1] in ('approved', 'rejected', 'cloaked'): - return 'Error: You can not review a request with that status' - elif session['user'] + ': ' in data[0]: - return 'Error: You may only review a request once' - elif not status in ('+1', '-1'): + else: return 'Error: You can not set that status' new = '%s: %s %s' % (session['user'], status, time.strftime('%Y-%m-%d %H:%M:%S')) data = ','.join(history + [new]) + '\n' + data[1] status = status in ('+1', '-1') and 'reviewed' or status execute("UPDATE cloak SET cl_status = ?, cl_data = ? WHERE cl_id = ?", (status, data, _id)) return '(ok)' + new @app.route('/redundant') def redundantBans(): "Generate a list of redundant bans for all channels" if not 'user' in session: return redirect_login() bans, quiets = [], [] for channel in sorted(c for c in chanList if c[0] == '#'): check = [channel] + [ban[3:] for ban in chanList[channel]['ban'] if ban.startswith('$j:#')] bans.extend([(channel, ban2check, ban + (checkChan != channel and ' in ' + checkChan or '')) for checkChan in check for ban in chanList[checkChan]['ban'] for ban2check in chanList[channel]['ban'] if (checkChan != channel and not ban.startswith('$j') if ban == ban2check else maskre(ban, ban2check))]) quiets.extend([(channel, quiet2check, quiet) for quiet in chanList[channel]['quiet'] for quiet2check in chanList[channel]['quiet'] if quiet != quiet2check and maskre(quiet, quiet2check)]) return render_template('redundant.html', title='Redundant bans and quiets', bans=bans, quiets=quiets, **userparams()) @app.route('/reload') def reloadchans(): if not 'user' in session: return redirect_login() loadChans() return render_template('chansreloaded.html', title='Chans reloaded', **userparams()) def notAuth(): return render_template('notauth.html', title=u'Not authenticated') @app.errorhandler(404) def page_not_found(error): return render_template('page_not_found.html', title=u'Page not found'), 404 def redirect_login(): returnto = request.full_path url = returnto and returnto != '/' and url_for('login', returnto=returnto) or url_for('login') r = redirect(url) return r def dbconn(): global _dbconn if _dbconn[0] + 10 > time.time(): return _dbconn[1] connection = oursql.connect(db='s53213__wmopbot', host='tools.labsdb', read_default_file=os.path.expanduser('~/replica.my.cnf'), read_timeout=10, use_unicode=False) c = connection.cursor() _dbconn = [time.time(), c] return c def query(sql, args=()): c = dbconn() c.execute(sql, args) return c.fetchall() def execute(sql, args=()): c = dbconn() c.execute(sql, args) def loadChans(): - global chanList, chanTable, chanLoaded + global chanList, chanTable, chanLoaded, chanacs chanList = defaultdict(lambda: {'access': {}, 'ban': {}, 'quiet': {}, 'mode': {}, 'exempt': {}, 'config': {}}) for chan, _type, target, args in query('SELECT * FROM list'): chanList[chan].setdefault(_type, {})[target.decode('utf-8', 'replace')] = \ {k: v.decode('utf-8', 'replace').replace('\;', ';') for k, v in (i.split(':', 1) for i in sep.split(args))} stats = query('SELECT st_channel, SUM(st_msg), (SUM(st_users) / COUNT(*)) FROM stats WHERE st_hour > (UNIX_TIMESTAMP() / 3600 - 168) GROUP BY st_channel') stats = {chan: (int(msg), int(usr)) for chan, msg, usr in stats} with open('.online') as f: online = f.read().split('\n')[1:] chanTable = [] for chan in chanList: if chan[0] != '#': continue ops = len([1 for args in chanList[chan]['access'].values() if 'o' in args['flags']]) wmfgc = 'wmfgc' in chanList[chan]['access'] and chanList[chan]['access']['wmfgc'].get('flags', '') or \ chanList[chan]['config'].get('update', {}).get('cserror') or '' bans = len([1 for b in chanList[chan]['ban']]) jbans = ', '.join([b for b in chanList[chan]['ban'] if b.startswith('$j:')]) modes = chanList[chan]['config'].get('cs/irc', {}).get('mode', '?') csflags = chanList[chan]['config'].get('cs/irc', {}).get('csflags', '') msg = chan in stats and stats[chan][0] or chan in online and '0' or '' users = chan in stats and stats[chan][1] or '' sigyn = 'Sigyn' in chanList[chan]['config'] and 'yes' or '' if 'WM' in chanList[chan]['config']: chanTable.append((chan, ops, wmfgc, bans, jbans, modes, csflags, msg, users, sigyn)) chanTable.sort() + chanacs = [(c, i[9:]) for c in chanList for i in chanList[c]['access'] + if i.startswith('$chanacs:') and 'o' in chanList[c]['access'][i].get('flags', '')] chanLoaded = int(time.time()) loadChans() def userparams(): user = session['user'] chans = query(r"SELECT ls_channel FROM list WHERE ls_type = 'access' AND ls_target = ? AND ls_args RLIKE 'flags:\\w*[Oo]\\w*;'", (user,)) - return {'user': user, 'userChans': [i[0] for i in chans]} + userChans = {i[0] for i in chans} + for chan, pull in chanacs: + if pull in chanList and user in chanList[pull]['access']: + userChans.add(chan) + return {'user': user, 'userChans': sorted(userChans)} app.add_template_filter(lambda t: t and t.isdigit() and time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(int(t))) or t, name='ftime') app.add_template_filter(lambda t: t.decode('utf-8', 'replace'), name='utf8') def checkuser(wikiacc): with open('web.log', 'a') as f: f.write('checkuser: %r\n' % userCheck) if not wikiacc in userCheck: userCheck[wikiacc] = [time.time(), None] if not userCheck[wikiacc][1] and not wikiacc in threadQueue: threadQueue.append(wikiacc) if threadQueue and not lock.locked(): thread.start_new_thread(checkuser_thread, ()) def checkuser_thread(): lock.acquire() try: while threadQueue: checkuser_thread2(threadQueue.pop(0)) for u, v in userCheck.items(): if v[0] + 86400 < time.time(): del userCheck[u] finally: lock.release() def checkuser_thread2(user): dbs = None wikis = {} dbtime = [] for s in ('s1', 's2', 's3', 's4', 's5', 's6', 's7'): connection = oursql.connect(host=s + '.labsdb', read_default_file=os.path.expanduser('~/replica.my.cnf'), read_timeout=10, use_unicode=True, autoreconnect=True, autoping=True) c = connection.cursor() if not dbs: c.execute('SELECT dbname, url FROM meta_p.wiki') dbs = {db[0].encode('utf-8') + '_p': db[1] for db in c.fetchall() if db[1]} c.execute('SHOW DATABASES') sdbs = [db[0] for db in c.fetchall() if db[0] in dbs] if not 'global' in wikis and 'centralauth_p' in sdbs: c.execute("SELECT gb_expiry FROM centralauth_p.globalblocks WHERE gb_address = ?", (user,)) block = c.fetchall() wikis['global'] = block and 'Global blocked (expiry: %s) ' % expirytime(block[0]) or None for db in sdbs: wikis[dbs[db]] = checkwiki(c, db, user) del dbs[db] c.connection.close() if not dbs: break connection.close() data = '%s: %s' % (user, wikis.pop('global', None) or '') wikis = sorted(((w, d) for w, d in wikis.iteritems() if d), key=lambda i:i[1][0], reverse=True) data += ', '.join('%s (%s)' % (w.split('//')[-1], d[1]) for w, d in wikis[:(len(wikis) > 8 and 6 or 8)]) if len(wikis) > 8: data += ', and more %d wikis with the average of %.1f edits per wiki' % (len(wikis) - 6, float(sum(d[0] for w, d in wikis[6:])) / (len(wikis) - 6)) userCheck[user][1] = data id_data = query('SELECT cl_id, cl_data FROM cloak WHERE cl_wikiacc = ?', (user,)) for cl_id, cl_data in id_data: cl_data = cl_data.split('\n') if len(cl_data) == 1: cl_data.append('') if not cl_data[1]: cl_data[1] = data execute('UPDATE cloak SET cl_data = ? WHERE cl_id = ?', ('\n'.join(cl_data), cl_id)) def checkwiki(c, db, user): c.execute("SELECT user_editcount, user_id FROM %s.user WHERE user_name = ?" % db, (user,)) count_id = c.fetchall() if not count_id or not count_id[0][0]: return False count, user_id = int(count_id[0][0]), int(count_id[0][1]) data = '%d edit%s' % (count, count != 1 and 's' or '') c.execute("SELECT ipb_expiry FROM %s.ipblocks_ipindex WHERE ipb_address = ?" % db, (user,)) block = c.fetchall() if block: data += ', blocked (expiry: %s)' % block c.execute("SELECT ug_group FROM %s.user_groups WHERE ug_user = ?" % db, (user_id,)) groups = [g[0] for g in c.fetchall()] if groups: data += ', ' + ', '.join(groups) return count, data def expirytime(e): return e.isdigit and '%s-%s-%s %s:%s:%s' % (e[0:4], e[4:6], e[6:8], e[8:10], e[10:12], e[12:14]) or e def irclower(s): "IRC lower case: acording RFC 2812, the characters {}|^ are the respective lower case of []\~" return s.lower().replace('{', '[').replace('}', ']').replace('|', '\\').replace('^', '~') mask2re = {'\\': '[\\\\|]', '|': '[\\\\|]', '^': '[~^]', '~': '[~^]', '[': '[[{]', ']': '[]}]', '{': '[[{]', '}': '[]}]', '*': '[^!@]*', '?': '[^!@]', '+': '\\+', '.': '\\.', '(': '\\(', ')': '\\)', '$': '\\$'} def maskre(mask, match=None): "Transforms an IRC mask into a regex pattern" mask = irclower(mask) regex = '' for c in mask: regex += mask2re.get(c, c) try: r = re.compile(regex + '$', re.I) except: r = None if not match: return r return r.match(match) if __name__ == '__main__': with open('.secret') as f: app.secret_key = f.read() if os.uname()[1].startswith('tools-webgrid'): app.config['APPLICATION_ROOT'] = '/wmopbot/' from flup.server.fcgi_fork import WSGIServer WSGIServer(app).run() else: # Run outside Labs for tests (no database) app.run() # print 'This tool does not work outside wmopbot project in Tool Labs'