268 lines
10 KiB
Python
Executable File
268 lines
10 KiB
Python
Executable File
#!/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()
|