#! /usr/bin/env 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 Queue import PriorityQueue
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

# lol lazy
global authorized_users, whitelist_file
global checkup_delay, monitor_delay

# ==============
# CONFIG

irc_nick = 'C80|Kaichou'
hash_channel = '#Comiket-Hashes'
authorized_users = 'authorized_users.txt'

watch_dir = u'/home/Comiket/uploads/'
ok_dir = u'/home/Comiket/validated/'
whitelist_file = u'whitelist.txt'

## 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
# ==============

# queues for checking completed files
# priorities: 1 = doujinshi, 2 = audio/games, 0 = intervention!
fileq = PriorityQueue()

# force a bit of garbage collection
with open(whitelist_file, encoding='utf-8') as x:
    whitelist = x.readlines()

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, ok_dir, fileq, monitor_delay):
        self.watch_dir = watch_dir
        self.fileq = fileq
        self.ok_dir = ok_dir
        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 WhiteList.fcheck(rdyfile):
                        dest = os.path.join(self.ok_dir, basefile)
                        os.rename(rdyfile, dest)
                        print '%s whitelist. File moved.' % basefile
                    else:
                        print '%s not found in whitelist. Please verify.' % basefile
            dir_old = dir_new

class WhiteListManager:
    """Provides whitelist and auth management.
    """
    
    cmds = ('adduser', 'add', 'remove', 'check', 'help')
    # flat file dbs woo
    def __init__(self, whitelist, auth):
        if os.path.exists(whitelist):
            self.whitelist = open(whitelist, encoding='utf-8').read().splitlines()
        else:
            self.whitelist = []
        if os.path.exists(auth):
            self.auth = open(auth, encoding='utf-8').read().splitlines()    # authorized users
        else:
            self.auth = []
        
    def adduser(self, user):
        user = user.decode('utf-8').strip()
        self.auth.append(user)
        with open(authorized_users, 'w', encoding='utf-8') as x:
            for line in self.auth: x.write(line + '\n')
        r = user + ' now authorized.'
        print r
        return r
        
    def add(self, entry):
        entry = entry.decode('utf-8').strip()
        self.whitelist.append(entry)
        with open(whitelist_file, 'w', encoding='utf-8') as x:
            for line in self.whitelist: x.write(line + '\n')
        print('Entry added to whitelist.')
        return 'Entry added to whitelist.'
        
    def remove(self, entry):
        entry = entry.decode('utf-8').strip()
        removed = False
        try:
            self.whitelist.remove(entry)
            removed = True
        except ValueError:
            for line in self.whitelist:
                if entry in line:
                    self.whitelist.remove(line)
                    removed = True
                    break
        with open(whitelist_file, 'w', encoding='utf-8') as x:
            for line in self.whitelist: x.write(line + '\n')
        if True:
            print('Entry removed from whitelist.')
            return 'Entry removed from whitelist.'
        return 'Entry not found.'
        
    def check(self, entry):
        entry = entry.decode('utf-8').strip()
        for line 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<type>\(.*?\))?\s?(?P<author>\[.*?\])?\s?(?P<title>.*?)\.(?:rar|zip)?', file).group('title')
        for line in self.whitelist:
            wfile, x, size, whash = self._parse_hashline(line)
            if file == file or (sha1 == hash or sha256 == hash) 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 WhitelistIRC(irc.IRCClient):
    # now including heartbeat from changeset 32066
    hostname = None
    _heartbeat = None
    heartbeatInterval = 120
    nickname = 'C80|Kaichou'
    
    def signedOn(self):
        self.msg('Nickserv', 'identify paipan')
        self.join(self.factory.channel)
    
    def joined(self, channel):
        print '%s joined.' % channel
    
    def privmsg(self, user, channel, msg):
        # user commands
        if msg.startswith('!wl '):
            reply = ''
            print(user)
            print(msg)
            try:
                cmd, entry = msg.replace('!wl ', '').split(' ', 1)
            # !wl help tmpfix
            except:
                cmd = msg.replace('!wl ', '')
            cmd = cmd.lower()
            if cmd in WhiteList.cmds and user in WhiteList.auth:
                if channel == self.nickname and not cmd in ('help', 'adduser'):
                    self.msg(user, 'Please use the channel. Executing command anyway.')
                reply = getattr(WhiteList, cmd)(entry)
            # unauthorized access - do nothing.
            elif cmd not in ('help', 'check') and user not in WhiteList.auth:
                pass
            # public access
            elif cmd in ('help', 'check'):
                reply = getattr(WhiteList, cmd)(entry)
            else:
                self.msg(channel, 'Invalid command. Please try again.')
            if reply:
                self.msg(channel, reply)
    
    # below is heartbeat code
    def connectionLost(self, reason): 
        basic.LineReceiver.connectionLost(self, reason) 
        self.stopHeartbeat() 
    
    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)
        
    def irc_RPL_WELCOME(self, prefix, params): 
        self.hostname = prefix 
        self._registered = True 
        self.nickname = self._attemptedNick 
        self.signedOn() 
        self.startHeartbeat()

class RoboticsBay(protocol.ClientFactory):
    # the class of the protocol to build when new connection is made
    protocol = WhitelistIRC

    def __init__(self, nickname, channel):
        self.channel = channel
        self.nickname = nickname

    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 main():
    global WhiteList
    print('Booting monitors up.')
    WhiteList = WhiteListManager(whitelist_file, authorized_users)
    FolderMon(watch_dir, ok_dir, fileq, monitor_delay).start()
    IRC = RoboticsBay(irc_nick, hash_channel)
    reactor.connectTCP("irc.rizon.net", 6667, IRC)
    reactor.run()

if __name__ == '__main__':
    main()

