robut 0.2.1 → 0.3.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.
@@ -2,15 +2,21 @@ require 'calc'
2
2
 
3
3
  # A simple calculator. This delegates all calculations to the 'calc'
4
4
  # gem.
5
- class Robut::Plugin::Calc < Robut::Plugin::Base
5
+ class Robut::Plugin::Calc
6
+ include Robut::Plugin
6
7
 
8
+ # Returns a description of how to use this plugin
9
+ def usage
10
+ "#{at_nick} calc <calculation> - replies with the result of <calculation>"
11
+ end
12
+
7
13
  # Perform the calculation specified in +message+, and send the
8
14
  # result back.
9
15
  def handle(time, sender_nick, message)
10
16
  if sent_to_me?(message) && words(message).first == 'calc'
11
17
  calculation = words(message, 'calc').join(' ')
12
18
  begin
13
- reply("#{calculation} = #{Calc.evaluate(calculation)}")
19
+ reply("#{calculation} = #{::Calc.evaluate(calculation)}")
14
20
  rescue
15
21
  reply("Can't calculate that.")
16
22
  end
@@ -1,6 +1,6 @@
1
-
2
1
  # A plugin that tells robut to repeat whatever he's told.
3
- class Robut::Plugin::Echo < Robut::Plugin::Base
2
+ class Robut::Plugin::Echo
3
+ include Robut::Plugin
4
4
 
5
5
  # Responds with +message+ if the command sent to robut is 'echo'.
6
6
  def handle(time, sender_nick, message)
@@ -10,5 +10,10 @@ class Robut::Plugin::Echo < Robut::Plugin::Base
10
10
  reply(phrase) unless phrase.empty?
11
11
  end
12
12
  end
13
-
13
+
14
+ # Returns a description of how to use this plugin
15
+ def usage
16
+ "#{at_nick} echo <message> - replies to the channel with <message>"
17
+ end
18
+
14
19
  end
@@ -0,0 +1,24 @@
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
+ # Responds with a list of commands supported by all loaded plugins.
7
+ def handle(time, sender_nick, message)
8
+ words = words(message)
9
+ if sent_to_me?(message) && words.first == 'help'
10
+ reply("Supported commands:")
11
+ Robut::Plugin.plugins.each do |plugin|
12
+ plugin_instance = plugin.new(connection, private_sender)
13
+ Array(plugin_instance.usage).each do |command_usage|
14
+ reply(command_usage)
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ # Returns a description of how to use this plugin
21
+ def usage
22
+ "#{at_nick} help - displays this message"
23
+ end
24
+ end
@@ -12,11 +12,17 @@
12
12
  # @robut in <number> <mins|hrs|secs> [command]
13
13
  #
14
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,
15
+ # the time denominations it also recognizes minute, minutes, hour, hours,
16
16
  # second, seconds.
17
17
  #
18
- class Robut::Plugin::Later < Robut::Plugin::Base
18
+ class Robut::Plugin::Later
19
+ include Robut::Plugin
19
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
+
20
26
  # Passes +message+ back through the plugin chain if we've been given
21
27
  # a time to execute it later.
22
28
  def handle(time, sender_nick, message)
@@ -26,17 +32,15 @@ class Robut::Plugin::Later < Robut::Plugin::Base
26
32
  count = $1.to_i
27
33
  scale = $2
28
34
  future_message = at_nick + ' ' + $3
29
-
35
+
30
36
  sleep_time = count * scale_multiplier(scale)
31
-
37
+
32
38
  reply("Ok, see you in #{count} #{scale}")
33
-
39
+
34
40
  connection = self.connection
35
41
  threader do
36
42
  sleep sleep_time
37
- # TODO: ensure this connection is threadsafe
38
- plugins = Robut::Plugin.plugins.map { |p| p.new(connection, private_sender) }
39
- connection.handle_message(plugins, Time.now, sender_nick, future_message)
43
+ fake_message(Time.now, sender_nick, future_message)
40
44
  end
41
45
  return true
42
46
  end
@@ -69,5 +73,5 @@ class Robut::Plugin::Later < Robut::Plugin::Base
69
73
  yield
70
74
  end
71
75
  end
72
-
76
+
73
77
  end
@@ -1,9 +1,20 @@
1
1
  # Where should we go to lunch today?
2
- class Robut::Plugin::Lunch < Robut::Plugin::Base
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
3
14
 
4
15
  # Replies with a random string selected from +places+.
5
16
  def handle(time, sender_nick, message)
6
- words = words(message)
17
+ words = words(message)
7
18
  phrase = words.join(' ')
8
19
  # lunch?
9
20
  if phrase =~ /(lunch|food)\?/i
@@ -53,5 +64,5 @@ class Robut::Plugin::Lunch < Robut::Plugin::Base
53
64
  def places=(v)
54
65
  store["lunch_places"] = v
55
66
  end
56
-
67
+
57
68
  end
@@ -1,30 +1,63 @@
1
- require 'meme'
1
+ require 'cgi'
2
2
 
3
- # A simple plugin that wraps meme_generator. Requires the
4
- # 'meme_generator' gem.
5
- class Robut::Plugin::Meme < Robut::Plugin::Base
3
+ # A simple plugin that wraps memecaptain.
4
+ class Robut::Plugin::Meme
5
+ include Robut::Plugin
6
+
7
+ MEMES = {
8
+ 'bear_grylls' => 'http://memecaptain.com/bear_grylls.jpg',
9
+ 'insanity_wolf' => 'http://memecaptain.com/insanity_wolf.jpg',
10
+ 'most_interesting' => 'http://memecaptain.com/most_interesting.jpg',
11
+ 'philosoraptor' => 'http://memecaptain.com/philosoraptor.jpg',
12
+ 'scumbag_steve' => 'http://memecaptain.com/scumbag_steve.jpg',
13
+ 'town_crier' => 'http://memecaptain.com/town_crier.jpg',
14
+ 'troll_face' => 'http://memecaptain.com/troll_face.jpg',
15
+ 'y_u_no' => 'http://memecaptain.com/y_u_no.jpg',
16
+ 'yao_ming' => 'http://memecaptain.com/yao_ming.jpg',
17
+ 'business_cat' => 'http://memecaptain.com/business_cat.jpg',
18
+ 'all_the_things' => 'http://memecaptain.com/all_the_things.jpg',
19
+ 'fry' => 'http://memecaptain.com/fry.png',
20
+ 'sap' => 'http://memecaptain.com/sap.jpg'
21
+ }
22
+
23
+ # Returns a description of how to use this plugin
24
+ def usage
25
+ [
26
+ "#{at_nick} meme list - lists all the memes that #{nick} knows about",
27
+ "#{at_nick} meme <meme> <line1>;<line2> - responds with a link to a generated <meme> image using <line1> and <line2>"
28
+ ]
29
+ end
6
30
 
7
31
  # This plugin is activated when robut is sent a message starting
8
32
  # with the name of a meme. The list of generators can be discovered
9
33
  # by running
10
- #
11
- # meme list
34
+ #
35
+ # @robut meme list
12
36
  #
13
37
  # from the command line. Example:
14
38
  #
15
- # @robut h_mermaid look at this stuff, isn't it neat; my vinyl collection is almost complete
39
+ # @robut meme all_the_things write; all the plugins
16
40
  #
17
41
  # Send message to the specified meme generator. If the meme requires
18
42
  # more than one line of text, lines should be separated with a semicolon.
19
43
  def handle(time, sender_nick, message)
20
- word = words(message).first
21
- if sent_to_me?(message) && Meme::GENERATORS.has_key?(word.upcase)
22
- words = words(message)
23
- g = Meme.new(words.shift.upcase)
24
- line1, line2 = words.join(' ').split(';')
25
-
26
- reply(g.generate(line1, line2))
44
+ return unless sent_to_me?(message)
45
+ words = words(message)
46
+ command = words.shift.downcase
47
+ return unless command == 'meme'
48
+ meme = words.shift
49
+
50
+ if meme == 'list'
51
+ reply("Memes available: #{MEMES.keys.join(', ')}")
52
+ elsif MEMES[meme]
53
+ url = CGI.escape(MEMES[meme])
54
+ line1, line2 = words.join(' ').split(';').map { |line| CGI.escape(line.strip)}
55
+ meme_url = "http://memecaptain.com/i?u=#{url}&tt=#{line1}"
56
+ meme_url += "&tb=#{line2}" if line2
57
+ reply(meme_url)
58
+ else
59
+ reply("Meme not found: #{meme}")
27
60
  end
28
61
  end
29
-
62
+
30
63
  end
@@ -1,11 +1,15 @@
1
-
2
1
  # A simple plugin that replies with "pong" when messaged with ping
3
- class Robut::Plugin::Ping < Robut::Plugin::Base
2
+ class Robut::Plugin::Ping
3
+ include Robut::Plugin
4
+
5
+ def usage
6
+ "#{at_nick} ping - responds 'pong'"
7
+ end
4
8
 
5
9
  # Responds "pong" if +message+ is "ping"
6
10
  def handle(time, sender_nick, message)
7
11
  words = words(message)
8
12
  reply("pong") if sent_to_me?(message) && words.length == 1 && words.first.downcase == 'ping'
9
13
  end
10
-
14
+
11
15
  end
@@ -5,12 +5,13 @@ require 'robut/plugin/rdio/server'
5
5
  # HipChat. +key+ and +secret+ must be set before we can deal with any
6
6
  # Rdio commands. Additionally, you must call +start_server+ in your
7
7
  # Chatfile to start the Rdio web server.
8
- class Robut::Plugin::Rdio < Robut::Plugin::Base
8
+ class Robut::Plugin::Rdio
9
+ include Robut::Plugin
9
10
 
10
11
  class << self
11
12
  # Your Rdio API Key
12
13
  attr_accessor :key
13
-
14
+
14
15
  # Your Rdio API app secret
15
16
  attr_accessor :secret
16
17
 
@@ -30,7 +31,7 @@ class Robut::Plugin::Rdio < Robut::Plugin::Base
30
31
  #
31
32
  # ./rdio-call --consumer-key=YOUR_CONSUMER_KEY --consumer-secret=YOUR_CONSUMER_SECRET getPlaybackToken domain=YOUR_DOMAIN
32
33
  attr_accessor :token
33
-
34
+
34
35
  # The domain associated with +token+. Defaults to localhost.
35
36
  attr_accessor :domain
36
37
  end
@@ -44,6 +45,15 @@ class Robut::Plugin::Rdio < Robut::Plugin::Base
44
45
  Server.domain = self.domain || "localhost"
45
46
  end
46
47
 
48
+ # Returns a description of how to use this plugin
49
+ def usage
50
+ [
51
+ "#{at_nick} play <song> - queues <song> for playing",
52
+ "#{at_nick} play album <album> - queues <album> for playing",
53
+ "#{at_nick} play track <track> - queues <track> for playing"
54
+ ]
55
+ end
56
+
47
57
  # Queues songs into the Rdio web player. @nick play search query
48
58
  # will queue the first search result matching 'search query' into
49
59
  # the web player. It can be an artist, album, or song.
@@ -69,8 +79,8 @@ class Robut::Plugin::Rdio < Robut::Plugin::Base
69
79
  # 'track', it only searches tracks, same for 'album'. Otherwise,
70
80
  # matches both albums and tracks.
71
81
  def search(words)
72
- api = Rdio::Api.new(self.class.key, self.class.secret)
73
-
82
+ api = ::Rdio::Api.new(self.class.key, self.class.secret)
83
+
74
84
  if words[1] == "album"
75
85
  query_string = words[2..-1].join(' ')
76
86
  results = api.search(query_string, "Album")
@@ -82,5 +92,5 @@ class Robut::Plugin::Rdio < Robut::Plugin::Base
82
92
  results = api.search(query_string, "Album,Track")
83
93
  end
84
94
  end
85
-
95
+
86
96
  end
@@ -1,8 +1,9 @@
1
1
  require 'sinatra'
2
2
  require 'json'
3
3
 
4
- class Robut::Plugin::Rdio < Robut::Plugin::Base
5
-
4
+ class Robut::Plugin::Rdio
5
+ include Robut::Plugin
6
+
6
7
  # A simple server to communicate new Rdio sources to the Web
7
8
  # Playback API. The client will update
8
9
  # Robut::Plugin::Rdio::Server.queue with any new sources, and a call
@@ -10,7 +11,7 @@ class Robut::Plugin::Rdio < Robut::Plugin::Base
10
11
  class Server < Sinatra::Base
11
12
 
12
13
  set :root, File.dirname(__FILE__)
13
-
14
+
14
15
  class << self
15
16
  # A list of items that haven't been fetched by the web playback
16
17
  # API yet.
@@ -1,4 +1,3 @@
1
-
2
1
  # This is a simple plugin the envokes the "say" command on whatever is passed
3
2
  # Example:
4
3
  #
@@ -6,15 +5,25 @@
6
5
  #
7
6
  # *Requires that the "say" command is installed and in the path
8
7
  #
9
- class Robut::Plugin::Say < Robut::Plugin::Base
8
+ class Robut::Plugin::Say
9
+ include Robut::Plugin
10
10
 
11
+ # Returns a description of how to use this plugin
12
+ def usage
13
+ "#{at_nick} say <words> - uses Mac OS X's 'say' command to speak <words>"
14
+ end
15
+
11
16
  # Pipes +message+ through the +say+ command
12
17
  def handle(time, sender_nick, message)
13
18
  words = words(message)
14
19
  if sent_to_me?(message) && words.first == "say"
15
- phrase = words[1..-1].join(' ')
20
+ phrase = clean(words[1..-1].join(' '))
16
21
  system("say #{phrase}")
17
22
  end
18
23
  end
19
-
24
+
25
+ def clean(str)
26
+ str.gsub("'", "").gsub(/[^A-Za-z0-9\s]+/, " ").gsub(/\s+/, ' ').strip
27
+ end
28
+
20
29
  end
@@ -1,5 +1,6 @@
1
1
  # A simple regex => response plugin.
2
- class Robut::Plugin::Sayings < Robut::Plugin::Base
2
+ class Robut::Plugin::Sayings
3
+ include Robut::Plugin
3
4
 
4
5
  class << self
5
6
  # A list of arrays. The first element is a regex, the second is
@@ -13,6 +14,11 @@ class Robut::Plugin::Sayings < Robut::Plugin::Base
13
14
  end
14
15
  self.sayings = []
15
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
+
16
22
  # For each element in sayings, creates a regex out of the first
17
23
  # element, tries to match +message+ to it, and replies with the
18
24
  # second element if it found a match. Robut::Plugin::Sayings will
@@ -2,12 +2,18 @@ require 'twss'
2
2
 
3
3
  # A simple plugin that feeds everything said in the room through the
4
4
  # twss gem. Requires the 'twss' gem, obviously.
5
- class Robut::Plugin::TWSS < Robut::Plugin::Base
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
6
12
 
7
13
  # Responds "That's what she said!" if the TWSS gem returns true for
8
14
  # +message+. Strips out any reference to our nick in +message+
9
15
  # before it stuffs +message+ into the gem.
10
16
  def handle(time, sender_nick, message)
11
- reply("That's what she said!") if TWSS(words(message).join(" "))
17
+ reply("That's what she said!") if ::TWSS.classify(words(message).join(" "))
12
18
  end
13
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