hipbot 0.0.3 → 0.0.5
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/.travis.yml +6 -0
- data/Gemfile +0 -3
- data/Gemfile.lock +7 -10
- data/README.md +129 -21
- data/bin/hipbot +8 -1
- data/hipbot.gemspec +2 -1
- data/lib/hipbot.rb +8 -7
- data/lib/hipbot/adapters/hipchat/connection.rb +123 -0
- data/lib/hipbot/adapters/hipchat/hipchat.rb +20 -0
- data/lib/hipbot/adapters/{telnet.rb → telnet/connection.rb} +0 -10
- data/lib/hipbot/adapters/telnet/telnet.rb +16 -0
- data/lib/hipbot/bot.rb +10 -9
- data/lib/hipbot/configuration.rb +3 -0
- data/lib/hipbot/http_response.rb +15 -0
- data/lib/hipbot/message.rb +9 -1
- data/lib/hipbot/{encoding.rb → patches/encoding.rb} +0 -0
- data/lib/hipbot/patches/mucclient.rb +147 -0
- data/lib/hipbot/reaction.rb +14 -6
- data/lib/hipbot/response.rb +10 -18
- data/lib/hipbot/room.rb +2 -5
- data/lib/hipbot/version.rb +1 -1
- data/spec/integration/hipbot_spec.rb +33 -4
- data/spec/unit/hipbot_spec.rb +75 -3
- metadata +29 -12
- data/bot.rb +0 -92
- data/lib/hipbot/adapters/hipchat.rb +0 -86
- data/lib/hipbot/mucclient.rb +0 -60
- data/test_server.rb +0 -23
@@ -0,0 +1,16 @@
|
|
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
|
+
def start!
|
9
|
+
::EM::run do
|
10
|
+
::EM::connect('0.0.0.0', 3001, Connection, self)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
data/lib/hipbot/bot.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Hipbot
|
2
2
|
class Bot
|
3
3
|
attr_accessor :reactions, :configuration, :connection
|
4
|
-
CONFIGURABLE_OPTIONS = [:name, :jid, :password, :adapter]
|
4
|
+
CONFIGURABLE_OPTIONS = [:name, :jid, :password, :adapter, :helpers]
|
5
5
|
delegate *CONFIGURABLE_OPTIONS, to: :configuration
|
6
6
|
alias_method :to_s, :name
|
7
7
|
|
@@ -9,20 +9,21 @@ module Hipbot
|
|
9
9
|
self.configuration = Configuration.new.tap(&self.class.configuration)
|
10
10
|
self.reactions = []
|
11
11
|
self.class.reactions.each do |opts|
|
12
|
-
on(opts[0],
|
12
|
+
on(*opts[0], &opts[-1])
|
13
13
|
end
|
14
14
|
extend(self.adapter || ::Hipbot::Adapters::Hipchat)
|
15
15
|
end
|
16
16
|
|
17
|
-
def on
|
18
|
-
|
17
|
+
def on *regexps, &block
|
18
|
+
options = regexps[-1].kind_of?(Hash) ? regexps.pop : {}
|
19
|
+
self.reactions << Reaction.new(self, regexps, options, block)
|
19
20
|
end
|
20
21
|
|
21
22
|
def tell sender, room, message
|
22
23
|
return if sender == name
|
23
24
|
matches = matching_reactions(sender, room, message)
|
24
25
|
if matches.size > 0
|
25
|
-
matches
|
26
|
+
matches.first.invoke(sender, room, message)
|
26
27
|
end
|
27
28
|
end
|
28
29
|
|
@@ -32,9 +33,9 @@ module Hipbot
|
|
32
33
|
|
33
34
|
class << self
|
34
35
|
|
35
|
-
def on
|
36
|
+
def on *regexps, &block
|
36
37
|
@reactions ||= []
|
37
|
-
@reactions << [
|
38
|
+
@reactions << [regexps, block]
|
38
39
|
end
|
39
40
|
|
40
41
|
def configure &block
|
@@ -63,8 +64,8 @@ module Hipbot
|
|
63
64
|
end
|
64
65
|
|
65
66
|
def default_reaction
|
66
|
-
@default_reaction ||= Reaction.new(self,
|
67
|
-
reply("I don't understand \"#{message}\"")
|
67
|
+
@default_reaction ||= Reaction.new(self, [/.*/], {}, Proc.new {
|
68
|
+
reply("I don't understand \"#{message.body}\"")
|
68
69
|
})
|
69
70
|
end
|
70
71
|
|
data/lib/hipbot/configuration.rb
CHANGED
data/lib/hipbot/message.rb
CHANGED
@@ -9,7 +9,7 @@ module Hipbot
|
|
9
9
|
|
10
10
|
def recipients
|
11
11
|
results = raw_body.scan(/@(\w+)/) + raw_body.scan(/@"(.*)"/)
|
12
|
-
results.flatten
|
12
|
+
results.flatten.uniq
|
13
13
|
end
|
14
14
|
|
15
15
|
def for? recipient
|
@@ -19,5 +19,13 @@ module Hipbot
|
|
19
19
|
def strip_recipient body
|
20
20
|
body.gsub(/^@\w+\W*/, '')
|
21
21
|
end
|
22
|
+
|
23
|
+
def sender_name
|
24
|
+
sender.split.first
|
25
|
+
end
|
26
|
+
|
27
|
+
def mentions
|
28
|
+
recipients[1..-1]
|
29
|
+
end
|
22
30
|
end
|
23
31
|
end
|
File without changes
|
@@ -0,0 +1,147 @@
|
|
1
|
+
module Jabber
|
2
|
+
module MUC
|
3
|
+
|
4
|
+
class MUCClient
|
5
|
+
|
6
|
+
def join(jid, password=nil, opts={})
|
7
|
+
if active?
|
8
|
+
raise "MUCClient already active"
|
9
|
+
end
|
10
|
+
|
11
|
+
@jid = (jid.kind_of?(JID) ? jid : JID.new(jid))
|
12
|
+
activate
|
13
|
+
|
14
|
+
# Joining
|
15
|
+
pres = Presence.new
|
16
|
+
pres.to = @jid
|
17
|
+
pres.from = @my_jid
|
18
|
+
xmuc = XMUC.new
|
19
|
+
xmuc.password = password
|
20
|
+
|
21
|
+
if !opts[:history]
|
22
|
+
history = REXML::Element.new( 'history').tap {|h| h.add_attribute('maxstanzas','0') }
|
23
|
+
xmuc.add_element history
|
24
|
+
end
|
25
|
+
|
26
|
+
pres.add(xmuc)
|
27
|
+
|
28
|
+
# We don't use Stream#send_with_id here as it's unknown
|
29
|
+
# if the MUC component *always* uses our stanza id.
|
30
|
+
error = nil
|
31
|
+
@stream.send(pres) { |r|
|
32
|
+
if from_room?(r.from) and r.kind_of?(Presence) and r.type == :error
|
33
|
+
# Error from room
|
34
|
+
error = r.error
|
35
|
+
true
|
36
|
+
# type='unavailable' may occur when the MUC kills our previous instance,
|
37
|
+
# but all join-failures should be type='error'
|
38
|
+
elsif r.from == jid and r.kind_of?(Presence) and r.type != :unavailable
|
39
|
+
# Our own presence reflected back - success
|
40
|
+
if r.x(XMUCUser) and (i = r.x(XMUCUser).items.first)
|
41
|
+
@affiliation = i.affiliation # we're interested in if it's :owner
|
42
|
+
@role = i.role # :moderator ?
|
43
|
+
end
|
44
|
+
|
45
|
+
handle_presence(r, false)
|
46
|
+
true
|
47
|
+
else
|
48
|
+
# Everything else
|
49
|
+
false
|
50
|
+
end
|
51
|
+
}
|
52
|
+
|
53
|
+
if error
|
54
|
+
deactivate
|
55
|
+
raise ServerError.new(error)
|
56
|
+
end
|
57
|
+
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
def activate # :nodoc:
|
63
|
+
@active = true
|
64
|
+
|
65
|
+
# Callbacks
|
66
|
+
@stream.add_presence_callback(150, self) { |presence|
|
67
|
+
if from_room?(presence.from)
|
68
|
+
handle_presence(presence)
|
69
|
+
true
|
70
|
+
else
|
71
|
+
false
|
72
|
+
end
|
73
|
+
}
|
74
|
+
|
75
|
+
@stream.add_message_callback(150, self) { |message|
|
76
|
+
# Not sure if this was hipchat or client bug,
|
77
|
+
# but this callback didn't allow chat (private) messages since
|
78
|
+
# they don't belong to any conference room
|
79
|
+
if from_room?(message.from) || is_chat?(message.type)
|
80
|
+
handle_message(message)
|
81
|
+
true
|
82
|
+
else
|
83
|
+
false
|
84
|
+
end
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
def is_chat?(type)
|
89
|
+
type == :chat
|
90
|
+
end
|
91
|
+
|
92
|
+
def send(stanza, to = nil)
|
93
|
+
if stanza.kind_of? Message
|
94
|
+
stanza.type = to || stanza.to ? :chat : :groupchat
|
95
|
+
end
|
96
|
+
stanza.from = @my_jid
|
97
|
+
# We don't want to override existing message JID
|
98
|
+
stanza.to = JID.new(jid.node, jid.domain, to) if stanza.to.nil?
|
99
|
+
@stream.send(stanza)
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
class SimpleMUCClient < MUCClient
|
105
|
+
|
106
|
+
def say(text, jid = nil)
|
107
|
+
send(Message.new(jid, text))
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def handle_message(msg)
|
113
|
+
super
|
114
|
+
|
115
|
+
time = Time.now # Hipchat doesn't provide time stamp for message elements
|
116
|
+
msg.each_element('x') { |x|
|
117
|
+
if x.kind_of?(Delay::XDelay)
|
118
|
+
time = x.stamp
|
119
|
+
end
|
120
|
+
}
|
121
|
+
sender_nick = msg.from.resource
|
122
|
+
|
123
|
+
|
124
|
+
if msg.subject
|
125
|
+
@subject = msg.subject
|
126
|
+
@subject_block.call(time, sender_nick, @subject) if @subject_block
|
127
|
+
end
|
128
|
+
|
129
|
+
if msg.body
|
130
|
+
if sender_nick.nil?
|
131
|
+
@room_message_block.call(time, msg.body) if @room_message_block
|
132
|
+
else
|
133
|
+
if msg.type == :chat
|
134
|
+
# We need to send full jid here (msg.from)
|
135
|
+
@private_message_block.call(time, msg.from, msg.body) if @private_message_block
|
136
|
+
elsif msg.type == :groupchat
|
137
|
+
@message_block.call(time, msg.from.resource, msg.body) if @message_block
|
138
|
+
else
|
139
|
+
# ...?
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
data/lib/hipbot/reaction.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module Hipbot
|
2
|
-
class Reaction < Struct.new(:robot, :
|
2
|
+
class Reaction < Struct.new(:robot, :regexps, :options, :block)
|
3
3
|
|
4
4
|
def invoke sender, room, message
|
5
5
|
message = message_for(message, sender)
|
@@ -9,21 +9,26 @@ module Hipbot
|
|
9
9
|
|
10
10
|
def match? sender, room, message
|
11
11
|
message = message_for(message, sender)
|
12
|
-
|
12
|
+
matches_regexp?(message) && matches_scope?(message) && matches_sender?(message) && matches_room?(room)
|
13
13
|
end
|
14
14
|
|
15
15
|
private
|
16
16
|
|
17
|
+
# TODO: this is pointless since we can get actual message object from xmpp4r
|
17
18
|
def message_for message, sender
|
18
19
|
Message.new(message, sender)
|
19
20
|
end
|
20
21
|
|
21
22
|
def arguments_for message
|
22
|
-
message.body.match(
|
23
|
+
message.body.match(matching_regexp(message))[1..-1]
|
23
24
|
end
|
24
25
|
|
25
|
-
def
|
26
|
-
|
26
|
+
def matches_regexp?(message)
|
27
|
+
matching_regexp(message).present?
|
28
|
+
end
|
29
|
+
|
30
|
+
def matches_room?(room)
|
31
|
+
!options[:room] || Array(options[:room]).include?(room.name)
|
27
32
|
end
|
28
33
|
|
29
34
|
def matches_scope?(message)
|
@@ -34,6 +39,10 @@ module Hipbot
|
|
34
39
|
from_all? || Array(options[:from]).include?(message.sender)
|
35
40
|
end
|
36
41
|
|
42
|
+
def matching_regexp(message)
|
43
|
+
regexps.find { |regexp| regexp =~ message.body }
|
44
|
+
end
|
45
|
+
|
37
46
|
def global?
|
38
47
|
options[:global]
|
39
48
|
end
|
@@ -41,6 +50,5 @@ module Hipbot
|
|
41
50
|
def from_all?
|
42
51
|
!options[:from]
|
43
52
|
end
|
44
|
-
|
45
53
|
end
|
46
54
|
end
|
data/lib/hipbot/response.rb
CHANGED
@@ -1,36 +1,28 @@
|
|
1
1
|
module Hipbot
|
2
|
-
class Response < Struct.new(:bot, :reaction, :room, :
|
2
|
+
class Response < Struct.new(:bot, :reaction, :room, :message)
|
3
|
+
delegate :sender, :recipients, :body, :to => :message
|
4
|
+
|
5
|
+
def initialize bot, reaction, room, message
|
6
|
+
super
|
7
|
+
extend(bot.helpers)
|
8
|
+
end
|
3
9
|
|
4
10
|
def invoke arguments
|
5
11
|
instance_exec(*arguments, &reaction.block)
|
6
12
|
end
|
7
13
|
|
8
14
|
private
|
9
|
-
|
15
|
+
|
16
|
+
def reply string, room = self.room
|
10
17
|
bot.reply(room, string)
|
11
18
|
end
|
12
19
|
|
13
20
|
[:get, :post, :put, :delete].each do |http_verb|
|
14
21
|
define_method http_verb do |url, query={}, &block|
|
15
22
|
http = ::EM::HttpRequest.new(url).send(http_verb, :query => query)
|
16
|
-
http.callback { block.call(http) if block }
|
23
|
+
http.callback { block.call(::Hipbot::HttpResponse.new(http)) if block }
|
17
24
|
end
|
18
25
|
end
|
19
26
|
|
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
27
|
end
|
36
28
|
end
|
data/lib/hipbot/room.rb
CHANGED
data/lib/hipbot/version.rb
CHANGED
@@ -1,9 +1,20 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
+
module HipbotHelpers
|
4
|
+
def project_name
|
5
|
+
"#{room.name} project"
|
6
|
+
end
|
7
|
+
|
8
|
+
def sender_first_name
|
9
|
+
"you are #{message.sender.split[0]}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
3
13
|
class MyHipbot < Hipbot::Bot
|
4
14
|
configure do |config|
|
5
15
|
config.name = 'robbot'
|
6
16
|
config.jid = 'robbot@chat.hipchat.com'
|
17
|
+
config.helpers = HipbotHelpers
|
7
18
|
end
|
8
19
|
|
9
20
|
on /^hello hipbot!$/ do
|
@@ -15,9 +26,19 @@ class MyHipbot < Hipbot::Bot
|
|
15
26
|
on /hi everyone!/, global: true do
|
16
27
|
reply('hello!')
|
17
28
|
end
|
29
|
+
on /tell me the project name/ do
|
30
|
+
reply(project_name)
|
31
|
+
end
|
32
|
+
on /tell me my name/ do
|
33
|
+
reply(sender_first_name)
|
34
|
+
end
|
18
35
|
end
|
19
36
|
|
20
37
|
describe MyHipbot do
|
38
|
+
# TODO: replace with actual objects
|
39
|
+
let(:room) { Hipbot::Room.new('123', 'private') }
|
40
|
+
let(:sender) { 'John Doe' }
|
41
|
+
|
21
42
|
describe "configuration" do
|
22
43
|
it "should set robot name" do
|
23
44
|
subject.name.should == 'robbot'
|
@@ -29,10 +50,6 @@ describe MyHipbot do
|
|
29
50
|
end
|
30
51
|
|
31
52
|
describe "replying" do
|
32
|
-
# TODO: replace with actual objects
|
33
|
-
let(:room) { stub_everything }
|
34
|
-
let(:sender) { stub_everything }
|
35
|
-
|
36
53
|
it "should reply to hello" do
|
37
54
|
subject.expects(:reply).with(room, 'hello!')
|
38
55
|
subject.tell(sender, room, '@robbot hello hipbot!')
|
@@ -48,4 +65,16 @@ describe MyHipbot do
|
|
48
65
|
subject.tell(sender, room, "hi everyone!")
|
49
66
|
end
|
50
67
|
end
|
68
|
+
|
69
|
+
describe "custom helpers" do
|
70
|
+
it "should have access to room variable" do
|
71
|
+
subject.expects(:reply).with(room, 'private project')
|
72
|
+
subject.tell(sender, room, '@robbot tell me the project name')
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should have access to message variable" do
|
76
|
+
subject.expects(:reply).with(room, 'you are John')
|
77
|
+
subject.tell(sender, room, '@robbot tell me my name')
|
78
|
+
end
|
79
|
+
end
|
51
80
|
end
|