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 CHANGED
@@ -3,4 +3,6 @@
3
3
  Gemfile.lock
4
4
  pkg/*
5
5
  doc/*
6
- coverage/*
6
+ coverage/*
7
+ *.swp
8
+ .DS_Store
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
- end
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::Meme] requires the <tt>meme_generator</tt> gem
25
- [Robut::Plugin::Calc] requires the <tt>calc</tt> gem
26
- [Robut::Plugin::Rdio] requires the <tt>rdio</tt> and <tt>sinatra</tt> gems
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. <br /> <br /> Example: <tt>@robut calc 1 + 1 # => 2</tt>
92
- [Robut::Plugin::Lunch] a random decider for lunch locations. <br /> <br /> Example: <tt>@robut lunch? # => "Banh Mi!"</tt>
93
- [Robut::Plugin::Meme] an interface to memegenerator.net. <br /> <br /> Example: <tt>@robut meme Y_U_NO FIX THE BUILD?</tt>
94
- [Robut::Plugin::Sayings] a simple regex listener and responder. <br /> <br /> Example: <tt>You're the worst robot ever, @robut. # => I know. </tt>
95
- [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. <br /> <br /> Example: <tt>well hurry up, you're not going fast enough # => "That's what she said!" </tt>
96
- [Robut::Plugin::Echo] echo back whatever it gets. <br /> <br /> Example: <tt>@robut echo hello word</tt>
97
- [Robut::Plugin::Say] invokes the "say" command (text-to-speech). <br /> <br /> Example: <tt>@robut say this rocks</tt>
98
- [Robut::Plugin::Ping] responds with "pong". <br /> <br /> Example: <tt>@robut ping</tt>
99
- [Robut::Plugin::Later] performs the given command after waiting an arbitrary amount of time. <br /> <br /> Example: <tt>@robut in 5 minutes echo @justin wake up!</tt>
100
- [Robut::Plugin::Rdio] uses the Rdio[http://www.rdio.com] API and a simple web server to queue and play songs on Rdio. <br /> <br /> Example: <tt>@robut play ok computer</tt>
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, subclass
105
- Robut::Plugin::Base and implement the <tt>handle(time, sender_nick,
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::Base provides a few helper methods that are documented
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
- Once you've made your great commits:
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 - git checkout -b my_branch
117
- 3. Push to your branch - git push origin my_branch
118
- 4. Send me a pull request
119
- 5. That's it!
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
@@ -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) { |message|
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 inherit from
3
- # Robut::Plugin::Base.
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