robut 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -1
- data/Gemfile +4 -1
- data/README.rdoc +82 -23
- data/lib/rexml_patches.rb +26 -0
- data/lib/robut/connection.rb +20 -11
- data/lib/robut/plugin.rb +88 -3
- data/lib/robut/plugin/alias.rb +109 -0
- data/lib/robut/plugin/calc.rb +8 -2
- data/lib/robut/plugin/echo.rb +8 -3
- data/lib/robut/plugin/help.rb +24 -0
- data/lib/robut/plugin/later.rb +13 -9
- data/lib/robut/plugin/lunch.rb +14 -3
- data/lib/robut/plugin/meme.rb +48 -15
- data/lib/robut/plugin/ping.rb +7 -3
- data/lib/robut/plugin/rdio.rb +16 -6
- data/lib/robut/plugin/rdio/server.rb +4 -3
- data/lib/robut/plugin/say.rb +13 -4
- data/lib/robut/plugin/sayings.rb +7 -1
- data/lib/robut/plugin/twss.rb +8 -2
- data/lib/robut/plugin/weather.rb +126 -0
- data/lib/robut/storage/yaml_store.rb +18 -12
- data/lib/robut/version.rb +1 -1
- data/test/fixtures/bad_location.xml +1 -0
- data/test/fixtures/las_vegas.xml +1 -0
- data/test/fixtures/seattle.xml +1 -0
- data/test/fixtures/tacoma.xml +1 -0
- data/test/unit/connection_test.rb +10 -5
- data/test/unit/plugin/alias_test.rb +75 -0
- data/test/unit/plugin/help_test.rb +45 -0
- data/test/unit/plugin/say_test.rb +5 -0
- data/test/unit/plugin/weather_test.rb +100 -0
- data/test/unit/plugin_test.rb +30 -0
- data/test/unit/storage/yaml_store_test.rb +13 -8
- metadata +54 -39
- data/lib/robut/plugin/base.rb +0 -70
- data/test/unit/plugin/base_test.rb +0 -16
data/.gitignore
CHANGED
data/Gemfile
CHANGED
@@ -6,6 +6,8 @@ gem 'rake'
|
|
6
6
|
|
7
7
|
group :test do
|
8
8
|
gem 'simplecov'
|
9
|
+
gem 'webmock'
|
10
|
+
gem 'time-warp'
|
9
11
|
end
|
10
12
|
|
11
13
|
group :plugin do
|
@@ -14,4 +16,5 @@ group :plugin do
|
|
14
16
|
gem 'rdio', '0.0.91' # .92 is horked
|
15
17
|
gem 'sinatra'
|
16
18
|
gem 'meme_generator'
|
17
|
-
|
19
|
+
gem 'nokogiri'
|
20
|
+
end
|
data/README.rdoc
CHANGED
@@ -20,10 +20,10 @@ Others listen to everything, and don't require an @reply.
|
|
20
20
|
|
21
21
|
Some of the included plugins require extra gems to be installed:
|
22
22
|
|
23
|
-
[Robut::Plugin::TWSS] requires the <tt>twss</tt> gem
|
24
|
-
[Robut::Plugin::
|
25
|
-
[Robut::Plugin::
|
26
|
-
[Robut::Plugin::
|
23
|
+
[Robut::Plugin::TWSS] requires the <tt>twss</tt> gem.
|
24
|
+
[Robut::Plugin::Calc] requires the <tt>calc</tt> gem.
|
25
|
+
[Robut::Plugin::Rdio] requires the <tt>rdio</tt> and <tt>sinatra</tt> gems.
|
26
|
+
[Robut::Plugin::Weather] requires the <tt>nokogiri</tt> gem.
|
27
27
|
|
28
28
|
== The Chatfile
|
29
29
|
|
@@ -88,35 +88,94 @@ This block usually goes at the end of the Chatfile.
|
|
88
88
|
|
89
89
|
Robut includes a few plugins that we've found useful:
|
90
90
|
|
91
|
-
[Robut::Plugin::Calc] a simple calculator.
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
[Robut::Plugin::
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
91
|
+
[Robut::Plugin::Calc] a simple calculator.
|
92
|
+
|
93
|
+
@robut calc 1 + 1 # => 2
|
94
|
+
|
95
|
+
|
96
|
+
[Robut::Plugin::Lunch] a random decider for lunch locations.
|
97
|
+
|
98
|
+
@robut lunch? # => "Banh Mi!"
|
99
|
+
|
100
|
+
|
101
|
+
[Robut::Plugin::Meme] an interface to memecaptain.
|
102
|
+
|
103
|
+
@robut meme all_the_things drink; all the beer
|
104
|
+
|
105
|
+
|
106
|
+
[Robut::Plugin::Sayings] a simple regex listener and responder.
|
107
|
+
|
108
|
+
You're the worst robot ever, @robut. # => I know.
|
109
|
+
|
110
|
+
[Robut::Plugin::TWSS] an interface to the TWSS gem. Listens to anything said in the chat room and responds "That's what she said!" where appropriate.
|
111
|
+
|
112
|
+
well hurry up, you're not going fast enough # => "That's what she said!"
|
113
|
+
|
114
|
+
|
115
|
+
[Robut::Plugin::Echo] echo back whatever it gets.
|
116
|
+
|
117
|
+
@robut echo hello world # => "hello world"
|
118
|
+
|
119
|
+
|
120
|
+
[Robut::Plugin::Say] invokes the "say" command (text-to-speech).
|
121
|
+
|
122
|
+
@robut say this rocks # (make sure robut is running on a machine with speakers :)
|
123
|
+
|
124
|
+
|
125
|
+
[Robut::Plugin::Ping] responds with "pong".
|
126
|
+
|
127
|
+
@robut ping # => "pong"
|
128
|
+
|
129
|
+
|
130
|
+
[Robut::Plugin::Later] performs the given command after waiting an arbitrary amount of time.
|
131
|
+
|
132
|
+
@robut in 5 minutes echo @justin wake up! # => (5 minutes later) "@justin wake up!"
|
133
|
+
|
134
|
+
|
135
|
+
[Robut::Plugin::Rdio] uses the Rdio[http://www.rdio.com] API and a simple web server to queue and play songs on Rdio.
|
136
|
+
|
137
|
+
@robut play ok computer
|
138
|
+
|
139
|
+
|
140
|
+
[Robut::Plugin::Weather] uses Google Weather to fetch for the weather for a given location and day.
|
141
|
+
|
142
|
+
@robut seattle weather saturday? # => "Forecast for Seattle, WA on Sat: Sunny, High: 77F, Low: 55F"
|
143
|
+
|
144
|
+
[Robut::Plugin::Alias] creates aliases to other robut commands.
|
145
|
+
|
146
|
+
@robut alias "cowboy" "@robut play bon jovi wanted dead or alive" # Cuz somtimes you need it.
|
147
|
+
@robut alias w weather? # less typing for common stuff
|
148
|
+
|
149
|
+
[Robut::Plugin::Help] lists usage for all the plugins loaded into robut
|
150
|
+
|
151
|
+
@robut help # => command usage
|
101
152
|
|
102
153
|
== Writing custom plugins
|
103
154
|
|
104
|
-
You can supply your own plugins to Robut. To create a plugin,
|
105
|
-
Robut::Plugin
|
106
|
-
message)</tt> to perform any plugin-specific logic.
|
155
|
+
You can supply your own plugins to Robut. To create a plugin, include
|
156
|
+
the Robut::Plugin module and implement the <tt>handle(time,
|
157
|
+
sender_nick, message)</tt> to perform any plugin-specific logic.
|
107
158
|
|
108
|
-
Robut::Plugin
|
159
|
+
Robut::Plugin provides a few helper methods that are documented
|
109
160
|
in its class definition.
|
110
161
|
|
111
162
|
== Contributing
|
112
163
|
|
113
|
-
|
164
|
+
To test your changes:
|
165
|
+
|
166
|
+
1. Install [Bundler](http://gembundler.com/)
|
167
|
+
2. Run `bundle install`
|
168
|
+
3. Make your changes and run `bundle exec ruby -Ilib bin/robut ~/MyTestingChatfile`
|
169
|
+
4. Add tests and verify by running `bundle exec rake test`
|
170
|
+
|
171
|
+
Once your changes are ready:
|
114
172
|
|
115
173
|
1. Fork robut
|
116
|
-
2. Create a topic branch
|
117
|
-
3.
|
118
|
-
4.
|
119
|
-
5.
|
174
|
+
2. Create a topic branch: `git checkout -b my_branch`
|
175
|
+
3. Commit your changes
|
176
|
+
4. Push to your branch: `git push origin my_branch`
|
177
|
+
5. Send me a pull request
|
178
|
+
6. That's it!
|
120
179
|
|
121
180
|
== Todo
|
122
181
|
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Encoding patch, stolen from https://github.com/ln/xmpp4r/issues/3#issuecomment-1739952
|
2
|
+
require 'socket'
|
3
|
+
class TCPSocket
|
4
|
+
def external_encoding
|
5
|
+
Encoding::BINARY
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'rexml/source'
|
10
|
+
class REXML::IOSource
|
11
|
+
alias_method :encoding_assign, :encoding=
|
12
|
+
def encoding=(value)
|
13
|
+
encoding_assign(value) if value
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
begin
|
18
|
+
# OpenSSL is optional and can be missing
|
19
|
+
require 'openssl'
|
20
|
+
class OpenSSL::SSL::SSLSocket
|
21
|
+
def external_encoding
|
22
|
+
Encoding::BINARY
|
23
|
+
end
|
24
|
+
end
|
25
|
+
rescue
|
26
|
+
end
|
data/lib/robut/connection.rb
CHANGED
@@ -3,10 +3,15 @@ require 'xmpp4r/muc/helper/simplemucclient'
|
|
3
3
|
require 'xmpp4r/roster/helper/roster'
|
4
4
|
require 'ostruct'
|
5
5
|
|
6
|
+
if defined?(Encoding)
|
7
|
+
# Monkey-patch an incompatibility between ejabberd and rexml
|
8
|
+
require 'rexml_patches'
|
9
|
+
end
|
10
|
+
|
6
11
|
# Handles opening a connection to the HipChat server, and feeds all
|
7
12
|
# messages through our Robut::Plugin list.
|
8
13
|
class Robut::Connection
|
9
|
-
|
14
|
+
|
10
15
|
# The configuration used by the Robut connection.
|
11
16
|
#
|
12
17
|
# Parameters:
|
@@ -53,12 +58,12 @@ class Robut::Connection
|
|
53
58
|
def config=(config)
|
54
59
|
@config = config.kind_of?(Hash) ? OpenStruct.new(config) : config
|
55
60
|
end
|
56
|
-
|
61
|
+
|
57
62
|
# Initializes the connection. If no +config+ is passed, it defaults
|
58
63
|
# to the class_level +config+ instance variable.
|
59
64
|
def initialize(_config = nil)
|
60
65
|
self.config = _config || self.class.config
|
61
|
-
|
66
|
+
|
62
67
|
self.client = Jabber::Client.new(self.config.jid)
|
63
68
|
self.muc = Jabber::MUC::SimpleMUCClient.new(client)
|
64
69
|
self.store = self.config.store || Robut::Storage::HashStore # default to in-memory store only
|
@@ -68,7 +73,7 @@ class Robut::Connection
|
|
68
73
|
Jabber.debug = true
|
69
74
|
end
|
70
75
|
end
|
71
|
-
|
76
|
+
|
72
77
|
# Send +message+ to the room we're currently connected to, or
|
73
78
|
# directly to the person referenced by +to+. +to+ can be either a
|
74
79
|
# jid or the string name of the person.
|
@@ -77,7 +82,7 @@ class Robut::Connection
|
|
77
82
|
unless to.kind_of?(Jabber::JID)
|
78
83
|
to = find_jid_by_name(to)
|
79
84
|
end
|
80
|
-
|
85
|
+
|
81
86
|
msg = Jabber::Message.new(to || muc.room, message)
|
82
87
|
msg.type = :chat
|
83
88
|
client.send(msg)
|
@@ -86,8 +91,12 @@ class Robut::Connection
|
|
86
91
|
end
|
87
92
|
end
|
88
93
|
|
89
|
-
# Sends the chat message +message+ through +plugins+.
|
94
|
+
# Sends the chat message +message+ through +plugins+.
|
90
95
|
def handle_message(plugins, time, nick, message)
|
96
|
+
# ignore all messages sent by robut. If you really want robut to
|
97
|
+
# reply to itself, you can use +fake_message+.
|
98
|
+
return if nick == config.nick
|
99
|
+
|
91
100
|
plugins.each do |plugin|
|
92
101
|
begin
|
93
102
|
rsp = plugin.handle(time, nick, message)
|
@@ -122,7 +131,7 @@ class Robut::Connection
|
|
122
131
|
# Add the callback from direct messages. Turns out the
|
123
132
|
# on_private_message callback doesn't do what it sounds like, so I
|
124
133
|
# have to go a little deeper into xmpp4r to get this working.
|
125
|
-
client.add_message_callback(200, self)
|
134
|
+
client.add_message_callback(200, self) do |message|
|
126
135
|
if !muc.from_room?(message.from) && message.type == :chat && message.body
|
127
136
|
time = Time.now # TODO: get real timestamp? Doesn't seem like
|
128
137
|
# jabber gives it to us
|
@@ -133,8 +142,8 @@ class Robut::Connection
|
|
133
142
|
else
|
134
143
|
false
|
135
144
|
end
|
136
|
-
|
137
|
-
|
145
|
+
end
|
146
|
+
|
138
147
|
muc.join(config.room + '/' + config.nick)
|
139
148
|
|
140
149
|
trap_signals
|
@@ -142,7 +151,7 @@ class Robut::Connection
|
|
142
151
|
end
|
143
152
|
|
144
153
|
private
|
145
|
-
|
154
|
+
|
146
155
|
# Since we're entering an infinite loop, we have to trap TERM and
|
147
156
|
# INT. If something like the Rdio plugin has started a server that
|
148
157
|
# has already trapped those signals, we want to run those signal
|
@@ -153,7 +162,7 @@ class Robut::Connection
|
|
153
162
|
old_signal_callbacks[signal].call if old_signal_callbacks[signal]
|
154
163
|
exit
|
155
164
|
end
|
156
|
-
|
165
|
+
|
157
166
|
[:INT, :TERM].each do |sig|
|
158
167
|
old_signal_callbacks[sig] = trap(sig) { signal_callback.call(sig) }
|
159
168
|
end
|
data/lib/robut/plugin.rb
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
# Robut plugins implement a simple interface to listen for messages
|
2
|
-
# and optionally respond to them. All plugins
|
3
|
-
#
|
2
|
+
# and optionally respond to them. All plugins include the Robut::Plugin
|
3
|
+
# module.
|
4
4
|
module Robut::Plugin
|
5
|
-
autoload :Base, 'robut/plugin/base'
|
6
5
|
|
7
6
|
class << self
|
8
7
|
# A list of all available plugin classes. When you require a new
|
@@ -13,4 +12,90 @@ module Robut::Plugin
|
|
13
12
|
|
14
13
|
self.plugins = []
|
15
14
|
|
15
|
+
# A reference to the connection attached to this instance of the
|
16
|
+
# plugin. This is mostly used to communicate back to the server.
|
17
|
+
attr_accessor :connection
|
18
|
+
|
19
|
+
# If we are handling a private message, holds a reference to the
|
20
|
+
# sender of the message. +nil+ if the message was sent to the entire
|
21
|
+
# room.
|
22
|
+
attr_accessor :private_sender
|
23
|
+
|
24
|
+
# Creates a new instance of this plugin that references the
|
25
|
+
# specified connection.
|
26
|
+
def initialize(connection, private_sender = nil)
|
27
|
+
self.connection = connection
|
28
|
+
self.private_sender = private_sender
|
29
|
+
end
|
30
|
+
|
31
|
+
# Send +message+ back to the HipChat server. If +to+ == +:room+,
|
32
|
+
# replies to the room. If +to+ == nil, responds in the manner the
|
33
|
+
# original message was sent. Otherwise, PMs the message to +to+.
|
34
|
+
def reply(message, to = nil)
|
35
|
+
if to == :room
|
36
|
+
connection.reply(message, nil)
|
37
|
+
else
|
38
|
+
connection.reply(message, to || private_sender)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# An ordered list of all words in the message with any reference to
|
43
|
+
# the bot's nick stripped out. If +command+ is passed in, it is also
|
44
|
+
# stripped out. This is useful to separate the 'parameters' from the
|
45
|
+
# 'commands' in a message.
|
46
|
+
def words(message, command = nil)
|
47
|
+
reply = at_nick
|
48
|
+
command = command.downcase if command
|
49
|
+
message.split.reject {|word| word.downcase == reply || word.downcase == command }
|
50
|
+
end
|
51
|
+
|
52
|
+
# Removes the first word in message if it is a reference to the bot's nick
|
53
|
+
# Given "@robut do this thing", Returns "do this thing"
|
54
|
+
def without_nick(message)
|
55
|
+
possible_nick, command = message.split(' ', 2)
|
56
|
+
if possible_nick == at_nick
|
57
|
+
command
|
58
|
+
else
|
59
|
+
message
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# The bot's nickname, for @-replies.
|
64
|
+
def nick
|
65
|
+
connection.config.nick.split.first
|
66
|
+
end
|
67
|
+
|
68
|
+
# #nick with the @-symbol prepended
|
69
|
+
def at_nick
|
70
|
+
"@#{nick.downcase}"
|
71
|
+
end
|
72
|
+
|
73
|
+
# Was +message+ sent to Robut as an @reply?
|
74
|
+
def sent_to_me?(message)
|
75
|
+
message =~ /(^|\s)@#{nick}(\s|$)/i
|
76
|
+
end
|
77
|
+
|
78
|
+
# Do whatever you need to do to handle this message.
|
79
|
+
# If you want to stop the plugin execution chain, return +true+ from this
|
80
|
+
# method. Plugins are handled in the order that they appear in
|
81
|
+
# Robut::Plugin.plugins
|
82
|
+
def handle(time, sender_nick, message)
|
83
|
+
raise NotImplementedError, "Implement me in #{self.class.name}!"
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns a list of messages describing the commands this plugin
|
87
|
+
# handles.
|
88
|
+
def usage
|
89
|
+
end
|
90
|
+
|
91
|
+
def fake_message(time, sender_nick, msg)
|
92
|
+
# TODO: ensure this connection is threadsafe
|
93
|
+
plugins = Robut::Plugin.plugins.map { |p| p.new(connection, private_sender) }
|
94
|
+
connection.handle_message(plugins, time, sender_nick, msg)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Accessor for the store instance
|
98
|
+
def store
|
99
|
+
connection.store
|
100
|
+
end
|
16
101
|
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
|
3
|
+
# Alias robut commands:
|
4
|
+
#
|
5
|
+
# @robut alias "something" "some long message"
|
6
|
+
#
|
7
|
+
# Later if @robut receives the message "@robut something" he will
|
8
|
+
# repond as if he received "@robut some long message"
|
9
|
+
#
|
10
|
+
#
|
11
|
+
# Valid use:
|
12
|
+
#
|
13
|
+
# @robut alias this "something long"
|
14
|
+
# @robut alias "this thing" "something long"
|
15
|
+
# @robut alias this something_long
|
16
|
+
#
|
17
|
+
# Listing all aliases
|
18
|
+
#
|
19
|
+
# @robut aliases
|
20
|
+
#
|
21
|
+
# Removing aliases
|
22
|
+
#
|
23
|
+
# @robut remove alias "this alias"
|
24
|
+
# @robut remove alias this
|
25
|
+
# @robut remove clear aliases # removes everything
|
26
|
+
#
|
27
|
+
# Note: you probably want the Alias plugin as one of the first things
|
28
|
+
# in the plugin array (since plugins are executed in order).
|
29
|
+
class Robut::Plugin::Alias
|
30
|
+
include Robut::Plugin
|
31
|
+
|
32
|
+
# Returns a description of how to use this plugin
|
33
|
+
def usage
|
34
|
+
[
|
35
|
+
"#{at_nick} alias <words> <expansion> - when #{nick} sees <words>, pretend it saw <expansion> instead",
|
36
|
+
"#{at_nick} aliases - show all aliases",
|
37
|
+
"#{at_nick} remove alias <words> - remove <words> as an alias",
|
38
|
+
"#{at_nick} clear aliases - remove all aliases"
|
39
|
+
]
|
40
|
+
end
|
41
|
+
|
42
|
+
# Perform the calculation specified in +message+, and send the
|
43
|
+
# result back.
|
44
|
+
def handle(time, sender_nick, message)
|
45
|
+
if new_message = get_alias(message)
|
46
|
+
# Apply the alias
|
47
|
+
fake_message Time.now, sender_nick, new_message
|
48
|
+
elsif sent_to_me?(message)
|
49
|
+
message = without_nick message
|
50
|
+
if message =~ /^remove alias (.*)/
|
51
|
+
# Remove the alias
|
52
|
+
key = parse_alias_key($1)
|
53
|
+
remove_alias key
|
54
|
+
return true
|
55
|
+
elsif message =~ /^clear aliases$/
|
56
|
+
self.aliases = {}
|
57
|
+
return true
|
58
|
+
elsif message =~ /^alias (.*)/
|
59
|
+
# Create a new alias
|
60
|
+
message = $1
|
61
|
+
key, value = parse_alias message
|
62
|
+
store_alias key, value
|
63
|
+
return true # hault plugin execution chain
|
64
|
+
elsif words(message).first == 'aliases'
|
65
|
+
# List all aliases
|
66
|
+
m, a = [], aliases # create reference to avoid going to the store every time
|
67
|
+
a.keys.sort.each { |key| m << "#{key} => #{a[key]}" }
|
68
|
+
reply m.join("\n")
|
69
|
+
return true
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Given a message, returns what it is aliased to (or nil)
|
75
|
+
def get_alias(msg)
|
76
|
+
(store['aliases'] || {})[msg]
|
77
|
+
end
|
78
|
+
|
79
|
+
def store_alias(key, value)
|
80
|
+
aliases[key] = value
|
81
|
+
store['aliases'] = aliases
|
82
|
+
end
|
83
|
+
|
84
|
+
def remove_alias(key)
|
85
|
+
new_aliases = aliases
|
86
|
+
new_aliases.delete(key)
|
87
|
+
store['aliases'] = new_aliases
|
88
|
+
end
|
89
|
+
|
90
|
+
def aliases
|
91
|
+
store['aliases'] ||= {}
|
92
|
+
end
|
93
|
+
|
94
|
+
def aliases=(v)
|
95
|
+
store['aliases'] = v
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns alias and command
|
99
|
+
def parse_alias(str)
|
100
|
+
r = Shellwords.shellwords str
|
101
|
+
return r[0], r[1] if r.length == 2
|
102
|
+
return r[0], r[1..-1].join(' ')
|
103
|
+
end
|
104
|
+
|
105
|
+
def parse_alias_key(str)
|
106
|
+
Shellwords.shellwords(str).join(' ')
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|