robut 0.2.1 → 0.3.0

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