#!/usr/bin/python # -*- coding: utf-8 -*- # Created by TinFoil @ Rizon for #Comiket # rev. C80-1 # deps: (at least) python2.6, Twisted, zope.interface from codecs import open from collections import namedtuple from Queue import Queue, PriorityQueue, Empty from twisted.words.protocols import irc from twisted.internet import reactor, protocol, task import hashlib import os import os.path import re import threading, thread import time # oh shit import sqlite3 import paramiko global watch_dir, valid_dir, ok_dir global checkup_delay # ============== # CONFIG irc_nick = 'C80|Kaichou' main_channel = '#comiket' hash_channel = '#comiket-hashes' staff_channel = '#comiket-staff' staff_password = 'paipan' # sql used for backup concurrency more than a db atm sec_db = 'sec.sql' data_db = 'data.sql' watch_dir = u'/home/Comiket/uploads/dirty/' valid_dir = u'/home/Comiket/uploads/validated/' ok_dir = u'/home/Comiket/release/' # seconds to wait until seeing if a file is done being modified/created checkup_delay = 2 # seconds to wait until seeing if there are any changes in the download directory monitor_delay = 2 # END CONFIG # ============== Database = namedtuple('Database', 'security, data') dbs = Database(security=sec_db, data=data_db) # sql db initialization # syntax: (dbname, 'field type,'x100) sec_dbinit = [('staff', 'hostname text, permissions integer'), ('security', 'hostname text, type integer, ban_time integer, ban_start integer'), ] data_dbinit = [('bots', 'id integer primary key, name text, role text, region text, pw text'), ('bots_creds', 'id integer primary key, server text, port integer, user text, pw text, folder text'), ('whitelist', 'entry text, hash_type integer'), ] # less painful channel organization here - {channel : password} channels = {'main': (main_channel, ''), 'hash' : (hash_channel, ''), 'staff' : (staff_channel, staff_password) } # queues for checking completed files # priorities: 1 = doujinshi, 2 = audio/games, 0 = intervention! fileq = PriorityQueue() # force a bit of garbage collection class FileMon(threading.Thread): def __init__(self, file, fileq, checkup_delay, priority): self.file = file self.fileq = fileq self.checkup_delay = checkup_delay self.priority = priority threading.Thread.__init__(self) def run(self): while 1: oldtime = os.stat(self.file).st_mtime time.sleep(self.checkup_delay) newtime = os.stat(self.file).st_mtime if oldtime == newtime: self.fileq.put((self.priority, self.file)) break return class FolderMon(threading.Thread): def __init__(self, watch_dir, valid_dir, ok_dir, data_db, WhiteList, fileq, monitor_delay): self.watch_dir = watch_dir self.valid_dir = valid_dir self.ok_dir = ok_dir self.data_db = data_db self.WhiteList = WhiteList self.fileq = fileq self.monitor_delay = monitor_delay threading.Thread.__init__(self) def run(self): print 'Folder monitor started.' dir_old = os.listdir(self.watch_dir) while 1: time.sleep(self.monitor_delay) dir_new = os.listdir(watch_dir) if not dir_old == dir_new: # something deleted with no other changes? who cares. if len([x for x in dir_old if x not in dir_new]) > 0 and [x for x in dir_new if x not in dir_old] == 0: continue # threaded monitoring makes us all competitive and shit fnum = 0 for newfile in [x for x in dir_new if x not in dir_old]: print 'New file detected. Monitoring %s...' % newfile if (u'音楽' or u'ゲーム') in newfile: priority = 2 elif u'同人音楽' in newfile: priority = 1 else: print ' - Defaulting to priority 2.' priority = 2 newfile = os.path.join(watch_dir, newfile) FileMon(newfile, self.fileq, checkup_delay, priority).start() fnum += 1 for x in xrange(fnum): rdyfile = self.fileq.get()[1] basefile = os.path.basename(rdyfile) if self.WhiteList.fcheck(rdyfile): dest = os.path.join(self.valid_dir, basefile) os.rename(rdyfile, dest) print '%s whitelist. File moved.' % basefile else: print '%s not found in whitelist. Please verify.' % basefile self.fileq.task_done() dir_old = dir_new class WhiteListManager: """Provides whitelist and auth management. """ cmds = ('adduser', 'add', 'remove', 'check', 'help') # flat file dbs woo def __init__(self, dbs): self.datacon = sqlite3.connect(dbs.data) self.seccon = sqlite3.connect(dbs.security) x = self.datacon.execute('select * from whitelist') y = self.seccon.execute('select hostname from staff where permissions in (0,1)') self.whitelist = x.fetchall() self.auth = y.fetchall() del x, y def adduser(self, user): user = user.decode('utf-8').strip() added = False for rowid, permissions in self.seccon.execute('select rowid, permissions from staff where hostname=?', user): if permissions < 1: with self.seccon as x: x.execute('insert or replace into staff(rowid, hostname, permissions) values (?,?,?)', (rowid, hostname, permissions)) else: with self.seccon as x: x.execute('insert or replace into staff(rowid, hostname, permissions) values (?,?,?)', (rowid, hostname, 1)) added = True break if added: self.auth.append(hostname, permissions) else: with self.seccon as x: x.execute('insert into staff(hostname, permissions) values (?,?,?)', (hostname, 1)) r = user + ' now authorized.' print r return r def add(self, entry): entry = entry.decode('utf-8').strip() if len(entry.split()[-1]) == '64': hash_type = 256 else: hash_type = 1 with self.datacon as x: x.execute('insert or replace into whitelist(entry, hash_type) values (?,?)', (entry, hash_type)) self.whitelist.append((entry, hash_type)) print('Entry added to whitelist.') return 'Entry added to whitelist.' def remove(self, entry): entry = entry.decode('utf-8').strip() if len(entry.split()[-1]) == '64': hash_type = 256 else: hash_type = 1 removed = False try: self.whitelist.remove((entry, hash_type)) with self.datacon as x: x.execute('delete from whitelist where entry=? and hash_type=?', (entry,hash_type)) removed = True except ValueError: for white, hash_type in self.whitelist: if entry in white: self.whitelist.remove((white, hash_type)) with self.datacon as x: x.execute('delete from whitelist where entry=? and hash_type=?', (white,hash_type)) removed = True break if removed: print('Entry removed from whitelist.') return 'Entry removed from whitelist.' return 'Entry not found.' def user_verify(self, user): for authorized in self.auth: if user in authorized: return True return False def check(self, entry): entry = entry.decode('utf-8').strip() for line, type in self.whitelist: if entry in line: reply = 'Whitelisted as: ' + line return reply.encode('utf-8') return 'File not whitelisted.' def fcheck(self, file): print ' - Now hashing...' sha1 = sha1_hash(file) title = re.search(r'(?:\(C80\))?\s?(?P\(.*?\))?\s?(?P\[.*?\])?\s?(?P.*?)\.(?:rar|zip)?', file).group('title') for line, type in self.whitelist: wfile, x, size, whash = self._parse_hashline(line) if file == file or (sha1 == whash or sha256 == whash) or title in line: return True print 'Filename and share hash not found. Checking with a PD hash...' sha256 = sha256_hash(file) for line in self.whitelist: if sha256 in line: return True return False def help(self, entry): if not entry: x = 'Commands: ' + ', '.join(self.cmds) + '. For further help, say !wl help command' else: if entry == 'adduser': return '(Auth needed) Give user whitelist modification permissions. Usage: !wl adduser nick!~name@hostname' elif entry == 'add': return '(Auth needed) Add an entry to the file whitelist; accepts both PD and Share information! Usage: !wl add ctrl+c contents' elif entry == 'remove': return '(Auth needed) Remove entry from the file whitelist. WARNING - accepts partial matches. Usage: !wl remove file/hash/filesize/etc' elif entry == 'check': return 'Checks if a file is in the whitelist; accepts partial matches. Usage: !wl check file/hash/filesize/etc' elif entry == 'help': return 'http://youtu.be/JvKIWjnEPNY?t=2m28s' return x def _parse_hashline(self, entry): entry = entry.strip().split(' ') file = '' uploader = '' size = '' _hash = '' # perfect dark hash if entry[0] == 'file': try: file == re.search(r'(?:\(C80\))?\s?(?P<type>\(.*?\))?\s?(?P<author>\[.*?\])?\s?(?P<title>.*?)\.(?:rar|zip)?', ' '.join(entry[1:])).group('title') uploader = entry[-3] size = entry[-2] _hash = entry[-1] except IndexError: pass else: try: file = ''.join(entry[:-3]) uploader = entry[-3] size = entry[-2] _hash = entry[-1] except IndexError: pass return (file, uploader, size, _hash) class BotManager(threading.Thread): # hardcoded until I find somewhere sensible to put it realregions = ['US', 'EU', 'AU'] Bot = namedtuple('Bot', 'id, name, role, region, pw') BotCreds = namedtuple('BotCreds', 'server, port, user, pw, folder') def __init__(self, dbs, clientorders, botmsgs, botcmds, channels): self.bots = [] self.botcreds = {} self.botmsgs = botmsgs self.botcmds = botcmds self.channels = channels self.clientorders = clientorders self.commanded = [] threading.Thread.__init__(self) def run(self): self.sec_db = sqlite3.connect(dbs.security) self.data_db = sqlite3.connect(dbs.data) for id, name, role, region, pw in self.data_db.execute('select * from bots'): self.bots.append(self.Bot(id, name, role, region, pw)) for server, port, user, pw, folder in self.data_db.execute('select server, port, user, pw, folder from bots_creds'): self.botcreds[id] = self.BotCreds(server, port, user, pw, folder) print 'BotWrangler started.' secondary = 0 # design: block on sending commands, and then subsequently block on msgs. Incoming messages are reactive only. while 1: cmd, args = self.factory.botcmds.get() print 'Bot command recieved.' # flush msgs while not self.botmsgs.empty(): self.botmsgs.get_nowait() try: # verboten commands if cmd in ['_upload_file', '_chmsg', 'announce', 'msg']: raise AttributeError getattr(self, cmd)(args) except AttributeError: print 'Invalid command.' self._chmsg('staff', 'Invalid command.') self.factory.botcmds.task_done() def help(self, args): args = args.decode('utf-8').strip() if not args: self._chmsg('staff', 'Commands: addbot, addfile, orderbots') elif args == 'help': self._chmsg('staff', 'http://youtu.be/JvKIWjnEPNY?t=2m28s') elif args.startswith('addbot'): self._chmsg('staff', 'Add bot to synchronized pack additions. !admin addbot name role region pw server port user ssh_pw folder') elif args == 'addfile': self._chmsg('staff', 'Add file (in release directory) to packlist. !admin addfile region role file') elif args == 'orderbots': self._chmsg('staff', 'Order all bots to do something. Does not listen for replies. !admin orderbots admin pw command') def orderbots(self, args): args = args.decode('utf-8').strip() if not args: self._chmsg('staff', 'Wrong syntax. (!admin orderbots cmd)') return for bot in self.bots: self.msg(bot.name, args) def addbot(self, args): args = args.decode('utf-8').strip() # (addbot) name role region pw server port user ssh_pw folder try: name, role, region, pw, server, port, user, ssh_pw, folder = args.split(' ') port = int(port) except ValueError: self._chmsg('staff', 'Wrong syntax. (!admin addbot name role region pw server port user ssh_pw folder)') return self.data_db.execute('insert into bots(name, role, region, pw) values (?,?,?,?)', (name, role, region, pw)) self.data_db.commit() for x in self.data_db.execute('select id from bots where name=?', (name,)): id = x self.data_db.execute('insert into bots_creds(server, port, user, pw, folder) values (?,?,?,?,?)', (name, role, region, pw, folder)) self.bots.append(self.Bot(id, name, role, region, pw, folder)) self.botcreds[id] = self.BotCreds(server, port, user, ssh_pw, folder) print 'Bot added.' self._chmsg('staff', 'Bot added.') def addfile(self, args): # (addfile) region role file pack = '' try: args = args.split(' ', 2) region = args[0] role = args[1] file = args[2] except IndexError: self._chmsg('staff', 'Wrong syntax. (!admin addfile region role file)') return for bot in self.bots: print bot if bot.region == region and bot.role == role: creds = self.botcreds[bot.id] try: _upload_file(file, creds) except: self._chmsg('staff', 'ERROR: upload to {0} failed.'.format(bot.name)) continue command = 'admin {0} add {1}'.format(bot.pw, file) # send commands self.clientorders.put((bot.name, command)) self.commanded.append(bot.name) for x in xrange(len(self.commanded)): replybot, reply = self.botmsgs.get() replybot = replybot.split('!')[0] if reply.startswith('Added'): self.commanded.remove(replybot) if not pack: pack = re.search(r'([0-9]{1,3})').group(1) self.botmsgs.task_done() for desolate in self.commanded: self._chmsg('staff', 'ERROR: could not add pack to {0}. Announcing without it.'.format(desolate)) # announce pack after additions try: self.announce(file, pack) except UnboundLocalError: self.announce(file, 'TEST') def announce(self, file, pack): if pack == 'TEST': self._chmsg('staff', 'TEST: Announcing pack {0}'.format(file)) self._chmsg('staff', 'Now available on all bots as pack #{0}: {1} -- all HOPs except me are bots!'.format(pack, file)) return self._chmsg(self.channels['staff'], 'Announcing pack {0}'.format(file)) self._chmsg(self.channels['main'], 'Now available on all bots as pack #{0}: {1} -- all HOPs except me are bots!'.format(pack, file)) def msg(self, recipient, msg): self.clientorders.put((recipient, msg)) def _chmsg(self, recipient, msg): recipient = self.channels[recipient][0] self.clientorders.put((recipient, msg)) def _upload_file(self, file, bot): transport = paramiko.Transport(bot.host, bot.port) transport.connect(username=bot.user, password=bot.pw) sftp = paramiko.SFTPClient.from_transport(transport) origin = os.path.join(ok_dir, file) dest = os.path.join(bot.folder, file) sftp.put(origin, dest) sftp.close() transport.close() class MultiPurposeClient(irc.IRCClient): # now including heartbeat from changeset 32066 hostname = None _heartbeat = None _orderloop = None heartbeatInterval = 120 orderloopInterval = 1 nickname = 'C80|Kaichou' def signedOn(self): self.WhiteList = self.factory.WhiteList self.msg('Nickserv', 'identify paipan') for channel, pw in self.factory.channels.itervalues(): if pw: self.join(channel, pw) else: self.join(channel) def joined(self, channel): print '%s joined.' % channel def privmsg(self, user, channel, msg): # whitelist logic if user == 'Nickserv': print user, msg if channel.lower() == self.factory.channels['hash'][0]: if msg.startswith('!wl '): reply = '' print user, msg try: cmd, entry = msg.replace('!wl ', '').split(' ', 1) except ValueError: entry = '' cmd = cmd.lower() if cmd in self.WhiteList.cmds and self.WhiteList.user_verify(user): if channel.lower() == self.nickname.lower() and not cmd in ('help', 'adduser'): self.msg(user, 'Please use the channel. Executing command anyway.') reply = getattr(self.WhiteList, cmd)(entry) # unauthorized access - do nothing. elif cmd not in ('help', 'check') and user not in self.WhiteList.auth: pass # public access elif cmd in ('help', 'check'): reply = getattr(self.WhiteList, cmd)(entry) else: self.msg(channel, 'Invalid command. Please try again.') if reply: self.msg(channel, reply) # TODO: notify user and and tmp ban from bots elif channel.lower() == self.factory.channels['main'][0]: if msg.startswith(('!list', '!find', '@find')) and channel.lower() == self.factory.channels['main'][0]: pass # bot commands elif channel.lower() == self.factory.channels['staff'][0]: if msg.lower().startswith('!admin'): print user, msg msg = msg.split(' ', 2) self.factory.botcmds.put((msg[1], msg[2])) # actual bots elif user in self.factory.bots and channel.lower() == self.nickname.lower(): print user, msg self.factory.botmsgs.put((user, msg)) # below is heartbeat code def connectionLost(self, reason): basic.LineReceiver.connectionLost(self, reason) self.stopHeartbeat() self.stopOrderloop() def _createHeartbeat(self): """ Create the heartbeat L{LoopingCall}. """ return task.LoopingCall(self._sendHeartbeat) def _sendHeartbeat(self): """ Send a I{PING} message to the IRC server as a form of keepalive. """ self.sendLine('PING ' + self.hostname) def stopHeartbeat(self): """ Stop sending I{PING} messages to keep the connection to the server alive. @since: 11.1 """ if self._heartbeat is not None: self._heartbeat.stop() self._heartbeat = None def startHeartbeat(self): """ Start sending I{PING} messages every L{IRCClient.heartbeatInterval} seconds to keep the connection to the server alive during periods of no activity. @since: 11.1 """ self.stopHeartbeat() if self.heartbeatInterval is None: return self._heartbeat = self._createHeartbeat() self._heartbeat.start(self.heartbeatInterval, now=False) # below is a special order list taken from a Queue, modeled after the heartbeat # Loops through the factory queue every second because WE'RE HARDCORE. def _createOrderloop(self): return task.LoopingCall(self._doOrderloop) def _doOrderloop(self): size = self.factory.clientorders.qsize() for x in xrange(size): # a little delay prevents OH JESUS FLOOD /BANNNED time.sleep(.01) try: user, msg = self.factory.clientorders.get() print user, msg except Empty: break self.msg(user, msg) def stopOrderloop(self): if self._orderloop is not None: self._orderloop.stop() self._orderloop = None def startOrderloop(self): self.stopOrderloop() if self.orderloopInterval is None: return self._orderloop = self._createOrderloop() self._orderloop.start(self.orderloopInterval, now=False) print 'Order loop initalized.' def irc_RPL_WELCOME(self, prefix, params): self.hostname = prefix self._registered = True self.nickname = self._attemptedNick self.signedOn() self.startHeartbeat() self.startOrderloop() class RoboticsBay(protocol.ClientFactory): # the class of the protocol to build when new connection is made protocol = MultiPurposeClient def __init__(self, dbs, channels, WhiteList): self.channels = channels self.dbs = dbs self.WhiteList = WhiteList self.bots = [] x = sqlite3.connect(self.dbs.data) for bot in x.execute('select name from bots'): self.bots.append(bot) # syntax: (user, msg) self.clientorders = Queue() # syntax: (cmd, user, args) self.botmsgs = Queue() self.botcmds = Queue() # thread a BotWrangler self.BotWrangler = BotManager(self.dbs, self.clientorders, self.botcmds, self.botmsgs, self.channels) self.BotWrangler.factory = self self.BotWrangler.start() print 'Connecting to Server.' def clientConnectionLost(self, connector, reason): """If we get disconnected, reconnect to server.""" connector.connect() def clientConnectionFailed(self, connector, reason): print "connection failed:", reason reactor.stop() def sha1_hash(filename): file = open(filename, 'rb') hasher = hashlib.sha1() while True: data = file.read(160) if not data: break hasher.update(data) return hasher.hexdigest() def sha256_hash(filename): file = open(filename, 'rb') hasher = hashlib.sha256() while True: data = file.read(256) if not data: break hasher.update(data) return hasher.hexdigest() def prepare_dbs(): # create sql tables if missing con = sqlite3.connect(dbs.data) for db, values in data_dbinit: con.execute('create table if not exists {0} ({1})'.format(db, values)) con.commit() con.close() con = sqlite3.connect(dbs.security) for db, values in sec_dbinit: con.execute('create table if not exists {0} ({1})'.format(db, values)) con.execute('insert or ignore into staff(rowid, hostname, permissions) values (?,?,?)', (1, 'TinFoil!eviltape@the.damn.train.cj', 0)) con.commit() con.close() def main(): print 'Preparing SQLite dbs and booting monitors.' prepare_dbs() WhiteList = WhiteListManager(dbs) FolderMon(watch_dir, valid_dir,ok_dir, dbs.data, WhiteList, fileq, monitor_delay).start() IRC = RoboticsBay(dbs, channels, WhiteList) reactor.connectTCP("irc.rizon.net", 6667, IRC) reactor.run() if __name__ == '__main__': main()