3
0
mirror of https://github.com/jlu5/PyLink.git synced 2024-11-23 19:19:31 +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)
irc.pseudoclient = userobj
channels = set(irc.serverdata.get(name+'_channels', [])) | set(irc.serverdata.get('channels', [])) | \
sbot.extra_channels.get(irc.name, set())
sbot.join(irc, channels)
# Enumerate & join network defined channels.
sbot.join(irc, sbot.get_persistent_channels(irc))
utils.add_hook(spawn_service, 'PYLINK_NEW_SERVICE')
@ -121,13 +120,44 @@ def handle_kill(irc, source, command, args):
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):
"""Handle KICKs to the PyLink service bots, rejoining channels as needed."""
kicked = args['target']
channel = args['channel']
sbot = irc.get_service_bot(kicked)
if sbot:
sbot.join(irc, 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)
if sbot and channel in sbot.get_persistent_channels(irc):
sbot.join(irc, channel)
utils.add_hook(handle_kick, 'KICK')
def handle_commands(irc, source, command, args):
@ -142,7 +172,7 @@ def handle_commands(irc, source, command, args):
utils.add_hook(handle_commands, 'PRIVMSG')
# 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?
mydesc = "\x02PyLink\x02 provides extended network services for IRC."
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:
return
if channel not in map(str.lower, irc.serverdata.get('channels', [])):
world.services['pylink'].extra_channels[irc.name].discard(channel)
if irc.pseudoclient:
irc.part(irc.pseudoclient.uid, channel, 'Channel delinked.')
world.services['pylink'].dynamic_channels[irc.name].discard(channel)
relay = get_relay(irc, channel)
if relay and channel in irc.channels:
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)
# Don't ever part the main client from any of its autojoin channels.
else:
if user == irc.pseudoclient.uid and channel in \
irc.serverdata.get('channels', []):
# Part and quit all relay clients.
if irc.get_service_bot(user): # ...but ignore service bots
continue
irc.part(user, channel, 'Channel delinked.')
# Don't ever quit it either...
if user != irc.pseudoclient.uid and not irc.users[user].channels:
remoteuser = get_orig_user(irc, user)
del relayusers[remoteuser][irc.name]

View File

@ -13,7 +13,7 @@ import collections
import argparse
from .log import log
from . import world, conf
from . import world, conf, structures
# Load the protocol and plugin packages.
from pylinkirc import protocols, plugins
@ -152,9 +152,10 @@ class ServiceBot():
# spawned.
self.uids = {}
# Track what channels other than those defined in the config
# that the bot should join by default.
self.extra_channels = collections.defaultdict(set)
# Track plugin-defined persistent channels. The bot will leave them if they're empty,
# and rejoin whenever someone else does.
self.dynamic_channels = structures.KeyedDefaultdict(lambda netname:
structures.IRCCaseInsensitiveSet(world.networkobjects[netname]))
# Service description, used in the default help command if one is given.
self.desc = desc
@ -183,9 +184,14 @@ class ServiceBot():
else:
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):
@ -198,10 +204,6 @@ class ServiceBot():
channels = [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.
try:
irc = world.networkobjects[netname]
@ -209,6 +211,10 @@ class ServiceBot():
log.debug('(%s/%s) Skipping join(), IRC object not initialized yet', netname, self.name)
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:
u = self.uids[irc.name]
except KeyError:
@ -221,10 +227,16 @@ class ServiceBot():
for chan in channels:
if irc.is_channel(chan):
if chan in irc.channels and u in irc.channels[chan].users:
log.debug('(%s) Skipping join of services %s to channel %s - it is already present', irc.name, self.name, chan)
if chan in irc.channels:
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
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
irc.proto.sjoin(irc.sid, chan, [(joinmodes, u)])
else:
@ -387,6 +399,15 @@ class ServiceBot():
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
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):
"""
Shows help for the given command.