dr.botzo/ircbot/bot.py

1137 lines
42 KiB
Python
Raw Normal View History

2015-05-09 18:56:26 -05:00
"""Provide the base IRC client bot which other code can latch onto."""
import bisect
import collections
import copy
import importlib
import logging
import re
2016-01-16 17:58:11 -06:00
from xmlrpc.server import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
import socket
2015-05-09 18:56:26 -05:00
import ssl
import sys
2016-01-16 17:58:11 -06:00
import threading
import time
2015-05-09 18:56:26 -05:00
from django.conf import settings
import irc.buffer
2015-05-09 18:56:26 -05:00
import irc.client
from irc.connection import Factory
from irc.dict import IRCDict
import irc.modes
import ircbot.lib as ircbotlib
from ircbot.models import Alias, IrcChannel, IrcPlugin, IrcServer
log = logging.getLogger('ircbot.bot')
2015-05-09 18:56:26 -05:00
class IrcBotXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
"""Override the basic request handler to change the logging."""
def log_message(self, format, *args):
"""Use a logger rather than stderr."""
log.debug("XML-RPC - %s - %s", self.client_address[0], format % args)
class PrioritizedRegexHandler(collections.namedtuple('Base', ('priority', 'regex', 'callback'))):
"""Regex handler that still uses the normal handler priority stuff."""
def __lt__(self, other):
"""When sorting prioritized handlers, only use the priority"""
return self.priority < other.priority
class LenientServerConnection(irc.client.ServerConnection):
"""
An IRC server connection.
ServerConnection objects are instantiated by calling the server
method on a Reactor object.
"""
buffer_class = irc.buffer.LenientDecodingLineBuffer
server_config = None
def _prep_message(self, string):
"""Override SimpleIRCClient._prep_message to add some logging."""
log.debug("preparing message %s", string)
return super(LenientServerConnection, self)._prep_message(string)
class DrReactor(irc.client.Reactor):
"""Customize the basic IRC library's Reactor with more features."""
def __do_nothing(*args, **kwargs):
pass
def __init__(self, on_connect=__do_nothing, on_disconnect=__do_nothing):
"""Initialize our custom stuff."""
super(DrReactor, self).__init__(on_connect=on_connect, on_disconnect=on_disconnect)
self.regex_handlers = {}
def server(self):
"""Creates and returns a ServerConnection object."""
c = LenientServerConnection(self)
with self.mutex:
self.connections.append(c)
return c
def add_global_regex_handler(self, events, regex, handler, priority=0):
"""Adds a global handler function for a specific event type and regex.
Arguments:
events --- Event type(s) (a list of strings).
handler -- Callback function taking connection and event
parameters.
priority --- A number (the lower the number, the higher priority).
The handler function is called whenever the specified event is
triggered in any of the connections and the regex matches.
See documentation for the Event class.
The handler functions are called in priority order (lowest
number is highest priority). If a handler function returns
"NO MORE", no more handlers will be called.
This is basically an extension of add_global_handler(), either may
work, though it turns out most modules probably want this one.
"""
if type(events) != list:
events = [events]
handler = PrioritizedRegexHandler(priority, regex, handler)
for event in events:
with self.mutex:
2016-01-16 17:58:11 -06:00
log.debug("in add_global_regex_handler")
event_regex_handlers = self.regex_handlers.setdefault(event, [])
bisect.insort(event_regex_handlers, handler)
def remove_global_regex_handler(self, events, handler):
"""Removes a global regex handler function.
Arguments:
events -- Event type(s) (a list of strings).
handler -- Callback function.
Returns 1 on success, otherwise 0.
"""
ret = 1
if type(events) != list:
events = [events]
for event in events:
with self.mutex:
if event not in self.regex_handlers:
ret = 0
for h in self.regex_handlers[event]:
if handler == h.callback:
self.regex_handlers[event].remove(h)
return ret
def _handle_event(self, connection, event):
"""Handle an Event event incoming on ServerConnection connection.
Also supports regex handlers.
"""
try:
log.debug("EVENT: e[%s] s[%s] t[%s] a[%s]", event.type, event.source,
event.target, event.arguments)
# set up some default stuff
event.recursing = False
event.addressed = False
event.original_msg = None
event.addressed_msg = None
sender_nick = irc.client.NickMask(event.source).nick
sent_location = ircbotlib.reply_destination_for_event(event)
event.sender_nick = sender_nick
event.sent_location = sent_location
event.in_privmsg = sender_nick == sent_location
self.try_recursion(connection, event)
if event.type in ['pubmsg', 'privmsg']:
what = event.arguments[0]
event.original_msg = what
# check if we were addressed or not
all_nicks = '|'.join(connection.server_config.additional_addressed_nicks.split('\n') +
[connection.get_nickname()])
addressed_pattern = r'^(({nicks})[:,]|@({nicks}))\s+(?P<addressed_msg>.*)'.format(nicks=all_nicks)
match = re.match(addressed_pattern, what, re.IGNORECASE)
if match:
event.addressed = True
event.addressed_msg = match.group('addressed_msg')
# only do aliasing for pubmsg/privmsg
log.debug("checking for alias for %s", what)
for alias in Alias.objects.all():
repl = alias.replace(what)
if repl:
# we found an alias for our given string, doing a replace
event.arguments[0] = repl
# recurse, in case there's another alias in this one
return self._handle_event(connection, event)
with self.mutex:
# doing regex version first as it has the potential to be more specific
log.debug("checking regex handlers for %s", event.type)
matching_handlers = sorted(
self.regex_handlers.get("all_events", []) +
self.regex_handlers.get(event.type, [])
)
log.debug("got %d", len(matching_handlers))
for handler in matching_handlers:
log.debug("checking %s vs. %s", handler, event.arguments)
for line in event.arguments:
match = re.search(handler.regex, line)
if match:
log.debug("match!")
result = handler.callback(connection, event, match)
if result == "NO MORE":
return
matching_handlers = sorted(
self.handlers.get("all_events", []) +
self.handlers.get(event.type, [])
)
for handler in matching_handlers:
log.debug("not-match")
result = handler.callback(connection, event)
if result == "NO MORE":
return
except Exception as ex:
log.error("caught exception!")
log.exception(ex)
connection.privmsg(event.target, str(ex))
def try_recursion(self, connection, event):
"""Scan message for subcommands to execute and use as part of this command.
Upon seeing a line intended for this module, see if there are subcommands
that we should do what is basically a text replacement on. The intent is to
allow things like the following:
command arg1 [anothercommand arg1 arg2]
where the output of anothercommand is command's arg2..n.
Args:
connection source connection
event incoming event
"""
2016-01-16 17:58:11 -06:00
log.debug("RECURSING EVENT: e[%s] s[%s] t[%s] a[%s]", event.type, event.source,
event.target, event.arguments)
try:
# begin recursion search
attempt = event.arguments[0]
2016-01-16 17:58:11 -06:00
log.debug("checking it against %s", attempt)
start_idx = attempt.find('[')
subcmd = attempt[start_idx + 1:]
end_idx = subcmd.rfind(']')
subcmd = subcmd[:end_idx]
if start_idx != -1 and end_idx != -1 and len(subcmd) > 0:
2016-01-16 17:58:11 -06:00
log.debug("subcmd: %s", subcmd)
# found recursion candidate
# copy the event and see if IT has recursion to do
newevent = copy.deepcopy(event)
newevent.arguments[0] = subcmd
newevent.recursing = True
2016-01-16 17:58:11 -06:00
log.debug("new event copied")
self.try_recursion(connection, newevent)
# now that we have a string that has been
# recursed all the way deeper into its text, see if any
# modules can do something with it. this calls the same
# event handlers in the same way as if this were a native
# event.
replacements = self.try_to_replace_event_text_with_module_text(connection, newevent)
if replacements:
# we have done all we can do with the sub-event. whatever
# the text of that event now is, we should replace the parent
# event's [] section with it.
oldtext = event.arguments[0]
newtext = oldtext.replace('[' + subcmd + ']', newevent.arguments[0])
log.debug("oldtext: '%s' newtext: '%s'", oldtext, newtext)
event.arguments[0] = newtext
# we have now resolved the []. recursion will unfold, replacing
# it further and further, until we eventually get back to the
# original irc event in _handle_event, which will do one
# last search on the text.
else:
2016-01-16 17:58:11 -06:00
log.debug("no more recursion here")
except IndexError:
2016-01-16 17:58:11 -06:00
log.debug("no more recursion here")
def try_to_replace_event_text_with_module_text(self, connection, event):
"""Do something very similar to _handle_event, but for recursion.
The intent here is that we replace [text] with whatever a module
provides to us.
Args:
connection source connection
event incoming event
"""
replacement = False
replies = []
# only do aliasing for pubmsg/privmsg
if event.type in ['pubmsg', 'privmsg']:
what = event.arguments[0]
2016-01-16 17:58:11 -06:00
log.debug("checking for (recursion) alias for %s", what)
for alias in Alias.objects.all():
repl = alias.replace(what)
if repl:
# we found an alias for our given string, doing a replace
event.arguments[0] = repl
with self.mutex:
# doing regex version first as it has the potential to be more specific
2016-01-16 17:58:11 -06:00
log.debug("checking (recursion) regex handlers for %s", event.type)
matching_handlers = sorted(
self.regex_handlers.get("all_events", []) +
self.regex_handlers.get(event.type, [])
)
2016-01-16 17:58:11 -06:00
log.debug("got %d", len(matching_handlers))
for handler in matching_handlers:
2016-01-16 17:58:11 -06:00
log.debug("checking (recursion) %s vs. %s", handler, event.arguments)
for line in event.arguments:
match = re.search(handler.regex, line)
if match:
2016-01-16 17:58:11 -06:00
log.debug("match (recursion)!")
result = handler.callback(connection, event, match)
2016-01-16 17:58:11 -06:00
log.debug("result: %s", result)
if result:
2016-01-16 17:58:11 -06:00
log.debug("appending %s to replies", result)
replacement = True
replies.append(result)
matching_handlers = sorted(
self.handlers.get("all_events", []) +
self.handlers.get(event.type, [])
)
for handler in matching_handlers:
2016-01-16 17:58:11 -06:00
log.debug("not-match (recursion)")
result = handler.callback(connection, event)
2016-01-16 17:58:11 -06:00
log.debug("result: %s", result)
if result:
2016-01-16 17:58:11 -06:00
log.debug("appending %s to replies", result)
replacement = True
replies.append(result)
if len(replies):
event.arguments[0] = '\n'.join(replies)
return replacement
2015-05-09 18:56:26 -05:00
class IRCBot(irc.client.SimpleIRCClient):
"""A single-server IRC bot class."""
reactor_class = DrReactor
splitter = "..."
def __init__(self, server_name, reconnection_interval=60):
"""Initialize bot."""
2015-05-09 18:56:26 -05:00
super(IRCBot, self).__init__()
self.channels = IRCDict()
self.plugins = []
2015-05-09 18:56:26 -05:00
self.server_config = IrcServer.objects.get(name=server_name)
# the reactor made the connection, save the server reference in it since we pass that around
self.connection.server_config = self.server_config
2015-05-09 18:56:26 -05:00
# set reconnection interval
if not reconnection_interval or reconnection_interval < 0:
reconnection_interval = 2 ** 31
self.reconnection_interval = reconnection_interval
# set basic stuff
self._nickname = self.server_config.nickname
self._realname = self.server_config.realname
2015-05-09 18:56:26 -05:00
# guess at nickmask. hopefully _on_welcome() will set this, but this should be
# a pretty good guess if not
nick = self._nickname
user = self._nickname
host = socket.getfqdn()
self.nickmask = "{0:s}!~{1:s}@{2:s}".format(nick, user, host)
log.debug("guessing at nickmask '{0:s}'".format(self.nickmask))
2015-05-09 18:56:26 -05:00
# handlers
for i in ['disconnect', 'join', 'kick', 'mode', 'namreply', 'nick', 'part', 'quit', 'welcome']:
2015-05-09 18:56:26 -05:00
self.connection.add_global_handler(i, getattr(self, '_on_' + i), -20)
self.connection.reactor.add_global_regex_handler('pubmsg', r'^!load\s+([\S]+)$',
getattr(self, 'handle_load'), -20)
self.connection.reactor.add_global_regex_handler('privmsg', r'^!load\s+([\S]+)$',
getattr(self, 'handle_load'), -20)
self.connection.reactor.add_global_regex_handler('pubmsg', r'^!unload\s+([\S]+)$',
getattr(self, 'handle_unload'), -20)
self.connection.reactor.add_global_regex_handler('privmsg', r'^!unload\s+([\S]+)$',
getattr(self, 'handle_unload'), -20)
self.connection.reactor.add_global_regex_handler('pubmsg', r'^!reload\s+([\S]+)$',
getattr(self, 'handle_reload'), -20)
self.connection.reactor.add_global_regex_handler('privmsg', r'^!reload\s+([\S]+)$',
getattr(self, 'handle_reload'), -20)
2015-05-09 18:56:26 -05:00
# load XML-RPC server
self.xmlrpc = SimpleXMLRPCServer((self.server_config.xmlrpc_host, self.server_config.xmlrpc_port),
requestHandler=IrcBotXMLRPCRequestHandler, allow_none=True)
self.xmlrpc.register_introspection_functions()
2016-01-16 17:58:11 -06:00
t = threading.Thread(target=self._xmlrpc_listen, args=())
t.daemon = True
t.start()
# register XML-RPC stuff
self.xmlrpc_register_function(self.privmsg, 'privmsg')
self.xmlrpc_register_function(self.reply, 'reply')
2015-05-09 18:56:26 -05:00
def _connected_checker(self):
if not self.connection.is_connected():
self.connection.execute_delayed(self.reconnection_interval,
self._connected_checker)
self.jump_server()
def _connect(self):
try:
# build the connection factory as determined by IPV6/SSL settings
if self.server_config.use_ssl:
connect_factory = Factory(wrapper=ssl.wrap_socket, ipv6=self.server_config.use_ipv6)
2015-05-09 18:56:26 -05:00
else:
connect_factory = Factory(ipv6=self.server_config.use_ipv6)
2015-05-09 18:56:26 -05:00
self.connect(self.server_config.hostname, self.server_config.port, self._nickname,
self.server_config.password, ircname=self._realname, connect_factory=connect_factory)
2015-05-09 18:56:26 -05:00
except irc.client.ServerConnectionError:
pass
def _on_disconnect(self, c, e):
self.channels = IRCDict()
self.connection.execute_delayed(self.reconnection_interval,
self._connected_checker)
def _on_join(self, c, e):
ch = e.target
nick = e.source.nick
if nick == c.get_nickname():
self.channels[ch] = Channel()
self.channels[ch].add_user(nick)
def _on_kick(self, c, e):
nick = e.arguments[0]
channel = e.target
if nick == c.get_nickname():
del self.channels[channel]
else:
self.channels[channel].remove_user(nick)
def _on_mode(self, c, e):
modes = irc.modes.parse_channel_modes(" ".join(e.arguments))
t = e.target
if irc.client.is_channel(t):
ch = self.channels[t]
for mode in modes:
if mode[0] == "+":
f = ch.set_mode
else:
f = ch.clear_mode
f(mode[1], mode[2])
else:
# Mode on self... XXX
pass
def _on_namreply(self, c, e):
"""Get the list of names in a channel.
e.arguments[0] == "@" for secret channels,
"*" for private channels,
"=" for others (public channels)
e.arguments[1] == channel
e.arguments[2] == nick list
"""
ch_type, channel, nick_list = e.arguments
if channel == '*':
# User is not in any visible channel
# http://tools.ietf.org/html/rfc2812#section-3.2.5
return
for nick in nick_list.split():
nick_modes = []
if nick[0] in self.connection.features.prefix:
nick_modes.append(self.connection.features.prefix[nick[0]])
nick = nick[1:]
for mode in nick_modes:
self.channels[channel].set_mode(mode, nick)
self.channels[channel].add_user(nick)
def _on_nick(self, c, e):
before = e.source.nick
after = e.target
2016-01-16 17:58:11 -06:00
for ch in list(self.channels.values()):
2015-05-09 18:56:26 -05:00
if ch.has_user(before):
ch.change_nick(before, after)
def _on_part(self, c, e):
nick = e.source.nick
channel = e.target
if nick == c.get_nickname():
del self.channels[channel]
else:
self.channels[channel].remove_user(nick)
def _on_quit(self, c, e):
nick = e.source.nick
2016-01-16 17:58:11 -06:00
for ch in list(self.channels.values()):
2015-05-09 18:56:26 -05:00
if ch.has_user(nick):
ch.remove_user(nick)
def _on_welcome(self, connection, event):
"""Initialize/run a bunch of on-connection stuff.
Set the nickmask that the ircd tells us is us, autojoin channels, etc.
Args:
connection source connection
event incoming event
"""
what = event.arguments[0]
log.debug("welcome: %s", what)
# run automsg commands
for cmd in self.server_config.post_connect.split('\n'):
# TODO NOTE: if the bot is sending something that changes the vhost
# (like 'hostserv on') we don't pick it up
self.connection.privmsg(cmd.split(' ')[0], ' '.join(cmd.split(' ')[1:]))
# sleep before doing autojoins
time.sleep(self.server_config.delay_before_joins)
for chan in IrcChannel.objects.filter(autojoin=True):
2016-01-16 17:58:11 -06:00
log.info("autojoining %s", chan.name)
self.connection.join(chan)
for plugin in IrcPlugin.objects.filter(autoload=True):
2016-01-16 17:58:11 -06:00
log.info("autoloading %s", plugin.path)
self._load_plugin(connection, event, plugin.path, feedback=False)
match = re.search(r'(\S+!\S+@\S+)', what)
if match:
self.nickmask = match.group(1)
log.debug("setting nickmask: %s", self.nickmask)
2015-05-09 18:56:26 -05:00
def die(self, msg="Bye, cruel world!"):
"""Let the bot die.
Arguments:
msg -- Quit message.
"""
self._xmlrpc_shutdown()
2015-05-09 18:56:26 -05:00
self.connection.disconnect(msg)
sys.exit(0)
def disconnect(self, msg="I'll be back!"):
"""Disconnect the bot.
The bot will try to reconnect after a while.
Arguments:
msg -- Quit message.
"""
self.connection.disconnect(msg)
def get_version(self):
"""Returns the bot version.
Used when answering a CTCP VERSION request.
"""
return "Python irc.bot ({version})".format(
version=irc.client.VERSION_STRING)
def jump_server(self, msg="Changing servers"):
"""Connect to a new server, potentially disconnecting from the current one."""
if self.connection.is_connected():
self.connection.disconnect(msg)
self._connect()
def on_ctcp(self, c, e):
"""Default handler for ctcp events.
Replies to VERSION and PING requests and relays DCC requests
to the on_dccchat method.
"""
nick = e.source.nick
if e.arguments[0] == "VERSION":
c.ctcp_reply(nick, "VERSION " + self.get_version())
elif e.arguments[0] == "PING":
if len(e.arguments) > 1:
c.ctcp_reply(nick, "PING " + e.arguments[1])
elif e.arguments[0] == "DCC" and e.arguments[1].split(" ", 1)[0] == "CHAT":
self.on_dccchat(c, e)
def on_dccchat(self, c, e):
"""Do nothing."""
2015-05-09 18:56:26 -05:00
pass
def handle_load(self, connection, event, match):
"""Handle IRC requests to load a plugin.
This loads a plugin, as requested, and the interpreter takes care of
loading dependencies.
:param connection: connection for this load request
:type connection: LenientServerConnection
:param event: associated irc event object
:type event: Event
:param match: the matched regex (via add_global_regex_handler)
:type match: Match
"""
has_perm = ircbotlib.has_permission(event.source, 'ircbot.manage_loaded_plugins')
2016-01-16 17:58:11 -06:00
log.debug("has permission to load?: %s", str(has_perm))
if has_perm:
plugin_path = match.group(1)
2016-01-16 17:58:11 -06:00
log.debug("calling _load_plugin on %s", plugin_path)
self._load_plugin(connection, event, plugin_path)
def _load_plugin(self, connection, event, plugin_path, feedback=True):
"""Load an IRC plugin.
A plugin's init must initialize its hooks and handlers as references to this bot, this
is handed off but otherwise nothing is done with that here, this just loads it from a
Python sense.
:param connection: connection for this load request
:type connection: LenientServerConnection
:param event: associated irc event object
:type event: Event
:param plugin_path: path (likely a relative one) of the plugin to attempt to load
:type plugin_path: str
:param feedback: whether or not to send messages to IRC regarding the load attempt
:type feedback: bool
"""
log.debug("trying to load plugin %s", plugin_path)
dest = None
if feedback:
dest = ircbotlib.reply_destination_for_event(event)
# loop over the sys.modules entries, for debugging
log.debug("loaded modules:")
for module_name, module in sys.modules.items():
log.debug(" %s", module_name)
for path, plugin in self.plugins:
log.debug("looping already loaded plugin: %s %s", plugin, path)
if plugin_path == path:
if feedback:
self.privmsg(dest, "Plugin '{0:s}' is already loaded.".format(plugin_path))
return
# not loaded, let's get to work
try:
__import__(plugin_path)
module = sys.modules[plugin_path]
plugin = module.plugin(self, connection, event)
plugin.start()
self.plugins.append((plugin_path, plugin))
# give it a model
plugin_model, c = IrcPlugin.objects.get_or_create(path=plugin_path)
if feedback:
self.privmsg(dest, "Plugin '{0:s}' loaded.".format(plugin_path))
except ImportError as e:
log.error("Error loading '{0:s}'".format(plugin_path))
log.exception(e)
# i don't think this would get populated if __import__ failed del sys.modules[plugin_path]
if feedback:
self.privmsg(dest, "Plugin '{0:s}' could not be loaded: {1:s}".format(plugin_path, str(e)))
except AttributeError as e:
log.error("Error loading '{0:s}'".format(plugin_path))
log.exception(e)
del sys.modules[plugin_path]
if feedback:
self.privmsg(dest, "Plugin '{0:s}' could not be loaded: {1:s}".format(plugin_path, str(e)))
# loop over the sys.modules entries, for debugging
log.debug("new list of loaded modules:")
for module_name, module in sys.modules.items():
log.debug(" %s", module_name)
def handle_reload(self, connection, event, match):
"""Handle IRC requests to reload a plugin (module or package).
This allows code for a plugin (and potentially also its sibling modules in the package)
to be reloaded on demand, in the event that changes were made.
:param connection: connection for this reload request
:type connection: LenientServerConnection
:param event: associated irc event object
:type event: Event
:param match: the matched regex (via add_global_regex_handler)
:type match: Match
"""
has_perm = ircbotlib.has_permission(event.source, 'ircbot.manage_loaded_plugins')
log.debug("has permission to reload?: %s", str(has_perm))
if has_perm:
plugin_path = match.group(1)
log.debug("calling _reload_plugin on %s", plugin_path)
self._reload_plugin(connection, event, plugin_path)
def _reload_plugin(self, connection, event, plugin_path, feedback=True):
"""Execute the built-in reload() on a plugin (module) or it and its siblings (package).
:param connection: connection for this load request
:type connection: LenientServerConnection
:param event: associated irc event object
:type event: Event
:param plugin_path: path (likely a relative one) of the plugin to attempt to reload
:type plugin_path: str
:param feedback: whether or not to send messages to IRC regarding the reload attempt
:type feedback: bool
"""
log.debug("trying to unload plugin %s", plugin_path)
dest = ircbotlib.reply_destination_for_event(event)
# loop over the sys.modules entries, for debugging
log.debug("loaded modules:")
for module_name, module in sys.modules.items():
log.debug(" %s", module_name)
# make sure what was provided was a legitimate plugin
# (so that we don't go reloading totally arbitrary code)
search_path = None
for path, plugin in self.plugins:
# first check exact path
if path == plugin_path:
log.debug("requested %s is %s, so we will try to reload %s",
plugin_path, path, plugin_path)
search_path = plugin_path
if plugin_path[-1] == '.' and path.startswith(plugin_path):
log.debug("requested %s is substring of %s, so we will try to reload all %s items",
plugin_path, path, plugin_path)
search_path = plugin_path[:-1]
# we need to know the plugin names to do some extra logic with reload
plugin_names = [x[0] for x in self.plugins]
# do reload(s)
if search_path:
plugins_to_reload = []
for module_name, module in sys.modules.items():
if module_name.startswith(search_path):
if module_name in plugin_names:
# we need to take the plugin reload path for this, but not
# before all other code is reloaded, so we save this for the end
# if we did this here, the ircplugin might still reference the old
# version of its dependencies
plugins_to_reload.append(module_name)
else:
log.debug("reloading %s", module_name)
importlib.reload(module)
if feedback:
self.privmsg(dest, "'{0:s}' reloaded.".format(module_name))
# the references to event handlers will be stale (reload() doesn't go that deep)
# so we need to tear down and reinitialize plugins