import asyncio import logging import re import time import irc3 from asgiref.sync import sync_to_async from django.conf import settings from django.utils import timezone from ircbot.models import OutgoingIrcMessage from teams.models import Team, TeamMember from teams.utils import get_team_from_irc_channel 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 async 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 ) await asyncio.sleep(settings.IRCBOT_CHECK_MESSAGE_INTERVAL_SECONDS) await 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""" # we only handle NOTICEs for now if kwargs["event"] != "NOTICE": return logger.debug("inside on_privmsg(), kwargs: %s" % kwargs) # 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 async def do_stuff(self): """ Main periodic method called every N seconds. """ # logger.debug("inside do_stuff()") # call the methods we need to # call them with sync_to_async to be able to access database await sync_to_async(self.bot.check_irc_channels)() await sync_to_async(self.bot.fix_missing_acls)() await sync_to_async(self.bot.get_outgoing_messages)() # schedule a call of this function again in N seconds await asyncio.sleep(settings.IRCBOT_CHECK_MESSAGE_INTERVAL_SECONDS) await 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) # add a delay so the bot doesn't flood itself off, irc3 antiflood settings do not help here, why? time.sleep(1) @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, channel=settings.IRCBOT_VOLUNTEER_CHANNEL, invite=True, ) # mark membership as irc_acl_fix_needed=False and save membership.irc_acl_fix_needed = 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) 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"])