livechat 0.10.1 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f7868af4ac547374ec04ba6aac09d12db7f9ae53776cceb7967cc32c7bf56ef
4
- data.tar.gz: 40faba066cf75b325672728f1317a9dd1e56771cb2436c1e3a621ad36a560516
3
+ metadata.gz: 3784d2aa4059d1cc6faa8b79321ef0ec669dd69fac0b5543a3b881a5342b1961
4
+ data.tar.gz: d31ff12aa8a32a70315f38216fa4c423f0e130147e21bde0473ae189f337ed45
5
5
  SHA512:
6
- metadata.gz: 429aaa62a6b365b2314c2193faa3377879aee0b0c564c935065cc4df798e5471a5571c7e2f8b229fec2490b6ca382dfe8e50c2d88d809c063b5bf83b8d9183c3
7
- data.tar.gz: 8658bd68fe0bfd233f3ddddd30079bd71f6443394650c306171ecc7db2184602e93bff51622d049b1e0d33b6e355676e077eab8177f9c3c833a13f924cbd71d8
6
+ metadata.gz: b9a7ceca8836ce9238635a4dce4083eb5e04c60d1fb8241957b85a7c2ac5bff69099a8f19ceee1fa7075f1177075511790e783c9ff41df75cb01651a826f9ce0
7
+ data.tar.gz: 6fe9125a8525af6a62d4e40f728a4260d12aba7bcef2bc9fb75f12eb89fea1ebe1f75b125c20b47a36597eebe91321e3d94d4d43fe622912e35aea42ae34bb26
data/AGENTS.md CHANGED
@@ -5,7 +5,7 @@ Instructions for coding agents. Two audiences:
5
5
  - **[Installing livechat into a Rails app](#installing-into-a-rails-app)** — you are working in a host app and were asked to add support chat, live chat, or an in-app inbox.
6
6
  - **[Working on the gem itself](#working-on-the-gem-itself)** — you are working in this repository.
7
7
 
8
- Requirements: Ruby >= 3.2, Rails >= 7.1. Active Storage only for file attachments. **No Redis and no Action Cable** — the transport is polling unless you opt in.
8
+ Requirements: Ruby >= 3.2, Rails >= 7.1 and < 9. Active Storage only for file attachments. **No Redis and no Action Cable** — the transport is polling unless you opt in.
9
9
 
10
10
  If you are in a host app and this file is not in front of you, it ships inside the gem: `cat "$(bundle show livechat)/AGENTS.md"`.
11
11
 
@@ -104,6 +104,21 @@ Leave it off unless the app already has Action Cable working. Polling is not a d
104
104
 
105
105
  `config.attach_files` is on by default but **silently inert without Active Storage** in the host app (`rails active_storage:install`) — the widget keeps working, just without a paperclip. Caps: `max_attachments` (5), `max_attachment_size` (10 MB), `allowed_attachment_types` (nil = any). Files are served through the engine at `/livechat/attachments/:id`, gated per request — never a public blob URL. Do not build your own blob links.
106
106
 
107
+ Every authorized agent can read every conversation; visitor reads are scoped to
108
+ the signed-in id or guest cookie. There is no tenant-isolated agent inbox in
109
+ 1.x. No retention job runs automatically. Hosts should schedule their own
110
+ cutoff and use the destroy lifecycle so messages and attachments follow it:
111
+
112
+ ```ruby
113
+ Livechat::Conversation.purge_resolved(older_than: 90.days.ago)
114
+ ```
115
+
116
+ For an account-erasure request, destroy that user's conversations with
117
+ `where(visitor_id: user.id.to_s).find_each(&:destroy!)`. Do not use
118
+ `delete_all` when attachments must be purged. Confirm storage jobs, backups,
119
+ email notifications and hook destinations separately. The content-type
120
+ allowlist is not malware scanning.
121
+
107
122
  ### Do not
108
123
 
109
124
  - **Do not copy the widget JavaScript into `app/javascript`, or add a `<script>` tag for it.** `livechat_tag` renders what is needed, and the engine serves the code with a content fingerprint. There is no build step and nothing for esbuild/importmap/Tailwind to know about.
@@ -161,8 +176,14 @@ Turbo Drive and strict nonce-based CSP work out of the box. 26 locales ship with
161
176
 
162
177
  ## Working on the gem itself
163
178
 
179
+ The development toolchain is pinned to Ruby 4.0.5 in `mise.toml`. Do not use
180
+ macOS's `/usr/bin/ruby`. In shells, hooks, or automation where mise activation
181
+ is uncertain, explicitly prepend the configured Ruby, for example
182
+ `PATH="$(mise where ruby)/bin:$PATH" bundle exec rake test`.
183
+
164
184
  ```bash
165
185
  bundle exec rake test # minitest, dummy app under test/dummy
186
+ bundle exec rake test:system # real headless-Chrome visitor/agent loop
166
187
  bundle exec rubocop # must be clean
167
188
  BUNDLE_GEMFILE=gemfiles/rails_7.1.gemfile bundle exec rake test # 7.1, 7.2, 8.0, 8.1 in gemfiles/
168
189
  ```
data/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0 - 2026-08-11
4
+
5
+ - **The 1.x record names are settled.** `Livechat::Conversation` and
6
+ `Livechat::Message` are now documented as stable model constants rather than
7
+ being collapsed into a generic `Post`; the distinction preserves the thread
8
+ aggregate and the messages it owns.
9
+ - **A real Chrome test now proves the core support loop:** a visitor opens the
10
+ widget and sends a message, an agent reads and answers it in the inbox, and
11
+ the same visitor sees the reply back in the widget. Capybara and Selenium are
12
+ development/test dependencies only.
13
+ - **Attachments stay behind the conversation gate.** Full and byte-range data
14
+ now streams directly with a private, `no-store` cache policy after the
15
+ request is authorized. The URL is unchanged; audio playback gains seeking,
16
+ and no reusable Active Storage service URL leaves the engine.
17
+ - Added `Livechat::Conversation.purge_resolved(older_than:)` for explicit host
18
+ retention jobs. It destroys only resolved conversations older than the
19
+ supplied cutoff, cascades through messages and the configured Active Storage
20
+ lifecycle, and returns the number destroyed. Nothing is deleted
21
+ automatically.
22
+ - Added a security policy documenting stored visitor/message data, the shared
23
+ agent-inbox boundary, attachment risks, notifications, deletion, retention,
24
+ and private vulnerability reporting. Submitted page URLs now accept only
25
+ HTTP(S) and drop credentials, query strings, and fragments before storage,
26
+ keeping tokens out of the database and notification emails.
27
+ - Hosts that load Action View from an initializer boot normally. The widget
28
+ helper is loaded from `lib` before the engine registers its Action View hook;
29
+ `livechat_tag` and `livechat_button` are unchanged.
30
+ - Supported combinations are Ruby 3.2 through 4.0 and Rails 7.1 through 8.1;
31
+ Rails now has a `< 9` upper bound. Release validation runs the browser test,
32
+ lint and JavaScript checks, builds the gem, verifies RubyGems indexing, and
33
+ creates the GitHub Release.
34
+ - Existing 0.x installs need no migration, initializer change, constant alias,
35
+ or model rename. The new Rails upper bound, query-free stored page URLs, and
36
+ direct attachment streaming behavior are the compatibility costs.
37
+
3
38
  ## 0.10.1
4
39
 
5
40
  - **The demo inbox is now a live setup walkthrough.** The open conversation
data/README.md CHANGED
@@ -50,7 +50,7 @@ demo messages instead of duplicating conversations.
50
50
  > The inbox defaults to **development only**. Set `authorize_agent` before you
51
51
  > deploy — see [Configure](#configure).
52
52
 
53
- Ruby >= 3.2 · Rails >= 7.1 · Active Storage only if you want file attachments.
53
+ Ruby >= 3.2 · Rails >= 7.1, < 9 · Active Storage only if you want file attachments.
54
54
 
55
55
  Installing with a coding agent? Point it at [AGENTS.md](AGENTS.md) — the same
56
56
  steps in the order an agent needs them, plus the gates it tends to get wrong and
@@ -228,10 +228,11 @@ bin/rails active_storage:install && bin/rails db:migrate
228
228
  ```
229
229
 
230
230
  Visitors get a paperclip in the composer; agents get a file field on the reply
231
- form. Images render inline, other files as download links. Every file is served
231
+ form. Safe image formats render inline, other files as download links. Every file is served
232
232
  through the engine's own route and gated the same way the chat is — an agent,
233
233
  or the visitor who owns that conversation — so nothing leaks through a
234
- guessable or long-lived blob URL.
234
+ guessable or long-lived blob URL. Responses are private and `no-store`; audio
235
+ supports byte ranges for playback and seeking.
235
236
 
236
237
  Where Active Storage isn't installed, the widget quietly stays text-only.
237
238
 
@@ -244,6 +245,38 @@ config.storage_service = :livechat_uploads
244
245
 
245
246
  </details>
246
247
 
248
+ ## Privacy, retention, and deletion
249
+
250
+ Livechat stores conversation identity and context, message bodies and
251
+ attribution, read/status timestamps, and optional attachments in the host
252
+ application. Captured page URLs are limited to HTTP(S) and stored without
253
+ credentials, query strings, or fragments. The engine does not send data to a
254
+ Livechat service, but configured notification emails and message hooks can send
255
+ message content, visitor context, and attachment filenames wherever the host
256
+ chooses.
257
+
258
+ Every authorized agent can read every conversation in this one shared inbox.
259
+ Visitors can read only the conversation resolved from their signed-in id or
260
+ guest cookie. Livechat does not provide tenant-isolated agent inboxes in 1.x;
261
+ do not use one mount for mutually isolated tenant support teams.
262
+
263
+ No record is deleted automatically. Delete one conversation (including its
264
+ messages), all conversations for a user, or resolved history past a host-chosen
265
+ cutoff explicitly:
266
+
267
+ ```ruby
268
+ Livechat::Conversation.find(id).destroy!
269
+ Livechat::Conversation.where(visitor_id: user.id.to_s).find_each(&:destroy!)
270
+ Livechat::Conversation.purge_resolved(older_than: 90.days.ago)
271
+ ```
272
+
273
+ Use `destroy!`, not `delete_all`, when attachments must follow Rails' Active
274
+ Storage lifecycle. Confirm object-storage jobs, replicas, backups, notification
275
+ emails, hook destinations, logs, and exports separately: deleting live database
276
+ rows cannot retract those copies. `allowed_attachment_types` is an upload
277
+ allowlist, not malware scanning; choose a restrictive list and storage policy
278
+ for your application. See [SECURITY.md](SECURITY.md) for the full boundary.
279
+
247
280
  <details>
248
281
  <summary><b>Realtime with Action Cable</b></summary>
249
282
 
@@ -293,6 +326,36 @@ Contextual buttons are where a Rails app beats a generic support widget:
293
326
  </button>
294
327
  ```
295
328
 
329
+ ## Compatibility and public API
330
+
331
+ The following are the model and integration contracts that 1.x will keep
332
+ stable under semantic versioning:
333
+
334
+ - `Livechat::Conversation`, including `STATUSES`, `TYPING_TTL`, its persisted
335
+ fields, `messages`, `recent_first`, `for_visitor`, `claim!`, posting and
336
+ read/status methods, `display_name`, and `purge_resolved`.
337
+ - `Livechat::Message`, including `AUTHOR_TYPES`, `EVENTS`, `MAX_BODY_LENGTH`,
338
+ its persisted fields, optional `files`, documented scopes, author/read
339
+ predicates, `public_label`, `attached_files`, and `preview`.
340
+ - `Livechat.configure`, `mount_livechat`, `livechat_tag`, `livechat_button`,
341
+ `window.Livechat.open/close`, `data-livechat-open`, and
342
+ `data-livechat-message`.
343
+ - `config.on_visitor_message` and `config.on_agent_message`, each called with
344
+ the saved `Livechat::Message` documented by the initializer.
345
+
346
+ `Conversation` and `Message` are intentionally distinct and will not become a
347
+ generic `Post` during 1.x. Engine controllers, partials, CSS classes, generated
348
+ HTML, and widget implementation objects are private. Incompatible changes to
349
+ the public list above wait for a new major version; a deprecation normally
350
+ ships first.
351
+
352
+ ### Upgrading from 0.x
353
+
354
+ There is no migration, initializer change, constant alias, or model rename.
355
+ Stored page URLs become query-free on the next visitor message, attachment
356
+ responses stay on the same engine URL but now stream privately instead of being
357
+ buffered, and Rails 9 is excluded until a compatible release is tested.
358
+
296
359
  ## Use cases
297
360
 
298
361
  - SaaS apps: answer billing, onboarding and account questions from inside the app.
@@ -313,6 +376,7 @@ worth of exactly that.
313
376
 
314
377
  ```bash
315
378
  bundle exec rake test
379
+ bundle exec rake test:system
316
380
  bundle exec rubocop
317
381
  ```
318
382
 
data/Rakefile CHANGED
@@ -5,7 +5,15 @@ require 'rake/testtask'
5
5
 
6
6
  Rake::TestTask.new(:test) do |t|
7
7
  t.libs << 'test'
8
- t.pattern = 'test/**/*_test.rb'
8
+ t.test_files = FileList['test/**/*_test.rb'].exclude('test/system/**/*')
9
+ end
10
+
11
+ namespace :test do
12
+ desc 'Run browser (system) tests'
13
+ Rake::TestTask.new(:system) do |task|
14
+ task.libs << 'test'
15
+ task.test_files = FileList['test/system/**/*_test.rb']
16
+ end
9
17
  end
10
18
 
11
19
  task default: :test
data/SECURITY.md ADDED
@@ -0,0 +1,71 @@
1
+ # Security policy
2
+
3
+ ## Supported versions
4
+
5
+ Security fixes are released for the latest 1.x version. If a report also
6
+ affects the newest 0.x release, a backport may be published when the fix is
7
+ small and the affected application cannot upgrade immediately.
8
+
9
+ ## Reporting a vulnerability
10
+
11
+ Please do not open a public issue for a vulnerability. Use
12
+ [GitHub private vulnerability reporting](https://github.com/yshmarov/livechat/security/advisories/new)
13
+ and include the affected version, a minimal reproduction, impact, and any known
14
+ workaround. Do not include credentials, session cookies, visitor tokens,
15
+ private application URLs, customer messages, uploaded files, or production
16
+ database contents.
17
+
18
+ Public disclosure should wait until a fixed version is available and affected
19
+ users have had a reasonable opportunity to upgrade.
20
+
21
+ ## Data and authorization boundary
22
+
23
+ The widget and visitor endpoints are controlled by `config.enabled`; the agent
24
+ inbox is separately protected by `authorize_agent` and fails closed outside
25
+ development until the host grants access. A visitor can read only the
26
+ conversation resolved from their signed-in id or opaque guest cookie. Every
27
+ authorized agent can read and answer every conversation in the mounted inbox.
28
+ Livechat 1.x does not provide tenant-isolated agent inboxes.
29
+
30
+ Conversations can store a visitor token, host user id and label, email, status,
31
+ query-free HTTP(S) page URL, locale, message preview, activity timestamps, and
32
+ record timestamps. Messages can store bodies, author type, agent id and label,
33
+ system events, read state, timestamps, and optional files. Page credentials,
34
+ query strings, fragments, executable URL schemes, and invalid URLs are not
35
+ stored. Message bodies are plain text and length-capped; rendering escapes
36
+ them rather than interpreting Markdown or HTML.
37
+
38
+ Attachments stream through the engine only after the request is authorized as
39
+ an agent or the owning visitor. Responses are private and `no-store`, support
40
+ byte ranges, and never redirect to a reusable signed blob URL. Hosts own the
41
+ storage service, bucket policy, backups, retention, malware controls, access
42
+ logs, and incident response. `allowed_attachment_types` is an application
43
+ allowlist, not content inspection or malware scanning.
44
+
45
+ Livechat has no vendor service and does not transmit conversation data to this
46
+ project. Host-configured notification email can include message content,
47
+ visitor context, and attachment filenames. `on_visitor_message` and
48
+ `on_agent_message` receive the saved message and can send it anywhere the host
49
+ chooses. Those email and hook destinations are part of the host's privacy and
50
+ retention boundary.
51
+
52
+ ## Deletion and retention
53
+
54
+ Nothing is deleted automatically. Use the Active Record destroy lifecycle so
55
+ messages and Active Storage attachments follow their configured dependencies:
56
+
57
+ ```ruby
58
+ Livechat::Conversation.find(id).destroy!
59
+ Livechat::Conversation.where(visitor_id: user.id.to_s).find_each(&:destroy!)
60
+ Livechat::Conversation.purge_resolved(older_than: 90.days.ago)
61
+ ```
62
+
63
+ `purge_resolved` removes only resolved conversations whose last activity is
64
+ older than the supplied cutoff and returns the number destroyed. Open threads
65
+ are never removed by it. Do not use `delete_all` when dependent messages and
66
+ attachments must be removed.
67
+
68
+ Active Storage may enqueue blob purging through the host's Active Job adapter.
69
+ Confirm those jobs and deletion from object storage, replicas, backups, caches,
70
+ notification mailboxes, hook destinations, logs, analytics, and exports;
71
+ deleting live database rows does not rewrite those systems.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'uri'
4
+
3
5
  module Livechat
4
6
  # Who is asking — visitor or agent — and the gates that answer it.
5
7
  #
@@ -55,6 +57,24 @@ module Livechat
55
57
  render json: { errors: [message] }, status: :too_many_requests
56
58
  end
57
59
 
60
+ # The widget sends window.location.href so an agent knows where the visitor
61
+ # got stuck. Store only a normal HTTP(S) location, never credentials,
62
+ # fragments, or query parameters that can carry tokens and customer data.
63
+ def clean_page_url(value)
64
+ raw = value.to_s
65
+ return if raw.blank? || raw.length > 2_048
66
+
67
+ uri = URI.parse(raw)
68
+ return unless %w[http https].include?(uri.scheme&.downcase)
69
+ return if uri.host.blank? || uri.userinfo.present?
70
+
71
+ uri.query = nil
72
+ uri.fragment = nil
73
+ uri.to_s.first(255)
74
+ rescue URI::InvalidURIError
75
+ nil
76
+ end
77
+
58
78
  # Guests get a permanent random token — their key to the conversation
59
79
  # across visits. Signed-in visitors are keyed by id and only need the
60
80
  # cookie to carry a guest history into their account.
@@ -7,6 +7,8 @@ module Livechat
7
7
  # visitor whose conversation it belongs to. Streamed inline (not redirected
8
8
  # to a signed blob URL), so no long-lived link ever escapes the gate.
9
9
  class AttachmentsController < ApplicationController
10
+ include ActiveStorage::Streaming if defined?(::ActiveStorage::Streaming)
11
+
10
12
  def show
11
13
  attachment = find_attachment
12
14
  return head :not_found unless attachment
@@ -47,15 +49,72 @@ module Livechat
47
49
  end
48
50
 
49
51
  # Images and audio render inline (so the clients can show native media
50
- # controls); anything else downloads. nosniff keeps the browser from
51
- # second-guessing the type of a visitor-supplied file.
52
+ # controls); anything else downloads. Responses are private and never
53
+ # stored, and byte ranges keep audio seek/playback working without handing
54
+ # out an Active Storage service URL.
52
55
  def stream(blob)
53
56
  response.headers['X-Content-Type-Options'] = 'nosniff'
54
- inline = blob.content_type.to_s.start_with?('image/', 'audio/')
55
- send_data blob.download,
56
- filename: blob.filename.to_s,
57
- type: blob.content_type.presence || 'application/octet-stream',
58
- disposition: inline ? 'inline' : 'attachment'
57
+ response.headers['Cache-Control'] = 'private, no-store'
58
+ disposition = inline?(blob) ? 'inline' : 'attachment'
59
+
60
+ if request.headers['Range'].present?
61
+ if inline_audio?(blob)
62
+ send_audio_byte_range_data(blob, request.headers['Range'])
63
+ else
64
+ send_blob_byte_range_data(blob, request.headers['Range'], disposition:)
65
+ end
66
+ elsif inline_audio?(blob)
67
+ stream_audio(blob)
68
+ else
69
+ response.headers['Accept-Ranges'] = 'bytes'
70
+ response.headers['Content-Length'] = blob.byte_size.to_s
71
+ send_blob_stream(blob, disposition:)
72
+ end
73
+ end
74
+
75
+ # Active Storage deliberately forces types outside its image/PDF allowlist
76
+ # to download. The widget's recorded voice notes are a small explicit
77
+ # exception; nosniff plus an audio Content-Type keeps them media-only.
78
+ def inline?(blob)
79
+ AttachmentPolicy.inline?(blob.content_type)
80
+ end
81
+
82
+ def inline_audio?(blob)
83
+ AttachmentPolicy.audio?(blob.content_type)
84
+ end
85
+
86
+ def stream_audio(blob)
87
+ response.headers['Accept-Ranges'] = 'bytes'
88
+ response.headers['Content-Length'] = blob.byte_size.to_s
89
+ send_stream(filename: blob.filename.sanitized, disposition: 'inline', type: blob.content_type) do |stream|
90
+ blob.download { |chunk| stream.write(chunk) }
91
+ end
92
+ rescue ActiveStorage::FileNotFoundError
93
+ expires_now
94
+ head :not_found
95
+ end
96
+
97
+ def send_audio_byte_range_data(blob, range_header)
98
+ ranges = Rack::Utils.get_byte_ranges(range_header, blob.byte_size)
99
+ range = ranges.first if ranges&.one?
100
+ return head :range_not_satisfiable unless range
101
+ return head :range_not_satisfiable if range.end - range.begin >= streaming_chunk_max_size
102
+
103
+ data = blob.download_chunk(range)
104
+ response.headers['Content-Range'] = "bytes #{range.begin}-#{range.end}/#{blob.byte_size}"
105
+ response.headers['Accept-Ranges'] = 'bytes'
106
+ response.headers['Content-Length'] = data.bytesize.to_s
107
+ send_data data, filename: blob.filename.sanitized, disposition: 'inline',
108
+ status: :partial_content, type: blob.content_type
109
+ end
110
+
111
+ # Rails 8 exposes this knob; Rails 7.1/7.2 do not. Match the newer default
112
+ # on older supported releases so a Range header cannot force an unbounded
113
+ # in-memory chunk.
114
+ def streaming_chunk_max_size
115
+ return ActiveStorage.streaming_chunk_max_size if ActiveStorage.respond_to?(:streaming_chunk_max_size)
116
+
117
+ 100 * 1024 * 1024
59
118
  end
60
119
  end
61
120
  end
@@ -90,7 +90,7 @@ module Livechat
90
90
  visitor_token: current_visitor_id ? visitor_token : ensure_visitor_token,
91
91
  visitor_label: visitor_label,
92
92
  visitor_email: (current_visitor.try(:email).presence if current_visitor),
93
- page_url: params[:page_url].to_s.first(255).presence,
93
+ page_url: clean_page_url(params[:page_url]),
94
94
  locale: params[:locale].to_s.first(10).presence
95
95
  )
96
96
  end
@@ -105,7 +105,8 @@ module Livechat
105
105
  # "where are they stuck right now" is what an answering agent needs.
106
106
  def refresh_context(conversation)
107
107
  updates = {}
108
- updates[:page_url] = params[:page_url].to_s.first(255) if params[:page_url].present?
108
+ page_url = clean_page_url(params[:page_url])
109
+ updates[:page_url] = page_url if page_url
109
110
  updates[:locale] = params[:locale].to_s.first(10) if params[:locale].present?
110
111
  conversation.update_columns(updates) if updates.any?
111
112
  end
@@ -19,6 +19,20 @@ module Livechat
19
19
 
20
20
  scope :recent_first, -> { order(last_activity_at: :desc, id: :desc) }
21
21
 
22
+ # Host-controlled retention. Only resolved threads whose last activity is
23
+ # older than the supplied cutoff are removed; open conversations are never
24
+ # deleted automatically. Destroying (rather than delete_all) lets Rails run
25
+ # the message and Active Storage attachment lifecycle. Returns the number
26
+ # of conversations destroyed so a scheduled job can report what it did.
27
+ def self.purge_resolved(older_than:)
28
+ destroyed = 0
29
+ where(status: 'resolved', last_activity_at: ...older_than).find_each do |conversation|
30
+ conversation.destroy!
31
+ destroyed += 1
32
+ end
33
+ destroyed
34
+ end
35
+
22
36
  STATUSES.each do |status|
23
37
  define_method(:"#{status}?") { self.status == status }
24
38
  end
@@ -108,8 +108,8 @@ module Livechat
108
108
  { id: file.id,
109
109
  name: file.filename.to_s,
110
110
  url: "#{base}/attachments/#{file.id}",
111
- image: content_type.start_with?('image/'),
112
- audio: content_type.start_with?('audio/'),
111
+ image: AttachmentPolicy.image?(content_type),
112
+ audio: AttachmentPolicy.audio?(content_type),
113
113
  size: file.byte_size }
114
114
  end
115
115
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Livechat
4
+ # The small set of uploads the engine renders in a browser instead of
5
+ # downloading. Active Storage owns the safe image/PDF allowlist; Livechat
6
+ # adds common audio formats because voice messages are a first-class feature.
7
+ module AttachmentPolicy
8
+ INLINE_AUDIO_TYPES = %w[
9
+ audio/mpeg audio/mp4 audio/ogg audio/wav audio/webm audio/x-wav
10
+ ].freeze
11
+
12
+ module_function
13
+
14
+ def inline?(content_type)
15
+ audio?(content_type) || ActiveStorage.content_types_allowed_inline.include?(content_type)
16
+ end
17
+
18
+ def image?(content_type)
19
+ content_type.to_s.start_with?('image/') &&
20
+ ActiveStorage.content_types_allowed_inline.include?(content_type)
21
+ end
22
+
23
+ def audio?(content_type)
24
+ INLINE_AUDIO_TYPES.include?(content_type)
25
+ end
26
+ end
27
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Livechat
4
- VERSION = '0.10.1'
4
+ VERSION = '1.0.0'
5
5
  end
@@ -1,11 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Livechat
4
- # Included into the host's ActionView. Drop `<%= livechat_tag %>` before
5
- # </body> in your layout; it renders nothing unless chat is enabled for the
6
- # request. The widget shows a floating launcher (unless show_launcher is
7
- # off) and also opens from any element carrying `data-livechat-open`, or
8
- # from `window.Livechat.open()`.
4
+ # Included into the host's ActionView. Kept under lib and required before the
5
+ # engine registers its Action View load hook: a host initializer may load
6
+ # Action View before Rails sets up application autoloaders, when a helper
7
+ # living only under app/helpers is not yet a resolvable constant.
9
8
  module WidgetHelper
10
9
  def livechat_tag
11
10
  return unless Livechat.enabled?(request)
@@ -29,7 +28,7 @@ module Livechat
29
28
  data = (options.delete(:data) { {} } || {}).merge(livechat_open: '')
30
29
  data[:livechat_message] = message if message.present?
31
30
 
32
- tag.button(label, type: 'button', data: data, **options)
31
+ tag.button(label, type: 'button', data:, **options)
33
32
  end
34
33
 
35
34
  private
data/lib/livechat.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  require 'livechat/version'
4
4
  require 'livechat/configuration'
5
5
  require 'livechat/widget'
6
+ require 'livechat/widget_helper'
7
+ require 'livechat/attachment_policy'
6
8
  require 'livechat/notifications'
7
9
  require 'livechat/seeds'
8
10
  require 'livechat/engine'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: livechat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Shmarov
@@ -16,6 +16,9 @@ dependencies:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
18
  version: '7.1'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '9'
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -23,6 +26,9 @@ dependencies:
23
26
  - - ">="
24
27
  - !ruby/object:Gem::Version
25
28
  version: '7.1'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9'
26
32
  description: |
27
33
  A mountable Rails engine that adds support chat to your app — the thing
28
34
  you'd otherwise pay Crisp or Intercom for, or deploy Chatwoot to get. A
@@ -43,6 +49,7 @@ files:
43
49
  - MIT-LICENSE
44
50
  - README.md
45
51
  - Rakefile
52
+ - SECURITY.md
46
53
  - app/controllers/concerns/livechat/request_context.rb
47
54
  - app/controllers/livechat/application_controller.rb
48
55
  - app/controllers/livechat/attachments_controller.rb
@@ -52,7 +59,6 @@ files:
52
59
  - app/controllers/livechat/visitor_controller.rb
53
60
  - app/controllers/livechat/widgets_controller.rb
54
61
  - app/helpers/livechat/inbox_helper.rb
55
- - app/helpers/livechat/widget_helper.rb
56
62
  - app/mailers/livechat/mailer.rb
57
63
  - app/models/livechat/application_record.rb
58
64
  - app/models/livechat/conversation.rb
@@ -96,6 +102,7 @@ files:
96
102
  - lib/generators/livechat/install/templates/initializer.rb
97
103
  - lib/generators/livechat/migration_helpers.rb
98
104
  - lib/livechat.rb
105
+ - lib/livechat/attachment_policy.rb
99
106
  - lib/livechat/channels.rb
100
107
  - lib/livechat/configuration.rb
101
108
  - lib/livechat/dashboard.css
@@ -106,13 +113,14 @@ files:
106
113
  - lib/livechat/version.rb
107
114
  - lib/livechat/widget.js
108
115
  - lib/livechat/widget.rb
116
+ - lib/livechat/widget_helper.rb
109
117
  - lib/tasks/livechat_tasks.rake
110
118
  homepage: https://github.com/yshmarov/livechat
111
119
  licenses:
112
120
  - MIT
113
121
  metadata:
114
122
  homepage_uri: https://github.com/yshmarov/livechat
115
- source_code_uri: https://github.com/yshmarov/livechat
123
+ source_code_uri: https://github.com/yshmarov/livechat/tree/main
116
124
  changelog_uri: https://github.com/yshmarov/livechat/blob/main/CHANGELOG.md
117
125
  bug_tracker_uri: https://github.com/yshmarov/livechat/issues
118
126
  rubygems_mfa_required: 'true'
@@ -130,7 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
138
  - !ruby/object:Gem::Version
131
139
  version: '0'
132
140
  requirements: []
133
- rubygems_version: 4.0.10
141
+ rubygems_version: 4.0.16
134
142
  specification_version: 4
135
143
  summary: 'Open-source live chat for Rails: a drop-in support widget plus a team inbox,
136
144
  in your own database.'