robut 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. data/.gitignore +6 -0
  2. data/Gemfile +17 -0
  3. data/README.rdoc +124 -0
  4. data/Rakefile +27 -0
  5. data/bin/robut +9 -0
  6. data/examples/Chatfile +22 -0
  7. data/lib/robut.rb +5 -0
  8. data/lib/robut/connection.rb +167 -0
  9. data/lib/robut/plugin.rb +16 -0
  10. data/lib/robut/plugin/base.rb +70 -0
  11. data/lib/robut/plugin/calc.rb +20 -0
  12. data/lib/robut/plugin/echo.rb +14 -0
  13. data/lib/robut/plugin/later.rb +73 -0
  14. data/lib/robut/plugin/lunch.rb +57 -0
  15. data/lib/robut/plugin/meme.rb +30 -0
  16. data/lib/robut/plugin/ping.rb +11 -0
  17. data/lib/robut/plugin/rdio.rb +70 -0
  18. data/lib/robut/plugin/rdio/public/css/rdio.css +141 -0
  19. data/lib/robut/plugin/rdio/public/css/style.css +79 -0
  20. data/lib/robut/plugin/rdio/public/css/style_after.css +42 -0
  21. data/lib/robut/plugin/rdio/public/images/background.png +0 -0
  22. data/lib/robut/plugin/rdio/public/images/no-album.png +0 -0
  23. data/lib/robut/plugin/rdio/public/index.html +42 -0
  24. data/lib/robut/plugin/rdio/public/js/libs/dd_belatedpng.js +13 -0
  25. data/lib/robut/plugin/rdio/public/js/libs/jquery-1.5.1.min.js +16 -0
  26. data/lib/robut/plugin/rdio/public/js/libs/modernizr-1.7.min.js +2 -0
  27. data/lib/robut/plugin/rdio/public/js/rdio.js +129 -0
  28. data/lib/robut/plugin/rdio/public/js/script.js +3 -0
  29. data/lib/robut/plugin/rdio/server.rb +34 -0
  30. data/lib/robut/plugin/say.rb +20 -0
  31. data/lib/robut/plugin/sayings.rb +31 -0
  32. data/lib/robut/plugin/twss.rb +13 -0
  33. data/lib/robut/storage.rb +3 -0
  34. data/lib/robut/storage/base.rb +21 -0
  35. data/lib/robut/storage/hash_store.rb +26 -0
  36. data/lib/robut/storage/yaml_store.rb +51 -0
  37. data/lib/robut/version.rb +4 -0
  38. data/robut.gemspec +23 -0
  39. data/test/mocks/connection_mock.rb +26 -0
  40. data/test/simplecov_helper.rb +2 -0
  41. data/test/test_helper.rb +7 -0
  42. data/test/unit/connection_test.rb +123 -0
  43. data/test/unit/plugin/base_test.rb +16 -0
  44. data/test/unit/plugin/echo_test.rb +26 -0
  45. data/test/unit/plugin/later_test.rb +39 -0
  46. data/test/unit/plugin/lunch_test.rb +35 -0
  47. data/test/unit/plugin/ping_test.rb +21 -0
  48. data/test/unit/plugin/say_test.rb +28 -0
  49. data/test/unit/storage/hash_store_test.rb +15 -0
  50. data/test/unit/storage/yaml_store_test.rb +42 -0
  51. data/test/unit/storage/yaml_test.yml +1 -0
  52. metadata +135 -0
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ doc/*
6
+ coverage/*
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in robut.gemspec
4
+ gemspec
5
+ gem 'rake'
6
+
7
+ group :test do
8
+ gem 'simplecov'
9
+ end
10
+
11
+ group :plugin do
12
+ gem 'calc'
13
+ gem 'twss'
14
+ gem 'rdio'
15
+ gem 'sinatra'
16
+ gem 'meme_generator'
17
+ end
data/README.rdoc ADDED
@@ -0,0 +1,124 @@
1
+ = Robut
2
+
3
+ The friendly plugin-enabled HipChat bot.
4
+
5
+ == Installation and usage
6
+
7
+ Robut can be installed by running <tt>gem install robut</tt>. This
8
+ installs the +robut+ binary. When run, +robut+ reads a Chatfile,
9
+ connects to the specified HipChat server and chatroom, and feeds every
10
+ line said in the chatroom through the plugins configured by the
11
+ Chatfile.
12
+
13
+ Once robut is running, the plugins listen to what's being said in the
14
+ chatroom. Most plugins listen for @replies to robut:
15
+
16
+ @robut lunch? # => "Banh Mi!"
17
+ @robut calc 1 + 1 # => 2
18
+
19
+ Others listen to everything, and don't require an @reply.
20
+
21
+ Some of the included plugins require extra gems to be installed:
22
+
23
+ [Robut::Plugin::TWSS] requires the <tt>twss</tt> gem
24
+ [Robut::Plugin::Meme] requires the <tt>meme_generator</tt> gem
25
+ [Robut::Plugin::Calc] requires the <tt>calc</tt> gem
26
+ [Robut::Plugin::Rdio] requires the <tt>rdio</tt> and <tt>sinatra</tt> gems
27
+
28
+ == The Chatfile
29
+
30
+ When the +robut+ command runs, it looks for and evals ruby code in a
31
+ file called +Chatfile+ in the current directory. You can override the
32
+ configuration file by passing +robut+ a path to a Chatfile as the
33
+ first parameter:
34
+
35
+ robut /path/to/Chatfile
36
+
37
+ The Chatfile is just ruby code. A simple example can be found here: Chatfile[https://github.com/justinweiss/robut/blob/master/examples/Chatfile]
38
+
39
+ === Adding and configuring plugins
40
+
41
+ Plugins are ruby classes, so enabling a plugin just requires requiring
42
+ the plugin file, optionally configuring the plugin class, and adding
43
+ the class to the global plugin list:
44
+
45
+ require 'robut/plugin/lunch'
46
+ Robut::Plugin::Lunch.places = ["Banh Mi", "Mad Oven", "Mod Pizza", "Taphouse"]
47
+ Robut::Plugin.plugins << Robut::Plugin::Lunch
48
+
49
+ Each plugin can be configured differently, or not at all. It's best to
50
+ look at the docs for the plugins you want to use to figure out what
51
+ kind of configuration they support.
52
+
53
+ Some plugins might require storage (like the `lunch` plugin). You can
54
+ configure the type of storage you want to use based on the need for
55
+ persistence. The default is the HashStore which is in-memory only. Below
56
+ is an example of using the YamlStore.
57
+
58
+ Robut::Connection.configure do |config|
59
+ # ...
60
+ Robut::Storage::YamlStore.file = "~/.robut_store"
61
+ config.store = Robut::Storage::YamlStore
62
+ end
63
+
64
+
65
+ === Configuring the HipChat connection
66
+
67
+ The Chatfile also configures the HipChat connection. This is done in a
68
+ Robut::Connection.configure block:
69
+
70
+ # Configure the robut jabber connection and you're good to go!
71
+ Robut::Connection.configure do |config|
72
+ config.jid = '...@chat.hipchat.com/bot'
73
+ config.password = 'password'
74
+ config.nick = 'My Nick'
75
+ config.room = '...@conf.hipchat.com'
76
+
77
+ # Example of the YamlStore which uses a yaml file for persistence
78
+ Robut::Storage::YamlStore.file = "~/.robut_store"
79
+ config.store = Robut::Storage::YamlStore
80
+
81
+ # Add a logger if you want to debug the connection
82
+ # config.logger = Logger.new(STDOUT)
83
+ end
84
+
85
+ This block usually goes at the end of the Chatfile.
86
+
87
+ == Built-in plugins
88
+
89
+ Robut includes a few plugins that we've found useful:
90
+
91
+ [Robut::Plugin::Calc] a simple calculator. <br /> <br /> Example: <tt>@robut calc 1 + 1 # => 2</tt>
92
+ [Robut::Plugin::Lunch] a random decider for lunch locations. <br /> <br /> Example: <tt>@robut lunch? # => "Banh Mi!"</tt>
93
+ [Robut::Plugin::Meme] an interface to memegenerator.net. <br /> <br /> Example: <tt>@robut meme Y_U_NO FIX THE BUILD?</tt>
94
+ [Robut::Plugin::Sayings] a simple regex listener and responder. <br /> <br /> Example: <tt>You're the worst robot ever, @robut. # => I know. </tt>
95
+ [Robut::Plugin::TWSS] an interface to the TWSS gem. Listens to anything said in the chat room and responds "That's what she said!" where appropriate. <br /> <br /> Example: <tt>well hurry up, you're not going fast enough # => "That's what she said!" </tt>
96
+ [Robut::Plugin::Echo] echo back whatever it gets. <br /> <br /> Example: <tt>@robut echo hello word</tt>
97
+ [Robut::Plugin::Say] invokes the "say" command (text-to-speech). <br /> <br /> Example: <tt>@robut say this rocks</tt>
98
+ [Robut::Plugin::Ping] responds with "pong". <br /> <br /> Example: <tt>@robut ping</tt>
99
+ [Robut::Plugin::Later] performs the given command after waiting an arbitrary amount of time. <br /> <br /> Example: <tt>@robut in 5 minutes echo @justin wake up!</tt>
100
+ [Robut::Plugin::Rdio] uses the Rdio[http://www.rdio.com] API and a simple web server to queue and play songs on Rdio. <br /> <br /> Example: <tt>@robut play ok computer</tt>
101
+
102
+ == Writing custom plugins
103
+
104
+ You can supply your own plugins to Robut. To create a plugin, subclass
105
+ Robut::Plugin::Base and implement the <tt>handle(time, sender_nick,
106
+ message)</tt> to perform any plugin-specific logic.
107
+
108
+ Robut::Plugin::Base provides a few helper methods that are documented
109
+ in its class definition.
110
+
111
+ == Contributing
112
+
113
+ Once you've made your great commits:
114
+
115
+ 1. Fork robut
116
+ 2. Create a topic branch - git checkout -b my_branch
117
+ 3. Push to your branch - git push origin my_branch
118
+ 4. Send me a pull request
119
+ 5. That's it!
120
+
121
+ == Todo
122
+
123
+ * Support connections to multiple rooms
124
+ * More plugins!
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ require 'rake/testtask'
2
+ require 'rake/rdoctask'
3
+ require 'bundler'
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ task :default => :test
7
+ task :build => :test
8
+
9
+ Rake::TestTask.new do |t|
10
+ t.libs << "test"
11
+ t.test_files = FileList['test/**/*_test.rb']
12
+ t.verbose = true
13
+ end
14
+
15
+ Rake::TestTask.new(:coverage) do |t|
16
+ t.libs << "test"
17
+ t.ruby_opts = ["-rsimplecov_helper"]
18
+ t.test_files = FileList['test/**/*_test.rb']
19
+ t.verbose = true
20
+ end
21
+
22
+ Rake::RDocTask.new do |rd|
23
+ rd.main = "README.rdoc"
24
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
25
+ rd.rdoc_dir = 'doc'
26
+ end
27
+
data/bin/robut ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'robut'
4
+ require 'ostruct'
5
+ require 'logger'
6
+
7
+ load ARGV[0] || './Chatfile'
8
+
9
+ Robut::Connection.new.connect
data/examples/Chatfile ADDED
@@ -0,0 +1,22 @@
1
+ # Require your plugins here
2
+ require 'robut/plugin/twss'
3
+ require 'robut/storage/yaml_store'
4
+
5
+ # Add the plugin classes to the Robut plugin list.
6
+ # Plugins are handled in the order that they appear in this array.
7
+ Robut::Plugin.plugins << Robut::Plugin::TWSS
8
+
9
+ # Configure the robut jabber connection and you're good to go!
10
+ Robut::Connection.configure do |config|
11
+ config.jid = '...@chat.hipchat.com/bot'
12
+ config.password = 'password'
13
+ config.nick = 'My Nick'
14
+ config.room = '...@conf.hipchat.com'
15
+
16
+ # Some plugins require storage
17
+ Robut::Storage::YamlStore.file = ".robut"
18
+ config.store = Robut::Storage::YamlStore
19
+
20
+ # Add a logger if you want to debug the connection
21
+ # config.logger = Logger.new(STDOUT)
22
+ end
data/lib/robut.rb ADDED
@@ -0,0 +1,5 @@
1
+ module Robut
2
+ autoload :Plugin, 'robut/plugin'
3
+ autoload :Connection, 'robut/connection'
4
+ autoload :Storage, 'robut/storage'
5
+ end
@@ -0,0 +1,167 @@
1
+ require 'xmpp4r'
2
+ require 'xmpp4r/muc/helper/simplemucclient'
3
+ require 'xmpp4r/roster/helper/roster'
4
+ require 'ostruct'
5
+
6
+ # Handles opening a connection to the HipChat server, and feeds all
7
+ # messages through our Robut::Plugin list.
8
+ class Robut::Connection
9
+
10
+ # The configuration used by the Robut connection.
11
+ #
12
+ # Parameters:
13
+ #
14
+ # [+jid+, +password+, +nick+] The HipChat credentials given on
15
+ # https://www.hipchat.com/account/xmpp
16
+ #
17
+ # [+room+] The chat room to join, in the format <tt>jabber_name</tt>@<tt>conference_server</tt>
18
+ #
19
+ # [+logger+] a logger instance to use for debug output.
20
+ attr_accessor :config
21
+
22
+ # The Jabber::Client that's connected to the HipChat server.
23
+ attr_accessor :client
24
+
25
+ # The MUC that wraps the Jabber Chat protocol.
26
+ attr_accessor :muc
27
+
28
+ # The storage instance that's available to plugins
29
+ attr_accessor :store
30
+
31
+ # The roster of currently available people
32
+ attr_accessor :roster
33
+
34
+ class << self
35
+ # Class-level config. This is set by the +configure+ class method,
36
+ # and is used if no configuration is passed to the +initialize+
37
+ # method.
38
+ attr_accessor :config
39
+ end
40
+
41
+ # Configures the connection at the class level. When the +robut+ bin
42
+ # file is loaded, it evals the file referenced by the first
43
+ # command-line parameter. This file can configure the connection
44
+ # instance later created by +robut+ by setting parameters in the
45
+ # Robut::Connection.configure block.
46
+ def self.configure
47
+ self.config = OpenStruct.new
48
+ yield config
49
+ end
50
+
51
+ # Sets the instance config to +config+, converting it into an
52
+ # OpenStruct if necessary.
53
+ def config=(config)
54
+ @config = config.kind_of?(Hash) ? OpenStruct.new(config) : config
55
+ end
56
+
57
+ # Initializes the connection. If no +config+ is passed, it defaults
58
+ # to the class_level +config+ instance variable.
59
+ def initialize(_config = nil)
60
+ self.config = _config || self.class.config
61
+
62
+ self.client = Jabber::Client.new(self.config.jid)
63
+ self.muc = Jabber::MUC::SimpleMUCClient.new(client)
64
+ self.store = self.config.store || Robut::Storage::HashStore # default to in-memory store only
65
+
66
+ if self.config.logger
67
+ Jabber.logger = self.config.logger
68
+ Jabber.debug = true
69
+ end
70
+ end
71
+
72
+ # Send +message+ to the room we're currently connected to, or
73
+ # directly to the person referenced by +to+. +to+ can be either a
74
+ # jid or the string name of the person.
75
+ def reply(message, to = nil)
76
+ if to
77
+ unless to.kind_of?(Jabber::JID)
78
+ to = find_jid_by_name(to)
79
+ end
80
+
81
+ msg = Jabber::Message.new(to || muc.room, message)
82
+ msg.type = :chat
83
+ client.send(msg)
84
+ else
85
+ muc.send(Jabber::Message.new(muc.room, message))
86
+ end
87
+ end
88
+
89
+ # Sends the chat message +message+ through +plugins+.
90
+ def handle_message(plugins, time, nick, message)
91
+ plugins.each do |plugin|
92
+ begin
93
+ rsp = plugin.handle(time, nick, message)
94
+ break if rsp == true
95
+ rescue => e
96
+ reply("I just pooped myself trying to run #{plugin.class.name}. AWK-WAAAARD!")
97
+ if config.logger
98
+ config.logger.error e
99
+ config.logger.error e.backtrace.join("\n")
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ # Connects to the specified room with the given credentials, and
106
+ # enters an infinite loop. Any messages sent to the room will pass
107
+ # through all the included plugins.
108
+ def connect
109
+ client.connect
110
+ client.auth(config.password)
111
+ client.send(Jabber::Presence.new.set_type(:available))
112
+
113
+ self.roster = Jabber::Roster::Helper.new(client)
114
+ roster.wait_for_roster
115
+
116
+ # Add the callback from messages that occur inside the room
117
+ muc.on_message do |time, nick, message|
118
+ plugins = Robut::Plugin.plugins.map { |p| p.new(self, nil) }
119
+ handle_message(plugins, time, nick, message)
120
+ end
121
+
122
+ # Add the callback from direct messages. Turns out the
123
+ # on_private_message callback doesn't do what it sounds like, so I
124
+ # have to go a little deeper into xmpp4r to get this working.
125
+ client.add_message_callback(200, self) { |message|
126
+ if !muc.from_room?(message.from) && message.type == :chat && message.body
127
+ time = Time.now # TODO: get real timestamp? Doesn't seem like
128
+ # jabber gives it to us
129
+ sender_jid = message.from
130
+ plugins = Robut::Plugin.plugins.map { |p| p.new(self, sender_jid) }
131
+ handle_message(plugins, time, self.roster[sender_jid].iname, message.body)
132
+ true
133
+ else
134
+ false
135
+ end
136
+ }
137
+
138
+ muc.join(config.room + '/' + config.nick)
139
+
140
+ trap_signals
141
+ loop { sleep 1 }
142
+ end
143
+
144
+ private
145
+
146
+ # Since we're entering an infinite loop, we have to trap TERM and
147
+ # INT. If something like the Rdio plugin has started a server that
148
+ # has already trapped those signals, we want to run those signal
149
+ # handlers first.
150
+ def trap_signals
151
+ old_signal_callbacks = {}
152
+ signal_callback = Proc.new do |signal|
153
+ old_signal_callbacks[signal].call if old_signal_callbacks[signal]
154
+ exit
155
+ end
156
+
157
+ [:INT, :TERM].each do |sig|
158
+ old_signal_callbacks[sig] = trap(sig) { signal_callback.call(sig) }
159
+ end
160
+ end
161
+
162
+ # Find a jid in the roster with the given name, case-insensitively
163
+ def find_jid_by_name(name)
164
+ name = name.downcase
165
+ roster.items.detect {|jid, item| item.iname.downcase == name}.first
166
+ end
167
+ end
@@ -0,0 +1,16 @@
1
+ # Robut plugins implement a simple interface to listen for messages
2
+ # and optionally respond to them. All plugins inherit from
3
+ # Robut::Plugin::Base.
4
+ module Robut::Plugin
5
+ autoload :Base, 'robut/plugin/base'
6
+
7
+ class << self
8
+ # A list of all available plugin classes. When you require a new
9
+ # plugin class, you should add it to this list if you want it to
10
+ # respond to messages.
11
+ attr_accessor :plugins
12
+ end
13
+
14
+ self.plugins = []
15
+
16
+ end
@@ -0,0 +1,70 @@
1
+ # All Robut plugins inherit from this base class. Plugins should
2
+ # implement the +handle+ method to implement their functionality.
3
+ class Robut::Plugin::Base
4
+
5
+ # A reference to the connection attached to this instance of the
6
+ # plugin. This is mostly used to communicate back to the server.
7
+ attr_accessor :connection
8
+
9
+ # If we are handling a private message, holds a reference to the
10
+ # sender of the message. +nil+ if the message was sent to the entire
11
+ # room.
12
+ attr_accessor :private_sender
13
+
14
+ # Creates a new instance of this plugin that references the
15
+ # specified connection.
16
+ def initialize(connection, private_sender = nil)
17
+ self.connection = connection
18
+ self.private_sender = private_sender
19
+ end
20
+
21
+ # Send +message+ back to the HipChat server. If +to+ == +:room+,
22
+ # replies to the room. If +to+ == nil, responds in the manner the
23
+ # original message was sent. Otherwise, PMs the message to +to+.
24
+ def reply(message, to = nil)
25
+ if to == :room
26
+ connection.reply(message, nil)
27
+ else
28
+ connection.reply(message, to || private_sender)
29
+ end
30
+ end
31
+
32
+ # An ordered list of all words in the message with any reference to
33
+ # the bot's nick stripped out. If +command+ is passed in, it is also
34
+ # stripped out. This is useful to separate the 'parameters' from the
35
+ # 'commands' in a message.
36
+ def words(message, command = nil)
37
+ reply = at_nick
38
+ command = command.downcase if command
39
+ message.split.reject {|word| word.downcase == reply || word.downcase == command }
40
+ end
41
+
42
+ # The bot's nickname, for @-replies.
43
+ def nick
44
+ connection.config.nick.split.first
45
+ end
46
+
47
+ # #nick with the @-symbol prepended
48
+ def at_nick
49
+ "@#{nick.downcase}"
50
+ end
51
+
52
+ # Was +message+ sent to Robut as an @reply?
53
+ def sent_to_me?(message)
54
+ message =~ /(^|\s)@#{nick}(\s|$)/i
55
+ end
56
+
57
+ # Do whatever you need to do to handle this message.
58
+ # If you want to stop the plugin execution chain, return +true+ from this
59
+ # method. Plugins are handled in the order that they appear in
60
+ # Robut::Plugin.plugins
61
+ def handle(time, sender_nick, message)
62
+ raise NotImplementedError, "Implement me in #{self.class.name}!"
63
+ end
64
+
65
+ # Accessor for the store instance
66
+ def store
67
+ connection.store
68
+ end
69
+
70
+ end