# pylint: disable=no-self-use,too-few-public-methods """ Confload: Create dotfiles-managable weechat configs with password manager integration. * reads config file containing weechat commands with m4 macro processor * config file is expected to be in $WEECHAT_HOME/weechatrc * provides special m4 macro KEEPASS(, <attr>) to read KeePassXC database * specify KeePassXC files with KEEPASSXC_DATABASE and KEEPASSXC_KEYFILE env vars """ from subprocess import check_output, CalledProcessError from os import getenv from os.path import join from shlex import quote from abc import ABC import weechat as w # pylint: disable=import-error import __main__ DATABASE = getenv("KEEPASSXC_DATABASE", "~/database.kdbx") KEYFILE = getenv("KEEPASSXC_KEYFILE", "") TEMPLATE = join(getenv("WEECHAT_HOME", join(getenv("HOME"), ".weechat")), "weechatrc") class Confload(): """Preprocess config file and execute execute commands.""" keexc = "keepassxc-cli show --attributes $2 --key-file '{}' --quiet '{}' $1 <<< {}" abort = "ifelse(eval(sysval != 0), 1, `m4exit(sysval)')dnl" commands = None def preprocess(self, password): """Preprocess weechat config file.""" keexc = self.keexc.format(KEYFILE, DATABASE, quote(password)) try: self.commands = check_output(["m4", "-DKEEPASS=syscmd(" + keexc + ")" + self.abort, TEMPLATE]) return True except CalledProcessError: return False def execute_commands(self): """Execute weechat commands.""" for command in self.commands.splitlines(): if command.strip(): w.command("", command.decode()) class CallbackCreator(ABC): """Base class used by classes who must specify weechat callbacks as strings.""" def callback(self, method): """This function will take a bound method or function and make it a callback.""" name = str(id(method)) setattr(__main__, name, method) return name class CommandAdder(CallbackCreator): """Add weechat command to trigger confload manually.""" confload = Confload() def add_command(self): """Add command and create callbacks.""" name = "confload" desc = "Load $WEECHAT_HOME/weechatrc. Expects KeePassXC passphrase" args = "<passphrase>" w.hook_command(name, desc, args, '', '', self.callback(self.command_cb), '') w.hook_modifier("input_text_display", self.callback(self.passwd_conceil_cb), "") def command_cb(self, data, buffer, password): """Callback for command hook which triggers processing of config file.""" if self.confload.preprocess(password): self.confload.execute_commands() return w.WEECHAT_RC_OK w.prnt("", "{}Something went wrong! Maybe wrong password?".format(w.prefix("error"))) return w.WEECHAT_RC_ERROR def passwd_conceil_cb(self, data, modifier, modifier_data, string): """Callback for hook which hides password when entering command.""" prefix = "/confload " if string.startswith(prefix): return prefix + (len(string) - len(prefix)) * "*" return string class Initializer(CallbackCreator): """Class which initializes stuff when loading confload for the first time.""" option = "initialized" confload = Confload() passwd_conceil_hook = None passwd_grab_hook = None @staticmethod def initialized(): """Returns true when loading confload for the first time else false.""" return w.config_get_plugin(Initializer.option) == "on" def initialize(self): """Create hooks to grab all user input to obtain KeePassXC password.""" w.config_set_desc_plugin( self.option, "If not yet initialized, run once. (default: \"off\")") self.passwd_conceil_hook = w.hook_modifier( "input_text_display_with_cursor", self.callback(self.passwd_conceil_cb), "") self.passwd_grab_hook = w.hook_modifier( "input_text_for_buffer", self.callback(self.passwd_grab_cb), "") def is_command(self, cmd): """Check if string is a valid weechat command.""" if cmd.startswith("/") and not cmd.startswith("//"): return True return False def passwd_conceil_cb(self, data, modifier, modifier_data, string): """Add a prompt to notify user and mask all user input.""" prompt = f"Enter password to unlock {DATABASE}: " cursor = "b#" if self.is_command(string.lstrip(cursor)): return prompt + string pos = string.find(cursor) return prompt + pos * "*" + cursor + (len(string) - 3 - pos) * "*" def passwd_grab_cb(self, data, modifier, modifier_data, string): """Check if user input is a potential password and try to load config.""" if self.is_command(string): return string if self.confload.preprocess(string.replace("/", "", 1)): self.confload.execute_commands() self.cleanup_and_finish() return "/input delete_line" w.prnt("", "{}Something went wrong! Maybe wrong password?".format(w.prefix("error"))) return "" def cleanup_and_finish(self): """Remove all hooks and set a config option to remember that confload is initialized.""" w.unhook(self.passwd_conceil_hook) w.unhook(self.passwd_grab_hook) w.config_set_plugin(self.option, "on") def register_script(): """Register the script for weechat.""" name = "confload" author = "potamides" version = "0.1" license_ = "GPL3" desc = __doc__ return w.register(name, author, version, license_, desc, "", "") if __name__ == "__main__" and register_script(): if not Initializer.initialized(): Initializer().initialize() CommandAdder().add_command()