3
0
mirror of https://github.com/jlu5/PyLink.git synced 2024-12-18 08:02:51 +01:00

Merge branch 'wip/dynamic-services' into devel

This commit is contained in:
James Lu 2018-04-13 22:08:18 -07:00
commit 4d9fbc55ba
3 changed files with 89 additions and 33 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')
@ -108,26 +107,66 @@ def handle_kill(irc, source, command, args):
irc.pseudoclient = None irc.pseudoclient = None
userdata = args.get('userdata') userdata = args.get('userdata')
sbot = irc.get_service_bot(target) sbot = irc.get_service_bot(target)
servicename = None
if userdata and hasattr(userdata, 'service'): # Look for the target's service name attribute servicename = None
servicename = userdata.service channels = []
elif sbot: # Or their service bot instance
if userdata:
# Look for the target's service name
servicename = getattr(userdata, 'service', servicename)
channels = getattr(userdata, 'channels', channels)
elif sbot:
# Or its service bot instance
servicename = sbot.name servicename = sbot.name
channels = irc.users[target].channels
if servicename: if servicename:
log.info('(%s) Received kill to service %r (nick: %r) from %s (reason: %r).', irc.name, servicename, log.info('(%s) Received kill to service %r (nick: %r) from %s (reason: %r).', irc.name, servicename,
userdata.nick if userdata else irc.users[target].nick, irc.get_hostmask(source), args.get('text')) userdata.nick if userdata else irc.users[target].nick, irc.get_hostmask(source), args.get('text'))
spawn_service(irc, source, command, {'name': servicename}) spawn_service(irc, source, command, {'name': servicename})
# Rejoin the killed service bot to all channels it was previously in.
world.services[servicename].join(irc, channels)
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']
sbot = irc.get_service_bot(kicked) # Skip autorejoin routines if the channel is now empty.
if sbot: if not _services_dynamic_part(irc, channel):
sbot.join(irc, channel) kicked = args['target']
sbot = irc.get_service_bot(kicked)
if sbot:
sbot.join(irc, channel)
utils.add_hook(handle_kick, 'KICK') utils.add_hook(handle_kick, 'KICK')
def handle_commands(irc, source, command, args): def handle_commands(irc, source, command, args):
@ -142,7 +181,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 irc.is_internal_client(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
elif ignore_empty:
log.debug('(%s) Skipping joining service %r to empty channel %r', irc.name, self.name, chan)
continue 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.