268 lines
10 KiB
Python
268 lines
10 KiB
Python
|
#!/usr/bin/python
|
|||
|
# -*- coding: utf-8 -*-
|
|||
|
"""
|
|||
|
tenquestionmarks is an extensible, modular IRC bot.
|
|||
|
|
|||
|
This file is governed by the following license:
|
|||
|
Copyright (c) 2011 Adrian Malacoda, Monarch Pass
|
|||
|
|
|||
|
This program is free software: you can redistribute it and/or modify
|
|||
|
it under the terms of the GNU General Public License, version 3, as published by
|
|||
|
the Free Software Foundation.
|
|||
|
|
|||
|
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, see <http://www.gnu.org/licenses/>.
|
|||
|
|
|||
|
Based on the IRC bot at:
|
|||
|
http://kstars.wordpress.com/2009/09/05/a-python-irc-bot-for-keeping-up-with-arxiv-or-any-rss-feed/
|
|||
|
Copyright 2009 by Akarsh Simha
|
|||
|
"""
|
|||
|
|
|||
|
import sys
|
|||
|
import os
|
|||
|
import threading
|
|||
|
import time
|
|||
|
import optparse
|
|||
|
import json
|
|||
|
import traceback
|
|||
|
import datetime
|
|||
|
import inspect
|
|||
|
import re
|
|||
|
|
|||
|
for libdir in os.listdir(os.path.join(os.path.dirname(__file__),"lib")):
|
|||
|
sys.path.append(os.path.join(os.path.dirname(__file__),"lib",libdir))
|
|||
|
|
|||
|
import irclib
|
|||
|
from monarchpass import butterfree
|
|||
|
|
|||
|
class Tenquestionmarks(butterfree.Base,irclib.SimpleIRCClient):
|
|||
|
VERSION_NAME = "Tenquestionmarks"
|
|||
|
VERSION_NUMBER = "0.16.15"
|
|||
|
|
|||
|
def __init__(self):
|
|||
|
irclib.SimpleIRCClient.__init__(self)
|
|||
|
self.msgqueue = []
|
|||
|
butterfree.Base.__init__(self,"tenquestionmarks")
|
|||
|
|
|||
|
def config(self):
|
|||
|
conf = butterfree.Base.config(self)
|
|||
|
if "port" not in conf:
|
|||
|
conf["port"] = 6667
|
|||
|
return conf
|
|||
|
|
|||
|
def _dispatcher(self,connection,event):
|
|||
|
irclib.SimpleIRCClient._dispatcher(self,connection,event)
|
|||
|
self.log(event.eventtype(),"%s from %s to %s" % (event.arguments(), event.source(), event.target()))
|
|||
|
if not self.modules() == {}:
|
|||
|
method = "on_" + event.eventtype()
|
|||
|
self.dispatch(method,self,event)
|
|||
|
|
|||
|
def version(self):
|
|||
|
"""Creates the version string that is returned from a CTCP version
|
|||
|
request. This can be overridden."""
|
|||
|
return "%s %s" % (Tenquestionmarks.VERSION_NAME, Tenquestionmarks.VERSION_NUMBER)
|
|||
|
|
|||
|
def connect(self):
|
|||
|
"""Connects to the IRC network given in the constructor and joins a
|
|||
|
set of channels.
|
|||
|
|
|||
|
When this method completes, the on_connected event is fired for modules
|
|||
|
to handle."""
|
|||
|
|
|||
|
irclib.SimpleIRCClient.connect(self,self.config()["host"], self.config()["port"], self.config()["nick"], None, self.config()["nick"], ircname=self.config()["nick"])
|
|||
|
if "password" in self.config():
|
|||
|
self.server.privmsg("NickServ", "identify %s" % (self.config()["password"]))
|
|||
|
for channel in self.config()["channels"]:
|
|||
|
self.connection.join(channel)
|
|||
|
self.dispatch("on_connected",self)
|
|||
|
|
|||
|
def loop(self):
|
|||
|
"""Starts the IRC bot's loop."""
|
|||
|
|
|||
|
while 1:
|
|||
|
while len(self.msgqueue) > 0:
|
|||
|
msg = self.msgqueue.pop()
|
|||
|
for channel in self.config()["channels"]:
|
|||
|
self.log("post","Posting queued message %s" % (msg))
|
|||
|
try:
|
|||
|
self.connection.privmsg(channel, msg)
|
|||
|
except Exception, e:
|
|||
|
traceback.print_exc(file=sys.stdout)
|
|||
|
self.log("Error","%s %s" % (e,msg))
|
|||
|
time.sleep(1) # TODO: Fix bad code
|
|||
|
self.ircobj.process_once()
|
|||
|
time.sleep(1) # So that we don't hog the CPU!
|
|||
|
|
|||
|
def queue(self, message):
|
|||
|
"""Adds a message to the end of the bot's message queue."""
|
|||
|
self.msgqueue.append(message.encode("UTF-8"))
|
|||
|
|
|||
|
def on_ctcp(self, connection, event):
|
|||
|
"""Event handler for CTCP messages. If the CTCP message is a version request,
|
|||
|
a version string is returned."""
|
|||
|
|
|||
|
self.log("ctcp","%s from %s" % (event.arguments(), event.source()))
|
|||
|
ctcptype = event.arguments()[0]
|
|||
|
if ctcptype == "VERSION":
|
|||
|
self.connection.ctcp_reply(event.source().split("!")[0],self.version())
|
|||
|
|
|||
|
def on_privmsg(self, connection, event):
|
|||
|
"""Event handler for messages sent directly to the bot. These
|
|||
|
are always treated as commands."""
|
|||
|
|
|||
|
message = event.arguments()[0]
|
|||
|
nick = event.source().split("!")[0]
|
|||
|
try:
|
|||
|
(command, arg) = message.split(" ",1)
|
|||
|
except ValueError:
|
|||
|
command = message
|
|||
|
arg = ""
|
|||
|
self.log("cmd","%s called command %s with arg %s" % (nick,command,arg))
|
|||
|
self.call_command(nick, None, command, arg)
|
|||
|
|
|||
|
def on_pubmsg(self, connection, event):
|
|||
|
"""Event handler for messages sent to a channel in which the
|
|||
|
bot resides. If the message starts with a certain string, the
|
|||
|
message is treated as a command."""
|
|||
|
|
|||
|
message = event.arguments()[0]
|
|||
|
nick = event.source().split("!")[0]
|
|||
|
channel = event.target()
|
|||
|
if message.startswith(self.command_prefix):
|
|||
|
try:
|
|||
|
(command, arg) = message[1:].split(" ",1)
|
|||
|
except ValueError:
|
|||
|
command = message[1:]
|
|||
|
arg = ""
|
|||
|
self.log("cmd","%s called command %s in %s with arg %s" % (nick,command,channel,arg))
|
|||
|
self.call_command(nick, channel, command, arg)
|
|||
|
|
|||
|
def call_command(self, nick, channel, command, arg):
|
|||
|
"""Calls a command defined in one or more modules. This method is
|
|||
|
called indirectly through IRC messages sent from a user, who may
|
|||
|
optionally be sending that message publicly in a channel.
|
|||
|
|
|||
|
The argument is a single string. Depending on how many arguments
|
|||
|
the command takes, this string may be broken up into a number
|
|||
|
of strings separated by spaces.
|
|||
|
|
|||
|
The return value of the command is output either back to the
|
|||
|
user or to the channel in which the command was invoked."""
|
|||
|
|
|||
|
command_functions = butterfree.find_functions(self.modules(),command)
|
|||
|
if len(command_functions) > 0:
|
|||
|
for func in command_functions:
|
|||
|
argspec = inspect.getargspec(func)
|
|||
|
numargs = len(argspec.args) - 3
|
|||
|
varargs = not argspec.varargs == None
|
|||
|
args = []
|
|||
|
if varargs:
|
|||
|
args = arg.split(" ")
|
|||
|
else:
|
|||
|
args = arg.split(" ",numargs - 1)
|
|||
|
args.insert(0,self)
|
|||
|
args.insert(0,channel)
|
|||
|
args.insert(0,nick)
|
|||
|
try:
|
|||
|
returnvalue = func(*args)
|
|||
|
if not returnvalue == None:
|
|||
|
if not channel == None:
|
|||
|
self.multiline_privmsg(channel, returnvalue)
|
|||
|
else:
|
|||
|
self.multiline_privmsg(nick, returnvalue)
|
|||
|
except Exception, e:
|
|||
|
traceback.print_exc(file=sys.stdout)
|
|||
|
self.log("Error","Command %s caused an %s" % (command, e))
|
|||
|
|
|||
|
def degrade_to_ascii(self,string):
|
|||
|
"""In order to allow as wide a range of inputs as possible, if
|
|||
|
a Unicode string cannot be decoded, it can instead be run through
|
|||
|
this function, which will change "fancy quotes" to regular quotes
|
|||
|
and remove diacritics and accent marks, among other things.
|
|||
|
|
|||
|
To doubly sure that the string is safe for ASCII, any non-ASCII
|
|||
|
characters are then removed after any conversion is done."""
|
|||
|
|
|||
|
chars = {
|
|||
|
u"’": "'",
|
|||
|
u"‘": "'",
|
|||
|
u"“": '"',
|
|||
|
u"”": '"',
|
|||
|
u"Æ": "AE",
|
|||
|
u"À": "A",
|
|||
|
u"Á": "A",
|
|||
|
u"Â": "A",
|
|||
|
u"Ã": "A",
|
|||
|
u"Ä": "A",
|
|||
|
u"Å": "A",
|
|||
|
u"Ç": "C",
|
|||
|
u"È": "E",
|
|||
|
u"É": "E",
|
|||
|
u"Ê": "E",
|
|||
|
u"Ë": "E",
|
|||
|
u"Ì": "I",
|
|||
|
u"Í": "I",
|
|||
|
u"Î": "I",
|
|||
|
u"Ï": "I",
|
|||
|
u"Ð": "D",
|
|||
|
u"Ñ": "N",
|
|||
|
u"Ò": "O",
|
|||
|
u"Ó": "O",
|
|||
|
u"Ô": "O",
|
|||
|
u"Õ": "O",
|
|||
|
u"Ö": "O",
|
|||
|
u"Ø": "O",
|
|||
|
u"Ù": "U",
|
|||
|
u"Ú": "U",
|
|||
|
u"Û": "U",
|
|||
|
u"Ü": "U",
|
|||
|
u"Ý": "Y",
|
|||
|
u"Þ": "Th",
|
|||
|
u"ß": "S",
|
|||
|
u"–": "-"
|
|||
|
}
|
|||
|
for char in chars:
|
|||
|
string = string.replace(char,chars[char]).replace(char.lower(),chars[char].lower())
|
|||
|
|
|||
|
# Strip away anything non-ascii that remains
|
|||
|
string = "".join([char for char in string if ord(char) < 128])
|
|||
|
return string
|
|||
|
def html(self,string):
|
|||
|
"""Basic parser that converts between a very limited subset of
|
|||
|
HTML and IRC control codes.
|
|||
|
|
|||
|
Note that this parser is very strict about the font tag. It expects
|
|||
|
either color or color and bgcolor, but not bgcolor alone. It also wants
|
|||
|
both color and bgcolor to be quoted and separated by no more or less than one space."""
|
|||
|
|
|||
|
tags = [
|
|||
|
[re.compile("<b>(.+?)</b>"),"\x02%(0)s\x02"],
|
|||
|
[re.compile("<u>(.+?)</u>"),"\x1f%(0)s\x1f"],
|
|||
|
[re.compile("<font color=\"(.+?)\">(.+?)</font>"),"\x03%(0)s%(1)s\x03"],
|
|||
|
[re.compile("<font color=\"(.+?)\" bgcolor=\"(.+?)\">(.+?)</font>"),"\x03%(0)s,%(1)s%(2)s\x03"],
|
|||
|
[re.compile("<font bgcolor=\"(.+?)\" color=\"(.+?)\">(.+?)</font>"),"\x03%(1)s,%(0)s%2(2)s\x03"]
|
|||
|
]
|
|||
|
for (regex,replacement) in tags:
|
|||
|
regex_match = regex.search(string)
|
|||
|
while regex_match is not None:
|
|||
|
groups_dict = {}
|
|||
|
for i in xrange(len(regex_match.groups())):
|
|||
|
groups_dict[str(i)] = regex_match.groups()[i]
|
|||
|
|
|||
|
string = string.replace(regex_match.group(0), replacement % groups_dict)
|
|||
|
regex_match = regex.search(string)
|
|||
|
return string
|
|||
|
def multiline_privmsg(self, target, message):
|
|||
|
for line in message.split("\n"):
|
|||
|
self.connection.privmsg(target, line)
|
|||
|
|
|||
|
if __name__ == "__main__":
|
|||
|
tqm = Tenquestionmarks()
|
|||
|
tqm.connect()
|
|||
|
tqm.loop()
|