Extending XMPP and Ruby
After writing up my XMPP Agent written in Ruby, my friend Billy wrote a comment about a similar system written by Michael Still the author of Practical MythTV. His system for controlling MythTV over XMPP is the gtalkbot.
Curious as to how he worked around some issues, I downloaded the Python source code and stumbled my way through it. I was struck by the fact that he used a plugin architecture that would allow other authors to extend the functionality of his original program.
After seeing that, I decided that I would try something similar.
Without worrying about the XMPP Agent, I set out to code a plugin architecture. In a directory I have the following files:
- init.rb (should probably be called plugin_loader.rb or something more descriptive)
- test.rb
- /plugins (this is a directory)
- network.rb (inside the plugins directory)
- system.rb (inside the plugins directory)
The code inside the test.rb file is simple enough:
require 'init'puts “This is the allowed command set”
puts ALLOWED_COMMANDS
The first line of the code, loads the init.rb file which does the heavy lifting. The init.rb file has a plugin class defined. Other plugins will inherit from this class.
The plugin class is defined as:
require 'find'
class Plugin
attr_reader :allowed_commands
def initialize
@allowed_commands = %w{ iisreset }
end
def load_plugins(dir, name="/^[a-z]+.rb/")
plugins = []
Find.find(dir) do |path|
Find.prune if [".",".."].include? path
plugins << path if File.basename(path).include? ".rb"
end
plugins.each do |item|
puts "Loading plugin => #{item}"
#Load the file
require File.join(File.dirname(__FILE__), item)
#Extract the file name from the directory name (without .rb extension)
file_name = File.basename(item, ".rb")
#Create a new object and instantiate it and find the allowed_methods attribute
c = Object.const_get(file_name.capitalize).new
puts "Loading command set for plugin #{file_name.capitalize} "
puts c.allowed_commands
#Add it to the array for allowed_commands
@allowed_commands << c.allowed_commands
end
end
def run_command(command)
#Strip the command part out of the string - we don't need it anymore
command.slice!("command: ")
#Create an array for the arguments
arguments = command.split(" ")
arguments.delete_at(0) #Delete the first index, this is the command itself without arguments
#Loop through the arguments and then delete them from the command string
arguments.each { |item| command.slice!(item) }
if @allowed_commands.include? command
puts "#{command} is an allowed command"
result = `#{command} #{arguments.join(" ")}` #Backticks are a shortcut for system(yourcommand).
else
result = "#{command} cannot be run"
end
return result
end
end
At the end of the init.rb file is the code that actually starts loading the plugins:
app_plugins = Plugin.new app_plugins.load_plugins("./plugins") ALLOWED_COMMANDS = app_plugins.allowed_commands
Now it’s easy to extend the test.rb script by instantiating a new System object (one of our plugins) just by this simple code in the test.rb file:
tester = System.new
I can then call methods defined in the System.rb file (shown below):
tester.test
System.rb file:
class System < Plugin
attr_reader :allowed_commands
def initialize
@allowed_commands = %w{ set shutdown }
end
def test
"This is a test"
end
end
Now all of this was just really another proof of concept to find out if it could be done. I’m happy with the results even though I’m sure that there is a better way of doing it.