Make some stuff nicer

This commit is contained in:
dave 2016-11-06 23:00:15 +00:00
parent 0d3fbfc91c
commit 19b7a95d76
10 changed files with 206 additions and 203 deletions

View File

@ -1,32 +1,33 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os
import sys import sys
import logging import logging
from optparse import OptionParser from argparse import ArgumentParser
from pyircbot import PyIRCBot from pyircbot import PyIRCBot
from time import sleep
if __name__ == "__main__": if __name__ == "__main__":
" logging level and facility " " 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') log = logging.getLogger('main')
" parse command line args " " parse command line args "
parser = OptionParser() parser = ArgumentParser(description="Run pyircbot")
parser.add_option("-c", "--config", action="store", type="string", dest="config", help="Path to config file") parser.add_argument("-c", "--config", help="Path to config file", required=True)
parser.add_argument("--debug", action="store_true", help="Dump raw irc network")
(options, args) = parser.parse_args()
args = parser.parse_args()
log.debug(options)
if args.debug:
if not options.config: logging.getLogger().setLevel(logging.DEBUG)
log.debug(args)
if not args.config:
log.critical("No bot config file specified (-c). Exiting.") log.critical("No bot config file specified (-c). Exiting.")
sys.exit(0) sys.exit(0)
botconfig = PyIRCBot.load(options.config) botconfig = PyIRCBot.load(args.config)
log.debug(botconfig) log.debug(botconfig)
bot = PyIRCBot(botconfig) bot = PyIRCBot(botconfig)
try: try:
bot.loop() bot.loop()

View File

@ -16,30 +16,36 @@ import sys
from inspect import getargspec from inspect import getargspec
from socket import SHUT_RDWR from socket import SHUT_RDWR
from threading import Thread from threading import Thread
from time import sleep,time from time import sleep, time
from collections import namedtuple
try: try:
from cStringIO import StringIO from cStringIO import StringIO
except: except:
from io import BytesIO as StringIO 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): class IRCCore(asynchat.async_chat):
def __init__(self): def __init__(self):
asynchat.async_chat.__init__(self) asynchat.async_chat.__init__(self)
self.connected=False self.connected=False
"""If we're connected or not""" """If we're connected or not"""
self.log = logging.getLogger('IRCCore') self.log = logging.getLogger('IRCCore')
"""Reference to logger object""" """Reference to logger object"""
self.buffer = StringIO() self.buffer = StringIO()
"""cStringIO used as a buffer""" """cStringIO used as a buffer"""
self.alive = True self.alive = True
"""True if we should try to stay connected""" """True if we should try to stay connected"""
self.server = 0 self.server = 0
"""Current server index""" """Current server index"""
self.servers = [] self.servers = []
@ -48,22 +54,22 @@ class IRCCore(asynchat.async_chat):
"""Server port""" """Server port"""
self.ipv6 = False self.ipv6 = False
"""Use IPv6?""" """Use IPv6?"""
self.OUTPUT_BUFFER_SIZE = 1000 self.OUTPUT_BUFFER_SIZE = 1000
self.SEND_WAIT = 0.800 self.SEND_WAIT = 0.800
self.outputQueue = queue.PriorityQueue(self.OUTPUT_BUFFER_SIZE) self.outputQueue = queue.PriorityQueue(self.OUTPUT_BUFFER_SIZE)
self.outputQueueRunner = OutputQueueRunner(self) self.outputQueueRunner = OutputQueueRunner(self)
self.outputQueueRunner.start() self.outputQueueRunner.start()
# IRC Messages are terminated with \r\n # IRC Messages are terminated with \r\n
self.set_terminator(b"\r\n") self.set_terminator(b"\r\n")
# Set up hooks for modules # Set up hooks for modules
self.initHooks() self.initHooks()
# Map for asynchat # Map for asynchat
self.asynmap = {} self.asynmap = {}
def loop(self): def loop(self):
while self.alive: while self.alive:
try: try:
@ -83,10 +89,10 @@ class IRCCore(asynchat.async_chat):
except Exception as e2: except Exception as e2:
self.log.error("Error reconnecting: ") self.log.error("Error reconnecting: ")
self.log.error(IRCCore.trace()) self.log.error(IRCCore.trace())
def kill(self, message="Help! Another thread is killing me :(", alive=False): def kill(self, message="Help! Another thread is killing me :(", alive=False):
"""Send quit message, flush queue, and close the socket """Send quit message, flush queue, and close the socket
:param message: Quit message :param message: Quit message
:type message: str :type message: str
:param alive: True causes a reconnect after disconnecting :param alive: True causes a reconnect after disconnecting
@ -105,24 +111,24 @@ class IRCCore(asynchat.async_chat):
self.socket.shutdown(SHUT_RDWR) self.socket.shutdown(SHUT_RDWR)
self.close() self.close()
self.log.info("Kill complete") self.log.info("Kill complete")
" Net related code here on down " " Net related code here on down "
def getBuf(self): def getBuf(self):
"""Return the network buffer and clear it""" """Return the network buffer and clear it"""
self.buffer.seek(0) self.buffer.seek(0)
data = self.buffer.read() data = self.buffer.read()
self.buffer = StringIO() self.buffer = StringIO()
return data return data
def collect_incoming_data(self, data): def collect_incoming_data(self, data):
"""Recieve data from the IRC server, append it to the buffer """Recieve data from the IRC server, append it to the buffer
:param data: the data that was recieved :param data: the data that was recieved
:type data: str""" :type data: str"""
#self.log.debug("<< %(message)s", {"message":repr(data)}) #self.log.info("<< %(message)s", {"message":repr(data)})
self.buffer.write(data) self.buffer.write(data)
def found_terminator(self): def found_terminator(self):
"""A complete command was pushed through, so clear the buffer and process it.""" """A complete command was pushed through, so clear the buffer and process it."""
line = None 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(): repr(data): %s" % repr(line))
self.log.error("found_terminator(): error: %s" % str(ude)) self.log.error("found_terminator(): error: %s" % str(ude))
return return
self.log.debug("< {}".format(line))
self.process_data(line) self.process_data(line)
def handle_close(self): def handle_close(self):
"""Called when the socket is disconnected. Triggers the _DISCONNECT hook""" """Called when the socket is disconnected. Triggers the _DISCONNECT hook"""
self.log.debug("handle_close") self.log.info("handle_close")
self.connected=False self.connected=False
self.close() self.close()
self.fire_hook("_DISCONNECT") self.fire_hook("_DISCONNECT")
def handle_error(self, *args, **kwargs): def handle_error(self, *args, **kwargs):
"""Called on fatal network errors.""" """Called on fatal network errors."""
self.log.error("Connection failed (handle_error)") self.log.error("Connection failed (handle_error)")
self.log.error(str(args)) self.log.error(str(args))
self.log.error(str(kwargs)) self.log.error(str(kwargs))
self.log.error(IRCCore.trace()); self.log.error(IRCCore.trace());
def _connect(self): def _connect(self):
"""Connect to IRC""" """Connect to IRC"""
self.server+=1 self.server+=1
if self.server >= len(self.servers): if self.server >= len(self.servers):
self.server=0 self.server=0
serverHostname = self.servers[self.server] 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 socket_type = socket.AF_INET
if self.ipv6: if self.ipv6:
self.log.info("IPv6 is enabled.") self.log.info("IPv6 is enabled.")
socket_type = socket.AF_INET6 socket_type = socket.AF_INET6
socketInfo = socket.getaddrinfo(serverHostname, self.port, socket_type) socketInfo = socket.getaddrinfo(serverHostname, self.port, socket_type)
self.create_socket(socket_type, socket.SOCK_STREAM) 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.connect(socketInfo[0][4])
self.log.debug("Connection established") self.log.info("Connection established")
self._fileno = self.socket.fileno() self._fileno = self.socket.fileno()
self.asynmap[self._fileno] = self # http://willpython.blogspot.com/2010/08/multiple-event-loops-with-asyncore-and.html 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)) self.log.info("_connect: Socket map: %s" % str(self.asynmap))
def handle_connect(self): def handle_connect(self):
"""When asynchat indicates our socket is connected, fire the _CONNECT hook""" """When asynchat indicates our socket is connected, fire the _CONNECT hook"""
self.connected=True self.connected=True
self.log.debug("handle_connect: connected") self.log.info("handle_connect: connected")
self.fire_hook("_CONNECT") self.fire_hook("_CONNECT")
self.log.debug("handle_connect: complete") self.log.info("handle_connect: complete")
def sendRaw(self, text, prio=2): def sendRaw(self, text, prio=2):
"""Queue messages (raw string) to be sent to the IRC server """Queue messages (raw string) to be sent to the IRC server
:param text: the string to send :param text: the string to send
:type text: str""" :type text: str"""
text = (text+"\r\n").encode("UTF-8").decode().encode("UTF-8") text = (text+"\r\n").encode("UTF-8").decode().encode("UTF-8")
self.outputQueue.put((prio, text), block=False) self.outputQueue.put((prio, text), block=False)
def process_data(self, data): def process_data(self, data):
"""Process one line of tet irc sent us """Process one line of tet irc sent us
:param data: the data to process :param data: the data to process
:type data: str""" :type data: str"""
if data.strip() == "": if data.strip() == "":
return return
prefix = None prefix = None
command = None command = None
args=[] args=[]
trailing=None trailing=None
if data[0]==":": if data[0]==":":
prefix=data.split(" ")[0][1:] prefix=data.split(" ")[0][1:]
data=data[data.find(" ")+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)) self.log.warning("Unknown command: cmd='%s' prefix='%s' args='%s' trailing='%s'" % (command, prefix, args, trailing))
else: else:
self.fire_hook(command, args=args, prefix=prefix, trailing=trailing) self.fire_hook(command, args=args, prefix=prefix, trailing=trailing)
" Module related code " " Module related code "
def initHooks(self): def initHooks(self):
"""Defines hooks that modules can listen for events of""" """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 '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) '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 '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. '366', # :irc.129irc.com 366 CloneABCD #clonea :End of /NAMES list.
'372', # :chaos.esper.net 372 xBotxShell :motd text here '372', # :chaos.esper.net 372 xBotxShell :motd text here
'375', # :chaos.esper.net 375 xBotxShellTest :- chaos.esper.net Message of the Day - '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 " " mapping of hooks to methods "
self.hookcalls = {command:[] for command in self.hooks} self.hookcalls = {command:[] for command in self.hooks}
def fire_hook(self, command, args=None, prefix=None, trailing=None): def fire_hook(self, command, args=None, prefix=None, trailing=None):
"""Run any listeners for a specific hook """Run any listeners for a specific hook
:param command: the hook to fire :param command: the hook to fire
:type command: str :type command: str
:param args: the list of arguments, if any, the command was passed :param args: the list of arguments, if any, the command was passed
@ -273,20 +280,20 @@ class IRCCore(asynchat.async_chat):
:type prefix: str :type prefix: str
:param trailing: data payload of the command :param trailing: data payload of the command
:type trailing: str""" :type trailing: str"""
for hook in self.hookcalls[command]: for hook in self.hookcalls[command]:
try: try:
if len(getargspec(hook).args) == 2: if len(getargspec(hook).args) == 2:
hook(IRCCore.packetAsObject(args, prefix, trailing)) hook(IRCCore.packetAsObject(args, prefix, trailing))
else: else:
hook(args, prefix, trailing) hook(args, prefix, trailing)
except: except:
self.log.warning("Error processing hook: \n%s"% self.trace()) self.log.warning("Error processing hook: \n%s"% self.trace())
def addHook(self, command, method): def addHook(self, command, method):
"""**Internal.** Enable (connect) a single hook of a module """**Internal.** Enable (connect) a single hook of a module
:param command: command this hook will trigger on :param command: command this hook will trigger on
:type command: str :type command: str
:param method: callable method object to hook in :param method: callable method object to hook in
@ -297,10 +304,10 @@ class IRCCore(asynchat.async_chat):
else: else:
self.log.warning("Invalid hook - %s" % command) self.log.warning("Invalid hook - %s" % command)
return False return False
def removeHook(self, command, method): def removeHook(self, command, method):
"""**Internal.** Disable (disconnect) a single hook of a module """**Internal.** Disable (disconnect) a single hook of a module
:param command: command this hook triggers on :param command: command this hook triggers on
:type command: str :type command: str
:param method: callable method that should be removed :param method: callable method that should be removed
@ -313,10 +320,10 @@ class IRCCore(asynchat.async_chat):
else: else:
self.log.warning("Invalid hook - %s" % command) self.log.warning("Invalid hook - %s" % command)
return False return False
def packetAsObject(args, prefix, trailing): def packetAsObject(args, prefix, trailing):
"""Given an irc message's args, prefix, and trailing data return an object with these properties """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 :param args: list of args from the IRC packet
:type args: list :type args: list
:param prefix: prefix object parsed from the IRC packet :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 :param trailing: trailing data from the IRC packet
:type trailing: str :type trailing: str
:returns: object -- a IRCEvent object with the ``args``, ``prefix``, ``trailing``""" :returns: object -- a IRCEvent object with the ``args``, ``prefix``, ``trailing``"""
return type('IRCEvent', (object,), { return IRCEvent(args,
"args": args, IRCCore.decodePrefix(prefix) if prefix else None,
"prefix": IRCCore.decodePrefix(prefix) if prefix else None, trailing)
"trailing": trailing
})
" Utility methods " " Utility methods "
@staticmethod @staticmethod
def decodePrefix(prefix): def decodePrefix(prefix):
"""Given a prefix like nick!username@hostname, return an object with these properties """Given a prefix like nick!username@hostname, return an object with these properties
:param prefix: the prefix to disassemble :param prefix: the prefix to disassemble
:type prefix: str :type prefix: str
:returns: object -- an UserPrefix object with the properties `nick`, `username`, `hostname` or a ServerPrefix object with the property `hostname`""" :returns: object -- an UserPrefix object with the properties `nick`, `username`, `hostname` or a ServerPrefix object with the property `hostname`"""
if "!" in prefix: if "!" in prefix:
ob = type('UserPrefix', (object,), {}) nick, prefix = prefix.split("!")
ob.str = prefix username, hostname = prefix.split("@")
ob.nick, prefix = prefix.split("!") return UserPrefix(nick, username, hostname)
ob.username, ob.hostname = prefix.split("@")
return ob
else: else:
ob = type('ServerPrefix', (object,), {}) return ServerPrefix(prefix)
ob.str = prefix
ob.hostname = prefix
return ob
@staticmethod @staticmethod
def trace(): def trace():
"""Return the stack trace of the bot as a string""" """Return the stack trace of the bot as a string"""
return traceback.format_exc() return traceback.format_exc()
@staticmethod @staticmethod
def fulltrace(): def fulltrace():
"""Return the stack trace of the bot as a string""" """Return the stack trace of the bot as a string"""
@ -372,25 +372,25 @@ class IRCCore(asynchat.async_chat):
result += line + "\n" result += line + "\n"
result += "\n*** STACKTRACE - END ***\n" result += "\n*** STACKTRACE - END ***\n"
return result return result
" Data Methods " " Data Methods "
def get_nick(self): def get_nick(self):
"""Get the bot's current nick """Get the bot's current nick
:returns: str - the bot's current nickname""" :returns: str - the bot's current nickname"""
return self.nick return self.nick
" Action Methods " " Action Methods "
def act_PONG(self, data): def act_PONG(self, data):
"""Use the `/pong` command - respond to server pings """Use the `/pong` command - respond to server pings
:param data: the string or number the server sent with it's ping :param data: the string or number the server sent with it's ping
:type data: str""" :type data: str"""
self.sendRaw("PONG :%s" % data) self.sendRaw("PONG :%s" % data)
def act_USER(self, username, hostname, realname): def act_USER(self, username, hostname, realname):
"""Use the USER protocol command. Used during connection """Use the USER protocol command. Used during connection
:param username: the bot's username :param username: the bot's username
:type username: str :type username: str
:param hostname: the bot's hostname :param hostname: the bot's hostname
@ -398,34 +398,34 @@ class IRCCore(asynchat.async_chat):
:param realname: the bot's realname :param realname: the bot's realname
:type realname: str""" :type realname: str"""
self.sendRaw("USER %s %s %s :%s" % (username, hostname, self.servers[self.server], realname)) self.sendRaw("USER %s %s %s :%s" % (username, hostname, self.servers[self.server], realname))
def act_NICK(self, newNick): def act_NICK(self, newNick):
"""Use the `/nick` command """Use the `/nick` command
:param newNick: new nick for the bot :param newNick: new nick for the bot
:type newNick: str""" :type newNick: str"""
self.nick = newNick self.nick = newNick
self.sendRaw("NICK %s" % newNick) self.sendRaw("NICK %s" % newNick)
def act_JOIN(self, channel): def act_JOIN(self, channel):
"""Use the `/join` command """Use the `/join` command
:param channel: the channel to attempt to join :param channel: the channel to attempt to join
:type channel: str""" :type channel: str"""
self.sendRaw("JOIN %s"%channel) self.sendRaw("JOIN %s"%channel)
def act_PRIVMSG(self, towho, message): def act_PRIVMSG(self, towho, message):
"""Use the `/msg` command """Use the `/msg` command
:param towho: the target #channel or user's name :param towho: the target #channel or user's name
:type towho: str :type towho: str
:param message: the message to send :param message: the message to send
:type message: str""" :type message: str"""
self.sendRaw("PRIVMSG %s :%s"%(towho,message)) self.sendRaw("PRIVMSG %s :%s"%(towho,message))
def act_MODE(self, channel, mode, extra=None): def act_MODE(self, channel, mode, extra=None):
"""Use the `/mode` command """Use the `/mode` command
:param channel: the channel this mode is for :param channel: the channel this mode is for
:type channel: str :type channel: str
:param mode: the mode string. Example: +b :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)) self.sendRaw("MODE %s %s %s" % (channel,mode,extra))
else: else:
self.sendRaw("MODE %s %s" % (channel,mode)) self.sendRaw("MODE %s %s" % (channel,mode))
def act_ACTION(self, channel, action): def act_ACTION(self, channel, action):
"""Use the `/me <action>` command """Use the `/me <action>` command
:param channel: the channel name or target's name the message is sent to :param channel: the channel name or target's name the message is sent to
:type channel: str :type channel: str
:param action: the text to send :param action: the text to send
:type action: str""" :type action: str"""
self.sendRaw("PRIVMSG %s :\x01ACTION %s"%(channel,action)) self.sendRaw("PRIVMSG %s :\x01ACTION %s"%(channel,action))
def act_KICK(self, channel, who, comment=""): def act_KICK(self, channel, who, comment=""):
"""Use the `/kick <user> <message>` command """Use the `/kick <user> <message>` command
:param channel: the channel from which the user will be kicked :param channel: the channel from which the user will be kicked
:type channel: str :type channel: str
:param who: the nickname of the user to kick :param who: the nickname of the user to kick
@ -456,14 +456,14 @@ class IRCCore(asynchat.async_chat):
:param comment: the kick message :param comment: the kick message
:type comment: str""" :type comment: str"""
self.sendRaw("KICK %s %s :%s" % (channel, who, comment)) self.sendRaw("KICK %s %s :%s" % (channel, who, comment))
def act_QUIT(self, message): def act_QUIT(self, message):
"""Use the `/quit` command """Use the `/quit` command
:param message: quit message :param message: quit message
:type message: str""" :type message: str"""
self.sendRaw("QUIT :%s" % message, prio=0) self.sendRaw("QUIT :%s" % message, prio=0)
class OutputQueueRunner(Thread): class OutputQueueRunner(Thread):
"""Rate-limited output queue""" """Rate-limited output queue"""
def __init__(self, bot): def __init__(self, bot):
@ -471,7 +471,7 @@ class OutputQueueRunner(Thread):
self.bot = bot #reference to main bot thread self.bot = bot #reference to main bot thread
self.log = logging.getLogger('OutputQueueRunner') self.log = logging.getLogger('OutputQueueRunner')
self.paused = False self.paused = False
def run(self): def run(self):
"""Constantly sends messages unless bot is disconnecting/ed""" """Constantly sends messages unless bot is disconnecting/ed"""
lastSend = time() lastSend = time()
@ -481,24 +481,25 @@ class OutputQueueRunner(Thread):
if sinceLast < self.bot.SEND_WAIT: if sinceLast < self.bot.SEND_WAIT:
toSleep = self.bot.SEND_WAIT - sinceLast toSleep = self.bot.SEND_WAIT - sinceLast
sleep(toSleep) sleep(toSleep)
# Pop item and execute # Pop item and execute
if self.bot.connected and not self.paused: if self.bot.connected and not self.paused:
try: try:
self.process_queue_item() self.process_queue_item()
lastSend = time() lastSend = time()
except queue.Empty: except queue.Empty:
#self.log.debug("Queue is empty") #self.log.info("Queue is empty")
pass pass
sleep(0.01) sleep(0.01)
def process_queue_item(self): def process_queue_item(self):
"""Remove 1 item from queue and process it""" """Remove 1 item from queue and process it"""
prio,text = self.bot.outputQueue.get(block=True, timeout=10) 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.bot.outputQueue.task_done()
self.log.debug("> {}".format(text.decode('UTF-8')))
self.bot.send(text) self.bot.send(text)
def clear(self): def clear(self):
"""Discard all items from queue""" """Discard all items from queue"""
length = self.bot.outputQueue.qsize() length = self.bot.outputQueue.qsize()
@ -507,9 +508,9 @@ class OutputQueueRunner(Thread):
self.bot.outputQueue.get(block=False) self.bot.outputQueue.get(block=False)
except queue.Empty: except queue.Empty:
pass pass
#self.log.debug("output queue cleared") #self.log.info("output queue cleared")
return length return length
def flush(self): def flush(self):
"""Process all items in queue""" """Process all items in queue"""
for i in range(0, self.bot.outputQueue.qsize()): for i in range(0, self.bot.outputQueue.qsize()):
@ -517,4 +518,4 @@ class OutputQueueRunner(Thread):
self.process_queue_item() self.process_queue_item()
except: except:
pass pass
#self.log.debug("output queue flushed") #self.log.info("output queue flushed")

View File

@ -166,9 +166,9 @@ class AttributeStorage(ModuleBase):
if value == None: if value == None:
# delete it # delete it
c = self.db.connection.query("DELETE FROM `values` WHERE `itemid`=%s AND `attributeid`=%s ;", (itemId, attributeId)) 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: else:
# add attribute # add attribute
c = self.db.connection.query("REPLACE INTO `values` (`itemid`, `attributeid`, `value`) VALUES (%s, %s, %s);", (itemId, attributeId, value)) 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() c.close()

View File

@ -161,9 +161,9 @@ class AttributeStorageLite(ModuleBase):
if value == None: if value == None:
# delete it # delete it
c = self.db.query("DELETE FROM `values` WHERE `itemid`=? AND `attributeid`=? ;", (itemId, attributeId)) 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: else:
# add attribute # add attribute
c = self.db.query("REPLACE INTO `values` (`itemid`, `attributeid`, `value`) VALUES (?, ?, ?);", (itemId, attributeId, value)) 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() c.close()

View File

@ -121,13 +121,13 @@ class BitcoinRPC:
def connect(self): def connect(self):
# internal. connect to the service # 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: try:
self.con = AuthServiceProxy("http://%s:%s@%s:%s" % (self.username, self.password, self.host, self.port)) self.con = AuthServiceProxy("http://%s:%s@%s:%s" % (self.username, self.password, self.host, self.port))
except Exception as e: 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 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))

View File

@ -73,7 +73,7 @@ class DogeController:
def connect(self): def connect(self):
"Connect to RPC endpoint" "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 = AuthServiceProxy("http://%s:%s@%s:%s" % (self.config["username"], self.config["password"], self.config["host"], self.config["port"]))
self.con.getinfo() 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"]))

View File

@ -145,7 +145,7 @@ class scrambleGame:
self.nextTimer.start() self.nextTimer.start()
self.guesses=0 self.guesses=0
self.category_count+=1 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: if self.category_count >= self.change_category_after_words:
self.should_change_category = True self.should_change_category = True
else: else:
@ -247,7 +247,7 @@ class scrambleGame:
picked = f.readline().strip().lower() picked = f.readline().strip().lower()
f.close() 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) self.lastwords.append(picked)
if len(self.lastwords) > 5: if len(self.lastwords) > 5:
self.lastwords.pop(0) self.lastwords.pop(0)

View File

@ -111,13 +111,13 @@ class LinkTitler(ModuleBase):
def url_headers(self, url): def url_headers(self, url):
"HEAD requests a url to check content type & length, returns something like: {'type': 'image/jpeg', 'size': '90583'}" "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) resp = head(url=url, allow_redirects=True)
return resp.headers return resp.headers
def url_htmltitle(self, url): def url_htmltitle(self, url):
"Requests page html and returns title in a safe way" "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) resp = get(url=url, stream=True)
# Fetch no more than first 10kb # Fetch no more than first 10kb
# if the title isn't seen by then, you're doing it wrong # if the title isn't seen by then, you're doing it wrong

View File

@ -41,6 +41,6 @@ class Triggered(ModuleBase):
def scream(self, channel): def scream(self, channel):
delay = randrange(self.config["mindelay"], self.config["maxdelay"]) 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) sleep(delay)
self.bot.act_PRIVMSG(channel, choice(self.config["responses"])) self.bot.act_PRIVMSG(channel, choice(self.config["responses"]))

View File

@ -12,38 +12,42 @@ import sys
import traceback import traceback
from pyircbot.rpc import BotRPC from pyircbot.rpc import BotRPC
from pyircbot.irccore import IRCCore from pyircbot.irccore import IRCCore
from collections import namedtuple
import os.path import os.path
ParsedCommand = namedtuple("ParsedCommand", "command args args_str message")
class PyIRCBot: class PyIRCBot:
""":param botconfig: The configuration of this instance of the bot. Passed by main.py. """:param botconfig: The configuration of this instance of the bot. Passed by main.py.
:type botconfig: dict :type botconfig: dict
""" """
version = "4.0.0-r03" version = "4.0.0-r03"
def __init__(self, botconfig): def __init__(self, botconfig):
self.log = logging.getLogger('PyIRCBot') self.log = logging.getLogger('PyIRCBot')
"""Reference to logger object""" """Reference to logger object"""
self.botconfig = botconfig self.botconfig = botconfig
"""saved copy of the instance config""" """saved copy of the instance config"""
"""storage of imported modules""" """storage of imported modules"""
self.modules = {} self.modules = {}
"""instances of modules""" """instances of modules"""
self.moduleInstances = {} self.moduleInstances = {}
self.rpc = BotRPC(self) self.rpc = BotRPC(self)
"""Reference to BotRPC thread""" """Reference to BotRPC thread"""
self.irc = IRCCore() self.irc = IRCCore()
"""IRC protocol class""" """IRC protocol class"""
self.irc.servers = self.botconfig["connection"]["servers"] self.irc.servers = self.botconfig["connection"]["servers"]
self.irc.port = self.botconfig["connection"]["port"] self.irc.port = self.botconfig["connection"]["port"]
self.irc.ipv6 = True if self.botconfig["connection"]["ipv6"]=="on" else False self.irc.ipv6 = True if self.botconfig["connection"]["ipv6"]=="on" else False
self.irc.addHook("_DISCONNECT", self.connection_closed) self.irc.addHook("_DISCONNECT", self.connection_closed)
# legacy support # legacy support
self.act_PONG = self.irc.act_PONG self.act_PONG = self.irc.act_PONG
self.act_USER = self.irc.act_USER self.act_USER = self.irc.act_USER
@ -56,26 +60,26 @@ class PyIRCBot:
self.act_QUIT = self.irc.act_QUIT self.act_QUIT = self.irc.act_QUIT
self.get_nick = self.irc.get_nick self.get_nick = self.irc.get_nick
self.decodePrefix = IRCCore.decodePrefix self.decodePrefix = IRCCore.decodePrefix
# Load modules # Load modules
self.initModules() self.initModules()
# Connect to IRC # Connect to IRC
self.connect() self.connect()
def connect(self): def connect(self):
try: try:
self.irc._connect() self.irc._connect()
except: except:
self.log.error("Pyircbot attempted to connect and failed!") self.log.error("Pyircbot attempted to connect and failed!")
self.log.error(traceback.format_exc()) self.log.error(traceback.format_exc())
def loop(self): def loop(self):
self.irc.loop() self.irc.loop()
def disconnect(self, message, reconnect=True): def disconnect(self, message, reconnect=True):
"""Send quit message and disconnect from IRC. """Send quit message and disconnect from IRC.
:param message: Quit message :param message: Quit message
:type message: str :type message: str
:param reconnect: True causes a reconnection attempt to be made after the disconnect :param reconnect: True causes a reconnection attempt to be made after the disconnect
@ -83,10 +87,10 @@ class PyIRCBot:
""" """
self.log.info("disconnect") self.log.info("disconnect")
self.irc.kill(message=message, alive=reconnect) self.irc.kill(message=message, alive=reconnect)
def kill(self, sys_exit=True, message="Help! Another thread is killing me :("): def kill(self, sys_exit=True, message="Help! Another thread is killing me :("):
"""Shut down the bot violently """Shut down the bot violently
:param sys_exit: True causes sys.exit(0) to be called :param sys_exit: True causes sys.exit(0) to be called
:type sys_exit: bool :type sys_exit: bool
:param message: Quit message :param message: Quit message
@ -94,39 +98,39 @@ class PyIRCBot:
""" """
#Close all modules #Close all modules
self.closeAllModules() self.closeAllModules()
self.irc.kill(message=message, alive=not sys_exit) self.irc.kill(message=message, alive=not sys_exit)
if sys_exit: if sys_exit:
sys.exit(0) sys.exit(0)
def connection_closed(self, args, prefix, trailing): def connection_closed(self, args, prefix, trailing):
"""Called when the socket is disconnected. We will want to reconnect. """ """Called when the socket is disconnected. We will want to reconnect. """
if self.irc.alive: if self.irc.alive:
self.log.warning("Connection was lost. Reconnecting in 5 seconds.") self.log.warning("Connection was lost. Reconnecting in 5 seconds.")
time.sleep(5) time.sleep(5)
self.connect() self.connect()
def initModules(self): def initModules(self):
"""load modules specified in instance config""" """load modules specified in instance config"""
" append module location to path " " append module location to path "
sys.path.append(os.path.dirname(__file__)+"/modules/") sys.path.append(os.path.dirname(__file__)+"/modules/")
" append usermodule dir to beginning of path" " append usermodule dir to beginning of path"
for path in self.botconfig["bot"]["usermodules"]: for path in self.botconfig["bot"]["usermodules"]:
sys.path.insert(0, path+"/") sys.path.insert(0, path+"/")
for modulename in self.botconfig["modules"]: for modulename in self.botconfig["modules"]:
self.loadmodule(modulename) self.loadmodule(modulename)
def importmodule(self, name): def importmodule(self, name):
"""Import a module """Import a module
:param moduleName: Name of the module to import :param moduleName: Name of the module to import
:type moduleName: str""" :type moduleName: str"""
" check if already exists " " check if already exists "
if not name in self.modules: if not name in self.modules:
self.log.debug("Importing %s" % name) self.log.info("Importing %s" % name)
" attempt to load " " attempt to load "
try: try:
moduleref = __import__(name) moduleref = __import__(name)
@ -140,13 +144,13 @@ class PyIRCBot:
else: else:
self.log.warning("Module %s already imported" % name) self.log.warning("Module %s already imported" % name)
return (False, "Module already imported") return (False, "Module already imported")
def deportmodule(self, name): def deportmodule(self, name):
"""Remove a module's code from memory. If the module is loaded it will be unloaded silently. """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 :param moduleName: Name of the module to import
:type moduleName: str""" :type moduleName: str"""
self.log.debug("Deporting %s" % name) self.log.info("Deporting %s" % name)
" unload if necessary " " unload if necessary "
if name in self.moduleInstances: if name in self.moduleInstances:
self.unloadmodule(name) self.unloadmodule(name)
@ -158,10 +162,10 @@ class PyIRCBot:
" delete copy that python stores in sys.modules " " delete copy that python stores in sys.modules "
if name in sys.modules: if name in sys.modules:
del sys.modules[name] del sys.modules[name]
def loadmodule(self, name): def loadmodule(self, name):
"""Activate a module. """Activate a module.
:param moduleName: Name of the module to activate :param moduleName: Name of the module to activate
:type moduleName: str""" :type moduleName: str"""
" check if already loaded " " check if already loaded "
@ -177,10 +181,10 @@ class PyIRCBot:
self.moduleInstances[name] = getattr(self.modules[name], name)(self, name) self.moduleInstances[name] = getattr(self.modules[name], name)(self, name)
" load hooks " " load hooks "
self.loadModuleHooks(self.moduleInstances[name]) self.loadModuleHooks(self.moduleInstances[name])
def unloadmodule(self, name): def unloadmodule(self, name):
"""Deactivate a module. """Deactivate a module.
:param moduleName: Name of the module to deactivate :param moduleName: Name of the module to deactivate
:type moduleName: str""" :type moduleName: str"""
if name in self.moduleInstances: if name in self.moduleInstances:
@ -197,10 +201,10 @@ class PyIRCBot:
else: else:
self.log.info("Module %s not loaded" % name) self.log.info("Module %s not loaded" % name)
return (False, "Module not loaded") return (False, "Module not loaded")
def reloadmodule(self, name): def reloadmodule(self, name):
"""Deactivate and activate a module. """Deactivate and activate a module.
:param moduleName: Name of the target module :param moduleName: Name of the target module
:type moduleName: str""" :type moduleName: str"""
" make sure it's imporeted" " make sure it's imporeted"
@ -215,10 +219,10 @@ class PyIRCBot:
self.loadmodule(name) self.loadmodule(name)
return (True, None) return (True, None)
return (False, "Module is not loaded") return (False, "Module is not loaded")
def redomodule(self, name): def redomodule(self, name):
"""Reload a running module from disk """Reload a running module from disk
:param moduleName: Name of the target module :param moduleName: Name of the target module
:type moduleName: str""" :type moduleName: str"""
" remember if it was loaded before " " remember if it was loaded before "
@ -233,10 +237,10 @@ class PyIRCBot:
if loadedbefore: if loadedbefore:
self.loadmodule(name) self.loadmodule(name)
return (True, None) return (True, None)
def loadModuleHooks(self, module): def loadModuleHooks(self, module):
"""**Internal.** Enable (connect) hooks of a module """**Internal.** Enable (connect) hooks of a module
:param module: module object to hook in :param module: module object to hook in
:type module: object""" :type module: object"""
" activate a module's hooks " " activate a module's hooks "
@ -246,10 +250,10 @@ class PyIRCBot:
self.irc.addHook(hookcmd, hook.method) self.irc.addHook(hookcmd, hook.method)
else: else:
self.irc.addHook(hook.hook, hook.method) self.irc.addHook(hook.hook, hook.method)
def unloadModuleHooks(self, module): def unloadModuleHooks(self, module):
"""**Internal.** Disable (disconnect) hooks of a module """**Internal.** Disable (disconnect) hooks of a module
:param module: module object to unhook :param module: module object to unhook
:type module: object""" :type module: object"""
" remove a modules hooks " " remove a modules hooks "
@ -259,20 +263,20 @@ class PyIRCBot:
self.irc.removeHook(hookcmd, hook.method) self.irc.removeHook(hookcmd, hook.method)
else: else:
self.irc.removeHook(hook.hook, hook.method) self.irc.removeHook(hook.hook, hook.method)
def getmodulebyname(self, name): def getmodulebyname(self, name):
"""Get a module object by name """Get a module object by name
:param name: name of the module to return :param name: name of the module to return
:type name: str :type name: str
:returns: object -- the module object""" :returns: object -- the module object"""
if not name in self.moduleInstances: if not name in self.moduleInstances:
return None return None
return self.moduleInstances[name] return self.moduleInstances[name]
def getmodulesbyservice(self, service): 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 :param service: name of the service searched for
:type service: str :type service: str
:returns: list -- a list of module objects""" :returns: list -- a list of module objects"""
@ -281,10 +285,10 @@ class PyIRCBot:
if service in self.moduleInstances[module].services: if service in self.moduleInstances[module].services:
validModules.append(self.moduleInstances[module]) validModules.append(self.moduleInstances[module])
return validModules return validModules
def getBestModuleForService(self, service): 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 :param service: name of the service searched for
:type service: str :type service: str
:returns: object -- the module object, if found. None if not found.""" :returns: object -- the module object, if found. None if not found."""
@ -292,7 +296,7 @@ class PyIRCBot:
if len(m)>0: if len(m)>0:
return m[0] return m[0]
return None return None
def closeAllModules(self): def closeAllModules(self):
""" Deport all modules (for shutdown). Modules are unloaded in the opposite order listed in the config. """ """ Deport all modules (for shutdown). Modules are unloaded in the opposite order listed in the config. """
loaded = list(self.moduleInstances.keys()) loaded = list(self.moduleInstances.keys())
@ -304,41 +308,41 @@ class PyIRCBot:
self.deportmodule(key) self.deportmodule(key)
for key in loaded: for key in loaded:
self.deportmodule(key) self.deportmodule(key)
" Filesystem Methods " " Filesystem Methods "
def getDataPath(self, moduleName): def getDataPath(self, moduleName):
"""Return the absolute path for a module's data dir """Return the absolute path for a module's data dir
:param moduleName: the module who's data dir we want :param moduleName: the module who's data dir we want
:type moduleName: str""" :type moduleName: str"""
if not os.path.exists("%s/data/%s" % (self.botconfig["bot"]["datadir"], moduleName)): if not os.path.exists("%s/data/%s" % (self.botconfig["bot"]["datadir"], moduleName)):
os.mkdir("%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) return "%s/data/%s/" % (self.botconfig["bot"]["datadir"], moduleName)
def getConfigPath(self, moduleName): def getConfigPath(self, moduleName):
"""Return the absolute path for a module's config file """Return the absolute path for a module's config file
:param moduleName: the module who's config file we want :param moduleName: the module who's config file we want
:type moduleName: str""" :type moduleName: str"""
basepath = "%s/config/%s" % (self.botconfig["bot"]["datadir"], moduleName) basepath = "%s/config/%s" % (self.botconfig["bot"]["datadir"], moduleName)
if os.path.exists("%s.json"%basepath): if os.path.exists("%s.json"%basepath):
return "%s.json"%basepath return "%s.json"%basepath
return None return None
" Utility methods " " Utility methods "
@staticmethod @staticmethod
def messageHasCommand(command, message, requireArgs=False): def messageHasCommand(command, message, requireArgs=False):
"""Check if a message has a command with or without args in it """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. :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 :type command: str or list
:param message: the message string to look in, like "!ban Lil_Mac" :param message: the message string to look in, like "!ban Lil_Mac"
:type message: str :type message: str
:param requireArgs: if true, only validate if the command use has any amount of trailing text :param requireArgs: if true, only validate if the command use has any amount of trailing text
:type requireArgs: bool""" :type requireArgs: bool"""
if not type(command)==list: if not type(command)==list:
command = [command] command = [command]
for item in command: for item in command:
@ -346,7 +350,7 @@ class PyIRCBot:
if cmd: if cmd:
return cmd return cmd
return False return False
@staticmethod @staticmethod
def messageHasCommandSingle(command, message, requireArgs=False): def messageHasCommandSingle(command, message, requireArgs=False):
# Check if the message at least starts with the command # Check if the message at least starts with the command
@ -357,34 +361,31 @@ class PyIRCBot:
subsetCheck = message[len(command):len(command)+1] subsetCheck = message[len(command):len(command)+1]
if subsetCheck!=" " and subsetCheck!="": if subsetCheck!=" " and subsetCheck!="":
return False return False
# We've got the command! Do we need args? # We've got the command! Do we need args?
argsStart = len(command) argsStart = len(command)
args = "" args = ""
if argsStart > 0: if argsStart > 0:
args = message[argsStart+1:] args = message[argsStart+1:]
if requireArgs and args.strip() == '': if requireArgs and args.strip() == '':
return False return False
# Verified! Return the set. # Verified! Return the set.
ob = type('ParsedCommand', (object,), {}) return ParsedCommand(command,
ob.command = command args.split(" "),
ob.args = [] if args=="" else args.split(" ") args,
ob.args_str = args message)
ob.message = message
return ob
# return (True, command, args, message)
@staticmethod @staticmethod
def load(filepath): def load(filepath):
"""Return an object from the passed filepath """Return an object from the passed filepath
:param filepath: path to a json file. filename must end with .json :param filepath: path to a json file. filename must end with .json
:type filepath: str :type filepath: str
:Returns: | dict :Returns: | dict
""" """
if filepath.endswith(".json"): if filepath.endswith(".json"):
from json import load from json import load
return load(open(filepath, 'r')) return load(open(filepath, 'r'))