1
0
Fork 0
chalkbot/main.py
2014-03-01 01:25:12 +01:00

360 lines
13 KiB
Python
Executable file

#!/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.
#
#
# SETTINGS CAN BE FOUND AT THE BOTTOM OF THE FILE
import sys, time, string, socket, re, signal, struct
from select import poll, POLLIN, POLLPRI, POLLOUT,\
POLLERR, POLLHUP, POLLNVAL
from threading import Thread, Event
from APIcollection import Twitter
bot = None
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!")
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')
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.port = port
self.serverpasswd = serverpasswd
self.encodings = encodings
self.nicknames = nicknames
self.ident = ident
self.realname = realname
self.admins = admins
self.cmdprefix = command_prefix
self.parser_wait_time=parser_wait_time
self.user = {}
self.socket = None
self.recvloop = None
self.recvbuffer = bytearray(1024)
self.ready = False
self.die_event = Event()
def recv_loop(self):
"""
Loop for reciving data
"""
parser = Thread(target=self.parse_loop, name="parser")
p = poll()
p.register(self.socket.fileno(), POLLIN)
while not self.die_event.is_set():
ap = p.poll(1000)
if (self.socket.fileno(), POLLIN) in ap:
self.recvbuffer.extend(self.socket.recv(1024))
if not parser.is_alive():
parser = Thread(target=self.parse_loop, name="parser")
parser.start()
print("--- RECVLOOP EXITING ---")
def parse_loop(self):
"""
Loop for parsing incoming data
"""
while not self.die_event.is_set():# and self.recvbuffer.endswith(b"\r\n"):# != b"":
if self.recvbuffer.endswith(b"\r\n"):
# 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
origin = {}
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
else:
prefix = ""
# parse command
command = head.pop(0)
# parse params
params = head
params.append(larg)
log.show(log.DEBUG, " > %s" % rawline)
# PING
if command == "PING":
self.send("PONG %s" % params[0])
# PRIVMSG
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:
self.send(sndline)
# 001 (RPL_WELCOME)
elif command == "001":
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()
self.ready = True
# 433 (ERR_NICKNAMEINUSE)
elif command == "433":
self.send("NICK %s" % self.nicknames.pop(0))
# KILL
elif command == "KILL":
log.show(log.WARNING, "Got killed by %s: %s" % (params[0], params[1:]))
self.quit(send=False)
else:
time.sleep(self.parser_wait_time)
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))
def is_admin(self, user):
for admin in self.admins:
if re.search(admin, user) != None:
return True
return False
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
def join(self, channel):
while not self.ready: time.sleep(1)
self.send("JOIN %s" % channel)
def part(self, channel):
while not self.ready: time.sleep(1)
self.send("PART %s" % channel)
def set_mode(self, modes):
while not self.ready: time.sleep(1)
self.send("MODE %s :%s" % (self.user["nick"], modes))
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.
"""
# hello
if command == "hello":
greeting = "".join(("Hi " + origin["nick"] +"!"))
if source[0] in {"#", "+", "!", "&"}:
return "PRIVMSG %s :%s" % (source, greeting)
else:
return "PRIVMSG %s :%s" % (origin["nick"], greeting)
# join <channel>
elif command == "join" and len(params)>0:
if self.is_admin(origin["nick"]): self.join(params[0])
else: return "PRIVMSG %s :You can't do that!" % origin["nick"]
# part <channel>
elif command == "part" and len(params)>0:
if self.is_admin(origin["nick"]): self.part(params[0])
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...")))
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():
global bot
args = parseargs()
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:
bot.connect()
# wait for the bot to become ready
while bot.ready and not bot.die_event.is_set() == False:
time.sleep(1)
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():
time.sleep(1)
except KeyboardInterrupt:
log.show(log.INFO, "Got Ctrl-C, dying now...")
bot.quit()
elif args.action == "stop": print("nope!")
print("--- MAIN EXITING ---")
return 0
if __name__ == '__main__':
################
### 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.
# 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
SERVERPASSWD = ""
# 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 = "."
# 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
#######################
### END OF SETTINGS ###
#######################
main()