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,17 @@
1
+ require 'google-search'
2
+
3
+ # Responds with the first google image search result matching a query.
4
+ class Robut::Plugin::GoogleImages
5
+ include Robut::Plugin
6
+
7
+ desc "image <query> - responds with the first image from a Google Images search for <query>"
8
+ match /^image (.*)/, :sent_to_me => true do |query|
9
+ image = Google::Search::Image.new(:query => query).first
10
+
11
+ if image
12
+ reply image.uri
13
+ else
14
+ reply "Couldn't find an image"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ # When asked for help, responds with a list of commands supported by
2
+ # all loaded plugins
3
+ class Robut::Plugin::Help
4
+ include Robut::Plugin
5
+
6
+ desc "help - displays this message"
7
+ match /^help$/, :sent_to_me => true do
8
+ reply("Supported commands:")
9
+ Robut::Plugin.plugins.each do |plugin|
10
+ plugin_instance = plugin.new(reply_to, private_sender)
11
+ Array(plugin_instance.usage).each do |command_usage|
12
+ reply(command_usage)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,81 @@
1
+ # The Later plugin allows you to send messages/commands to robut based on
2
+ # a time delay. Like so:
3
+ #
4
+ # @robut in 5 minutes lunch?
5
+ # @robut in 1 hr echo @justin wake up!
6
+ #
7
+ # @robut will respond effectively the same as he would if someone had told
8
+ # him "lunch?" 5 minutes from now. In the case of the Lunch plugin, he
9
+ # would repond with a lunch suggestion. The Later plugin works with all other
10
+ # plugins as you follow this syntax:
11
+ #
12
+ # @robut in <number> <mins|hrs|secs> [command]
13
+ #
14
+ # Where command is the message you want to send to @robut in the future. For
15
+ # the time denominations it also recognizes minute, minutes, hour, hours,
16
+ # second, seconds.
17
+ #
18
+ class Robut::Plugin::Later
19
+ include Robut::Plugin
20
+
21
+ # Returns a description of how to use this plugin
22
+ def usage
23
+ "#{at_nick} in <number> <mins|hrs|secs> <command> - sends <command> to #{nick} after the specified interval"
24
+ end
25
+
26
+ # Passes +message+ back through the plugin chain if we've been given
27
+ # a time to execute it later.
28
+ def handle(time, sender_nick, message)
29
+ if sent_to_me?(message)
30
+ phrase = words(message).join(' ')
31
+ if phrase =~ timer_regex
32
+ count = $1.to_i
33
+ scale = $2
34
+ future_message = at_nick + ' ' + $3
35
+
36
+ sleep_time = count * scale_multiplier(scale)
37
+
38
+ if sleep_time >= 3600
39
+ reply "Too far into the future--ain't no body got (timeforthat)"
40
+ return
41
+ end
42
+
43
+ reply("Ok, see you in #{count} #{scale}")
44
+ connection = self.connection
45
+ threader do
46
+ sleep sleep_time
47
+ fake_message(Time.now, sender_nick, future_message)
48
+ end
49
+ return true
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ # A regex that detects a time.
57
+ def timer_regex
58
+ /in (.*) (sec|secs|second|seconds|min|mins|minute|minutes|hr|hrs|hour|hours) (.*)$/
59
+ end
60
+
61
+ # Takes a time scale (secs, mins, hours, etc.) and returns the
62
+ # factor to convert it into seconds.
63
+ def scale_multiplier(time_scale)
64
+ case time_scale
65
+ when /sec(s|ond|onds)?/
66
+ 1
67
+ when /min(s|ute|utes)?/
68
+ 60
69
+ when /(hr|hrs|hour|hours)/
70
+ 60 * 60
71
+ end
72
+ end
73
+
74
+ # Asynchronously runs the given block.
75
+ def threader
76
+ Thread.new do
77
+ yield
78
+ end
79
+ end
80
+
81
+ end
@@ -0,0 +1,76 @@
1
+ # Where should we go to lunch today?
2
+ class Robut::Plugin::Lunch
3
+ include Robut::Plugin
4
+
5
+ # Returns a description of how to use this plugin
6
+ def usage
7
+ [
8
+ "lunch? / food? - #{nick} will suggest a place to go eat",
9
+ "#{at_nick} lunch places - lists all the lunch places #{nick} knows about",
10
+ "#{at_nick} new lunch place <place> - tells #{nick} about a new place to eat",
11
+ "#{at_nick} remove lunch place <place> - tells #{nick} not to suggest <place> anymore"
12
+ ]
13
+ end
14
+
15
+ # Replies with a random string selected from +places+.
16
+ def handle(time, sender_nick, message)
17
+ words = words(message)
18
+ phrase = words.join(' ')
19
+ # lunch?
20
+ if phrase =~ /^(lunch|food)\?$/i
21
+ if places.empty?
22
+ reply "I don't know about any lunch places"
23
+ else
24
+ $stderr.puts "*************************** -> #{sender_nick}"
25
+ reply choose_place(sender_nick) + "!"
26
+ end
27
+ # @robut lunch places
28
+ elsif phrase == "lunch places" && sent_to_me?(message)
29
+ if places.empty?
30
+ reply "I don't know about any lunch places"
31
+ else
32
+ reply places.join(', ')
33
+ end
34
+ # @robut new lunch place Green Leaf
35
+ elsif phrase =~ /new lunch place (.*)/i && sent_to_me?(message)
36
+ place = $1
37
+ new_place(place)
38
+ reply "Ok, I'll add \"#{place}\" to the the list of lunch places"
39
+ # @robut remove luynch place Green Leaf
40
+ elsif phrase =~ /remove lunch place (.*)/i && sent_to_me?(message)
41
+ place = $1
42
+ remove_place(place)
43
+ reply "I removed \"#{place}\" from the list of lunch places"
44
+ end
45
+ end
46
+
47
+ def choose_place(sender_nick)
48
+ excluded = sender_nick =~ /\bchu\b/i ? /\bchu\b/i : nil
49
+ # excluded = sender_nick =~ /\bclemmer\b/i ? /^[^g]/i : nil
50
+ valid_places = places
51
+ valid_places = places.reject { |p| p =~ excluded } unless excluded.nil?
52
+ return valid_places[rand(valid_places.length)]
53
+ end
54
+
55
+ # Stores +place+ as a new lunch place.
56
+ def new_place(place)
57
+ store["lunch_places"] ||= []
58
+ store["lunch_places"] = (store["lunch_places"] + Array(place)).uniq
59
+ end
60
+
61
+ # Removes +place+ from the list of lunch places.
62
+ def remove_place(place)
63
+ store["lunch_places"] ||= []
64
+ store["lunch_places"] = store["lunch_places"] - Array(place)
65
+ end
66
+
67
+ # Returns the list of lunch places we know about.
68
+ def places
69
+ store["lunch_places"] ||= []
70
+ end
71
+
72
+ # Sets the list of lunch places to +v+
73
+ def places=(v)
74
+ store["lunch_places"] = v
75
+ end
76
+ end
@@ -0,0 +1,32 @@
1
+ require 'cgi'
2
+
3
+ # A simple plugin that wraps memecaptain.
4
+ # This plugin is activated when robut is sent a message starting
5
+ # with the name of a meme. The list of generators can be discovered
6
+ # by running
7
+ #
8
+ # @robut meme list
9
+ #
10
+ # from the command line. Example:
11
+ #
12
+ # @robut meme all_the_things write; all the plugins
13
+ #
14
+ # Send message to the specified meme generator. If the meme requires
15
+ # more than one line of text, lines should be separated with a semicolon.
16
+ class Robut::Plugin::Meme
17
+ include Robut::Plugin
18
+
19
+ desc "meme <meme> <line1>;<line2> - responds with a link to a generated <meme> image using <line1> and <line2>. " +
20
+ "See http://memecaptain.com/ for a list of memes. You can also pass a link to your own image as the meme."
21
+ match /^meme (\S+) (.*)$/, :sent_to_me => true do |meme, text|
22
+ if meme.include?("://")
23
+ url = meme
24
+ else
25
+ url = "http://memecaptain.com/#{meme}.jpg"
26
+ end
27
+ line1, line2 = text.split(';').map { |line| CGI.escape(line.strip)}
28
+ meme_url = "http://memecaptain.com/i?u=#{url}&tt=#{line1}"
29
+ meme_url += "&tb=#{line2}" if line2
30
+ reply(meme_url)
31
+ end
32
+ end
@@ -0,0 +1,18 @@
1
+ require 'calc'
2
+
3
+ # Let fate decide!
4
+ class Robut::Plugin::Pick
5
+ include Robut::Plugin
6
+
7
+ desc "pick <a>, <b>, <c>, ... - randomly selects one of the options"
8
+ match /^pick (.*)/, :sent_to_me => true do |message|
9
+ options = message.split(',').map { |s| s.strip }
10
+ rsp = options[random(options.length)]
11
+ reply("And the winner is... #{rsp}") if rsp
12
+ end
13
+
14
+ def random(c)
15
+ rand(c)
16
+ end
17
+
18
+ end
@@ -0,0 +1,9 @@
1
+ # A simple plugin that replies with "pong" when messaged with ping
2
+ class Robut::Plugin::Ping
3
+ include Robut::Plugin
4
+
5
+ desc "ping - responds 'pong'"
6
+ match /^ping$/, :sent_to_me => true do
7
+ reply("pong")
8
+ end
9
+ end
@@ -0,0 +1,60 @@
1
+ # Stores quotes and replies with a random stored quote.
2
+ class Robut::Plugin::Quips
3
+ include Robut::Plugin
4
+
5
+ desc "add quip <text> - adds a quip to the quip database"
6
+ match /^add quip (.+)/, :sent_to_me => true do |new_quip|
7
+ if add_quip(new_quip)
8
+ reply "I added the quip to the quip database"
9
+ else
10
+ reply "I didn't add the quip, since it was already added"
11
+ end
12
+ end
13
+
14
+ desc "remove quip <text> - removes a quip from the quip database"
15
+ match /^remove quip (.+)/, :sent_to_me => true do |quip|
16
+ if remove_quip(quip)
17
+ reply "I removed the quip from the quip database"
18
+ else
19
+ reply "I couldn't remove the quip, since it wasn't in the quip database"
20
+ end
21
+ end
22
+
23
+ desc "quip - returns a random quip"
24
+ match /^quip$/, :sent_to_me => true do
25
+ quip = random_quip
26
+ if quip
27
+ reply quip
28
+ else
29
+ reply "I don't know any quips"
30
+ end
31
+ end
32
+
33
+ # The list of quips stored in the quip database
34
+ def quips
35
+ # I'd love to use a set here, but it doesn't serialize right to yaml
36
+ store["quips"] ||= []
37
+ end
38
+
39
+ # Update the list of quips stored in the quip database
40
+ def quips=(new_quips)
41
+ # I'd love to use a set here, but it doesn't serialize right to yaml
42
+ store["quips"] = new_quips
43
+ end
44
+
45
+ # Adds +quip+ to the quip database
46
+ def add_quip(quip)
47
+ self.quips = (quips + Array(quip)) unless quips.include?(quip)
48
+ end
49
+
50
+ # Removes +quip+ from the quip database
51
+ def remove_quip(quip)
52
+ self.quips = (quips - Array(quip)) if quips.include?(quip)
53
+ end
54
+
55
+ # Returns a random quip
56
+ def random_quip
57
+ quips[rand(quips.length)] unless quips.empty?
58
+ end
59
+
60
+ end
@@ -0,0 +1,23 @@
1
+ # This is a simple plugin the envokes the "say" command on whatever is passed
2
+ # Example:
3
+ #
4
+ # @robut say that was awesome!
5
+ #
6
+ # *Requires that the "say" command is installed and in the path
7
+ #
8
+ class Robut::Plugin::Say
9
+ include Robut::Plugin
10
+
11
+ desc "say <words> - uses Mac OS X's 'say' command to speak <words>"
12
+ match "^say (.*)$", :sent_to_me => true do |phrase|
13
+ phrase = clean(phrase)
14
+ system("say #{phrase}")
15
+ end
16
+
17
+ private
18
+
19
+ def clean(str)
20
+ str.gsub("'", "").gsub(/[^A-Za-z0-9\s]+/, " ").gsub(/\s+/, ' ').strip
21
+ end
22
+
23
+ end
@@ -0,0 +1,37 @@
1
+ # A simple regex => response plugin.
2
+ class Robut::Plugin::Sayings
3
+ include Robut::Plugin
4
+
5
+ class << self
6
+ # A list of arrays. The first element is a regex, the second is
7
+ # the reply sent if the regex matches. After the first match, we
8
+ # don't try to match any other sayings. Configuration looks like the following:
9
+ #
10
+ # [["you're the worst", "I know."], ["sucks", "You know something, you suck!" ]]
11
+ #
12
+ # All regex matches are case-insensitive.
13
+ attr_accessor :sayings
14
+ end
15
+ self.sayings = []
16
+
17
+ # Returns a description of how to use this plugin
18
+ def usage
19
+ "#{at_nick} <words> - if <words> matches a regular expression defined in the Chatfile, responds with the specified response"
20
+ end
21
+
22
+ # For each element in sayings, creates a regex out of the first
23
+ # element, tries to match +message+ to it, and replies with the
24
+ # second element if it found a match. Robut::Plugin::Sayings will
25
+ # only respond once to each message, with the first match.
26
+ def handle(time, sender_nick, message)
27
+ # Tries to respond to any message sent to robut.
28
+ if sent_to_me?(message)
29
+ self.class.sayings.each do |saying|
30
+ if words(message).join(' ').match(/#{saying.first}/i)
31
+ reply(saying.last)
32
+ return
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,45 @@
1
+ begin
2
+ require 'yahoofinance' # require yahoofinance gem
3
+ rescue LoadError
4
+ puts "You must install the yahoofinance gem in order to use the Stock plugin"
5
+ raise $!
6
+ end
7
+
8
+ class Robut::Plugin::Stock
9
+ include Robut::Plugin
10
+
11
+ desc "stock <symbol> - Returns a stock data from Yahoo Finance"
12
+ match /^stock (.*)/, :sent_to_me => true do |phrase|
13
+ stock_data = get_stock_data(format_stock_symbols(phrase))
14
+ reply format_reply(stock_data)
15
+ end
16
+
17
+ private
18
+
19
+ def format_reply(stock_data)
20
+ r = []
21
+ stock_data.keys.sort.each do |sym|
22
+ sd = stock_data[sym]
23
+ r << "#{sym}: #{format_number(sd.changePoints)} / #{format_number(sd.changePercent)}%,\tbid: #{pad_number(sd.bid)},\task: #{pad_number(sd.ask)},\tprevious close: #{pad_number(sd.previousClose)}"
24
+ end
25
+ r.join("\n")
26
+ end
27
+
28
+ def format_number(n)
29
+ n > 0 ? "+" + pad_number(n) : pad_number(n)
30
+ end
31
+
32
+ def pad_number(n)
33
+ sprintf("%.2f", n)
34
+ end
35
+
36
+ def format_stock_symbols(phrase)
37
+ phrase.downcase.split(/[\s,;]+/).join(',')
38
+ end
39
+
40
+ def get_stock_data(symbols)
41
+ YahooFinance::get_quotes(YahooFinance::StandardQuote, symbols)
42
+ end
43
+
44
+
45
+ end
@@ -0,0 +1,19 @@
1
+ require 'twss-classifier'
2
+
3
+ # A simple plugin that feeds everything said in the room through the
4
+ # twss gem. Requires the 'twss' gem, obviously.
5
+ class Robut::Plugin::TWSS
6
+ include Robut::Plugin
7
+
8
+ # Returns a description of how to use this plugin
9
+ def usage
10
+ "<words> - responds with \"That's what she said!\" if #{nick} thinks <words> is a valid TWSS"
11
+ end
12
+
13
+ # Responds "That's what she said!" if the TWSS gem returns true for
14
+ # +message+. Strips out any reference to our nick in +message+
15
+ # before it stuffs +message+ into the gem.
16
+ def handle(time, sender_nick, message)
17
+ reply("That's what she said!") if TWSSClassifier.is_twss?(words(message).join(" "))
18
+ end
19
+ end
@@ -0,0 +1,126 @@
1
+ require 'open-uri'
2
+ require 'nokogiri'
3
+
4
+ # What's the current weather forecast?
5
+ class Robut::Plugin::Weather
6
+ include Robut::Plugin
7
+
8
+ class << self
9
+ attr_accessor :default_location
10
+ end
11
+
12
+ # Returns a description of how to use this plugin
13
+ def usage
14
+ [
15
+ "#{at_nick} weather - returns the weather in the default location for today",
16
+ "#{at_nick} weather tomorrow - returns the weather in the default location for tomorrow",
17
+ "#{at_nick} weather <location> - returns the weather for <location> today",
18
+ "#{at_nick} weather <location> Tuesday - returns the weather for <location> Tuesday"
19
+ ]
20
+ end
21
+
22
+ def handle(time, sender_nick, message)
23
+ # ignore messages that don't end in ?
24
+ return unless message[message.length - 1, 1] == "?"
25
+ message = message[0..message.length - 2]
26
+
27
+ words = words(message)
28
+ i = words.index("weather")
29
+
30
+ # ignore messages that don't have "weather" in them
31
+ return if i.nil?
32
+
33
+ location = i.zero? ? self.class.default_location : words[0..i-1].join(" ")
34
+ if location.nil?
35
+ reply "I don't have a default location!"
36
+ return
37
+ end
38
+
39
+ day_of_week = nil
40
+ day_string = words[i+1..-1].join(" ").downcase
41
+ if day_string != ""
42
+ day_of_week = parse_day(day_string)
43
+ if day_of_week.nil?
44
+ reply "I don't recognize the date: \"#{day_string}\""
45
+ return
46
+ end
47
+ end
48
+
49
+ if bad_location?(location)
50
+ reply "I don't recognize the location: \"#{location}\""
51
+ return
52
+ end
53
+
54
+ if day_of_week
55
+ reply forecast(location, day_of_week)
56
+ else
57
+ reply current_conditions(location)
58
+ end
59
+ end
60
+
61
+ def parse_day(day)
62
+ day_map = {
63
+ "monday" => "Mon",
64
+ "mon" => "Mon",
65
+ "tuesday" => "Tue",
66
+ "tue" => "Tue",
67
+ "tues" => "Tue",
68
+ "wed" => "Wed",
69
+ "wednesday" => "Wed",
70
+ "thurs" => "Thu",
71
+ "thu" => "Thu",
72
+ "thursday" => "Thu",
73
+ "friday" => "Fri",
74
+ "fri" => "Fri",
75
+ "saturday" => "Sat",
76
+ "sat" => "Sat",
77
+ "sunday" => "Sun",
78
+ "sun" => "Sun",
79
+ }
80
+ if day_map.has_key?(day)
81
+ return day_map[day]
82
+ end
83
+
84
+ if day == "tomorrow"
85
+ return (Time.now + 60*60*24).strftime("%a")
86
+ end
87
+
88
+ if day == "today"
89
+ return Time.now.strftime("%a")
90
+ end
91
+ end
92
+
93
+ def current_conditions(location)
94
+ doc = weather_data(location)
95
+ condition = doc.search("current_conditions condition").first["data"]
96
+ temperature = doc.search("current_conditions temp_f").first["data"]
97
+ normalized_location = doc.search("forecast_information city").first["data"]
98
+ "Weather for #{normalized_location}: #{condition}, #{temperature}F"
99
+ end
100
+
101
+ def forecast(location, day_of_week)
102
+ doc = weather_data(location)
103
+ forecast = doc.search("forecast_conditions").detect{|el| c = el.children.detect{|c| c.name == "day_of_week"}; c && c["data"] == day_of_week}
104
+ return "Can't find a forecast for #{day_of_week}" if forecast.nil?
105
+
106
+ condition = forecast.children.detect{|c| c.name == "condition"}["data"]
107
+ high = forecast.children.detect{|c| c.name == "high"}["data"]
108
+ low = forecast.children.detect{|c| c.name == "low"}["data"]
109
+ normalized_location = doc.search("forecast_information city").first["data"]
110
+ "Forecast for #{normalized_location} on #{day_of_week}: #{condition}, High: #{high}F, Low: #{low}F"
111
+ end
112
+
113
+ def weather_data(location = "")
114
+ @weather_data ||= {}
115
+ @weather_data[location] ||= begin
116
+ url = "http://www.google.com/ig/api?weather=#{URI.escape(location)}"
117
+ Nokogiri::XML(open(url))
118
+ end
119
+ @weather_data[location]
120
+ end
121
+
122
+ def bad_location?(location = "")
123
+ weather_data(location).search("forecast_information").empty?
124
+ end
125
+
126
+ end