hipbot 0.0.5 → 0.1.0
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/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
|