mirror of
https://github.com/jlu5/PyLink.git
synced 2024-11-27 21: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:
parent
b46ab844fe
commit
e9fe15bd7d
@ -35,13 +35,6 @@ def main(irc=None):
|
|||||||
# Register our permissions.
|
# Register our permissions.
|
||||||
permissions.add_default_permissions(default_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):
|
def die(irc=None):
|
||||||
"""Saves the Automode database and quit."""
|
"""Saves the Automode database and quit."""
|
||||||
datastore.die()
|
datastore.die()
|
||||||
@ -120,6 +113,19 @@ def match(irc, channel, uids=None):
|
|||||||
irc.call_hooks([modebot_uid, 'AUTOMODE_MODE',
|
irc.call_hooks([modebot_uid, 'AUTOMODE_MODE',
|
||||||
{'target': channel, 'modes': outgoing_modes, 'parse_as': '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):
|
def handle_join(irc, source, command, args):
|
||||||
"""
|
"""
|
||||||
Automode JOIN listener. This sets modes accordingly if the person joining matches a mask in the
|
Automode JOIN listener. This sets modes accordingly if the person joining matches a mask in the
|
||||||
|
@ -532,14 +532,14 @@ def initialize_channel(irc, channel):
|
|||||||
relay_joins(irc, channel, c.users, c.ts)
|
relay_joins(irc, channel, c.users, c.ts)
|
||||||
|
|
||||||
if 'pylink' in world.services:
|
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):
|
def remove_channel(irc, channel):
|
||||||
"""Destroys a relay channel by parting all of its users."""
|
"""Destroys a relay channel by parting all of its users."""
|
||||||
if irc is None:
|
if irc is None:
|
||||||
return
|
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)
|
relay = get_relay(irc, channel)
|
||||||
if relay and channel in irc.channels:
|
if relay and channel in irc.channels:
|
||||||
@ -549,8 +549,13 @@ def remove_channel(irc, channel):
|
|||||||
relay_part(irc, channel, user)
|
relay_part(irc, channel, user)
|
||||||
else:
|
else:
|
||||||
# Part and quit all relay clients.
|
# Part and quit all relay clients.
|
||||||
if irc.get_service_bot(user): # ...but ignore service bots
|
# Service bots are treated differently: they have plugin-defined persistent
|
||||||
continue
|
# 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.')
|
irc.part(user, channel, 'Channel delinked.')
|
||||||
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)
|
||||||
@ -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.
|
# proto.sjoin() takes its users as a list of (prefix mode characters, UID) pairs.
|
||||||
userpair = (prefixes, u)
|
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)
|
queued_users.append(userpair)
|
||||||
|
|
||||||
if queued_users:
|
if queued_users:
|
||||||
@ -806,6 +817,14 @@ def relay_part(irc, *args, **kwargs):
|
|||||||
# user doesn't exist, just do nothing.
|
# user doesn't exist, just do nothing.
|
||||||
return
|
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.
|
# Part the relay client with the channel delinked message.
|
||||||
remoteirc.part(remoteuser, remotechan, 'Channel delinked.')
|
remoteirc.part(remoteuser, remotechan, 'Channel delinked.')
|
||||||
|
|
||||||
|
147
utils.py
147
utils.py
@ -154,8 +154,9 @@ class ServiceBot():
|
|||||||
|
|
||||||
# Track plugin-defined persistent channels. The bot will leave them if they're empty,
|
# Track plugin-defined persistent channels. The bot will leave them if they're empty,
|
||||||
# and rejoin whenever someone else does.
|
# and rejoin whenever someone else does.
|
||||||
self.dynamic_channels = structures.KeyedDefaultdict(lambda netname:
|
# This is stored as a nested dictionary:
|
||||||
structures.IRCCaseInsensitiveSet(world.networkobjects[netname]))
|
# {"plugin1": {"net1": IRCCaseInsensitiveSet({"#a", "#b"}), "net2": ...}, ...}
|
||||||
|
self.dynamic_channels = {}
|
||||||
|
|
||||||
# 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
|
||||||
@ -186,65 +187,93 @@ class ServiceBot():
|
|||||||
|
|
||||||
def join(self, irc, channels, ignore_empty=True):
|
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
|
Joins the given service bot to the given channel(s). "channels" can be
|
||||||
or the name of a single channel (str).
|
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
|
The ignore_empty option sets whether we should skip joining empty
|
||||||
later when we see someone else join. This is option is disabled on networks where we cannot
|
channels and join them later when we see someone else join. This is
|
||||||
monitor channel state.
|
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):
|
if isinstance(channels, str):
|
||||||
channels = [channels]
|
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'):
|
if irc.has_cap('visible-state-only'):
|
||||||
# Disable dynamic channel joining on networks where we can't monitor channels for joins.
|
# Disable dynamic channel joining on networks where we can't monitor channels for joins.
|
||||||
ignore_empty = False
|
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.
|
# 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])
|
joinmodes = ''.join([m for m in joinmodes if m in irc.prefixmodes])
|
||||||
|
|
||||||
for chan in channels:
|
for channel in channels:
|
||||||
if irc.is_channel(chan):
|
if irc.is_channel(channel):
|
||||||
if chan in irc.channels:
|
if channel in irc.channels:
|
||||||
if u in irc.channels[chan].users:
|
if uid in irc.channels[channel].users:
|
||||||
log.debug('(%s) Skipping join of service %r to channel %r - it is already present', irc.name, self.name, chan)
|
log.debug('(%s/%s) Skipping join to %r - we are already present', irc.name, self.name, channel)
|
||||||
continue
|
continue
|
||||||
elif ignore_empty:
|
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
|
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
|
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:
|
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:
|
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):
|
def reply(self, irc, text, notice=None, private=None):
|
||||||
"""Replies to a message as the service in question."""
|
"""Replies to a message as the service in question."""
|
||||||
@ -399,11 +428,45 @@ 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):
|
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(self.name+'_channels', []))
|
||||||
channels |= set(irc.serverdata.get('channels', []))
|
channels |= set(irc.serverdata.get('channels', []))
|
||||||
return channels
|
return channels
|
||||||
|
Loading…
Reference in New Issue
Block a user