#!/usr/bin/python import ConfigParser, sys, logging, socket, time, re, requests, urllib from urllib import quote # DBism import psycopg2, psycopg2.extras import psycopg2.extensions psycopg2.extensions.register_type(psycopg2.extensions.UNICODE) psycopg2.extensions.register_type(psycopg2.extensions.UNICODEARRAY) import time, datetime from datetime import datetime ############################################################################## cfg = ConfigParser.ConfigParser() ############################################################################## # Single mandatory arg: config file path if len(sys.argv[1:]) != 1: # If no args, print usage and exit: print sys.argv[0] + " CONFIG" exit(0) # Read Config cfg.readfp(open(sys.argv[1])) # Get log path logpath = cfg.get("bofh", "log") # Get IRCism debug toggle irc_dbg = cfg.get("irc", "irc_dbg") if irc_dbg == 1: log_lvl = logging.DEBUG else: log_lvl = logging.INFO # Init logo logging.basicConfig(filename=logpath, filemode='a', level=log_lvl, format='%(asctime)s %(levelname)s %(message)s', datefmt='%d-%b-%y %H:%M:%S') # Date format used in log lines Date_Short_Format = "%Y-%m-%d" # Date format used in echoes Date_Long_Format = "%Y-%m-%d %H:%M:%S" ############################################################################## # Get the remaining knob values: try: # IRCism: Buf_Size = int(cfg.get("tcp", "bufsize")) Timeout = int(cfg.get("tcp", "timeout")) TX_Delay = float(cfg.get("tcp", "t_delay")) Servers = [x.strip() for x in cfg.get("irc", "servers").split(',')] Port = int(cfg.get("irc", "port")) Nick = cfg.get("irc", "nick") Pass = cfg.get("irc", "pass") Channels = [x.strip() for x in cfg.get("irc", "chans").split(',')] Join_Delay = int(cfg.get("irc", "join_t")) Prefix = cfg.get("control", "prefix") # DBism: DB_Name = cfg.get("db", "db_name") DB_User = cfg.get("db", "db_user") DB_DEBUG = cfg.get("db", "db_debug") # Logism: Base_URL = cfg.get("logotron", "base_url") Era = int(cfg.get("logotron", "era")) NewChan_Idx = int(cfg.get("logotron", "newchan_idx")) Src_URL = cfg.get("logotron", "src_url") except Exception as e: print "Invalid config: ", e exit(1) ############################################################################## # Connect to the given DB try: db = psycopg2.connect("dbname=%s user=%s" % (DB_Name, DB_User)) except Exception: print "Could not connect to DB!" logging.error("Could not connect to DB!") exit(1) else: logging.info("Connected to DB!") ############################################################################## def close_db(): db.close() def exec_db(query, args=()): cur = db.cursor(cursor_factory=psycopg2.extras.RealDictCursor) if (DB_DEBUG): logging.debug("query: '{0}'".format(query)) if (DB_DEBUG): logging.debug("args: '{0}'".format(args)) cur.execute(query, args) def query_db(query, args=(), one=False): cur = db.cursor(cursor_factory=psycopg2.extras.RealDictCursor) if (DB_DEBUG): logging.debug("query: '{0}'".format(query)) cur.execute(query, args) rv = cur.fetchone() if one else cur.fetchall() if (DB_DEBUG): logging.debug("query res: '{0}'".format(rv)) return rv def rollback_db(): cur = db.cursor(cursor_factory=psycopg2.extras.RealDictCursor) cur.execute("ROLLBACK") db.commit() def commit_db(): cur = db.cursor(cursor_factory=psycopg2.extras.RealDictCursor) db.commit() ############################################################################## # IRCism ############################################################################## # Used to compute 'uptime' time_last_conn = datetime.now() # Init socket: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Set keepalive: sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) # Initially we are not connected to anything connected = False # Connect to given host:port; return whether connected def connect(host, port): logging.info("Connecting to %s:%s" % (host, port)) sock.settimeout(Timeout) try: sock.connect((host, port)) except (socket.timeout, socket.error) as e: logging.warning(e) return False except Exception as e: logging.exception(e) return False else: logging.info("Connected.") return True # Attempt connect to each of hosts, in order, on port; return whether connected def connect_any(hosts, port): for host in hosts: if connect(host, port): return True return False # Transmit IRC message def send(message): global connected if not connected: logging.warning("Tried to send while disconnected?") return False time.sleep(TX_Delay) logging.debug("> '%s'" % message) message = "%s\r\n" % message try: sock.send(message.encode("utf-8")) except (socket.timeout, socket.error) as e: logging.warning("Socket could not send! Disconnecting.") connected = False return False except Exception as e: logging.exception(e) return False # Speak given message on a selected channel def speak(channel, message): send("PRIVMSG #%s :%s" % (channel, message)) # Now save what the bot spoke: save_line(datetime.now(), channel, Nick, False, message) # Standard incoming IRC line (excludes fleanode liquishit, etc) irc_line_re = re.compile("""^:([^!]+)\!\S+\s+PRIVMSG\s+\#(\S+)\s+\:(.*)""") # The '#' prevents interaction via PM, this is not a PM-able bot. # 'Actions' irc_act_re = re.compile(""".*ACTION\s+(.*)""") # A line was received from IRC def received_line(line): # Process the traditional pingpong if line.startswith("PING"): send("PONG " + line.split()[1]) else: logging.debug("< '%s'" % line) standard_line = re.search(irc_line_re, line) if standard_line: # Break this line into the standard segments (user, chan, text) = [s.strip() for s in standard_line.groups()] # Determine whether this line is an 'action' : action = False act = re.search(irc_act_re, line) if act: action = True text = act.group(1) # This line is edible, process it. eat_logline(user, chan, text, action) # IRCate until we get disconnected def irc(): global connected # Connect to one among the specified servers, in given priority : while not connected: connected = connect_any(Servers, Port) # Save time of last successful connect time_last_conn = datetime.now() # Auth to server send("NICK %s\r\n" % Nick) send("USER %s %s %s :%s\r\n" % (Nick, Nick, Nick, Nick)) # If this is a production bot, rather than test, there will be a PW: if Pass != "": send("NICKSERV IDENTIFY %s %s\r\n" % (Nick, Pass)) time.sleep(Join_Delay) # wait to join until fleanode eats auth # Join selected channels for chan in Channels: logging.info("Joining channel '%s'..." % chan) send("JOIN #%s\r\n" % chan) while connected: try: data = sock.recv(Buf_Size) except socket.timeout as e: logging.debug("Listen timed out") continue except socket.error as e: logging.warning("Listen socket error, disconnecting.") connected = False continue except Exception as e: logging.exception(e) connected = False continue else: if len(data) == 0: logging.warning("Listen socket closed, disconnecting.") connected = False continue try: try: data = data.strip(b'\r\n').decode("utf-8") except UnicodeDecodeError: data = data.strip(b'\r\n').decode('latin-1') for l in data.splitlines(): received_line(l) continue except Exception as e: logging.exception(e) continue ############################################################################## html_escape_table = { "&": "&", '"': """, "'": "'", ">": ">", "<": "<", } def html_escape(text): res = ("".join(html_escape_table.get(c,c) for c in text)) return urllib.quote(res.encode('utf-8')) searcher_re = re.compile("""(\d+) Results""") # Retrieve a search result count using the WWWistic frontend. # This way it is not necessary to have query parser in two places. # However it is slightly wasteful of CPU (requires actually loading results.) def get_search_res(chan, query): try: esc_q = html_escape(query) url = Base_URL + "log-search?q=" + esc_q + "&chan=" + chan res = requests.get(url).text t = res[res.find('