bornhack-website/src/ircbot/irc3module.py

339 lines
14 KiB
Python

import irc3, re
from ircbot.models import OutgoingIrcMessage
from teams.models import Team, TeamMember
from django.conf import settings
from django.utils import timezone
from events.models import Routing
import logging
logger = logging.getLogger("bornhack.%s" % __name__)
@irc3.plugin
class Plugin(object):
"""BornHack IRC3 class"""
requires = [
'irc3.plugins.core', # makes the bot able to connect to an irc server and do basic irc stuff
'irc3.plugins.userlist', # maintains a convenient list of the channels the bot is in and their users
]
def __init__(self, bot):
self.bot = bot
###############################################################################################
### builtin irc3 event methods
def server_ready(self, **kwargs):
"""triggered after the server sent the MOTD (require core plugin)"""
logger.debug("inside server_ready(), kwargs: %s" % kwargs)
logger.info("Identifying with %s" % settings.IRCBOT_NICKSERV_MASK)
self.bot.privmsg(settings.IRCBOT_NICKSERV_MASK, "identify %s %s" % (settings.IRCBOT_NICK, settings.IRCBOT_NICKSERV_PASSWORD))
logger.info("Calling self.bot.do_stuff() in %s seconds.." % settings.IRCBOT_CHECK_MESSAGE_INTERVAL_SECONDS)
self.bot.loop.call_later(settings.IRCBOT_CHECK_MESSAGE_INTERVAL_SECONDS, self.bot.do_stuff)
def connection_lost(self, **kwargs):
"""triggered when connection is lost"""
logger.debug("inside connection_lost(), kwargs: %s" % kwargs)
def connection_made(self, **kwargs):
"""triggered when connection is up"""
logger.debug("inside connection_made(), kwargs: %s" % kwargs)
###############################################################################################
### decorated irc3 event methods
@irc3.event(irc3.rfc.JOIN_PART_QUIT)
def on_join_part_quit(self, **kwargs):
"""triggered when there is a join part or quit on a channel the bot is in"""
logger.debug("inside on_join_part_quit(), kwargs: %s" % kwargs)
# TODO: on part or quit check if the bot is the only remaining member of a channel,
# if so, check if the channel should be managed, and if so, part and join the channel
# to gain @ and register with ChanServ
@irc3.event(irc3.rfc.JOIN)
def on_join(self, mask, channel, **kwargs):
"""Triggered when a channel is joined by someone, including the bot itself"""
if mask.nick == self.bot.nick:
# the bot just joined a channel
if channel in self.get_managed_team_channels():
logger.debug("Just joined a channel I am supposed to be managing, asking ChanServ for info about %s" % channel)
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "info %s" % channel)
return
@irc3.event(irc3.rfc.PRIVMSG)
def on_privmsg(self, **kwargs):
"""triggered when a privmsg is sent to the bot or to a channel the bot is in"""
logger.debug("inside on_privmsg(), kwargs: %s" % kwargs)
# we only handle NOTICEs for now
if kwargs['event'] != "NOTICE":
return
# check if this is a message from nickserv
if kwargs['mask'] == "NickServ!%s" % settings.IRCBOT_NICKSERV_MASK:
self.bot.handle_nickserv_privmsg(**kwargs)
# check if this is a message from chanserv
if kwargs['mask'] == "ChanServ!%s" % settings.IRCBOT_CHANSERV_MASK:
self.bot.handle_chanserv_privmsg(**kwargs)
@irc3.event(irc3.rfc.KICK)
def on_kick(self, **kwargs):
logger.debug("inside on_kick(), kwargs: %s" % kwargs)
###############################################################################################
### custom irc3 methods
@irc3.extend
def do_stuff(self):
"""
Main periodic method called every N seconds.
"""
#logger.debug("inside do_stuff()")
# call the methods we need to
self.bot.check_irc_channels()
self.bot.fix_missing_acls()
self.bot.get_outgoing_messages()
# schedule a call of this function again in N seconds
self.bot.loop.call_later(settings.IRCBOT_CHECK_MESSAGE_INTERVAL_SECONDS, self.bot.do_stuff)
@irc3.extend
def get_outgoing_messages(self):
"""
This method gets unprocessed OutgoingIrcMessage objects and attempts to send them to
the target channel. Messages are skipped if the bot is not in the channel.
"""
for msg in OutgoingIrcMessage.objects.filter(processed=False).order_by('created'):
logger.info("processing irc message to %s: %s" % (msg.target, msg.message))
# if this message expired mark it as expired and processed without doing anything
if msg.timeout < timezone.now():
logger.info("this message is expired, marking it as such instead of sending it to irc")
msg.expired=True
msg.processed=True
msg.save()
continue
# is this message for a channel or a nick?
if msg.target[0] == "#" and msg.target in self.bot.channels:
logger.info("sending privmsg to %s: %s" % (msg.target, msg.message))
self.bot.privmsg(msg.target, msg.message)
msg.processed=True
msg.save()
elif msg.target:
logger.info("sending privmsg to %s: %s" % (msg.target, msg.message))
self.bot.privmsg(msg.target, msg.message)
msg.processed=True
msg.save()
else:
logger.warning("skipping message to %s" % msg.target)
###############################################################################################
### irc channel methods
@irc3.extend
def check_irc_channels(self):
"""
Compare the list of IRC channels the bot is currently in with the list of IRC channels the bot is supposed to be in.
Join or part channels as needed.
"""
desired_channel_list = list(set(list(self.get_managed_team_channels()) + list(self.get_unmanaged_team_channels()) + [settings.IRCBOT_PUBLIC_CHANNEL]))
#logger.debug("Inside check_irc_channels(), desired_channel_list is: %s and self.bot.channels is: %s" % (desired_channel_list, self.bot.channels.keys()))
# loop over desired_channel_list, join as needed
for channel in desired_channel_list:
if channel not in self.bot.channels:
logger.debug("I should be in %s but I am not, attempting to join..." % channel)
self.bot.join(channel)
# loop over self.bot.channels, part as needed
for channel in self.bot.channels:
if channel not in desired_channel_list:
logger.debug("I am in %s but I shouldn't be, parting..." % channel)
self.bot.part(channel, "I am no longer needed here")
@irc3.extend
def get_managed_team_channels(self):
"""
Return a unique list of team IRC channels which the bot is supposed to be managing.
"""
return Team.objects.filter(
irc_channel=True,
irc_channel_managed=True
).values_list("irc_channel_name", flat=True)
@irc3.extend
def get_unmanaged_team_channels(self):
"""
Return a unique list of team IRC channels which the bot is not supposed to be managing.
"""
return Team.objects.filter(
irc_channel=True,
irc_channel_managed=False
).values_list("irc_channel_name", flat=True)
@irc3.extend
def setup_private_channel(self, team):
"""
Configures a private team IRC channel by setting modes and adding all members to ACL
"""
# basic private channel modes
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s mlock +inpst" % team.irc_channel_name)
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s SECURE on" % team.irc_channel_name)
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s RESTRICTED on" % team.irc_channel_name)
# add the bot to the ACL
self.bot.add_user_to_team_channel_acl(
username=settings.IRCBOT_NICK,
channel=team.irc_channel_name
)
# add all members to the acl
for membership in team.memberships.all():
if membership.approved and membership.user.profile.nickserv_username:
self.bot.add_user_to_team_channel_acl(
username=membership.user.profile.nickserv_username,
channel=membership.team.irc_channel_name,
)
# mark membership as irc_channel_acl_ok=True and save
membership.irc_channel_acl_ok=True
membership.save()
@irc3.extend
def add_user_to_team_channel_acl(self, username, channel):
"""
Add user to team IRC channel ACL
"""
# set autoop for this username
self.bot.privmsg(
settings.IRCBOT_CHANSERV_MASK,
"flags %(channel)s %(user)s +oO" % {
'channel': channel,
'user': username,
},
)
# also add autoinvite for this username
self.bot.mode(channel, '+I', '$a:%s' % username)
@irc3.extend
def fix_missing_acls(self):
"""
Called periodically by do_stuff()
Loops over TeamMember objects and adds and removes ACL entries as needed
"""
missing_acls = TeamMember.objects.filter(
team__irc_channel=True,
team__irc_channel_managed=True,
team__irc_channel_private=True,
irc_channel_acl_ok=False
).exclude(
user__profile__nickserv_username=''
)
if not missing_acls:
return
logger.debug("Found %s memberships which need IRC ACL fixing.." % missing_acls.count())
for membership in missing_acls:
self.bot.add_user_to_team_channel_acl(
username=membership.user.profile.nickserv_username,
channel=membership.team.irc_channel_name,
)
# mark membership as irc_channel_acl_ok=True and save
membership.irc_channel_acl_ok=True
membership.save()
###############################################################################################
### services (ChanServ & NickServ) methods
@irc3.extend
def handle_chanserv_privmsg(self, **kwargs):
"""
Handle messages from ChanServ on networks with Services.
"""
logger.debug("Got a message from ChanServ")
###############################################
# handle "Channel \x02#example\x02 is not registered." message
###############################################
match = re.compile("Channel (#[a-zA-Z0-9-]+) is not registered.").match(kwargs['data'].replace("\x02", ""))
if match:
# the irc channel is not registered
channel = match.group(1)
# get a list of the channels we are supposed to be managing
if channel in self.bot.get_managed_team_channels():
# we want to register this channel! but we can only do so if we have a @ in the channel
if self.bot.nick in self.bot.channels[channel].modes['@']:
logger.debug("ChanServ says channel %s is not registered, bot is supposed to be managing this channel, registering it with chanserv" % channel)
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "register %s" % channel)
else:
logger.debug("ChanServ says channel %s is not registered, bot is supposed to be managing this channel, but the bot cannot register without @ in the channel" % channel)
self.bot.privmsg(channel, "I need @ before I can register this channel with ChanServ")
return
###############################################
# handle "\x02#example\x02 is now registered to \x02tykbhdev\x02" message
###############################################
match = re.compile("(#[a-zA-Z0-9-]+) is now registered to ([a-zA-Z0-9-]+)\\.").match(kwargs['data'].replace("\x02", ""))
if match:
# the irc channel is now registered
channel = match.group(1)
botnick = match.group(2)
logger.debug("Channel %s was registered with ChanServ, looking up Team..." % channel)
# if this channel is a private team IRC channel set modes and add initial ACL
try:
team = Team.objects.get(irc_channel_name=channel)
except Team.DoesNotExist:
logger.debug("Unable to find Team matching IRC channel %s" % channel)
return
if not team.irc_channel_private:
# this channel is not private, no mode change and ACL needed
return
# set channel modes and ACL
self.bot.setup_private_channel(team)
return
logger.debug("Unhandled ChanServ message: %s" % kwargs['data'])
@irc3.extend
def handle_nickserv_privmsg(self, **kwargs):
"""th
Handles messages from NickServ on networks with Services.
"""
logger.debug("Got a message from NickServ")
# handle "\x02botnick\x02 is not a registered nickname." message
if kwargs['data'] == '\x02%s\x02 is not a registered nickname.' % self.bot.nick:
# the bots nickname is not registered, register new account with nickserv
self.bot.privmsg(settings.IRCBOT_NICKSERV_MASK, "register %s %s" % (settings.IRCBOT_NICKSERV_PASSWORD, settings.IRCBOT_NICKSERV_EMAIL))
return
logger.debug("Unhandled NickServ message: %s" % kwargs['data'])