#!/usr/local/bin/python

"""
  This is GoogleBot v2. The original was written with irclib, which turned out
  to be utter pants.
  This version uses twistedwords which I expect is a lot better.
  The idea behind this was taken from the ircLogBot example that ships with
  twistedwords, however the code is almost completely new (barring a few
  borrowed snippets which were great as-is).

  The bot looks for messages in the channel, written as:
    GoogleBot: <message>
  If the message starts with 'quote' followed by a string, the string is looked
  up as a financial quote, otherwise a Google search is conducted.

  This script takes two arguments, first is the network ID, second is the
  logfile name.

  In future, it will always connect to one admin network, and from there join
  other networks as instructed.
"""

# system imports
import ConfigParser
import csv
import datetime
import os
import pickle
import random
import re
import string
import sys
import threading
import time

# twisted imports
import types
from twisted.application import internet, service, app
from twisted.conch import manhole
from twisted.conch.insults import insults
from twisted.conch import manhole_ssh
from twisted.cred import checkers, portal
from twisted.internet import ssl, reactor, protocol
from twisted.python import log
from twisted.words.protocols import irc
from OpenSSL import SSL

# googlebot imports
import imp

triggers = {}
find_triggers = {}
googlebot_mod_dir = os.path.abspath('modules')
sys.path.append(googlebot_mod_dir)
for module_file in os.listdir(googlebot_mod_dir):
  module_name, ext = os.path.splitext(module_file)
  if ext == '.py':
    module_location = imp.find_module(module_name)
    module = imp.load_module(module_name, *module_location)
    globals()[module_name] = module
    try:
      for word in module.module_triggers:
        triggers[word] = module_name
    except AttributeError:
      pass
    try:
      for word in module.module_find_triggers:
        find_triggers[word] = module_name
    except AttributeError:
      pass


class BotThreads(threading.Thread):
  """Generic threading doohicky"""

  def __init__(self, requested_module, requested_keyword, msg, irc_session, channel):
    self.requested_module = requested_module
    self.requested_keyword = requested_keyword
    self.msg = msg
    self.irc_session = irc_session
    self.channel = channel
    threading.Thread.__init__(self)

  def run(self):
    print 'Firing %s, with msg "%s", from %s' % \
        (self.requested_keyword, self.msg, self.channel)
    self.requested_module.Gather(self.requested_keyword, self.msg,
        self.irc_session, self.channel)


class MessageLogger:
  """
  An independant logger class (because separation of application
  and protocol logic is a good thing).
  """
  def __init__(self, file):
      self.file = file

  def log(self, message):
      """Write a message to the file."""
      timestamp = time.strftime('[%H:%M:%S]', time.localtime(time.time()))
      self.file.write('%s %s\n' % (timestamp, message))
      self.file.flush()

  def close(self):
      self.file.close()


class GoogleBot(irc.IRCClient):
  """A logging IRC bot."""
  
  networks_config = ConfigParser.ConfigParser()
  networks_config.readfp(open('config/networks.conf'))
  for section in networks_config.sections():
    if section == sys.argv[1]:
      try:
        nickname = networks_config.get(section, 'nickname')
      except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
        nickname = 'GoogleBot'
      try:
        username = networks_config.get(section, 'username')
      except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
        username = 'GoogleBot'
      try:
        password = networks_config.get(section, 'server_password')
      except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
        pass
      try:
        realname = networks_config.get(section, 'realname')
      except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
        realname = 'avleen\'s twisted googlebot'
    
  def connectionMade(self):
    irc.IRCClient.connectionMade(self)
    self.logger = MessageLogger(open(self.factory.filename, 'a'))
    self.logger.log('[connected at %s]' % 
        time.asctime(time.localtime(time.time())))
    self.channels_config_mtime = 0
    self.ignore_users_config_mtime = 0

  def connectionLost(self, reason):
    irc.IRCClient.connectionLost(self, reason)
    self.logger.log('[disconnected at %s]' % 
    time.asctime(time.localtime(time.time())))
    self.logger.close()


  # callbacks for events
  def signedOn(self):
    """Called when bot has succesfully signed on to server."""
    for channel in self.factory.channels:
      self.join(channel)
#      self.msg(channel, 'Hello, I am GoogleBot v2, if you find any problems '
#               'with me, please let Av know. Thanks!')

  def joined(self, channel):
    """This will get called when the bot joins the channel."""
    self.logger.log('[I have joined %s]' % channel)

  def privmsg(self, user, channel, msg):
    """ This will get called when the bot receives a message.
    Preferences for each channel are stored in config/channels.conf
    """
    user = user.split('!', 1)[0]
    if (self.ignore_users_config_mtime == 0
      or ignore_users_config_mtime != os.path.getmtime('config/ignore_users.conf')):
        ignore_users_config = self.LoadIgnoreUsersConfig()
    if user in ignore_users_config.options('Ignore Users'):
      return
    self.logger.log('%s: <%s> %s' % (channel, user, msg))
    
    # Load per-channel configs
    if (self.channels_config_mtime == 0
      or channels_config_mtime != os.path.getmtime('config/channels.conf')):
        channels_config = self.LoadChannelConfig()

    # Check to see if they're sending me a private message
    if channel == self.nickname and user == 'Av':
        if msg.startswith('join'):
          self.join(msg.split()[-1])
        if msg.startswith('part'):
          self.part(msg.split()[-1])

    if (msg.startswith(self.nickname)):
      requested_keyword = msg.split()[1]
    else:
      requested_keyword = msg.split()[0]
    if requested_keyword == 'default':
      return
    if requested_keyword in triggers.keys():
      requested_module = triggers[requested_keyword]
      option_enabled = self.CheckChannelConfig(channels_config, channel,
                                               requested_module)
      if not option_enabled:
        return
      print 'Firing %s, with msg "%s", from %s' % (requested_keyword, msg, channel)
      requested_module = imp.load_module(requested_module, *imp.find_module(requested_module))
      #BotThreads(requested_module, requested_keyword, msg, self, channel).start()
      requested_module.Gather(requested_keyword, msg, self, channel)
      return

    for requested_keyword in find_triggers:
      if msg.find(requested_keyword) != -1:
        requested_module = find_triggers[requested_keyword]
        option_enabled = self.CheckChannelConfig(channels_config, channel,
                                                 requested_module)
        if not option_enabled:
          return
        print 'Looking up %s, from %s' % (msg, channel)
        requested_module = imp.load_module(requested_module, *imp.find_module(requested_module))
        requested_module.Gather(requested_keyword, msg, self, channel)
        return

    if msg.startswith(self.nickname + ':'):
      requested_module = triggers['default']
      option_enabled  = self.CheckChannelConfig(channels_config, channel,
                                                requested_module)
      if not option_enabled:
        return
      requested_module = imp.load_module(requested_module, *imp.find_module(requested_module))
      requested_module.Gather('default', msg, self, channel)
      return

  def action(self, user, channel, msg):
    """This will get called when the bot sees someone do an action."""
    user = user.split('!', 1)[0]
    self.logger.log('* %s %s' % (user, msg))

  # irc callbacks
  def irc_NICK(self, prefix, params):
    """Called when an IRC user changes their nickname."""
    old_nick = prefix.split('!')[0]
    new_nick = params[0]
    self.logger.log('%s is now known as %s' % (old_nick, new_nick))

  def LoadChannelConfig(self):
    """ Called to reload the channels configuration file
    """
    channels_config = ConfigParser.ConfigParser()
    channels_config.readfp(open('config/channels.conf'))
    self.channel_config_mtime = os.path.getmtime('config/channels.conf')
    return channels_config

  def LoadIgnoreUsersConfig(self):
    """ Called to reload the ignored users configuration file
    """
    ignore_users_config = ConfigParser.ConfigParser()
    ignore_users_config.readfp(open('config/ignore_users.conf'))
    self.ignore_usersconfig_mtime = os.path.getmtime('config/ignore_users.conf')
    return ignore_users_config

  def CheckChannelConfig(self, channels_config, channel, option):
    try:
      channels_config.get(channel, option)
    except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
      if channels_config.get('default', option) == 'no':
        return False
      else:
        return True
    else:
      if channels_config.get(channel, option) == 'no':
        return False
      else:
        return True

class GoogleBotFactory(protocol.ClientFactory):
  """A factory for GoogleBots.
  A new protocol instance will be created each time we connect to the server.
  """

  # the class of the protocol to build when new connection is made
  protocol = GoogleBot

  def __init__(self, channels, filename):
    self.channels = channels
    self.filename = filename

  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()


_TOOLS = {}

def num():
  return len(gc.get_objects())

_TOOLS['num'] = num
_TOOLS['grc'] = sys.getrefcount

def Manhole(ns=None):
  ns = ns or {}
  ns.update(_TOOLS)
  checker = checkers.InMemoryUsernamePasswordDatabaseDontUse(username="password")

  def chainProtocolFactory():
      return insults.ServerProtocol(manhole.ColoredManhole, ns)

  rlm = manhole_ssh.TerminalRealm()
  rlm.chainedProtocolFactory = chainProtocolFactory
  ptl = portal.Portal(rlm, [checker])
  f = manhole_ssh.ConchFactory(ptl)
  return f


if __name__ == '__main__':
  # initialize logging
  log.startLogging(sys.stdout)

  # parse config files
  networks_config = ConfigParser.ConfigParser()
  networks_config.readfp(open('config/networks.conf'))

  for section in networks_config.sections():
    if section == sys.argv[1]:
      for row in csv.reader([networks_config.get(section, 'channels')]):
        channels = row
      server = networks_config.get(section, 'server')
      port = int(networks_config.get(section, 'port'))
      logfile = networks_config.get(section, 'logfile')
      ssl_connect = networks_config.get(section, 'ssl_connect')

      # create factory protocol and application
      f = GoogleBotFactory(channels, logfile)
      if ssl_connect == 'yes':
        reactor.connectSSL(server, port, f, ssl.ClientContextFactory())
      else:
        reactor.connectTCP(server, port, f)
      print 'Server: %s\nPort: %s' % (server, port)
      break

  # start manhole
  #ns = {'GoogleBot': GoogleBot}
  #fact = Manhole(ns)
  #reactor.listenTCP(5022, fact)

  # start listening
  reactor.run()
