hipbot 0.0.5 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +19 -12
- data/README.md +7 -4
- data/examples/cleverbot.rb +23 -0
- data/examples/google_images.rb +35 -0
- data/lib/hipbot.rb +5 -2
- data/lib/hipbot/adapters/hipchat/connection.rb +116 -83
- data/lib/hipbot/adapters/hipchat/hipchat.rb +4 -2
- data/lib/hipbot/adapters/telnet/connection.rb +1 -1
- data/lib/hipbot/adapters/telnet/telnet.rb +1 -1
- data/lib/hipbot/bot.rb +11 -20
- data/lib/hipbot/collection.rb +48 -0
- data/lib/hipbot/configuration.rb +4 -2
- data/lib/hipbot/message.rb +6 -9
- data/lib/hipbot/patches/hipchat_client.rb +192 -0
- data/lib/hipbot/reaction.rb +24 -17
- data/lib/hipbot/response.rb +3 -3
- data/lib/hipbot/room.rb +18 -3
- data/lib/hipbot/user.rb +11 -0
- data/lib/hipbot/version.rb +1 -1
- data/spec/integration/hipbot_spec.rb +13 -13
- data/spec/unit/hipbot_spec.rb +61 -57
- metadata +7 -3
- data/lib/hipbot/patches/mucclient.rb +0 -147
data/lib/hipbot/bot.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
module Hipbot
|
2
2
|
class Bot
|
3
3
|
attr_accessor :reactions, :configuration, :connection
|
4
|
-
|
4
|
+
cattr_accessor :default_reaction
|
5
|
+
|
6
|
+
CONFIGURABLE_OPTIONS = [:name, :jid, :password, :adapter, :helpers, :teams, :rooms]
|
5
7
|
delegate *CONFIGURABLE_OPTIONS, to: :configuration
|
6
8
|
alias_method :to_s, :name
|
7
9
|
|
@@ -11,6 +13,7 @@ module Hipbot
|
|
11
13
|
self.class.reactions.each do |opts|
|
12
14
|
on(*opts[0], &opts[-1])
|
13
15
|
end
|
16
|
+
on(*default_reaction[0], &default_reaction[-1]) if default_reaction.present?
|
14
17
|
extend(self.adapter || ::Hipbot::Adapters::Hipchat)
|
15
18
|
end
|
16
19
|
|
@@ -19,25 +22,21 @@ module Hipbot
|
|
19
22
|
self.reactions << Reaction.new(self, regexps, options, block)
|
20
23
|
end
|
21
24
|
|
22
|
-
def
|
23
|
-
return if sender == name
|
25
|
+
def react sender, room, message
|
24
26
|
matches = matching_reactions(sender, room, message)
|
25
|
-
if matches.size > 0
|
26
|
-
matches.first.invoke(sender, room, message)
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
def reactions_list
|
31
|
-
self.reactions.regexp
|
27
|
+
matches.first.invoke(sender, room, message) if matches.size > 0
|
32
28
|
end
|
33
29
|
|
34
30
|
class << self
|
35
|
-
|
36
31
|
def on *regexps, &block
|
37
32
|
@reactions ||= []
|
38
33
|
@reactions << [regexps, block]
|
39
34
|
end
|
40
35
|
|
36
|
+
def default &block
|
37
|
+
@@default_reaction = [[/(.*)/], block]
|
38
|
+
end
|
39
|
+
|
41
40
|
def configure &block
|
42
41
|
@configuration = block
|
43
42
|
end
|
@@ -53,20 +52,12 @@ module Hipbot
|
|
53
52
|
def start!
|
54
53
|
new.start!
|
55
54
|
end
|
56
|
-
|
57
55
|
end
|
58
56
|
|
59
57
|
private
|
60
58
|
|
61
59
|
def matching_reactions sender, room, message
|
62
|
-
|
63
|
-
all_reactions.select { |r| r.match?(sender, room, message) }
|
64
|
-
end
|
65
|
-
|
66
|
-
def default_reaction
|
67
|
-
@default_reaction ||= Reaction.new(self, [/.*/], {}, Proc.new {
|
68
|
-
reply("I don't understand \"#{message.body}\"")
|
69
|
-
})
|
60
|
+
self.reactions.select { |r| r.match?(sender, room, message) }
|
70
61
|
end
|
71
62
|
|
72
63
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Hipbot
|
2
|
+
class Collection < Struct.new(:id, :name, :params)
|
3
|
+
private_class_method :new
|
4
|
+
alias_method :to_s, :name
|
5
|
+
|
6
|
+
def initialize *args
|
7
|
+
super
|
8
|
+
self.params = OpenStruct.new(params)
|
9
|
+
end
|
10
|
+
|
11
|
+
def delete
|
12
|
+
self.class.collection.delete(self.id)
|
13
|
+
end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
attr_accessor :bot
|
17
|
+
|
18
|
+
def create *args, &block
|
19
|
+
collection[args[0]] = new(*args, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def collection
|
23
|
+
@collection ||= {}
|
24
|
+
end
|
25
|
+
|
26
|
+
def [] *items
|
27
|
+
items.first.is_a?(Array) ? find_many(*items) : find_one(items.first)
|
28
|
+
end
|
29
|
+
|
30
|
+
def find_one item
|
31
|
+
collection[item] || collection.find{ |_, i| i.name == item }.try(:last)
|
32
|
+
end
|
33
|
+
|
34
|
+
def find_many *items
|
35
|
+
items.flatten!
|
36
|
+
items.map{ |i| find_one(i) }.compact.uniq
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
def method_missing name, *args, &block
|
42
|
+
return collection.public_send(name, *args, &block) if collection.respond_to?(name)
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
data/lib/hipbot/configuration.rb
CHANGED
@@ -3,9 +3,11 @@ module Hipbot
|
|
3
3
|
attr_accessor *Bot::CONFIGURABLE_OPTIONS
|
4
4
|
|
5
5
|
def initialize
|
6
|
-
self.name
|
7
|
-
self.jid
|
6
|
+
self.name = 'robot'
|
7
|
+
self.jid = 'changeme'
|
8
8
|
self.password = 'changeme'
|
9
|
+
self.teams = {}
|
10
|
+
self.rooms = {}
|
9
11
|
end
|
10
12
|
|
11
13
|
def helpers
|
data/lib/hipbot/message.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
module Hipbot
|
2
2
|
class Message
|
3
|
-
|
3
|
+
attr_reader :body, :sender, :raw_body
|
4
|
+
|
4
5
|
def initialize body, sender
|
5
|
-
|
6
|
-
|
7
|
-
|
6
|
+
@raw_body = body
|
7
|
+
@body = strip_recipient(body)
|
8
|
+
@sender = sender
|
8
9
|
end
|
9
10
|
|
10
11
|
def recipients
|
@@ -20,12 +21,8 @@ module Hipbot
|
|
20
21
|
body.gsub(/^@\w+\W*/, '')
|
21
22
|
end
|
22
23
|
|
23
|
-
def sender_name
|
24
|
-
sender.split.first
|
25
|
-
end
|
26
|
-
|
27
24
|
def mentions
|
28
|
-
recipients[1..-1]
|
25
|
+
recipients[1..-1] # TODO: Fix global message case
|
29
26
|
end
|
30
27
|
end
|
31
28
|
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
require 'xmpp4r/muc/x/muc'
|
2
|
+
require 'xmpp4r/muc/iq/mucowner'
|
3
|
+
require 'xmpp4r/muc/iq/mucadmin'
|
4
|
+
require 'xmpp4r/dataforms'
|
5
|
+
require 'xmpp4r/roster'
|
6
|
+
require 'xmpp4r/vcard'
|
7
|
+
|
8
|
+
module Jabber
|
9
|
+
module MUC
|
10
|
+
class HipchatClient
|
11
|
+
|
12
|
+
def initialize(jid)
|
13
|
+
@my_jid = JID.new(jid)
|
14
|
+
|
15
|
+
@stream = Client.new(@my_jid.strip) # TODO: Error Handling
|
16
|
+
Jabber::debuglog "Stream initialized"
|
17
|
+
@chat_domain = @my_jid.domain
|
18
|
+
|
19
|
+
@presence_cbs = CallbackList.new
|
20
|
+
@message_cbs = CallbackList.new
|
21
|
+
@private_message_cbs = CallbackList.new
|
22
|
+
@invite_cbs = CallbackList.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def join(jid, password = nil, opts = { :history => false })
|
26
|
+
room_jid = JID.new(jid)
|
27
|
+
xmuc = XMUC.new
|
28
|
+
xmuc.password = password
|
29
|
+
|
30
|
+
if !opts[:history]
|
31
|
+
history = REXML::Element.new('history').tap{ |h| h.add_attribute('maxstanzas','0') }
|
32
|
+
xmuc.add_element history
|
33
|
+
end
|
34
|
+
|
35
|
+
room_jid.resource = name
|
36
|
+
set_presence(:available, room_jid, nil, xmuc) # TODO: Handle all join responses
|
37
|
+
end
|
38
|
+
|
39
|
+
def exit(jid, reason = nil)
|
40
|
+
room_jid = JID.new(jid)
|
41
|
+
Jabber::debuglog "Exiting #{jid}"
|
42
|
+
set_presence(:unavailable, room_jid, reason)
|
43
|
+
end
|
44
|
+
|
45
|
+
def keep_alive password
|
46
|
+
if @stream.is_disconnected?
|
47
|
+
connect(password)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def name
|
52
|
+
@my_jid.resource
|
53
|
+
end
|
54
|
+
|
55
|
+
def on_presence(prio = 0, ref = nil, &block)
|
56
|
+
@presence_cbs.add(prio, ref) do |room_jid, user_name, pres_type|
|
57
|
+
block.call(room_jid, user_name, pres_type)
|
58
|
+
false
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def on_message(prio = 0, ref = nil, &block)
|
63
|
+
@message_cbs.add(prio, ref) do |room_jid, user_name, message|
|
64
|
+
block.call(room_jid, user_name, message)
|
65
|
+
false
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def on_private_message(prio = 0, ref = nil, &block)
|
70
|
+
@private_message_cbs.add(prio, ref) do |user_jid, message|
|
71
|
+
block.call(user_jid, message)
|
72
|
+
false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def on_invite(prio = 0, ref = nil, &block)
|
77
|
+
@invite_cbs.add(prio, ref) do |room_jid, user_name, room_name, topic|
|
78
|
+
block.call(room_jid, user_name, room_name, topic)
|
79
|
+
false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def set_presence(type, to = nil, reason = nil, xmuc = nil, &block)
|
84
|
+
pres = Presence.new(:chat, reason)
|
85
|
+
pres.type = type
|
86
|
+
pres.to = to if to
|
87
|
+
pres.from = @my_jid
|
88
|
+
pres.add(xmuc) if xmuc
|
89
|
+
@stream.send(pres) { |r| block.call(r) }
|
90
|
+
end
|
91
|
+
|
92
|
+
def send_message(type, jid, text, subject = nil)
|
93
|
+
message = Message.new(JID.new(jid), text)
|
94
|
+
message.type = type
|
95
|
+
message.from = @my_jid
|
96
|
+
message.subject = subject
|
97
|
+
|
98
|
+
@send_thread.join if @send_thread.present? && @send_thread.alive?
|
99
|
+
@send_thread = Thread.new {
|
100
|
+
@stream.send(message)
|
101
|
+
sleep(0.2)
|
102
|
+
}
|
103
|
+
end
|
104
|
+
|
105
|
+
def connect password
|
106
|
+
begin
|
107
|
+
@stream.connect
|
108
|
+
Jabber::debuglog "Connected to stream"
|
109
|
+
@stream.auth(password)
|
110
|
+
Jabber::debuglog "Authenticated"
|
111
|
+
@muc_browser = MUCBrowser.new(@stream)
|
112
|
+
Jabber::debuglog "MUCBrowser initialized"
|
113
|
+
@conference_domain = @muc_browser.muc_rooms(@chat_domain).keys.first
|
114
|
+
Jabber::debuglog "No conference domain found" if !@conference_domain.present?
|
115
|
+
true
|
116
|
+
rescue => e
|
117
|
+
Jabber::debuglog "Connection failed"
|
118
|
+
false
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def activate_callbacks
|
123
|
+
@stream.add_presence_callback(150, self) { |presence|
|
124
|
+
@presence_cbs.process(presence.from.strip, presence.from.resource, presence.type.to_s)
|
125
|
+
}
|
126
|
+
|
127
|
+
@stream.add_message_callback(150, self) { |message|
|
128
|
+
handle_message(message)
|
129
|
+
}
|
130
|
+
Jabber::debuglog "Callbacks activated"
|
131
|
+
end
|
132
|
+
|
133
|
+
def get_rooms
|
134
|
+
iq = Iq.new(:get, @conference_domain)
|
135
|
+
iq.from = @stream.jid
|
136
|
+
iq.add(Discovery::IqQueryDiscoItems.new)
|
137
|
+
|
138
|
+
rooms = []
|
139
|
+
@stream.send_with_id(iq) do |answer|
|
140
|
+
answer.query.each_element('item') do |item|
|
141
|
+
details = {}
|
142
|
+
item.first.children.each{ |c| details[c.name] = c.text }
|
143
|
+
rooms << {
|
144
|
+
:item => item,
|
145
|
+
:details => details
|
146
|
+
}
|
147
|
+
end
|
148
|
+
end
|
149
|
+
rooms
|
150
|
+
end
|
151
|
+
|
152
|
+
def get_users
|
153
|
+
roster = Roster::Helper.new(@stream) # TODO: Error handling
|
154
|
+
vcard = Vcard::Helper.new(@stream) # TODO: Error handling
|
155
|
+
roster.wait_for_roster
|
156
|
+
roster.items.map do |jid, item|
|
157
|
+
{
|
158
|
+
:item => item,
|
159
|
+
:vcard => vcard.get(jid)
|
160
|
+
}
|
161
|
+
end.compact
|
162
|
+
end
|
163
|
+
|
164
|
+
def deactivate_callbacks
|
165
|
+
@stream.delete_presence_callback(self)
|
166
|
+
@stream.delete_message_callback(self)
|
167
|
+
Jabber::debuglog "Callbacks deactivated"
|
168
|
+
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def handle_message(message)
|
173
|
+
if is_invite?(message)
|
174
|
+
room_name = message.children.last.first_element_text('name')
|
175
|
+
topic = message.children.last.first_element_text('topic')
|
176
|
+
@invite_cbs.process(message.from.strip, message.from.resource, room_name, topic)
|
177
|
+
elsif message.type == :chat
|
178
|
+
@private_message_cbs.process(message.from.strip, message)
|
179
|
+
elsif message.type == :groupchat
|
180
|
+
@message_cbs.process(message.from.strip, message.from.resource, message)
|
181
|
+
elsif message.type == :error
|
182
|
+
false
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def is_invite?(message)
|
187
|
+
!message.x.nil? && message.x.kind_of?(XMUCUser) && message.x.first.kind_of?(XMUCUserInvite)
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
data/lib/hipbot/reaction.rb
CHANGED
@@ -1,54 +1,61 @@
|
|
1
1
|
module Hipbot
|
2
|
-
class Reaction < Struct.new(:
|
2
|
+
class Reaction < Struct.new(:bot, :regexps, :options, :block)
|
3
3
|
|
4
4
|
def invoke sender, room, message
|
5
|
-
message
|
5
|
+
message = message_for(message, sender)
|
6
6
|
arguments = arguments_for(message)
|
7
|
-
Response.new(
|
7
|
+
Response.new(bot, self, room, message).invoke(arguments)
|
8
8
|
end
|
9
9
|
|
10
10
|
def match? sender, room, message
|
11
11
|
message = message_for(message, sender)
|
12
|
-
matches_regexp?(message) && matches_scope?(message) && matches_sender?(message) && matches_room?(room)
|
12
|
+
matches_regexp?(message) && matches_scope?(room, message) && matches_sender?(message) && matches_room?(room)
|
13
13
|
end
|
14
14
|
|
15
|
-
|
15
|
+
protected
|
16
16
|
|
17
|
-
# TODO: this is pointless since we can get actual message object from xmpp4r
|
18
17
|
def message_for message, sender
|
19
18
|
Message.new(message, sender)
|
20
19
|
end
|
21
20
|
|
22
21
|
def arguments_for message
|
23
|
-
message.body.match(matching_regexp(message))[1..-1]
|
22
|
+
(global? ? message.raw_body : message.body).match(matching_regexp(message))[1..-1]
|
24
23
|
end
|
25
24
|
|
26
25
|
def matches_regexp?(message)
|
27
26
|
matching_regexp(message).present?
|
28
27
|
end
|
29
28
|
|
30
|
-
def
|
31
|
-
|
29
|
+
def matching_regexp(message)
|
30
|
+
regexps.find{ |regexp| regexp =~ (global? ? message.raw_body : message.body) }
|
32
31
|
end
|
33
32
|
|
34
|
-
def matches_scope?(message)
|
35
|
-
global? || message.for?(
|
33
|
+
def matches_scope?(room, message)
|
34
|
+
global? || message.for?(bot) || room.nil?
|
36
35
|
end
|
37
36
|
|
38
|
-
def
|
39
|
-
|
37
|
+
def matches_room?(room)
|
38
|
+
!options[:room] || rooms.include?(room.name) || room.nil?
|
40
39
|
end
|
41
40
|
|
42
|
-
def
|
43
|
-
|
41
|
+
def matches_sender?(message)
|
42
|
+
from_all? || users.include?(message.sender.name)
|
44
43
|
end
|
45
44
|
|
46
45
|
def global?
|
47
|
-
options[:global]
|
46
|
+
!!options[:global]
|
48
47
|
end
|
49
48
|
|
50
49
|
def from_all?
|
51
|
-
|
50
|
+
options[:from].blank?
|
51
|
+
end
|
52
|
+
|
53
|
+
def rooms
|
54
|
+
Array(options[:room]).flat_map{ |v| bot.rooms[v].presence || [v] }
|
55
|
+
end
|
56
|
+
|
57
|
+
def users
|
58
|
+
Array(options[:from]).flat_map{ |v| bot.teams[v].presence || [v] }
|
52
59
|
end
|
53
60
|
end
|
54
61
|
end
|
data/lib/hipbot/response.rb
CHANGED
@@ -14,15 +14,15 @@ module Hipbot
|
|
14
14
|
private
|
15
15
|
|
16
16
|
def reply string, room = self.room
|
17
|
-
bot.
|
17
|
+
return bot.send_to_user(sender, string) if room.nil?
|
18
|
+
bot.send_to_room(room, string)
|
18
19
|
end
|
19
20
|
|
20
21
|
[:get, :post, :put, :delete].each do |http_verb|
|
21
|
-
define_method http_verb do |url, query={}, &block|
|
22
|
+
define_method http_verb do |url, query = {}, &block|
|
22
23
|
http = ::EM::HttpRequest.new(url).send(http_verb, :query => query)
|
23
24
|
http.callback { block.call(::Hipbot::HttpResponse.new(http)) if block }
|
24
25
|
end
|
25
26
|
end
|
26
|
-
|
27
27
|
end
|
28
28
|
end
|