1
0
Fork 0
chalkbot/main.py

360 lines
13 KiB
Python
Raw Normal View History

2014-02-27 12:05:38 +01:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# main.py
#
# Copyright 2014 Fanir <projects@mail.fanir.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
#
#
2014-02-28 17:04:08 +01:00
# SETTINGS CAN BE FOUND AT THE BOTTOM OF THE FILE
2014-03-01 01:34:46 +01:00
import sys, string, socket, re, signal
from time import sleep
2014-02-28 17:04:08 +01:00
from select import poll, POLLIN, POLLPRI, POLLOUT,\
POLLERR, POLLHUP, POLLNVAL
2014-03-01 01:25:12 +01:00
from threading import Thread, Event
2014-02-28 17:04:08 +01:00
bot = None
2014-03-01 01:25:12 +01:00
2014-02-28 17:04:08 +01:00
class log():
DEBUG, INFO, WARNING, ERROR, FATAL, SILENT =\
0, 1, 2, 3, 4, 5
def show(level, msg):
if level in range(log.DEBUG, log.SILENT):
if LOGLEVEL <= level: print(msg)
else:
raise ValueError("That's not a loglevel!")
2014-02-27 12:05:38 +01:00
2014-03-01 01:25:12 +01:00
2014-02-27 12:05:38 +01:00
class pircbot():
def encode(self, textstring):
for codec in self.encodings:
try: return textstring.encode(codec)
except UnicodeDecodeError: continue
return textstring.encode(self.encodings[0], 'ignore')
def decode(self, textstring):
for codec in self.encodings:
try: return textstring.decode(codec)
except UnicodeDecodeError: continue
return textstring.decode(self.encodings[0], 'ignore')
2014-02-28 17:04:08 +01:00
def __init__(self, nicknames, server, port=6667, ident="pircbot",
realname="pircbot", encodings=("utf-8", "latin-1"),
2014-03-01 01:25:12 +01:00
command_prefix=".", admins="", serverpasswd="", parser_wait_time=0.1):
2014-02-27 12:05:38 +01:00
self.server = server
self.port = port
2014-03-01 01:25:12 +01:00
self.serverpasswd = serverpasswd
self.encodings = encodings
self.nicknames = nicknames
2014-02-27 12:05:38 +01:00
self.ident = ident
self.realname = realname
2014-03-01 01:25:12 +01:00
2014-02-28 17:04:08 +01:00
self.admins = admins
self.cmdprefix = command_prefix
2014-03-01 01:25:12 +01:00
self.parser_wait_time=parser_wait_time
self.user = {}
2014-02-28 17:04:08 +01:00
self.socket = None
self.recvloop = None
self.recvbuffer = bytearray(1024)
2014-03-01 01:25:12 +01:00
2014-02-28 17:04:08 +01:00
self.ready = False
2014-03-01 01:25:12 +01:00
self.die_event = Event()
2014-02-28 17:04:08 +01:00
2014-03-01 01:25:12 +01:00
def recv_loop(self):
2014-02-28 17:04:08 +01:00
"""
Loop for reciving data
"""
2014-03-01 01:25:12 +01:00
parser = Thread(target=self.parse_loop, name="parser")
2014-02-28 17:04:08 +01:00
p = poll()
p.register(self.socket.fileno(), POLLIN)
2014-03-01 01:25:12 +01:00
while not self.die_event.is_set():
2014-02-28 17:04:08 +01:00
ap = p.poll(1000)
if (self.socket.fileno(), POLLIN) in ap:
self.recvbuffer.extend(self.socket.recv(1024))
if not parser.is_alive():
2014-03-01 01:25:12 +01:00
parser = Thread(target=self.parse_loop, name="parser")
2014-02-28 17:04:08 +01:00
parser.start()
2014-03-01 01:25:12 +01:00
print("--- RECVLOOP EXITING ---")
2014-02-28 17:04:08 +01:00
2014-03-01 01:25:12 +01:00
def parse_loop(self):
2014-02-28 17:04:08 +01:00
"""
Loop for parsing incoming data
"""
2014-03-01 01:25:12 +01:00
while not self.die_event.is_set():# and self.recvbuffer.endswith(b"\r\n"):# != b"":
if self.recvbuffer.endswith(b"\r\n"):
2014-02-28 17:04:08 +01:00
# get and decode line from buffer
rawline, _, self.recvbuffer = self.recvbuffer.partition(b"\r\n")
rawline = self.decode(rawline)
# prepare line
line = rawline.split(" :", 1)
head = line[0].lstrip("\x00").split(" ")
larg = line[1] if len(line)>1 else ""
# parse prefix
2014-03-01 01:25:12 +01:00
origin = {}
2014-02-28 17:04:08 +01:00
if head[0].startswith(":"):
prefix = head.pop(0)[1:]
if "@" in prefix:
origin["nick"], origin["ident"], origin["host"] = re.match(r"(\S+)!(\S+)@(\S+)", prefix).groups()
else:
origin["server"] = prefix
2014-02-27 12:05:38 +01:00
else:
2014-02-28 17:04:08 +01:00
prefix = ""
# parse command
command = head.pop(0)
# parse params
params = head
params.append(larg)
2014-03-01 01:25:12 +01:00
log.show(log.DEBUG, " > %s" % rawline)
2014-02-28 17:04:08 +01:00
2014-03-01 01:25:12 +01:00
# PING
2014-02-28 17:04:08 +01:00
if command == "PING":
2014-03-01 01:25:12 +01:00
self.send("PONG %s" % params[0])
# PRIVMSG
2014-02-28 17:04:08 +01:00
elif command == "PRIVMSG":
if params[1].startswith(self.cmdprefix):
args = params[1].lstrip(self.cmdprefix).split(" ")
sndline = self.on_command(prefix, origin, params[0], args[0], args[1:])
if sndline != None:
2014-03-01 01:25:12 +01:00
self.send(sndline)
# 001 (RPL_WELCOME)
2014-02-28 17:04:08 +01:00
elif command == "001":
2014-03-01 01:25:12 +01:00
self.user["mask"] = re.search(r" (\S+!\S+@\S+)$", rawline.split(" :", 1)[1]).groups()[0]
self.user["nick"], self.user["ident"], self.user["host"] = re.match(r"(\S+)!(\S+)@(\S+)", self.user["mask"]).groups()
2014-02-28 17:04:08 +01:00
self.ready = True
2014-03-01 01:25:12 +01:00
# 433 (ERR_NICKNAMEINUSE)
2014-02-28 17:04:08 +01:00
elif command == "433":
2014-03-01 01:25:12 +01:00
self.send("NICK %s" % self.nicknames.pop(0))
# KILL
2014-02-28 17:04:08 +01:00
elif command == "KILL":
2014-03-01 01:25:12 +01:00
log.show(log.WARNING, "Got killed by %s: %s" % (params[0], params[1:]))
self.quit(send=False)
else:
2014-03-01 01:34:46 +01:00
sleep(self.parser_wait_time)
2014-03-01 01:25:12 +01:00
print("--- 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):
# 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))
2014-02-28 17:04:08 +01:00
def is_admin(self, user):
for admin in self.admins:
if re.search(admin, user) != None:
return True
return False
2014-03-01 01:25:12 +01:00
def quit(self, reason="", send=True):
if send:
try:
self.send("QUIT :%s" % reason)
except: pass
self.die_event.set()
print("--- SOCKET CLOSING ---")
try:
self.socket.shutdown(socket.SHUT_RDWR)
self.socket.close()
except: pass
2014-02-28 17:04:08 +01:00
2014-02-27 12:05:38 +01:00
def join(self, channel):
2014-03-01 01:34:46 +01:00
while not self.ready: sleep(1)
2014-03-01 01:25:12 +01:00
self.send("JOIN %s" % channel)
2014-02-28 17:04:08 +01:00
2014-02-27 12:05:38 +01:00
def part(self, channel):
2014-03-01 01:34:46 +01:00
while not self.ready: sleep(1)
2014-03-01 01:25:12 +01:00
self.send("PART %s" % channel)
def set_mode(self, modes):
2014-03-01 01:34:46 +01:00
while not self.ready: sleep(1)
2014-03-01 01:25:12 +01:00
self.send("MODE %s :%s" % (self.user["nick"], modes))
2014-02-28 17:04:08 +01:00
def on_command(self, prefix, origin, source, command, params):
"""
Executed when getting a PRIVMSG starting with self.cmdprefix
Prefix contains the optional prefix of the raw line.
Origin is a map holding the parsed prefix, containing
"nick", "ident" and "host" for users and "server" for servers.
Source is the first parameter after the irc-command, specifying
the channel or user, from where the line comes.
Command is the command for the bot.
Params contains a list of originally space separated parameters.
"""
2014-03-01 01:25:12 +01:00
# hello
if command == "hello":
2014-02-28 17:04:08 +01:00
greeting = "".join(("Hi " + origin["nick"] +"!"))
if source[0] in {"#", "+", "!", "&"}:
2014-03-01 01:25:12 +01:00
return "PRIVMSG %s :%s" % (source, greeting)
2014-02-28 17:04:08 +01:00
else:
2014-03-01 01:25:12 +01:00
return "PRIVMSG %s :%s" % (origin["nick"], greeting)
# join <channel>
elif command == "join" and len(params)>0:
2014-02-28 17:04:08 +01:00
if self.is_admin(origin["nick"]): self.join(params[0])
2014-03-01 01:25:12 +01:00
else: return "PRIVMSG %s :You can't do that!" % origin["nick"]
# part <channel>
elif command == "part" and len(params)>0:
2014-02-28 17:04:08 +01:00
if self.is_admin(origin["nick"]): self.part(params[0])
2014-03-01 01:25:12 +01:00
else: return "PRIVMSG %s :You can't do that!" % origin["nick"]
# die [<quitmsg>]
elif command == "die":
if self.is_admin(origin["nick"]):
self.quit("".join(params) if len(params)>0 else "".join((origin["nick"], " shot me, dying now... Bye...")))
2014-02-27 12:05:38 +01:00
def parseargs():
import argparse
p = argparse.ArgumentParser(
description = "i think my desc is missing")
p.add_argument("action", default="help", choices = ["start", "stop"], help="What to do?")
#p.add_argument("--daemon", "-d", type = bool, choices = [1, 0], default=1, help="Daemonize, Default: 1")
return p.parse_args()
def main():
2014-02-28 17:04:08 +01:00
global bot
2014-02-27 12:05:38 +01:00
args = parseargs()
if args.action == "start":
2014-02-28 17:04:08 +01:00
bot = pircbot(nicknames=NICKNAMES, ident=IDENT, realname=REALNAME,
2014-03-01 01:25:12 +01:00
server=SERVER, port=PORT, encodings=ENCODINGS, command_prefix=COMMAND_PREFIX,
admins=ADMINS, serverpasswd=SERVERPASSWD, parser_wait_time=PARSER_WAIT_TIME)
2014-02-28 17:04:08 +01:00
try:
bot.connect()
2014-03-01 01:25:12 +01:00
# wait for the bot to become ready
while bot.ready and not bot.die_event.is_set() == False:
2014-03-01 01:34:46 +01:00
sleep(1)
2014-03-01 01:25:12 +01:00
if not bot.die_event.is_set():
# set modes and join channels
bot.set_mode(MODES)
for channel in CHANNELS:
bot.join(channel)
# while bot is active, do nothing
while not bot.die_event.is_set():
2014-03-01 01:34:46 +01:00
sleep(1)
2014-03-01 01:25:12 +01:00
except KeyboardInterrupt:
log.show(log.INFO, "Got Ctrl-C, dying now...")
bot.quit()
2014-02-27 12:05:38 +01:00
elif args.action == "stop": print("nope!")
2014-03-01 01:25:12 +01:00
print("--- MAIN EXITING ---")
2014-02-27 12:05:38 +01:00
return 0
if __name__ == '__main__':
2014-02-28 17:04:08 +01:00
2014-02-27 12:05:38 +01:00
################
### SETTINGS ###
################
2014-02-28 17:04:08 +01:00
# 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.
# By default, the nickname will be used as ident.
IDENT = "chalkbot"
# The list of nicknames to try. If the first one is not aviable, it will
# try the second, and so on...
# You should specfy at least two nicks.
NICKNAMES = ["chalkbot", "chalkbot_", "chalkbot__"]
REALNAME = "A ChalkBot Instance"
# Command for registering with nickserv, without leading slash.
NICKSERVCMD = ""
MODES = "+B"
# The Server name to connect to. Duh!
#SERVER = "fanir.de"
SERVER = "localhost"
# "Default" is 6667. An often used port for SSL would be 6697, if SSL would be
# supported. Maybe in a future, far far away...
PORT = 6667
2014-03-01 01:25:12 +01:00
SERVERPASSWD = ""
2014-02-28 17:04:08 +01:00
# A comma-seperated list of channels to join, enclosed by braces.
CHANNELS = ["#bots", "#main"]
# 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 list given shoud do just fine in most networks, I assume.
# Also comma seperated and enclosed by braces.
ENCODINGS = ['utf-8', 'latin-1', 'iso-8859-1', 'cp1252']
# List of users (hostmasks, will be parsed as regex) who can do important stuff,
# like joining and parting channels and shutting down the bot.
ADMINS = ["Fanir.*"]
COMMAND_PREFIX = "."
2014-03-01 01:25:12 +01:00
# 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.
# You should keep it between 1 and 0.001 seconds.
# For gods sake, don't set it to 0!
PARSER_WAIT_TIME = 0.05
2014-02-28 17:04:08 +01:00
#######################
### END OF SETTINGS ###
#######################
2014-02-27 12:05:38 +01:00
main()