sclemmer-robut 0.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/.travis.yml +11 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +65 -0
- data/README.rdoc +199 -0
- data/Rakefile +27 -0
- data/bin/robut +10 -0
- data/examples/Chatfile +31 -0
- data/examples/config.ru +13 -0
- data/lib/rexml_patches.rb +26 -0
- data/lib/robut/connection.rb +124 -0
- data/lib/robut/plugin/alias.rb +109 -0
- data/lib/robut/plugin/calc.rb +26 -0
- data/lib/robut/plugin/echo.rb +9 -0
- data/lib/robut/plugin/google_images.rb +17 -0
- data/lib/robut/plugin/help.rb +16 -0
- data/lib/robut/plugin/later.rb +81 -0
- data/lib/robut/plugin/lunch.rb +76 -0
- data/lib/robut/plugin/meme.rb +32 -0
- data/lib/robut/plugin/pick.rb +18 -0
- data/lib/robut/plugin/ping.rb +9 -0
- data/lib/robut/plugin/quips.rb +60 -0
- data/lib/robut/plugin/say.rb +23 -0
- data/lib/robut/plugin/sayings.rb +37 -0
- data/lib/robut/plugin/stock.rb +45 -0
- data/lib/robut/plugin/twss.rb +19 -0
- data/lib/robut/plugin/weather.rb +126 -0
- data/lib/robut/plugin.rb +201 -0
- data/lib/robut/pm.rb +40 -0
- data/lib/robut/presence.rb +39 -0
- data/lib/robut/room.rb +30 -0
- data/lib/robut/storage/base.rb +21 -0
- data/lib/robut/storage/hash_store.rb +26 -0
- data/lib/robut/storage/yaml_store.rb +57 -0
- data/lib/robut/storage.rb +3 -0
- data/lib/robut/version.rb +4 -0
- data/lib/robut/web.rb +26 -0
- data/lib/robut.rb +9 -0
- data/sclemmer-robut.gemspec +24 -0
- data/test/fixtures/bad_location.xml +1 -0
- data/test/fixtures/las_vegas.xml +1 -0
- data/test/fixtures/seattle.xml +1 -0
- data/test/fixtures/tacoma.xml +1 -0
- data/test/mocks/connection_mock.rb +22 -0
- data/test/mocks/presence_mock.rb +25 -0
- data/test/simplecov_helper.rb +2 -0
- data/test/test_helper.rb +9 -0
- data/test/unit/connection_test.rb +161 -0
- data/test/unit/plugin/alias_test.rb +76 -0
- data/test/unit/plugin/echo_test.rb +27 -0
- data/test/unit/plugin/help_test.rb +46 -0
- data/test/unit/plugin/later_test.rb +40 -0
- data/test/unit/plugin/lunch_test.rb +36 -0
- data/test/unit/plugin/pick_test.rb +35 -0
- data/test/unit/plugin/ping_test.rb +22 -0
- data/test/unit/plugin/quips_test.rb +58 -0
- data/test/unit/plugin/say_test.rb +34 -0
- data/test/unit/plugin/weather_test.rb +101 -0
- data/test/unit/plugin_test.rb +91 -0
- data/test/unit/room_test.rb +51 -0
- data/test/unit/storage/hash_store_test.rb +15 -0
- data/test/unit/storage/yaml_store_test.rb +47 -0
- data/test/unit/storage/yaml_test.yml +1 -0
- data/test/unit/web_test.rb +46 -0
- metadata +162 -0
data/lib/robut/plugin.rb
ADDED
@@ -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
|
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,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
|