hipbot 0.0.1
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 +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +50 -0
- data/LICENSE +22 -0
- data/README.md +62 -0
- data/Rakefile +4 -0
- data/bin/hipbot +5 -0
- data/bot.rb +92 -0
- data/hipbot.gemspec +17 -0
- data/lib/hipbot.rb +19 -0
- data/lib/hipbot/adapters/hipchat.rb +79 -0
- data/lib/hipbot/adapters/telnet.rb +28 -0
- data/lib/hipbot/bot.rb +72 -0
- data/lib/hipbot/configuration.rb +12 -0
- data/lib/hipbot/message.rb +23 -0
- data/lib/hipbot/mucclient.rb +60 -0
- data/lib/hipbot/reaction.rb +46 -0
- data/lib/hipbot/response.rb +36 -0
- data/lib/hipbot/room.rb +10 -0
- data/lib/hipbot/version.rb +3 -0
- data/spec/integration/hipbot_spec.rb +51 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/unit/hipbot_spec.rb +132 -0
- data/spec/unit/message_spec.rb +59 -0
- data/test_server.rb +23 -0
- metadata +77 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*.gem
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
active_support (3.0.0)
|
5
|
+
activesupport (= 3.0.0)
|
6
|
+
activesupport (3.0.0)
|
7
|
+
addressable (2.2.8)
|
8
|
+
crack (0.1.6)
|
9
|
+
daemons (1.1.8)
|
10
|
+
diff-lcs (1.1.3)
|
11
|
+
em-http-request (0.3.0)
|
12
|
+
addressable (>= 2.0.0)
|
13
|
+
escape_utils
|
14
|
+
eventmachine (>= 0.12.9)
|
15
|
+
escape_utils (0.2.4)
|
16
|
+
eventmachine (0.12.10)
|
17
|
+
google-weather (0.3.0)
|
18
|
+
httparty (~> 0.5.0)
|
19
|
+
httparty (0.5.2)
|
20
|
+
crack (= 0.1.6)
|
21
|
+
i18n (0.6.0)
|
22
|
+
metaclass (0.0.1)
|
23
|
+
mocha (0.11.4)
|
24
|
+
metaclass (~> 0.0.1)
|
25
|
+
rake (0.9.2.2)
|
26
|
+
rspec (2.10.0)
|
27
|
+
rspec-core (~> 2.10.0)
|
28
|
+
rspec-expectations (~> 2.10.0)
|
29
|
+
rspec-mocks (~> 2.10.0)
|
30
|
+
rspec-core (2.10.1)
|
31
|
+
rspec-expectations (2.10.0)
|
32
|
+
diff-lcs (~> 1.1.3)
|
33
|
+
rspec-mocks (2.10.1)
|
34
|
+
xmpp4r (0.5)
|
35
|
+
|
36
|
+
PLATFORMS
|
37
|
+
ruby
|
38
|
+
|
39
|
+
DEPENDENCIES
|
40
|
+
active_support
|
41
|
+
daemons
|
42
|
+
em-http-request
|
43
|
+
eventmachine
|
44
|
+
google-weather
|
45
|
+
httparty
|
46
|
+
i18n
|
47
|
+
mocha
|
48
|
+
rake
|
49
|
+
rspec
|
50
|
+
xmpp4r
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Tomasz Pewiński
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# Hipbot
|
2
|
+
|
3
|
+
[](http://travis-ci.org/pewniak747/hipbot)
|
4
|
+
|
5
|
+
Hipbot is a bot for HipChat, written in ruby & eventmachine.
|
6
|
+
|
7
|
+
This is a work in progress.
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
Install via RubyGems
|
12
|
+
|
13
|
+
```
|
14
|
+
gem install hipbot
|
15
|
+
```
|
16
|
+
|
17
|
+
In bot.rb file, subclass Hipbot::Bot and customize robot responses.
|
18
|
+
|
19
|
+
``` ruby
|
20
|
+
require 'hipbot'
|
21
|
+
|
22
|
+
class MyCompanyBot < Hipbot::Bot
|
23
|
+
configure do |c|
|
24
|
+
c.name = 'robot'
|
25
|
+
c.jid = 'changeme@chat.hipchat.com'
|
26
|
+
c.password = 'secret'
|
27
|
+
end
|
28
|
+
|
29
|
+
# simple reply to '@robot hello!'
|
30
|
+
on /^hello.*/ do
|
31
|
+
reply('hello!')
|
32
|
+
end
|
33
|
+
|
34
|
+
# tasks with arguments: '@robot deploy to production pls'
|
35
|
+
on /deploy to (.*) pls/ do |stage|
|
36
|
+
reply('deploying to #{stage}!')
|
37
|
+
post("http://#{stage}.example.com") do |http|
|
38
|
+
reply("deploy server responded with #{http.response_header.status}")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# global messages
|
43
|
+
on /.*hello everyone.*/, global: true do
|
44
|
+
reply('hello!')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
MyCompanyBot.start!
|
49
|
+
```
|
50
|
+
|
51
|
+
Run hipbot as daemon by saying:
|
52
|
+
|
53
|
+
```
|
54
|
+
hipbot start
|
55
|
+
```
|
56
|
+
|
57
|
+
Run `hipbot` to see all available commands.
|
58
|
+
|
59
|
+
## TODO:
|
60
|
+
|
61
|
+
* add support for custom response helpers
|
62
|
+
* error handling, reconnecting
|
data/Rakefile
ADDED
data/bin/hipbot
ADDED
data/bot.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require_relative './lib/hipbot'
|
2
|
+
|
3
|
+
class MyBot < Hipbot::Bot
|
4
|
+
configure do |c|
|
5
|
+
c.jid = ENV['HIPBOT_JID']
|
6
|
+
c.password = ENV['HIPBOT_PASSWORD']
|
7
|
+
c.name = ENV['HIPBOT_NAME']
|
8
|
+
end
|
9
|
+
|
10
|
+
# Works for message and response
|
11
|
+
# TODO: Reload existing objects? / Full bot restart
|
12
|
+
on /^reload (.*)/, global: true do |clazz|
|
13
|
+
const = clazz.capitalize.to_sym
|
14
|
+
Hipbot.class_eval do
|
15
|
+
remove_const const if const_defined? const
|
16
|
+
end
|
17
|
+
load("./lib/hipbot/" + clazz + ".rb")
|
18
|
+
reply('Reloaded')
|
19
|
+
end
|
20
|
+
|
21
|
+
on /get (.*)/ do |url|
|
22
|
+
get(url) do |http|
|
23
|
+
reply("Server responded with code: #{http.response_header.status}")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
on /^wiki (.+)/, global: true do |search|
|
28
|
+
search = URI::encode search
|
29
|
+
get("http://en.wikipedia.org/w/api.php?action=query&format=json&titles=#{search}&prop=extracts&exlimit=3&explaintext=1&exsentences=2") do |http|
|
30
|
+
extracts = JSON.parse http.response.force_encoding 'utf-8'
|
31
|
+
extracts['query']['pages'].each do |page|
|
32
|
+
reply(page[1]['extract'])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
on /^google (.+)/, global: true do |search|
|
38
|
+
search = URI::encode search
|
39
|
+
get("http://ajax.googleapis.com/ajax/services/search/web?v=1.0&safe=off&q=#{search}") do |http|
|
40
|
+
extracts = JSON.parse http.response.force_encoding 'utf-8'
|
41
|
+
extracts['responseData']['results'].each do |page|
|
42
|
+
reply("#{page['url']} - #{page['titleNoFormatting']}")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
on /weather\s(.*)/ do |city|
|
48
|
+
reply("checking weather for #{city}")
|
49
|
+
conditions = ::GoogleWeather.new(city).forecast_conditions.first
|
50
|
+
reply("weather in #{city} - #{conditions.condition}, max #{conditions.high}F")
|
51
|
+
end
|
52
|
+
|
53
|
+
on /^hello.*/ do
|
54
|
+
reply('hello!')
|
55
|
+
end
|
56
|
+
|
57
|
+
on /deploy to (.*) pls/ do |stage|
|
58
|
+
reply("deploying #{room} to #{stage}!")
|
59
|
+
sleep(3)
|
60
|
+
reply("deployed!")
|
61
|
+
end
|
62
|
+
|
63
|
+
on /.*hello everyone.*/, global: true do
|
64
|
+
reply('hello!')
|
65
|
+
end
|
66
|
+
|
67
|
+
on /sleep/, global: true do
|
68
|
+
reply("sleeping...")
|
69
|
+
sleep(10)
|
70
|
+
reply("woke up")
|
71
|
+
end
|
72
|
+
|
73
|
+
on /help|commands/ do
|
74
|
+
commands = []
|
75
|
+
bot.reactions.each do |r|
|
76
|
+
commands << r.regexp
|
77
|
+
end
|
78
|
+
reply(commands.join("\n"))
|
79
|
+
end
|
80
|
+
|
81
|
+
on /^rand (.*)/, global: true do |message|
|
82
|
+
options = message.split(',').map { |s| s.strip }
|
83
|
+
rsp = options[rand(options.length)]
|
84
|
+
reply("And the winner is... #{rsp}") if rsp
|
85
|
+
end
|
86
|
+
|
87
|
+
on /^debug/, global: true do
|
88
|
+
reply "Debug " + (Jabber::debug = !Jabber::debug ? 'on' : 'off')
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
MyBot.start!
|
data/hipbot.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/hipbot/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Tomasz Pewiński"]
|
6
|
+
gem.email = ["pewniak747@gmail.com"]
|
7
|
+
gem.description = "Hipbot is a bot for HipChat, written in ruby & eventmachine."
|
8
|
+
gem.summary = "Hipbot is a bot for HipChat, written in ruby & eventmachine."
|
9
|
+
gem.homepage = "http://github.com/pewniak747/hipbot"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "hipbot"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Hipbot::VERSION
|
17
|
+
end
|
data/lib/hipbot.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'em-http-request'
|
4
|
+
require 'xmpp4r'
|
5
|
+
require 'xmpp4r/muc'
|
6
|
+
require 'hipbot/mucclient'
|
7
|
+
|
8
|
+
require 'hipbot/adapters/hipchat'
|
9
|
+
require 'hipbot/adapters/telnet'
|
10
|
+
require 'hipbot/bot'
|
11
|
+
require 'hipbot/configuration'
|
12
|
+
require 'hipbot/message'
|
13
|
+
require 'hipbot/reaction'
|
14
|
+
require 'hipbot/response'
|
15
|
+
require 'hipbot/room'
|
16
|
+
require 'hipbot/version'
|
17
|
+
|
18
|
+
# Plugins
|
19
|
+
require 'google_weather'
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Hipbot
|
2
|
+
module Adapters
|
3
|
+
module Hipchat
|
4
|
+
delegate :reply, to: :connection
|
5
|
+
|
6
|
+
class Connection
|
7
|
+
|
8
|
+
def initialize bot
|
9
|
+
initialize_bot(bot)
|
10
|
+
initialize_jabber
|
11
|
+
initialize_rooms
|
12
|
+
join_rooms
|
13
|
+
end
|
14
|
+
|
15
|
+
def reply room, message
|
16
|
+
for_room room do |room|
|
17
|
+
puts("Replied to #{room.name} - #{message}")
|
18
|
+
send_message(room, message)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def initialize_rooms
|
25
|
+
@rooms ||= []
|
26
|
+
@muc_browser = Jabber::MUC::MUCBrowser.new(@jabber)
|
27
|
+
@rooms = @muc_browser.muc_rooms('conf.hipchat.com').map { |jid, name|
|
28
|
+
::Hipbot::Room.new(jid, name)
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize_bot bot
|
33
|
+
@bot = bot
|
34
|
+
@bot.connection = self
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize_jabber
|
38
|
+
@jabber = ::Jabber::Client.new(@bot.jid)
|
39
|
+
@jabber.connect
|
40
|
+
@jabber.auth(@bot.password)
|
41
|
+
end
|
42
|
+
|
43
|
+
def join_rooms
|
44
|
+
rooms.each do |room|
|
45
|
+
puts "joining #{room.name}"
|
46
|
+
room.connection = ::Jabber::MUC::SimpleMUCClient.new(@jabber)
|
47
|
+
room.connection.on_message do |time, sender, message|
|
48
|
+
puts "#{Time.now} <#{sender}> #{message}"
|
49
|
+
@bot.tell(sender, room.name, message)
|
50
|
+
end
|
51
|
+
room.connection.join("#{room.jid}/#{@bot.name}", nil, :history => false)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def for_room room_name
|
56
|
+
room = rooms.find { |r| r.name == room_name }
|
57
|
+
if room.present?
|
58
|
+
yield(room) if block_given?
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def send_message room, message
|
63
|
+
room.connection.say(message)
|
64
|
+
end
|
65
|
+
|
66
|
+
def rooms
|
67
|
+
@rooms || []
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def start!
|
72
|
+
::EM::run do
|
73
|
+
Connection.new(self)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Hipbot
|
2
|
+
module Adapters
|
3
|
+
module Telnet
|
4
|
+
def reply room, message
|
5
|
+
connection.send_data("#{self}:#{room}:#{message}\n")
|
6
|
+
end
|
7
|
+
|
8
|
+
class Connection < EM::Connection
|
9
|
+
def initialize bot
|
10
|
+
@bot = bot
|
11
|
+
@bot.connection = self
|
12
|
+
end
|
13
|
+
|
14
|
+
def receive_data(data)
|
15
|
+
sender, room, message = *data.strip.split(':')
|
16
|
+
@bot.tell(sender, room, message)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def start!
|
21
|
+
::EM::run do
|
22
|
+
::EM::connect('0.0.0.0', 3001, Connection, self)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
data/lib/hipbot/bot.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
module Hipbot
|
2
|
+
class Bot
|
3
|
+
attr_accessor :reactions, :configuration, :connection
|
4
|
+
CONFIGURABLE_OPTIONS = [:name, :jid, :password, :adapter]
|
5
|
+
delegate *CONFIGURABLE_OPTIONS, to: :configuration
|
6
|
+
alias_method :to_s, :name
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
self.configuration = Configuration.new.tap(&self.class.configuration)
|
10
|
+
self.reactions = []
|
11
|
+
self.class.reactions.each do |opts|
|
12
|
+
on(opts[0], opts[1], &opts[-1])
|
13
|
+
end
|
14
|
+
extend(self.adapter || ::Hipbot::Adapters::Hipchat)
|
15
|
+
end
|
16
|
+
|
17
|
+
def on regexp, options={}, &block
|
18
|
+
self.reactions << Reaction.new(self, regexp, options, block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def tell sender, room, message
|
22
|
+
return if sender == name
|
23
|
+
matches = matching_reactions(sender, room, message)
|
24
|
+
if matches.size > 0
|
25
|
+
matches[0].invoke(sender, room, message)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def reactions_list
|
30
|
+
self.reactions.regexp
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
|
35
|
+
def on regexp, options={}, &block
|
36
|
+
@reactions ||= []
|
37
|
+
@reactions << [regexp, options, block]
|
38
|
+
end
|
39
|
+
|
40
|
+
def configure &block
|
41
|
+
@configuration = block
|
42
|
+
end
|
43
|
+
|
44
|
+
def reactions
|
45
|
+
@reactions || []
|
46
|
+
end
|
47
|
+
|
48
|
+
def configuration
|
49
|
+
@configuration || Proc.new{}
|
50
|
+
end
|
51
|
+
|
52
|
+
def start!
|
53
|
+
new.start!
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def matching_reactions sender, room, message
|
61
|
+
all_reactions = reactions + [default_reaction]
|
62
|
+
all_reactions.select { |r| r.match?(sender, room, message) }
|
63
|
+
end
|
64
|
+
|
65
|
+
def default_reaction
|
66
|
+
@default_reaction ||= Reaction.new(self, /.*/, {}, Proc.new {
|
67
|
+
reply("I don't understand \"#{message}\"")
|
68
|
+
})
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Hipbot
|
2
|
+
class Message
|
3
|
+
attr_accessor :body, :sender, :raw_body
|
4
|
+
def initialize body, sender
|
5
|
+
self.raw_body = body
|
6
|
+
self.body = strip_recipient(body)
|
7
|
+
self.sender = sender
|
8
|
+
end
|
9
|
+
|
10
|
+
def recipients
|
11
|
+
results = raw_body.scan(/@(\w+)/) + raw_body.scan(/@"(.*)"/)
|
12
|
+
results.flatten
|
13
|
+
end
|
14
|
+
|
15
|
+
def for? recipient
|
16
|
+
recipients.include? recipient.to_s.gsub(/\s+/, '')
|
17
|
+
end
|
18
|
+
|
19
|
+
def strip_recipient body
|
20
|
+
body.gsub(/^@\w+\W*/, '')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Jabber
|
2
|
+
module MUC
|
3
|
+
class MUCClient
|
4
|
+
def join(jid, password=nil, opts={})
|
5
|
+
if active?
|
6
|
+
raise "MUCClient already active"
|
7
|
+
end
|
8
|
+
|
9
|
+
@jid = (jid.kind_of?(JID) ? jid : JID.new(jid))
|
10
|
+
activate
|
11
|
+
|
12
|
+
# Joining
|
13
|
+
pres = Presence.new
|
14
|
+
pres.to = @jid
|
15
|
+
pres.from = @my_jid
|
16
|
+
xmuc = XMUC.new
|
17
|
+
xmuc.password = password
|
18
|
+
|
19
|
+
if !opts[:history]
|
20
|
+
history = REXML::Element.new( 'history').tap {|h| h.add_attribute('maxstanzas','0') }
|
21
|
+
xmuc.add_element history
|
22
|
+
end
|
23
|
+
|
24
|
+
pres.add(xmuc)
|
25
|
+
|
26
|
+
# We don't use Stream#send_with_id here as it's unknown
|
27
|
+
# if the MUC component *always* uses our stanza id.
|
28
|
+
error = nil
|
29
|
+
@stream.send(pres) { |r|
|
30
|
+
if from_room?(r.from) and r.kind_of?(Presence) and r.type == :error
|
31
|
+
# Error from room
|
32
|
+
error = r.error
|
33
|
+
true
|
34
|
+
# type='unavailable' may occur when the MUC kills our previous instance,
|
35
|
+
# but all join-failures should be type='error'
|
36
|
+
elsif r.from == jid and r.kind_of?(Presence) and r.type != :unavailable
|
37
|
+
# Our own presence reflected back - success
|
38
|
+
if r.x(XMUCUser) and (i = r.x(XMUCUser).items.first)
|
39
|
+
@affiliation = i.affiliation # we're interested in if it's :owner
|
40
|
+
@role = i.role # :moderator ?
|
41
|
+
end
|
42
|
+
|
43
|
+
handle_presence(r, false)
|
44
|
+
true
|
45
|
+
else
|
46
|
+
# Everything else
|
47
|
+
false
|
48
|
+
end
|
49
|
+
}
|
50
|
+
|
51
|
+
if error
|
52
|
+
deactivate
|
53
|
+
raise ServerError.new(error)
|
54
|
+
end
|
55
|
+
|
56
|
+
self
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Hipbot
|
2
|
+
class Reaction < Struct.new(:robot, :regexp, :options, :block)
|
3
|
+
|
4
|
+
def invoke sender, room, message
|
5
|
+
message = message_for(message, sender)
|
6
|
+
arguments = arguments_for(message)
|
7
|
+
Response.new(robot, self, room, message).invoke(arguments)
|
8
|
+
end
|
9
|
+
|
10
|
+
def match? sender, room, message
|
11
|
+
message = message_for(message, sender)
|
12
|
+
matches?(message) && matches_scope?(message) && matches_sender?(message)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def message_for message, sender
|
18
|
+
Message.new(message, sender)
|
19
|
+
end
|
20
|
+
|
21
|
+
def arguments_for message
|
22
|
+
message.body.match(regexp)[1..-1]
|
23
|
+
end
|
24
|
+
|
25
|
+
def matches?(message)
|
26
|
+
regexp =~ message.body
|
27
|
+
end
|
28
|
+
|
29
|
+
def matches_scope?(message)
|
30
|
+
global? || message.for?(robot)
|
31
|
+
end
|
32
|
+
|
33
|
+
def matches_sender?(message)
|
34
|
+
from_all? || Array(options[:from]).include?(message.sender)
|
35
|
+
end
|
36
|
+
|
37
|
+
def global?
|
38
|
+
options[:global]
|
39
|
+
end
|
40
|
+
|
41
|
+
def from_all?
|
42
|
+
!options[:from]
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Hipbot
|
2
|
+
class Response < Struct.new(:bot, :reaction, :room, :message_object)
|
3
|
+
|
4
|
+
def invoke arguments
|
5
|
+
instance_exec(*arguments, &reaction.block)
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
def reply string
|
10
|
+
bot.reply(room, string)
|
11
|
+
end
|
12
|
+
|
13
|
+
[:get, :post, :put, :delete].each do |http_verb|
|
14
|
+
define_method http_verb do |url, query={}, &block|
|
15
|
+
http = ::EM::HttpRequest.new(url).send(http_verb, :query => query)
|
16
|
+
http.callback { block.call(http) if block }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def first_name
|
21
|
+
message_object.sender.split(' ').first
|
22
|
+
end
|
23
|
+
|
24
|
+
def message
|
25
|
+
message_object.body
|
26
|
+
end
|
27
|
+
|
28
|
+
def sender
|
29
|
+
message_object.sender
|
30
|
+
end
|
31
|
+
|
32
|
+
def recipients
|
33
|
+
message_object.recipients
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/hipbot/room.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class MyHipbot < Hipbot::Bot
|
4
|
+
configure do |config|
|
5
|
+
config.name = 'robbot'
|
6
|
+
config.jid = 'robbot@chat.hipchat.com'
|
7
|
+
end
|
8
|
+
|
9
|
+
on /^hello hipbot!$/ do
|
10
|
+
reply("hello!")
|
11
|
+
end
|
12
|
+
on /you're (.*), robot/ do |adj|
|
13
|
+
reply("I know I'm #{adj}")
|
14
|
+
end
|
15
|
+
on /hi everyone!/, global: true do
|
16
|
+
reply('hello!')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe MyHipbot do
|
21
|
+
describe "configuration" do
|
22
|
+
it "should set robot name" do
|
23
|
+
subject.name.should == 'robbot'
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should set hipchat token" do
|
27
|
+
subject.jid.should == 'robbot@chat.hipchat.com'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "replying" do
|
32
|
+
# TODO: replace with actual objects
|
33
|
+
let(:room) { stub_everything }
|
34
|
+
let(:sender) { stub_everything }
|
35
|
+
|
36
|
+
it "should reply to hello" do
|
37
|
+
subject.expects(:reply).with(room, 'hello!')
|
38
|
+
subject.tell(sender, room, '@robbot hello hipbot!')
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should reply with argument" do
|
42
|
+
subject.expects(:reply).with(room, "I know I'm cool")
|
43
|
+
subject.tell(sender, room, "@robbot you're cool, robot")
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should reply to global message" do
|
47
|
+
subject.expects(:reply).with(room, "hello!")
|
48
|
+
subject.tell(sender, room, "hi everyone!")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Hipbot::Bot do
|
4
|
+
context "#on" do
|
5
|
+
let(:sender) { stub_everything }
|
6
|
+
let(:room) { stub_everything }
|
7
|
+
|
8
|
+
it "should reply to no arguments" do
|
9
|
+
subject.on /^hello there$/ do
|
10
|
+
reply('hi!')
|
11
|
+
end
|
12
|
+
subject.expects(:reply).with(room, 'hi!')
|
13
|
+
subject.tell(sender, room, '@robot hello there')
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should reply with one argument" do
|
17
|
+
subject.on /^you are (.*), robot$/ do |adj|
|
18
|
+
reply("i know i'm #{adj}!")
|
19
|
+
end
|
20
|
+
subject.expects(:reply).with(room, "i know i'm cool!")
|
21
|
+
subject.tell(sender, room, '@robot you are cool, robot')
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should reply with multiple arguments" do
|
25
|
+
subject.on /^send "(.*)" to (.*@.*)$/ do |message, recipient|
|
26
|
+
reply("sent \"#{message}\" to #{recipient}")
|
27
|
+
end
|
28
|
+
subject.expects(:reply).with(room, 'sent "hello!" to robot@robots.org')
|
29
|
+
subject.tell(sender, room, '@robot send "hello!" to robot@robots.org')
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should say when does not understand" do
|
33
|
+
subject.on /^hello there$/ do
|
34
|
+
reply('hi!')
|
35
|
+
end
|
36
|
+
subject.expects(:reply).with(room, 'I don\'t understand "hello robot!"')
|
37
|
+
subject.tell(sender, room, '@robot hello robot!')
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should choose first option when multiple options match" do
|
41
|
+
subject.on /hello there/ do reply('hello there') end
|
42
|
+
subject.on /hello (.*)/ do reply('hello') end
|
43
|
+
subject.expects(:reply).with(room, 'hello there')
|
44
|
+
subject.tell(sender, room, '@robot hello there')
|
45
|
+
end
|
46
|
+
|
47
|
+
context "global messages" do
|
48
|
+
it "should reply if callback is global" do
|
49
|
+
subject.on /^you are (.*)$/, global: true do |adj|
|
50
|
+
reply("i know i'm #{adj}!")
|
51
|
+
end
|
52
|
+
subject.expects(:reply).with(room, "i know i'm cool!")
|
53
|
+
subject.tell(sender, room, 'you are cool')
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should not reply if callback not global" do
|
57
|
+
subject.on /^you are (.*)$/ do |adj|
|
58
|
+
reply("i know i'm #{adj}!")
|
59
|
+
end
|
60
|
+
subject.expects(:reply).never
|
61
|
+
subject.tell(sender, room, 'you are cool')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context "messages from particular sender" do
|
66
|
+
it "should reply" do
|
67
|
+
subject.on /wazzup\?/, from: sender do
|
68
|
+
reply('Wazzup, Tom?')
|
69
|
+
end
|
70
|
+
subject.expects(:reply).with(room, 'Wazzup, Tom?')
|
71
|
+
subject.tell(sender, room, '@robot wazzup?')
|
72
|
+
end
|
73
|
+
it "should reply if sender acceptable" do
|
74
|
+
subject.on /wazzup\?/, from: [stub, sender] do
|
75
|
+
reply('wazzup, tom?')
|
76
|
+
end
|
77
|
+
subject.expects(:reply).with(room, 'wazzup, tom?')
|
78
|
+
subject.tell(sender, room, '@robot wazzup?')
|
79
|
+
end
|
80
|
+
it "should not reply if sender unacceptable" do
|
81
|
+
subject.on /wazzup\?/, from: sender do
|
82
|
+
reply('wazzup, tom?')
|
83
|
+
end
|
84
|
+
subject.expects(:reply).with(room, "I don't understand \"wazzup?\"")
|
85
|
+
subject.tell(stub, room, '@robot wazzup?')
|
86
|
+
end
|
87
|
+
it "should not reply if sender does not match" do
|
88
|
+
subject.on /wazzup\?/, from: [sender] do
|
89
|
+
reply('wazzup, tom?')
|
90
|
+
end
|
91
|
+
subject.expects(:reply).with(room, "I don't understand \"wazzup?\"")
|
92
|
+
subject.tell(stub, room, '@robot wazzup?')
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context "default variables" do
|
97
|
+
it "message" do
|
98
|
+
subject.on /.*/ do
|
99
|
+
reply("you said: #{message}")
|
100
|
+
end
|
101
|
+
subject.expects(:reply).with(room, "you said: hello")
|
102
|
+
subject.tell(stub, room, "@robot hello")
|
103
|
+
end
|
104
|
+
|
105
|
+
it "sender" do
|
106
|
+
subject.on /.*/ do
|
107
|
+
reply("you are: #{sender}")
|
108
|
+
end
|
109
|
+
subject.expects(:reply).with(room, "you are: tom")
|
110
|
+
subject.tell('tom', room, "@robot hello")
|
111
|
+
end
|
112
|
+
|
113
|
+
it "recipients" do
|
114
|
+
subject.on /.*/ do
|
115
|
+
reply("recipients: #{recipients.join(', ')}")
|
116
|
+
end
|
117
|
+
subject.expects(:reply).with(room, "recipients: robot, dave")
|
118
|
+
subject.tell('tom', room, "@robot tell @dave hello from me")
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
describe "configurable options" do
|
124
|
+
Hipbot::Bot::CONFIGURABLE_OPTIONS.each do |option|
|
125
|
+
it "should delegate #{option} to configuration" do
|
126
|
+
value = stub
|
127
|
+
subject.configuration.expects(option).returns(value)
|
128
|
+
subject.send(option)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Hipbot::Message do
|
4
|
+
subject { Hipbot::Message }
|
5
|
+
let(:sender) { stub }
|
6
|
+
|
7
|
+
it "should have a body" do
|
8
|
+
message = subject.new('this is a message', sender)
|
9
|
+
message.body.should == 'this is a message'
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should have a sender" do
|
13
|
+
message = subject.new('this is a message', sender)
|
14
|
+
message.sender.should == sender
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should have no recipients" do
|
18
|
+
message = subject.new('this is a message', sender)
|
19
|
+
message.recipients.should be_empty
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should have one recipient" do
|
23
|
+
message = subject.new('this is a message for @tom', sender)
|
24
|
+
message.recipients.should include('tom')
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should have one long recipient" do
|
28
|
+
message = subject.new('message for @"tom jones", deal with it', sender)
|
29
|
+
message.recipients.should include('tom jones')
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should have two recipients" do
|
33
|
+
message = subject.new('@dave, this is a message for @tom', sender)
|
34
|
+
message.recipients.should include('tom')
|
35
|
+
message.recipients.should include('dave')
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should strip primary recipient from message" do
|
39
|
+
message = subject.new('@dave this is a message for @tom', sender)
|
40
|
+
message.body.should == 'this is a message for @tom'
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should strip primary recipient from message with commma" do
|
44
|
+
message = subject.new('@dave, this is a message for @tom', sender)
|
45
|
+
message.body.should == 'this is a message for @tom'
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should be for bot" do
|
49
|
+
bot = stub(:to_s => 'robot')
|
50
|
+
message = subject.new('hello @robot!', sender)
|
51
|
+
message.for?(bot).should be_true
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should not be for bot" do
|
55
|
+
bot = stub(:to_s => 'robot')
|
56
|
+
message = subject.new('hello @tom!', sender)
|
57
|
+
message.for?(bot).should be_false
|
58
|
+
end
|
59
|
+
end
|
data/test_server.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
$connections = []
|
4
|
+
|
5
|
+
class ChatServer < EM::Connection
|
6
|
+
def post_init
|
7
|
+
$connections << self
|
8
|
+
end
|
9
|
+
|
10
|
+
def receive_data data
|
11
|
+
messages = data.split("\n").map{ |m| "#{m}\n" }.each do |msg|
|
12
|
+
sender, room, message = *msg.strip.split(':')
|
13
|
+
$connections.reject{|c| c==self}.each do |connection|
|
14
|
+
connection.send_data msg
|
15
|
+
end
|
16
|
+
puts "#{sender}@#{room} > #{message}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
EM::run do
|
22
|
+
EM::start_server('0.0.0.0', 3001, ChatServer)
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hipbot
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Tomasz Pewiński
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-07-20 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Hipbot is a bot for HipChat, written in ruby & eventmachine.
|
15
|
+
email:
|
16
|
+
- pewniak747@gmail.com
|
17
|
+
executables:
|
18
|
+
- hipbot
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- .gitignore
|
23
|
+
- .travis.yml
|
24
|
+
- Gemfile
|
25
|
+
- Gemfile.lock
|
26
|
+
- LICENSE
|
27
|
+
- README.md
|
28
|
+
- Rakefile
|
29
|
+
- bin/hipbot
|
30
|
+
- bot.rb
|
31
|
+
- hipbot.gemspec
|
32
|
+
- lib/hipbot.rb
|
33
|
+
- lib/hipbot/adapters/hipchat.rb
|
34
|
+
- lib/hipbot/adapters/telnet.rb
|
35
|
+
- lib/hipbot/bot.rb
|
36
|
+
- lib/hipbot/configuration.rb
|
37
|
+
- lib/hipbot/message.rb
|
38
|
+
- lib/hipbot/mucclient.rb
|
39
|
+
- lib/hipbot/reaction.rb
|
40
|
+
- lib/hipbot/response.rb
|
41
|
+
- lib/hipbot/room.rb
|
42
|
+
- lib/hipbot/version.rb
|
43
|
+
- spec/integration/hipbot_spec.rb
|
44
|
+
- spec/spec_helper.rb
|
45
|
+
- spec/unit/hipbot_spec.rb
|
46
|
+
- spec/unit/message_spec.rb
|
47
|
+
- test_server.rb
|
48
|
+
homepage: http://github.com/pewniak747/hipbot
|
49
|
+
licenses: []
|
50
|
+
post_install_message:
|
51
|
+
rdoc_options: []
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ! '>='
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
requirements: []
|
67
|
+
rubyforge_project:
|
68
|
+
rubygems_version: 1.8.23
|
69
|
+
signing_key:
|
70
|
+
specification_version: 3
|
71
|
+
summary: Hipbot is a bot for HipChat, written in ruby & eventmachine.
|
72
|
+
test_files:
|
73
|
+
- spec/integration/hipbot_spec.rb
|
74
|
+
- spec/spec_helper.rb
|
75
|
+
- spec/unit/hipbot_spec.rb
|
76
|
+
- spec/unit/message_spec.rb
|
77
|
+
has_rdoc:
|