720 lines
24 KiB
Python
720 lines
24 KiB
Python
|
# coding=utf-8
|
||
|
"""Tasks that allow the bot to run, but aren't user-facing functionality
|
||
|
|
||
|
This is written as a module to make it easier to extend to support more
|
||
|
responses to standard IRC codes without having to shove them all into the
|
||
|
dispatch function in bot.py and making it easier to maintain.
|
||
|
"""
|
||
|
# Copyright 2008-2011, Sean B. Palmer (inamidst.com) and Michael Yanovich
|
||
|
# (yanovich.net)
|
||
|
# Copyright © 2012, Elad Alfassa <elad@fedoraproject.org>
|
||
|
# Copyright 2012-2015, Elsie Powell embolalia.com
|
||
|
# Licensed under the Eiffel Forum License 2.
|
||
|
from __future__ import unicode_literals, absolute_import, print_function, division
|
||
|
|
||
|
|
||
|
from random import randint
|
||
|
import re
|
||
|
import sys
|
||
|
import time
|
||
|
import module
|
||
|
from bot import _CapReq
|
||
|
from tools import Identifier, iteritems, events
|
||
|
from tools.target import User, Channel
|
||
|
import base64
|
||
|
from logger import get_logger
|
||
|
|
||
|
if sys.version_info.major >= 3:
|
||
|
unicode = str
|
||
|
|
||
|
LOGGER = get_logger(__name__)
|
||
|
|
||
|
batched_caps = {}
|
||
|
who_reqs = {} # Keeps track of reqs coming from this module, rather than others
|
||
|
|
||
|
|
||
|
def auth_after_register(bot):
|
||
|
"""Do NickServ/AuthServ auth"""
|
||
|
if bot.config.core.auth_method == 'nickserv':
|
||
|
nickserv_name = bot.config.core.auth_target or 'NickServ'
|
||
|
bot.msg(
|
||
|
nickserv_name,
|
||
|
'IDENTIFY %s' % bot.config.core.auth_password
|
||
|
)
|
||
|
|
||
|
elif bot.config.core.auth_method == 'authserv':
|
||
|
account = bot.config.core.auth_username
|
||
|
password = bot.config.core.auth_password
|
||
|
bot.write((
|
||
|
'AUTHSERV auth',
|
||
|
account + ' ' + password
|
||
|
))
|
||
|
|
||
|
elif bot.config.core.auth_method == 'Q':
|
||
|
account = bot.config.core.auth_username
|
||
|
password = bot.config.core.auth_password
|
||
|
bot.write((
|
||
|
'AUTH',
|
||
|
account + ' ' + password
|
||
|
))
|
||
|
|
||
|
|
||
|
@module.event(events.RPL_WELCOME, events.RPL_LUSERCLIENT)
|
||
|
@module.rule('.*')
|
||
|
@module.thread(False)
|
||
|
@module.unblockable
|
||
|
def startup(bot, trigger):
|
||
|
"""Do tasks related to connecting to the network.
|
||
|
|
||
|
001 RPL_WELCOME is from RFC2812 and is the first message that is sent after
|
||
|
the connection has been registered on the network.
|
||
|
|
||
|
251 RPL_LUSERCLIENT is a mandatory message that is sent after client
|
||
|
connects to the server in rfc1459. RFC2812 does not require it and all
|
||
|
networks might not send it. We support both.
|
||
|
|
||
|
"""
|
||
|
if bot.connection_registered:
|
||
|
return
|
||
|
|
||
|
bot.connection_registered = True
|
||
|
|
||
|
auth_after_register(bot)
|
||
|
|
||
|
modes = bot.config.core.modes
|
||
|
bot.write(('MODE ', '%s +%s' % (bot.nick, modes)))
|
||
|
|
||
|
bot.memory['retry_join'] = dict()
|
||
|
|
||
|
if bot.config.core.throttle_join:
|
||
|
throttle_rate = int(bot.config.core.throttle_join)
|
||
|
channels_joined = 0
|
||
|
for channel in bot.config.core.channels:
|
||
|
channels_joined += 1
|
||
|
if not channels_joined % throttle_rate:
|
||
|
time.sleep(1)
|
||
|
bot.join(channel)
|
||
|
else:
|
||
|
for channel in bot.config.core.channels:
|
||
|
bot.join(channel)
|
||
|
|
||
|
if (not bot.config.core.owner_account and
|
||
|
'account-tag' in bot.enabled_capabilities and
|
||
|
'@' not in bot.config.core.owner):
|
||
|
msg = (
|
||
|
"This network supports using network services to identify you as "
|
||
|
"my owner, rather than just matching your nickname. This is much "
|
||
|
"more secure. If you'd like to do this, make sure you're logged in "
|
||
|
"and reply with \"{}useserviceauth\""
|
||
|
).format(bot.config.core.help_prefix)
|
||
|
bot.msg(bot.config.core.owner, msg)
|
||
|
|
||
|
|
||
|
@module.require_privmsg()
|
||
|
@module.require_owner()
|
||
|
@module.commands('useserviceauth')
|
||
|
def enable_service_auth(bot, trigger):
|
||
|
if bot.config.core.owner_account:
|
||
|
return
|
||
|
if 'account-tag' not in bot.enabled_capabilities:
|
||
|
bot.say('This server does not fully support services auth, so this '
|
||
|
'command is not available.')
|
||
|
return
|
||
|
if not trigger.account:
|
||
|
bot.say('You must be logged in to network services before using this '
|
||
|
'command.')
|
||
|
return
|
||
|
bot.config.core.owner_account = trigger.account
|
||
|
bot.config.save()
|
||
|
bot.say('Success! I will now use network services to identify you as my '
|
||
|
'owner.')
|
||
|
|
||
|
|
||
|
@module.event(events.ERR_NOCHANMODES)
|
||
|
@module.rule('.*')
|
||
|
@module.priority('high')
|
||
|
def retry_join(bot, trigger):
|
||
|
"""Give NickServer enough time to identify on a +R channel.
|
||
|
|
||
|
Give NickServ enough time to identify, and retry rejoining an
|
||
|
identified-only (+R) channel. Maximum of ten rejoin attempts.
|
||
|
|
||
|
"""
|
||
|
channel = trigger.args[1]
|
||
|
if channel in bot.memory['retry_join'].keys():
|
||
|
bot.memory['retry_join'][channel] += 1
|
||
|
if bot.memory['retry_join'][channel] > 10:
|
||
|
LOGGER.warning('Failed to join %s after 10 attempts.', channel)
|
||
|
return
|
||
|
else:
|
||
|
bot.memory['retry_join'][channel] = 0
|
||
|
bot.join(channel)
|
||
|
return
|
||
|
|
||
|
time.sleep(6)
|
||
|
bot.join(channel)
|
||
|
|
||
|
|
||
|
@module.rule('(.*)')
|
||
|
@module.event(events.RPL_NAMREPLY)
|
||
|
@module.priority('high')
|
||
|
@module.thread(False)
|
||
|
@module.unblockable
|
||
|
def handle_names(bot, trigger):
|
||
|
"""Handle NAMES response, happens when joining to channels."""
|
||
|
names = trigger.split()
|
||
|
|
||
|
#TODO specific to one channel type. See issue 281.
|
||
|
channels = re.search('(#\S*)', trigger.raw)
|
||
|
if not channels:
|
||
|
return
|
||
|
channel = Identifier(channels.group(1))
|
||
|
if channel not in bot.privileges:
|
||
|
bot.privileges[channel] = dict()
|
||
|
|
||
|
# This could probably be made flexible in the future, but I don't think
|
||
|
# it'd be worth it.
|
||
|
mapping = {'+': module.VOICE,
|
||
|
'%': module.HALFOP,
|
||
|
'@': module.OP,
|
||
|
'&': module.ADMIN,
|
||
|
'~': module.OWNER}
|
||
|
|
||
|
for name in names:
|
||
|
priv = 0
|
||
|
for prefix, value in iteritems(mapping):
|
||
|
if prefix in name:
|
||
|
priv = priv | value
|
||
|
nick = Identifier(name.lstrip(''.join(mapping.keys())))
|
||
|
bot.privileges[channel][nick] = priv
|
||
|
|
||
|
|
||
|
@module.rule('(.*)')
|
||
|
@module.event('MODE')
|
||
|
@module.priority('high')
|
||
|
@module.thread(False)
|
||
|
@module.unblockable
|
||
|
def track_modes(bot, trigger):
|
||
|
"""Track usermode changes and keep our lists of ops up to date."""
|
||
|
# Mode message format: <channel> *( ( "-" / "+" ) *<modes> *<modeparams> )
|
||
|
channel = Identifier(trigger.args[0])
|
||
|
line = trigger.args[1:]
|
||
|
|
||
|
# If the first character of where the mode is being set isn't a #
|
||
|
# then it's a user mode, not a channel mode, so we'll ignore it.
|
||
|
if channel.is_nick():
|
||
|
return
|
||
|
|
||
|
mapping = {'v': module.VOICE,
|
||
|
'h': module.HALFOP,
|
||
|
'o': module.OP,
|
||
|
'a': module.ADMIN,
|
||
|
'q': module.OWNER}
|
||
|
|
||
|
modes = []
|
||
|
for arg in line:
|
||
|
if len(arg) == 0:
|
||
|
continue
|
||
|
if arg[0] in '+-':
|
||
|
# There was a comment claiming IRC allows e.g. MODE +aB-c foo, but
|
||
|
# I don't see it in any RFCs. Leaving in the extra parsing for now.
|
||
|
sign = ''
|
||
|
modes = []
|
||
|
for char in arg:
|
||
|
if char == '+' or char == '-':
|
||
|
sign = char
|
||
|
else:
|
||
|
modes.append(sign + char)
|
||
|
else:
|
||
|
arg = Identifier(arg)
|
||
|
for mode in modes:
|
||
|
priv = bot.privileges[channel].get(arg, 0)
|
||
|
value = mapping.get(mode[1])
|
||
|
if value is not None:
|
||
|
if mode[0] == '+':
|
||
|
priv = priv | value
|
||
|
else:
|
||
|
priv = priv & ~value
|
||
|
bot.privileges[channel][arg] = priv
|
||
|
|
||
|
|
||
|
@module.rule('.*')
|
||
|
@module.event('NICK')
|
||
|
@module.priority('high')
|
||
|
@module.thread(False)
|
||
|
@module.unblockable
|
||
|
def track_nicks(bot, trigger):
|
||
|
"""Track nickname changes and maintain our chanops list accordingly."""
|
||
|
old = trigger.nick
|
||
|
new = Identifier(trigger)
|
||
|
|
||
|
# Give debug mssage, and PM the owner, if the bot's own nick changes.
|
||
|
if old == bot.nick and new != bot.nick:
|
||
|
privmsg = ("Hi, I'm your bot, %s."
|
||
|
"Something has made my nick change. "
|
||
|
"This can cause some problems for me, "
|
||
|
"and make me do weird things. "
|
||
|
"You'll probably want to restart me, "
|
||
|
"and figure out what made that happen "
|
||
|
"so you can stop it happening again. "
|
||
|
"(Usually, it means you tried to give me a nick "
|
||
|
"that's protected by NickServ.)") % bot.nick
|
||
|
debug_msg = ("Nick changed by server. "
|
||
|
"This can cause unexpected behavior. Please restart the bot.")
|
||
|
LOGGER.critical(debug_msg)
|
||
|
bot.msg(bot.config.core.owner, privmsg)
|
||
|
return
|
||
|
|
||
|
for channel in bot.privileges:
|
||
|
channel = Identifier(channel)
|
||
|
if old in bot.privileges[channel]:
|
||
|
value = bot.privileges[channel].pop(old)
|
||
|
bot.privileges[channel][new] = value
|
||
|
|
||
|
for channel in bot.channels.values():
|
||
|
channel.rename_user(old, new)
|
||
|
if old in bot.users:
|
||
|
bot.users[new] = bot.users.pop(old)
|
||
|
|
||
|
|
||
|
@module.rule('(.*)')
|
||
|
@module.event('PART')
|
||
|
@module.priority('high')
|
||
|
@module.thread(False)
|
||
|
@module.unblockable
|
||
|
def track_part(bot, trigger):
|
||
|
nick = trigger.nick
|
||
|
channel = trigger.sender
|
||
|
_remove_from_channel(bot, nick, channel)
|
||
|
|
||
|
|
||
|
@module.rule('.*')
|
||
|
@module.event('KICK')
|
||
|
@module.priority('high')
|
||
|
@module.thread(False)
|
||
|
@module.unblockable
|
||
|
def track_kick(bot, trigger):
|
||
|
nick = Identifier(trigger.args[1])
|
||
|
channel = trigger.sender
|
||
|
_remove_from_channel(bot, nick, channel)
|
||
|
|
||
|
|
||
|
def _remove_from_channel(bot, nick, channel):
|
||
|
if nick == bot.nick:
|
||
|
bot.privileges.pop(channel, None)
|
||
|
bot.channels.pop(channel, None)
|
||
|
|
||
|
lost_users = []
|
||
|
for nick_, user in bot.users.items():
|
||
|
user.channels.pop(channel, None)
|
||
|
if not user.channels:
|
||
|
lost_users.append(nick_)
|
||
|
for nick_ in lost_users:
|
||
|
bot.users.pop(nick_, None)
|
||
|
else:
|
||
|
bot.privileges[channel].pop(nick, None)
|
||
|
|
||
|
user = bot.users.get(nick)
|
||
|
if user and channel in user.channels:
|
||
|
bot.channels[channel].clear_user(nick)
|
||
|
if not user.channels:
|
||
|
bot.users.pop(nick, None)
|
||
|
|
||
|
|
||
|
def _whox_enabled(bot):
|
||
|
# Either privilege tracking or away notification. For simplicity, both
|
||
|
# account notify and extended join must be there for account tracking.
|
||
|
return (('account-notify' in bot.enabled_capabilities and
|
||
|
'extended-join' in bot.enabled_capabilities) or
|
||
|
'away-notify' in bot.enabled_capabilities)
|
||
|
|
||
|
|
||
|
def _send_who(bot, channel):
|
||
|
if _whox_enabled(bot):
|
||
|
# WHOX syntax, see http://faerion.sourceforge.net/doc/irc/whox.var
|
||
|
# Needed for accounts in who replies. The random integer is a param
|
||
|
# to identify the reply as one from this command, because if someone
|
||
|
# else sent it, we have no fucking way to know what the format is.
|
||
|
rand = str(randint(0, 999))
|
||
|
while rand in who_reqs:
|
||
|
rand = str(randint(0, 999))
|
||
|
who_reqs[rand] = channel
|
||
|
bot.write(['WHO', channel, 'a%nuachtf,' + rand])
|
||
|
else:
|
||
|
# We might be on an old network, but we still care about keeping our
|
||
|
# user list updated
|
||
|
bot.write(['WHO', channel])
|
||
|
|
||
|
|
||
|
@module.rule('.*')
|
||
|
@module.event('JOIN')
|
||
|
@module.priority('high')
|
||
|
@module.thread(False)
|
||
|
@module.unblockable
|
||
|
def track_join(bot, trigger):
|
||
|
if trigger.nick == bot.nick and trigger.sender not in bot.channels:
|
||
|
bot.write(('TOPIC', trigger.sender))
|
||
|
|
||
|
bot.privileges[trigger.sender] = dict()
|
||
|
bot.channels[trigger.sender] = Channel(trigger.sender)
|
||
|
_send_who(bot, trigger.sender)
|
||
|
|
||
|
bot.privileges[trigger.sender][trigger.nick] = 0
|
||
|
|
||
|
user = bot.users.get(trigger.nick)
|
||
|
if user is None:
|
||
|
user = User(trigger.nick, trigger.user, trigger.host)
|
||
|
bot.users[trigger.nick] = user
|
||
|
bot.channels[trigger.sender].add_user(user)
|
||
|
|
||
|
if len(trigger.args) > 1 and trigger.args[1] != '*' and (
|
||
|
'account-notify' in bot.enabled_capabilities and
|
||
|
'extended-join' in bot.enabled_capabilities):
|
||
|
user.account = trigger.args[1]
|
||
|
|
||
|
|
||
|
@module.rule('.*')
|
||
|
@module.event('QUIT')
|
||
|
@module.priority('high')
|
||
|
@module.thread(False)
|
||
|
@module.unblockable
|
||
|
def track_quit(bot, trigger):
|
||
|
for chanprivs in bot.privileges.values():
|
||
|
chanprivs.pop(trigger.nick, None)
|
||
|
for channel in bot.channels.values():
|
||
|
channel.clear_user(trigger.nick)
|
||
|
bot.users.pop(trigger.nick, None)
|
||
|
|
||
|
|
||
|
@module.rule('.*')
|
||
|
@module.event('CAP')
|
||
|
@module.thread(False)
|
||
|
@module.priority('high')
|
||
|
@module.unblockable
|
||
|
def recieve_cap_list(bot, trigger):
|
||
|
cap = trigger.strip('-=~')
|
||
|
# Server is listing capabilites
|
||
|
if trigger.args[1] == 'LS':
|
||
|
recieve_cap_ls_reply(bot, trigger)
|
||
|
# Server denied CAP REQ
|
||
|
elif trigger.args[1] == 'NAK':
|
||
|
entry = bot._cap_reqs.get(cap, None)
|
||
|
# If it was requested with bot.cap_req
|
||
|
if entry:
|
||
|
for req in entry:
|
||
|
# And that request was mandatory/prohibit, and a callback was
|
||
|
# provided
|
||
|
if req.prefix and req.failure:
|
||
|
# Call it.
|
||
|
req.failure(bot, req.prefix + cap)
|
||
|
# Server is removing a capability
|
||
|
elif trigger.args[1] == 'DEL':
|
||
|
entry = bot._cap_reqs.get(cap, None)
|
||
|
# If it was requested with bot.cap_req
|
||
|
if entry:
|
||
|
for req in entry:
|
||
|
# And that request wasn't prohibit, and a callback was
|
||
|
# provided
|
||
|
if req.prefix != '-' and req.failure:
|
||
|
# Call it.
|
||
|
req.failure(bot, req.prefix + cap)
|
||
|
# Server is adding new capability
|
||
|
elif trigger.args[1] == 'NEW':
|
||
|
entry = bot._cap_reqs.get(cap, None)
|
||
|
# If it was requested with bot.cap_req
|
||
|
if entry:
|
||
|
for req in entry:
|
||
|
# And that request wasn't prohibit
|
||
|
if req.prefix != '-':
|
||
|
# Request it
|
||
|
bot.write(('CAP', 'REQ', req.prefix + cap))
|
||
|
# Server is acknowledging a capability
|
||
|
elif trigger.args[1] == 'ACK':
|
||
|
caps = trigger.args[2].split()
|
||
|
for cap in caps:
|
||
|
cap.strip('-~= ')
|
||
|
bot.enabled_capabilities.add(cap)
|
||
|
entry = bot._cap_reqs.get(cap, [])
|
||
|
for req in entry:
|
||
|
if req.success:
|
||
|
req.success(bot, req.prefix + trigger)
|
||
|
if cap == 'sasl': # TODO why is this not done with bot.cap_req?
|
||
|
recieve_cap_ack_sasl(bot)
|
||
|
|
||
|
|
||
|
def recieve_cap_ls_reply(bot, trigger):
|
||
|
if bot.server_capabilities:
|
||
|
# We've already seen the results, so someone sent CAP LS from a module.
|
||
|
# We're too late to do SASL, and we don't want to send CAP END before
|
||
|
# the module has done what it needs to, so just return
|
||
|
return
|
||
|
|
||
|
for cap in trigger.split():
|
||
|
c = cap.split('=')
|
||
|
if len(c) == 2:
|
||
|
batched_caps[c[0]] = c[1]
|
||
|
else:
|
||
|
batched_caps[c[0]] = None
|
||
|
|
||
|
# Not the last in a multi-line reply. First two args are * and LS.
|
||
|
if trigger.args[2] == '*':
|
||
|
return
|
||
|
|
||
|
bot.server_capabilities = batched_caps
|
||
|
|
||
|
# If some other module requests it, we don't need to add another request.
|
||
|
# If some other module prohibits it, we shouldn't request it.
|
||
|
core_caps = ['multi-prefix', 'away-notify', 'cap-notify', 'server-time']
|
||
|
for cap in core_caps:
|
||
|
if cap not in bot._cap_reqs:
|
||
|
bot._cap_reqs[cap] = [_CapReq('', 'coretasks')]
|
||
|
|
||
|
def acct_warn(bot, cap):
|
||
|
LOGGER.info('Server does not support %s, or it conflicts with a custom '
|
||
|
'module. User account validation unavailable or limited.',
|
||
|
cap[1:])
|
||
|
if bot.config.core.owner_account or bot.config.core.admin_accounts:
|
||
|
LOGGER.warning(
|
||
|
'Owner or admin accounts are configured, but %s is not '
|
||
|
'supported by the server. This may cause unexpected behavior.',
|
||
|
cap[1:])
|
||
|
auth_caps = ['account-notify', 'extended-join', 'account-tag']
|
||
|
for cap in auth_caps:
|
||
|
if cap not in bot._cap_reqs:
|
||
|
bot._cap_reqs[cap] = [_CapReq('=', 'coretasks', acct_warn)]
|
||
|
|
||
|
for cap, reqs in iteritems(bot._cap_reqs):
|
||
|
# At this point, we know mandatory and prohibited don't co-exist, but
|
||
|
# we need to call back for optionals if they're also prohibited
|
||
|
prefix = ''
|
||
|
for entry in reqs:
|
||
|
if prefix == '-' and entry.prefix != '-':
|
||
|
entry.failure(bot, entry.prefix + cap)
|
||
|
continue
|
||
|
if entry.prefix:
|
||
|
prefix = entry.prefix
|
||
|
|
||
|
# It's not required, or it's supported, so we can request it
|
||
|
if prefix != '=' or cap in bot.server_capabilities:
|
||
|
# REQs fail as a whole, so we send them one capability at a time
|
||
|
bot.write(('CAP', 'REQ', entry.prefix + cap))
|
||
|
# If it's required but not in server caps, we need to call all the
|
||
|
# callbacks
|
||
|
else:
|
||
|
for entry in reqs:
|
||
|
if entry.failure and entry.prefix == '=':
|
||
|
entry.failure(bot, entry.prefix + cap)
|
||
|
|
||
|
# If we want to do SASL, we have to wait before we can send CAP END. So if
|
||
|
# we are, wait on 903 (SASL successful) to send it.
|
||
|
if bot.config.core.auth_method == 'sasl':
|
||
|
bot.write(('CAP', 'REQ', 'sasl'))
|
||
|
else:
|
||
|
bot.write(('CAP', 'END'))
|
||
|
|
||
|
|
||
|
def recieve_cap_ack_sasl(bot):
|
||
|
# Presumably we're only here if we said we actually *want* sasl, but still
|
||
|
# check anyway.
|
||
|
password = bot.config.core.auth_password
|
||
|
if not password:
|
||
|
return
|
||
|
mech = bot.config.core.auth_target or 'PLAIN'
|
||
|
bot.write(('AUTHENTICATE', mech))
|
||
|
|
||
|
|
||
|
@module.event('AUTHENTICATE')
|
||
|
@module.rule('.*')
|
||
|
def auth_proceed(bot, trigger):
|
||
|
if trigger.args[0] != '+':
|
||
|
# How did we get here? I am not good with computer.
|
||
|
return
|
||
|
# Is this right?
|
||
|
sasl_username = bot.config.core.auth_username or bot.nick
|
||
|
sasl_password = bot.config.core.auth_password
|
||
|
sasl_token = '\0'.join((sasl_username, sasl_username, sasl_password))
|
||
|
# Spec says we do a base 64 encode on the SASL stuff
|
||
|
bot.write(('AUTHENTICATE', base64.b64encode(sasl_token.encode('utf-8'))))
|
||
|
|
||
|
|
||
|
@module.event(events.RPL_SASLSUCCESS)
|
||
|
@module.rule('.*')
|
||
|
def sasl_success(bot, trigger):
|
||
|
bot.write(('CAP', 'END'))
|
||
|
|
||
|
|
||
|
#Live blocklist editing
|
||
|
|
||
|
|
||
|
@module.commands('blocks')
|
||
|
@module.priority('low')
|
||
|
@module.thread(False)
|
||
|
@module.unblockable
|
||
|
def blocks(bot, trigger):
|
||
|
"""Manage Sopel's blocking features.
|
||
|
|
||
|
https://github.com/sopel-irc/sopel/wiki/Making-Sopel-ignore-people
|
||
|
|
||
|
"""
|
||
|
if not trigger.admin:
|
||
|
return
|
||
|
|
||
|
STRINGS = {
|
||
|
"success_del": "Successfully deleted block: %s",
|
||
|
"success_add": "Successfully added block: %s",
|
||
|
"no_nick": "No matching nick block found for: %s",
|
||
|
"no_host": "No matching hostmask block found for: %s",
|
||
|
"invalid": "Invalid format for %s a block. Try: .blocks add (nick|hostmask) sopel",
|
||
|
"invalid_display": "Invalid input for displaying blocks.",
|
||
|
"nonelisted": "No %s listed in the blocklist.",
|
||
|
'huh': "I could not figure out what you wanted to do.",
|
||
|
}
|
||
|
|
||
|
masks = set(s for s in bot.config.core.host_blocks if s != '')
|
||
|
nicks = set(Identifier(nick)
|
||
|
for nick in bot.config.core.nick_blocks
|
||
|
if nick != '')
|
||
|
text = trigger.group().split()
|
||
|
|
||
|
if len(text) == 3 and text[1] == "list":
|
||
|
if text[2] == "hostmask":
|
||
|
if len(masks) > 0:
|
||
|
blocked = ', '.join(unicode(mask) for mask in masks)
|
||
|
bot.say("Blocked hostmasks: {}".format(blocked))
|
||
|
else:
|
||
|
bot.reply(STRINGS['nonelisted'] % ('hostmasks'))
|
||
|
elif text[2] == "nick":
|
||
|
if len(nicks) > 0:
|
||
|
blocked = ', '.join(unicode(nick) for nick in nicks)
|
||
|
bot.say("Blocked nicks: {}".format(blocked))
|
||
|
else:
|
||
|
bot.reply(STRINGS['nonelisted'] % ('nicks'))
|
||
|
else:
|
||
|
bot.reply(STRINGS['invalid_display'])
|
||
|
|
||
|
elif len(text) == 4 and text[1] == "add":
|
||
|
if text[2] == "nick":
|
||
|
nicks.add(text[3])
|
||
|
bot.config.core.nick_blocks = nicks
|
||
|
bot.config.save()
|
||
|
elif text[2] == "hostmask":
|
||
|
masks.add(text[3].lower())
|
||
|
bot.config.core.host_blocks = list(masks)
|
||
|
else:
|
||
|
bot.reply(STRINGS['invalid'] % ("adding"))
|
||
|
return
|
||
|
|
||
|
bot.reply(STRINGS['success_add'] % (text[3]))
|
||
|
|
||
|
elif len(text) == 4 and text[1] == "del":
|
||
|
if text[2] == "nick":
|
||
|
if Identifier(text[3]) not in nicks:
|
||
|
bot.reply(STRINGS['no_nick'] % (text[3]))
|
||
|
return
|
||
|
nicks.remove(Identifier(text[3]))
|
||
|
bot.config.core.nick_blocks = [unicode(n) for n in nicks]
|
||
|
bot.config.save()
|
||
|
bot.reply(STRINGS['success_del'] % (text[3]))
|
||
|
elif text[2] == "hostmask":
|
||
|
mask = text[3].lower()
|
||
|
if mask not in masks:
|
||
|
bot.reply(STRINGS['no_host'] % (text[3]))
|
||
|
return
|
||
|
masks.remove(mask)
|
||
|
bot.config.core.host_blocks = [unicode(m) for m in masks]
|
||
|
bot.config.save()
|
||
|
bot.reply(STRINGS['success_del'] % (text[3]))
|
||
|
else:
|
||
|
bot.reply(STRINGS['invalid'] % ("deleting"))
|
||
|
return
|
||
|
else:
|
||
|
bot.reply(STRINGS['huh'])
|
||
|
|
||
|
|
||
|
@module.event('ACCOUNT')
|
||
|
@module.rule('.*')
|
||
|
def account_notify(bot, trigger):
|
||
|
if trigger.nick not in bot.users:
|
||
|
bot.users[trigger.nick] = User(trigger.nick, trigger.user, trigger.host)
|
||
|
account = trigger.args[0]
|
||
|
if account == '*':
|
||
|
account = None
|
||
|
bot.users[trigger.nick].account = account
|
||
|
|
||
|
|
||
|
@module.event(events.RPL_WHOSPCRPL)
|
||
|
@module.rule('.*')
|
||
|
@module.priority('high')
|
||
|
@module.unblockable
|
||
|
def recv_whox(bot, trigger):
|
||
|
if len(trigger.args) < 2 or trigger.args[1] not in who_reqs:
|
||
|
# Ignored, some module probably called WHO
|
||
|
return
|
||
|
if len(trigger.args) != 8:
|
||
|
return LOGGER.warning('While populating `bot.accounts` a WHO response was malformed.')
|
||
|
_, _, channel, user, host, nick, status, account = trigger.args
|
||
|
away = 'G' in status
|
||
|
_record_who(bot, channel, user, host, nick, account, away)
|
||
|
|
||
|
|
||
|
def _record_who(bot, channel, user, host, nick, account=None, away=None):
|
||
|
nick = Identifier(nick)
|
||
|
channel = Identifier(channel)
|
||
|
if nick not in bot.users:
|
||
|
bot.users[nick] = User(nick, user, host)
|
||
|
user = bot.users[nick]
|
||
|
if account == '0':
|
||
|
user.account = None
|
||
|
else:
|
||
|
user.account = account
|
||
|
user.away = away
|
||
|
if channel not in bot.channels:
|
||
|
bot.channels[channel] = Channel(channel)
|
||
|
bot.channels[channel].add_user(user)
|
||
|
|
||
|
|
||
|
@module.event(events.RPL_WHOREPLY)
|
||
|
@module.rule('.*')
|
||
|
@module.priority('high')
|
||
|
@module.unblockable
|
||
|
def recv_who(bot, trigger):
|
||
|
channel, user, host, _, nick, = trigger.args[1:6]
|
||
|
_record_who(bot, channel, user, host, nick)
|
||
|
|
||
|
|
||
|
@module.event(events.RPL_ENDOFWHO)
|
||
|
@module.rule('.*')
|
||
|
@module.priority('high')
|
||
|
@module.unblockable
|
||
|
def end_who(bot, trigger):
|
||
|
if _whox_enabled(bot):
|
||
|
who_reqs.pop(trigger.args[1], None)
|
||
|
|
||
|
|
||
|
@module.rule('.*')
|
||
|
@module.event('AWAY')
|
||
|
@module.priority('high')
|
||
|
@module.thread(False)
|
||
|
@module.unblockable
|
||
|
def track_notify(bot, trigger):
|
||
|
if trigger.nick not in bot.users:
|
||
|
bot.users[trigger.nick] = User(trigger.nick, trigger.user, trigger.host)
|
||
|
user = bot.users[trigger.nick]
|
||
|
user.away = bool(trigger.args)
|
||
|
|
||
|
|
||
|
@module.rule('.*')
|
||
|
@module.event('TOPIC')
|
||
|
@module.event(events.RPL_TOPIC)
|
||
|
@module.priority('high')
|
||
|
@module.thread(False)
|
||
|
@module.unblockable
|
||
|
def track_topic(bot, trigger):
|
||
|
if trigger.event != 'TOPIC':
|
||
|
channel = trigger.args[1]
|
||
|
else:
|
||
|
channel = trigger.args[0]
|
||
|
if channel not in bot.channels:
|
||
|
return
|
||
|
bot.channels[channel].topic = trigger.args[-1]
|