mirror of
https://github.com/jlu5/PyLink.git
synced 2024-12-24 11:42:51 +01:00
Merge branch 'services-v3' into devel
- Revamp persistent channel registration to be plugin specific, effectively working around relay-services conflicts (closes #265) - New abstraction in ServiceBot: add/remove_persistent_channel() to manage persistent channels independently of explicit joins and parts - Introduce ServiceBot.part(), which sends a part "request" that succeeds only if a channel is not marked persistent by any plugin - Replace ServiceBot.join() calls with the new registration mechanism, which queues joins instead of dropping them if the service client is not yet ready (closes #596)
This commit is contained in:
commit
c71e9a6410
@ -98,7 +98,7 @@ def handle_endburst(irc, source, command, args):
|
||||
for name, sbot in world.services.items():
|
||||
spawn_service(irc, source, command, {'name': name})
|
||||
|
||||
utils.add_hook(handle_endburst, 'ENDBURST')
|
||||
utils.add_hook(handle_endburst, 'ENDBURST', priority=500)
|
||||
|
||||
def handle_kill(irc, source, command, args):
|
||||
"""Handle KILLs to PyLink service bots, respawning them as needed."""
|
||||
@ -107,26 +107,17 @@ def handle_kill(irc, source, command, args):
|
||||
irc.pseudoclient = None
|
||||
userdata = args.get('userdata')
|
||||
sbot = irc.get_service_bot(target)
|
||||
|
||||
servicename = None
|
||||
channels = []
|
||||
|
||||
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
|
||||
if userdata and hasattr(userdata, 'service'): # Look for the target's service name attribute
|
||||
servicename = userdata.service
|
||||
elif sbot: # Or their service bot instance
|
||||
servicename = sbot.name
|
||||
channels = irc.users[target].channels
|
||||
if 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'))
|
||||
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')
|
||||
|
||||
def handle_join(irc, source, command, args):
|
||||
@ -173,7 +164,7 @@ def handle_kick(irc, source, command, args):
|
||||
if not _services_dynamic_part(irc, channel):
|
||||
kicked = args['target']
|
||||
sbot = irc.get_service_bot(kicked)
|
||||
if sbot:
|
||||
if sbot and channel in sbot.get_persistent_channels(irc):
|
||||
sbot.join(irc, channel)
|
||||
utils.add_hook(handle_kick, 'KICK')
|
||||
|
||||
|
@ -26,6 +26,20 @@ db = datastore.store
|
||||
default_permissions = {"$ircop": ['automode.manage.relay_owned', 'automode.sync.relay_owned',
|
||||
'automode.list']}
|
||||
|
||||
def _join_db_channels(irc):
|
||||
"""
|
||||
Joins the Automode service client to channels on the current network in its DB.
|
||||
"""
|
||||
if not irc.connected.is_set():
|
||||
log.debug('(%s) _join_db_channels: aborting, network not ready yet', irc.name)
|
||||
return
|
||||
|
||||
for entry in db:
|
||||
netname, channel = entry.split('#', 1)
|
||||
channel = '#' + channel
|
||||
if netname == irc.name:
|
||||
modebot.add_persistent_channel(irc, 'automode', channel)
|
||||
|
||||
def main(irc=None):
|
||||
"""Main function, called during plugin loading at start."""
|
||||
|
||||
@ -35,12 +49,9 @@ def main(irc=None):
|
||||
# Register our permissions.
|
||||
permissions.add_default_permissions(default_permissions)
|
||||
|
||||
# Queue joins to all channels where Automode has entries.
|
||||
for entry in db:
|
||||
netname, channel = entry.split('#', 1)
|
||||
channel = '#' + channel
|
||||
log.debug('automode: auto-joining %s on %s', channel, netname)
|
||||
modebot.join(netname, channel, ignore_empty=True)
|
||||
if irc: # After initial startup
|
||||
for ircobj in world.networkobjects.values():
|
||||
_join_db_channels(ircobj)
|
||||
|
||||
def die(irc=None):
|
||||
"""Saves the Automode database and quit."""
|
||||
@ -120,6 +131,12 @@ def match(irc, channel, uids=None):
|
||||
irc.call_hooks([modebot_uid, 'AUTOMODE_MODE',
|
||||
{'target': channel, 'modes': outgoing_modes, 'parse_as': 'MODE'}])
|
||||
|
||||
def handle_endburst(irc, source, command, args):
|
||||
"""ENDBURST hook handler - used to join the Automode service to channels where it has entries."""
|
||||
if source == irc.uplink:
|
||||
_join_db_channels(irc)
|
||||
utils.add_hook(handle_endburst, 'ENDBURST')
|
||||
|
||||
def handle_join(irc, source, command, args):
|
||||
"""
|
||||
Automode JOIN listener. This sets modes accordingly if the person joining matches a mask in the
|
||||
@ -213,8 +230,8 @@ def setacc(irc, source, args):
|
||||
log.info('(%s) %s set modes +%s for %s on %s', ircobj.name, irc.get_hostmask(source), modes, mask, channel)
|
||||
reply(irc, "Done. \x02%s\x02 now has modes \x02+%s\x02 in \x02%s\x02." % (mask, modes, channel))
|
||||
|
||||
# Join the Automode bot to the channel if not explicitly told to.
|
||||
modebot.join(ircobj, channel)
|
||||
# Join the Automode bot to the channel persistently.
|
||||
modebot.add_persistent_channel(irc, 'automode', channel)
|
||||
|
||||
modebot.add_cmd(setacc, aliases=('setaccess', 'set'), featured=True)
|
||||
|
||||
@ -248,6 +265,7 @@ def delacc(irc, source, args):
|
||||
if not dbentry:
|
||||
log.debug("Automode: purging empty channel pair %s/%s", ircobj.name, channel)
|
||||
del db[ircobj.name+channel]
|
||||
modebot.remove_persistent_channel(irc, 'automode', channel)
|
||||
|
||||
modebot.add_cmd(delacc, aliases=('delaccess', 'del'), featured=True)
|
||||
|
||||
@ -327,6 +345,7 @@ def clearacc(irc, source, args):
|
||||
del db[ircobj.name+channel]
|
||||
log.info('(%s) %s cleared modes on %s', ircobj.name, irc.get_hostmask(source), channel)
|
||||
reply(irc, "Done. Removed all Automode access entries for \x02%s\x02." % channel)
|
||||
modebot.remove_persistent_channel(irc, 'automode', channel)
|
||||
else:
|
||||
error(irc, "No Automode access entries exist for \x02%s\x02." % channel)
|
||||
|
||||
|
@ -12,6 +12,8 @@ from pylinkirc.coremods import permissions
|
||||
# Sets the timeout to wait for as individual servers / the PyLink daemon to start up.
|
||||
TCONDITION_TIMEOUT = 2
|
||||
|
||||
CHANNEL_DELINKED_PARTMSG = 'Channel delinked.'
|
||||
|
||||
### GLOBAL (statekeeping) VARIABLES
|
||||
relayusers = defaultdict(dict)
|
||||
relayservers = defaultdict(dict)
|
||||
@ -509,16 +511,6 @@ def initialize_channel(irc, channel):
|
||||
# Join their (remote) users and set their modes, if applicable.
|
||||
if remotechan in remoteirc.channels:
|
||||
rc = remoteirc.channels[remotechan]
|
||||
'''
|
||||
if not hasattr(rc, '_relay_initial_burst'):
|
||||
rc._relay_initial_burst = threading.Event()
|
||||
|
||||
if rc._relay_initial_burst.is_set():
|
||||
log.debug('(%s) relay.initialize_channel: skipping inbound burst from %s/%s => %s/%s '
|
||||
'as it has already been bursted', irc.name, remoteirc.name, remotechan, irc.name, channel)
|
||||
continue
|
||||
rc._relay_initial_burst.set()
|
||||
'''
|
||||
relay_joins(remoteirc, remotechan, rc.users, rc.ts, targetirc=irc)
|
||||
|
||||
# Only update the topic if it's different from what we already have,
|
||||
@ -532,14 +524,17 @@ def initialize_channel(irc, channel):
|
||||
relay_joins(irc, channel, c.users, c.ts)
|
||||
|
||||
if 'pylink' in world.services:
|
||||
world.services['pylink'].join(irc, channel)
|
||||
world.services['pylink'].add_persistent_channel(irc, 'relay', channel)
|
||||
|
||||
def remove_channel(irc, channel):
|
||||
"""Destroys a relay channel by parting all of its users."""
|
||||
if irc is None:
|
||||
return
|
||||
|
||||
world.services['pylink'].dynamic_channels[irc.name].discard(channel)
|
||||
try:
|
||||
world.services['pylink'].remove_persistent_channel(irc, 'relay', channel, part_reason=CHANNEL_DELINKED_PARTMSG)
|
||||
except KeyError:
|
||||
log.warning('(%s) relay: failed to remove persistent channel %r on delink', irc.name, channel, exc_info=True)
|
||||
|
||||
relay = get_relay(irc, channel)
|
||||
if relay and channel in irc.channels:
|
||||
@ -549,13 +544,21 @@ def remove_channel(irc, channel):
|
||||
relay_part(irc, channel, user)
|
||||
else:
|
||||
# Part and quit all relay clients.
|
||||
if irc.get_service_bot(user): # ...but ignore service bots
|
||||
continue
|
||||
irc.part(user, channel, 'Channel delinked.')
|
||||
if user != irc.pseudoclient.uid and not irc.users[user].channels:
|
||||
remoteuser = get_orig_user(irc, user)
|
||||
del relayusers[remoteuser][irc.name]
|
||||
irc.quit(user, 'Left all shared channels.')
|
||||
# Service bots are treated differently: they have plugin-defined persistent
|
||||
# channels, so we can request a part and it will apply if no other plugins
|
||||
# have the channel registered.
|
||||
sbot = irc.get_service_bot(user)
|
||||
if sbot:
|
||||
try:
|
||||
sbot.remove_persistent_channel(irc, 'relay', channel, part_reason=CHANNEL_DELINKED_PARTMSG)
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
irc.part(user, channel, CHANNEL_DELINKED_PARTMSG)
|
||||
if user != irc.pseudoclient.uid and not irc.users[user].channels:
|
||||
remoteuser = get_orig_user(irc, user)
|
||||
del relayusers[remoteuser][irc.name]
|
||||
irc.quit(user, 'Left all shared channels.')
|
||||
|
||||
def check_claim(irc, channel, sender, chanobj=None):
|
||||
"""
|
||||
@ -715,6 +718,11 @@ def relay_joins(irc, channel, users, ts, targetirc=None, **kwargs):
|
||||
if not u:
|
||||
continue
|
||||
|
||||
# Join the service bot on the remote channel persistently.
|
||||
rsbot = remoteirc.get_service_bot(u)
|
||||
if rsbot:
|
||||
rsbot.add_persistent_channel(irc, 'relay', channel, try_join=False)
|
||||
|
||||
if (remotechan not in remoteirc.channels) or u not in remoteirc.channels[remotechan].users:
|
||||
# Note: only join users if they aren't already joined. This prevents op floods
|
||||
# on charybdis from repeated SJOINs sent for one user.
|
||||
@ -734,6 +742,7 @@ def relay_joins(irc, channel, users, ts, targetirc=None, **kwargs):
|
||||
|
||||
# proto.sjoin() takes its users as a list of (prefix mode characters, UID) pairs.
|
||||
userpair = (prefixes, u)
|
||||
|
||||
queued_users.append(userpair)
|
||||
|
||||
if queued_users:
|
||||
@ -806,8 +815,16 @@ def relay_part(irc, *args, **kwargs):
|
||||
# user doesn't exist, just do nothing.
|
||||
return
|
||||
|
||||
# Remove any persistent channel entries from the remote end.
|
||||
rsbot = remoteirc.get_service_bot(remoteuser)
|
||||
if rsbot:
|
||||
try:
|
||||
sbot.remove_persistent_channel(irc, 'relay', channel, try_part=False)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Part the relay client with the channel delinked message.
|
||||
remoteirc.part(remoteuser, remotechan, 'Channel delinked.')
|
||||
remoteirc.part(remoteuser, remotechan, CHANNEL_DELINKED_PARTMSG)
|
||||
|
||||
# If the relay client no longer has any channels, quit them to prevent inflating /lusers.
|
||||
if is_relay_client(remoteirc, remoteuser) and not remoteirc.users[remoteuser].channels:
|
||||
|
134
utils.py
134
utils.py
@ -154,8 +154,9 @@ class ServiceBot():
|
||||
|
||||
# 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]))
|
||||
# This is stored as a nested dictionary:
|
||||
# {"plugin1": {"net1": IRCCaseInsensitiveSet({"#a", "#b"}), "net2": ...}, ...}
|
||||
self.dynamic_channels = {}
|
||||
|
||||
# Service description, used in the default help command if one is given.
|
||||
self.desc = desc
|
||||
@ -186,65 +187,80 @@ class ServiceBot():
|
||||
|
||||
def join(self, irc, channels, ignore_empty=True):
|
||||
"""
|
||||
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).
|
||||
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.
|
||||
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.
|
||||
"""
|
||||
uid = self.uids.get(irc.name)
|
||||
if uid is None:
|
||||
return
|
||||
|
||||
if isinstance(irc, str):
|
||||
netname = irc
|
||||
else:
|
||||
netname = irc.name
|
||||
|
||||
# Ensure type safety: pluralize strings if only one channel was given, then convert to set.
|
||||
if isinstance(channels, str):
|
||||
channels = [channels]
|
||||
channels = set(channels)
|
||||
|
||||
# If the network was given as a string, look up the Irc object here.
|
||||
try:
|
||||
irc = world.networkobjects[netname]
|
||||
except KeyError:
|
||||
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:
|
||||
log.debug('(%s/%s) Skipping join(), UID not initialized yet', irc.name, self.name)
|
||||
return
|
||||
|
||||
# Specify modes to join the services bot with.
|
||||
joinmodes = irc.serverdata.get("%s_joinmodes" % self.name) or conf.conf.get(self.name, {}).get('joinmodes') or ''
|
||||
joinmodes = irc.get_service_option(self.name, 'joinmodes', default='')
|
||||
joinmodes = ''.join([m for m in joinmodes if m in irc.prefixmodes])
|
||||
|
||||
for chan in channels:
|
||||
if irc.is_channel(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)
|
||||
for channel in channels:
|
||||
if irc.is_channel(channel):
|
||||
if channel in irc.channels:
|
||||
if uid in irc.channels[channel].users:
|
||||
log.debug('(%s/%s) Skipping join to %r - we are already present', irc.name, self.name, channel)
|
||||
continue
|
||||
elif ignore_empty:
|
||||
log.debug('(%s) Skipping joining service %r to empty channel %r', irc.name, self.name, chan)
|
||||
log.debug('(%s/%s) Skipping joining empty channel %r', irc.name, self.name, channel)
|
||||
continue
|
||||
|
||||
log.debug('(%s) Joining services %s to channel %s with modes %r', irc.name, self.name, chan, joinmodes)
|
||||
log.debug('(%s/%s) Joining channel %s with modes %r', irc.name, self.name, channel, joinmodes)
|
||||
|
||||
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, channel, [(joinmodes, uid)])
|
||||
else:
|
||||
irc.proto.join(u, chan)
|
||||
irc.proto.join(uid, channel)
|
||||
|
||||
irc.call_hooks([irc.sid, 'PYLINK_SERVICE_JOIN', {'channel': chan, 'users': [u]}])
|
||||
irc.call_hooks([irc.sid, 'PYLINK_SERVICE_JOIN', {'channel': channel, 'users': [uid]}])
|
||||
else:
|
||||
log.warning('(%s) Ignoring invalid autojoin channel %r.', irc.name, chan)
|
||||
log.warning('(%s/%s) Ignoring invalid channel %r', irc.name, self.name, channel)
|
||||
|
||||
def part(self, irc, channels, reason=''):
|
||||
"""
|
||||
Parts the given service bot from the given channel(s) if no plugins
|
||||
still register it as a persistent dynamic channel.
|
||||
|
||||
"channels" can be an iterable of channel names or the name of a single
|
||||
channel (str).
|
||||
"""
|
||||
uid = self.uids.get(irc.name)
|
||||
if uid is None:
|
||||
return
|
||||
|
||||
if isinstance(channels, str):
|
||||
channels = [channels]
|
||||
|
||||
to_part = []
|
||||
persistent_channels = self.get_persistent_channels(irc)
|
||||
for channel in channels:
|
||||
if channel in irc.channels and uid in irc.channels[channel].users:
|
||||
if channel in persistent_channels:
|
||||
log.debug('(%s/%s) Not parting %r because it is registered '
|
||||
'as a dynamic channel: %r', irc.name, self.name, channel,
|
||||
persistent_channels)
|
||||
continue
|
||||
to_part.append(channel)
|
||||
irc.part(uid, channel, reason)
|
||||
else:
|
||||
log.debug('(%s/%s) Ignoring part to %r, we are not there', irc.name, self.name, channel)
|
||||
continue
|
||||
|
||||
irc.call_hooks([uid, 'PYLINK_SERVICE_PART', {'channels': to_part, 'text': reason}])
|
||||
|
||||
def reply(self, irc, text, notice=None, private=None):
|
||||
"""Replies to a message as the service in question."""
|
||||
@ -399,11 +415,45 @@ 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):
|
||||
def add_persistent_channel(self, irc, namespace, channel, try_join=True):
|
||||
"""
|
||||
Returns a set of persistent channels for the IRC network.
|
||||
Adds a persistent channel to the service bot on the given network and namespace.
|
||||
"""
|
||||
channels = self.dynamic_channels[irc.name].copy()
|
||||
namespace = self.dynamic_channels.setdefault(namespace, {})
|
||||
chanlist = namespace.setdefault(irc.name, structures.IRCCaseInsensitiveSet(irc))
|
||||
chanlist.add(channel)
|
||||
|
||||
if try_join:
|
||||
self.join(irc, [channel])
|
||||
|
||||
def remove_persistent_channel(self, irc, namespace, channel, try_part=True, part_reason=''):
|
||||
"""
|
||||
Removes a persistent channel from the service bot on the given network and namespace.
|
||||
"""
|
||||
chanlist = self.dynamic_channels[namespace][irc.name].remove(channel)
|
||||
|
||||
if try_part and irc.connected.is_set():
|
||||
self.part(irc, [channel], reason=part_reason)
|
||||
|
||||
def get_persistent_channels(self, irc, namespace=None):
|
||||
"""
|
||||
Returns a set of persistent channels for the IRC network, optionally filtering
|
||||
by namespace is one is given.
|
||||
"""
|
||||
channels = structures.IRCCaseInsensitiveSet(irc)
|
||||
if namespace:
|
||||
chanlist = self.dynamic_channels.get(namespace, {}).get(irc.name, set())
|
||||
log.debug('(%s/%s) get_persistent_channels: adding channels '
|
||||
'%r from namespace %r (single)', irc.name, self.name,
|
||||
chanlist, namespace)
|
||||
channels |= chanlist
|
||||
else:
|
||||
for dch_namespace, dch_data in self.dynamic_channels.items():
|
||||
chanlist = dch_data.get(irc.name, set())
|
||||
log.debug('(%s/%s) get_persistent_channels: adding channels '
|
||||
'%r from namespace %r', irc.name, self.name,
|
||||
chanlist, dch_namespace)
|
||||
channels |= chanlist
|
||||
channels |= set(irc.serverdata.get(self.name+'_channels', []))
|
||||
channels |= set(irc.serverdata.get('channels', []))
|
||||
return channels
|
||||
|
Loading…
Reference in New Issue
Block a user