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

[WIP] Further revise the persistent channels implementation

- Make dynamic_channels per plugin as well as per network to work around relay-service conflicts (#265)
- Introduce ServiceBot.(add|remove)_persistent_channel() to add/remove persistent channels and optionally join/part them
- Introduce ServiceBot.part(), which checks remaining persistent channels list and parts a channel only if it is still not marked persistent
- Refactor automode to autojoin channels on ENDBURST instead of plugin load
- Refactor relay to manage persistent channels on join/part/(de)init, both locally (namespace 'relay_local') and remotely (namespace 'relay_remote')
This commit is contained in:
James Lu 2018-05-04 22:37:25 -07:00
parent b46ab844fe
commit e9fe15bd7d
3 changed files with 146 additions and 58 deletions

View File

@ -35,13 +35,6 @@ 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)
def die(irc=None):
"""Saves the Automode database and quit."""
datastore.die()
@ -120,6 +113,19 @@ 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:
return
for entry in db:
netname, channel = entry.split('#', 1)
channel = '#' + channel
if netname == irc.name:
modebot.add_persistent_channel(irc, 'automode', channel)
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

View File

@ -532,14 +532,14 @@ 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_local', 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)
world.services['pylink'].remove_persistent_channel(channel, 'relay_local', try_part=False)
relay = get_relay(irc, channel)
if relay and channel in irc.channels:
@ -549,13 +549,18 @@ 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:
sbot.part(irc, channel)
else:
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.')
def check_claim(irc, channel, sender, chanobj=None):
"""
@ -734,6 +739,12 @@ 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)
# Join the service bot on the remote channel persistently.
rsbot = remoteirc.get_service_bot(u)
if rsbot:
rsbot.add_persistent_channel(irc, 'relay_remote', channel, try_join=False)
queued_users.append(userpair)
if queued_users:
@ -806,6 +817,14 @@ 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(u)
if rsbot:
try:
sbot.remove_persistent_channel(irc, 'relay_remote', channel, try_part=False)
except KeyError:
pass
# Part the relay client with the channel delinked message.
remoteirc.part(remoteuser, remotechan, 'Channel delinked.')

147
utils.py
View File

@ -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,93 @@ 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):
"""
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:
for dch_namespace, dch_data in self.dynamic_channels.items():
if irc.name in dch_data and channel in dch_data[irc.name]:
log.debug('(%s/%s) Not parting %r because namespace %r still registers it '
'as a dynamic channel.', irc.name, self.name, channel,
dch_namespace)
break
else:
to_part.append(channel)
irc.part(uid, channel, '') # TODO: configurable part message?
'''
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, '') # TODO: configurable part message?
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': ''}])
def reply(self, irc, text, notice=None, private=None):
"""Replies to a message as the service in question."""
@ -399,11 +428,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):
"""
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])
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