3
0
mirror of https://github.com/jlu5/PyLink.git synced 2024-11-27 13:09:23 +01:00

Initial work on dynamic service bot joining / parting (#265)

This commit is contained in:
James Lu 2018-04-12 12:45:33 -07:00
parent 2cb4a06e64
commit 2f12a5b710
3 changed files with 76 additions and 29 deletions

View File

@ -73,9 +73,8 @@ def spawn_service(irc, source, command, args):
log.debug('(%s) spawn_service: irc.pseudoclient set to UID %s', irc.name, u) log.debug('(%s) spawn_service: irc.pseudoclient set to UID %s', irc.name, u)
irc.pseudoclient = userobj irc.pseudoclient = userobj
channels = set(irc.serverdata.get(name+'_channels', [])) | set(irc.serverdata.get('channels', [])) | \ # Enumerate & join network defined channels.
sbot.extra_channels.get(irc.name, set()) sbot.join(irc, sbot.get_persistent_channels(irc))
sbot.join(irc, channels)
utils.add_hook(spawn_service, 'PYLINK_NEW_SERVICE') utils.add_hook(spawn_service, 'PYLINK_NEW_SERVICE')
@ -121,12 +120,43 @@ def handle_kill(irc, source, command, args):
utils.add_hook(handle_kill, 'KILL') utils.add_hook(handle_kill, 'KILL')
def handle_join(irc, source, command, args):
"""Monitors channel joins for dynamic service bot joining."""
channel = args['channel']
users = irc.channels[channel].users
for servicename, sbot in world.services.items():
if channel in sbot.get_persistent_channels(irc) and \
sbot.uids.get(irc.name) not in users:
log.debug('(%s) Dynamically joining service %r to channel %r.', irc.name, servicename, channel)
sbot.join(irc, channel)
utils.add_hook(handle_join, 'JOIN')
utils.add_hook(handle_join, 'PYLINK_SERVICE_JOIN')
def _services_dynamic_part(irc, channel):
"""Dynamically removes service bots from empty channels."""
# If all remaining users in the channel are service bots, make them all part.
if all(irc.is_internal_client(u) for u in irc.channels[channel].users):
for u in irc.channels[channel].users.copy():
sbot = irc.get_service_bot(u)
if sbot:
log.debug('(%s) Dynamically parting service %r from channel %r.', irc.name, sbot.name, channel)
irc.part(u, channel)
return True
def handle_part(irc, source, command, args):
"""Monitors channel joins for dynamic service bot joining."""
for channel in args['channels']:
_services_dynamic_part(irc, channel)
utils.add_hook(handle_part, 'PART')
def handle_kick(irc, source, command, args): def handle_kick(irc, source, command, args):
"""Handle KICKs to the PyLink service bots, rejoining channels as needed.""" """Handle KICKs to the PyLink service bots, rejoining channels as needed."""
kicked = args['target']
channel = args['channel'] channel = args['channel']
# Skip autorejoin routines if the channel is now empty.
if not _services_dynamic_part(irc, channel):
kicked = args['target']
sbot = irc.get_service_bot(kicked) sbot = irc.get_service_bot(kicked)
if sbot: if sbot and channel in sbot.get_persistent_channels(irc):
sbot.join(irc, channel) sbot.join(irc, channel)
utils.add_hook(handle_kick, 'KICK') utils.add_hook(handle_kick, 'KICK')
@ -142,7 +172,7 @@ def handle_commands(irc, source, command, args):
utils.add_hook(handle_commands, 'PRIVMSG') utils.add_hook(handle_commands, 'PRIVMSG')
# Register the main PyLink service. All command definitions MUST go after this! # Register the main PyLink service. All command definitions MUST go after this!
# TODO: be more specific, and possibly allow plugins to modify this to mention # TODO: be more specific in description, and possibly allow plugins to modify this to mention
# their features? # their features?
mydesc = "\x02PyLink\x02 provides extended network services for IRC." mydesc = "\x02PyLink\x02 provides extended network services for IRC."
utils.register_service('pylink', default_nick="PyLink", desc=mydesc, manipulatable=True) utils.register_service('pylink', default_nick="PyLink", desc=mydesc, manipulatable=True)

View File

@ -539,23 +539,19 @@ def remove_channel(irc, channel):
if irc is None: if irc is None:
return return
if channel not in map(str.lower, irc.serverdata.get('channels', [])): world.services['pylink'].dynamic_channels[irc.name].discard(channel)
world.services['pylink'].extra_channels[irc.name].discard(channel)
if irc.pseudoclient:
irc.part(irc.pseudoclient.uid, channel, 'Channel delinked.')
relay = get_relay(irc, channel) relay = get_relay(irc, channel)
if relay and channel in irc.channels: if relay and channel in irc.channels:
for user in irc.channels[channel].users.copy(): for user in irc.channels[channel].users.copy():
if not is_relay_client(irc, user): # Relay a /part of all local users.
if not is_internal_client(irc, user):
relay_part(irc, channel, user) relay_part(irc, channel, user)
# Don't ever part the main client from any of its autojoin channels.
else: else:
if user == irc.pseudoclient.uid and channel in \ # Part and quit all relay clients.
irc.serverdata.get('channels', []): if irc.get_service_bot(user): # ...but ignore service bots
continue continue
irc.part(user, channel, 'Channel delinked.') irc.part(user, channel, 'Channel delinked.')
# Don't ever quit it either...
if user != irc.pseudoclient.uid and not irc.users[user].channels: if user != irc.pseudoclient.uid and not irc.users[user].channels:
remoteuser = get_orig_user(irc, user) remoteuser = get_orig_user(irc, user)
del relayusers[remoteuser][irc.name] del relayusers[remoteuser][irc.name]

View File

@ -13,7 +13,7 @@ import collections
import argparse import argparse
from .log import log from .log import log
from . import world, conf from . import world, conf, structures
# Load the protocol and plugin packages. # Load the protocol and plugin packages.
from pylinkirc import protocols, plugins from pylinkirc import protocols, plugins
@ -152,9 +152,10 @@ class ServiceBot():
# spawned. # spawned.
self.uids = {} self.uids = {}
# Track what channels other than those defined in the config # Track plugin-defined persistent channels. The bot will leave them if they're empty,
# that the bot should join by default. # and rejoin whenever someone else does.
self.extra_channels = collections.defaultdict(set) self.dynamic_channels = structures.KeyedDefaultdict(lambda netname:
structures.IRCCaseInsensitiveSet(world.networkobjects[netname]))
# Service description, used in the default help command if one is given. # Service description, used in the default help command if one is given.
self.desc = desc self.desc = desc
@ -183,9 +184,14 @@ class ServiceBot():
else: else:
raise NotImplementedError("Network specific plugins not supported yet.") raise NotImplementedError("Network specific plugins not supported yet.")
def join(self, irc, channels, autojoin=True): def join(self, irc, channels, ignore_empty=True):
""" """
Joins the given service bot to the given channel(s). Joins the given service bot to the given channel(s). channels can be an iterable of channel names
or the name of a single channel (str).
The ignore_empty option sets whether we should skip joining empty channels and join them
later when we see someone else join. This is option is disabled on networks where we cannot
monitor channel state.
""" """
if isinstance(irc, str): if isinstance(irc, str):
@ -198,10 +204,6 @@ class ServiceBot():
channels = [channels] channels = [channels]
channels = set(channels) channels = set(channels)
if autojoin:
log.debug('(%s/%s) Adding channels %s to autojoin', netname, self.name, channels)
self.extra_channels[netname] |= channels
# If the network was given as a string, look up the Irc object here. # If the network was given as a string, look up the Irc object here.
try: try:
irc = world.networkobjects[netname] irc = world.networkobjects[netname]
@ -209,6 +211,10 @@ class ServiceBot():
log.debug('(%s/%s) Skipping join(), IRC object not initialized yet', netname, self.name) log.debug('(%s/%s) Skipping join(), IRC object not initialized yet', netname, self.name)
return return
if irc.has_cap('visible-state-only'):
# Disable dynamic channel joining on networks where we can't monitor channels for joins.
ignore_empty = False
try: try:
u = self.uids[irc.name] u = self.uids[irc.name]
except KeyError: except KeyError:
@ -221,10 +227,16 @@ class ServiceBot():
for chan in channels: for chan in channels:
if irc.is_channel(chan): if irc.is_channel(chan):
if chan in irc.channels and u in irc.channels[chan].users: if chan in irc.channels:
log.debug('(%s) Skipping join of services %s to channel %s - it is already present', irc.name, self.name, chan) if u in irc.channels[chan].users:
log.debug('(%s) Skipping join of service %r to channel %r - it is already present', irc.name, self.name, chan)
continue continue
elif ignore_empty:
log.debug('(%s) Skipping joining service %r to empty channel %r', irc.name, self.name, chan)
continue
log.debug('(%s) Joining services %s to channel %s with modes %r', irc.name, self.name, chan, joinmodes) log.debug('(%s) Joining services %s to channel %s with modes %r', irc.name, self.name, chan, joinmodes)
if joinmodes: # Modes on join were specified; use SJOIN to burst our service if joinmodes: # Modes on join were specified; use SJOIN to burst our service
irc.proto.sjoin(irc.sid, chan, [(joinmodes, u)]) irc.proto.sjoin(irc.sid, chan, [(joinmodes, u)])
else: else:
@ -387,6 +399,15 @@ class ServiceBot():
sbconf = conf.conf.get(self.name, {}) sbconf = conf.conf.get(self.name, {})
return irc.serverdata.get("%s_realname" % self.name) or sbconf.get('realname') or conf.conf['pylink'].get('realname') or self.name return irc.serverdata.get("%s_realname" % self.name) or sbconf.get('realname') or conf.conf['pylink'].get('realname') or self.name
def get_persistent_channels(self, irc):
"""
Returns a set of persistent channels for the IRC network.
"""
channels = self.dynamic_channels[irc.name].copy()
channels |= set(irc.serverdata.get(self.name+'_channels', []))
channels |= set(irc.serverdata.get('channels', []))
return channels
def _show_command_help(self, irc, command, private=False, shortform=False): def _show_command_help(self, irc, command, private=False, shortform=False):
""" """
Shows help for the given command. Shows help for the given command.