godobject/smuxi-hooks-ruby

View on GitHub
lib/god_object/smuxi_hooks/plugin.rb

Summary

Maintainability
A
1 hr
Test Coverage
# encoding: UTF-8
=begin
Copyright GodObject Team <dev@godobject.net>, 2015

This file is part of Smuxi hooks API for Ruby.

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
=end

require 'pathname'
require 'set'
require 'yaml'

module GodObject
  module SmuxiHooks

    # Base class that implements basic features of a Smuxi plugin.
    #
    # @note This class is supposed to be an abstract base class and is not
    #   meant to produce instances directly.
    class Plugin

      # @return [String] name of the base configuration directory
      BASE_DIRECTORY_NAME = 'smuxi'

      # @return [String] name of the sub-directory containing the hook handlers
      HOOKS_DIRECTORY_NAME = 'hooks'

      # @return [String] name of the sub-directory containing plugin state
      STATE_DIRECTORY_NAME = 'hook-state'
   
      # @return [String] name of the file in which the permanent state is held
      STATE_FILE_NAME = 'state.yml'

      # @return [{String => Symbol}] maps Smuxi hook names to named of the
      #   method supposed to handle that hook
      HOOK_TABLE = {
        'engine/protocol-manager/on-connected'        => :connected,
        'engine/protocol-manager/on-disconnected'     => :disconnected,
        'engine/protocol-manager/on-message-received' => :message_received,
        'engine/protocol-manager/on-message-sent'     => :message_sent,
        'engine/protocol-manager/on-presence-status-changed' => :presence_status_changed,
        'engine/session/on-group-chat-person-added'   => :chat_person_added,
        'engine/session/on-group-chat-person-removed' => :chat_person_removed,
        'engine/session/on-group-chat-person-updated' => :chat_person_updated,
        'engine/session/on-event-message'             => :event_message 
      }

      # @return [{String => Symbol}] maps Smuxi environment variable names to
      #   instance variables which will inhert the value supplied by Smuxi
      VARIABLE_TABLE = {
        'SMUXI_CHAT_ID'   => :@chat_id,
        'SMUXI_CHAT_NAME' => :@chat_name,
        'SMUXI_CHAT_TYPE' => :@chat_type,
        'SMUXI_MSG'                     => :@message,
        'SMUXI_MSG_TYPE'                => :@message_type,
        'SMUXI_MSG_TIMESTAMP_UNIX'      => :@message_timestamp_unix,
        'SMUXI_MSG_TIMESTAMP_ISO_UTC'   => :@message_timestamp_iso_utc,
        'SMUXI_MSG_TIMESTAMP_ISO_LOCAL' => :@message_timestamp_iso_local,
        'SMUXI_SENDER'   => :@sender,
        'SMUXI_RECEIVER' => :@receiver,
        'SMUXI_PROTOCOL_MANAGER_TYPE'            => :@protocol_manager_type,
        'SMUXI_PROTOCOL_MANAGER_PROTOCOL'        => :@protocol_manager_protocol,
        'SMUXI_PROTOCOL_MANAGER_NETWORK'         => :@protocol_manager_network,
        'SMUXI_PROTOCOL_MANAGER_HOST'            => :@protocol_manager_host,
        'SMUXI_PROTOCOL_MANAGER_PORT'            => :@protocol_manager_port,
        'SMUXI_PROTOCOL_MANAGER_ME_ID'           => :@protocol_manager_me_id,
        'SMUXI_PROTOCOL_MANAGER_PRESENCE_STATUS' => :@protocol_manager_presence_status,
        'SMUXI_PRESENCE_STATUS_CHANGED_OLD_STATUS'  => :@presence_status_old,
        'SMUXI_PRESENCE_STATUS_CHANGED_NEW_STATUS'  => :@presence_status_new,
        'SMUXI_PRESENCE_STATUS_CHANGED_NEW_MESSAGE' => :@presence_status_new_message,
        'SMUXI_CMD'              => :@command,
        'SMUXI_CMD_PARAMETER'    => :@command_parameter,
        'SMUXI_CMD_CHARACTER'    => :@command_character,
        'SMUXI_FRONTEND_VERSION' => :@frontend_version,
        'SMUXI_ENGINE_VERSION'   => :@engine_version 
      }

      class << self

        # Decides whether to execute a plugin hook handler or plugin maintenance
        # features.
        #
        # In case the first command line argument is `install` or `uninstall`,
        # the plugin installation or uninstallation maintenance methods are
        # executed.
        #
        # Otherwise, the plugin expects to be called as a hook handler by Smuxi.
        #
        # @param [String] executable_path path of the plugin executable file
        # @param [{String => String}] environment the environment variables
        #   available to the plugin, defaults to the actual system environment
        # @param [Array<String>] arguments list of command line arguments given
        #   to the plugin, defaults to the actual command line arguments
        # @return [void]
        def execute(executable_path, environment = ENV, arguments = ARGV)
          case arguments.first
          when 'install'
            cli_install(executable_path)
          when 'uninstall'
            cli_uninstall(executable_path)
          else
            execute_hook(executable_path, environment)
          end
        end

        # The name of the hook executed is detected through the name of the
        # executable called. A plugin instance will be created and the hook
        # name is then used to decide which instance method is called on that.
        #
        # @param [String] executable_path path of the plugin executable file
        # @param [{String => String}] environment the environment variables
        #   available to the plugin, defaults to the actual system environment
        # @return [void]
        def execute_hook(executable_path, environment = ENV)
          split_pattern = /\/#{BASE_DIRECTORY_NAME}\/#{HOOKS_DIRECTORY_NAME}\//
          config_directory, relative_executable = executable_path.
                                                  split(split_pattern)

          base_directory      = Pathname.new(config_directory) +
                                BASE_DIRECTORY_NAME
          relative_executable = Pathname.new(relative_executable)
          script_name         = relative_executable.basename.to_s
          hook_name           = relative_executable.dirname.to_s
          state_directory     = base_directory +
                                STATE_DIRECTORY_NAME +
                                hook_name +
                                script_name

          state_file = state_directory + STATE_FILE_NAME

          if state_file.exist?
            state = YAML.load(state_file.read)
          else
            state = {}
          end

          instance = new(
            environment:     environment,
            base_directory:  base_directory,
            script_name:     script_name,
            hook_name:       hook_name,
            state_directory: state_directory,
            state:           state
          )

          if method_name = HOOK_TABLE[hook_name]
            instance.public_send(method_name)

            state_file.open('w') do |io|
              io.write(YAML.dump(instance.state))
            end
          else
            raise "Hook `#{hook_name}` unsupported"
          end
        end

        # Installs the plugin by placing symlinks to the plugin executable into
        # Smuxi's hook handler paths.
        #
        # @note This method outputs to STDOUT and exits the program after it
        #   finishes.
        #
        # @param [String] executable_path path of the plugin executable file
        # @return [void]
        def cli_install(executable_path)
          puts "Creating symlinks at the Smuxi plugin hook locations…"
          puts

          install(executable_path) do |hook_executable_file|
            puts "Creating `#{hook_executable_file}`"
          end

          puts
          puts "Plugin `#{name}` installed."

          exit
        end

        # Uninstalls the plugin by removing symlinks to the plugin executable
        # from Smuxi's hook handler paths.
        #
        # @note This method outputs to STDOUT and exits the program after it
        #   finishes.
        #
        # @param [String] executable_path path of the plugin executable file
        # @return [void]
        def cli_uninstall(executable_path)
          puts "Trying to remove hook symlinks…"
          puts
          
          uninstall(executable_path) do |hook_executable_file|
            puts "Removing `#{hook_executable_file}`"
          end

          puts
          puts "Plugin `#{name}` uninstalled."

          exit
        end

        # @return [Pathname] path to the base directory
        def base_directory_guess
          Pathname.new('~/.local/share').expand_path + BASE_DIRECTORY_NAME
        end

        # @return [Array<Symbol>] a list of the names of all hook handler
        #   instance methods implemented by this class
        def used_hook_methods
          instance_methods.to_set.intersection(HOOK_TABLE.values.to_set)
        end

        # @return [Array<String>] a list of the names of all hooks supported by
        #   this class
        def used_hook_names
          inverted_hook_table = HOOK_TABLE.invert

          used_hook_methods.map do |method_name|
            inverted_hook_table[method_name]
          end
        end

        # @return [Array<String>] a list of the Smuxi hook paths this plugin
        #   needs to be linked in
        # @param [Pathname] base_directory the Smuxi configuration directory.
        #   The hooks directory is expected to reside here
        def used_hook_paths(base_directory = base_directory_guess)
          hooks_directory = base_directory + HOOKS_DIRECTORY_NAME
          
          used_hook_names.map do |hook_name|
            hooks_directory + hook_name
          end
        end

        # Installs the plugin by placing symlinks to the plugin executable into
        # Smuxi's hook handler paths.
        #
        # @param [String] executable_path path of the plugin executable file
        # @param [Pathname] base_directory the configuration directory for
        #   Smuxi. The hooks sub-directory is expected to reside here
        # @return [void]
        def install(executable_path, base_directory = base_directory_guess, &block)
          executable_file = Pathname.new(executable_path).expand_path
          executable_name = executable_file.basename.to_s

          used_hook_paths(base_directory).each do |hook_path|
            hook_path.mkpath
            hook_executable_file = hook_path + executable_name 
            block.call(hook_executable_file) if block
            hook_executable_file.unlink if hook_executable_file.symlink?
            hook_executable_file.make_symlink(executable_file)
          end
        end

        # Uninstalls the plugin by removing symlinks to the plugin executable
        # from Smuxi's hook handler paths.
        #
        # @param [String] executable_path path of the plugin executable file
        # @param [Pathname] base_directory the configuration directory for
        #   Smuxi. The hooks sub-directory is expected to reside here
        # @yield [hook_executable
        # @return [void]
        def uninstall(executable_path, base_directory = base_directory_guess, &block)
          executable_file = Pathname.new(executable_path).expand_path
          executable_name = executable_file.basename.to_s

          used_hook_paths(base_directory).each do |hook_path|
            hook_executable_file = hook_path + executable_name
            block.call(hook_executable_file) if block
            hook_executable_file.unlink if hook_executable_file.symlink?
          end
        end

      end

      attr_reader :state

      # Initializes the plugin.
      #
      # Known variables' values from the supplied environment will be transferred
      # to the respective instance variables. 
      #
      # @see VARIABLE_TABLE
      #
      # @param [Hash] options
      # @option options [{String => String}] environment environment variables for the plugin execution
      # @option options [Pathname] base_directory the configuration directory for
      #   Smuxi. The hooks and hook-state sub-directories are expected to reside
      #   here
      # @option options [Pathname] state_directory the directory that Smuxi
      #   reserved for this plugin to store its state.
      # @option options [String] hook_name name of the Smuxi hook that is executed
      # @option options [String] script_name name of plugin hook executable, without path
      def initialize(options = {})
        @environment     = options[:environment]
        @base_directory  = options[:base_directory]
        @state_directory = options[:state_directory]
        @state           = options[:state]
        @hook_name       = options[:hook_name]
        @script_name     = options[:script_name]

        VARIABLE_TABLE.each do |environment_variable, instance_variable|
          instance_variable_set(instance_variable,
                                @environment[environment_variable])
        end
      end

      # Calls a command in Smuxi
      #
      # @param [String] type the command type
      # @param [Symbol, String] name the command name
      # @param [String] data the argument data added to the command
      # @return [void]
      def command(type, name, data = nil)
        command = "#{type} /#{name}"
        command += " #{data}" if data
        
        STDOUT.puts(command)
      end

      # Calls a session command in Smuxi
      #
      # @param [Symbol, String] name the command name
      # @param [String] data the argument data added to the command
      # @return [void]
      def session_command(name, data = nil)
        command('Session.Command', name, data)
      end

      # Calls a protocol manager command in Smuxi
      #
      # @param [Symbol, String] name the command name
      # @param [String] data the argument data added to the command
      # @return [void]
      def protocol_manager_command(name, data = nil)
        command('ProtocolManager.Command', name, data)
      end

      # Outputs a message to the Smuxi user
      #
      # @param [String] message the text message sent to the Smuxi user
      def puts(message)
        session_command(:echo, message)
      end

    end

  end
end