import sys
from datetime import datetime
from humanize import naturaltime
from twisted.internet import reactor, protocol, ssl, task
from twisted.words.protocols import irc
from twisted.internet.threads import deferToThread
from twisted.python import log
import time
import pkgutil
import dave.modules as modules
import re
import subprocess
import dave.config as config
import requests
from dave.ratelimit import ratelimit
class Dave(irc.IRCClient):
nickname = config.config["irc"]["nick"]
def __init__(self):
Dave.instance = self
def lineReceived(self, line):
"""Override lineReceived to ignore invalid characters so non-utf8 messages
don't crash the bot"""
if isinstance(line, bytes):
line = line.decode("utf-8", errors="ignore")
super(Dave, self).lineReceived(line)
def connectionMade(self):
irc.IRCClient.connectionMade(self)
log.msg("Connected to server at {} with name {}".format(
time.asctime(time.localtime(time.time())), self.nickname))
def connectionLost(self, reason):
irc.IRCClient.connectionLost(self, reason)
log.err("Disconnected from server at {}".format(
time.asctime(time.localtime(time.time()))))
def signedOn(self):
"""Called when bot has succesfully signed on to server."""
self.mode(self.nickname, True, "B")
for channel in config.config["irc"]["channels"]:
self.join(channel)
if config.config["irc"]["nickserv_password"]:
self.msg("nickserv", "identify {}".format(config.config["irc"]["nickserv_password"]))
def joined(self, channel):
"""This will get called when the bot joins the channel."""
def privmsg(self, user, channel, msg):
"""This will get called when the bot receives a message."""
nick, userhost = user.split("!", 1)
log.msg("<{}> {}".format(user, msg))
path = modules.__path__
prefix = "{}.".format(modules.__name__)
method = (99999, None)
run = []
if channel == self.nickname:
channel = nick
match = re.match(r"^(?:{}[:,]? ){}(.*)$".format(
self.nickname,
"?" if channel == nick else ""
), msg)
invoked = bool(match)
msg = match.group(1) if invoked else msg
for importer, modname, ispkg in pkgutil.iter_modules(path, prefix):
m = importer.find_module(modname).load_module(modname)
for name, val in m.__dict__.items():
if not callable(val) or not hasattr(val, "rule"):
continue
priority = val.priority.value if hasattr(val, "priority") else 0
if method[0] < priority and not hasattr(val, "always_run"):
continue
for rule in val.rule:
if rule["named"] and not invoked:
continue
match = re.match(rule["pattern"], msg)
if match:
if hasattr(val, "always_run"):
run.append((val, match.groups()))
else:
method = (priority, val, match.groups(), rule["named"])
break
ignore_dont_always_run = False
if method[1] is not None:
if ratelimit(method[1], userhost):
deferToThread(method[1], self, method[2], nick, channel)
elif method[3]:
self.reply(channel, nick, "You have been ratelimited for this command.")
else:
ignore_dont_always_run = True
if method[1] is None or ignore_dont_always_run or \
not (hasattr(method[1], "dont_always_run") and method[1].dont_always_run):
for m in run:
if not hasattr(m[0], "ratelimit") or ratelimit(m[0], userhost):
deferToThread(m[0], self, m[1], nick, channel)
def irc_unknown(self, prefix, command, params):
if command == "INVITE":
self.join(params[1])
def msg(self, dest, message, length=None):
"""Override msg() to log what the bot says and to make msg thread safe."""
log.msg("<{}> {}".format(self.nickname, message))
reactor.callFromThread(super(Dave, self).msg, dest, message, length)
def reply(self, source, sender, msg):
if source == sender:
self.msg(source, msg)
else:
self.msg(source, "{}: {}".format(sender, msg))
def autopull():
log.msg("Pulling latest code from git.")
output = subprocess.check_output(["git", "pull"])
if not "Already up-to-date" in str(output):
args = ["git", "log", "-1", "--pretty=format:{}".format("|||".join([
"%h", "%s", "%at", "%an", "%ae"
]))]
output = subprocess.check_output(args).split(b"|||")
log.msg("Pulled latest commit.")
try:
r = requests.post('https://git.io/', {
"url": "https://github.com/{}/commit/{}".format(
config.config["repo"],
str(output[0], 'utf-8')
)
}, allow_redirects=False, timeout=3)
commit = r.headers['Location']
except:
commit = ""
msg = "{} ({}) authored by {} ({}) {} {}".format(
str(output[1], 'utf-8'),
str(output[0], 'utf-8'),
str(output[3], 'utf-8'),
str(output[4], 'utf-8'),
naturaltime(datetime.utcnow().timestamp() -
float(output[2])),
commit
)
log.msg("Updated, {}".format(msg))
if hasattr(Dave, "instance"):
for channel in config.config["irc"]["channels"]:
Dave.instance.msg(channel, msg)
else:
log.msg("Already up to date.")
def main():
log.startLogging(sys.stdout)
task.LoopingCall(lambda: deferToThread(autopull)).start(120.0)
factory = protocol.ReconnectingClientFactory.forProtocol(Dave)
reactor.connectSSL(config.config["irc"]["host"], config.config["irc"]["port"],
factory, ssl.ClientContextFactory())
reactor.run()