robut 0.2.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.
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; }