1
0
Fork 0

cleanups, commenting, moar code and other nice things.. (i'll start writing better commitmessages soon, i promise!)

This commit is contained in:
fanir 2014-03-03 18:06:42 +01:00
parent 91c86c1d92
commit df1ca3ad2d

322
main.py
View file

@ -27,6 +27,7 @@
import sys, string, socket, re, signal import sys, string, socket, re, signal
from random import choice
from time import sleep from time import sleep
from select import poll, POLLIN, POLLPRI, POLLOUT,\ from select import poll, POLLIN, POLLPRI, POLLOUT,\
POLLERR, POLLHUP, POLLNVAL POLLERR, POLLHUP, POLLNVAL
@ -39,30 +40,25 @@ class log():
DEBUG, INFO, WARNING, ERROR, FATAL, SILENT =\ DEBUG, INFO, WARNING, ERROR, FATAL, SILENT =\
0, 1, 2, 3, 4, 5 0, 1, 2, 3, 4, 5
levels = {
0: "[DEBUG] ", 1: "[INFO] ", 2: "[WARNING] ",
3: "[ERROR] ", 4: "[FATAL] ", 5: "[SILENT] "
}
def show(level, msg): def show(level, msg):
if level in range(log.DEBUG, log.SILENT): if level in range(log.DEBUG, log.SILENT):
if LOGLEVEL <= level: print(msg) if LOGLEVEL <= level: print("".join((log.levels[level], msg)))
else: else:
raise ValueError("That's not a loglevel!") raise ValueError("That's not a loglevel!")
class pircbot(): class pircbot():
def encode(self, textstring): def __init__(self, nicknames, server, port=6667,
for codec in self.encodings: ident="pircbot", realname="pircbot", serverpasswd="",
try: return textstring.encode(codec) encodings=("utf-8", "latin-1"), admins="",
except UnicodeDecodeError: continue query_type="", command_prefix=".",
return textstring.encode(self.encodings[0], 'ignore') parser_wait_time=0.1):
def decode(self, textstring):
for codec in self.encodings:
try: return textstring.decode(codec)
except UnicodeDecodeError: continue
return textstring.decode(self.encodings[0], 'ignore')
def __init__(self, nicknames, server, port=6667, ident="pircbot",
realname="pircbot", encodings=("utf-8", "latin-1"),
command_prefix=".", admins="", serverpasswd="", parser_wait_time=0.1):
self.server = server self.server = server
self.port = port self.port = port
self.serverpasswd = serverpasswd self.serverpasswd = serverpasswd
@ -74,39 +70,89 @@ class pircbot():
self.admins = admins self.admins = admins
self.cmdprefix = command_prefix self.cmdprefix = command_prefix
self.query_type = query_type.upper()
self.parser_wait_time=parser_wait_time self.parser_wait_time=parser_wait_time
self.user = {} self.user = {}
self.socket = None self.socket = None
self.recvloop = None
self.recvbuffer = bytearray(1024) self.recvbuffer = bytearray(1024)
self.ready = False self.recvloop = None
self.parseloop = None
self.die_event = Event() self.die_event = Event()
self.ready = False
self.mode_reply_to = None
def recv_loop(self): def connect(self):
# connect
self.socket = socket.socket()
log.show(log.DEBUG, "--- SOCKET OPENING ---")
try:
self.socket.connect((self.server, self.port))
except socket.error as e:
log.show(log.FATAL, "Fehler: %s" % e)
return
# start getting data
self.recvloop = Thread(target=self.recv, name="recvloop")
self.recvloop.start()
# optionally send a server password
if self.serverpasswd != "": self.send("PASS %s" % self.serverpasswd)
# get a nick
self.send("NICK %s" % self.nicknames.pop(0))
# set user data
self.send("USER %s 0 * :%s" % (self.ident, self.realname))
# implements irc command QUIT and (more or less) clean exiting
def disconnect(self, reason="", send_quit=True):
if send_quit:
try:
self.send("QUIT :%s" % reason)
sleep(1)
except: pass
self.die_event.set()
ctr = 0
while self.recvloop.is_alive() and self.parseloop.is_alive() and ctr < 15:
ctr += 1
sleep(1)
log.show(log.DEBUG, "--- SOCKET CLOSING ---")
try:
self.socket.shutdown(socket.SHUT_RDWR)
self.socket.close()
except: pass
### threaded functions ###
# loop for recieving data from irc
def recv(self):
""" """
Loop for reciving data Loop for reciving data
""" """
parser = Thread(target=self.parse_loop, name="parser") log.show(log.DEBUG, "--- RECVLOOP STARTING ---")
self.parseloop = Thread(target=self.parser, name="parser")
p = poll() p = poll()
p.register(self.socket.fileno(), POLLIN) p.register(self.socket.fileno(), POLLIN)
while not self.die_event.is_set(): while not self.die_event.is_set():
ap = p.poll(1000) ap = p.poll(1000)
if (self.socket.fileno(), POLLIN) in ap: if (self.socket.fileno(), POLLIN) in ap:
self.recvbuffer.extend(self.socket.recv(1024)) self.recvbuffer.extend(self.socket.recv(1024))
if not parser.is_alive(): if not self.parseloop.is_alive():
parser = Thread(target=self.parse_loop, name="parser") self.parseloop = Thread(target=self.parser, name="parser")
parser.start() self.parseloop.start()
print("--- RECVLOOP EXITING ---") log.show(log.DEBUG, "--- RECVLOOP EXITING ---")
def parse_loop(self): # loop for parsing incoming data
def parser(self):
""" """
Loop for parsing incoming data Loop for parsing incoming data
""" """
log.show(log.DEBUG, "--- PARSELOOP STARTING ---")
while not self.die_event.is_set():# and self.recvbuffer.endswith(b"\r\n"):# != b"": while not self.die_event.is_set():# and self.recvbuffer.endswith(b"\r\n"):# != b"":
if self.recvbuffer.endswith(b"\r\n"): if self.recvbuffer.endswith(b"\r\n"):
# get and decode line from buffer # get and decode line from buffer
@ -122,6 +168,7 @@ class pircbot():
prefix = head.pop(0)[1:] prefix = head.pop(0)[1:]
if "@" in prefix: if "@" in prefix:
origin["nick"], origin["ident"], origin["host"] = re.match(r"(\S+)!(\S+)@(\S+)", prefix).groups() origin["nick"], origin["ident"], origin["host"] = re.match(r"(\S+)!(\S+)@(\S+)", prefix).groups()
origin["mask"] = prefix
else: else:
origin["server"] = prefix origin["server"] = prefix
else: else:
@ -130,20 +177,32 @@ class pircbot():
command = head.pop(0) command = head.pop(0)
# parse params # parse params
params = head params = head
params.append(larg) params.append(larg.strip())
print(params)
log.show(log.DEBUG, " > %s" % rawline) log.show(log.DEBUG, " > %s" % rawline)
# PING # PING
if command == "PING": if command == "PING":
self.send("PONG %s" % params[0]) self.send("PONG %s" % params[0])
# PRIVMSG # PRIVMSG and NOTICE
elif command == "PRIVMSG": elif command == "PRIVMSG" or command == "NOTICE":
if params[1].startswith(self.cmdprefix): if params[1].startswith(self.cmdprefix):
args = params[1].lstrip(self.cmdprefix).split(" ") args = []
sndline = self.on_command(prefix, origin, params[0], args[0], args[1:]) for v in params[1].lstrip(self.cmdprefix).split(" "):
if v!="": args.append(v)
sndline = self.on_command(command, prefix, origin, params[0], args[0].lower(), args[1:])
if sndline != None: if sndline != None:
self.send(sndline) self.send(sndline)
# 221 (RPL_UMODEIS)
elif command == "221" and self.mode_reply_to is not None:
self.send("PRIVMSG %s :Modes are %s" % (self.mode_reply_to, params[1]))
# INVITE
elif command == "INVITE":
if self.is_admin(origin["mask"]):
self.join(params[1])
else:
self.send("NOTICE %s :You can not force me to do that!" % origin["nick"])
# 001 (RPL_WELCOME) # 001 (RPL_WELCOME)
elif command == "001": elif command == "001":
self.user["mask"] = re.search(r" (\S+!\S+@\S+)$", rawline.split(" :", 1)[1]).groups()[0] self.user["mask"] = re.search(r" (\S+!\S+@\S+)$", rawline.split(" :", 1)[1]).groups()[0]
@ -155,69 +214,110 @@ class pircbot():
# KILL # KILL
elif command == "KILL": elif command == "KILL":
log.show(log.WARNING, "Got killed by %s: %s" % (params[0], params[1:])) log.show(log.WARNING, "Got killed by %s: %s" % (params[0], params[1:]))
self.quit(send=False) self.disconnect(send_quit=False)
else: else:
sleep(self.parser_wait_time) sleep(self.parser_wait_time)
print("--- PARSELOOP EXITING ---") log.show(log.DEBUG, "--- PARSELOOP EXITING ---")
def send(self, data):
log.show(log.DEBUG, "< %s" % data)
try: self.socket.send(self.encode("".join((data, "\r\n"))))
except BrokenPipeError as e:
log.show(log.FATAL, e)
self.quit(send=False)
def connect(self): ### helper functions ###
# connect
self.socket = socket.socket()
try:
self.socket.connect((self.server, self.port))
except socket.error as e:
log.show(log.FATAL, "Fehler: %s" % e)
return
# start getting data
self.recvloop = Thread(target=self.recv_loop, name="recvloop")
self.recvloop.start()
# optionally send a server password
if self.serverpasswd != "": self.send("PASS %s" % self.serverpasswd)
# get a nick
self.send("NICK %s" % self.nicknames.pop(0))
# set user data
self.send("USER %s 0 * :%s" % (self.ident, self.realname))
# encodes data for irc
def encode(self, textstring):
for codec in self.encodings:
try: return textstring.encode(codec)
except UnicodeDecodeError: continue
return textstring.encode(self.encodings[0], 'ignore')
# decodes data from irc
def decode(self, textstring):
for codec in self.encodings:
try: return textstring.decode(codec)
except UnicodeDecodeError: continue
return textstring.decode(self.encodings[0], 'ignore')
# checks if a given user is listed in admins
def is_admin(self, user): def is_admin(self, user):
for admin in self.admins: for admin in self.admins:
if re.search(admin, user) != None: if re.search(admin, user) != None:
return True return True
return False return False
def quit(self, reason="", send=True): # checks if s is a valid channel name
if send: def is_channel(self, s):
try: return (True if s[0] in ('&', '#', '+', '!') else False)
self.send("QUIT :%s" % reason)
except: pass
self.die_event.set() def exec_on_ready(self, command, retry_times=-1, interval=1):
print("--- SOCKET CLOSING ---") if command.startswith("self."):
try: if retry_times == -1:
self.socket.shutdown(socket.SHUT_RDWR) while not self.ready: sleep(interval)
self.socket.close() else:
except: pass cnt = 0
while not self.ready and cnt<retry_times: sleep(interval)
if not self.ready:
log.show(log.WARNING, "Connection did not get ready in time (%sx %s seconds), \"%s\" not executed" % (retry_times, interval, command))
return True
exec("ret = %s" % command)
return False
else:
log.show(log.FATAL, "exec_on_ready() called with an invalid command: %s" % command)
return True
def exec_retry(self, command, times=5, interval=1, wait_for_ready=True):
if command.startswith("self."):
if wait_for_ready:
while not self.ready: sleep(interval)
cnt = 0
exec("while %s and cnt<times: sleep(interval)" % command)
else: log.show(log.FATAL, "exec_retry() called with an invalid command.")
def send(self, data):
log.show(log.DEBUG, "< %s" % data)
try: self.socket.send(self.encode("".join((data, "\r\n"))))
except BrokenPipeError as e:
log.show(log.FATAL, e)
self.disconnect(send_quit=False)
# replies to channel by PRIVMSG
def chanmsg(self, channel, msg):
self.send("".join(("PRIVMSG ", channel, " :", msg)))
# replies to user by NOTICE or PRIVMSG
def query(self, nick, msg, in_query_type):
self.send("".join((self.query_type if self.query_type!="" else in_query_type, " ", nick, " :", msg)))
# decides whether to print to user or to channel based on the string origin
def reply(self, origin, source, msg, in_query_type):
if self.is_channel(source):
self.chanmsg(source, msg)
else:
self.query(origin["nick"], msg, in_query_type)
### irc command wrapper ###
def nick(self, nick):
self.exec_on_ready("".join(('self.send("JOIN %s" % "', channel, '")')))
def join(self, channel): def join(self, channel):
while not self.ready: sleep(1) self.exec_on_ready("".join(('self.send("JOIN %s" % "', channel, '")')))
self.send("JOIN %s" % channel)
def part(self, channel): def part(self, channel):
while not self.ready: sleep(1) self.exec_on_ready("".join(('self.send("PART %s" % "', channel, '")')))
self.send("PART %s" % channel)
def set_mode(self, modes): def set_mode(self, modes):
while not self.ready: sleep(1) self.exec_on_ready("".join(('self.send("MODE %s :%s" % (self.user["nick"], "', modes, '"))')))
self.send("MODE %s :%s" % (self.user["nick"], modes))
def get_modes(self):
self.exec_on_ready("".join(('self.send("MODE %s" % self.user["nick"])')))
def on_command(self, prefix, origin, source, command, params): ### handler ###
# bot-command handler
def on_command(self, in_query_type, prefix, origin, source, command, params):
""" """
Executed when getting a PRIVMSG starting with self.cmdprefix Executed when getting a PRIVMSG starting with self.cmdprefix
Prefix contains the optional prefix of the raw line. Prefix contains the optional prefix of the raw line.
@ -231,22 +331,36 @@ class pircbot():
# hello # hello
if command == "hello": if command == "hello":
greeting = "".join(("Hi " + origin["nick"] +"!")) greeting = "".join(("Hi " + origin["nick"] +"!"))
if source[0] in {"#", "+", "!", "&"}: self.reply(origin, source, greeting, in_query_type)
return "PRIVMSG %s :%s" % (source, greeting)
else:
return "PRIVMSG %s :%s" % (origin["nick"], greeting)
# join <channel> # join <channel>
elif command == "join" and len(params)>0: elif command == "join" and len(params)>0:
if self.is_admin(origin["nick"]): self.join(params[0]) if self.is_admin(origin["mask"]): self.join(params[0])
else: return "PRIVMSG %s :You can't do that!" % origin["nick"] else: return "PRIVMSG %s :You cannot do that!" % origin["nick"]
# part <channel> # part <channel>
elif command == "part" and len(params)>0: elif command == "part" and len(params)>0:
if self.is_admin(origin["nick"]): self.part(params[0]) if self.is_admin(origin["mask"]): self.part(params[0])
else: return "PRIVMSG %s :You can't do that!" % origin["nick"] else: return "PRIVMSG %s :You cannot do that!" % origin["nick"]
# mode ±<modes>
elif command == "mode":
if self.is_admin(origin["mask"]):
if len(params) > 0:
self.set_mode(params[0])
else:
self.mode_reply_to = origin["nick"]
self.get_modes()
else: return "PRIVMSG %s :You cannot do that!" % origin["nick"]
# die [<quitmsg>] # die [<quitmsg>]
elif command == "die": elif command == "die":
if self.is_admin(origin["nick"]): if self.is_admin(origin["mask"]):
self.quit("".join(params) if len(params)>0 else "".join((origin["nick"], " shot me, dying now... Bye..."))) self.disconnect("".join(params) if len(params)>0 else "".join((origin["nick"], " shot me, dying now... Bye...")))
else: return "PRIVMSG %s :Go die yourself!" % origin["nick"]
else:
replies = [
("PRIVMSG %s :What? \"%s\" is not a command!", 15),
("PRIVMSG %s :%s? What's that?", 3),
("PRIVMSG %s :Sorry, I don't know how to %s...", 1)
]
return choice([val for val, cnt in replies for i in range(cnt)]) % (origin["nick"], command)
@ -262,10 +376,12 @@ def main():
global bot global bot
args = parseargs() args = parseargs()
if args.action == "start": if args.action == "start":
bot = pircbot(nicknames=NICKNAMES, ident=IDENT, realname=REALNAME,
server=SERVER, port=PORT, encodings=ENCODINGS, command_prefix=COMMAND_PREFIX,
admins=ADMINS, serverpasswd=SERVERPASSWD, parser_wait_time=PARSER_WAIT_TIME)
try: try:
bot = pircbot(nicknames=NICKNAMES, ident=IDENT, realname=REALNAME,
server=SERVER, port=PORT, serverpasswd=SERVERPASSWD,
encodings=ENCODINGS, admins=ADMINS,
query_type=QUERY_TYPE, command_prefix=COMMAND_PREFIX,
parser_wait_time=PARSER_WAIT_TIME)
bot.connect() bot.connect()
# wait for the bot to become ready # wait for the bot to become ready
while bot.ready and not bot.die_event.is_set() == False: while bot.ready and not bot.die_event.is_set() == False:
@ -282,10 +398,10 @@ def main():
sleep(1) sleep(1)
except KeyboardInterrupt: except KeyboardInterrupt:
log.show(log.INFO, "Got Ctrl-C, dying now...") log.show(log.INFO, "Got Ctrl-C, dying now...")
bot.quit() bot.disconnect("Ouch! Got shot by Ctrl-C, dying now... See you!")
elif args.action == "stop": print("nope!") elif args.action == "stop": print("nope!")
print("--- MAIN EXITING ---") log.show(log.DEBUG, "--- MAIN EXITING ---")
return 0 return 0
@ -295,12 +411,6 @@ if __name__ == '__main__':
### SETTINGS ### ### SETTINGS ###
################ ################
# With how much information do you want to be annoyed?
# DEBUG spams most, FATAL least. WARNING should be a good tradeoff.
# One of: log.DEBUG, log.INFO, log.WARNING, log.ERROR, log.FATAL
LOGLEVEL = log.DEBUG
# Also known as username. Some IRC-internal. # Also known as username. Some IRC-internal.
# By default, the nickname will be used as ident. # By default, the nickname will be used as ident.
IDENT = "chalkbot" IDENT = "chalkbot"
@ -319,17 +429,18 @@ if __name__ == '__main__':
# The Server name to connect to. Duh! # The Server name to connect to. Duh!
#SERVER = "fanir.de" SERVER = "fanir.de"
SERVER = "localhost" #SERVER = "localhost"
# "Default" is 6667. An often used port for SSL would be 6697, if SSL would be # "Default" is 6667. An often used port for SSL would be 6697, if SSL would be
# supported. Maybe in a future, far far away... # supported. Maybe in a future, far far away...
PORT = 6667 PORT = 6667
# Serverpassword. Empty in most cases.
SERVERPASSWD = "" SERVERPASSWD = ""
# A comma-seperated list of channels to join, enclosed by braces. # A comma-seperated list of channels to join, enclosed by braces.
CHANNELS = ["#bots", "#main"] CHANNELS = ["#bots"]
# The encodings to try when getting messages from IRC. Will be tried in the given order. # The encodings to try when getting messages from IRC. Will be tried in the given order.
# The first one is used to encode data when sending stuff. # The first one is used to encode data when sending stuff.
@ -340,10 +451,21 @@ if __name__ == '__main__':
# List of users (hostmasks, will be parsed as regex) who can do important stuff, # List of users (hostmasks, will be parsed as regex) who can do important stuff,
# like joining and parting channels and shutting down the bot. # like joining and parting channels and shutting down the bot.
ADMINS = ["Fanir.*"] ADMINS = ["Fanir\!.*"]
# The prefix for commands for the bot.
COMMAND_PREFIX = "." COMMAND_PREFIX = "."
# Which way shouldbe used to speak to users?
# "" means, the type of the incomming message should be used.
# One of: "NOTICE", "PRIVMSG", ""
QUERY_TYPE = ""
# With how much information do you want to be annoyed?
# DEBUG spams most, FATAL least. WARNING should be a good tradeoff.
# One of: log.DEBUG, log.INFO, log.WARNING, log.ERROR, log.FATAL
LOGLEVEL = log.DEBUG
# The time the parser for incoming data should wait between each attempt to read new data in seconds. # The time the parser for incoming data should wait between each attempt to read new data in seconds.
# High values will certainly make the bot reply slowly while very low values increads cpu load and therefore will perform badly on slow machines. # High values will certainly make the bot reply slowly while very low values increads cpu load and therefore will perform badly on slow machines.