Module: Webhookdb::Message

Extended by:
MethodUtilities
Includes:
Appydays::Configurable
Defined in:
lib/webhookdb/message.rb,
lib/webhookdb/message/liquid_drops.rb

Defined Under Namespace

Classes: Body, CustomerDrop, Delivery, EmailTransport, EnvironmentDrop, FakeTransport, InvalidTransportError, MissingTemplateError, Recipient, Rendering, Template, Transport

Constant Summary collapse

DEFAULT_TRANSPORT =
:email
DATA_DIR =
Webhookdb::DATA_DIR + "messages"

Class Method Summary collapse

Methods included from MethodUtilities

attr_predicate, attr_predicate_accessor, singleton_attr_accessor, singleton_attr_reader, singleton_attr_writer, singleton_method_alias, singleton_predicate_accessor, singleton_predicate_reader

Class Method Details

.dispatch(template, to, transport_type) ⇒ Object

Create a Webhookdb::Message::Delivery ready to deliver (rendered, all bodies set up) using the given transport_type to the given user.



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/webhookdb/message.rb', line 39

def self.dispatch(template, to, transport_type)
  (transport = Webhookdb::Message::Transport.for(transport_type)) or
    raise InvalidTransportError, "Invalid transport #{transport_type}"
  recipient = transport.recipient(to)

  contents = self.render(template, transport_type, recipient)

  Webhookdb::Message::Delivery.db.transaction do
    delivery = Webhookdb::Message::Delivery.create(
      template: template.full_template_name,
      transport_type: transport.type,
      transport_service: transport.service,
      to: recipient.to,
      recipient: recipient.customer,
      extra_fields: template.extra_fields,
    )
    transport.add_bodies(delivery, contents)
    delivery.publish_deferred("dispatched", delivery.id)
    return delivery
  end
end

.render(template, transport_type, recipient) ⇒ Object

Render the transport-specific version of the given template and return a the rendering (content and exposed variables).

Templates can expose data to the caller by using the ‘expose’ tag, like expose subject %Hello from Webhookdb{% endexpose %}. This is available as [:subject] on the returned rendering.



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/webhookdb/message.rb', line 67

def self.render(template, transport_type, recipient)
  template_file = template.template_path(transport_type)
  raise MissingTemplateError, "#{template_file} does not exist" unless template_file.exist?

  drops = template.liquid_drops.stringify_keys.merge(
    "recipient" => Webhookdb::Message::CustomerDrop.new(recipient),
    "environment" => Webhookdb::Message::EnvironmentDrop.new,
    "app_url" => Webhookdb.app_url,
  )
  drops = self.unify_drops_encoding(drops)

  content_tmpl = Liquid::Template.parse(template_file.read)
  # The 'expose' drop smashes data into the register.
  # We need to keep track of the register to get the subject back out,
  # so we need to make our own context.
  lctx = Liquid::Context.new(
    [drops, content_tmpl.assigns],
    content_tmpl.instance_assigns,
    content_tmpl.registers,
    true,
    content_tmpl.resource_limits,
  )
  content = content_tmpl.render!(lctx, strict_variables: true)

  transport = Webhookdb::Message::Transport.for(transport_type)
  if transport.supports_layout?
    layout_file = template.layout_path(transport_type)
    if layout_file
      raise MissingTemplateError, "#{template_file} does not exist" unless layout_file.exist?
      layout_tmpl = Liquid::Template.parse(layout_file.read)
      drops["content"] = content.dup
      content = layout_tmpl.render!(drops, strict_variables: true, registers: content_tmpl.registers)
    end
  end

  return Rendering.new(content, lctx.registers)
end

.send_unsentObject



155
156
157
# File 'lib/webhookdb/message.rb', line 155

def self.send_unsent
  Webhookdb::Message::Delivery.unsent.each(&:send!)
end

.unify_drops_encoding(drops) ⇒ Hash

Handle encoding in liquid drop string values that would likely crash message rendering.

If there is a mixed character encoding of string values in a liquid drop, such as when handling user-supplied values, force all strings into UTF-8.

This is needed because the way Ruby does encoding coercion when parsing input which does not declare an encoding, such as a file or especially an HTTP response. Ruby will:

  • Use ASCII if the values fit into 7 bits

  • Use ASCII-8BIT if the values fit into 8 bits (128 to 255)

  • Otherwise, use UTF-8.

The actual rules are more complex, but this is common enough.

While ASCII encoding can be used as UTF-8, ASCII-8BIT cannot. So adding ‘(ascii-8bit string) + (utf-8 string)` will error with an `Encoding::CompatibilityError`.

Instead, if we see a series of liquid drop string values with different encodings, force them all to be UTF-8. This can result in some unexpected behavior, but it should be fine, since you’d only see it with unexpected input (all valid inputs should be UTF-8).

Parameters:

  • drops (Hash)

Returns:

  • (Hash)


131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/webhookdb/message.rb', line 131

def self.unify_drops_encoding(drops)
  return drops if drops.empty?
  seen_enc = nil
  force_enc = false
  drops.each_value do |v|
    next unless v.respond_to?(:encoding)
    seen_enc ||= v.encoding
    next if seen_enc == v.encoding
    force_enc = true
    break
  end
  return drops unless force_enc
  utf8 = Encoding.find("UTF-8")
  result = drops.each_with_object({}) do |(k, v), memo|
    if v.respond_to?(:encoding) && v.encoding != utf8
      v2 = v.encode(utf8, invalid: :replace, undef: :replace, replace: "?")
      memo[k] = v2
    else
      memo[k] = v
    end
  end
  return result
end