waylon-core 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0381c0f70f922e5958355430bef0ec1c8fd5d966b77b032f866fa9ecdcd3dd18'
4
- data.tar.gz: 13b63cef7b5e7575b5d564d43cb599bd326e0b9ed499be5df1b08a1c191fc073
3
+ metadata.gz: bf4a171a8420b5e982d54cf24b991a7ae667f3aa93f08b361d10ac28d9726207
4
+ data.tar.gz: 63982b47f1c05134c4fa5c69d07c4e05e8a1d748c245a4d7bd0695dabe4171c5
5
5
  SHA512:
6
- metadata.gz: bf835c89b14aa09e591fd8ca11b7c27065d0ad8262c2248ca261d897f320c0c5128d15154454407c32d35021dead332bcfb0597507e337dd7ee9de186bea2f1f
7
- data.tar.gz: 2eb7a29a02dcc44c85ed8dd0c74999299dae4ed1bb2736d6d076e9c1885ea80b96ffdc71288f730fe68de5c2487e23183d6adf0239f23112bec2cf9cf2e006ed
6
+ metadata.gz: 17a2baf807fd68312c76da6d9d1ca328ed5e5fec065711a7b8ce14a7d86850843d3ce2f6cfa3d4ea26f3c3ce30f329796427deeb98dcd6df77ec181da0afaf3f
7
+ data.tar.gz: 3edb93a7e82655d44d553687493d11a0a7559b6ae318ffb92051d481872c882baa2c6191a88e96ddd3936df278844e1caee97837f15523fd2158275d0a84eb03
data/.rubocop.yml CHANGED
@@ -21,7 +21,10 @@ Gemspec/RequireMFA:
21
21
  Enabled: false
22
22
 
23
23
  Metrics/AbcSize:
24
- Max: 19
24
+ Max: 19.5
25
+
26
+ Metrics/ClassLength:
27
+ Max: 200
25
28
 
26
29
  Metrics/CyclomaticComplexity:
27
30
  Max: 9
@@ -32,7 +35,11 @@ Metrics/PerceivedComplexity:
32
35
  Metrics/MethodLength:
33
36
  Max: 20
34
37
 
38
+ Metrics/ParameterLists:
39
+ Max: 6
40
+
35
41
  Metrics/BlockLength:
36
42
  Exclude:
37
43
  - "**/*_spec.rb"
38
44
  - "*.gemspec"
45
+ - "lib/waylon/rspec/matchers/**/*.rb"
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.0.3
1
+ 3.1.0
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- waylon-core (0.1.1)
4
+ waylon-core (0.1.2)
5
5
  addressable (~> 2.8)
6
6
  faraday (~> 1.8)
7
7
  i18n (~> 1.8)
@@ -17,7 +17,7 @@ GEM
17
17
  public_suffix (>= 2.0.2, < 5.0)
18
18
  ast (2.4.2)
19
19
  concurrent-ruby (1.1.9)
20
- diff-lcs (1.4.4)
20
+ diff-lcs (1.5.0)
21
21
  docile (1.4.0)
22
22
  faraday (1.9.3)
23
23
  faraday-em_http (~> 1.0)
@@ -53,7 +53,7 @@ GEM
53
53
  ruby2_keywords (~> 0.0.1)
54
54
  nio4r (2.5.8)
55
55
  parallel (1.21.0)
56
- parser (3.0.3.2)
56
+ parser (3.1.0.0)
57
57
  ast (~> 2.4.1)
58
58
  public_suffix (4.0.6)
59
59
  puma (5.5.2)
@@ -87,20 +87,20 @@ GEM
87
87
  diff-lcs (>= 1.2.0, < 2.0)
88
88
  rspec-support (~> 3.10.0)
89
89
  rspec-support (3.10.3)
90
- rubocop (1.23.0)
90
+ rubocop (1.24.1)
91
91
  parallel (~> 1.10)
92
92
  parser (>= 3.0.0.0)
93
93
  rainbow (>= 2.2.2, < 4.0)
94
94
  regexp_parser (>= 1.8, < 3.0)
95
95
  rexml
96
- rubocop-ast (>= 1.12.0, < 2.0)
96
+ rubocop-ast (>= 1.15.1, < 2.0)
97
97
  ruby-progressbar (~> 1.7)
98
98
  unicode-display_width (>= 1.4.0, < 3.0)
99
- rubocop-ast (1.15.0)
99
+ rubocop-ast (1.15.1)
100
100
  parser (>= 3.0.1.1)
101
101
  rubocop-rake (0.6.0)
102
102
  rubocop (~> 1.0)
103
- rubocop-rspec (2.6.0)
103
+ rubocop-rspec (2.7.0)
104
104
  rubocop (~> 1.19)
105
105
  ruby-progressbar (1.11.0)
106
106
  ruby2_keywords (0.0.5)
@@ -127,7 +127,7 @@ PLATFORMS
127
127
  arm64-darwin-21
128
128
 
129
129
  DEPENDENCIES
130
- bundler (~> 2.2)
130
+ bundler (~> 2.3)
131
131
  rake (~> 13.0)
132
132
  rspec (~> 3.10)
133
133
  rubocop (~> 1.23)
@@ -138,4 +138,4 @@ DEPENDENCIES
138
138
  yard (~> 0.9, >= 0.9.27)
139
139
 
140
140
  BUNDLED WITH
141
- 2.2.32
141
+ 2.3.4
@@ -10,19 +10,37 @@ module Waylon
10
10
  # @param action [Symbol] The method to call if the condition matches
11
11
  # @param allowed_groups [Array<Symbol>] The group names allowed to use this action
12
12
  # @param help [String] Optional help text to describe usage for this action
13
- def initialize(mechanism, action, allowed_groups, help = nil)
13
+ # @param mention_only [Boolean] Only applies to messages that directly mention (or IM) this bot
14
+ # rubocop:disable Style/OptionalBooleanParameter
15
+ def initialize(mechanism, action, allowed_groups, help = nil, mention_only = true)
14
16
  @mechanism = mechanism
15
17
  @action = action
16
18
  @allowed_groups = allowed_groups
17
19
  @help = help
20
+ @mention_only = mention_only
18
21
  end
22
+ # rubocop:enable Style/OptionalBooleanParameter
19
23
 
20
24
  # Placeholder for determining if this condition applies to the given input
21
25
  # @param _input [Waylon::Message] The input message
26
+ # @return [Boolean]
22
27
  def matches?(_input)
23
28
  false
24
29
  end
25
30
 
31
+ # Is this condition only valid for Messages that directly mention the bot?
32
+ # @return [Boolean]
33
+ def mention_only?
34
+ @mention_only
35
+ end
36
+
37
+ # Placeholder for optionally providing _named_ tokens
38
+ # @param _input [String] The message content
39
+ # @return [Hash<String,Object>]
40
+ def named_tokens(_input)
41
+ {}
42
+ end
43
+
26
44
  # Checks if a user is allowed based on this condition
27
45
  # @param user [Waylon::User] abstract user
28
46
  def permits?(user)
@@ -44,6 +62,13 @@ module Waylon
44
62
  permitted
45
63
  end
46
64
 
65
+ # Determines of a message complies with the {#mention_only?} setting for this condition
66
+ # @param message [Waylon::Message] The received message
67
+ # @return [Boolean]
68
+ def properly_mentions?(message)
69
+ (mention_only? && message.to_bot?) || (!mention_only? && !message.to_bot?)
70
+ end
71
+
47
72
  # Tokens is used to provide details about the message input to the action
48
73
  # @param _input [String] The message content as text
49
74
  # @return [Array<String>] The tokens extracted from the input message
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Waylon
4
+ module Conditions
5
+ # A pre-made catch-all condition for ignoring messages
6
+ class BlackHole < Condition
7
+ # Overrides normal Condition initialization to force a specific action
8
+ def initialize(*_args) # rubocop:disable Lint/MissingSuper
9
+ @mechanism = nil
10
+ @action = :ignore
11
+ @allowed_groups = [:everyone]
12
+ @help = ""
13
+ end
14
+
15
+ # Matches any input (since the Default route, when used, should always function)
16
+ # @return [Boolean]
17
+ def matches?(_input)
18
+ true
19
+ end
20
+
21
+ # Permits any user (since the Default route, when used, should always function)
22
+ # @return [Boolean]
23
+ def permits?(_user)
24
+ true
25
+ end
26
+
27
+ # Just provides back all input as a single token
28
+ # @return [Array<String>]
29
+ def tokens(input)
30
+ [input]
31
+ end
32
+ end
33
+ end
34
+ end
@@ -17,6 +17,11 @@ module Waylon
17
17
  def tokens(input)
18
18
  @mechanism.match(input).to_a[1..]
19
19
  end
20
+
21
+ def named_tokens(input)
22
+ match_data = @mechanism.match(input)
23
+ match_data.names.to_h { |n| [n.to_sym, match_data[n]] }
24
+ end
20
25
  end
21
26
  end
22
27
  end
data/lib/waylon/config.rb CHANGED
@@ -23,7 +23,7 @@ module Waylon
23
23
  true
24
24
  end
25
25
 
26
- # A list of emails specified via the CONF_GLOBAL_ADMINS environment variable
26
+ # A list of emails specified via the GLOBAL_ADMINS environment variable
27
27
  # @return [Array<String>] a list of emails
28
28
  def admins
29
29
  admin_emails = self["global.admins"]
@@ -32,11 +32,12 @@ module Waylon
32
32
 
33
33
  # Load in the config from env variables
34
34
  # @return [Boolean] Was the configuration loaded?
35
- def load_env
35
+ def load_env # rubocop:disable Metrics/AbcSize
36
36
  @schema ||= {}
37
37
  self["global.log.level"] = ENV.fetch("LOG_LEVEL", "info")
38
38
  self["global.redis.host"] = ENV.fetch("REDIS_HOST", "redis")
39
39
  self["global.redis.port"] = ENV.fetch("REDIS_PORT", "6379")
40
+ self["global.admins"] = ENV.fetch("GLOBAL_ADMINS", "")
40
41
  ENV.keys.grep(/CONF_/).each do |env_key|
41
42
  conf_key = env_key.downcase.split("_")[1..].join(".")
42
43
  ::Waylon::Logger.log("Attempting to set #{conf_key} from #{env_key}", :debug)
data/lib/waylon/core.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  # Standard Library dependencies
4
4
  require "base64"
5
+ require "benchmark"
5
6
  require "digest"
6
7
  require "English"
7
8
  require "fileutils"
@@ -21,6 +22,7 @@ require "i18n"
21
22
  require "json"
22
23
  require "moneta"
23
24
  require "resque"
25
+ require "sinatra"
24
26
 
25
27
  # Internal requirements
26
28
  require "waylon/version"
@@ -39,8 +41,11 @@ require "waylon/sense"
39
41
  require "waylon/skill_registry"
40
42
  require "waylon/skill"
41
43
  require "waylon/user"
44
+ require "waylon/conditions/black_hole"
42
45
  require "waylon/conditions/default"
43
46
  require "waylon/conditions/permission_denied"
44
47
  require "waylon/conditions/regex"
48
+ require "waylon/routes/black_hole"
45
49
  require "waylon/routes/default"
46
50
  require "waylon/routes/permission_denied"
51
+ require "waylon/webhook"
data/lib/waylon/group.rb CHANGED
@@ -30,7 +30,7 @@ module Waylon
30
30
  return false unless include?(user)
31
31
 
32
32
  users = members
33
- users.delete(user)
33
+ users.delete(user.email.downcase)
34
34
  storage.store(key, users)
35
35
  true
36
36
  end
data/lib/waylon/logger.rb CHANGED
@@ -6,7 +6,7 @@ module Waylon
6
6
  # The log level as defined in the global Config singleton
7
7
  # @return [String] The current log level
8
8
  def self.level
9
- Config.instance["global.log.level"]
9
+ Config.instance["global.log.level"] || "info"
10
10
  end
11
11
 
12
12
  # Abstraction for sending logs to the logger at some level
@@ -9,9 +9,28 @@ module Waylon
9
9
  nil
10
10
  end
11
11
 
12
+ # Message body
13
+ def body
14
+ nil
15
+ end
16
+
12
17
  # Message channel (meant to be overwritten by mixing classes)
13
18
  def channel
14
19
  nil
15
20
  end
21
+
22
+ # Does the Message mention the bot (meant to be overwritten by mixing classes)
23
+ def mentions_bot?
24
+ nil
25
+ end
26
+
27
+ # Is the Message a private/direct Message?
28
+ def private?
29
+ false
30
+ end
31
+
32
+ def to_bot?
33
+ true
34
+ end
16
35
  end
17
36
  end
data/lib/waylon/route.rb CHANGED
@@ -23,7 +23,7 @@ module Waylon
23
23
  @priority = priority
24
24
  end
25
25
 
26
- delegate %i[action help matches? permits? tokens] => :@condition
26
+ delegate %i[action help matches? mention_only? named_tokens permits? properly_mentions? tokens] => :@condition
27
27
 
28
28
  private
29
29
 
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Waylon
4
+ module Routes
5
+ # The route for unroutable events
6
+ class BlackHole < Route
7
+ def initialize(
8
+ name: "black_hole",
9
+ destination: Skills::Default,
10
+ condition: Conditions::BlackHole.new,
11
+ priority: 0
12
+ )
13
+ super
14
+ end
15
+ end
16
+ end
17
+ end
@@ -36,6 +36,13 @@ module Waylon
36
36
  chain :to do |method_name|
37
37
  @method_name = method_name
38
38
  end
39
+
40
+ description do
41
+ result = "route \"#{expected}\""
42
+ result += " to action \"#{@method_name}\"" if @method_name
43
+ result += " while a member of \"#{@group}\"" if @group
44
+ result
45
+ end
39
46
  end
40
47
  end
41
48
  end
@@ -63,7 +63,7 @@ module Waylon
63
63
  msg_details[:channel_id] = channel ? channel.id : chatroom.id
64
64
  end
65
65
 
66
- TestSense.process(msg_details)
66
+ TestSense.perform(msg_details)
67
67
  TestWorker.handle(TestSense.fake_queue)
68
68
  end
69
69
  end
@@ -51,6 +51,8 @@ module Waylon
51
51
  details[:text]
52
52
  end
53
53
 
54
+ alias body text
55
+
54
56
  # Lazily provides the details for TestMessages
55
57
  # @api private
56
58
  # @return [Hash] The details for this TestMessage instance
@@ -22,14 +22,14 @@ module Waylon
22
22
  end
23
23
 
24
24
  # Overrides the Sense.enqueue class method to avoid Resque
25
- def self.enqueue(route, request_id, body)
25
+ def self.enqueue(route, request)
26
26
  details = {
27
27
  "sense" => self,
28
- "message" => request_id,
29
- "tokens" => route.tokens(body.strip)
28
+ "request" => request,
29
+ "route" => route.name
30
30
  }
31
31
 
32
- fake_queue.push [route.destination, route.action, details]
32
+ fake_queue.push [route.destination, details]
33
33
  end
34
34
 
35
35
  # Allows access to the fake version of Resque
@@ -50,15 +50,10 @@ module Waylon
50
50
  @message_list ||= []
51
51
  end
52
52
 
53
- # Receives incoming message details and places work on a queue to be performed by a Skill
54
- # @param message_details [Hash] The details necessary for creating a TestMessage
55
- # @return [void]
56
- def self.process(message_details)
57
- message_list << message_details
58
- message_id = message_list.size - 1
59
- msg = message_class.new(message_id)
60
- route = SkillRegistry.instance.route(msg) || SkillRegistry.instance.default_route
61
- enqueue(route, msg.id, msg.text)
53
+ # Provides a way to use an initial request to reconstitute a Sense-specific Message
54
+ # @return [Waylon::Message]
55
+ def self.message_from_request(request)
56
+ message_class.new(message_list.size - 1, request)
62
57
  end
63
58
 
64
59
  # Emulates reactions by sending a message with the reaction type
@@ -66,7 +61,7 @@ module Waylon
66
61
  # @param type [Symbol,String] The type of reaction to send
67
62
  # @return [void]
68
63
  def self.react(request, type)
69
- msg = message_class.new(request)
64
+ msg = message_from_request(request)
70
65
  msg.channel.post_message(":#{type}:")
71
66
  end
72
67
 
@@ -81,10 +76,21 @@ module Waylon
81
76
  # @param text [String] The message content to send in response to the request
82
77
  # @return [void]
83
78
  def self.reply(request, text)
84
- msg = message_class.new(request)
79
+ msg = message_from_request(request)
85
80
  msg.channel.post_message(text)
86
81
  end
87
82
 
83
+ # Receives incoming message details and places work on a queue to be performed by a Skill
84
+ # @param message_details [Hash] The details necessary for creating a TestMessage
85
+ # @return [void]
86
+ def self.run(message_details)
87
+ message_list << message_details
88
+ message_id = message_list.size - 1
89
+ msg = message_class.new(message_id)
90
+ route = SkillRegistry.route(msg) || SkillRegistry.instance.default_route(msg)
91
+ enqueue(route, message_details)
92
+ end
93
+
88
94
  # Provides all message text sent _by_ Waylon
89
95
  # @return [Array<String>]
90
96
  def self.sent_messages
@@ -7,8 +7,8 @@ module Waylon
7
7
  # Instructs the worker to grab an item off the Queue and run it
8
8
  # @param queue [Queue] The queue that contains work to be done
9
9
  def self.handle(queue)
10
- skill, action, details = queue.pop
11
- skill.perform(action, details)
10
+ skill, details = queue.pop
11
+ skill.perform(details)
12
12
  end
13
13
  end
14
14
  end
data/lib/waylon/sense.rb CHANGED
@@ -20,17 +20,16 @@ module Waylon
20
20
 
21
21
  # The connection between Senses and Skills happens here, via a Route and a Hash of details
22
22
  # @param route [Route] route The matching Route from the SkillRegistry
23
- # @param request_id [String] The ID (from the messaging platform) of the request
24
- # @param body [String] Message content for the Skill
23
+ # @param request [String] The request message (or its ID) from the messaging platform
25
24
  # @api private
26
- def self.enqueue(route, request_id, body)
25
+ def self.enqueue(route, request)
27
26
  details = {
28
27
  "sense" => self,
29
- "message" => request_id,
30
- "tokens" => route.tokens(body.strip)
28
+ "request" => request,
29
+ "route" => route.name
31
30
  }
32
31
 
33
- Resque.enqueue route.destination, route.action, details
32
+ Resque.enqueue route.destination, details
34
33
  end
35
34
 
36
35
  # Provides a simple mechanism for referencing the Group subclass provided by this Sense
@@ -5,6 +5,8 @@ module Waylon
5
5
  class SenseRegistry
6
6
  include Singleton
7
7
 
8
+ attr_reader :senses
9
+
8
10
  # A convenience wrapper around the singleton instance #register method
9
11
  # @param (see #register)
10
12
  # @return (see #register)
data/lib/waylon/skill.rb CHANGED
@@ -5,7 +5,7 @@ module Waylon
5
5
  class Skill
6
6
  include BaseComponent
7
7
 
8
- attr_reader :sense, :tokens, :request
8
+ attr_reader :sense, :tokens, :request, :route
9
9
 
10
10
  # Config namespace for config keys
11
11
  # @return [String] The namespace for config keys
@@ -14,32 +14,31 @@ module Waylon
14
14
  end
15
15
 
16
16
  # Resque uses this to execute the Skill. Just defers to the `action` subclass method
17
- # @param action [Symbol,String] The method on the Skill subclass to call
18
17
  # @param details [Hash] Input details about the message for running a Skill action
19
- def self.perform(action, details)
20
- new(details["sense"], details["tokens"], details["message"], details["meta"])
21
- .send(action.to_sym)
18
+ def self.perform(details)
19
+ skill = new(details["sense"], details["route"], details["request"], details["meta"])
20
+ skill.send(skill.route.action.to_sym)
22
21
  end
23
22
 
24
23
  # Redis/Resque queue name
25
24
  # @api private
26
25
  # @return [Symbol]
27
26
  def self.queue
28
- :senses
27
+ :skills
29
28
  end
30
29
 
31
30
  # Adds skills to the SkillRegistry
32
31
  # @param condition [Condition,Regexp] The condition that determines if this route applies
33
32
  # @param action [Symbol,String] The method on the Skill subclass to call
34
33
  # @param allowed_groups [Symbol,Array<Symbol>] The group or list of groups allowed to use this
35
- # @param help [String] A description of how to use the skill
36
- def self.route(condition, action, allowed_groups: :everyone, help: nil, name: nil)
34
+ # @param help [String,Hash] A description of how to use the skill
35
+ def self.route(condition, action, allowed_groups: :everyone, help: nil, name: nil, mention_only: true)
37
36
  name ||= "#{to_s.split("::").last.downcase}##{action}"
38
37
  real_cond = case condition
39
38
  when Condition
40
39
  condition
41
40
  when Regexp
42
- Conditions::Regex.new(condition, action, allowed_groups, help)
41
+ Conditions::Regex.new(condition, action, allowed_groups, help, mention_only)
43
42
  else
44
43
  log("Unknown condition for route for #{name}##{action}", :warn)
45
44
  nil
@@ -48,13 +47,13 @@ module Waylon
48
47
  end
49
48
 
50
49
  # @param sense [Class,String] Class (or Class name) of the source Sense
51
- # @param tokens [Array<String>] Tokenized message content for use in the Skill
52
- # @param request [String,Integer] Reference to the request from the Sense provider (usually an ID)
50
+ # @param request [String,Integer] Reference to the request from the Sense provider (usually ID or message itself)
53
51
  # @param meta [Hash] Optional meta data that can be passed along from a Sense for use in Skills
54
- def initialize(sense, tokens, request, meta)
52
+ def initialize(sense, route, request, meta)
55
53
  @sense = sense.is_a?(Class) ? sense : Module.const_get(sense)
56
- @tokens = tokens || []
54
+ @route = SkillRegistry.find_by_name(route)
57
55
  @request = request
56
+ @tokens = @route.tokens(message.body) || []
58
57
  @meta = meta
59
58
  end
60
59
 
@@ -62,7 +61,6 @@ module Waylon
62
61
  # @return [String]
63
62
  def acknowledgement
64
63
  responses = [
65
- "I'll get back to you in just a sec.",
66
64
  "You got it!",
67
65
  "As you wish.",
68
66
  "Certainly!",
@@ -73,11 +71,10 @@ module Waylon
73
71
  "Of course!",
74
72
  "I'd be delighted!",
75
73
  "Right away!",
76
- "Gladly",
74
+ "Gladly!",
77
75
  "All right.",
78
76
  "I'm all over it.",
79
77
  "I'm on it!",
80
- "Let me see what I can do.",
81
78
  "Will do!"
82
79
  ]
83
80
  responses.sample
@@ -97,7 +94,8 @@ module Waylon
97
94
  sense: @sense,
98
95
  message: @request,
99
96
  tokens: @tokens,
100
- meta: @meta
97
+ meta: @meta,
98
+ route: route.name
101
99
  }
102
100
  end
103
101
 
@@ -111,7 +109,11 @@ module Waylon
111
109
  # Provides a wrapped message for responding to the received Sense
112
110
  # @return [Waylon::Message]
113
111
  def message
114
- sense.message_class.new(request)
112
+ sense.message_from_request(request)
113
+ end
114
+
115
+ def named_tokens
116
+ @named_tokens ||= @route.named_tokens(message.body) || {}
115
117
  end
116
118
 
117
119
  # Defers to the Sense to react to a message
@@ -124,9 +126,40 @@ module Waylon
124
126
  end
125
127
 
126
128
  # Defers to the Sense to determine how to reply to a message
127
- # @param [String] text The reply text
128
- def reply(text)
129
- sense.reply(request, text)
129
+ # @param text [String] The reply text
130
+ def reply(text, private: false)
131
+ if private && sense.supports?(:private_messages)
132
+ sense.private_reply(request, text)
133
+ else
134
+ log("Unable to send private message for Sense #{sense.name}, replying instead", :debug) if private
135
+ sense.reply(request, text)
136
+ end
137
+ end
138
+
139
+ # Defers to the Sense to determine how to reply to a message with rich content
140
+ # @param blocks [String] The reply blocks
141
+ def reply_with_blocks(blocks, private: false)
142
+ unless sense.supports?(:blocks)
143
+ log("Unable to use blocks with Sense #{sense.name}")
144
+ return false
145
+ end
146
+ if private
147
+ sense.private_reply_with_blocks(request, blocks)
148
+ else
149
+ log("Unable to send private message for Sense #{sense.name}, replying instead", :debug) if private
150
+ sense.reply_with_blocks(request, blocks)
151
+ end
152
+ end
153
+
154
+ # Defers to the Sense to do threaded replies if it can, otherwise it falls back to normal replies
155
+ # @param text [String] The reply text
156
+ def threaded_reply(text)
157
+ if sense.supports?(:threads)
158
+ sense.threaded_reply(request, text)
159
+ else
160
+ log("Unable to reply in theads for Sense #{sense.name}, replying instead", :debug)
161
+ sense.reply(request, text)
162
+ end
130
163
  end
131
164
  end
132
165
  end