hipbot 0.1.0 → 0.2.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.
@@ -2,16 +2,7 @@ module Hipbot
2
2
  module Adapters
3
3
  module Hipchat
4
4
  def start!
5
- ::EM::run do
6
- ::EM.error_handler do |e|
7
- puts e.inspect
8
- e.backtrace.each do |line|
9
- puts line
10
- end
11
- end
12
-
13
- Connection.new(self)
14
- end
5
+ connection = Connection.new(self)
15
6
  end
16
7
 
17
8
  def method_missing(sym, *args, &block)
@@ -6,9 +6,7 @@ module Hipbot
6
6
  end
7
7
 
8
8
  def start!
9
- ::EM::run do
10
- ::EM::connect('0.0.0.0', 3001, Connection, self)
11
- end
9
+ ::EM::connect('0.0.0.0', 3001, Connection, self)
12
10
  end
13
11
  end
14
12
  end
data/lib/hipbot/bot.rb CHANGED
@@ -1,64 +1,81 @@
1
1
  module Hipbot
2
- class Bot
3
- attr_accessor :reactions, :configuration, :connection
4
- cattr_accessor :default_reaction
2
+ class Bot < Reactable
3
+ include Singleton
4
+ attr_accessor :configuration, :connection, :error_handler
5
5
 
6
- CONFIGURABLE_OPTIONS = [:name, :jid, :password, :adapter, :helpers, :teams, :rooms]
6
+ CONFIGURABLE_OPTIONS = [:name, :jid, :password, :adapter, :helpers, :plugins, :teams, :rooms, :logger]
7
7
  delegate *CONFIGURABLE_OPTIONS, to: :configuration
8
8
  alias_method :to_s, :name
9
9
 
10
10
  def initialize
11
+ super
11
12
  self.configuration = Configuration.new.tap(&self.class.configuration)
12
- self.reactions = []
13
- self.class.reactions.each do |opts|
14
- on(*opts[0], &opts[-1])
15
- end
16
- on(*default_reaction[0], &default_reaction[-1]) if default_reaction.present?
17
- extend(self.adapter || ::Hipbot::Adapters::Hipchat)
13
+ self.error_handler = self.class.error_handler
14
+ extend(self.adapter || Adapters::Hipchat)
18
15
  end
19
16
 
20
- def on *regexps, &block
21
- options = regexps[-1].kind_of?(Hash) ? regexps.pop : {}
22
- self.reactions << Reaction.new(self, regexps, options, block)
17
+ def reactions
18
+ defined_reactions + plugin_reactions + default_reactions
23
19
  end
24
20
 
25
21
  def react sender, room, message
26
- matches = matching_reactions(sender, room, message)
27
- matches.first.invoke(sender, room, message) if matches.size > 0
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
28
27
  end
29
28
 
30
29
  class << self
31
- def on *regexps, &block
32
- @reactions ||= []
33
- @reactions << [regexps, block]
34
- end
30
+ ACCESSORS = { configure: :configuration, on_preload: :preloader, on_error: :error_handler }
35
31
 
36
- def default &block
37
- @@default_reaction = [[/(.*)/], block]
38
- end
32
+ ACCESSORS.each do |setter, getter|
33
+ define_method(setter) do |&block|
34
+ instance_variable_set("@#{getter}", block)
35
+ end
39
36
 
40
- def configure &block
41
- @configuration = block
37
+ define_method(getter) do
38
+ instance_variable_get("@#{getter}") || Proc.new{}
39
+ end
42
40
  end
43
41
 
44
- def reactions
45
- @reactions || []
42
+ def start!
43
+ ::EM::run do
44
+ Jabber.debug = true
45
+ Jabber.logger = Hipbot::Bot.instance.logger
46
+ Helpers.module_exec(&preloader)
47
+ instance.start!
48
+ end
46
49
  end
50
+ end
47
51
 
48
- def configuration
49
- @configuration || Proc.new{}
50
- end
52
+ def plugin_for(reaction)
53
+ included_plugins.find { |p| p.defined_reactions.include?(reaction) }
54
+ end
51
55
 
52
- def start!
53
- new.start!
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
54
69
  end
55
70
  end
56
71
 
57
- private
72
+ def default_reactions
73
+ super + included_plugins.flat_map(&:default_reactions)
74
+ end
58
75
 
59
76
  def matching_reactions sender, room, message
60
- self.reactions.select { |r| r.match?(sender, room, message) }
77
+ matches = reactions.select{ |r| r.match?(sender, room, message) }
78
+ yield matches if matches.any?
61
79
  end
62
-
63
80
  end
64
81
  end
@@ -13,8 +13,6 @@ module Hipbot
13
13
  end
14
14
 
15
15
  class << self
16
- attr_accessor :bot
17
-
18
16
  def create *args, &block
19
17
  collection[args[0]] = new(*args, &block)
20
18
  end
@@ -28,7 +26,7 @@ module Hipbot
28
26
  end
29
27
 
30
28
  def find_one item
31
- collection[item] || collection.find{ |_, i| i.name == item }.try(:last)
29
+ collection[item] || find{ |i| i.name == item }
32
30
  end
33
31
 
34
32
  def find_many *items
@@ -39,7 +37,7 @@ module Hipbot
39
37
  protected
40
38
 
41
39
  def method_missing name, *args, &block
42
- return collection.public_send(name, *args, &block) if collection.respond_to?(name)
40
+ return collection.values.public_send(name, *args, &block) if Array.instance_methods.include?(name)
43
41
  super
44
42
  end
45
43
  end
@@ -8,10 +8,8 @@ module Hipbot
8
8
  self.password = 'changeme'
9
9
  self.teams = {}
10
10
  self.rooms = {}
11
- end
12
-
13
- def helpers
14
- @helpers ||= Module.new
11
+ self.helpers = Module.new
12
+ self.logger = Hipbot::Logger.new($stdout)
15
13
  end
16
14
  end
17
15
  end
@@ -0,0 +1,40 @@
1
+ module Hipbot
2
+ module Helpers
3
+ [:get, :post, :put, :delete].each do |http_verb|
4
+ define_method http_verb do |url, query = {}, &block|
5
+ Hipbot.logger.info("HTTP-REQUEST: #{url} #{query}")
6
+ query.merge!({ :head => {'accept-encoding' => 'gzip, compressed'} })
7
+ conn = ::EM::HttpRequest.new(url, :connect_timeout => 5, :inactivity_timeout => 10)
8
+ http = conn.send(http_verb, :query => query)
9
+ http.callback do
10
+ response = HttpResponse.new(http)
11
+ Hipbot.logger.info("HTTP-RESPONSE: #{response}")
12
+ block.call(response)
13
+ end if block.present?
14
+ http.errback do
15
+ Hipbot.logger.error("HTTP-RESPONSE-ERROR: #{url}")
16
+ end
17
+ end
18
+
19
+ module_function http_verb
20
+ end
21
+
22
+ class HttpResponse < Struct.new(:raw_response)
23
+ def body
24
+ raw_response.response
25
+ end
26
+
27
+ def code
28
+ raw_response.response_header.status
29
+ end
30
+
31
+ def headers
32
+ raw_response.response_header
33
+ end
34
+
35
+ def json
36
+ @json ||= JSON.parse(body) rescue {}
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,15 @@
1
+ require 'logger'
2
+
3
+ module Hipbot
4
+ class Logger < ::Logger
5
+ def add(severity, message = nil, progname = nil, &block)
6
+ msg = message || (block_given? and block.call) || progname
7
+ severity_name = { 0 => "DEBUG", 1 => "INFO", 2 => "WARN", 3 => "ERROR", 4 => "FATAL", 5 => "UNKNOWN" }[severity]
8
+ super(severity, "[#{severity_name}][#{Time.now}] #{msg}")
9
+ end
10
+ end
11
+
12
+ def self.logger
13
+ Hipbot::Bot.instance.logger
14
+ end
15
+ end
@@ -22,7 +22,7 @@ module Hipbot
22
22
  end
23
23
 
24
24
  def mentions
25
- recipients[1..-1] # TODO: Fix global message case
25
+ recipients[1..-1] || [] # TODO: Fix global message case
26
26
  end
27
27
  end
28
28
  end
@@ -90,7 +90,7 @@ module Jabber
90
90
  end
91
91
 
92
92
  def send_message(type, jid, text, subject = nil)
93
- message = Message.new(JID.new(jid), text)
93
+ message = Message.new(JID.new(jid), text.to_s)
94
94
  message.type = type
95
95
  message.from = @my_jid
96
96
  message.subject = subject
@@ -0,0 +1,11 @@
1
+ module Hipbot
2
+ class Plugin < Reactable
3
+ attr_accessor :bot
4
+
5
+ private
6
+
7
+ def reaction_target
8
+ bot
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,60 @@
1
+ module Hipbot
2
+ class Reactable
3
+ def on *regexps, &block
4
+ @defined_reactions ||= []
5
+ @defined_reactions << to_reaction(regexps, block)
6
+ end
7
+
8
+ def defined_reactions
9
+ @defined_reactions ||= []
10
+ @defined_reactions + class_reactions
11
+ end
12
+
13
+ def default_reactions
14
+ @default_reactions ||= begin
15
+ if reaction = self.class.default_reaction
16
+ [ to_reaction(reaction[0], reaction[-1]) ]
17
+ else
18
+ []
19
+ end
20
+ end
21
+ end
22
+
23
+ class << self
24
+ def on *regexps, &block
25
+ @reactions ||= []
26
+ @reactions << [regexps, block]
27
+ end
28
+
29
+ def reactions
30
+ @reactions || []
31
+ end
32
+
33
+ def default &block
34
+ @default_reaction = [[/(.*)/], block]
35
+ end
36
+
37
+ def default_reaction
38
+ @default_reaction
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def class_reactions
45
+ @class_reactions ||= self.class.reactions.map do |opts|
46
+ to_reaction(opts[0], opts[-1])
47
+ end
48
+ end
49
+
50
+ def to_reaction(regexps, block)
51
+ regexps = regexps.dup
52
+ options = regexps[-1].kind_of?(Hash) ? regexps.pop : {}
53
+ Reaction.new(reaction_target, regexps, options, block)
54
+ end
55
+
56
+ def reaction_target
57
+ self
58
+ end
59
+ end
60
+ end
@@ -12,6 +12,10 @@ module Hipbot
12
12
  matches_regexp?(message) && matches_scope?(room, message) && matches_sender?(message) && matches_room?(room)
13
13
  end
14
14
 
15
+ def inspect
16
+ "#<Hipbot::Reaction #{regexps} #{options}>"
17
+ end
18
+
15
19
  protected
16
20
 
17
21
  def message_for message, sender
@@ -35,7 +39,8 @@ module Hipbot
35
39
  end
36
40
 
37
41
  def matches_room?(room)
38
- !options[:room] || rooms.include?(room.name) || room.nil?
42
+ return true if options[:room].nil?
43
+ room.present? && (rooms.include?(room.name) || options[:room] == true)
39
44
  end
40
45
 
41
46
  def matches_sender?(message)
@@ -1,6 +1,7 @@
1
1
  module Hipbot
2
2
  class Response < Struct.new(:bot, :reaction, :room, :message)
3
3
  delegate :sender, :recipients, :body, :to => :message
4
+ include Helpers
4
5
 
5
6
  def initialize bot, reaction, room, message
6
7
  super
@@ -9,20 +10,20 @@ module Hipbot
9
10
 
10
11
  def invoke arguments
11
12
  instance_exec(*arguments, &reaction.block)
13
+ rescue Exception => e
14
+ Hipbot.logger.error(e)
15
+ instance_exec(e, &bot.error_handler)
12
16
  end
13
17
 
14
18
  private
15
19
 
16
- def reply string, room = self.room
17
- return bot.send_to_user(sender, string) if room.nil?
18
- bot.send_to_room(room, string)
20
+ def reply message, room = self.room
21
+ Hipbot.logger.info("REPLY in #{room}: #{message}")
22
+ room.nil? ? bot.send_to_user(sender, message) : bot.send_to_room(room, message)
19
23
  end
20
24
 
21
- [:get, :post, :put, :delete].each do |http_verb|
22
- define_method http_verb do |url, query = {}, &block|
23
- http = ::EM::HttpRequest.new(url).send(http_verb, :query => query)
24
- http.callback { block.call(::Hipbot::HttpResponse.new(http)) if block }
25
- end
25
+ def plugin
26
+ bot.plugin_for(reaction)
26
27
  end
27
28
  end
28
29
  end
data/lib/hipbot/room.rb CHANGED
@@ -8,15 +8,15 @@ module Hipbot
8
8
  end
9
9
 
10
10
  def set_topic topic
11
- self.class.bot.set_topic(self, topic)
11
+ Bot.instance.set_topic(self, topic)
12
12
  end
13
13
 
14
14
  def send_message message
15
- self.class.bot.send_to_room(self, message)
15
+ Bot.instance.send_to_room(self, message)
16
16
  end
17
17
 
18
18
  def users
19
- user_ids.map{ |id| User[id] }
19
+ User.find_many(user_ids)
20
20
  end
21
21
  end
22
22
  end
data/lib/hipbot/user.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  module Hipbot
2
2
  class User < Collection
3
3
  def send_message message
4
- self.class.bot.send_to_user name, message
4
+ Bot.instance.send_to_user name, message
5
5
  end
6
6
 
7
7
  def first_name
@@ -1,3 +1,3 @@
1
1
  module Hipbot
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -10,32 +10,54 @@ module HipbotHelpers
10
10
  end
11
11
  end
12
12
 
13
+ class AwesomePlugin < Hipbot::Plugin
14
+ on /respond awesome/ do
15
+ reply("awesome responded")
16
+ end
17
+ end
18
+
19
+ class CoolPlugin < Hipbot::Plugin
20
+ on /respond cool/ do
21
+ reply("cool responded")
22
+ end
23
+ end
24
+
13
25
  class MyHipbot < Hipbot::Bot
14
26
  configure do |config|
15
27
  config.name = 'robbot'
16
28
  config.jid = 'robbot@chat.hipchat.com'
17
29
  config.helpers = HipbotHelpers
30
+ config.plugins = [ AwesomePlugin, CoolPlugin.new ]
18
31
  end
19
32
 
20
33
  on /^hello hipbot!$/ do
21
34
  reply("hello!")
22
35
  end
36
+
23
37
  on /you're (.*), robot/ do |adj|
24
38
  reply("I know I'm #{adj}")
25
39
  end
40
+
26
41
  on /hi everyone!/, global: true do
27
42
  reply('hello!')
28
43
  end
44
+
29
45
  on /tell me the project name/ do
30
46
  reply(project_name)
31
47
  end
48
+
32
49
  on /tell me my name/ do
33
50
  reply("you are #{sender.first_name}")
34
51
  end
52
+
53
+ default do
54
+ reply("I didn't understand you")
55
+ end
35
56
  end
36
57
 
37
58
  describe MyHipbot do
38
- # TODO: replace with actual objects
59
+ subject { described_class.instance }
60
+
39
61
  let(:room) { Hipbot::Room.create('1', 'private', topic: 'topic') }
40
62
  let(:sender) { Hipbot::User.create('1', 'John Doe') }
41
63
 
@@ -64,6 +86,11 @@ describe MyHipbot do
64
86
  subject.expects(:send_to_room).with(room, "hello!")
65
87
  subject.react(sender, room, "hi everyone!")
66
88
  end
89
+
90
+ it "should respond with default reply" do
91
+ subject.expects(:send_to_room).with(room, "I didn't understand you")
92
+ subject.react(sender, room, "@robbot blahlblah")
93
+ end
67
94
  end
68
95
 
69
96
  describe "custom helpers" do
@@ -77,4 +104,16 @@ describe MyHipbot do
77
104
  subject.react(sender, room, '@robbot tell me my name')
78
105
  end
79
106
  end
107
+
108
+ describe "plugins" do
109
+ it "should reply to reaction defined in plugin" do
110
+ subject.expects(:send_to_room).with(room, 'awesome responded')
111
+ subject.react(sender, room, '@robbot respond awesome')
112
+ end
113
+
114
+ it "should reply to reaction defined in second plugin" do
115
+ subject.expects(:send_to_room).with(room, 'cool responded')
116
+ subject.react(sender, room, '@robbot respond cool')
117
+ end
118
+ end
80
119
  end