Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic sketch of configurable limits and sanitization #180

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/slack/block_kit.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require_relative './block_kit/limits'

module Slack
module BlockKit
module Composition; end
Expand Down
8 changes: 8 additions & 0 deletions lib/slack/block_kit/block_kit_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

module Slack
module BlockKit
class BlockKitError < StandardError
end
end
end
2 changes: 2 additions & 0 deletions lib/slack/block_kit/composition/option.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ module Composition
# https://api.slack.com/reference/messaging/composition-objects#option
# https://api.slack.com/reference/messaging/block-elements#select
class Option
prepend Limits::Limitable

def initialize(value:, text:, initial: false, emoji: nil, description: nil, url: nil)
@text = PlainText.new(text: text, emoji: emoji)
@value = value
Expand Down
1 change: 1 addition & 0 deletions lib/slack/block_kit/element/static_select.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module Element
# https://api.slack.com/reference/messaging/block-elements#static-select
class StaticSelect
include Composition::ConfirmationDialog::Confirmable
prepend Limits::Limitable

TYPE = 'static_select'

Expand Down
43 changes: 43 additions & 0 deletions lib/slack/block_kit/limits.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

module Slack
module BlockKit
module Limits
# See https://api.slack.com/reference/block-kit/blocks for limits

MAX_STATIC_SELECT_OPTIONS = 100

MAX_OPTION_TEXT_LENGTH = 75
MAX_OPTION_VALUE_LENGTH = 75
MAX_OPTION_DESCRIPTION_LENGTH = 75

MAX_NUMBER_OF_MODAL_BLOCKS = 100

class << self
attr_writer :default_limiter

def set_limiter(class_name, limiter)
registry[class_name] = limiter
end

def limiter_for(block_kit_instance)
registry[block_kit_instance.class.name] || default_limiter
end

def default_limiter
@default_limiter ||= Limiters::NoopLimiter.new
end

private

def registry
@registry ||= {}
end
end

set_limiter 'Slack::BlockKit::Element::StaticSelect', Limiters::StaticSelectOptionsConfigRaiseOnErrorLimiter.new
set_limiter 'Slack::BlockKit::Composition::Option', Limiters::OptionTruncateLabelAndDescriptionLimiter.new
set_limiter 'Slack::Surfaces::Modal', Limiters::SurfaceRaiseOnTooManyBlocksLimiter.new(surface_type: :modal, max_blocks: MAX_NUMBER_OF_MODAL_BLOCKS)
end
end
end
15 changes: 15 additions & 0 deletions lib/slack/block_kit/limits/errors/incompatible_options_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module Slack
module BlockKit
module Limits
module Errors
class IncompatibleOptionsError < ::Slack::BlockKit::BlockKitError
def initialize(msg = 'The parameters of this block are incompatible')
super
end
end
end
end
end
end
12 changes: 12 additions & 0 deletions lib/slack/block_kit/limits/errors/limit_exceeded_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

module Slack
module BlockKit
module Limits
module Errors
class LimitExceededError < ::Slack::BlockKit::BlockKitError
end
end
end
end
end
20 changes: 20 additions & 0 deletions lib/slack/block_kit/limits/limitable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module Slack
module BlockKit
module Limits
module Limitable
def as_json(*)
original_json = super
limiter.call(original_json)
end

private

def limiter
::Slack::BlockKit::Limits.limiter_for(self)
end
end
end
end
end
15 changes: 15 additions & 0 deletions lib/slack/block_kit/limits/limiters/noop_limiter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module Slack
module BlockKit
module Limits
module Limiters
class NoopLimiter
def call(json)
json
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

module Slack
module BlockKit
module Limits
module Limiters
class OptionTruncateLabelAndDescriptionLimiter < NoopLimiter
include Limits::TruncationHelper

attr_reader :max_label_length, :max_value_length, :max_description_length, :label_truncation_replacement, :description_truncation_replacement

def initialize(max_label_length: MAX_OPTION_TEXT_LENGTH, max_value_length: MAX_OPTION_VALUE_LENGTH, max_description_length: MAX_OPTION_DESCRIPTION_LENGTH, label_truncation_replacement: '…', description_truncation_replacement: '…')
super()
@max_label_length = max_label_length
@max_value_length = max_value_length
@max_description_length = max_description_length
@label_truncation_replacement = label_truncation_replacement
@description_truncation_replacement = description_truncation_replacement
end

def call(option_json)
raise Errors::LimitExceededError, "Option values must be less than #{max_value_length} in size. Given value has size #{option_json[:value].size}" if option_json[:value].size > max_value_length

truncate_label!(option_json)
truncate_description!(option_json)

super(option_json)
end

private

def truncate_label!(option_json)
option_json[:text][:text] = truncate_string(option_json[:text][:text], max_label_length, omission: label_truncation_replacement)
end

def truncate_description!(option_json)
option_json[:description][:text] = truncate_string(option_json[:description][:text], max_description_length, omission: description_truncation_replacement) unless option_json[:description].nil?
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

module Slack
module BlockKit
module Limits
module Limiters
class StaticSelectOptionsConfigRaiseOnErrorLimiter < NoopLimiter
attr_reader :max_options

def initialize(max_options: MAX_STATIC_SELECT_OPTIONS)
super()
@max_options = max_options
end

def call(static_select_json)
check_one_of_options_or_option_groups_is_set!(static_select_json)
check_not_both_options_and_option_groups_are_set!(static_select_json)

check_options!(static_select_json) unless static_select_json[:options].nil?

super(static_select_json)
end

private

def check_one_of_options_or_option_groups_is_set!(static_select_json)
raise Errors::IncompatibleOptionsError.new("Either 'options' or 'option_groups' must have items. Both are empty.", parameter_names: %w[options option_groups]) if static_select_json[:options]&.empty? && static_select_json[:option_groups]&.empty?
end

def check_not_both_options_and_option_groups_are_set!(static_select_json)
raise Errors::IncompatibleOptionsError, "'options' and 'option_groups' cannot both be set" if !(static_select_json[:options].nil? || static_select_json[:options].empty?) && !(static_select_json[:option_groups].nil? || static_select_json[:option_groups].empty?)
end

def check_options!(static_select_json)
# TODO: Check that 'initial' is one of the options

raise Errors::LimitExceededError, "Static select elements are limited to #{max_options} options. #{static_select_json[:options].size} options provided" if static_select_json[:options].size > max_options
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module Slack
module BlockKit
module Limits
module Limiters
class SurfaceRaiseOnTooManyBlocksLimiter < NoopLimiter
attr_reader :surface_type, :max_blocks

def initialize(surface_type:, max_blocks:)
super()
@surface_type = surface_type
@max_blocks = max_blocks
end

def call(surface_json)
return surface_json if surface_json[:blocks].nil?

raise Errors::LimitExceededError, "Surfaces with type '#{surface_type}' allow a maximum of #{max_blocks} blocks. #{surface_json[:blocks].size} blocks have been specified." if surface_json[:blocks].size > max_blocks

super(surface_json)
end
end
end
end
end
end
15 changes: 15 additions & 0 deletions lib/slack/block_kit/limits/truncation_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module Slack
module BlockKit
module Limits
module TruncationHelper
def truncate_string(str, truncate_at, omission: '…')
return str.dup unless str.length > truncate_at

"#{str[0, truncate_at - omission.length]}#{omission}"
end
end
end
end
end
2 changes: 2 additions & 0 deletions lib/slack/surfaces/modal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ module Surfaces
# or using #title for detail setup
#
class Modal
prepend ::Slack::BlockKit::Limits::Limitable

TYPE = 'modal'

def initialize(
Expand Down
35 changes: 35 additions & 0 deletions spec/lib/slack/block_kit/composition/option_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,30 @@
end
end

context 'when label is very long' do
it 'truncates the label' do
option_with_long_label = described_class.new(
text: 'a' * (::Slack::BlockKit::Limits::MAX_OPTION_TEXT_LENGTH + 1),
value: 'a value',
emoji: true
)

expect(option_with_long_label.as_json.dig(:text, :text)).to eq("#{'a' * (::Slack::BlockKit::Limits::MAX_OPTION_TEXT_LENGTH - '…'.length)}…")
end
end

context 'when value is very long' do
it 'raises an error' do
options = described_class.new(
text: 'some text',
value: 'a' * (::Slack::BlockKit::Limits::MAX_OPTION_VALUE_LENGTH + 1),
emoji: true
)

expect { options.as_json }.to raise_error(::Slack::BlockKit::Limits::Errors::LimitExceededError)
end
end

context 'when description is set' do
let(:expected) do
{
Expand All @@ -56,6 +80,17 @@
it 'includes description as a plain_text object in the payload' do
expect(instance.as_json).to eq(expected)
end

it 'truncates long descriptions' do
option_with_long_description = described_class.new(
text: 'some text',
value: 'a value',
emoji: true,
description: 'a' * (::Slack::BlockKit::Limits::MAX_OPTION_DESCRIPTION_LENGTH + 1)
)

expect(option_with_long_description.as_json.dig(:description, :text)).to eq("#{'a' * (::Slack::BlockKit::Limits::MAX_OPTION_DESCRIPTION_LENGTH - '…'.length)}…")
end
end

context 'when url is set' do
Expand Down
13 changes: 13 additions & 0 deletions spec/lib/slack/block_kit/element/static_select_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,19 @@
expected_json.merge(options: expected_options)
)
end

context 'when there are too many options' do
subject(:as_json) do
(::Slack::BlockKit::Limits::MAX_STATIC_SELECT_OPTIONS + 1).times do |i|
instance.option(value: "__VALUE_#{i}__", text: "__TEXT_#{i}__")
end
instance.as_json
end

it 'raises an error when too many options are added' do
expect { as_json }.to raise_error(::Slack::BlockKit::Limits::Errors::LimitExceededError)
end
end
end

context 'with options and initial option' do
Expand Down
14 changes: 14 additions & 0 deletions spec/lib/slack/surfaces/modal_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@
it 'correctly serializes' do
expect(instance.as_json).to eq(expected_json)
end

context 'with too many blocks' do
let(:blocks) do
Slack::BlockKit.blocks do |block|
(Slack::BlockKit::Limits::MAX_NUMBER_OF_MODAL_BLOCKS + 1).times do |i|
block.image(url: image_url, alt_text: "__ALT_TEXT_#{i}__")
end
end
end

it 'raises an error' do
expect { instance.as_json }.to raise_error(Slack::BlockKit::Limits::Errors::LimitExceededError)
end
end
end

context 'without blocks argument' do
Expand Down