sclemmer-robut 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/.travis.yml +11 -0
  4. data/Gemfile +21 -0
  5. data/Gemfile.lock +65 -0
  6. data/README.rdoc +199 -0
  7. data/Rakefile +27 -0
  8. data/bin/robut +10 -0
  9. data/examples/Chatfile +31 -0
  10. data/examples/config.ru +13 -0
  11. data/lib/rexml_patches.rb +26 -0
  12. data/lib/robut/connection.rb +124 -0
  13. data/lib/robut/plugin/alias.rb +109 -0
  14. data/lib/robut/plugin/calc.rb +26 -0
  15. data/lib/robut/plugin/echo.rb +9 -0
  16. data/lib/robut/plugin/google_images.rb +17 -0
  17. data/lib/robut/plugin/help.rb +16 -0
  18. data/lib/robut/plugin/later.rb +81 -0
  19. data/lib/robut/plugin/lunch.rb +76 -0
  20. data/lib/robut/plugin/meme.rb +32 -0
  21. data/lib/robut/plugin/pick.rb +18 -0
  22. data/lib/robut/plugin/ping.rb +9 -0
  23. data/lib/robut/plugin/quips.rb +60 -0
  24. data/lib/robut/plugin/say.rb +23 -0
  25. data/lib/robut/plugin/sayings.rb +37 -0
  26. data/lib/robut/plugin/stock.rb +45 -0
  27. data/lib/robut/plugin/twss.rb +19 -0
  28. data/lib/robut/plugin/weather.rb +126 -0
  29. data/lib/robut/plugin.rb +201 -0
  30. data/lib/robut/pm.rb +40 -0
  31. data/lib/robut/presence.rb +39 -0
  32. data/lib/robut/room.rb +30 -0
  33. data/lib/robut/storage/base.rb +21 -0
  34. data/lib/robut/storage/hash_store.rb +26 -0
  35. data/lib/robut/storage/yaml_store.rb +57 -0
  36. data/lib/robut/storage.rb +3 -0
  37. data/lib/robut/version.rb +4 -0
  38. data/lib/robut/web.rb +26 -0
  39. data/lib/robut.rb +9 -0
  40. data/sclemmer-robut.gemspec +24 -0
  41. data/test/fixtures/bad_location.xml +1 -0
  42. data/test/fixtures/las_vegas.xml +1 -0
  43. data/test/fixtures/seattle.xml +1 -0
  44. data/test/fixtures/tacoma.xml +1 -0
  45. data/test/mocks/connection_mock.rb +22 -0
  46. data/test/mocks/presence_mock.rb +25 -0
  47. data/test/simplecov_helper.rb +2 -0
  48. data/test/test_helper.rb +9 -0
  49. data/test/unit/connection_test.rb +161 -0
  50. data/test/unit/plugin/alias_test.rb +76 -0
  51. data/test/unit/plugin/echo_test.rb +27 -0
  52. data/test/unit/plugin/help_test.rb +46 -0
  53. data/test/unit/plugin/later_test.rb +40 -0
  54. data/test/unit/plugin/lunch_test.rb +36 -0
  55. data/test/unit/plugin/pick_test.rb +35 -0
  56. data/test/unit/plugin/ping_test.rb +22 -0
  57. data/test/unit/plugin/quips_test.rb +58 -0
  58. data/test/unit/plugin/say_test.rb +34 -0
  59. data/test/unit/plugin/weather_test.rb +101 -0
  60. data/test/unit/plugin_test.rb +91 -0
  61. data/test/unit/room_test.rb +51 -0
  62. data/test/unit/storage/hash_store_test.rb +15 -0
  63. data/test/unit/storage/yaml_store_test.rb +47 -0
  64. data/test/unit/storage/yaml_test.yml +1 -0
  65. data/test/unit/web_test.rb +46 -0
  66. metadata +162 -0
@@ -0,0 +1,201 @@
1
+ # Robut plugins implement a simple interface to listen for messages
2
+ # and optionally respond to them. All plugins include the Robut::Plugin
3
+ # module.
4
+ module Robut::Plugin
5
+
6
+ # Contains methods that will be added directly to a class including
7
+ # Robut::Plugin.
8
+ module ClassMethods
9
+
10
+ # Sets up a 'matcher' that will try to match input being sent to
11
+ # this plugin with a regular expression. If the expression
12
+ # matches, +action+ will be performed. +action+ will be passed any
13
+ # captured groups in the regular expression as parameters. For
14
+ # example:
15
+ #
16
+ # match /^say hello to (\w+)/ do |name| ...
17
+ #
18
+ # The action is run in the context of an instance of a class that
19
+ # includes Robut::Plugin. Like +handle+, an action explicitly
20
+ # returning +true+ will stop the plugin chain from matching any
21
+ # further.
22
+ #
23
+ # Supported options:
24
+ # :sent_to_me - only try to match this regexp if it contains an @reply to robut.
25
+ # This will also strip the @reply from the message we're trying
26
+ # to match on, so ^ and $ will still do the right thing.
27
+ def match(regexp, options = {}, &action)
28
+ matchers << [regexp, options, action, @last_description]
29
+ @last_description = nil
30
+ end
31
+
32
+ # Provides a description for the next matcher
33
+ def desc(string)
34
+ @last_description = string
35
+ end
36
+
37
+ # A list of regular expressions to apply to input being sent to
38
+ # the plugin, along with blocks of actions to perform. Each
39
+ # element is a [regexp, options, action, description] array.
40
+ def matchers
41
+ @matchers ||= []
42
+ end
43
+
44
+ # Allows you to define http methods that this plugin should respond to.
45
+ # This is useful if you're want to accept generic post data for Robut to
46
+ # display in all rooms (e.g. Heroku deploy hooks, GitHub post receive
47
+ # hooks, etc).
48
+ #
49
+ # http do
50
+ # post '/heroku' do
51
+ # payload = Hashie::Mash.new(params)
52
+ # say "#{payload.user} deployed #{payload.head} to #{payload.app}", nil
53
+ # halt 200
54
+ # end
55
+ # end
56
+ #
57
+ # The action is run in the context of a Sinatra app (+Robut::Web+). So
58
+ # anything that Sinatra provides is available within the block.
59
+ def http(&block)
60
+ Robut::Web.class_eval &block
61
+ end
62
+ end
63
+
64
+ class << self
65
+ # A list of all available plugin classes. When you require a new
66
+ # plugin class, you should add it to this list if you want it to
67
+ # respond to messages.
68
+ attr_accessor :plugins
69
+ end
70
+
71
+ self.plugins = []
72
+
73
+ # A reference to the connection attached to this instance of the
74
+ # plugin. This is mostly used to communicate back to the server.
75
+ attr_accessor :connection
76
+
77
+ # If we are handling a private message, holds a reference to the
78
+ # sender of the message. +nil+ if the message was sent to the entire
79
+ # room.
80
+ attr_accessor :private_sender
81
+
82
+ attr_accessor :reply_to
83
+
84
+ # :nodoc:
85
+ def self.included(klass)
86
+ klass.send(:extend, ClassMethods)
87
+ end
88
+
89
+ # Creates a new instance of this plugin to reply to a particular
90
+ # object over that object's connection
91
+ def initialize(reply_to, private_sender = nil)
92
+ self.reply_to = reply_to
93
+ self.connection = reply_to.connection
94
+ self.private_sender = private_sender
95
+ end
96
+
97
+ # Send +message+ back to the HipChat server. If +to+ == +:room+,
98
+ # replies to the room. If +to+ == nil, responds in the manner the
99
+ # original message was sent. Otherwise, PMs the message to +to+.
100
+ def reply(message, to = nil)
101
+ if to == :room
102
+ reply_to.reply(message, nil)
103
+ else
104
+ reply_to.reply(message, to || private_sender)
105
+ end
106
+ end
107
+
108
+ # An ordered list of all words in the message with any reference to
109
+ # the bot's nick stripped out. If +command+ is passed in, it is also
110
+ # stripped out. This is useful to separate the 'parameters' from the
111
+ # 'commands' in a message.
112
+ def words(message, command = nil)
113
+ reply = at_nick.downcase
114
+ command = command.downcase if command
115
+ message.split.reject {|word| word.downcase == reply || word.downcase == command }
116
+ end
117
+
118
+ # Removes the first word in message if it is a reference to the bot's nick
119
+ # Given "@robut do this thing", Returns "do this thing"
120
+ def without_nick(message)
121
+ possible_nick, command = message.split(' ', 2)
122
+ if possible_nick.casecmp(at_nick) == 0
123
+ command
124
+ else
125
+ message
126
+ end
127
+ end
128
+
129
+ # The bot's nickname, for @-replies.
130
+ def nick
131
+ connection.config.mention_name || connection.config.nick.gsub(/[^0-9a-z]/i, '')
132
+ end
133
+
134
+ # #nick with the @-symbol prepended
135
+ def at_nick
136
+ "@#{nick}"
137
+ end
138
+
139
+ # Was +message+ sent to Robut as an @reply?
140
+ def sent_to_me?(message)
141
+ message =~ /(^|\s)@#{nick}(\s|$)/i
142
+ end
143
+
144
+ # Do whatever you need to do to handle this message.
145
+ # If you want to stop the plugin execution chain, return +true+ from this
146
+ # method. Plugins are handled in the order that they appear in
147
+ # Robut::Plugin.plugins
148
+ def handle(time, sender_nick, message)
149
+ if matchers.empty?
150
+ raise NotImplementedError, "Implement me in #{self.class.name}!"
151
+ else
152
+ find_match(message)
153
+ end
154
+ end
155
+
156
+ # Returns a list of messages describing the commands this plugin
157
+ # handles.
158
+ def usage
159
+ matchers.map do |regexp, options, action, description|
160
+ next unless description
161
+ if options[:sent_to_me]
162
+ at_nick + " " + description
163
+ else
164
+ description
165
+ end
166
+ end.compact
167
+ end
168
+
169
+ def fake_message(time, sender_nick, msg)
170
+ # TODO: ensure this connection is threadsafe
171
+ plugins = Robut::Plugin.plugins.map { |p| p.new(reply_to, private_sender) }
172
+ reply_to.handle_message(plugins, time, sender_nick, msg)
173
+ end
174
+
175
+ # Accessor for the store instance
176
+ def store
177
+ connection.store
178
+ end
179
+
180
+ private
181
+
182
+ # Find and run all the actions associated with matchers that match
183
+ # +message+.
184
+ def find_match(message)
185
+ matchers.each do |regexp, options, action, description|
186
+ if options[:sent_to_me] && !sent_to_me?(message)
187
+ next
188
+ end
189
+
190
+ if match_data = without_nick(message).match(regexp)
191
+ # Return true explicitly if this matcher explicitly returned true
192
+ break true if instance_exec(*match_data[1..-1], &action) == true
193
+ end
194
+ end
195
+ end
196
+
197
+ # The matchers defined by this plugin's class
198
+ def matchers
199
+ self.class.matchers
200
+ end
201
+ end
data/lib/robut/pm.rb ADDED
@@ -0,0 +1,40 @@
1
+ class Robut::PM < Robut::Presence
2
+
3
+ def initialize(connection, rooms)
4
+ # Add the callback from direct messages. Turns out the
5
+ # on_private_message callback doesn't do what it sounds like, so I
6
+ # have to go a little deeper into xmpp4r to get this working.
7
+ self.connection = connection
8
+ connection.client.add_message_callback(200, self) do |message|
9
+ from_room = rooms.any? {|room| room.muc.from_room?(message.from)}
10
+ if !from_room && message.type == :chat && message.body
11
+ time = Time.now # TODO: get real timestamp? Doesn't seem like
12
+ # jabber gives it to us
13
+ sender_jid = message.from
14
+ plugins = Robut::Plugin.plugins.map { |p| p.new(self, sender_jid) }
15
+ handle_message(plugins, time, connection.roster[sender_jid].iname, message.body)
16
+ true
17
+ else
18
+ false
19
+ end
20
+ end
21
+ end
22
+
23
+ def reply(message, to)
24
+ unless to.kind_of?(Jabber::JID)
25
+ to = find_jid_by_name(to)
26
+ end
27
+
28
+ msg = Jabber::Message.new(to, message)
29
+ msg.type = :chat
30
+ connection.client.send(msg)
31
+ end
32
+
33
+ private
34
+
35
+ # Find a jid in the roster with the given name, case-insensitively
36
+ def find_jid_by_name(name)
37
+ name = name.downcase
38
+ connection.roster.items.detect {|jid, item| item.iname.downcase == name}.first
39
+ end
40
+ end
@@ -0,0 +1,39 @@
1
+ STARTED = Time.now
2
+
3
+ class Robut::Presence
4
+
5
+ # The Robut::Connection that has all the connection info.
6
+ attr_accessor :connection
7
+
8
+ def initialize(connection)
9
+ self.connection = connection
10
+ end
11
+
12
+ # Sends the chat message +message+ through +plugins+.
13
+ def handle_message(plugins, time, nick, message)
14
+ # ignore all messages sent by robut. If you really want robut to
15
+ # reply to itself, you can use +fake_message+.
16
+ return if nick == connection.config.nick
17
+
18
+ time = Time.now
19
+ return if time < (STARTED + 10)
20
+ plugins.each do |plugin|
21
+ begin
22
+ rsp = plugin.handle(time, nick, message)
23
+ break if rsp == true
24
+ rescue => e
25
+ error = "UH OH! #{plugin.class.name} just crashed!"
26
+
27
+ if nick
28
+ reply(error, nick) # Connection#reply
29
+ else
30
+ reply(error) # Room#reply
31
+ end
32
+ if connection.config.logger
33
+ connection.config.logger.error e
34
+ connection.config.logger.error e.backtrace.join("\n")
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
data/lib/robut/room.rb ADDED
@@ -0,0 +1,30 @@
1
+ # Handles connections and responses to different rooms.
2
+ class Robut::Room < Robut::Presence
3
+
4
+ # The MUC that wraps the Jabber Chat protocol.
5
+ attr_accessor :muc
6
+
7
+ # The room jid
8
+ attr_accessor :name
9
+
10
+ def initialize(connection, room_name)
11
+ self.muc = Jabber::MUC::SimpleMUCClient.new(connection.client)
12
+ self.connection = connection
13
+ self.name = room_name
14
+ end
15
+
16
+ def join
17
+ # Add the callback from messages that occur inside the room
18
+ muc.on_message do |time, nick, message|
19
+ plugins = Robut::Plugin.plugins.map { |p| p.new(self) }
20
+ handle_message(plugins, time, nick, message)
21
+ end
22
+
23
+ muc.join(self.name + '/' + connection.config.nick)
24
+ end
25
+
26
+ # Send +message+ to the room we're currently connected to
27
+ def reply(message, to)
28
+ muc.send(Jabber::Message.new(muc.room, message))
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ # A Robut::Storage implementation is a simple key-value store
2
+ # accessible to all plugins. Plugins can access the global storage
3
+ # object with the method +store+. All storage implementations inherit
4
+ # from Robut::Storage::Base. All implementations must implement the class
5
+ # methods [] and []=.
6
+ class Robut::Storage::Base
7
+
8
+ class << self
9
+
10
+ # Sets the key +k+ to the value +v+ in the current storage system
11
+ def []=(k,v)
12
+ raise "Must be implemented"
13
+ end
14
+
15
+ # Returns the value at the key +k+.
16
+ def [](k)
17
+ raise "Must be implemented"
18
+ end
19
+ end
20
+
21
+ end
@@ -0,0 +1,26 @@
1
+ require 'yaml'
2
+
3
+ # A simple in-memory store backed by a Hash.
4
+ class Robut::Storage::HashStore < Robut::Storage::Base
5
+
6
+ class << self
7
+
8
+ # Stores +v+ in the hash.
9
+ def []=(k, v)
10
+ internal[k] = v
11
+ end
12
+
13
+ # Returns the value at key +k+.
14
+ def [](k)
15
+ internal[k]
16
+ end
17
+
18
+ private
19
+ # The hash the data is being stored in.
20
+ def internal
21
+ @internal ||= {}
22
+ end
23
+
24
+ end
25
+
26
+ end
@@ -0,0 +1,57 @@
1
+ require 'yaml'
2
+
3
+ # A store backed by a persistent on-disk yaml file.
4
+ class Robut::Storage::YamlStore < Robut::Storage::Base
5
+
6
+ class << self
7
+
8
+ # The path to the file this store will persist to.
9
+ attr_reader :file
10
+
11
+ # Sets the path to the file this store will persist to, and forces
12
+ # a reload of all of the data.
13
+ def file=(f)
14
+ @file = f
15
+ @internal = nil # force reload
16
+ @file
17
+ end
18
+
19
+ # Sets the key +k+ to the value +v+
20
+ def []=(k, v)
21
+ internal[k] = v
22
+ persist!
23
+ v
24
+ end
25
+
26
+ # Returns the value at the key +k+.
27
+ def [](k)
28
+ internal[k]
29
+ end
30
+
31
+ private
32
+
33
+ # The internal in-memory representation of the yaml file
34
+ def internal
35
+ @internal ||= load_from_file
36
+ end
37
+
38
+ # Persists the data in this store to disk. Throws an exception if
39
+ # we don't have a file set.
40
+ def persist!
41
+ raise "Robut::Storage::YamlStore.file must be set" unless file
42
+ File.open(file, "w") do |f|
43
+ f.puts internal.to_yaml
44
+ end
45
+ end
46
+
47
+ def load_from_file
48
+ begin
49
+ store = YAML.load_file(file)
50
+ rescue Errno::ENOENT
51
+ end
52
+
53
+ store || Hash.new
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ module Robut::Storage # :nodoc:
2
+ autoload :Base, 'robut/storage/base'
3
+ end
@@ -0,0 +1,4 @@
1
+ module Robut # :nodoc:
2
+ # Robut's version number.
3
+ VERSION = "0.5.2"
4
+ end
data/lib/robut/web.rb ADDED
@@ -0,0 +1,26 @@
1
+ require 'sinatra/base'
2
+
3
+ module Robut
4
+ class Web < Sinatra::Base
5
+ helpers do
6
+ # Say something to all connected rooms. Delegates to #reply
7
+ def say(*args)
8
+ reply(*args)
9
+ end
10
+
11
+ # Easy access to the current connection context.
12
+ def connection
13
+ settings.connection
14
+ end
15
+
16
+ # Delegates to Connection#reply
17
+ def reply(*args)
18
+ connection.reply(*args)
19
+ end
20
+ end
21
+
22
+ get '/' do
23
+ 'ok'
24
+ end
25
+ end
26
+ end
data/lib/robut.rb ADDED
@@ -0,0 +1,9 @@
1
+ module Robut
2
+ autoload :Plugin, 'robut/plugin'
3
+ autoload :Presence, 'robut/presence'
4
+ autoload :Room, 'robut/room'
5
+ autoload :PM, 'robut/pm'
6
+ autoload :Connection, 'robut/connection'
7
+ autoload :Storage, 'robut/storage'
8
+ autoload :Web, 'robut/web'
9
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "robut/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "sclemmer-robut"
7
+ s.version = Robut::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Justin Weiss"]
10
+ s.email = ["justin@uberweiss.org"]
11
+ s.homepage = "http://rdoc.info/github/justinweiss/robut/master/frames"
12
+ s.summary = %q{A simple plugin-enabled HipChat bot}
13
+ s.description = %q{A simple plugin-enabled HipChat bot}
14
+
15
+ s.rubyforge_project = "robut"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_dependency "xmpp4r", "~> 0.5.0"
23
+ s.add_dependency "sinatra", "~> 1.3"
24
+ end
@@ -0,0 +1 @@
1
+ <?xml version="1.0"?><xml_api_reply version="1"><weather module_id="0" tab_id="0" mobile_row="0" mobile_zipped="1" row="0" section="0" ><problem_cause data=""/></weather></xml_api_reply>
@@ -0,0 +1 @@
1
+ <?xml version="1.0"?><xml_api_reply version="1"><weather module_id="0" tab_id="0" mobile_row="0" mobile_zipped="1" row="0" section="0" ><forecast_information><city data="Las Vegas, NV"/><postal_code data="Las Vegas"/><latitude_e6 data=""/><longitude_e6 data=""/><forecast_date data="2011-05-31"/><current_date_time data="2011-05-31 19:50:12 +0000"/><unit_system data="US"/></forecast_information><current_conditions><condition data="Mostly Cloudy"/><temp_f data="83"/><temp_c data="28"/><humidity data="Humidity: 8%"/><icon data="http://g0.gstatic.com/images/icons/onebox/weather_mostlycloudy-40.gif"/><wind_condition data="Wind: S at 9 mph"/></current_conditions><forecast_conditions><day_of_week data="Tue"/><low data="66"/><high data="91"/><icon data="http://g0.gstatic.com/images/icons/onebox/weather_partlycloudy-40.gif"/><condition data="Partly Cloudy"/></forecast_conditions><forecast_conditions><day_of_week data="Wed"/><low data="60"/><high data="86"/><icon data="http://g0.gstatic.com/images/icons/onebox/weather_partlycloudy-40.gif"/><condition data="Partly Cloudy"/></forecast_conditions><forecast_conditions><day_of_week data="Thu"/><low data="63"/><high data="80"/><icon data="http://g0.gstatic.com/images/icons/onebox/weather_sunny-40.gif"/><condition data="Sunny"/></forecast_conditions><forecast_conditions><day_of_week data="Fri"/><low data="70"/><high data="84"/><icon data="http://g0.gstatic.com/images/icons/onebox/weather_partlycloudy-40.gif"/><condition data="Partly Cloudy"/></forecast_conditions></weather></xml_api_reply>
@@ -0,0 +1 @@
1
+ <?xml version="1.0"?><xml_api_reply version="1"><weather module_id="0" tab_id="0" mobile_row="0" mobile_zipped="1" row="0" section="0" ><forecast_information><city data="Seattle, WA"/><postal_code data="Seattle"/><latitude_e6 data=""/><longitude_e6 data=""/><forecast_date data="2011-05-23"/><current_date_time data="2011-05-23 22:52:57 +0000"/><unit_system data="US"/></forecast_information><current_conditions><condition data="Mostly Cloudy"/><temp_f data="58"/><temp_c data="14"/><humidity data="Humidity: 45%"/><icon data="/ig/images/weather/mostly_cloudy.gif"/><wind_condition data="Wind: E at 7 mph"/></current_conditions><forecast_conditions><day_of_week data="Mon"/><low data="48"/><high data="59"/><icon data="/ig/images/weather/partly_cloudy.gif"/><condition data="Partly Cloudy"/></forecast_conditions><forecast_conditions><day_of_week data="Tue"/><low data="51"/><high data="67"/><icon data="/ig/images/weather/partly_cloudy.gif"/><condition data="Partly Cloudy"/></forecast_conditions><forecast_conditions><day_of_week data="Wed"/><low data="49"/><high data="61"/><icon data="/ig/images/weather/rain.gif"/><condition data="Showers"/></forecast_conditions><forecast_conditions><day_of_week data="Thu"/><low data="49"/><high data="57"/><icon data="/ig/images/weather/rain.gif"/><condition data="Showers"/></forecast_conditions></weather></xml_api_reply>
@@ -0,0 +1 @@
1
+ <?xml version="1.0"?><xml_api_reply version="1"><weather module_id="0" tab_id="0" mobile_row="0" mobile_zipped="1" row="0" section="0" ><forecast_information><city data="Tacoma, WA"/><postal_code data="tacoma"/><latitude_e6 data=""/><longitude_e6 data=""/><forecast_date data="2011-05-23"/><current_date_time data="2011-05-23 22:46:28 +0000"/><unit_system data="US"/></forecast_information><current_conditions><condition data="Cloudy"/><temp_f data="60"/><temp_c data="16"/><humidity data="Humidity: 51%"/><icon data="/ig/images/weather/cloudy.gif"/><wind_condition data="Wind: N at 4 mph"/></current_conditions><forecast_conditions><day_of_week data="Mon"/><low data="45"/><high data="61"/><icon data="/ig/images/weather/rain.gif"/><condition data="Showers"/></forecast_conditions><forecast_conditions><day_of_week data="Tue"/><low data="50"/><high data="67"/><icon data="/ig/images/weather/partly_cloudy.gif"/><condition data="Partly Cloudy"/></forecast_conditions><forecast_conditions><day_of_week data="Wed"/><low data="48"/><high data="59"/><icon data="/ig/images/weather/rain.gif"/><condition data="Showers"/></forecast_conditions><forecast_conditions><day_of_week data="Thu"/><low data="48"/><high data="57"/><icon data="/ig/images/weather/rain.gif"/><condition data="Showers"/></forecast_conditions></weather></xml_api_reply>
@@ -0,0 +1,22 @@
1
+ require 'robut/storage/hash_store'
2
+
3
+ class Robut::ConnectionMock < Robut::Connection
4
+
5
+ attr_accessor :messages
6
+
7
+ def initialize(config = nil)
8
+ self.messages = []
9
+ self.config = config || self.class.config
10
+ self.store = Robut::Storage::HashStore
11
+ self.client = Jabber::Client.new ''
12
+ end
13
+
14
+ def connect
15
+ self.rooms = []
16
+ self
17
+ end
18
+
19
+ def reply(message, to)
20
+ self.messages << [message, to]
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ require 'robut/storage/hash_store'
2
+
3
+ class Robut::PresenceMock < Robut::Room
4
+
5
+ def initialize(connection)
6
+ self.connection = connection
7
+ end
8
+
9
+ def replies
10
+ @replies ||= []
11
+ end
12
+
13
+ def reply(msg, to = nil)
14
+ replies << msg
15
+ end
16
+
17
+ def handle_message(plugins, time, nick, message)
18
+ messages << [time, nick, message]
19
+ end
20
+
21
+ def messages
22
+ @messages ||= []
23
+ end
24
+
25
+ end
@@ -0,0 +1,2 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
@@ -0,0 +1,9 @@
1
+ require 'robut'
2
+ require 'test/unit'
3
+ require 'mocks/presence_mock'
4
+ require 'mocks/connection_mock'
5
+
6
+ Robut::ConnectionMock.configure do |config|
7
+ config.nick = "Robut t. Robot"
8
+ config.mention_name = "robut"
9
+ end