hipbot 0.2.0 → 1.0.0.rc1

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.
@@ -1,81 +1,94 @@
1
1
  module Hipbot
2
- class Bot < Reactable
2
+ class << self
3
+ attr_accessor :bot, :plugins
4
+ delegate :name, to: :bot
5
+
6
+ def plugins
7
+ @plugins ||= []
8
+ end
9
+
10
+ def method_missing name, *params, &block
11
+ bot.send(name, *params, &block)
12
+ end
13
+ end
14
+
15
+ class Bot
16
+ extend Reactable
3
17
  include Singleton
4
- attr_accessor :configuration, :connection, :error_handler
5
18
 
6
- CONFIGURABLE_OPTIONS = [:name, :jid, :password, :adapter, :helpers, :plugins, :teams, :rooms, :logger]
19
+ attr_accessor :configuration, :connection
20
+
21
+ CONFIGURABLE_OPTIONS = [:adapter, :error_handler, :helpers, :jid, :logger, :password, :plugins, :preloader, :rooms, :storage, :teams, :user]
7
22
  delegate *CONFIGURABLE_OPTIONS, to: :configuration
23
+ delegate :name, to: :user
8
24
  alias_method :to_s, :name
9
25
 
10
26
  def initialize
11
- super
12
- self.configuration = Configuration.new.tap(&self.class.configuration)
13
- self.error_handler = self.class.error_handler
14
- extend(self.adapter || Adapters::Hipchat)
27
+ self.configuration ||= Configuration.new
15
28
  end
16
29
 
17
- def reactions
18
- defined_reactions + plugin_reactions + default_reactions
30
+ def react sender, room, message
31
+ message = Message.new(message, room, sender)
32
+ matching_reactions(message, sender.reactions, plugin_reactions, default_reactions).each(&:invoke)
19
33
  end
20
34
 
21
- def react sender, room, message
22
- Hipbot.logger.info("MESSAGE from #{sender} in #{room}")
23
- matching_reactions(sender, room, message) do |matches|
24
- Hipbot.logger.info("REACTION #{matches.first.inspect}")
25
- matches.first.invoke(sender, room, message)
26
- end
35
+ def setup
36
+ extend adapter
37
+ Hipbot.bot = self
38
+
39
+ User.send(:include, storage)
40
+ Room.send(:include, storage)
41
+ Response.send(:include, helpers)
42
+
43
+ helpers.module_exec(&preloader)
44
+ plugins.append(self)
45
+ Jabber.debug = true
46
+ Jabber.logger = logger
47
+ end
48
+
49
+ def plugin_reactions
50
+ plugins.flat_map{ |p| p.class.reactions }
51
+ end
52
+
53
+ def default_reactions
54
+ plugins.flat_map{ |p| p.class.default_reactions }
27
55
  end
28
56
 
29
57
  class << self
30
- ACCESSORS = { configure: :configuration, on_preload: :preloader, on_error: :error_handler }
58
+ def configure &block
59
+ instance.configuration = Configuration.new.tap(&block)
60
+ end
31
61
 
32
- ACCESSORS.each do |setter, getter|
33
- define_method(setter) do |&block|
34
- instance_variable_set("@#{getter}", block)
35
- end
62
+ def on_preload &block
63
+ instance.configuration.preloader = block
64
+ end
36
65
 
37
- define_method(getter) do
38
- instance_variable_get("@#{getter}") || Proc.new{}
39
- end
66
+ def on_error &block
67
+ instance.configuration.error_handler = block
40
68
  end
41
69
 
42
70
  def start!
43
71
  ::EM::run do
44
- Jabber.debug = true
45
- Jabber.logger = Hipbot::Bot.instance.logger
46
- Helpers.module_exec(&preloader)
72
+ instance.setup
47
73
  instance.start!
48
74
  end
49
75
  end
50
76
  end
51
77
 
52
- def plugin_for(reaction)
53
- included_plugins.find { |p| p.defined_reactions.include?(reaction) }
54
- end
78
+ protected
55
79
 
56
- private
57
-
58
- def plugin_reactions
59
- included_plugins.map(&:defined_reactions).flatten
60
- end
61
-
62
- def included_plugins
63
- @included_plugins ||= begin
64
- Array(plugins).map do |object|
65
- plugin = object.kind_of?(Plugin) ? object : object.new
66
- plugin.bot = self
67
- plugin
68
- end
80
+ def matching_reactions message, *reaction_sets
81
+ reaction_sets.each do |reactions|
82
+ matches = reactions.map{ |reaction| matching_rection(message, reaction) }.compact
83
+ return matches if matches.any?
69
84
  end
85
+ []
70
86
  end
71
87
 
72
- def default_reactions
73
- super + included_plugins.flat_map(&:default_reactions)
88
+ def matching_rection message, reaction
89
+ match = reaction.match_with(message)
90
+ match.matches? ? match : nil
74
91
  end
75
92
 
76
- def matching_reactions sender, room, message
77
- matches = reactions.select{ |r| r.match?(sender, room, message) }
78
- yield matches if matches.any?
79
- end
80
93
  end
81
94
  end
@@ -1,20 +1,41 @@
1
1
  module Hipbot
2
- class Collection < Struct.new(:id, :name, :params)
3
- private_class_method :new
4
- alias_method :to_s, :name
2
+ module Collection
3
+ extend ActiveSupport::Concern
5
4
 
6
- def initialize *args
7
- super
8
- self.params = OpenStruct.new(params)
5
+ included do
6
+ extend ClassMethods
7
+
8
+ attr_accessor :id, :name, :attributes
9
+ alias_method :to_s, :name
10
+ end
11
+
12
+ def initialize params
13
+ self.id = params.delete(:id)
14
+ self.name = params.delete(:name)
15
+ self.attributes = params
16
+ end
17
+
18
+ def update_attribute key, value
19
+ if key == :name
20
+ self.name = value
21
+ else
22
+ self.attributes[key] = value
23
+ end
24
+ end
25
+
26
+ def update_attributes hash
27
+ hash.each do |k, v|
28
+ update_attribute k, v
29
+ end
9
30
  end
10
31
 
11
32
  def delete
12
33
  self.class.collection.delete(self.id)
13
34
  end
14
35
 
15
- class << self
16
- def create *args, &block
17
- collection[args[0]] = new(*args, &block)
36
+ module ClassMethods
37
+ def create params, &block
38
+ collection[params[:id]] = new(params, &block)
18
39
  end
19
40
 
20
41
  def collection
@@ -34,6 +55,10 @@ module Hipbot
34
55
  items.map{ |i| find_one(i) }.compact.uniq
35
56
  end
36
57
 
58
+ def find_or_create_by params
59
+ find_one(params[:id] || params[:name]) || create(params)
60
+ end
61
+
37
62
  protected
38
63
 
39
64
  def method_missing name, *args, &block
@@ -3,13 +3,21 @@ module Hipbot
3
3
  attr_accessor *Bot::CONFIGURABLE_OPTIONS
4
4
 
5
5
  def initialize
6
- self.name = 'robot'
7
- self.jid = 'changeme'
8
- self.password = 'changeme'
9
- self.teams = {}
10
- self.rooms = {}
11
- self.helpers = Module.new
12
- self.logger = Hipbot::Logger.new($stdout)
6
+ self.adapter = Adapters::Hipchat
7
+ self.error_handler = Proc.new{}
8
+ self.helpers = Module.new
9
+ self.jid = ''
10
+ self.logger = Logger.new($stdout)
11
+ self.password = ''
12
+ self.plugins = Hipbot.plugins
13
+ self.preloader = Proc.new{}
14
+ self.rooms = {}
15
+ self.storage = Collection
16
+ self.teams = {}
17
+ end
18
+
19
+ def user
20
+ @user ||= User.new(name: 'robot')
13
21
  end
14
22
  end
15
23
  end
@@ -7,10 +7,16 @@ module Hipbot
7
7
  conn = ::EM::HttpRequest.new(url, :connect_timeout => 5, :inactivity_timeout => 10)
8
8
  http = conn.send(http_verb, :query => query)
9
9
  http.callback do
10
- response = HttpResponse.new(http)
11
- Hipbot.logger.info("HTTP-RESPONSE: #{response}")
12
- block.call(response)
10
+ begin
11
+ response = HttpResponse.new(http)
12
+ Hipbot.logger.info("HTTP-RESPONSE: #{response}")
13
+ block.call(response)
14
+ rescue => e
15
+ Hipbot.logger.error(e)
16
+ instance_exec(e, &Hipbot.error_handler)
17
+ end
13
18
  end if block.present?
19
+
14
20
  http.errback do
15
21
  Hipbot.logger.error("HTTP-RESPONSE-ERROR: #{url}")
16
22
  end
@@ -33,7 +39,7 @@ module Hipbot
33
39
  end
34
40
 
35
41
  def json
36
- @json ||= JSON.parse(body) rescue {}
42
+ @json ||= JSON.parse(body)
37
43
  end
38
44
  end
39
45
  end
@@ -8,8 +8,4 @@ module Hipbot
8
8
  super(severity, "[#{severity_name}][#{Time.now}] #{msg}")
9
9
  end
10
10
  end
11
-
12
- def self.logger
13
- Hipbot::Bot.instance.logger
14
- end
15
11
  end
@@ -0,0 +1,48 @@
1
+ module Hipbot
2
+ class Match < Struct.new(:reaction, :message)
3
+ def matches?
4
+ matches_regexp? && matches_scope? && matches_sender? && matches_place?
5
+ end
6
+
7
+ def invoke
8
+ Response.new(reaction, message).invoke(params)
9
+ end
10
+
11
+ protected
12
+
13
+ def params
14
+ reaction.anything? ? [message.body] : regexp_match[1..-1]
15
+ end
16
+
17
+ def matches_regexp?
18
+ reaction.anything? || regexp_match.present?
19
+ end
20
+
21
+ def regexp_match
22
+ @regexp_match ||= reaction.regexps.inject(nil) do |result, regexp|
23
+ break result if result
24
+ message_text.match(regexp)
25
+ end
26
+ end
27
+
28
+ def matches_scope?
29
+ reaction.global? || message.for?(Hipbot.user) || message.private?
30
+ end
31
+
32
+ def matches_place?
33
+ reaction.anywhere? || (message.room.present? ? matches_room? : reaction.private_message_only?)
34
+ end
35
+
36
+ def matches_room?
37
+ reaction.any_room? || reaction.rooms.include?(message.room.name)
38
+ end
39
+
40
+ def matches_sender?
41
+ reaction.from_all? || reaction.users.include?(message.sender.name)
42
+ end
43
+
44
+ def message_text
45
+ reaction.global? ? message.raw_body : message.body
46
+ end
47
+ end
48
+ end
@@ -1,28 +1,28 @@
1
1
  module Hipbot
2
- class Message
3
- attr_reader :body, :sender, :raw_body
2
+ class Message < Struct.new(:raw_body, :room, :sender)
3
+ attr_accessor :body, :recipients
4
4
 
5
- def initialize body, sender
6
- @raw_body = body
7
- @body = strip_recipient(body)
8
- @sender = sender
9
- end
10
-
11
- def recipients
12
- results = raw_body.scan(/@(\w+)/) + raw_body.scan(/@"(.*)"/)
13
- results.flatten.uniq
5
+ def initialize *args
6
+ super
7
+ Hipbot.logger.info("MESSAGE from #{sender} in #{room}")
8
+ self.body = strip_recipient(raw_body)
9
+ self.recipients = raw_body.scan(/@(\p{Word}++)/).flatten.compact.uniq
14
10
  end
15
11
 
16
12
  def for? recipient
17
- recipients.include? recipient.to_s.gsub(/\s+/, '')
13
+ recipients.include? recipient.mention
18
14
  end
19
15
 
20
16
  def strip_recipient body
21
- body.gsub(/^@\w+\W*/, '')
17
+ body.gsub(/^@\p{Word}++[^\p{Word}]*/, '').strip
22
18
  end
23
19
 
24
20
  def mentions
25
21
  recipients[1..-1] || [] # TODO: Fix global message case
26
22
  end
23
+
24
+ def private?
25
+ room.nil?
26
+ end
27
27
  end
28
28
  end
@@ -52,6 +52,10 @@ module Jabber
52
52
  @my_jid.resource
53
53
  end
54
54
 
55
+ def name= resource
56
+ @my_jid.resource = resource
57
+ end
58
+
55
59
  def on_presence(prio = 0, ref = nil, &block)
56
60
  @presence_cbs.add(prio, ref) do |room_jid, user_name, pres_type|
57
61
  block.call(room_jid, user_name, pres_type)
@@ -89,6 +93,30 @@ module Jabber
89
93
  @stream.send(pres) { |r| block.call(r) }
90
94
  end
91
95
 
96
+ def kick(recipients, room_jid)
97
+ iq = Iq.new(:set, room_jid)
98
+ iq.from = @my_jid
99
+ iq.add(IqQueryMUCAdmin.new)
100
+ recipients.each do |recipient|
101
+ item = IqQueryMUCAdminItem.new
102
+ item.nick = recipient
103
+ item.role = :none
104
+ iq.query.add(item)
105
+ end
106
+ @stream.send_with_id(iq)
107
+ end
108
+
109
+ def invite(recipients, room_jid)
110
+ msg = Message.new
111
+ msg.from = @my_jid
112
+ msg.to = room_jid
113
+ x = msg.add(XMUCUser.new)
114
+ recipients.each do |jid|
115
+ x.add(XMUCUserInvite.new(jid))
116
+ end
117
+ @stream.send(msg)
118
+ end
119
+
92
120
  def send_message(type, jid, text, subject = nil)
93
121
  message = Message.new(JID.new(jid), text.to_s)
94
122
  message.type = type
@@ -112,6 +140,8 @@ module Jabber
112
140
  Jabber::debuglog "MUCBrowser initialized"
113
141
  @conference_domain = @muc_browser.muc_rooms(@chat_domain).keys.first
114
142
  Jabber::debuglog "No conference domain found" if !@conference_domain.present?
143
+ @roster = Roster::Helper.new(@stream) # TODO: Error handling
144
+ @vcard = Vcard::Helper.new(@stream) # TODO: Error handling
115
145
  true
116
146
  rescue => e
117
147
  Jabber::debuglog "Connection failed"
@@ -121,7 +151,7 @@ module Jabber
121
151
 
122
152
  def activate_callbacks
123
153
  @stream.add_presence_callback(150, self) { |presence|
124
- @presence_cbs.process(presence.from.strip, presence.from.resource, presence.type.to_s)
154
+ @presence_cbs.process(presence.from.strip.to_s, presence.from.resource, presence.type.to_s)
125
155
  }
126
156
 
127
157
  @stream.add_message_callback(150, self) { |message|
@@ -141,7 +171,7 @@ module Jabber
141
171
  details = {}
142
172
  item.first.children.each{ |c| details[c.name] = c.text }
143
173
  rooms << {
144
- :item => item,
174
+ :item => item,
145
175
  :details => details
146
176
  }
147
177
  end
@@ -150,15 +180,23 @@ module Jabber
150
180
  end
151
181
 
152
182
  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|
183
+ @roster.wait_for_roster
184
+ @roster.items.map do |jid, item|
157
185
  {
158
- :item => item,
159
- :vcard => vcard.get(jid)
186
+ jid: item.jid.to_s,
187
+ name: item.iname,
188
+ mention: item.attributes['mention_name'],
160
189
  }
161
- end.compact
190
+ end
191
+ end
192
+
193
+ def get_user_details user_jid
194
+ vcard = @vcard.get(user_jid)
195
+ {
196
+ email: vcard['EMAIL/USERID'],
197
+ title: vcard['TITLE'],
198
+ photo: vcard['PHOTO'],
199
+ }
162
200
  end
163
201
 
164
202
  def deactivate_callbacks
@@ -173,11 +211,11 @@ module Jabber
173
211
  if is_invite?(message)
174
212
  room_name = message.children.last.first_element_text('name')
175
213
  topic = message.children.last.first_element_text('topic')
176
- @invite_cbs.process(message.from.strip, message.from.resource, room_name, topic)
214
+ @invite_cbs.process(message.from.strip.to_s, message.from.resource, room_name, topic)
177
215
  elsif message.type == :chat
178
- @private_message_cbs.process(message.from.strip, message)
216
+ @private_message_cbs.process(message.from.strip.to_s, message)
179
217
  elsif message.type == :groupchat
180
- @message_cbs.process(message.from.strip, message.from.resource, message)
218
+ @message_cbs.process(message.from.strip.to_s, message.from.resource, message)
181
219
  elsif message.type == :error
182
220
  false
183
221
  end