mirror of https://github.com/str4d/RelayBot.git
214 lines
8.1 KiB
Python
214 lines
8.1 KiB
Python
from twisted.words.protocols import irc
|
|
from twisted.internet import reactor
|
|
from twisted.internet.protocol import ReconnectingClientFactory
|
|
from twisted.internet.task import LoopingCall
|
|
from twisted.python import log
|
|
from twisted.application import service
|
|
from signal import signal, SIGINT
|
|
from ConfigParser import SafeConfigParser
|
|
import sys
|
|
from tempfile import TemporaryFile
|
|
import subprocess
|
|
|
|
#
|
|
# RelayBot is a derivative of http://code.google.com/p/relaybot/
|
|
#
|
|
|
|
log.startLogging(sys.stdout)
|
|
|
|
__version__ = "0.1"
|
|
application = service.Application("RelayBot")
|
|
|
|
|
|
def main():
|
|
config = SafeConfigParser()
|
|
config.read("relaybot.config")
|
|
defaults = config.defaults()
|
|
|
|
for section in config.sections():
|
|
|
|
def get(option):
|
|
if option in defaults or config.has_option(section, option):
|
|
return config.get(section, option) or defaults[option]
|
|
else:
|
|
return None
|
|
|
|
options = {}
|
|
for option in ["timeout", "host", "port", "nick", "channel",
|
|
"heartbeat", "password", "username", "realname",
|
|
"recognizedNicks", "program", "useSSL"]:
|
|
options[option] = get(option)
|
|
|
|
mode = get("mode")
|
|
|
|
#Not using endpoints pending http://twistedmatrix.com/trac/ticket/4735
|
|
#(ReconnectingClientFactory equivalent for endpoints.)
|
|
factory = None
|
|
if mode == "Default":
|
|
factory = RelayFactory
|
|
elif mode == "NickServ":
|
|
factory = NickServFactory
|
|
options["nickServPassword"] = get("nickServPassword")
|
|
factory = factory(options)
|
|
reactor.connectTCP(options['host'], int(options['port']), factory, int(options['timeout']))
|
|
|
|
reactor.callWhenRunning(signal, SIGINT, handler)
|
|
|
|
|
|
class IRCRelayer(irc.IRCClient):
|
|
|
|
def __init__(self, config):
|
|
self.network = config['host']
|
|
self.password = config['password']
|
|
self.channel = config['channel']
|
|
self.nickname = config['nick']
|
|
self.identifier = config['identifier']
|
|
self.heartbeatInterval = float(config['heartbeat'])
|
|
self.username = config['username']
|
|
self.realname = config['realname']
|
|
self.recognized_nicks = config['recognizedNicks'].split(',')
|
|
self.program = config['program']
|
|
|
|
log.msg("IRC Relay created. Name: {0} | Host: {1} | Channel: {2}"
|
|
.format(self.nickname, self.network, self.channel))
|
|
|
|
# IRC RFC: https://tools.ietf.org/html/rfc2812#page-4
|
|
if len(self.nickname) > 9:
|
|
log.msg("Nickname {0} is {1} characters long, which exceeds the "
|
|
"RFC maximum of 9 characters. This may cause connection "
|
|
"problems.".format(self.nickname, len(self.nickname)))
|
|
|
|
def formatUsername(self, username):
|
|
return username.split("!")[0]
|
|
|
|
def signedOn(self):
|
|
log.msg("[{0}] Connected to network.".format(self.network))
|
|
self.startHeartbeat()
|
|
self.join(self.channel, "")
|
|
|
|
def connectionLost(self, reason):
|
|
log.msg("[{0}] Connection lost.",format(self.network))
|
|
|
|
def sayToChannel(self, message):
|
|
self.say(self.channel, message)
|
|
|
|
def joined(self, channel):
|
|
log.msg("Joined channel {0}.".format(channel))
|
|
|
|
def privmsg(self, user, channel, message):
|
|
#If someone addresses the bot directly.
|
|
if channel == self.nickname:
|
|
if user in self.recognized_nicks:
|
|
log.msg("Received privmsg from recognized {0}.".format(user))
|
|
|
|
# Create input and output of processing program. StringIO
|
|
# file objects are insufficient for these purposes -
|
|
# apparently they must have real file descriptors.
|
|
in_file = TemporaryFile()
|
|
out_file = TemporaryFile()
|
|
|
|
in_file.write(message)
|
|
in_file.flush()
|
|
|
|
ret_code = subprocess.call(self.program, stdin=in_file,
|
|
stdout=out_file)
|
|
|
|
if ret_code != 0:
|
|
self.msg(user, "Program {0} failed with return code {1}."
|
|
.format(self.program, ret_code))
|
|
return
|
|
|
|
# Return output file to the beginning before reading.
|
|
out_file.seek(0)
|
|
|
|
for line in out_file:
|
|
self.msg(user, line)
|
|
else:
|
|
log.msg("Received privmsg from unrecognized {0}.".format(user))
|
|
|
|
def kickedFrom(self, channel, kicker, message):
|
|
log.msg("Kicked by {0}. Message \"{1}\"".format(kicker, message))
|
|
|
|
|
|
class RelayFactory(ReconnectingClientFactory):
|
|
protocol = IRCRelayer
|
|
#Log information which includes reconnection status.
|
|
noisy = True
|
|
|
|
def __init__(self, config):
|
|
config["identifier"] = "{0}{1}{2}".format(config["host"], config["port"], config["channel"])
|
|
config['useSSL'] = config['useSSL'] == 'True'
|
|
self.config = config
|
|
|
|
def buildProtocol(self, addr):
|
|
#Connected - reset reconnect attempt delay.
|
|
self.resetDelay()
|
|
x = self.protocol(self.config)
|
|
x.factory = self
|
|
return x
|
|
|
|
class NickServRelayer(IRCRelayer):
|
|
NickServ = "nickserv"
|
|
NickPollInterval = 30
|
|
|
|
def signedOn(self):
|
|
log.msg("[%s] Connected to network."%self.network)
|
|
self.startHeartbeat()
|
|
self.join(self.channel, "")
|
|
self.checkDesiredNick()
|
|
|
|
def checkDesiredNick(self):
|
|
"""
|
|
Checks that the nick is as desired, and if not attempts to retrieve it with
|
|
NickServ GHOST and trying again to change it after a polling interval.
|
|
"""
|
|
if self.nickname != self.desiredNick:
|
|
log.msg("[%s] Using GHOST to reclaim nick %s."%(self.network, self.desiredNick))
|
|
self.msg(NickServRelayer.NickServ, "GHOST %s %s"%(self.desiredNick, self.password))
|
|
# If NickServ does not respond try to regain nick anyway.
|
|
self.nickPoll.start(self.NickPollInterval)
|
|
|
|
def regainNickPoll(self):
|
|
if self.nickname != self.desiredNick:
|
|
log.msg("[%s] Reclaiming desired nick in polling."%(self.network))
|
|
self.setNick(self.desiredNick)
|
|
else:
|
|
log.msg("[%s] Have desired nick."%(self.network))
|
|
self.nickPoll.stop()
|
|
|
|
def nickChanged(self, nick):
|
|
log.msg("[%s] Nick changed from %s to %s."%(self.network, self.nickname, nick))
|
|
self.nickname = nick
|
|
self.checkDesiredNick()
|
|
|
|
def noticed(self, user, channel, message):
|
|
log.msg("[%s] Recieved notice \"%s\" from %s."%(self.network, message, user))
|
|
#Identify with nickserv if requested
|
|
if IRCRelayer.formatUsername(self, user).lower() == NickServRelayer.NickServ:
|
|
msg = message.lower()
|
|
if msg.startswith("this nickname is registered and protected"):
|
|
log.msg("[%s] Password requested; identifying with %s."%(self.network, NickServRelayer.NickServ))
|
|
self.msg(NickServRelayer.NickServ, "IDENTIFY %s"%self.password)
|
|
elif msg == "ghost with your nickname has been killed." or msg == "ghost with your nick has been killed.":
|
|
log.msg("[%s] GHOST successful, reclaiming nick %s."%(self.network,self.desiredNick))
|
|
self.setNick(self.desiredNick)
|
|
elif msg.endswith("isn't currently in use."):
|
|
log.msg("[%s] GHOST not needed, reclaiming nick %s."%(self.network,self.desiredNick))
|
|
self.setNick(self.desiredNick)
|
|
|
|
def __init__(self, config):
|
|
IRCRelayer.__init__(self, config)
|
|
self.password = config['nickServPassword']
|
|
self.desiredNick = config['nick']
|
|
self.nickPoll = LoopingCall(self.regainNickPoll)
|
|
|
|
class NickServFactory(RelayFactory):
|
|
protocol = NickServRelayer
|
|
|
|
def handler(signum, frame):
|
|
reactor.stop()
|
|
|
|
#Main if run as script, builtin for twistd.
|
|
if __name__ in ["__main__", "__builtin__"]:
|
|
main()
|