2015-04-25 08:00:01 +02:00
|
|
|
# commands.py: base PyLink commands
|
2018-04-12 19:56:47 +02:00
|
|
|
import time
|
2015-05-31 07:35:00 +02:00
|
|
|
|
2019-06-26 22:46:04 +02:00
|
|
|
from pylinkirc import conf, utils, __version__, world, real_version
|
2016-06-21 03:18:54 +02:00
|
|
|
from pylinkirc.log import log
|
2016-12-10 07:12:54 +01:00
|
|
|
from pylinkirc.coremods import permissions
|
2015-04-25 08:00:01 +02:00
|
|
|
|
2017-01-23 01:23:30 +01:00
|
|
|
from pylinkirc.coremods.login import pwd_context
|
|
|
|
|
2019-06-26 22:46:04 +02:00
|
|
|
default_permissions = {"*!*@*": ['commands.status', 'commands.showuser', 'commands.showchan', 'commands.shownet']}
|
2016-12-17 03:31:19 +01:00
|
|
|
|
|
|
|
def main(irc=None):
|
|
|
|
"""Commands plugin main function, called on plugin load."""
|
|
|
|
# Register our permissions.
|
2017-08-02 16:24:23 +02:00
|
|
|
permissions.add_default_permissions(default_permissions)
|
2016-12-17 03:31:19 +01:00
|
|
|
|
2017-05-13 04:19:52 +02:00
|
|
|
def die(irc=None):
|
2016-12-17 03:31:19 +01:00
|
|
|
"""Commands plugin die function, called on plugin unload."""
|
2017-08-02 16:24:23 +02:00
|
|
|
permissions.remove_default_permissions(default_permissions)
|
2016-12-17 03:31:19 +01:00
|
|
|
|
2015-05-31 21:20:09 +02:00
|
|
|
@utils.add_cmd
|
|
|
|
def status(irc, source, args):
|
2015-07-18 07:21:16 +02:00
|
|
|
"""takes no arguments.
|
|
|
|
|
|
|
|
Returns your current PyLink login status."""
|
2017-08-02 16:24:23 +02:00
|
|
|
permissions.check_permissions(irc, source, ['commands.status'])
|
2016-07-30 05:15:31 +02:00
|
|
|
identified = irc.users[source].account
|
2015-05-31 21:20:09 +02:00
|
|
|
if identified:
|
2015-10-24 03:29:10 +02:00
|
|
|
irc.reply('You are identified as \x02%s\x02.' % identified)
|
2015-05-31 21:20:09 +02:00
|
|
|
else:
|
2015-10-24 03:29:10 +02:00
|
|
|
irc.reply('You are not identified as anyone.')
|
2017-06-30 08:01:39 +02:00
|
|
|
irc.reply('Operator access: \x02%s\x02' % bool(irc.is_oper(source)))
|
2015-05-31 07:35:00 +02:00
|
|
|
|
2016-02-21 03:24:29 +01:00
|
|
|
_none = '\x1D(none)\x1D'
|
2019-07-14 22:19:26 +02:00
|
|
|
_notavail = '\x1DN/A\x1D'
|
2019-06-16 20:14:33 +02:00
|
|
|
def _do_showuser(irc, source, u):
|
|
|
|
"""Helper function for showuser."""
|
2019-04-10 04:10:20 +02:00
|
|
|
# Some protocol modules store UIDs as ints; make sure we check for that.
|
|
|
|
try:
|
|
|
|
int_u = int(u)
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
if int_u in irc.users:
|
|
|
|
u = int_u
|
|
|
|
|
2015-08-30 04:29:05 +02:00
|
|
|
# Only show private info if the person is calling 'showuser' on themselves,
|
|
|
|
# or is an oper.
|
2017-06-30 08:01:39 +02:00
|
|
|
verbose = irc.is_oper(source) or u == source
|
2019-04-10 04:10:20 +02:00
|
|
|
|
2015-08-30 04:29:05 +02:00
|
|
|
if u not in irc.users:
|
2019-06-16 20:14:33 +02:00
|
|
|
irc.error('Unknown user %r.' % u)
|
2015-08-30 04:29:05 +02:00
|
|
|
return
|
|
|
|
|
2019-07-14 22:19:26 +02:00
|
|
|
f = lambda s: irc.reply(' ' + s, private=True)
|
2016-07-01 03:43:35 +02:00
|
|
|
|
2015-08-30 04:29:05 +02:00
|
|
|
userobj = irc.users[u]
|
2019-07-14 22:19:26 +02:00
|
|
|
irc.reply('Showing information on user \x02%s\x02 (%s@%s): %s' % (userobj.nick, userobj.ident,
|
|
|
|
userobj.host, userobj.realname), private=True)
|
2016-01-01 02:28:47 +01:00
|
|
|
|
2017-06-30 08:01:39 +02:00
|
|
|
sid = irc.get_server(u)
|
2015-08-30 04:29:05 +02:00
|
|
|
serverobj = irc.servers[sid]
|
|
|
|
ts = userobj.ts
|
2016-02-21 03:24:29 +01:00
|
|
|
|
2018-05-26 10:13:45 +02:00
|
|
|
# Show connected server & nick TS if available
|
|
|
|
serverinfo = '%s[%s]' % (serverobj.name, sid) \
|
2019-07-14 22:19:26 +02:00
|
|
|
if irc.has_cap('can-track-servers') else None
|
2018-05-26 10:13:45 +02:00
|
|
|
tsinfo = '%s [UTC] (%s)' % (time.asctime(time.gmtime(int(ts))), ts) \
|
2019-07-14 22:19:26 +02:00
|
|
|
if irc.has_cap('has-ts') else None
|
|
|
|
if tsinfo or serverinfo:
|
|
|
|
f('\x02Home server\x02: %s; \x02Nick TS:\x02 %s' % (serverinfo or _notavail, tsinfo or _notavail))
|
2016-02-21 03:24:29 +01:00
|
|
|
|
2018-05-26 10:13:45 +02:00
|
|
|
if verbose: # Oper/self only data: user modes, channels in, account info, etc.
|
2016-02-21 03:24:29 +01:00
|
|
|
f('\x02Protocol UID\x02: %s; \x02Real host\x02: %s; \x02IP\x02: %s' % \
|
2019-07-14 22:19:26 +02:00
|
|
|
(u, userobj.realhost or _notavail, userobj.ip))
|
2016-03-14 22:35:09 +01:00
|
|
|
channels = sorted(userobj.channels)
|
2019-04-10 04:10:20 +02:00
|
|
|
f('\x02Channels\x02: %s' % (' '.join(map(str, channels)) or _none))
|
2016-02-21 03:24:29 +01:00
|
|
|
f('\x02PyLink identification\x02: %s; \x02Services account\x02: %s; \x02Away status\x02: %s' % \
|
2016-07-30 05:15:31 +02:00
|
|
|
((userobj.account or _none), (userobj.services_account or _none), userobj.away or _none))
|
2019-07-14 22:19:26 +02:00
|
|
|
f('\x02User modes\x02: %s' % irc.join_modes(userobj.modes, sort=True))
|
2016-02-21 03:24:29 +01:00
|
|
|
|
2019-06-16 20:22:09 +02:00
|
|
|
# Show relay user data if available
|
|
|
|
relay = world.plugins.get('relay')
|
|
|
|
if relay:
|
|
|
|
try:
|
|
|
|
userpair = relay.get_orig_user(irc, u) or (irc.name, u)
|
|
|
|
remoteusers = relay.relayusers[userpair].items()
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
nicks = []
|
|
|
|
if remoteusers:
|
|
|
|
# Display all of the user's relay subclients, if there are any
|
|
|
|
nicks.append('%s:\x02%s\x02' % (userpair[0],
|
|
|
|
world.networkobjects[userpair[0]].users[userpair[1]].nick))
|
|
|
|
for r in remoteusers:
|
|
|
|
remotenet, remoteuser = r
|
|
|
|
remoteirc = world.networkobjects[remotenet]
|
|
|
|
nicks.append('%s:\x02%s\x02' % (remotenet, remoteirc.users[remoteuser].nick))
|
2019-07-14 22:19:26 +02:00
|
|
|
f("\x02Relay nicks\x02: %s" % ', '.join(nicks))
|
2019-06-16 20:22:09 +02:00
|
|
|
if verbose:
|
|
|
|
# Show the relay channels the user is in, if applicable
|
|
|
|
relaychannels = []
|
|
|
|
for ch in irc.users[u].channels:
|
|
|
|
relayentry = relay.get_relay(irc, ch)
|
|
|
|
if relayentry:
|
|
|
|
relaychannels.append(''.join(relayentry))
|
|
|
|
if relaychannels and verbose:
|
2019-07-14 22:19:26 +02:00
|
|
|
f("\x02Relay channels\x02: %s" % ' '.join(relaychannels))
|
2019-06-16 20:22:09 +02:00
|
|
|
|
2019-06-16 20:14:33 +02:00
|
|
|
@utils.add_cmd
|
|
|
|
def showuser(irc, source, args):
|
|
|
|
"""<user>
|
|
|
|
|
|
|
|
Shows information about <user>."""
|
|
|
|
permissions.check_permissions(irc, source, ['commands.showuser'])
|
2019-07-14 22:19:26 +02:00
|
|
|
target = ' '.join(args)
|
|
|
|
|
|
|
|
if not target:
|
2019-06-16 20:14:33 +02:00
|
|
|
irc.error("Not enough arguments. Needs 1: nick.")
|
|
|
|
return
|
|
|
|
|
|
|
|
users = irc.nick_to_uid(target, multi=True) or [target]
|
|
|
|
|
|
|
|
for user in users:
|
|
|
|
_do_showuser(irc, source, user)
|
2015-09-03 08:46:59 +02:00
|
|
|
|
2019-06-26 22:46:04 +02:00
|
|
|
@utils.add_cmd
|
|
|
|
def shownet(irc, source, args):
|
|
|
|
"""[<network name>]
|
|
|
|
|
|
|
|
Shows information about <network name>, or the current network if no argument is given."""
|
|
|
|
permissions.check_permissions(irc, source, ['commands.shownet'])
|
|
|
|
try:
|
|
|
|
extended = permissions.check_permissions(irc, source, ['commands.shownet.extended'])
|
|
|
|
except utils.NotAuthorizedError:
|
|
|
|
extended = False
|
|
|
|
|
|
|
|
try:
|
|
|
|
target = args[0]
|
|
|
|
except IndexError:
|
|
|
|
target = irc.name
|
|
|
|
|
|
|
|
try:
|
|
|
|
netobj = world.networkobjects[target]
|
|
|
|
serverdata = netobj.serverdata
|
|
|
|
except KeyError:
|
|
|
|
netobj = None
|
|
|
|
|
|
|
|
# If we have extended access, also look for disconnected networks
|
|
|
|
if extended and target in conf.conf['servers']:
|
|
|
|
serverdata = conf.conf['servers'][target]
|
|
|
|
else:
|
|
|
|
irc.error('Unknown network %r' % target)
|
|
|
|
return
|
|
|
|
|
|
|
|
# Get extended protocol details: IRCd type, virtual server info
|
|
|
|
protocol_name = serverdata.get('protocol')
|
|
|
|
ircd_type = None
|
|
|
|
|
|
|
|
# A bit of hardcoding here :(
|
|
|
|
if protocol_name == 'ts6':
|
|
|
|
ircd_type = serverdata.get('ircd', 'charybdis[default]')
|
|
|
|
elif protocol_name == 'inspircd':
|
|
|
|
ircd_type = serverdata.get('target_version', 'insp20[default]')
|
|
|
|
elif protocol_name == 'p10':
|
|
|
|
ircd_type = serverdata.get('ircd') or serverdata.get('p10_ircd') or 'nefarious[default]'
|
|
|
|
|
|
|
|
if protocol_name and ircd_type:
|
|
|
|
protocol_name = '%s/%s' % (protocol_name, ircd_type)
|
|
|
|
elif netobj and not protocol_name: # Show virtual server detail if applicable
|
|
|
|
try:
|
|
|
|
parent_name = netobj.virtual_parent.name
|
|
|
|
except AttributeError:
|
|
|
|
parent_name = None
|
|
|
|
protocol_name = 'none; virtual server defined by \x02%s\x02' % parent_name
|
|
|
|
|
|
|
|
irc.reply('Information on network \x02%s\x02: \x02%s\x02' %
|
|
|
|
(target, netobj.get_full_network_name() if netobj else '\x1dCurrently not connected\x1d'))
|
|
|
|
|
|
|
|
irc.reply('\x02PyLink protocol module\x02: %s; \x02Encoding\x02: %s' %
|
|
|
|
(protocol_name, netobj.encoding if netobj else serverdata.get('encoding', 'utf-8[default]')))
|
|
|
|
|
|
|
|
# Extended info: target host, defined hostname / SID
|
|
|
|
if extended:
|
|
|
|
connected = netobj and netobj.connected.is_set()
|
|
|
|
irc.reply('\x02Connected?\x02 %s' % ('\x0303true' if connected else '\x0304false'))
|
|
|
|
|
|
|
|
if serverdata.get('ip'):
|
|
|
|
irc.reply('\x02Server target\x02: \x1f%s:%s' % (serverdata['ip'], serverdata.get('port')))
|
|
|
|
if serverdata.get('hostname'):
|
|
|
|
irc.reply('\x02PyLink hostname\x02: %s; \x02SID:\x02 %s; \x02SID range:\x02 %s' %
|
|
|
|
(serverdata.get('hostname') or _none,
|
|
|
|
serverdata.get('sid') or _none,
|
|
|
|
serverdata.get('sidrange') or _none))
|
|
|
|
|
2015-09-15 03:43:19 +02:00
|
|
|
@utils.add_cmd
|
|
|
|
def showchan(irc, source, args):
|
|
|
|
"""<channel>
|
|
|
|
|
|
|
|
Shows information about <channel>."""
|
2017-08-02 16:24:23 +02:00
|
|
|
permissions.check_permissions(irc, source, ['commands.showchan'])
|
2015-09-15 03:43:19 +02:00
|
|
|
try:
|
2017-08-08 06:47:31 +02:00
|
|
|
channel = args[0]
|
2015-09-15 03:43:19 +02:00
|
|
|
except IndexError:
|
2016-11-19 07:52:08 +01:00
|
|
|
irc.error("Not enough arguments. Needs 1: channel.")
|
2015-09-15 03:43:19 +02:00
|
|
|
return
|
|
|
|
if channel not in irc.channels:
|
2016-11-19 07:52:08 +01:00
|
|
|
irc.error('Unknown channel %r.' % channel)
|
2015-09-15 03:43:19 +02:00
|
|
|
return
|
|
|
|
|
2016-07-01 03:43:35 +02:00
|
|
|
f = lambda s: irc.reply(s, private=True)
|
|
|
|
|
2015-09-15 03:43:19 +02:00
|
|
|
c = irc.channels[channel]
|
|
|
|
# Only show verbose info if caller is oper or is in the target channel.
|
2017-06-30 08:01:39 +02:00
|
|
|
verbose = source in c.users or irc.is_oper(source)
|
2015-09-15 03:43:19 +02:00
|
|
|
secret = ('s', None) in c.modes
|
|
|
|
if secret and not verbose:
|
|
|
|
# Hide secret channels from normal users.
|
2017-04-27 16:28:26 +02:00
|
|
|
irc.error('Unknown channel %r.' % channel)
|
2015-09-15 03:43:19 +02:00
|
|
|
return
|
|
|
|
|
|
|
|
nicks = [irc.users[u].nick for u in c.users]
|
|
|
|
|
|
|
|
f('Information on channel \x02%s\x02:' % channel)
|
2016-08-03 08:57:18 +02:00
|
|
|
if c.topic:
|
|
|
|
f('\x02Channel topic\x02: %s' % c.topic)
|
|
|
|
|
2017-04-09 23:52:44 +02:00
|
|
|
# Mark TS values as untrusted on Clientbot and others (where TS is read-only or not trackable)
|
2018-04-12 19:56:47 +02:00
|
|
|
f('\x02Channel creation time\x02: %s (%s) [UTC]%s' %
|
|
|
|
(time.asctime(time.gmtime(int(c.ts))), c.ts,
|
|
|
|
' [UNTRUSTED]' if not irc.has_cap('has-ts') else ''))
|
2016-08-03 08:57:18 +02:00
|
|
|
|
2015-09-15 03:43:19 +02:00
|
|
|
# Show only modes that aren't list-style modes.
|
2017-06-30 08:01:39 +02:00
|
|
|
modes = irc.join_modes([m for m in c.modes if m[0] not in irc.cmodes['*A']], sort=True)
|
2015-09-15 03:43:19 +02:00
|
|
|
f('\x02Channel modes\x02: %s' % modes)
|
|
|
|
if verbose:
|
|
|
|
nicklist = []
|
|
|
|
# Iterate over the user list, sorted by nick.
|
|
|
|
for user, nick in sorted(zip(c.users, nicks),
|
|
|
|
key=lambda userpair: userpair[1].lower()):
|
2018-01-22 17:10:55 +01:00
|
|
|
# Note: reversed() is used here because we're adding prefixes onto the nick in reverse
|
|
|
|
for pmode in reversed(c.get_prefix_modes(user)):
|
2016-07-24 20:03:23 +02:00
|
|
|
# Show prefix modes in order from highest to lowest.
|
|
|
|
nick = irc.prefixmodes.get(irc.cmodes.get(pmode, ''), '') + nick
|
|
|
|
nicklist.append(nick)
|
2015-09-15 03:43:19 +02:00
|
|
|
|
2019-06-16 20:20:26 +02:00
|
|
|
f('\x02User list\x02: %s' % ' '.join(nicklist))
|
2015-09-15 03:43:19 +02:00
|
|
|
|
2019-06-16 20:22:09 +02:00
|
|
|
# Show relay info, if applicable
|
|
|
|
relay = world.plugins.get('relay')
|
|
|
|
if relay:
|
|
|
|
relayentry = relay.get_relay(irc, channel)
|
|
|
|
if relayentry:
|
|
|
|
relays = ['\x02%s\x02' % ''.join(relayentry)]
|
|
|
|
relays += [''.join(link) for link in relay.db[relayentry]['links']]
|
|
|
|
f('\x02Relayed channels:\x02 %s' % (' '.join(relays)))
|
|
|
|
|
2015-09-19 20:51:56 +02:00
|
|
|
@utils.add_cmd
|
|
|
|
def version(irc, source, args):
|
|
|
|
"""takes no arguments.
|
|
|
|
|
|
|
|
Returns the version of the currently running PyLink instance."""
|
2016-07-24 19:59:25 +02:00
|
|
|
irc.reply("PyLink version \x02%s\x02 (in VCS: %s), released under the Mozilla Public License version 2.0." % (__version__, real_version))
|
2015-10-24 03:29:10 +02:00
|
|
|
irc.reply("The source of this program is available at \x02%s\x02." % world.source)
|
2015-09-26 19:15:07 +02:00
|
|
|
|
|
|
|
@utils.add_cmd
|
|
|
|
def echo(irc, source, args):
|
|
|
|
"""<text>
|
|
|
|
|
|
|
|
Echoes the text given."""
|
2017-08-02 16:24:23 +02:00
|
|
|
permissions.check_permissions(irc, source, ['commands.echo'])
|
2017-08-07 09:48:28 +02:00
|
|
|
if not args:
|
|
|
|
irc.error('No text to send!')
|
|
|
|
return
|
2015-10-24 03:29:10 +02:00
|
|
|
irc.reply(' '.join(args))
|
2015-09-27 20:54:06 +02:00
|
|
|
|
2016-12-17 04:05:08 +01:00
|
|
|
def _check_logout_access(irc, source, target, perms):
|
|
|
|
"""
|
|
|
|
Checks whether the source UID has access to log out the target UID.
|
|
|
|
This returns True if the source user has a permission specified,
|
|
|
|
or if the source and target are both logged in and have the same account.
|
|
|
|
"""
|
|
|
|
assert source in irc.users, "Unknown source user"
|
|
|
|
assert target in irc.users, "Unknown target user"
|
|
|
|
try:
|
2017-08-02 16:24:23 +02:00
|
|
|
permissions.check_permissions(irc, source, perms)
|
2016-12-17 04:05:08 +01:00
|
|
|
except utils.NotAuthorizedError:
|
|
|
|
if irc.users[source].account and (irc.users[source].account == irc.users[target].account):
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
raise
|
|
|
|
else:
|
|
|
|
return True
|
|
|
|
|
|
|
|
@utils.add_cmd
|
|
|
|
def logout(irc, source, args):
|
|
|
|
"""[<other nick/UID>]
|
|
|
|
|
|
|
|
Logs your account out of PyLink. If you have the 'commands.logout.force' permission, or are
|
|
|
|
attempting to log out yourself, you can also specify a nick to force a logout for."""
|
|
|
|
|
|
|
|
try:
|
|
|
|
othernick = args[0]
|
|
|
|
except IndexError: # No user specified
|
|
|
|
if irc.users[source].account:
|
|
|
|
irc.users[source].account = ''
|
|
|
|
else:
|
|
|
|
irc.error("You are not logged in!")
|
|
|
|
return
|
|
|
|
else:
|
2017-06-30 08:01:39 +02:00
|
|
|
otheruid = irc.nick_to_uid(othernick)
|
2016-12-17 04:05:08 +01:00
|
|
|
if not otheruid:
|
|
|
|
irc.error("Unknown user %s." % othernick)
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
_check_logout_access(irc, source, otheruid, ['commands.logout.force'])
|
|
|
|
if irc.users[otheruid].account:
|
|
|
|
irc.users[otheruid].account = ''
|
|
|
|
else:
|
|
|
|
irc.error("%s is not logged in." % othernick)
|
|
|
|
return
|
|
|
|
|
|
|
|
irc.reply("Done.")
|
|
|
|
|
2015-10-25 18:18:51 +01:00
|
|
|
loglevels = {'DEBUG': 10, 'INFO': 20, 'WARNING': 30, 'ERROR': 40, 'CRITICAL': 50}
|
|
|
|
@utils.add_cmd
|
|
|
|
def loglevel(irc, source, args):
|
|
|
|
"""<level>
|
|
|
|
|
|
|
|
Sets the log level to the given <level>. <level> must be either DEBUG, INFO, WARNING, ERROR, or CRITICAL.
|
|
|
|
If no log level is given, shows the current one."""
|
2017-08-02 16:24:23 +02:00
|
|
|
permissions.check_permissions(irc, source, ['commands.loglevel'])
|
2015-10-25 18:18:51 +01:00
|
|
|
try:
|
|
|
|
level = args[0].upper()
|
|
|
|
try:
|
|
|
|
loglevel = loglevels[level]
|
|
|
|
except KeyError:
|
2016-11-19 07:52:08 +01:00
|
|
|
irc.error('Unknown log level "%s".' % level)
|
2015-10-25 18:18:51 +01:00
|
|
|
return
|
|
|
|
else:
|
2017-06-02 17:42:32 +02:00
|
|
|
world.console_handler.setLevel(loglevel)
|
2015-10-25 18:18:51 +01:00
|
|
|
irc.reply("Done.")
|
|
|
|
except IndexError:
|
2017-06-02 17:42:32 +02:00
|
|
|
irc.reply(world.console_handler.level)
|
2017-01-23 01:23:30 +01:00
|
|
|
|
|
|
|
@utils.add_cmd
|
|
|
|
def mkpasswd(irc, source, args):
|
|
|
|
"""<password>
|
|
|
|
Hashes a password for use in the configuration file."""
|
|
|
|
# TODO: restrict to only certain users?
|
|
|
|
try:
|
|
|
|
password = args[0]
|
|
|
|
except IndexError:
|
|
|
|
irc.error("Not enough arguments. (Needs 1, password)")
|
|
|
|
return
|
|
|
|
if not password:
|
|
|
|
irc.error("Password cannot be empty.")
|
|
|
|
return
|
|
|
|
|
|
|
|
if not pwd_context:
|
|
|
|
irc.error("Password encryption is not available (missing passlib).")
|
|
|
|
return
|
|
|
|
|
|
|
|
hashed_pass = pwd_context.encrypt(password)
|
|
|
|
irc.reply(hashed_pass, private=True)
|