435 lines
18 KiB
Python
435 lines
18 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
|
|
from teams.utils import get_team_from_irc_channel
|
|
|
|
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() or channel == settings.IRCBOT_PUBLIC_CHANNEL or channel == settings.IRCBOT_VOLUNTEER_CHANNEL:
|
|
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 below here
|
|
|
|
@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 = self.bot.get_desired_channel_list()
|
|
#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_desired_channel_list(self):
|
|
"""
|
|
Return a list of strings of all the IRC channels the bot is supposed to be in
|
|
"""
|
|
desired_channel_list = self.get_managed_team_channels()
|
|
desired_channel_list += self.get_unmanaged_team_channels()
|
|
desired_channel_list.append(settings.IRCBOT_PUBLIC_CHANNEL)
|
|
desired_channel_list.append(settings.IRCBOT_VOLUNTEER_CHANNEL)
|
|
return desired_channel_list
|
|
|
|
|
|
@irc3.extend
|
|
def get_managed_team_channels(self):
|
|
"""
|
|
Return a list of team IRC channels which the bot is supposed to be managing.
|
|
"""
|
|
pubchans = Team.objects.filter(
|
|
public_irc_channel_name__isnull=False,
|
|
public_irc_channel_bot=True,
|
|
public_irc_channel_managed=True
|
|
).values_list("public_irc_channel_name", flat=True)
|
|
|
|
privchans = Team.objects.filter(
|
|
private_irc_channel_name__isnull=False,
|
|
private_irc_channel_bot=True,
|
|
private_irc_channel_managed=True
|
|
).values_list("private_irc_channel_name", flat=True)
|
|
|
|
return list(pubchans) + list(privchans)
|
|
|
|
|
|
@irc3.extend
|
|
def get_unmanaged_team_channels(self):
|
|
"""
|
|
Return a list of team IRC channels which the bot is supposed to be in, but not managing.
|
|
"""
|
|
pubchans = Team.objects.filter(
|
|
public_irc_channel_name__isnull=False,
|
|
public_irc_channel_bot=True,
|
|
public_irc_channel_managed=False
|
|
).values_list("public_irc_channel_name", flat=True)
|
|
|
|
privchans = Team.objects.filter(
|
|
private_irc_channel_name__isnull=False,
|
|
private_irc_channel_bot=True,
|
|
private_irc_channel_managed=False
|
|
).values_list("private_irc_channel_name", flat=True)
|
|
|
|
return list(pubchans) + list(privchans)
|
|
|
|
|
|
@irc3.extend
|
|
def setup_private_channel(self, channel):
|
|
"""
|
|
Configures a private IRC channel by setting modes and adding all members to ACL if it is a team channel
|
|
"""
|
|
logger.debug("Inside setup_private_channel() for %s" % channel)
|
|
|
|
# basic private channel modes
|
|
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s mlock +inpst" % channel)
|
|
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s SECURE on" % channel)
|
|
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s RESTRICTED on" % channel)
|
|
|
|
# add the bot to the ACL
|
|
self.bot.add_user_to_channel_acl(
|
|
username=settings.IRCBOT_NICK,
|
|
channel=channel,
|
|
invite=True
|
|
)
|
|
|
|
team = get_team_from_irc_channel(channel)
|
|
if team:
|
|
# this is a team channel, add team members to channel ACL
|
|
self.bot.add_team_members_to_channel_acl(team)
|
|
# make sure private_irc_channel_fix_needed is set to False and save
|
|
team.private_irc_channel_fix_needed=False
|
|
team.save()
|
|
|
|
|
|
@irc3.extend
|
|
def setup_public_channel(self, channel):
|
|
"""
|
|
Configures a public IRC channel by setting modes and giving all team members +oO if it is a team channel
|
|
"""
|
|
logger.debug("Inside setup_public_channel() for %s" % channel)
|
|
|
|
# basic private channel modes
|
|
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s mlock +nt-lk" % channel)
|
|
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s SECURE off" % channel)
|
|
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s RESTRICTED off" % channel)
|
|
|
|
team = get_team_from_irc_channel(channel)
|
|
if team:
|
|
# add members to ACL
|
|
self.bot.add_team_members_to_channel_acl(team)
|
|
# make sure public_irc_channel_fix_needed is set to False and save
|
|
team.public_irc_channel_fix_needed=False
|
|
team.save()
|
|
|
|
|
|
@irc3.extend
|
|
def add_team_members_to_channel_acl(self, team):
|
|
"""
|
|
Handles initial ACL for team channels.
|
|
Sets membership.irc_acl_fix_needed=True for each approved teammember with a NickServ username
|
|
"""
|
|
# add all members to the acl
|
|
for membership in team.memberships.all():
|
|
if membership.approved and membership.user.profile.nickserv_username:
|
|
membership.irc_acl_fix_needed=True
|
|
membership.save()
|
|
|
|
|
|
@irc3.extend
|
|
def add_user_to_channel_acl(self, username, channel, invite):
|
|
"""
|
|
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,
|
|
},
|
|
)
|
|
|
|
if invite:
|
|
# 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 ACL entries as needed
|
|
Loops over Team objects and fixes permissions and ACLS as needed
|
|
"""
|
|
# first find all TeamMember objects which needs a loving hand
|
|
missing_acls = TeamMember.objects.filter(
|
|
irc_acl_fix_needed=True
|
|
).exclude(
|
|
user__profile__nickserv_username=''
|
|
)
|
|
|
|
# loop over them and fix what needs to be fixed
|
|
if missing_acls:
|
|
logger.debug("Found %s memberships which need IRC ACL fixing.." % missing_acls.count())
|
|
for membership in missing_acls:
|
|
# add to team public channel?
|
|
if membership.team.public_irc_channel_name and membership.team.public_irc_channel_managed:
|
|
self.bot.add_user_to_channel_acl(
|
|
username=membership.user.profile.nickserv_username,
|
|
channel=membership.team.public_irc_channel_name,
|
|
invite=False
|
|
)
|
|
|
|
# add to team private channel?
|
|
if membership.team.private_irc_channel_name and membership.team.private_irc_channel_managed:
|
|
self.bot.add_user_to_channel_acl(
|
|
username=membership.user.profile.nickserv_username,
|
|
channel=membership.team.private_irc_channel_name,
|
|
invite=True
|
|
)
|
|
|
|
# add to volunteer channel
|
|
self.bot.add_user_to_channel_acl(
|
|
username=membership.user.profile.nickserv_username,
|
|
chanel=settings.IRCBOT_VOLUNTEER_CHANNEL,
|
|
invite=True
|
|
)
|
|
|
|
# mark membership as irc_channel_acl_ok=True and save
|
|
membership.irc_acl_fix_neede=False
|
|
membership.save()
|
|
|
|
# loop over teams where the private channel needs fixing
|
|
for team in Team.objects.filter(private_irc_channel_fix_needed=True):
|
|
logger.debug("Team %s private IRC channel %s needs ACL fixing" % (team, team.private_irc_channel_name))
|
|
self.bot.setup_private_channel(team.private_irc_channel_name)
|
|
|
|
# loop over teams where the public channel needs fixing
|
|
for team in Team.objects.filter(public_irc_channel_fix_needed=True):
|
|
logger.debug("Team %s public IRC channel %s needs ACL fixing" % (team, team.public_irc_channel_name))
|
|
self.bot.setup_public_channel(team.public_irc_channel_name)
|
|
|
|
|
|
###############################################################################################
|
|
### 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() or channel == settings.IRCBOT_VOLUNTEER_CHANNEL:
|
|
# we want to register this channel! though 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)
|
|
|
|
team = get_team_from_irc_channel(channel)
|
|
if team:
|
|
if team.private_irc_channel_name == channel:
|
|
# set private channel modes, +I and ACL
|
|
self.bot.setup_private_channel(channel)
|
|
else:
|
|
# set public channel modes and +oO for all members
|
|
self.bot.setup_public_channel(channel)
|
|
return
|
|
logger.debug("Unable to find Team matching IRC channel %s" % channel)
|
|
|
|
# check if this channel is the volunteer channel
|
|
if channel == settings.IRCBOT_VOLUNTEER_CHANNEL:
|
|
logger.debug("%s is the volunteer channel, setting up" % channel)
|
|
self.bot.setup_private_channel(channel)
|
|
# lets handle the volunteer channels initial ACL manually..
|
|
return
|
|
|
|
logger.debug("Unhandled ChanServ message: %s" % kwargs['data'])
|
|
|
|
|
|
@irc3.extend
|
|
def handle_nickserv_privmsg(self, **kwargs):
|
|
"""
|
|
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'])
|
|
|