Abstract protocol handling code from bot logic. This will likely break a few modules.
This commit is contained in:
parent
0e28e96321
commit
595a38c741
|
@ -0,0 +1,372 @@
|
|||
"""
|
||||
.. module:: IRCCore
|
||||
:synopsis: IRC protocol class
|
||||
|
||||
.. moduleauthor:: Dave Pedu <dave@davepedu.com>
|
||||
|
||||
"""
|
||||
|
||||
import socket
|
||||
import asynchat
|
||||
import asyncore
|
||||
import logging
|
||||
import traceback
|
||||
from socket import SHUT_RDWR
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except:
|
||||
from io import BytesIO as StringIO
|
||||
|
||||
class IRCCore(asynchat.async_chat):
|
||||
""":param coreconfig: The core configuration of the bot. Passed by main.py.
|
||||
:type coreconfig: dict
|
||||
:param botconfig: The configuration of this instance of the bot. Passed by main.py.
|
||||
:type botconfig: dict
|
||||
"""
|
||||
|
||||
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"""
|
||||
|
||||
# Connection details
|
||||
self.server = None
|
||||
self.port = 0
|
||||
self.ipv6 = False
|
||||
|
||||
# IRC Messages are terminated with \r\n
|
||||
self.set_terminator(b"\r\n")
|
||||
|
||||
# Set up hooks for modules
|
||||
self.initHooks()
|
||||
|
||||
|
||||
def loop(self):
|
||||
asyncore.loop()
|
||||
|
||||
def kill(self):
|
||||
"""TODO close the socket"""
|
||||
pass
|
||||
|
||||
" 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.buffer.write(data)
|
||||
|
||||
def found_terminator(self):
|
||||
"""A complete command was pushed through, so clear the buffer and process it."""
|
||||
line = None
|
||||
buf = self.getBuf()
|
||||
try:
|
||||
line = buf.decode("UTF-8")
|
||||
except UnicodeDecodeError as ude:
|
||||
self.log.error("found_terminator(): could not decode input as UTF-8")
|
||||
self.log.error("found_terminator(): data: %s" % line)
|
||||
self.log.error("found_terminator(): repr(data): %s" % repr(line))
|
||||
self.log.error("found_terminator(): error: %s" % str(ude))
|
||||
return
|
||||
self.process_data(line)
|
||||
|
||||
def handle_close(self):
|
||||
"""Called when the socket is disconnected. We will want to reconnect. """
|
||||
self.log.debug("handle_close")
|
||||
self.connected=False
|
||||
self.close()
|
||||
self.fire_hook("_DISCONNECT")
|
||||
|
||||
def handle_error(self, *args, **kwargs):
|
||||
"""Called on fatal network errors."""
|
||||
self.log.warning("Connection failed.")
|
||||
|
||||
def _connect(self):
|
||||
"""Connect to IRC"""
|
||||
self.log.debug("Connecting to %(server)s:%(port)i", {"server":self.server, "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(self.server, self.port, socket_type)
|
||||
self.create_socket(socket_type, socket.SOCK_STREAM)
|
||||
|
||||
self.connect(socketInfo[0][4])
|
||||
|
||||
def handle_connect(self):
|
||||
"""When asynchat indicates our socket is connected, fire the connect hook"""
|
||||
self.connected=True
|
||||
# TODO move to an event
|
||||
self.log.debug("handle_connect: setting USER and NICK")
|
||||
self.fire_hook("_CONNECT")
|
||||
self.log.debug("handle_connect: complete")
|
||||
|
||||
def sendRaw(self, text):
|
||||
"""Send a raw string to the IRC server
|
||||
|
||||
:param text: the string to send
|
||||
:type text: str"""
|
||||
if self.connected:
|
||||
#self.log.debug(">> "+text)
|
||||
self.send( (text+"\r\n").encode("UTF-8").decode().encode("UTF-8"))
|
||||
else:
|
||||
self.log.warning("Send attempted while disconnected. >> "+text)
|
||||
|
||||
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:]
|
||||
command = data.split(" ")[0]
|
||||
data=data[data.find(" ")+1:]
|
||||
if(data[0]==":"):
|
||||
# no args
|
||||
trailing = data[1:].strip()
|
||||
else:
|
||||
trailing = data[data.find(" :")+2:].strip()
|
||||
data = data[:data.find(" :")]
|
||||
args = data.split(" ")
|
||||
for index,arg in enumerate(args):
|
||||
args[index]=arg.strip()
|
||||
if not command in self.hookcalls:
|
||||
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"""
|
||||
self.hooks = [
|
||||
'_CONNECT', # Called when the bot connects to IRC on the socket level
|
||||
'_DISCONNECT', # Called when the irc socket is forcibly closed
|
||||
'NOTICE', # :irc.129irc.com NOTICE AUTH :*** Looking up your hostname...
|
||||
'MODE', # :CloneABCD MODE CloneABCD :+iwx
|
||||
'PING', # PING :irc.129irc.com
|
||||
'JOIN', # :CloneA!dave@hidden-B4F6B1AA.rit.edu JOIN :#clonea
|
||||
'QUIT', # :HCSMPBot!~HCSMPBot@108.170.48.18 QUIT :Quit: Disconnecting!
|
||||
'NICK', # :foxiAway!foxi@irc.hcsmp.com NICK :foxi
|
||||
'PART', # :CloneA!dave@hidden-B4F6B1AA.rit.edu PART #clonea
|
||||
'PRIVMSG', # :CloneA!dave@hidden-B4F6B1AA.rit.edu PRIVMSG #clonea :aaa
|
||||
'KICK', # :xMopxShell!~rduser@host KICK #xMopx2 xBotxShellTest :xBotxShellTest
|
||||
'INVITE', # :gmx!~gmxgeek@irc.hcsmp.com INVITE Tyrone :#hcsmp'
|
||||
'001', # :irc.129irc.com 001 CloneABCD :Welcome to the 129irc IRC Network CloneABCD!CloneABCD@djptwc-laptop1.rit.edu
|
||||
'002', # :irc.129irc.com 002 CloneABCD :Your host is irc.129irc.com, running version Unreal3.2.8.1
|
||||
'003', # :irc.129irc.com 003 CloneABCD :This server was created Mon Jul 19 2010 at 03:12:01 EDT
|
||||
'004', # :irc.129irc.com 004 CloneABCD irc.129irc.com Unreal3.2.8.1 iowghraAsORTVSxNCWqBzvdHtGp lvhopsmntikrRcaqOALQbSeIKVfMCuzNTGj
|
||||
'005', # :irc.129irc.com 005 CloneABCD CMDS=KNOCK,MAP,DCCALLOW,USERIP UHNAMES NAMESX SAFELIST HCN MAXCHANNELS=10 CHANLIMIT=#:10 MAXLIST=b:60,e:60,I:60 NICKLEN=30 CHANNELLEN=32 TOPICLEN=307 KICKLEN=307 AWAYLEN=307 :are supported by this server
|
||||
'250', # :chaos.esper.net 250 xBotxShellTest :Highest connection count: 1633 (1632 clients) (186588 connections received)
|
||||
'251', # :irc.129irc.com 251 CloneABCD :There are 1 users and 48 invisible on 2 servers
|
||||
'252', # :irc.129irc.com 252 CloneABCD 9 :operator(s) online
|
||||
'254', # :irc.129irc.com 254 CloneABCD 6 :channels formed
|
||||
'255', # :irc.129irc.com 255 CloneABCD :I have 42 clients and 1 servers
|
||||
'265', # :irc.129irc.com 265 CloneABCD :Current Local Users: 42 Max: 47
|
||||
'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
|
||||
'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 -
|
||||
'376', # :chaos.esper.net 376 xBotxShell :End of /MOTD command.
|
||||
'422', # :irc.129irc.com 422 CloneABCD :MOTD File is missing
|
||||
'433', # :nova.esper.net 433 * pyircbot3 :Nickname is already in use.
|
||||
]
|
||||
" mapping of hooks to methods "
|
||||
self.hookcalls = {}
|
||||
for command in self.hooks:
|
||||
self.hookcalls[command]=[]
|
||||
|
||||
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
|
||||
:type args: list
|
||||
:param prefix: prefix of the sender of this command
|
||||
:type prefix: str
|
||||
:param trailing: data payload of the command
|
||||
:type trailing: str"""
|
||||
|
||||
for hook in self.hookcalls[command]:
|
||||
try:
|
||||
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
|
||||
:type method: object"""
|
||||
" add a single hook "
|
||||
if command in self.hooks:
|
||||
self.hookcalls[command].append(method)
|
||||
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
|
||||
:type method: object"""
|
||||
" remove a single hook "
|
||||
if command in self.hooks:
|
||||
for hookedMethod in self.hookcalls[command]:
|
||||
if hookedMethod == method:
|
||||
self.hookcalls[command].remove(hookedMethod)
|
||||
else:
|
||||
self.log.warning("Invalid hook - %s" % command)
|
||||
return False
|
||||
|
||||
" 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.nick, prefix = prefix.split("!")
|
||||
ob.username, ob.hostname = prefix.split("@")
|
||||
return ob
|
||||
else:
|
||||
ob = type('ServerPrefix', (object,), {})
|
||||
ob.hostname = prefix
|
||||
return ob
|
||||
|
||||
@staticmethod
|
||||
def trace():
|
||||
"""Return the stack trace of the bot as a string"""
|
||||
return traceback.format_exc()
|
||||
|
||||
" 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
|
||||
:type hostname: str
|
||||
:param realname: the bot's realname
|
||||
:type realname: str"""
|
||||
self.sendRaw("USER %s %s %s :%s" % (username, hostname, 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
|
||||
:type mode: str
|
||||
:param extra: additional argument if the mode needs it. Example: user@*!*
|
||||
:type extra: str"""
|
||||
if extra != None:
|
||||
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 <action>` 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 <user> <message>` command
|
||||
|
||||
:param channel: the channel from which the user will be kicked
|
||||
:type channel: str
|
||||
:param who: the nickname of the user to kick
|
||||
:type action: str
|
||||
: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)
|
||||
|
|
@ -6,22 +6,14 @@
|
|||
|
||||
"""
|
||||
|
||||
import socket
|
||||
import asynchat
|
||||
import logging
|
||||
import traceback
|
||||
import time
|
||||
import sys
|
||||
from socket import SHUT_RDWR
|
||||
from core.rpc import BotRPC
|
||||
from core.irccore import IRCCore
|
||||
import os.path
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except:
|
||||
from io import BytesIO as StringIO
|
||||
|
||||
class PyIRCBot(asynchat.async_chat):
|
||||
class PyIRCBot:
|
||||
""":param coreconfig: The core configuration of the bot. Passed by main.py.
|
||||
:type coreconfig: dict
|
||||
:param botconfig: The configuration of this instance of the bot. Passed by main.py.
|
||||
|
@ -32,11 +24,6 @@ class PyIRCBot(asynchat.async_chat):
|
|||
""" PyIRCBot version """
|
||||
|
||||
def __init__(self, coreconfig, botconfig):
|
||||
asynchat.async_chat.__init__(self)
|
||||
|
||||
self.connected=False
|
||||
"""If we're connected or not"""
|
||||
|
||||
self.log = logging.getLogger('PyIRCBot')
|
||||
"""Reference to logger object"""
|
||||
|
||||
|
@ -49,215 +36,58 @@ class PyIRCBot(asynchat.async_chat):
|
|||
self.rpc = BotRPC(self)
|
||||
"""Reference to BotRPC thread"""
|
||||
|
||||
self.buffer = StringIO()
|
||||
"""cSTringIO used as a buffer"""
|
||||
self.irc = IRCCore()
|
||||
"""IRC protocol class"""
|
||||
self.irc.server = self.botconfig["connection"]["server"]
|
||||
self.irc.port = self.botconfig["connection"]["port"]
|
||||
self.irc.ipv6 = True if self.botconfig["connection"]["ipv6"]=="on" else False
|
||||
|
||||
self.alive = True
|
||||
""" True if we should try to stay connected"""
|
||||
self.irc.addHook("_DISCONNECT", self.handle_close)
|
||||
|
||||
# IRC Messages are terminated with \r\n
|
||||
self.set_terminator(b"\r\n")
|
||||
|
||||
# Set up hooks for modules
|
||||
self.initHooks()
|
||||
# legacy support
|
||||
self.act_PONG = self.irc.act_PONG
|
||||
self.act_USER = self.irc.act_USER
|
||||
self.act_NICK = self.irc.act_NICK
|
||||
self.act_JOIN = self.irc.act_JOIN
|
||||
self.act_PRIVMSG = self.irc.act_PRIVMSG
|
||||
self.act_MODE = self.irc.act_MODE
|
||||
self.act_ACTION = self.irc.act_ACTION
|
||||
self.act_KICK = self.irc.act_KICK
|
||||
self.act_QUIT = self.irc.act_QUIT
|
||||
self.get_nick = self.irc.get_nick
|
||||
|
||||
# Load modules
|
||||
self.initModules()
|
||||
|
||||
# Connect to IRC
|
||||
self._connect()
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
self.irc._connect()
|
||||
|
||||
def loop(self):
|
||||
self.irc.loop()
|
||||
|
||||
def kill(self):
|
||||
"""Shut down the bot violently"""
|
||||
#TODO: have rpc thread be daemonized so it just dies
|
||||
#try:
|
||||
# self.rpc.server._Server__transport.shutdown(SHUT_RDWR)
|
||||
#except Exception as e:
|
||||
# self.log.error(str(e))
|
||||
#try:
|
||||
# self.rpc.server._Server__transport.close()
|
||||
#except Exception as e:
|
||||
# self.log.error(str(e))
|
||||
|
||||
#Kill RPC thread
|
||||
#self.rpc._stop()
|
||||
|
||||
#Close all modules
|
||||
self.closeAllModules()
|
||||
# Mark for shutdown
|
||||
self.alive = False
|
||||
# Exit
|
||||
|
||||
self.irc.kill()
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
" 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.buffer.write(data)
|
||||
|
||||
def found_terminator(self):
|
||||
"""A complete command was pushed through, so clear the buffer and process it."""
|
||||
line = None
|
||||
buf = self.getBuf()
|
||||
try:
|
||||
line = buf.decode("UTF-8")
|
||||
except UnicodeDecodeError as ude:
|
||||
self.log.error("found_terminator(): could not decode input as UTF-8")
|
||||
self.log.error("found_terminator(): data: %s" % line)
|
||||
self.log.error("found_terminator(): repr(data): %s" % repr(line))
|
||||
self.log.error("found_terminator(): error: %s" % str(ude))
|
||||
return
|
||||
self.process_data(line)
|
||||
|
||||
# TODO move handle_close to an event hook
|
||||
def handle_close(self):
|
||||
"""Called when the socket is disconnected. We will want to reconnect. """
|
||||
self.log.debug("handle_close")
|
||||
self.connected=False
|
||||
self.close()
|
||||
if self.alive:
|
||||
self.log.warning("Connection was lost. Reconnecting in 5 seconds.")
|
||||
time.sleep(5)
|
||||
self._connect()
|
||||
|
||||
def handle_error(self, *args, **kwargs):
|
||||
"""Called on fatal network errors."""
|
||||
self.log.warning("Connection failed.")
|
||||
|
||||
def _connect(self):
|
||||
"""Connect to IRC"""
|
||||
self.log.debug("Connecting to %(server)s:%(port)i", {"server":self.botconfig["connection"]["server"], "port":self.botconfig["connection"]["port"]})
|
||||
socket_type = socket.AF_INET
|
||||
if self.botconfig["connection"]["ipv6"]:
|
||||
self.log.info("IPv6 is enabled.")
|
||||
socket_type = socket.AF_INET6
|
||||
socketInfo = socket.getaddrinfo(self.botconfig["connection"]["server"], self.botconfig["connection"]["port"], socket_type)
|
||||
self.create_socket(socket_type, socket.SOCK_STREAM)
|
||||
if "bindaddr" in self.botconfig["connection"]:
|
||||
self.bind((self.botconfig["connection"]["bindaddr"], 0))
|
||||
self.connect(socketInfo[0][4])
|
||||
|
||||
def handle_connect(self):
|
||||
"""When asynchat indicates our socket is connected, fire the connect hook"""
|
||||
self.connected=True
|
||||
self.log.debug("handle_connect: setting USER and NICK")
|
||||
self.fire_hook("_CONNECT")
|
||||
self.log.debug("handle_connect: complete")
|
||||
|
||||
def sendRaw(self, text):
|
||||
"""Send a raw string to the IRC server
|
||||
|
||||
:param text: the string to send
|
||||
:type text: str"""
|
||||
if self.connected:
|
||||
#self.log.debug(">> "+text)
|
||||
self.send( (text+"\r\n").encode("UTF-8").decode().encode("UTF-8"))
|
||||
else:
|
||||
self.log.warning("Send attempted while disconnected. >> "+text)
|
||||
|
||||
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:]
|
||||
command = data.split(" ")[0]
|
||||
data=data[data.find(" ")+1:]
|
||||
if(data[0]==":"):
|
||||
# no args
|
||||
trailing = data[1:].strip()
|
||||
else:
|
||||
trailing = data[data.find(" :")+2:].strip()
|
||||
data = data[:data.find(" :")]
|
||||
args = data.split(" ")
|
||||
for index,arg in enumerate(args):
|
||||
args[index]=arg.strip()
|
||||
if not command in self.hookcalls:
|
||||
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"""
|
||||
self.hooks = [
|
||||
'_CONNECT', # Called when the bot connects to IRC on the socket level
|
||||
'NOTICE', # :irc.129irc.com NOTICE AUTH :*** Looking up your hostname...
|
||||
'MODE', # :CloneABCD MODE CloneABCD :+iwx
|
||||
'PING', # PING :irc.129irc.com
|
||||
'JOIN', # :CloneA!dave@hidden-B4F6B1AA.rit.edu JOIN :#clonea
|
||||
'QUIT', # :HCSMPBot!~HCSMPBot@108.170.48.18 QUIT :Quit: Disconnecting!
|
||||
'NICK', # :foxiAway!foxi@irc.hcsmp.com NICK :foxi
|
||||
'PART', # :CloneA!dave@hidden-B4F6B1AA.rit.edu PART #clonea
|
||||
'PRIVMSG', # :CloneA!dave@hidden-B4F6B1AA.rit.edu PRIVMSG #clonea :aaa
|
||||
'KICK', # :xMopxShell!~rduser@host KICK #xMopx2 xBotxShellTest :xBotxShellTest
|
||||
'INVITE', # :gmx!~gmxgeek@irc.hcsmp.com INVITE Tyrone :#hcsmp'
|
||||
'001', # :irc.129irc.com 001 CloneABCD :Welcome to the 129irc IRC Network CloneABCD!CloneABCD@djptwc-laptop1.rit.edu
|
||||
'002', # :irc.129irc.com 002 CloneABCD :Your host is irc.129irc.com, running version Unreal3.2.8.1
|
||||
'003', # :irc.129irc.com 003 CloneABCD :This server was created Mon Jul 19 2010 at 03:12:01 EDT
|
||||
'004', # :irc.129irc.com 004 CloneABCD irc.129irc.com Unreal3.2.8.1 iowghraAsORTVSxNCWqBzvdHtGp lvhopsmntikrRcaqOALQbSeIKVfMCuzNTGj
|
||||
'005', # :irc.129irc.com 005 CloneABCD CMDS=KNOCK,MAP,DCCALLOW,USERIP UHNAMES NAMESX SAFELIST HCN MAXCHANNELS=10 CHANLIMIT=#:10 MAXLIST=b:60,e:60,I:60 NICKLEN=30 CHANNELLEN=32 TOPICLEN=307 KICKLEN=307 AWAYLEN=307 :are supported by this server
|
||||
'250', # :chaos.esper.net 250 xBotxShellTest :Highest connection count: 1633 (1632 clients) (186588 connections received)
|
||||
'251', # :irc.129irc.com 251 CloneABCD :There are 1 users and 48 invisible on 2 servers
|
||||
'252', # :irc.129irc.com 252 CloneABCD 9 :operator(s) online
|
||||
'254', # :irc.129irc.com 254 CloneABCD 6 :channels formed
|
||||
'255', # :irc.129irc.com 255 CloneABCD :I have 42 clients and 1 servers
|
||||
'265', # :irc.129irc.com 265 CloneABCD :Current Local Users: 42 Max: 47
|
||||
'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
|
||||
'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 -
|
||||
'376', # :chaos.esper.net 376 xBotxShell :End of /MOTD command.
|
||||
'422', # :irc.129irc.com 422 CloneABCD :MOTD File is missing
|
||||
'433', # :nova.esper.net 433 * pyircbot3 :Nickname is already in use.
|
||||
]
|
||||
" mapping of hooks to methods "
|
||||
self.hookcalls = {}
|
||||
for command in self.hooks:
|
||||
self.hookcalls[command]=[]
|
||||
|
||||
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
|
||||
:type args: list
|
||||
:param prefix: prefix of the sender of this command
|
||||
:type prefix: str
|
||||
:param trailing: data payload of the command
|
||||
:type trailing: str"""
|
||||
|
||||
for hook in self.hookcalls[command]:
|
||||
try:
|
||||
hook(args, prefix, trailing)
|
||||
except:
|
||||
self.log.warning("Error processing hook: \n%s"% self.trace())
|
||||
|
||||
def initModules(self):
|
||||
"""load modules specified in instance config"""
|
||||
" storage of imported modules "
|
||||
|
@ -394,9 +224,9 @@ class PyIRCBot(asynchat.async_chat):
|
|||
for hook in module.hooks:
|
||||
if type(hook.hook) == list:
|
||||
for hookcmd in hook.hook:
|
||||
self.addHook(hookcmd, hook.method)
|
||||
self.irc.addHook(hookcmd, hook.method)
|
||||
else:
|
||||
self.addHook(hook.hook, hook.method)
|
||||
self.irc.addHook(hook.hook, hook.method)
|
||||
|
||||
def unloadModuleHooks(self, module):
|
||||
"""**Internal.** Disable (disconnect) hooks of a module
|
||||
|
@ -407,39 +237,9 @@ class PyIRCBot(asynchat.async_chat):
|
|||
for hook in module.hooks:
|
||||
if type(hook.hook) == list:
|
||||
for hookcmd in hook.hook:
|
||||
self.removeHook(hookcmd, hook.method)
|
||||
self.irc.removeHook(hookcmd, hook.method)
|
||||
else:
|
||||
self.removeHook(hook.hook, hook.method)
|
||||
|
||||
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
|
||||
:type method: object"""
|
||||
" add a single hook "
|
||||
if command in self.hooks:
|
||||
self.hookcalls[command].append(method)
|
||||
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
|
||||
:type method: object"""
|
||||
" remove a single hook "
|
||||
if command in self.hooks:
|
||||
for hookedMethod in self.hookcalls[command]:
|
||||
if hookedMethod == method:
|
||||
self.hookcalls[command].remove(hookedMethod)
|
||||
else:
|
||||
self.log.warning("Invalid hook - %s" % command)
|
||||
return False
|
||||
self.irc.removeHook(hook.hook, hook.method)
|
||||
|
||||
def getmodulebyname(self, name):
|
||||
"""Get a module object by name
|
||||
|
@ -504,28 +304,6 @@ class PyIRCBot(asynchat.async_chat):
|
|||
return "%s/config/%s.yml" % (self.botconfig["bot"]["datadir"], moduleName)
|
||||
|
||||
" 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.nick, prefix = prefix.split("!")
|
||||
ob.username, ob.hostname = prefix.split("@")
|
||||
return ob
|
||||
else:
|
||||
ob = type('ServerPrefix', (object,), {})
|
||||
ob.hostname = prefix
|
||||
return ob
|
||||
|
||||
@staticmethod
|
||||
def trace():
|
||||
"""Return the stack trace of the bot as a string"""
|
||||
return traceback.format_exc()
|
||||
|
||||
@staticmethod
|
||||
def messageHasCommand(command, message, requireArgs=False):
|
||||
"""Check if a message has a command with or without args in it
|
||||
|
@ -573,96 +351,3 @@ class PyIRCBot(asynchat.async_chat):
|
|||
ob.message = message
|
||||
return ob
|
||||
# return (True, command, args, message)
|
||||
|
||||
|
||||
" Data Methods "
|
||||
def get_nick(self):
|
||||
"""Get the bot's current nick
|
||||
|
||||
:returns: str - the bot's current nickname"""
|
||||
return self.config["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
|
||||
:type hostname: str
|
||||
:param realname: the bot's realname
|
||||
:type realname: str"""
|
||||
self.sendRaw("USER %s %s %s :%s" % (username, hostname, self.botconfig["connection"]["server"], realname))
|
||||
|
||||
def act_NICK(self, newNick):
|
||||
"""Use the `/nick` command
|
||||
|
||||
:param newNick: new nick for the bot
|
||||
:type newNick: str"""
|
||||
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
|
||||
:type mode: str
|
||||
:param extra: additional argument if the mode needs it. Example: user@*!*
|
||||
:type extra: str"""
|
||||
if extra != None:
|
||||
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 <action>` 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 <user> <message>` command
|
||||
|
||||
:param channel: the channel from which the user will be kicked
|
||||
:type channel: str
|
||||
:param who: the nickname of the user to kick
|
||||
:type action: str
|
||||
: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)
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ import os
|
|||
import sys
|
||||
import logging
|
||||
import yaml
|
||||
import asyncore
|
||||
from optparse import OptionParser
|
||||
from core.pyircbot import PyIRCBot
|
||||
|
||||
|
@ -36,7 +35,7 @@ if __name__ == "__main__":
|
|||
|
||||
bot = PyIRCBot(coreconfig, botconfig)
|
||||
try:
|
||||
asyncore.loop()
|
||||
bot.loop()
|
||||
except KeyboardInterrupt:
|
||||
bot.kill()
|
||||
|
||||
|
|
|
@ -16,4 +16,4 @@ class PingResponder(ModuleBase):
|
|||
def pingrespond(self, args, prefix, trailing):
|
||||
# got a ping? send it right back
|
||||
self.bot.act_PONG(trailing)
|
||||
self.log.info("Responded to a ping: %s" % trailing)
|
||||
self.log.info("%s Responded to a ping: %s" % (self.bot.get_nick(), trailing))
|
||||
|
|
Loading…
Reference in New Issue