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.
- 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
|