hipbot 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://secure.travis-ci.org/pewniak747/hipbot.png?branch=master)](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:
|