2016-07-08 06:06:28 +02:00
|
|
|
"""
|
|
|
|
automode.py - Provide simple channel ACL management by giving prefix modes to users matching
|
|
|
|
hostmasks or exttargets.
|
|
|
|
"""
|
|
|
|
import collections
|
|
|
|
import threading
|
|
|
|
import json
|
|
|
|
|
2016-11-10 07:47:22 +01:00
|
|
|
from pylinkirc import utils, conf, world, structures
|
2016-07-08 06:06:28 +02:00
|
|
|
from pylinkirc.log import log
|
2016-08-25 09:45:28 +02:00
|
|
|
from pylinkirc.coremods import permissions
|
2016-07-08 06:06:28 +02:00
|
|
|
|
|
|
|
mydesc = ("The \x02Automode\x02 plugin provides simple channel ACL management by giving prefix modes "
|
|
|
|
"to users matching hostmasks or exttargets.")
|
|
|
|
|
|
|
|
# Register ourselves as a service.
|
2016-07-24 06:46:29 +02:00
|
|
|
modebot = world.services.get("automode", utils.registerService("automode", desc=mydesc))
|
2016-07-08 06:06:28 +02:00
|
|
|
reply = modebot.reply
|
|
|
|
|
|
|
|
# Databasing variables.
|
|
|
|
dbname = utils.getDatabaseName('automode')
|
2016-11-10 07:52:46 +01:00
|
|
|
datastore = structures.JSONDataStore('automode', dbname, default_db=collections.defaultdict(dict))
|
2016-11-10 07:47:22 +01:00
|
|
|
|
|
|
|
db = datastore.store
|
2016-07-08 07:16:21 +02:00
|
|
|
|
2016-08-25 20:41:37 +02:00
|
|
|
# The default set of Automode permissions.
|
2016-09-12 20:26:31 +02:00
|
|
|
default_permissions = {"$ircop": ['automode.manage.relay_owned', 'automode.sync.relay_owned',
|
|
|
|
'automode.list']}
|
2016-08-25 20:41:37 +02:00
|
|
|
|
2016-07-08 06:06:28 +02:00
|
|
|
|
|
|
|
def main(irc=None):
|
|
|
|
"""Main function, called during plugin loading at start."""
|
|
|
|
|
2016-07-08 21:58:01 +02:00
|
|
|
# Load the automode database.
|
2016-11-10 07:47:22 +01:00
|
|
|
datastore.load()
|
2016-07-08 06:06:28 +02:00
|
|
|
|
2016-08-25 20:41:37 +02:00
|
|
|
# Register our permissions.
|
|
|
|
permissions.addDefaultPermissions(default_permissions)
|
|
|
|
|
2016-07-08 21:58:01 +02:00
|
|
|
# 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)
|
2016-09-24 21:33:57 +02:00
|
|
|
modebot.join(netname, channel)
|
2016-07-08 21:58:01 +02:00
|
|
|
|
2016-07-08 06:06:28 +02:00
|
|
|
def die(sourceirc):
|
|
|
|
"""Saves the Automode database and quit."""
|
2016-11-10 07:47:22 +01:00
|
|
|
datastore.die()
|
2016-08-25 20:41:37 +02:00
|
|
|
permissions.removeDefaultPermissions(default_permissions)
|
2016-07-08 07:41:51 +02:00
|
|
|
utils.unregisterService('automode')
|
|
|
|
|
2016-08-25 09:45:28 +02:00
|
|
|
def checkAccess(irc, uid, channel, command):
|
|
|
|
"""Checks the caller's access to Automode."""
|
|
|
|
# Automode defines the following permissions, where <command> is either "manage", "list",
|
2016-11-12 21:20:25 +01:00
|
|
|
# "sync", "clear", "remotemanage", "remotelist", "remotesync", "remoteclear":
|
2016-08-25 09:45:28 +02:00
|
|
|
# - automode.<command> OR automode.<command>.*: ability to <command> automode on all channels.
|
|
|
|
# - automode.<command>.relay_owned: ability to <command> automode on channels owned via Relay.
|
|
|
|
# If Relay isn't loaded, this permission check FAILS.
|
|
|
|
# - automode.<command>.#channel: ability to <command> automode on the given channel.
|
|
|
|
# - automode.savedb: ability to save the automode DB.
|
|
|
|
log.debug('(%s) Automode: checking access for %s/%s for %s capability on %s', irc.name, uid,
|
|
|
|
irc.getHostmask(uid), command, channel)
|
|
|
|
|
|
|
|
baseperm = 'automode.%s' % command
|
|
|
|
try:
|
|
|
|
# First, check the catch all and channel permissions.
|
2016-08-25 20:41:37 +02:00
|
|
|
perms = [baseperm, baseperm+'.*', '%s.%s' % (baseperm, channel)]
|
|
|
|
return permissions.checkPermissions(irc, uid, perms)
|
2016-08-25 09:45:28 +02:00
|
|
|
except utils.NotAuthorizedError:
|
2016-11-12 21:20:25 +01:00
|
|
|
if not command.startswith('remote'):
|
|
|
|
# Relay-based ACL checking only works with local calls.
|
|
|
|
log.debug('(%s) Automode: falling back to automode.%s.relay_owned', irc.name, command)
|
|
|
|
permissions.checkPermissions(irc, uid, [baseperm+'.relay_owned'], also_show=perms)
|
|
|
|
|
|
|
|
relay = world.plugins.get('relay')
|
|
|
|
if relay is None:
|
|
|
|
raise utils.NotAuthorizedError("You are not authorized to use Automode when Relay is "
|
|
|
|
"disabled. You are missing one of the following "
|
|
|
|
"permissions: %s or %s.%s" % (baseperm, baseperm, channel))
|
|
|
|
elif (irc.name, channel) not in relay.db:
|
|
|
|
raise utils.NotAuthorizedError("The network you are on does not own the relay channel %s." % channel)
|
|
|
|
return True
|
|
|
|
raise
|
2016-08-25 09:45:28 +02:00
|
|
|
|
2016-08-25 20:10:55 +02:00
|
|
|
def match(irc, channel, uids=None):
|
|
|
|
"""
|
|
|
|
Automode matcher engine.
|
|
|
|
"""
|
|
|
|
dbentry = db.get(irc.name+channel)
|
|
|
|
if dbentry is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
modebot_uid = modebot.uids.get(irc.name)
|
|
|
|
|
|
|
|
# Check every mask defined in the channel ACL.
|
|
|
|
outgoing_modes = []
|
|
|
|
|
|
|
|
# If UIDs are given, match those. Otherwise, match all users in the given channel.
|
|
|
|
uids = uids or irc.channels[channel].users
|
|
|
|
|
|
|
|
for mask, modes in dbentry.items():
|
|
|
|
for uid in uids:
|
|
|
|
if irc.matchHost(mask, uid):
|
|
|
|
# User matched a mask. Filter the mode list given to only those that are valid
|
|
|
|
# prefix mode characters.
|
|
|
|
outgoing_modes += [('+'+mode, uid) for mode in modes if mode in irc.prefixmodes]
|
|
|
|
log.debug("(%s) automode: Filtered mode list of %s to %s (protocol:%s)",
|
|
|
|
irc.name, modes, outgoing_modes, irc.protoname)
|
|
|
|
|
|
|
|
# If the Automode bot is missing, send the mode through the PyLink server.
|
|
|
|
if modebot_uid not in irc.users:
|
|
|
|
modebot_uid = irc.sid
|
|
|
|
|
|
|
|
log.debug("(%s) automode: sending modes from modebot_uid %s",
|
|
|
|
irc.name, modebot_uid)
|
|
|
|
|
|
|
|
irc.proto.mode(modebot_uid, channel, outgoing_modes)
|
|
|
|
|
|
|
|
# Create a hook payload to support plugins like relay.
|
|
|
|
irc.callHooks([modebot_uid, 'AUTOMODE_MODE',
|
|
|
|
{'target': channel, 'modes': outgoing_modes, 'parse_as': 'MODE'}])
|
|
|
|
|
|
|
|
def handle_join(irc, source, command, args):
|
|
|
|
"""
|
|
|
|
Automode JOIN listener. This sets modes accordingly if the person joining matches a mask in the
|
|
|
|
ACL.
|
|
|
|
"""
|
|
|
|
channel = irc.toLower(args['channel'])
|
|
|
|
match(irc, channel, args['users'])
|
|
|
|
|
|
|
|
utils.add_hook(handle_join, 'JOIN')
|
|
|
|
utils.add_hook(handle_join, 'PYLINK_RELAY_JOIN') # Handle the relay version of join
|
|
|
|
utils.add_hook(handle_join, 'PYLINK_SERVICE_JOIN') # And the version for service bots
|
|
|
|
|
|
|
|
def handle_services_login(irc, source, command, args):
|
|
|
|
"""
|
|
|
|
Handles services login change, to trigger Automode matching.
|
|
|
|
"""
|
|
|
|
for channel in irc.users[source].channels:
|
|
|
|
# Look at all the users' channels for any possible changes.
|
|
|
|
match(irc, channel, [source])
|
|
|
|
|
|
|
|
utils.add_hook(handle_services_login, 'CLIENT_SERVICES_LOGIN')
|
|
|
|
utils.add_hook(handle_services_login, 'PYLINK_RELAY_SERVICES_LOGIN')
|
|
|
|
|
2016-11-12 21:20:25 +01:00
|
|
|
def getChannelPair(irc, source, chanpair, perm=None):
|
|
|
|
"""
|
|
|
|
Fetches the network and channel given a channel pair,
|
|
|
|
also optionally checking the caller's permissions.
|
|
|
|
"""
|
|
|
|
log.debug('(%s) Looking up chanpair %s', irc.name, chanpair)
|
|
|
|
try:
|
|
|
|
network, channel = chanpair.split('#')
|
|
|
|
except ValueError:
|
|
|
|
raise ValueError("Invalid channel pair %r" % chanpair)
|
|
|
|
channel = '#' + channel
|
|
|
|
channel = irc.toLower(channel)
|
|
|
|
|
|
|
|
assert utils.isChannel(channel), "Invalid channel name %s." % channel
|
|
|
|
|
|
|
|
if network:
|
|
|
|
ircobj = world.networkobjects.get(network)
|
|
|
|
else:
|
|
|
|
ircobj = irc
|
|
|
|
|
|
|
|
assert ircobj, "Unknown network %s" % network
|
|
|
|
|
|
|
|
if perm is not None:
|
|
|
|
# Only check for permissions if we're told to and the irc object exists.
|
|
|
|
if ircobj.name != irc.name:
|
|
|
|
perm = 'remote' + perm
|
|
|
|
|
|
|
|
checkAccess(irc, source, channel, perm)
|
|
|
|
|
|
|
|
return (ircobj, channel)
|
|
|
|
|
2016-07-08 06:06:28 +02:00
|
|
|
def setacc(irc, source, args):
|
2016-11-12 21:20:25 +01:00
|
|
|
"""<channel/chanpair> <mask> <mode list>
|
2016-07-08 06:06:28 +02:00
|
|
|
|
|
|
|
Assigns the given prefix mode characters to the given mask for the channel given. Extended targets are supported for masks - use this to your advantage!
|
|
|
|
|
2016-11-12 21:20:25 +01:00
|
|
|
Channel pairs are also supported (for operations on remote channels), using the form "network#channel".
|
|
|
|
|
2016-07-08 06:06:28 +02:00
|
|
|
Examples:
|
|
|
|
SET #channel *!*@localhost ohv
|
|
|
|
SET #channel $account v
|
2016-11-12 21:20:25 +01:00
|
|
|
SET othernet#channel $oper:Network?Administrator qo
|
2016-07-08 06:06:28 +02:00
|
|
|
SET #staffchan $channel:#mainchan:op o
|
|
|
|
"""
|
2016-08-25 09:45:28 +02:00
|
|
|
|
2016-07-08 06:06:28 +02:00
|
|
|
try:
|
2016-11-12 21:20:25 +01:00
|
|
|
chanpair, mask, modes = args
|
2016-07-08 06:06:28 +02:00
|
|
|
except ValueError:
|
|
|
|
reply(irc, "Error: Invalid arguments given. Needs 3: channel, mask, mode list.")
|
|
|
|
return
|
|
|
|
else:
|
2016-11-12 21:20:25 +01:00
|
|
|
ircobj, channel = getChannelPair(irc, source, chanpair, perm='manage')
|
2016-08-25 09:45:28 +02:00
|
|
|
|
2016-07-08 06:06:28 +02:00
|
|
|
# Database entries for any network+channel pair are automatically created using
|
|
|
|
# defaultdict. Note: string keys are used here instead of tuples so they can be
|
|
|
|
# exported easily as JSON.
|
2016-11-12 21:20:25 +01:00
|
|
|
dbentry = db[ircobj.name+channel]
|
2016-07-08 06:06:28 +02:00
|
|
|
|
|
|
|
# Otherwise, update the modes as is.
|
|
|
|
dbentry[mask] = modes
|
2016-11-12 21:20:25 +01:00
|
|
|
log.info('(%s) %s set modes +%s for %s on %s', ircobj.name, irc.getHostmask(source), modes, mask, channel)
|
2016-07-08 06:06:28 +02:00
|
|
|
reply(irc, "Done. \x02%s\x02 now has modes \x02%s\x02 in \x02%s\x02." % (mask, modes, channel))
|
|
|
|
|
2016-09-01 07:23:44 +02:00
|
|
|
# Join the Automode bot to the channel if not explicitly told to.
|
2016-11-12 21:20:25 +01:00
|
|
|
modebot.join(ircobj, channel)
|
2016-09-01 07:23:44 +02:00
|
|
|
|
2016-07-08 06:06:28 +02:00
|
|
|
modebot.add_cmd(setacc, 'setaccess')
|
|
|
|
modebot.add_cmd(setacc, 'set')
|
|
|
|
modebot.add_cmd(setacc, featured=True)
|
|
|
|
|
|
|
|
def delacc(irc, source, args):
|
2016-11-12 21:20:25 +01:00
|
|
|
"""<channel/chanpair> <mask>
|
2016-07-08 06:06:28 +02:00
|
|
|
|
|
|
|
Removes the Automode entry for the given mask on the given channel, if one exists.
|
|
|
|
"""
|
|
|
|
try:
|
2016-11-12 21:20:25 +01:00
|
|
|
chanpair, mask = args
|
2016-07-08 06:06:28 +02:00
|
|
|
except ValueError:
|
|
|
|
reply(irc, "Error: Invalid arguments given. Needs 2: channel, mask")
|
|
|
|
return
|
2016-11-12 21:20:25 +01:00
|
|
|
else:
|
|
|
|
ircobj, channel = getChannelPair(irc, source, chanpair, perm='manage')
|
2016-07-08 06:06:28 +02:00
|
|
|
|
2016-11-12 21:20:25 +01:00
|
|
|
dbentry = db.get(ircobj.name+channel)
|
2016-07-08 06:06:28 +02:00
|
|
|
|
|
|
|
if dbentry is None:
|
|
|
|
reply(irc, "Error: no Automode access entries exist for \x02%s\x02." % channel)
|
|
|
|
return
|
|
|
|
|
|
|
|
if mask in dbentry:
|
|
|
|
del dbentry[mask]
|
2016-11-12 21:20:25 +01:00
|
|
|
log.info('(%s) %s removed modes for %s on %s', ircobj.name, irc.getHostmask(source), mask, channel)
|
2016-07-08 06:06:28 +02:00
|
|
|
reply(irc, "Done. Removed the Automode access entry for \x02%s\x02 in \x02%s\x02." % (mask, channel))
|
|
|
|
else:
|
|
|
|
reply(irc, "Error: No Automode access entry for \x02%s\x02 exists in \x02%s\x02." % (mask, channel))
|
|
|
|
|
|
|
|
# Remove channels if no more entries are left.
|
|
|
|
if not dbentry:
|
2016-11-12 21:20:25 +01:00
|
|
|
log.debug("Automode: purging empty channel pair %s/%s", ircobj.name, channel)
|
|
|
|
del db[ircobj.name+channel]
|
2016-07-08 06:06:28 +02:00
|
|
|
|
|
|
|
modebot.add_cmd(delacc, 'delaccess')
|
|
|
|
modebot.add_cmd(delacc, 'del')
|
|
|
|
modebot.add_cmd(delacc, featured=True)
|
|
|
|
|
|
|
|
def listacc(irc, source, args):
|
2016-11-12 21:20:25 +01:00
|
|
|
"""<channel/chanpair>
|
2016-07-08 06:06:28 +02:00
|
|
|
|
|
|
|
Lists all Automode entries for the given channel."""
|
|
|
|
try:
|
2016-11-12 21:20:25 +01:00
|
|
|
chanpair = args[0]
|
2016-07-08 06:06:28 +02:00
|
|
|
except IndexError:
|
|
|
|
reply(irc, "Error: Invalid arguments given. Needs 1: channel.")
|
|
|
|
return
|
2016-11-12 21:20:25 +01:00
|
|
|
else:
|
|
|
|
ircobj, channel = getChannelPair(irc, source, chanpair, perm='list')
|
2016-08-25 09:45:28 +02:00
|
|
|
|
2016-11-12 21:20:25 +01:00
|
|
|
dbentry = db.get(ircobj.name+channel)
|
2016-07-08 06:06:28 +02:00
|
|
|
if not dbentry:
|
|
|
|
reply(irc, "Error: No Automode access entries exist for \x02%s\x02." % channel)
|
|
|
|
return
|
|
|
|
|
|
|
|
else:
|
|
|
|
# Iterate over all entries and print them. Do this in private to prevent channel
|
|
|
|
# floods.
|
|
|
|
reply(irc, "Showing Automode entries for \x02%s\x02:" % channel, private=True)
|
|
|
|
for entrynum, entry in enumerate(dbentry.items(), start=1):
|
|
|
|
mask, modes = entry
|
|
|
|
reply(irc, "[%s] \x02%s\x02 has modes +\x02%s\x02" % (entrynum, mask, modes), private=True)
|
2016-07-08 06:23:06 +02:00
|
|
|
reply(irc, "End of Automode entries list.", private=True)
|
2016-11-12 21:20:25 +01:00
|
|
|
|
2016-07-08 06:06:28 +02:00
|
|
|
modebot.add_cmd(listacc, featured=True)
|
|
|
|
modebot.add_cmd(listacc, 'listaccess')
|
|
|
|
|
|
|
|
def save(irc, source, args):
|
|
|
|
"""takes no arguments.
|
|
|
|
|
|
|
|
Saves the Automode database to disk."""
|
2016-08-25 09:45:28 +02:00
|
|
|
permissions.checkPermissions(irc, source, ['automode.savedb'])
|
2016-11-10 07:47:22 +01:00
|
|
|
datastore.save()
|
2016-07-08 06:06:28 +02:00
|
|
|
reply(irc, 'Done.')
|
2016-11-10 07:47:22 +01:00
|
|
|
|
2016-07-08 06:06:28 +02:00
|
|
|
modebot.add_cmd(save)
|
2016-07-08 06:23:06 +02:00
|
|
|
|
2016-07-12 01:29:17 +02:00
|
|
|
def syncacc(irc, source, args):
|
2016-11-12 21:20:25 +01:00
|
|
|
"""<channel/chanpair>
|
2016-07-17 19:52:56 +02:00
|
|
|
|
2016-07-12 01:29:17 +02:00
|
|
|
Syncs Automode access lists to the channel.
|
|
|
|
"""
|
|
|
|
try:
|
2016-11-12 21:20:25 +01:00
|
|
|
chanpair = args[0]
|
2016-07-12 01:29:17 +02:00
|
|
|
except IndexError:
|
|
|
|
reply(irc, "Error: Invalid arguments given. Needs 1: channel.")
|
|
|
|
return
|
2016-11-12 21:20:25 +01:00
|
|
|
else:
|
|
|
|
ircobj, channel = getChannelPair(irc, source, chanpair, perm='sync')
|
2016-07-12 01:29:17 +02:00
|
|
|
|
2016-11-12 21:20:25 +01:00
|
|
|
log.info('(%s) %s synced modes on %s', ircobj.name, irc.getHostmask(source), channel)
|
|
|
|
match(ircobj, channel)
|
2016-07-12 01:29:17 +02:00
|
|
|
|
|
|
|
reply(irc, 'Done.')
|
|
|
|
|
|
|
|
modebot.add_cmd(syncacc, featured=True)
|
|
|
|
modebot.add_cmd(syncacc, 'sync')
|
|
|
|
modebot.add_cmd(syncacc, 'syncaccess')
|
|
|
|
|
|
|
|
def clearacc(irc, source, args):
|
|
|
|
"""<channel>
|
|
|
|
|
|
|
|
Removes all Automode entries for the given channel.
|
|
|
|
"""
|
|
|
|
|
|
|
|
try:
|
2016-11-12 21:20:25 +01:00
|
|
|
chanpair = args[0]
|
2016-07-12 01:29:17 +02:00
|
|
|
except IndexError:
|
|
|
|
reply(irc, "Error: Invalid arguments given. Needs 1: channel.")
|
|
|
|
return
|
2016-11-12 21:20:25 +01:00
|
|
|
else:
|
|
|
|
ircobj, channel = getChannelPair(irc, source, chanpair, perm='clear')
|
2016-07-12 01:29:17 +02:00
|
|
|
|
2016-11-12 21:20:25 +01:00
|
|
|
if db.get(ircobj.name+channel):
|
|
|
|
del db[ircobj.name+channel]
|
|
|
|
log.info('(%s) %s cleared modes on %s', ircobj.name, irc.getHostmask(source), channel)
|
|
|
|
reply(irc, "Done. Removed all Automode access entries for \x02%s\x02." % channel)
|
2016-07-12 01:29:17 +02:00
|
|
|
else:
|
|
|
|
reply(irc, "Error: No Automode access entries exist for \x02%s\x02." % channel)
|
|
|
|
|
|
|
|
modebot.add_cmd(clearacc, 'clearaccess')
|
|
|
|
modebot.add_cmd(clearacc, 'clear')
|
|
|
|
modebot.add_cmd(clearacc, featured=True)
|