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-05 15:44:04 +01:00
|
|
|
import sys, string, socket, re, signal, json, logging
|
2014-03-03 18:06:42 +01:00
|
|
|
from random import choice
|
2014-03-07 15:21:38 +01:00
|
|
|
from time import sleep, time, strftime, localtime
|
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-03-05 15:44:04 +01:00
|
|
|
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
|
|
|
|
|
2014-03-05 15:44:04 +01:00
|
|
|
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():
|
2014-03-05 15:44:04 +01:00
|
|
|
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"},
|
2014-03-19 10:20:23 +01:00
|
|
|
mapquest_cfg = {"Active": "0"},
|
2014-03-05 15:44:04 +01:00
|
|
|
logger = logging.getLogger(logging.basicConfig())
|
|
|
|
):
|
2014-03-07 15:21:38 +01:00
|
|
|
self.log = logger
|
|
|
|
|
|
|
|
|
2014-03-05 15:44:04 +01:00
|
|
|
self.nicknames = nicknames
|
|
|
|
self.ident = ident
|
|
|
|
self.realname = realname
|
|
|
|
|
2014-02-27 12:05:38 +01:00
|
|
|
self.server = server
|
2014-03-05 15:44:04 +01:00
|
|
|
self.port = int(port)
|
2014-03-01 01:25:12 +01:00
|
|
|
self.serverpasswd = serverpasswd
|
|
|
|
self.encodings = encodings
|
|
|
|
|
2014-03-05 15:44:04 +01:00
|
|
|
self.query_type = query_type.upper()
|
|
|
|
self.cmdprefix = command_prefix
|
|
|
|
self.msg_wait_time = float(msg_wait_time)
|
2014-03-07 15:21:38 +01:00
|
|
|
if self.msg_wait_time < 0.1:
|
|
|
|
self.log.info("msg_wait_time ist zu klein, nutze 0.1 Sekunden")
|
|
|
|
self.msg_wait_time = 0.1
|
2014-03-05 15:44:04 +01:00
|
|
|
self.parser_wait_time = float(parser_wait_time)
|
2014-03-01 01:25:12 +01:00
|
|
|
|
2014-03-04 09:44:55 +01:00
|
|
|
self.users = users
|
2014-03-01 01:25:12 +01:00
|
|
|
|
2014-03-05 15:44:04 +01:00
|
|
|
self.duckduckgo_cfg = duckduckgo_cfg
|
|
|
|
self.forecast_cfg = forecast_cfg
|
2014-03-19 10:20:23 +01:00
|
|
|
self.mapquest_cfg = mapquest_cfg
|
2014-03-05 15:44:04 +01:00
|
|
|
|
2014-03-01 01:25:12 +01:00
|
|
|
|
2014-03-07 15:21:38 +01:00
|
|
|
self.user = {"mask":"", "nick":"", "ident":"", "host":""}
|
2014-03-01 01:25:12 +01:00
|
|
|
|
2014-02-28 17:04:08 +01:00
|
|
|
self.socket = None
|
|
|
|
self.recvbuffer = bytearray(1024)
|
2014-03-01 01:25:12 +01:00
|
|
|
|
2014-03-03 18:06:42 +01:00
|
|
|
self.recvloop = None
|
|
|
|
self.parseloop = None
|
2014-03-01 01:25:12 +01:00
|
|
|
|
|
|
|
self.die_event = Event()
|
2014-03-03 18:06:42 +01:00
|
|
|
self.ready = False
|
|
|
|
|
2014-03-04 09:44:55 +01:00
|
|
|
self.mode_reply = {}
|
2014-03-05 15:44:04 +01:00
|
|
|
|
|
|
|
self.last_msg_ts = time()
|
2014-02-28 17:04:08 +01:00
|
|
|
|
2014-03-03 18:06:42 +01:00
|
|
|
def connect(self):
|
|
|
|
# connect
|
|
|
|
self.socket = socket.socket()
|
2014-03-05 15:44:04 +01:00
|
|
|
self.log.debug("--- SOCKET OPENING ---")
|
2014-03-03 18:06:42 +01:00
|
|
|
try:
|
|
|
|
self.socket.connect((self.server, self.port))
|
2014-03-06 16:44:14 +01:00
|
|
|
except Exception as e:
|
|
|
|
self.log.critical("Fehler beim Verbinden: %s", e)
|
|
|
|
self.disconnect(send_quit=False)
|
2014-03-03 18:06:42 +01:00
|
|
|
|
|
|
|
# start getting data
|
2014-03-06 16:44:14 +01:00
|
|
|
try:
|
|
|
|
self.recvloop = Thread(target=self.recv, name="recvloop")
|
|
|
|
self.recvloop.start()
|
|
|
|
except Exception as e:
|
|
|
|
self.log.critical("Fehler beim Starten des Empfangs von Daten vom IRC: %s", e)
|
|
|
|
self.disconnect(send_quit=False)
|
2014-03-03 18:06:42 +01:00
|
|
|
|
2014-03-06 16:44:14 +01:00
|
|
|
try:
|
|
|
|
# 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))
|
|
|
|
except Exception as e:
|
|
|
|
self.log.critical("Fehler beim IRC-Handshake: %s", e)
|
|
|
|
self.disconnect(send_quit=False)
|
2014-03-03 18:06:42 +01:00
|
|
|
|
|
|
|
# implements irc command QUIT and (more or less) clean exiting
|
|
|
|
def disconnect(self, reason="", send_quit=True):
|
|
|
|
if send_quit:
|
2014-03-06 16:44:14 +01:00
|
|
|
try: self.send("QUIT :%s" % reason)
|
|
|
|
except Exception as e: self.log.critical("Fehler beim Senden der QUIT-Nachricht: %s", e)
|
|
|
|
sleep(1)
|
2014-03-03 18:06:42 +01:00
|
|
|
self.die_event.set()
|
|
|
|
ctr = 0
|
|
|
|
while self.recvloop.is_alive() and self.parseloop.is_alive() and ctr < 15:
|
|
|
|
ctr += 1
|
|
|
|
sleep(1)
|
2014-03-05 15:44:04 +01:00
|
|
|
self.log.debug("--- SOCKET CLOSING ---")
|
2014-03-03 18:06:42 +01:00
|
|
|
try:
|
|
|
|
self.socket.shutdown(socket.SHUT_RDWR)
|
|
|
|
self.socket.close()
|
2014-03-06 16:44:14 +01:00
|
|
|
except Exception as e: self.log.warning("Fehler beim Schließen des Sockets: %s", e)
|
2014-03-03 18:06:42 +01:00
|
|
|
|
|
|
|
|
|
|
|
### threaded functions ###
|
|
|
|
|
|
|
|
# loop for recieving data from irc
|
|
|
|
def recv(self):
|
2014-02-28 17:04:08 +01:00
|
|
|
"""
|
|
|
|
Loop for reciving data
|
|
|
|
"""
|
2014-03-05 15:44:04 +01:00
|
|
|
self.log.debug("--- RECVLOOP STARTING ---")
|
2014-03-06 16:44:14 +01:00
|
|
|
try: self.parseloop = Thread(target=self.parser, name="parser")
|
|
|
|
except Exception as e:
|
|
|
|
self.log.error("Fehler beim Vorbereiten des Threads zum Parsen von Nachrichten von IRC: %s", e)
|
2014-02-28 17:04:08 +01:00
|
|
|
p = poll()
|
2014-03-06 16:44:14 +01:00
|
|
|
try: p.register(self.socket.fileno(), POLLIN)
|
|
|
|
except Exception as e:
|
|
|
|
self.log.critical("Fehler, der eigentlich nie auftreten sollte: %s", e)
|
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:
|
2014-03-06 16:44:14 +01:00
|
|
|
try: self.recvbuffer.extend(self.socket.recv(1024))
|
|
|
|
except Exception as e:
|
|
|
|
self.log.critical("Konnte keine Daten vom IRC lesen. Verbindung tot? Fehler: %s", e)
|
2014-03-03 18:06:42 +01:00
|
|
|
if not self.parseloop.is_alive():
|
2014-03-06 16:44:14 +01:00
|
|
|
try:
|
|
|
|
self.parseloop = Thread(target=self.parser, name="parser")
|
|
|
|
self.parseloop.start()
|
|
|
|
except Exception as e:
|
|
|
|
self.log.critical("Fehler beim Starten des Threads zum Parsen von Nachrichten vom IRC: %s", e)
|
2014-03-05 15:44:04 +01:00
|
|
|
self.log.debug("--- RECVLOOP EXITING ---")
|
2014-02-28 17:04:08 +01:00
|
|
|
|
2014-03-03 18:06:42 +01:00
|
|
|
# loop for parsing incoming data
|
|
|
|
def parser(self):
|
2014-02-28 17:04:08 +01:00
|
|
|
"""
|
|
|
|
Loop for parsing incoming data
|
|
|
|
"""
|
2014-03-05 15:44:04 +01:00
|
|
|
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()
|
2014-03-03 18:06:42 +01:00
|
|
|
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
|
2014-03-03 18:06:42 +01:00
|
|
|
params.append(larg.strip())
|
2014-02-28 17:04:08 +01:00
|
|
|
|
2014-03-05 15:44:04 +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])
|
2014-03-03 18:06:42 +01:00
|
|
|
# PRIVMSG and NOTICE
|
2014-03-07 15:21:38 +01:00
|
|
|
elif command == "PRIVMSG" or command == "NOTICE" and self.ready:
|
|
|
|
args = []
|
|
|
|
for v in params[1].split(" "):
|
|
|
|
if v!="": args.append(v)
|
|
|
|
if len(args)>1 and args[0][0:len(self.user["nick"])] == self.user["nick"]:
|
|
|
|
args.pop(0)
|
|
|
|
args[0] = "".join((self.cmdprefix, args[0]))
|
|
|
|
if args[0][0] == self.cmdprefix:
|
|
|
|
args[0] = args[0][1:]
|
2014-03-04 09:44:55 +01:00
|
|
|
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)
|
2014-03-03 18:06:42 +01:00
|
|
|
# 221 (RPL_UMODEIS)
|
2014-03-04 09:44:55 +01:00
|
|
|
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 = {}
|
2014-03-03 18:06:42 +01:00
|
|
|
# INVITE
|
|
|
|
elif command == "INVITE":
|
2014-03-04 09:44:55 +01:00
|
|
|
if self.check_privileges(origin["mask"], command):
|
2014-03-03 18:06:42 +01:00
|
|
|
self.join(params[1])
|
|
|
|
else:
|
2014-03-04 09:44:55 +01:00
|
|
|
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":
|
2014-03-05 15:44:04 +01:00
|
|
|
self.log.warning("Got killed by %s: %s" % (params[0], params[1:]))
|
2014-03-03 18:06:42 +01:00
|
|
|
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)
|
2014-03-05 15:44:04 +01:00
|
|
|
self.log.debug("--- PARSELOOP EXITING ---")
|
2014-03-01 01:25:12 +01:00
|
|
|
|
|
|
|
|
2014-03-03 18:06:42 +01:00
|
|
|
### helper functions ###
|
2014-02-28 17:04:08 +01:00
|
|
|
|
2014-03-03 18:06:42 +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')
|
|
|
|
|
|
|
|
|
2014-03-21 09:57:47 +01:00
|
|
|
# splits messages into parts with a specific maximal length
|
|
|
|
def msgsplit(self, msg, length=400):
|
|
|
|
return [msg[i:i+length] for i in range(0, len(msg), length)]
|
|
|
|
|
|
|
|
|
2014-03-04 09:44:55 +01:00
|
|
|
# checks if a given user may execute a given command
|
2014-03-19 10:20:23 +01:00
|
|
|
def check_privileges(self, usermask, command, log=True):
|
2014-03-04 09:44:55 +01:00
|
|
|
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-03-19 10:20:23 +01:00
|
|
|
if log: self.log.info("Unauthorized call of %s from user %s", command, usermask)
|
2014-02-28 17:04:08 +01:00
|
|
|
return False
|
|
|
|
|
2014-03-04 09:44:55 +01:00
|
|
|
|
2014-03-03 18:06:42 +01:00
|
|
|
# checks if s is a valid channel name
|
|
|
|
def is_channel(self, s):
|
2014-03-04 09:44:55 +01:00
|
|
|
return (True if s[0] in ('&', '#', '!') else False)
|
2014-03-03 18:06:42 +01:00
|
|
|
|
|
|
|
|
|
|
|
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:
|
2014-03-05 15:44:04 +01:00
|
|
|
self.log.warning("Connection did not get ready in time (%sx %s seconds), \"%s\" not executed" % (retry_times, interval, command))
|
2014-03-03 18:06:42 +01:00
|
|
|
return True
|
|
|
|
exec("ret = %s" % command)
|
|
|
|
return False
|
|
|
|
else:
|
2014-03-05 15:44:04 +01:00
|
|
|
self.log.critical("exec_on_ready() called with an invalid command: %s" % command)
|
2014-03-03 18:06:42 +01:00
|
|
|
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)
|
2014-03-05 15:44:04 +01:00
|
|
|
else: self.log.critical("exec_retry() called with an invalid command.")
|
2014-03-03 18:06:42 +01:00
|
|
|
|
|
|
|
|
|
|
|
def send(self, data):
|
2014-03-05 15:44:04 +01:00
|
|
|
self.log.debug("< %s" % data)
|
2014-03-03 18:06:42 +01:00
|
|
|
try: self.socket.send(self.encode("".join((data, "\r\n"))))
|
2014-03-05 15:44:04 +01:00
|
|
|
except Exception as e:
|
2014-03-06 16:44:14 +01:00
|
|
|
self.log.critical("Fehler beim Senden von Daten: %s", e)
|
2014-03-03 18:06:42 +01:00
|
|
|
self.disconnect(send_quit=False)
|
|
|
|
|
2014-03-04 09:44:55 +01:00
|
|
|
# decides whether to reply to user or to channel
|
2014-03-03 18:06:42 +01:00
|
|
|
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 ###
|
|
|
|
|
2014-03-04 09:44:55 +01:00
|
|
|
# replies to channel by PRIVMSG
|
|
|
|
def chanmsg(self, channel, msg):
|
2014-03-05 15:44:04 +01:00
|
|
|
while self.last_msg_ts + self.msg_wait_time > time():
|
|
|
|
sleep(0.1)
|
|
|
|
self.last_msg_ts = time()
|
2014-03-04 09:44:55 +01:00
|
|
|
self.send("".join(("PRIVMSG ", channel, " :", msg)))
|
|
|
|
|
|
|
|
# replies to user by NOTICE or PRIVMSG
|
2014-03-07 15:21:38 +01:00
|
|
|
def query(self, nick, msg, in_query_type="", force_query_type=""):
|
|
|
|
self.send("".join((force_query_type if force_query_type!="" else
|
|
|
|
(self.query_type if self.query_type!="" else
|
|
|
|
(in_query_type if in_query_type!="" else
|
|
|
|
"PRIVMSG")), " ", nick, " :", msg)))
|
2014-03-04 09:44:55 +01:00
|
|
|
|
2014-03-03 18:06:42 +01:00
|
|
|
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):
|
2014-03-03 18:06:42 +01:00
|
|
|
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):
|
2014-03-03 18:06:42 +01:00
|
|
|
self.exec_on_ready("".join(('self.send("PART %s" % "', channel, '")')))
|
2014-03-01 01:25:12 +01:00
|
|
|
|
2014-03-04 09:44:55 +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, '"))')))
|
2014-03-03 18:06:42 +01:00
|
|
|
|
2014-03-04 09:44:55 +01:00
|
|
|
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
|
|
|
|
2014-03-03 18:06:42 +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.
|
|
|
|
"""
|
2014-03-19 10:20:23 +01:00
|
|
|
self.log.info("Command from %s: %s", origin["mask"], command)
|
|
|
|
|
2014-03-05 15:44:04 +01:00
|
|
|
numparams = len(params)
|
2014-03-07 15:21:38 +01:00
|
|
|
### INTERNAL ###
|
|
|
|
# help [me]
|
|
|
|
if command == "help":
|
|
|
|
if numparams == 1 and params[0].startswith("me"):
|
|
|
|
rply = "\
|
|
|
|
Nope, you're on your own."
|
|
|
|
else:
|
|
|
|
rply = "\
|
|
|
|
Command Prefix is \"%s\" or \"%s\"\n\
|
|
|
|
\n\
|
|
|
|
Commands:\n\
|
|
|
|
hello [<...>]\n\
|
|
|
|
say <text>\n\
|
|
|
|
choose <choice1>, <choice2>[, <choice3>[, ...]] -- Let the bot decide!\n\
|
|
|
|
DuckDuckGo, ddg <query> -- Ask the DuckDuckGo Instant Answer API\n\
|
|
|
|
Forecast, fc <query> -- Query Forecast.io\n\
|
2014-03-21 09:57:47 +01:00
|
|
|
MapQuest, mq, OpenStreetMap, osm <query> -- Get lat and lon for a place\n\
|
|
|
|
Fefe [<id>] -- Show a given or the lastest post in Fefes Blog\n\
|
2014-03-07 15:21:38 +01:00
|
|
|
" % (
|
|
|
|
self.cmdprefix,
|
|
|
|
self.user["nick"]
|
|
|
|
)
|
2014-03-19 10:20:23 +01:00
|
|
|
if self.check_privileges(origin["mask"], command, log=False):
|
2014-03-07 15:21:38 +01:00
|
|
|
rply += " \n\
|
|
|
|
~ For the aristocrats ~\n\
|
|
|
|
join <channel>\n\
|
|
|
|
part <channel>\n\
|
|
|
|
mode ±<modes>\n\
|
|
|
|
die [<quitmsg>]"
|
|
|
|
for line in rply.splitlines(): self.query(origin["nick"], line[20:], force_query_type="PRIVMSG")
|
|
|
|
|
|
|
|
# hello [<...>]
|
|
|
|
elif command == "hello":
|
2014-02-28 17:04:08 +01:00
|
|
|
greeting = "".join(("Hi " + origin["nick"] +"!"))
|
2014-03-04 09:44:55 +01:00
|
|
|
return greeting
|
2014-03-07 15:21:38 +01:00
|
|
|
# say <text>
|
2014-03-06 16:44:14 +01:00
|
|
|
elif command == "say":
|
2014-03-04 09:44:55 +01:00
|
|
|
return " ".join(params)
|
2014-03-07 15:21:38 +01:00
|
|
|
# choose <choice1>, <choice2>[, <choice3>[, ...]]
|
2014-03-06 16:44:14 +01:00
|
|
|
elif command == "choose":
|
|
|
|
choices = " ".join(params).split(", ")# if numparams>1 else params.split(", ")
|
|
|
|
if choices[0] == "":
|
|
|
|
return "Whaddayawant? (Seperated by \", \")"
|
|
|
|
elif len(choices) == 1:
|
|
|
|
return "Such a difficult question... I don't know..."
|
|
|
|
else:
|
|
|
|
return choice(choices)
|
2014-03-05 15:44:04 +01:00
|
|
|
|
2014-03-07 15:21:38 +01:00
|
|
|
### EXTERNAL ###
|
2014-03-05 15:44:04 +01:00
|
|
|
# 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
|
2014-03-07 15:21:38 +01:00
|
|
|
for elem in [v for v in used_fields if v in rj]:
|
2014-03-05 15:44:04 +01:00
|
|
|
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()
|
2014-03-07 15:21:38 +01:00
|
|
|
# Forecast, fc <query>
|
|
|
|
elif command in ("forecast", "fc") and self.forecast_cfg["Active"] == "1":
|
2014-03-05 15:44:04 +01:00
|
|
|
if numparams==2:
|
2014-03-07 15:21:38 +01:00
|
|
|
try: rp = urlopen("https://api.forecast.io/forecast/%s/%s?units=si&exclude=minutely,hourly,daily"
|
|
|
|
% (self.forecast_cfg["ApiKey"], quote_plus(",".join(params))))
|
|
|
|
except Exception as e:
|
|
|
|
self.log.error("Error while querying Forecast.io: %s" % e)
|
|
|
|
return "Error while querying Forecast.io: %s" % e
|
|
|
|
if rp.getcode() == 200:
|
|
|
|
rj = json.loads(str(rp.readall(), "utf-8"))
|
|
|
|
self.reply(origin, source,
|
|
|
|
strftime("CURRENTLY (%d.%m.%Y %H:%M:%S)", localtime(rj["currently"]["time"])), in_query_type)
|
|
|
|
self.reply(origin, source,
|
|
|
|
"Summary: %s" % rj["currently"]["summary"], in_query_type)
|
|
|
|
self.reply(origin, source,
|
|
|
|
"Temperature: %s °C" % rj["currently"]["temperature"], in_query_type)
|
|
|
|
self.reply(origin, source,
|
|
|
|
"Apparent Temperature: %s °C" % rj["currently"]["apparentTemperature"], in_query_type)
|
|
|
|
self.reply(origin, source,
|
|
|
|
"Dew Point: %s °C" % rj["currently"]["dewPoint"], in_query_type)
|
|
|
|
self.reply(origin, source,
|
|
|
|
"Wind Speed: %s m/s" % rj["currently"]["windSpeed"], in_query_type)
|
|
|
|
self.reply(origin, source,
|
2014-03-19 10:20:23 +01:00
|
|
|
"Cloud Cover: %s %%" % (rj["currently"]["cloudCover"]*100), in_query_type)
|
2014-03-07 15:21:38 +01:00
|
|
|
self.reply(origin, source,
|
|
|
|
"Precipitation Probability: %s %%" % (rj["currently"]["precipProbability"]*100), in_query_type)
|
|
|
|
if "precipIntensity" in rj["currently"]: self.reply(origin, source,
|
|
|
|
"Precipitation Intensity: %s mm/h" % rj["currently"]["precipIntensity"], in_query_type)
|
|
|
|
if "precipType" in rj["currently"]: self.reply(origin, source,
|
|
|
|
"Precipitation Type: %s" % rj["currently"]["precipType"], in_query_type)
|
2014-03-21 09:57:47 +01:00
|
|
|
if "visibility" in rj["currently"]: self.reply(origin, source,
|
2014-03-07 15:21:38 +01:00
|
|
|
"Visibility: %s km" % rj["currently"]["visibility"], in_query_type)
|
|
|
|
self.reply(origin, source,
|
|
|
|
"Humidity: %s %%" % (rj["currently"]["humidity"]*100), in_query_type)
|
|
|
|
self.reply(origin, source,
|
|
|
|
"Pressure: %s hPa" % rj["currently"]["pressure"], in_query_type)
|
|
|
|
self.reply(origin, source,
|
|
|
|
"Ozone: %s DU" % rj["currently"]["ozone"], in_query_type)
|
|
|
|
if "nearestStormDistance" in rj["currently"]: self.reply(origin, source,
|
|
|
|
"Nearest Storm Distance: %s km" % rj["currently"]["nearestStormDistance"], in_query_type)
|
|
|
|
self.reply(origin, source,
|
|
|
|
"Sources: %s" % ", ".join(rj["flags"]["sources"]), in_query_type)
|
|
|
|
return "(Powered by Forecast <http://forecast.io/>)"
|
|
|
|
else:
|
|
|
|
return "Error while querying Forecast.io, got HTTP-Status %i" % rp.getcode()
|
2014-03-05 15:44:04 +01:00
|
|
|
else:
|
|
|
|
return "Usage: %s <lat> <lon>" % command
|
2014-03-21 09:57:47 +01:00
|
|
|
# MapQuest, mq, OpenStreetMap, osm <query>
|
2014-03-19 10:20:23 +01:00
|
|
|
elif command in ("mapquest", "mq", "openstreetmap", "osm") and self.mapquest_cfg["Active"] == "1":
|
|
|
|
if numparams==0:
|
|
|
|
return "You didn't ask anything..."
|
|
|
|
try: rp = urlopen("http://open.mapquestapi.com/nominatim/v1/search?q=%s&format=json"
|
|
|
|
% quote_plus(" ".join(params)))
|
|
|
|
except Exception as e:
|
|
|
|
self.log.error("Error while querying MapQuest: %s" % e)
|
|
|
|
return "Error while querying MapQuest: %s" % e
|
|
|
|
if rp.getcode() == 200:
|
|
|
|
used_fields = (
|
|
|
|
"display_name",
|
|
|
|
"lat", "lon"
|
|
|
|
)
|
|
|
|
rj = json.loads(str(rp.readall(), "utf-8"))
|
|
|
|
|
|
|
|
if len(rj) == 0:
|
|
|
|
return "No suitable reply from MapQuest for query %s" % " ".join(params)
|
|
|
|
|
|
|
|
rj = rj[0]
|
|
|
|
|
|
|
|
self.reply(origin, source, "Display Name: %s" % rj["display_name"], in_query_type)
|
|
|
|
self.reply(origin, source, "Lat: %s" % rj["lat"], in_query_type)
|
|
|
|
self.reply(origin, source, "Lon: %s" % rj["lon"], in_query_type)
|
|
|
|
return "(Nominatim Search Courtesy of MapQuest <http://www.mapquest.com/>)"
|
|
|
|
else:
|
|
|
|
return "Error while querying MapQuest, got HTTP-Status %i" % rp.getcode()
|
2014-03-21 09:57:47 +01:00
|
|
|
# Fefe <id>
|
|
|
|
elif command == "fefe":
|
|
|
|
if numparams>1:
|
|
|
|
return "Waddayawannasee? (The one and only optional argument needed is the ID of the blog post. If it is omitted, the last post will be shown.)"
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
if numparams==1:
|
|
|
|
rp = urlopen("http://blog.fefe.de/?ts=%s" % quote_plus(params[0]))
|
|
|
|
else:
|
|
|
|
rp = urlopen("http://blog.fefe.de/")
|
|
|
|
except Exception as e:
|
|
|
|
self.log.error("Error while querying Fefe: %s" % e)
|
|
|
|
return "Error while querying Fefe: %s" % e
|
|
|
|
if rp.getcode() == 200:
|
|
|
|
try:
|
|
|
|
rpd = str(rp.readall(), "utf-8")
|
|
|
|
m = re.search(r"<li>(.*?)<\/(?:li|ul)>", "".join(rpd.splitlines())).groups()[0]
|
|
|
|
print(m+"\n\n")
|
|
|
|
m = " ".join((re.split(r"<.+?>", m)))[6:]
|
|
|
|
if len(m)>400:
|
|
|
|
mmlen = 400
|
|
|
|
for l in self.msgsplit(m):
|
|
|
|
print(l)
|
|
|
|
self.reply(origin, source, l, in_query_type)
|
|
|
|
except Exception as e:
|
|
|
|
self.log.warning("Suspectious things happend while handling a response from fefes blog, but it's probably just an incorrect blogpost-id: %s" % e)
|
|
|
|
return "Nothing matched... (If you're sure your id was correct, see error log)"
|
2014-03-05 15:44:04 +01:00
|
|
|
|
2014-03-07 15:21:38 +01:00
|
|
|
### IRC ###
|
2014-03-01 01:25:12 +01:00
|
|
|
# join <channel>
|
2014-03-04 09:44:55 +01:00
|
|
|
elif command == "join":
|
2014-03-05 15:44:04 +01:00
|
|
|
if numparams>0:
|
2014-03-04 09:44:55 +01:00
|
|
|
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>
|
2014-03-04 09:44:55 +01:00
|
|
|
elif command == "part":
|
2014-03-05 15:44:04 +01:00
|
|
|
if numparams>0:
|
2014-03-04 09:44:55 +01:00
|
|
|
if self.check_privileges(origin["mask"], command): self.part(params[0])
|
|
|
|
else: self.query(origin["nick"], "You cannot do that!", in_query_type)
|
2014-03-03 18:06:42 +01:00
|
|
|
# mode ±<modes>
|
|
|
|
elif command == "mode":
|
2014-03-04 09:44:55 +01:00
|
|
|
if self.check_privileges(origin["mask"], command):
|
2014-03-05 15:44:04 +01:00
|
|
|
if numparams==0:
|
2014-03-04 09:44:55 +01:00
|
|
|
self.mode_reply["to"] = origin["nick"]
|
|
|
|
self.mode_reply["type"] = in_query_type
|
2014-03-03 18:06:42 +01:00
|
|
|
self.get_modes()
|
2014-03-04 09:44:55 +01:00
|
|
|
else:
|
2014-03-05 15:44:04 +01:00
|
|
|
self.set_mode(" ".join(params) if numparams>0 else params)
|
2014-03-04 09:44:55 +01:00
|
|
|
else: self.query(origin["nick"], "You cannot do that!", in_query_type)
|
2014-03-05 15:44:04 +01:00
|
|
|
|
2014-03-01 01:25:12 +01:00
|
|
|
# die [<quitmsg>]
|
|
|
|
elif command == "die":
|
2014-03-04 09:44:55 +01:00
|
|
|
if self.check_privileges(origin["mask"], command):
|
2014-03-05 15:44:04 +01:00
|
|
|
self.disconnect("".join(params) if numparams>0 else "".join((origin["nick"], " shot me, dying now... Bye...")))
|
2014-03-04 09:44:55 +01:00
|
|
|
else: self.query(origin["nick"], "Go die yourself!", in_query_type)
|
2014-03-03 18:06:42 +01:00
|
|
|
else:
|
|
|
|
replies = [
|
2014-03-04 09:44:55 +01:00
|
|
|
("What? \"%s\" is not a command!", 15),
|
|
|
|
("%s? What's that?", 3),
|
|
|
|
("Sorry, I don't know how to %s...", 1)
|
2014-03-03 18:06:42 +01:00
|
|
|
]
|
2014-03-04 09:44:55 +01:00
|
|
|
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(
|
2014-03-06 16:44:14 +01:00
|
|
|
description = "guess what? i think my desc is still missing!")
|
2014-03-05 15:44:04 +01:00
|
|
|
p.add_argument("action",
|
|
|
|
default = "help",
|
|
|
|
choices = ["start", "stop", "checkconf"],
|
2014-03-06 16:44:14 +01:00
|
|
|
help = "What to do?")
|
2014-03-05 15:44:04 +01:00
|
|
|
p.add_argument("--loglevel", "-l",
|
|
|
|
choices = ["critical", "error", "warning", "info", "debug"],
|
2014-03-06 16:44:14 +01:00
|
|
|
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():
|
2014-03-06 16:44:14 +01:00
|
|
|
# get the logging-module logger
|
2014-03-05 15:44:04 +01:00
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
2014-02-28 17:04:08 +01:00
|
|
|
global bot
|
2014-03-06 16:44:14 +01:00
|
|
|
|
|
|
|
# parse command line and config
|
2014-02-27 12:05:38 +01:00
|
|
|
args = parseargs()
|
2014-03-05 15:44:04 +01:00
|
|
|
cfg = ConfigObj("bot.conf")
|
|
|
|
|
2014-03-06 16:44:14 +01:00
|
|
|
# set the loglevel
|
2014-03-05 15:44:04 +01:00
|
|
|
nll = getattr(logging, cfg["Behavior"]["Loglevel"].upper(), None)
|
2014-03-06 16:44:14 +01:00
|
|
|
if not isinstance(nll, int): raise ValueError('Invalid log level: %s' % cfg["Behavior"]["Loglevel"])
|
2014-03-05 15:44:04 +01:00
|
|
|
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-03-06 16:44:14 +01:00
|
|
|
# do awesome stuff!
|
2014-02-27 12:05:38 +01:00
|
|
|
if args.action == "start":
|
2014-02-28 17:04:08 +01:00
|
|
|
try:
|
2014-03-05 15:44:04 +01:00
|
|
|
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"],
|
2014-03-19 10:20:23 +01:00
|
|
|
mapquest_cfg = cfg["MapQuest"],
|
2014-03-05 15:44:04 +01:00
|
|
|
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
|
2014-03-05 15:44:04 +01:00
|
|
|
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:
|
2014-03-05 15:44:04 +01:00
|
|
|
log.info("Got Ctrl-C, dying now...")
|
2014-03-03 18:06:42 +01:00
|
|
|
bot.disconnect("Ouch! Got shot by Ctrl-C, dying now... See you!")
|
2014-03-06 16:44:14 +01:00
|
|
|
except Exception as e:
|
|
|
|
log.exception("Fehler: %s", e)
|
2014-03-19 10:20:23 +01:00
|
|
|
bot.disconnect("Fehler: %s" % e)
|
2014-03-05 15:44:04 +01:00
|
|
|
log.debug("--- MAIN EXITING ---")
|
2014-02-27 12:05:38 +01:00
|
|
|
elif args.action == "stop": print("nope!")
|
2014-03-05 15:44:04 +01:00
|
|
|
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()
|