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