robut 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/.gitignore +6 -0
  2. data/Gemfile +17 -0
  3. data/README.rdoc +124 -0
  4. data/Rakefile +27 -0
  5. data/bin/robut +9 -0
  6. data/examples/Chatfile +22 -0
  7. data/lib/robut.rb +5 -0
  8. data/lib/robut/connection.rb +167 -0
  9. data/lib/robut/plugin.rb +16 -0
  10. data/lib/robut/plugin/base.rb +70 -0
  11. data/lib/robut/plugin/calc.rb +20 -0
  12. data/lib/robut/plugin/echo.rb +14 -0
  13. data/lib/robut/plugin/later.rb +73 -0
  14. data/lib/robut/plugin/lunch.rb +57 -0
  15. data/lib/robut/plugin/meme.rb +30 -0
  16. data/lib/robut/plugin/ping.rb +11 -0
  17. data/lib/robut/plugin/rdio.rb +70 -0
  18. data/lib/robut/plugin/rdio/public/css/rdio.css +141 -0
  19. data/lib/robut/plugin/rdio/public/css/style.css +79 -0
  20. data/lib/robut/plugin/rdio/public/css/style_after.css +42 -0
  21. data/lib/robut/plugin/rdio/public/images/background.png +0 -0
  22. data/lib/robut/plugin/rdio/public/images/no-album.png +0 -0
  23. data/lib/robut/plugin/rdio/public/index.html +42 -0
  24. data/lib/robut/plugin/rdio/public/js/libs/dd_belatedpng.js +13 -0
  25. data/lib/robut/plugin/rdio/public/js/libs/jquery-1.5.1.min.js +16 -0
  26. data/lib/robut/plugin/rdio/public/js/libs/modernizr-1.7.min.js +2 -0
  27. data/lib/robut/plugin/rdio/public/js/rdio.js +129 -0
  28. data/lib/robut/plugin/rdio/public/js/script.js +3 -0
  29. data/lib/robut/plugin/rdio/server.rb +34 -0
  30. data/lib/robut/plugin/say.rb +20 -0
  31. data/lib/robut/plugin/sayings.rb +31 -0
  32. data/lib/robut/plugin/twss.rb +13 -0
  33. data/lib/robut/storage.rb +3 -0
  34. data/lib/robut/storage/base.rb +21 -0
  35. data/lib/robut/storage/hash_store.rb +26 -0
  36. data/lib/robut/storage/yaml_store.rb +51 -0
  37. data/lib/robut/version.rb +4 -0
  38. data/robut.gemspec +23 -0
  39. data/test/mocks/connection_mock.rb +26 -0
  40. data/test/simplecov_helper.rb +2 -0
  41. data/test/test_helper.rb +7 -0
  42. data/test/unit/connection_test.rb +123 -0
  43. data/test/unit/plugin/base_test.rb +16 -0
  44. data/test/unit/plugin/echo_test.rb +26 -0
  45. data/test/unit/plugin/later_test.rb +39 -0
  46. data/test/unit/plugin/lunch_test.rb +35 -0
  47. data/test/unit/plugin/ping_test.rb +21 -0
  48. data/test/unit/plugin/say_test.rb +28 -0
  49. data/test/unit/storage/hash_store_test.rb +15 -0
  50. data/test/unit/storage/yaml_store_test.rb +42 -0
  51. data/test/unit/storage/yaml_test.yml +1 -0
  52. metadata +135 -0
@@ -0,0 +1,20 @@
1
+ require 'calc'
2
+
3
+ # A simple calculator. This delegates all calculations to the 'calc'
4
+ # gem.
5
+ class Robut::Plugin::Calc < Robut::Plugin::Base
6
+
7
+ # Perform the calculation specified in +message+, and send the
8
+ # result back.
9
+ def handle(time, sender_nick, message)
10
+ if sent_to_me?(message) && words(message).first == 'calc'
11
+ calculation = words(message, 'calc').join(' ')
12
+ begin
13
+ reply("#{calculation} = #{Calc.evaluate(calculation)}")
14
+ rescue
15
+ reply("Can't calculate that.")
16
+ end
17
+ end
18
+ end
19
+
20
+ end
@@ -0,0 +1,14 @@
1
+
2
+ # A plugin that tells robut to repeat whatever he's told.
3
+ class Robut::Plugin::Echo < Robut::Plugin::Base
4
+
5
+ # Responds with +message+ if the command sent to robut is 'echo'.
6
+ def handle(time, sender_nick, message)
7
+ words = words(message)
8
+ if sent_to_me?(message) && words.first == 'echo'
9
+ phrase = words[1..-1].join(' ')
10
+ reply(phrase) unless phrase.empty?
11
+ end
12
+ end
13
+
14
+ end
@@ -0,0 +1,73 @@
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 < Robut::Plugin::Base
19
+
20
+ # Passes +message+ back through the plugin chain if we've been given
21
+ # a time to execute it later.
22
+ def handle(time, sender_nick, message)
23
+ if sent_to_me?(message)
24
+ phrase = words(message).join(' ')
25
+ if phrase =~ timer_regex
26
+ count = $1.to_i
27
+ scale = $2
28
+ future_message = at_nick + ' ' + $3
29
+
30
+ sleep_time = count * scale_multiplier(scale)
31
+
32
+ reply("Ok, see you in #{count} #{scale}")
33
+
34
+ connection = self.connection
35
+ threader do
36
+ 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)
40
+ end
41
+ return true
42
+ end
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ # A regex that detects a time.
49
+ def timer_regex
50
+ /in (.*) (sec|secs|second|seconds|min|mins|minute|minutes|hr|hrs|hour|hours) (.*)$/
51
+ end
52
+
53
+ # Takes a time scale (secs, mins, hours, etc.) and returns the
54
+ # factor to convert it into seconds.
55
+ def scale_multiplier(time_scale)
56
+ case time_scale
57
+ when /sec(s|ond|onds)?/
58
+ 1
59
+ when /min(s|ute|utes)?/
60
+ 60
61
+ when /(hr|hrs|hour|hours)/
62
+ 60 * 60
63
+ end
64
+ end
65
+
66
+ # Asynchronously runs the given block.
67
+ def threader
68
+ Thread.new do
69
+ yield
70
+ end
71
+ end
72
+
73
+ end
@@ -0,0 +1,57 @@
1
+ # Where should we go to lunch today?
2
+ class Robut::Plugin::Lunch < Robut::Plugin::Base
3
+
4
+ # Replies with a random string selected from +places+.
5
+ def handle(time, sender_nick, message)
6
+ words = words(message)
7
+ phrase = words.join(' ')
8
+ # lunch?
9
+ if phrase =~ /(lunch|food)\?/i
10
+ if places.empty?
11
+ reply "I don't know about any lunch places"
12
+ else
13
+ reply places[rand(places.length)] + "!"
14
+ end
15
+ # @robut lunch places
16
+ elsif phrase == "lunch places" && sent_to_me?(message)
17
+ if places.empty?
18
+ reply "I don't know about any lunch places"
19
+ else
20
+ reply places.join(', ')
21
+ end
22
+ # @robut new lunch place Green Leaf
23
+ elsif phrase =~ /new lunch place (.*)/i && sent_to_me?(message)
24
+ place = $1
25
+ new_place(place)
26
+ reply "Ok, I'll add \"#{place}\" to the the list of lunch places"
27
+ # @robut remove luynch place Green Leaf
28
+ elsif phrase =~ /remove lunch place (.*)/i && sent_to_me?(message)
29
+ place = $1
30
+ remove_place(place)
31
+ reply "I removed \"#{place}\" from the list of lunch places"
32
+ end
33
+ end
34
+
35
+ # Stores +place+ as a new lunch place.
36
+ def new_place(place)
37
+ store["lunch_places"] ||= []
38
+ store["lunch_places"] = (store["lunch_places"] + Array(place)).uniq
39
+ end
40
+
41
+ # Removes +place+ from the list of lunch places.
42
+ def remove_place(place)
43
+ store["lunch_places"] ||= []
44
+ store["lunch_places"] = store["lunch_places"] - Array(place)
45
+ end
46
+
47
+ # Returns the list of lunch places we know about.
48
+ def places
49
+ store["lunch_places"] ||= []
50
+ end
51
+
52
+ # Sets the list of lunch places to +v+
53
+ def places=(v)
54
+ store["lunch_places"] = v
55
+ end
56
+
57
+ end
@@ -0,0 +1,30 @@
1
+ require 'meme'
2
+
3
+ # A simple plugin that wraps meme_generator. Requires the
4
+ # 'meme_generator' gem.
5
+ class Robut::Plugin::Meme < Robut::Plugin::Base
6
+
7
+ # This plugin is activated when robut is sent a message starting
8
+ # with the name of a meme. The list of generators can be discovered
9
+ # by running
10
+ #
11
+ # meme list
12
+ #
13
+ # from the command line. Example:
14
+ #
15
+ # @robut h_mermaid look at this stuff, isn't it neat; my vinyl collection is almost complete
16
+ #
17
+ # Send message to the specified meme generator. If the meme requires
18
+ # more than one line of text, lines should be separated with a semicolon.
19
+ 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))
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,11 @@
1
+
2
+ # A simple plugin that replies with "pong" when messaged with ping
3
+ class Robut::Plugin::Ping < Robut::Plugin::Base
4
+
5
+ # Responds "pong" if +message+ is "ping"
6
+ def handle(time, sender_nick, message)
7
+ words = words(message)
8
+ reply("pong") if sent_to_me?(message) && words.length == 1 && words.first.downcase == 'ping'
9
+ end
10
+
11
+ end
@@ -0,0 +1,70 @@
1
+ require 'rdio'
2
+ require 'robut/plugin/rdio/server'
3
+
4
+ # A plugin that hooks into Rdio, allowing you to queue songs from
5
+ # HipChat. +key+ and +secret+ must be set before we can deal with any
6
+ # Rdio commands. Additionally, you must call +start_server+ in your
7
+ # Chatfile to start the Rdio web server.
8
+ class Robut::Plugin::Rdio < Robut::Plugin::Base
9
+
10
+ class << self
11
+ # Your Rdio API Key
12
+ attr_accessor :key
13
+
14
+ # Your Rdio API app secret
15
+ attr_accessor :secret
16
+
17
+ # The port the Rdio web player will run on. Defaults to 4567.
18
+ attr_accessor :port
19
+
20
+ # The host the Rdio web player will run on. Defaults to localhost.
21
+ attr_accessor :host
22
+ end
23
+
24
+ # Starts a Robut::Plugin::Rdio::Server server for communicating with
25
+ # the actual Rdio web player. You must call this in the Chatfile if
26
+ # you plan on using this gem.
27
+ def self.start_server
28
+ @server = Thread.new { Server.run! :host => (host || "localhost"), :port => (port || 4567) }
29
+ end
30
+
31
+ # Queues songs into the Rdio web player. @nick play search query
32
+ # will queue the first search result matching 'search query' into
33
+ # the web player. It can be an artist, album, or song.
34
+ def handle(time, sender_nick, message)
35
+ words = words(message)
36
+ if sent_to_me?(message) && words.first == 'play'
37
+ results = search(words)
38
+ result = results.first
39
+ if result
40
+ Server.queue << result.key
41
+ name = result.name
42
+ name = "#{result.artist_name} - #{name}" if result.respond_to?(:artist_name) && result.artist_name
43
+ reply("Playing #{name}")
44
+ else
45
+ reply("I couldn't find #{query_string} on Rdio.")
46
+ end
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ # Searches Rdio for sources matching +words+. If the first word is
53
+ # 'track', it only searches tracks, same for 'album'. Otherwise,
54
+ # matches both albums and tracks.
55
+ def search(words)
56
+ api = Rdio::Api.new(self.class.key, self.class.secret)
57
+
58
+ if words[1] == "album"
59
+ query_string = words[2..-1].join(' ')
60
+ results = api.search(query_string, "Album")
61
+ elsif words[1] == "track"
62
+ query_string = words[2..-1].join(' ')
63
+ results = api.search(query_string, "Track")
64
+ else
65
+ query_string = words[1..-1].join(' ')
66
+ results = api.search(query_string, "Album,Track")
67
+ end
68
+ end
69
+
70
+ end
@@ -0,0 +1,141 @@
1
+
2
+ #content {
3
+ margin: 16px 120px;
4
+ }
5
+
6
+ body {
7
+ background-color: #bbb;
8
+ background: url('/images/background.png');
9
+ }
10
+
11
+ h1, h2 {
12
+ background-color: #fff;
13
+ text-rendering: optimizeLegibility;
14
+ padding: 8px 12px;
15
+ display: none;
16
+ }
17
+
18
+ h1 {
19
+ font-size: 2.2em;
20
+ margin-bottom: 24px;
21
+ }
22
+
23
+ h2 {
24
+ clear: left;
25
+ font-size: 1.8em;
26
+ -moz-border-radius-topleft: 4px;
27
+ -moz-border-radius-topright: 4px;
28
+ -moz-border-radius-bottomright: 0px;
29
+ -moz-border-radius-bottomleft: 0px;
30
+ border-top-left-radius: 4px;
31
+ border-top-right-radius: 4px;
32
+ border-bottom-right-radius: 0px;
33
+ border-bottom-left-radius: 0px;
34
+ }
35
+
36
+ #player {
37
+ margin-bottom: 32px;
38
+ }
39
+
40
+ #player:after {
41
+ content: " ";
42
+ display: block;
43
+ height: 0;
44
+ clear: both;
45
+ visibility: hidden;
46
+ }
47
+
48
+ #filter {
49
+ background: none; /* Old browsers */
50
+
51
+ background: -webkit-gradient(linear, 0 0, 0 100%,
52
+ color-stop(0, rgba(255,255,255,0.30)),
53
+ color-stop(0.5, rgba(255,255,255,0.25)),
54
+ color-stop(0.5, rgba(255,255,255,0.1)),
55
+ color-stop(1, rgba(255,255,255,0.30)));
56
+
57
+ position: absolute;
58
+ top: 0px;
59
+ left: 0px;
60
+ height: 200px;
61
+ width: 200px;
62
+ }
63
+
64
+ #controls {
65
+ position: relative;
66
+ width: 200px;
67
+ float: left;
68
+ min-height: 200px;
69
+ }
70
+
71
+ #controls img {
72
+ -webkit-box-shadow: 0px 3px 4px #666;
73
+ -moz-box-shadow: 0px 3px 4px #666;
74
+ box-shadow: 0px 3px 4px #666;
75
+ }
76
+
77
+ #controls img, #controls #filter, h1 {
78
+ -moz-border-radius: 4px;
79
+ border-radius: 4px;
80
+ }
81
+
82
+ ul {
83
+ margin: 0px;
84
+ list-style-type: none;
85
+ margin-bottom: 24px;
86
+ }
87
+
88
+ #now_playing {
89
+ margin-left: 220px;
90
+ }
91
+
92
+ li, h1, h2 {
93
+ text-shadow: 0px 1px 1px #ffffff;
94
+ filter: dropshadow(color=#ffffff, offx=0, offy=1);
95
+ background: #eeeeee; /* Old browsers */
96
+ background: -moz-linear-gradient(top, #eeeeee 0%, #cccccc 100%); /* FF3.6+ */
97
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#eeeeee), color-stop(100%,#cccccc)); /* Chrome,Safari4+ */
98
+ background: -webkit-linear-gradient(top, #eeeeee 0%,#cccccc 100%); /* Chrome10+,Safari5.1+ */
99
+ background: -o-linear-gradient(top, #eeeeee 0%,#cccccc 100%); /* Opera11.10+ */
100
+ background: -ms-linear-gradient(top, #eeeeee 0%,#cccccc 100%); /* IE10+ */
101
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#cccccc',GradientType=0 ); /* IE6-9 */
102
+ background: linear-gradient(top, #eeeeee 0%,#cccccc 100%); /* W3C */
103
+ }
104
+
105
+ li, h2, h1 {
106
+ border: 1px solid #989898;
107
+ }
108
+
109
+ li, h2 {
110
+ border-bottom-width: 0px;
111
+ }
112
+
113
+ li {
114
+ padding: 8px;
115
+ font-weight: bold;
116
+ }
117
+
118
+ li:last-child {
119
+ border-bottom-width: 1px;
120
+ }
121
+
122
+ li.playing {
123
+ border-color: #007cf9;
124
+ background: #7abcff; /* Old browsers */
125
+ background: -moz-linear-gradient(top, #7abcff 0%, #60abf8 44%, #4096ee 100%); /* FF3.6+ */
126
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#7abcff), color-stop(44%,#60abf8), color-stop(100%,#4096ee)); /* Chrome,Safari4+ */
127
+ background: -webkit-linear-gradient(top, #7abcff 0%,#60abf8 44%,#4096ee 100%); /* Chrome10+,Safari5.1+ */
128
+ background: -o-linear-gradient(top, #7abcff 0%,#60abf8 44%,#4096ee 100%); /* Opera11.10+ */
129
+ background: -ms-linear-gradient(top, #7abcff 0%,#60abf8 44%,#4096ee 100%); /* IE10+ */
130
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#7abcff', endColorstr='#4096ee',GradientType=0 ); /* IE6-9 */
131
+ background: linear-gradient(top, #7abcff 0%,#60abf8 44%,#4096ee 100%); /* W3C */
132
+ color: #fff;
133
+ text-shadow: 0px -1px 1px #777777;
134
+ filter: dropshadow(color=#777777, offx=0, offy=-1);
135
+ }
136
+
137
+ li.playing+li {
138
+ border-top-color: #007cf9;
139
+ }
140
+
141
+
@@ -0,0 +1,79 @@
1
+
2
+ /* ==== Scroll down to find where to put your styles :) ==== */
3
+
4
+ /* HTML5 ✰ Boilerplate */
5
+
6
+ html, body, div, span, object, iframe,
7
+ h1, h2, h3, h4, h5, h6, p, blockquote, pre,
8
+ abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp,
9
+ small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li,
10
+ fieldset, form, label, legend,
11
+ table, caption, tbody, tfoot, thead, tr, th, td,
12
+ article, aside, canvas, details, figcaption, figure,
13
+ footer, header, hgroup, menu, nav, section, summary,
14
+ time, mark, audio, video {
15
+ margin: 0;
16
+ padding: 0;
17
+ border: 0;
18
+ font-size: 100%;
19
+ font: inherit;
20
+ vertical-align: baseline;
21
+ }
22
+
23
+ article, aside, details, figcaption, figure,
24
+ footer, header, hgroup, menu, nav, section {
25
+ display: block;
26
+ }
27
+
28
+ blockquote, q { quotes: none; }
29
+ blockquote:before, blockquote:after,
30
+ q:before, q:after { content: ''; content: none; }
31
+ ins { background-color: #ff9; color: #000; text-decoration: none; }
32
+ mark { background-color: #ff9; color: #000; font-style: italic; font-weight: bold; }
33
+ del { text-decoration: line-through; }
34
+ abbr[title], dfn[title] { border-bottom: 1px dotted; cursor: help; }
35
+ table { border-collapse: collapse; border-spacing: 0; }
36
+ hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; }
37
+ input, select { vertical-align: middle; }
38
+
39
+ body { font:13px/1.231 sans-serif; *font-size:small; }
40
+ select, input, textarea, button { font:99% sans-serif; }
41
+ pre, code, kbd, samp { font-family: monospace, sans-serif; }
42
+
43
+ html { overflow-y: scroll; }
44
+ a:hover, a:active { outline: none; }
45
+ ul, ol { margin-left: 2em; }
46
+ ol { list-style-type: decimal; }
47
+ nav ul, nav li { margin: 0; list-style:none; list-style-image: none; }
48
+ small { font-size: 85%; }
49
+ strong, th { font-weight: bold; }
50
+ td { vertical-align: top; }
51
+
52
+ sub, sup { font-size: 75%; line-height: 0; position: relative; }
53
+ sup { top: -0.5em; }
54
+ sub { bottom: -0.25em; }
55
+
56
+ pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; padding: 15px; }
57
+ textarea { overflow: auto; }
58
+ .ie6 legend, .ie7 legend { margin-left: -7px; }
59
+ input[type="radio"] { vertical-align: text-bottom; }
60
+ input[type="checkbox"] { vertical-align: bottom; }
61
+ .ie7 input[type="checkbox"] { vertical-align: baseline; }
62
+ .ie6 input { vertical-align: text-bottom; }
63
+ label, input[type="button"], input[type="submit"], input[type="image"], button { cursor: pointer; }
64
+ button, input, select, textarea { margin: 0; }
65
+ input:valid, textarea:valid { }
66
+ input:invalid, textarea:invalid { border-radius: 1px; -moz-box-shadow: 0px 0px 5px red; -webkit-box-shadow: 0px 0px 5px red; box-shadow: 0px 0px 5px red; }
67
+ .no-boxshadow input:invalid, .no-boxshadow textarea:invalid { background-color: #f0dddd; }
68
+
69
+ ::-moz-selection{ background: #FF5E99; color:#fff; text-shadow: none; }
70
+ ::selection { background:#FF5E99; color:#fff; text-shadow: none; }
71
+ a:link { -webkit-tap-highlight-color: #FF5E99; }
72
+
73
+ button { width: auto; overflow: visible; }
74
+ .ie7 img { -ms-interpolation-mode: bicubic; }
75
+
76
+ body, select, input, textarea { color: #444; }
77
+ h1, h2, h3, h4, h5, h6 { font-weight: bold; }
78
+ a, a:active, a:visited { color: #607890; }
79
+ a:hover { color: #036; }