1
0
Fork 0
chalkbot/main.py

506 lines
20 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
import sys, string, socket, re, signal, json, logging
from random import choice
from time import sleep, time
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
from urllib.request import urlopen
from urllib.parse import quote_plus
from configobj import ConfigObj
2014-03-01 01:25:12 +01:00
2014-02-28 17:04:08 +01:00
bot = None
logging.basicConfig(format="[%(asctime)s] %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
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 __init__( self,
server,
nicknames,
ident = "pircbot",
realname = "pircbot",
port = 6667,
serverpasswd = "",
encodings = ("utf-8", "latin-1"),
query_type = "",
command_prefix = "!",
msg_wait_time = 0,
parser_wait_time = 0.1,
users = "",
duckduckgo_cfg = {"Active": "0"},
forecast_cfg = {"Active": "0"},
logger = logging.getLogger(logging.basicConfig())
):
self.nicknames = nicknames
self.ident = ident
self.realname = realname
2014-02-27 12:05:38 +01:00
self.server = server
self.port = int(port)
2014-03-01 01:25:12 +01:00
self.serverpasswd = serverpasswd
self.encodings = encodings
self.query_type = query_type.upper()
self.cmdprefix = command_prefix
self.msg_wait_time = float(msg_wait_time)
self.parser_wait_time = float(parser_wait_time)
2014-03-01 01:25:12 +01:00
self.users = users
2014-03-01 01:25:12 +01:00
self.duckduckgo_cfg = duckduckgo_cfg
self.forecast_cfg = forecast_cfg
self.log = logger
2014-03-01 01:25:12 +01:00
self.user = {}
2014-02-28 17:04:08 +01:00
self.socket = None
self.recvbuffer = bytearray(1024)
2014-03-01 01:25:12 +01:00
self.recvloop = None
self.parseloop = None
2014-03-01 01:25:12 +01:00
self.die_event = Event()
self.ready = False
self.mode_reply = {}
self.last_msg_ts = time()
2014-02-28 17:04:08 +01:00
def connect(self):
# connect
self.socket = socket.socket()
self.log.debug("--- SOCKET OPENING ---")
try:
self.socket.connect((self.server, self.port))
except socket.error as e:
self.log.critical("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)
self.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):
2014-02-28 17:04:08 +01:00
"""
Loop for reciving data
"""
self.log.debug("--- RECVLOOP STARTING ---")
self.parseloop = Thread(target=self.parser, 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 self.parseloop.is_alive():
self.parseloop = Thread(target=self.parser, name="parser")
self.parseloop.start()
self.log.debug("--- RECVLOOP EXITING ---")
2014-02-28 17:04:08 +01:00
# loop for parsing incoming data
def parser(self):
2014-02-28 17:04:08 +01:00
"""
Loop for parsing incoming data
"""
self.log.debug("--- PARSELOOP STARTING ---")
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()
origin["mask"] = prefix
2014-02-28 17:04:08 +01:00
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.strip())
2014-02-28 17:04:08 +01:00
self.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 and NOTICE
elif command == "PRIVMSG" or command == "NOTICE":
if params[1][0] == self.cmdprefix:
args = []
for v in params[1][1:].split(" "):
if v!="": args.append(v)
rp = self.on_command(command, prefix, origin, params[0], args[0].lower(), args[1:])
if rp not in (None, ""):
self.reply(origin, params[0], rp, command)
# 221 (RPL_UMODEIS)
elif command == "221" and self.mode_reply is not {}:
self.query(self.mode_reply["to"], "Modes are %s" % params[1], self.mode_reply["type"])
self.mode_reply = {}
# INVITE
elif command == "INVITE":
if self.check_privileges(origin["mask"], command):
self.join(params[1])
else:
self.query(origin["nick"], "You can not force me to do that!", "NOTICE")
2014-03-01 01:25:12 +01:00
# 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":
self.log.warning("Got killed by %s: %s" % (params[0], params[1:]))
self.disconnect(send_quit=False)
2014-03-01 01:25:12 +01:00
else:
2014-03-01 01:34:46 +01:00
sleep(self.parser_wait_time)
self.log.debug("--- PARSELOOP EXITING ---")
2014-03-01 01:25:12 +01:00
### helper functions ###
2014-02-28 17:04:08 +01:00
# 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 may execute a given command
def check_privileges(self, usermask, command):
for user, privs in self.users.items():
if re.search(user, usermask, re.IGNORECASE) != None:
if command.lower() in privs or "*" in privs:
return True
2014-02-28 17:04:08 +01:00
return False
# checks if s is a valid channel name
def is_channel(self, s):
return (True if s[0] in ('&', '#', '!') else False)
def exec_on_ready(self, command, retry_times=-1, interval=1):
if command.startswith("self."):
if retry_times == -1:
while not self.ready: sleep(interval)
else:
cnt = 0
while not self.ready and cnt<retry_times: sleep(interval)
if not self.ready:
self.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:
self.log.critical("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: self.log.critical("exec_retry() called with an invalid command.")
def send(self, data):
self.log.debug("< %s" % data)
try: self.socket.send(self.encode("".join((data, "\r\n"))))
except Exception as e:
self.log.critical(e)
self.disconnect(send_quit=False)
# decides whether to reply to user or to channel
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 ###
# replies to channel by PRIVMSG
def chanmsg(self, channel, msg):
while self.last_msg_ts + self.msg_wait_time > time():
sleep(0.1)
self.last_msg_ts = time()
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)))
def nick(self, nick):
self.exec_on_ready("".join(('self.send("JOIN %s" % "', channel, '")')))
2014-02-28 17:04:08 +01:00
2014-02-27 12:05:38 +01:00
def join(self, channel):
self.exec_on_ready("".join(('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):
self.exec_on_ready("".join(('self.send("PART %s" % "', channel, '")')))
2014-03-01 01:25:12 +01:00
def set_mode(self, modes, target=""):
self.exec_on_ready("".join(('self.send("MODE %s :%s" % (', ''.join(('"', target, '"')) if target != "" else 'self.user["nick"]', ', "', modes, '"))')))
def get_modes(self, target=""):
self.exec_on_ready("".join(('self.send("MODE %s" % ', ''.join(('"', target, '"')) if target != "" else 'self.user["nick"]', ')')))
2014-02-28 17:04:08 +01:00
### handler ###
# bot-command handler
def on_command(self, in_query_type, prefix, origin, source, command, params):
2014-02-28 17:04:08 +01:00
"""
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.
"""
numparams = len(params)
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"] +"!"))
return greeting
# say
if command == "say":
return " ".join(params)
# DuckDuckGo, ddg <query>
elif command in ("duckduckgo", "ddg") and self.duckduckgo_cfg["Active"] == "1":
if numparams==0:
return "You didn't ask anything..."
try: rp = urlopen("https://api.duckduckgo.com/?q=%s&format=json&no_html=1&no_redirect=1&t=pircbot:chalkbot"
% quote_plus(" ".join(params)))
except Exception as e:
self.log.error("Error while querying DuckDuckGo: %s" % e)
return "Error while querying DuckDuckGo: %s" % e
if rp.getcode() == 200:
used_fields = (
"Heading", "AbstractText", "AbstractSource", "AbstractURL",
"AnswerType", "Answer",
"Definition", "DefinitionSource", "DefinitionURL",
)
rj = json.loads(str(rp.readall(), "utf-8"))
empty_field_counter = 0
for elem in [rj for rj in used_fields]:
if rj[elem] not in ("", []):
self.reply(origin, source, "%s: %s" % (elem, rj[elem]), in_query_type)
else:
empty_field_counter+=1
if empty_field_counter == len(used_fields):
return "No suitable reply from DuckDuckGo for query %s" % " ".join(params)
else:
return "(Results from DuckDuckGo <https://duckduckgo.com>)"
else:
return "Error while querying DuckDuckGo, got HTTP-Status %i" % rp.getcode()
# Forecast, fc, weather
elif command in ("weather", "forecast", "fc") and self.forecast_cfg["Active"] == "1":
if numparams==2:
return "(Powered by Forecast <http://forecast.io/>)"
else:
return "Usage: %s <lat> <lon>" % command
2014-03-01 01:25:12 +01:00
# join <channel>
elif command == "join":
if numparams>0:
if self.check_privileges(origin["mask"], command): self.join(params[0])
else: self.query(origin["nick"], "You cannot do that!", in_query_type)
2014-03-01 01:25:12 +01:00
# part <channel>
elif command == "part":
if numparams>0:
if self.check_privileges(origin["mask"], command): self.part(params[0])
else: self.query(origin["nick"], "You cannot do that!", in_query_type)
# mode ±<modes>
elif command == "mode":
if self.check_privileges(origin["mask"], command):
if numparams==0:
self.mode_reply["to"] = origin["nick"]
self.mode_reply["type"] = in_query_type
self.get_modes()
else:
self.set_mode(" ".join(params) if numparams>0 else params)
else: self.query(origin["nick"], "You cannot do that!", in_query_type)
2014-03-01 01:25:12 +01:00
# die [<quitmsg>]
elif command == "die":
if self.check_privileges(origin["mask"], command):
self.disconnect("".join(params) if numparams>0 else "".join((origin["nick"], " shot me, dying now... Bye...")))
else: self.query(origin["nick"], "Go die yourself!", in_query_type)
else:
replies = [
("What? \"%s\" is not a command!", 15),
("%s? What's that?", 3),
("Sorry, I don't know how to %s...", 1)
]
self.query(origin["nick"], choice([val for val, cnt in replies for i in range(cnt)]) % (command), in_query_type)
2014-03-01 01:25:12 +01:00
2014-02-27 12:05:38 +01:00
def parseargs():
import argparse
p = argparse.ArgumentParser(
description = "guess what? i think my desc is still missing!"
)
p.add_argument("action",
default = "help",
choices = ["start", "stop", "checkconf"],
help = "What to do?"
)
p.add_argument("--loglevel", "-l",
choices = ["critical", "error", "warning", "info", "debug"],
help = "Verbosity of logging"
)
2014-02-27 12:05:38 +01:00
#p.add_argument("--daemon", "-d", type = bool, choices = [1, 0], default=1, help="Daemonize, Default: 1")
return p.parse_args()
def main():
log = logging.getLogger(__name__)
2014-02-28 17:04:08 +01:00
global bot
2014-02-27 12:05:38 +01:00
args = parseargs()
cfg = ConfigObj("bot.conf")
nll = getattr(logging, cfg["Behavior"]["Loglevel"].upper(), None)
if not isinstance(nll, int):
raise ValueError('Invalid log level: %s' % cfg["Behavior"]["Loglevel"])
if args.loglevel != None: nll = getattr(logging, args.loglevel.upper(), None)
if not isinstance(nll, int):
raise ValueError('Invalid log level: %s' % args.loglevel)
log.setLevel(nll)
2014-02-27 12:05:38 +01:00
if args.action == "start":
2014-02-28 17:04:08 +01:00
try:
bot = pircbot(
nicknames = cfg["Bot"]["Nicknames"],
ident = cfg["Bot"]["Ident"],
realname = cfg["Bot"]["Realname"],
server = cfg["Network"]["Server"],
port = cfg["Network"]["Port"],
serverpasswd = cfg["Network"]["ServerPasswd"],
encodings = cfg["Network"]["Encodings"],
query_type = cfg["Behavior"]["QueryType"],
command_prefix = cfg["Behavior"]["CommandPrefix"],
msg_wait_time = cfg["Behavior"]["MsgWaitTime"],
parser_wait_time = cfg["Behavior"]["ParserWaitTime"],
users = cfg["Permissions"],
duckduckgo_cfg = cfg["DuckDuckGo"],
forecast_cfg = cfg["Forecast.io"],
logger = log,
)
2014-02-28 17:04:08 +01:00
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(cfg["Bot"]["Modes"])
for channel in cfg["Network"]["Channels"]:
2014-03-01 01:25:12 +01:00
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.info("Got Ctrl-C, dying now...")
bot.disconnect("Ouch! Got shot by Ctrl-C, dying now... See you!")
log.debug("--- MAIN EXITING ---")
2014-02-27 12:05:38 +01:00
elif args.action == "stop": print("nope!")
elif args.action == "checkconf":
for section, settings in cfg.items():
print("".join(("[", section, "]")))
for e in settings:
print("%20s : %s" % (e, settings[e]))
2014-02-27 12:05:38 +01:00
return 0
if __name__ == '__main__':
main()