robut 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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