From 19b7a95d7616435c590f40bfd8b15150dd58e7a8 Mon Sep 17 00:00:00 2001 From: dave Date: Sun, 6 Nov 2016 23:00:15 +0000 Subject: [PATCH] Make some stuff nicer --- bin/pyircbot | 35 ++-- pyircbot/irccore.py | 197 ++++++++++++----------- pyircbot/modules/AttributeStorage.py | 4 +- pyircbot/modules/AttributeStorageLite.py | 4 +- pyircbot/modules/CryptoWalletRPC.py | 6 +- pyircbot/modules/DogeRPC.py | 4 +- pyircbot/modules/DogeScramble.py | 4 +- pyircbot/modules/LinkTitler.py | 4 +- pyircbot/modules/Triggered.py | 2 +- pyircbot/pyircbot.py | 149 ++++++++--------- 10 files changed, 206 insertions(+), 203 deletions(-) diff --git a/bin/pyircbot b/bin/pyircbot index baa2d18..f181c6d 100755 --- a/bin/pyircbot +++ b/bin/pyircbot @@ -1,32 +1,33 @@ #!/usr/bin/env python3 -import os import sys import logging -from optparse import OptionParser +from argparse import ArgumentParser from pyircbot import PyIRCBot -from time import sleep if __name__ == "__main__": " logging level and facility " - logging.basicConfig(level=logging.DEBUG, format="%(asctime)-15s %(levelname)-8s %(filename)s:%(lineno)d %(message)s") + logging.basicConfig(level=logging.INFO, format="%(asctime)-15s %(levelname)-8s %(filename)s:%(lineno)d %(message)s") log = logging.getLogger('main') - + " parse command line args " - parser = OptionParser() - parser.add_option("-c", "--config", action="store", type="string", dest="config", help="Path to config file") - - (options, args) = parser.parse_args() - - log.debug(options) - - if not options.config: + parser = ArgumentParser(description="Run pyircbot") + parser.add_argument("-c", "--config", help="Path to config file", required=True) + parser.add_argument("--debug", action="store_true", help="Dump raw irc network") + + args = parser.parse_args() + + if args.debug: + logging.getLogger().setLevel(logging.DEBUG) + log.debug(args) + + if not args.config: log.critical("No bot config file specified (-c). Exiting.") sys.exit(0) - - botconfig = PyIRCBot.load(options.config) - + + botconfig = PyIRCBot.load(args.config) + log.debug(botconfig) - + bot = PyIRCBot(botconfig) try: bot.loop() diff --git a/pyircbot/irccore.py b/pyircbot/irccore.py index 5c5ef15..8b3ff54 100644 --- a/pyircbot/irccore.py +++ b/pyircbot/irccore.py @@ -16,30 +16,36 @@ import sys from inspect import getargspec from socket import SHUT_RDWR from threading import Thread -from time import sleep,time +from time import sleep, time +from collections import namedtuple try: from cStringIO import StringIO except: from io import BytesIO as StringIO +IRCEvent = namedtuple("IRCEvent", "args prefix trailing") +UserPrefix = namedtuple("UserPrefix", "nick username hostname") +ServerPrefix = namedtuple("ServerPrefix", "hostname") + + class IRCCore(asynchat.async_chat): - + def __init__(self): asynchat.async_chat.__init__(self) - + self.connected=False """If we're connected or not""" - + self.log = logging.getLogger('IRCCore') """Reference to logger object""" - + self.buffer = StringIO() """cStringIO used as a buffer""" - + self.alive = True """True if we should try to stay connected""" - + self.server = 0 """Current server index""" self.servers = [] @@ -48,22 +54,22 @@ class IRCCore(asynchat.async_chat): """Server port""" self.ipv6 = False """Use IPv6?""" - + self.OUTPUT_BUFFER_SIZE = 1000 self.SEND_WAIT = 0.800 self.outputQueue = queue.PriorityQueue(self.OUTPUT_BUFFER_SIZE) self.outputQueueRunner = OutputQueueRunner(self) self.outputQueueRunner.start() - + # IRC Messages are terminated with \r\n self.set_terminator(b"\r\n") - + # Set up hooks for modules self.initHooks() - + # Map for asynchat self.asynmap = {} - + def loop(self): while self.alive: try: @@ -83,10 +89,10 @@ class IRCCore(asynchat.async_chat): except Exception as e2: self.log.error("Error reconnecting: ") self.log.error(IRCCore.trace()) - + def kill(self, message="Help! Another thread is killing me :(", alive=False): """Send quit message, flush queue, and close the socket - + :param message: Quit message :type message: str :param alive: True causes a reconnect after disconnecting @@ -105,24 +111,24 @@ class IRCCore(asynchat.async_chat): self.socket.shutdown(SHUT_RDWR) self.close() self.log.info("Kill complete") - + " Net related code here on down " - + def getBuf(self): """Return the network buffer and clear it""" self.buffer.seek(0) data = self.buffer.read() self.buffer = StringIO() return data - + def collect_incoming_data(self, data): """Recieve data from the IRC server, append it to the buffer - + :param data: the data that was recieved :type data: str""" - #self.log.debug("<< %(message)s", {"message":repr(data)}) + #self.log.info("<< %(message)s", {"message":repr(data)}) self.buffer.write(data) - + def found_terminator(self): """A complete command was pushed through, so clear the buffer and process it.""" line = None @@ -135,70 +141,71 @@ class IRCCore(asynchat.async_chat): self.log.error("found_terminator(): repr(data): %s" % repr(line)) self.log.error("found_terminator(): error: %s" % str(ude)) return + self.log.debug("< {}".format(line)) self.process_data(line) - + def handle_close(self): """Called when the socket is disconnected. Triggers the _DISCONNECT hook""" - self.log.debug("handle_close") + self.log.info("handle_close") self.connected=False self.close() self.fire_hook("_DISCONNECT") - + def handle_error(self, *args, **kwargs): """Called on fatal network errors.""" self.log.error("Connection failed (handle_error)") self.log.error(str(args)) self.log.error(str(kwargs)) self.log.error(IRCCore.trace()); - + def _connect(self): """Connect to IRC""" self.server+=1 if self.server >= len(self.servers): self.server=0 serverHostname = self.servers[self.server] - self.log.debug("Connecting to %(server)s:%(port)i", {"server":serverHostname, "port":self.port}) + self.log.info("Connecting to %(server)s:%(port)i", {"server":serverHostname, "port":self.port}) socket_type = socket.AF_INET if self.ipv6: self.log.info("IPv6 is enabled.") socket_type = socket.AF_INET6 socketInfo = socket.getaddrinfo(serverHostname, self.port, socket_type) self.create_socket(socket_type, socket.SOCK_STREAM) - self.log.debug("Socket created: %s" % self.socket.fileno()) + self.log.info("Socket created: %s" % self.socket.fileno()) self.connect(socketInfo[0][4]) - self.log.debug("Connection established") + self.log.info("Connection established") self._fileno = self.socket.fileno() self.asynmap[self._fileno] = self # http://willpython.blogspot.com/2010/08/multiple-event-loops-with-asyncore-and.html self.log.info("_connect: Socket map: %s" % str(self.asynmap)) - + def handle_connect(self): """When asynchat indicates our socket is connected, fire the _CONNECT hook""" self.connected=True - self.log.debug("handle_connect: connected") + self.log.info("handle_connect: connected") self.fire_hook("_CONNECT") - self.log.debug("handle_connect: complete") - + self.log.info("handle_connect: complete") + def sendRaw(self, text, prio=2): """Queue messages (raw string) to be sent to the IRC server - + :param text: the string to send :type text: str""" text = (text+"\r\n").encode("UTF-8").decode().encode("UTF-8") self.outputQueue.put((prio, text), block=False) - + def process_data(self, data): """Process one line of tet irc sent us - + :param data: the data to process :type data: str""" if data.strip() == "": return - + prefix = None command = None args=[] trailing=None - + if data[0]==":": prefix=data.split(" ")[0][1:] data=data[data.find(" ")+1:] @@ -218,8 +225,8 @@ class IRCCore(asynchat.async_chat): self.log.warning("Unknown command: cmd='%s' prefix='%s' args='%s' trailing='%s'" % (command, prefix, args, trailing)) else: self.fire_hook(command, args=args, prefix=prefix, trailing=trailing) - - + + " Module related code " def initHooks(self): """Defines hooks that modules can listen for events of""" @@ -251,7 +258,7 @@ class IRCCore(asynchat.async_chat): '266', # :irc.129irc.com 266 CloneABCD :Current Global Users: 49 Max: 53 '332', # :chaos.esper.net 332 xBotxShellTest #xMopx2 :/ #XMOPX2 / https://code.google.com/p/pyircbot/ (Channel Topic) '333', # :chaos.esper.net 333 xBotxShellTest #xMopx2 xMopxShell!~rduser@108.170.60.242 1344370109 - '353', # :irc.129irc.com 353 CloneABCD = #clonea :CloneABCD CloneABC + '353', # :irc.129irc.com 353 CloneABCD = #clonea :CloneABCD CloneABC '366', # :irc.129irc.com 366 CloneABCD #clonea :End of /NAMES list. '372', # :chaos.esper.net 372 xBotxShell :motd text here '375', # :chaos.esper.net 375 xBotxShellTest :- chaos.esper.net Message of the Day - @@ -261,10 +268,10 @@ class IRCCore(asynchat.async_chat): ] " mapping of hooks to methods " self.hookcalls = {command:[] for command in self.hooks} - + def fire_hook(self, command, args=None, prefix=None, trailing=None): """Run any listeners for a specific hook - + :param command: the hook to fire :type command: str :param args: the list of arguments, if any, the command was passed @@ -273,20 +280,20 @@ class IRCCore(asynchat.async_chat): :type prefix: str :param trailing: data payload of the command :type trailing: str""" - + for hook in self.hookcalls[command]: try: if len(getargspec(hook).args) == 2: hook(IRCCore.packetAsObject(args, prefix, trailing)) else: hook(args, prefix, trailing) - + except: self.log.warning("Error processing hook: \n%s"% self.trace()) - + def addHook(self, command, method): """**Internal.** Enable (connect) a single hook of a module - + :param command: command this hook will trigger on :type command: str :param method: callable method object to hook in @@ -297,10 +304,10 @@ class IRCCore(asynchat.async_chat): else: self.log.warning("Invalid hook - %s" % command) return False - + def removeHook(self, command, method): """**Internal.** Disable (disconnect) a single hook of a module - + :param command: command this hook triggers on :type command: str :param method: callable method that should be removed @@ -313,10 +320,10 @@ class IRCCore(asynchat.async_chat): else: self.log.warning("Invalid hook - %s" % command) return False - + def packetAsObject(args, prefix, trailing): """Given an irc message's args, prefix, and trailing data return an object with these properties - + :param args: list of args from the IRC packet :type args: list :param prefix: prefix object parsed from the IRC packet @@ -324,38 +331,31 @@ class IRCCore(asynchat.async_chat): :param trailing: trailing data from the IRC packet :type trailing: str :returns: object -- a IRCEvent object with the ``args``, ``prefix``, ``trailing``""" - - return type('IRCEvent', (object,), { - "args": args, - "prefix": IRCCore.decodePrefix(prefix) if prefix else None, - "trailing": trailing - }) - + + return IRCEvent(args, + IRCCore.decodePrefix(prefix) if prefix else None, + trailing) + " Utility methods " @staticmethod def decodePrefix(prefix): """Given a prefix like nick!username@hostname, return an object with these properties - + :param prefix: the prefix to disassemble :type prefix: str :returns: object -- an UserPrefix object with the properties `nick`, `username`, `hostname` or a ServerPrefix object with the property `hostname`""" if "!" in prefix: - ob = type('UserPrefix', (object,), {}) - ob.str = prefix - ob.nick, prefix = prefix.split("!") - ob.username, ob.hostname = prefix.split("@") - return ob + nick, prefix = prefix.split("!") + username, hostname = prefix.split("@") + return UserPrefix(nick, username, hostname) else: - ob = type('ServerPrefix', (object,), {}) - ob.str = prefix - ob.hostname = prefix - return ob - + return ServerPrefix(prefix) + @staticmethod def trace(): """Return the stack trace of the bot as a string""" return traceback.format_exc() - + @staticmethod def fulltrace(): """Return the stack trace of the bot as a string""" @@ -372,25 +372,25 @@ class IRCCore(asynchat.async_chat): result += line + "\n" result += "\n*** STACKTRACE - END ***\n" return result - + " Data Methods " def get_nick(self): """Get the bot's current nick - + :returns: str - the bot's current nickname""" return self.nick - + " Action Methods " def act_PONG(self, data): """Use the `/pong` command - respond to server pings - + :param data: the string or number the server sent with it's ping :type data: str""" self.sendRaw("PONG :%s" % data) - + def act_USER(self, username, hostname, realname): """Use the USER protocol command. Used during connection - + :param username: the bot's username :type username: str :param hostname: the bot's hostname @@ -398,34 +398,34 @@ class IRCCore(asynchat.async_chat): :param realname: the bot's realname :type realname: str""" self.sendRaw("USER %s %s %s :%s" % (username, hostname, self.servers[self.server], realname)) - + def act_NICK(self, newNick): """Use the `/nick` command - + :param newNick: new nick for the bot :type newNick: str""" self.nick = newNick self.sendRaw("NICK %s" % newNick) - + def act_JOIN(self, channel): """Use the `/join` command - + :param channel: the channel to attempt to join :type channel: str""" self.sendRaw("JOIN %s"%channel) - + def act_PRIVMSG(self, towho, message): """Use the `/msg` command - + :param towho: the target #channel or user's name :type towho: str :param message: the message to send :type message: str""" self.sendRaw("PRIVMSG %s :%s"%(towho,message)) - + def act_MODE(self, channel, mode, extra=None): """Use the `/mode` command - + :param channel: the channel this mode is for :type channel: str :param mode: the mode string. Example: +b @@ -436,19 +436,19 @@ class IRCCore(asynchat.async_chat): self.sendRaw("MODE %s %s %s" % (channel,mode,extra)) else: self.sendRaw("MODE %s %s" % (channel,mode)) - + def act_ACTION(self, channel, action): """Use the `/me ` command - + :param channel: the channel name or target's name the message is sent to :type channel: str :param action: the text to send :type action: str""" self.sendRaw("PRIVMSG %s :\x01ACTION %s"%(channel,action)) - + def act_KICK(self, channel, who, comment=""): """Use the `/kick ` command - + :param channel: the channel from which the user will be kicked :type channel: str :param who: the nickname of the user to kick @@ -456,14 +456,14 @@ class IRCCore(asynchat.async_chat): :param comment: the kick message :type comment: str""" self.sendRaw("KICK %s %s :%s" % (channel, who, comment)) - + def act_QUIT(self, message): """Use the `/quit` command - + :param message: quit message :type message: str""" self.sendRaw("QUIT :%s" % message, prio=0) - + class OutputQueueRunner(Thread): """Rate-limited output queue""" def __init__(self, bot): @@ -471,7 +471,7 @@ class OutputQueueRunner(Thread): self.bot = bot #reference to main bot thread self.log = logging.getLogger('OutputQueueRunner') self.paused = False - + def run(self): """Constantly sends messages unless bot is disconnecting/ed""" lastSend = time() @@ -481,24 +481,25 @@ class OutputQueueRunner(Thread): if sinceLast < self.bot.SEND_WAIT: toSleep = self.bot.SEND_WAIT - sinceLast sleep(toSleep) - + # Pop item and execute if self.bot.connected and not self.paused: try: self.process_queue_item() lastSend = time() except queue.Empty: - #self.log.debug("Queue is empty") + #self.log.info("Queue is empty") pass sleep(0.01) - + def process_queue_item(self): """Remove 1 item from queue and process it""" prio,text = self.bot.outputQueue.get(block=True, timeout=10) - #self.log.debug("%s>> %s" % (prio,text)) + #self.log.info("%s>> %s" % (prio,text)) self.bot.outputQueue.task_done() + self.log.debug("> {}".format(text.decode('UTF-8'))) self.bot.send(text) - + def clear(self): """Discard all items from queue""" length = self.bot.outputQueue.qsize() @@ -507,9 +508,9 @@ class OutputQueueRunner(Thread): self.bot.outputQueue.get(block=False) except queue.Empty: pass - #self.log.debug("output queue cleared") + #self.log.info("output queue cleared") return length - + def flush(self): """Process all items in queue""" for i in range(0, self.bot.outputQueue.qsize()): @@ -517,4 +518,4 @@ class OutputQueueRunner(Thread): self.process_queue_item() except: pass - #self.log.debug("output queue flushed") + #self.log.info("output queue flushed") diff --git a/pyircbot/modules/AttributeStorage.py b/pyircbot/modules/AttributeStorage.py index bb7ced2..624b097 100755 --- a/pyircbot/modules/AttributeStorage.py +++ b/pyircbot/modules/AttributeStorage.py @@ -166,9 +166,9 @@ class AttributeStorage(ModuleBase): if value == None: # delete it c = self.db.connection.query("DELETE FROM `values` WHERE `itemid`=%s AND `attributeid`=%s ;", (itemId, attributeId)) - self.log.debug("AttributeStorage: Stored item %s attribute %s value: %s (Deleted)" % (itemId, attributeId, value)) + self.log.info("AttributeStorage: Stored item %s attribute %s value: %s (Deleted)" % (itemId, attributeId, value)) else: # add attribute c = self.db.connection.query("REPLACE INTO `values` (`itemid`, `attributeid`, `value`) VALUES (%s, %s, %s);", (itemId, attributeId, value)) - self.log.debug("AttributeStorage: Stored item %s attribute %s value: %s" % (itemId, attributeId, value)) + self.log.info("AttributeStorage: Stored item %s attribute %s value: %s" % (itemId, attributeId, value)) c.close() diff --git a/pyircbot/modules/AttributeStorageLite.py b/pyircbot/modules/AttributeStorageLite.py index 0f2322b..19b692a 100755 --- a/pyircbot/modules/AttributeStorageLite.py +++ b/pyircbot/modules/AttributeStorageLite.py @@ -161,9 +161,9 @@ class AttributeStorageLite(ModuleBase): if value == None: # delete it c = self.db.query("DELETE FROM `values` WHERE `itemid`=? AND `attributeid`=? ;", (itemId, attributeId)) - self.log.debug("Stored item %s attribute %s value: %s (Deleted)" % (itemId, attributeId, value)) + self.log.info("Stored item %s attribute %s value: %s (Deleted)" % (itemId, attributeId, value)) else: # add attribute c = self.db.query("REPLACE INTO `values` (`itemid`, `attributeid`, `value`) VALUES (?, ?, ?);", (itemId, attributeId, value)) - self.log.debug("Stored item %s attribute %s value: %s" % (itemId, attributeId, value)) + self.log.info("Stored item %s attribute %s value: %s" % (itemId, attributeId, value)) c.close() diff --git a/pyircbot/modules/CryptoWalletRPC.py b/pyircbot/modules/CryptoWalletRPC.py index bdba03e..dc28db9 100755 --- a/pyircbot/modules/CryptoWalletRPC.py +++ b/pyircbot/modules/CryptoWalletRPC.py @@ -121,13 +121,13 @@ class BitcoinRPC: def connect(self): # internal. connect to the service - self.log.debug("CryptoWalletRPC: %s: Connecting to %s:%s" % (self.name, self.host,self.port)) + self.log.info("CryptoWalletRPC: %s: Connecting to %s:%s" % (self.name, self.host,self.port)) try: self.con = AuthServiceProxy("http://%s:%s@%s:%s" % (self.username, self.password, self.host, self.port)) except Exception as e: - self.log.debug("CryptoWalletRPC: %s: Could not connect to %s:%s: %s" % (self.name, self.host, self.port, str(e))) + self.log.info("CryptoWalletRPC: %s: Could not connect to %s:%s: %s" % (self.name, self.host, self.port, str(e))) return - self.log.debug("CryptoWalletRPC: %s: Connected to %s:%s" % (self.name, self.host, self.port)) + self.log.info("CryptoWalletRPC: %s: Connected to %s:%s" % (self.name, self.host, self.port)) diff --git a/pyircbot/modules/DogeRPC.py b/pyircbot/modules/DogeRPC.py index 85272eb..9912fa6 100755 --- a/pyircbot/modules/DogeRPC.py +++ b/pyircbot/modules/DogeRPC.py @@ -73,7 +73,7 @@ class DogeController: def connect(self): "Connect to RPC endpoint" - self.log.debug("DogeRPC: Connecting to dogecoind") + self.log.info("DogeRPC: Connecting to dogecoind") self.con = AuthServiceProxy("http://%s:%s@%s:%s" % (self.config["username"], self.config["password"], self.config["host"], self.config["port"])) self.con.getinfo() - self.log.debug("DogeRPC: Connected to %s:%s" % (self.config["host"], self.config["port"])) + self.log.info("DogeRPC: Connected to %s:%s" % (self.config["host"], self.config["port"])) diff --git a/pyircbot/modules/DogeScramble.py b/pyircbot/modules/DogeScramble.py index 406dcea..b395150 100755 --- a/pyircbot/modules/DogeScramble.py +++ b/pyircbot/modules/DogeScramble.py @@ -145,7 +145,7 @@ class scrambleGame: self.nextTimer.start() self.guesses=0 self.category_count+=1 - self.master.log.debug("DogeScramble: category_count is: %s" % (self.category_count)) + self.master.log.info("DogeScramble: category_count is: %s" % (self.category_count)) if self.category_count >= self.change_category_after_words: self.should_change_category = True else: @@ -247,7 +247,7 @@ class scrambleGame: picked = f.readline().strip().lower() f.close() - self.master.log.debug("DogeScramble: picked %s for %s" % (picked, self.channel)) + self.master.log.info("DogeScramble: picked %s for %s" % (picked, self.channel)) self.lastwords.append(picked) if len(self.lastwords) > 5: self.lastwords.pop(0) diff --git a/pyircbot/modules/LinkTitler.py b/pyircbot/modules/LinkTitler.py index c994356..0cd35b6 100755 --- a/pyircbot/modules/LinkTitler.py +++ b/pyircbot/modules/LinkTitler.py @@ -111,13 +111,13 @@ class LinkTitler(ModuleBase): def url_headers(self, url): "HEAD requests a url to check content type & length, returns something like: {'type': 'image/jpeg', 'size': '90583'}" - self.log.debug("url_headers(%s)" % (url,)) + self.log.info("url_headers(%s)" % (url,)) resp = head(url=url, allow_redirects=True) return resp.headers def url_htmltitle(self, url): "Requests page html and returns title in a safe way" - self.log.debug("url_htmltitle(%s)" % (url,)) + self.log.info("url_htmltitle(%s)" % (url,)) resp = get(url=url, stream=True) # Fetch no more than first 10kb # if the title isn't seen by then, you're doing it wrong diff --git a/pyircbot/modules/Triggered.py b/pyircbot/modules/Triggered.py index 5f1d300..a7a7e43 100755 --- a/pyircbot/modules/Triggered.py +++ b/pyircbot/modules/Triggered.py @@ -41,6 +41,6 @@ class Triggered(ModuleBase): def scream(self, channel): delay = randrange(self.config["mindelay"], self.config["maxdelay"]) - self.log.debug("Sleeping for %s seconds" % delay) + self.log.info("Sleeping for %s seconds" % delay) sleep(delay) self.bot.act_PRIVMSG(channel, choice(self.config["responses"])) diff --git a/pyircbot/pyircbot.py b/pyircbot/pyircbot.py index 1667158..65c3af8 100644 --- a/pyircbot/pyircbot.py +++ b/pyircbot/pyircbot.py @@ -12,38 +12,42 @@ import sys import traceback from pyircbot.rpc import BotRPC from pyircbot.irccore import IRCCore +from collections import namedtuple import os.path +ParsedCommand = namedtuple("ParsedCommand", "command args args_str message") + + class PyIRCBot: """:param botconfig: The configuration of this instance of the bot. Passed by main.py. :type botconfig: dict """ version = "4.0.0-r03" - + def __init__(self, botconfig): self.log = logging.getLogger('PyIRCBot') """Reference to logger object""" - + self.botconfig = botconfig """saved copy of the instance config""" - + """storage of imported modules""" self.modules = {} - + """instances of modules""" self.moduleInstances = {} - + self.rpc = BotRPC(self) """Reference to BotRPC thread""" - + self.irc = IRCCore() """IRC protocol class""" self.irc.servers = self.botconfig["connection"]["servers"] self.irc.port = self.botconfig["connection"]["port"] self.irc.ipv6 = True if self.botconfig["connection"]["ipv6"]=="on" else False - + self.irc.addHook("_DISCONNECT", self.connection_closed) - + # legacy support self.act_PONG = self.irc.act_PONG self.act_USER = self.irc.act_USER @@ -56,26 +60,26 @@ class PyIRCBot: self.act_QUIT = self.irc.act_QUIT self.get_nick = self.irc.get_nick self.decodePrefix = IRCCore.decodePrefix - - # Load modules + + # Load modules self.initModules() - + # Connect to IRC self.connect() - + def connect(self): try: self.irc._connect() except: self.log.error("Pyircbot attempted to connect and failed!") self.log.error(traceback.format_exc()) - + def loop(self): self.irc.loop() - + def disconnect(self, message, reconnect=True): """Send quit message and disconnect from IRC. - + :param message: Quit message :type message: str :param reconnect: True causes a reconnection attempt to be made after the disconnect @@ -83,10 +87,10 @@ class PyIRCBot: """ self.log.info("disconnect") self.irc.kill(message=message, alive=reconnect) - + def kill(self, sys_exit=True, message="Help! Another thread is killing me :("): """Shut down the bot violently - + :param sys_exit: True causes sys.exit(0) to be called :type sys_exit: bool :param message: Quit message @@ -94,39 +98,39 @@ class PyIRCBot: """ #Close all modules self.closeAllModules() - + self.irc.kill(message=message, alive=not sys_exit) - + if sys_exit: sys.exit(0) - + def connection_closed(self, args, prefix, trailing): """Called when the socket is disconnected. We will want to reconnect. """ if self.irc.alive: self.log.warning("Connection was lost. Reconnecting in 5 seconds.") time.sleep(5) self.connect() - + def initModules(self): """load modules specified in instance config""" " append module location to path " sys.path.append(os.path.dirname(__file__)+"/modules/") - + " append usermodule dir to beginning of path" for path in self.botconfig["bot"]["usermodules"]: sys.path.insert(0, path+"/") - + for modulename in self.botconfig["modules"]: self.loadmodule(modulename) - + def importmodule(self, name): """Import a module - + :param moduleName: Name of the module to import :type moduleName: str""" " check if already exists " if not name in self.modules: - self.log.debug("Importing %s" % name) + self.log.info("Importing %s" % name) " attempt to load " try: moduleref = __import__(name) @@ -140,13 +144,13 @@ class PyIRCBot: else: self.log.warning("Module %s already imported" % name) return (False, "Module already imported") - + def deportmodule(self, name): """Remove a module's code from memory. If the module is loaded it will be unloaded silently. - + :param moduleName: Name of the module to import :type moduleName: str""" - self.log.debug("Deporting %s" % name) + self.log.info("Deporting %s" % name) " unload if necessary " if name in self.moduleInstances: self.unloadmodule(name) @@ -158,10 +162,10 @@ class PyIRCBot: " delete copy that python stores in sys.modules " if name in sys.modules: del sys.modules[name] - + def loadmodule(self, name): """Activate a module. - + :param moduleName: Name of the module to activate :type moduleName: str""" " check if already loaded " @@ -177,10 +181,10 @@ class PyIRCBot: self.moduleInstances[name] = getattr(self.modules[name], name)(self, name) " load hooks " self.loadModuleHooks(self.moduleInstances[name]) - + def unloadmodule(self, name): """Deactivate a module. - + :param moduleName: Name of the module to deactivate :type moduleName: str""" if name in self.moduleInstances: @@ -197,10 +201,10 @@ class PyIRCBot: else: self.log.info("Module %s not loaded" % name) return (False, "Module not loaded") - + def reloadmodule(self, name): """Deactivate and activate a module. - + :param moduleName: Name of the target module :type moduleName: str""" " make sure it's imporeted" @@ -215,10 +219,10 @@ class PyIRCBot: self.loadmodule(name) return (True, None) return (False, "Module is not loaded") - + def redomodule(self, name): """Reload a running module from disk - + :param moduleName: Name of the target module :type moduleName: str""" " remember if it was loaded before " @@ -233,10 +237,10 @@ class PyIRCBot: if loadedbefore: self.loadmodule(name) return (True, None) - + def loadModuleHooks(self, module): """**Internal.** Enable (connect) hooks of a module - + :param module: module object to hook in :type module: object""" " activate a module's hooks " @@ -246,10 +250,10 @@ class PyIRCBot: self.irc.addHook(hookcmd, hook.method) else: self.irc.addHook(hook.hook, hook.method) - + def unloadModuleHooks(self, module): """**Internal.** Disable (disconnect) hooks of a module - + :param module: module object to unhook :type module: object""" " remove a modules hooks " @@ -259,20 +263,20 @@ class PyIRCBot: self.irc.removeHook(hookcmd, hook.method) else: self.irc.removeHook(hook.hook, hook.method) - + def getmodulebyname(self, name): """Get a module object by name - + :param name: name of the module to return :type name: str :returns: object -- the module object""" if not name in self.moduleInstances: return None return self.moduleInstances[name] - + def getmodulesbyservice(self, service): - """Get a list of modules that provide the specified service - + """Get a list of modules that provide the specified service + :param service: name of the service searched for :type service: str :returns: list -- a list of module objects""" @@ -281,10 +285,10 @@ class PyIRCBot: if service in self.moduleInstances[module].services: validModules.append(self.moduleInstances[module]) return validModules - + def getBestModuleForService(self, service): - """Get the first module that provides the specified service - + """Get the first module that provides the specified service + :param service: name of the service searched for :type service: str :returns: object -- the module object, if found. None if not found.""" @@ -292,7 +296,7 @@ class PyIRCBot: if len(m)>0: return m[0] return None - + def closeAllModules(self): """ Deport all modules (for shutdown). Modules are unloaded in the opposite order listed in the config. """ loaded = list(self.moduleInstances.keys()) @@ -304,41 +308,41 @@ class PyIRCBot: self.deportmodule(key) for key in loaded: self.deportmodule(key) - + " Filesystem Methods " def getDataPath(self, moduleName): """Return the absolute path for a module's data dir - + :param moduleName: the module who's data dir we want :type moduleName: str""" if not os.path.exists("%s/data/%s" % (self.botconfig["bot"]["datadir"], moduleName)): os.mkdir("%s/data/%s/" % (self.botconfig["bot"]["datadir"], moduleName)) return "%s/data/%s/" % (self.botconfig["bot"]["datadir"], moduleName) - + def getConfigPath(self, moduleName): """Return the absolute path for a module's config file - + :param moduleName: the module who's config file we want :type moduleName: str""" - + basepath = "%s/config/%s" % (self.botconfig["bot"]["datadir"], moduleName) - + if os.path.exists("%s.json"%basepath): - return "%s.json"%basepath + return "%s.json"%basepath return None - + " Utility methods " @staticmethod def messageHasCommand(command, message, requireArgs=False): """Check if a message has a command with or without args in it - + :param command: the command string to look for, like !ban. If a list is passed, the first match is returned. :type command: str or list :param message: the message string to look in, like "!ban Lil_Mac" :type message: str :param requireArgs: if true, only validate if the command use has any amount of trailing text :type requireArgs: bool""" - + if not type(command)==list: command = [command] for item in command: @@ -346,7 +350,7 @@ class PyIRCBot: if cmd: return cmd return False - + @staticmethod def messageHasCommandSingle(command, message, requireArgs=False): # Check if the message at least starts with the command @@ -357,34 +361,31 @@ class PyIRCBot: subsetCheck = message[len(command):len(command)+1] if subsetCheck!=" " and subsetCheck!="": return False - + # We've got the command! Do we need args? argsStart = len(command) args = "" if argsStart > 0: args = message[argsStart+1:] - + if requireArgs and args.strip() == '': return False - + # Verified! Return the set. - ob = type('ParsedCommand', (object,), {}) - ob.command = command - ob.args = [] if args=="" else args.split(" ") - ob.args_str = args - ob.message = message - return ob - # return (True, command, args, message) - + return ParsedCommand(command, + args.split(" "), + args, + message) + @staticmethod def load(filepath): """Return an object from the passed filepath - + :param filepath: path to a json file. filename must end with .json :type filepath: str :Returns: | dict """ - + if filepath.endswith(".json"): from json import load return load(open(filepath, 'r'))