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 +4 -4
- data/AGENTS.md +22 -1
- data/CHANGELOG.md +35 -0
- data/README.md +67 -3
- data/Rakefile +9 -1
- data/SECURITY.md +71 -0
- data/app/controllers/concerns/livechat/request_context.rb +20 -0
- data/app/controllers/livechat/attachments_controller.rb +66 -7
- data/app/controllers/livechat/visitor_controller.rb +3 -2
- data/app/models/livechat/conversation.rb +14 -0
- data/app/models/livechat/message.rb +2 -2
- data/lib/livechat/attachment_policy.rb +27 -0
- data/lib/livechat/version.rb +1 -1
- data/{app/helpers → lib}/livechat/widget_helper.rb +5 -6
- data/lib/livechat.rb +2 -0
- metadata +12 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3784d2aa4059d1cc6faa8b79321ef0ec669dd69fac0b5543a3b881a5342b1961
|
|
4
|
+
data.tar.gz: d31ff12aa8a32a70315f38216fa4c423f0e130147e21bde0473ae189f337ed45
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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.
|
|
51
|
-
#
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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]
|
|
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
|
-
|
|
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:
|
|
112
|
-
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
|
data/lib/livechat/version.rb
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Livechat
|
|
4
|
-
# Included into the host's ActionView.
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
|
|
31
|
+
tag.button(label, type: 'button', data:, **options)
|
|
33
32
|
end
|
|
34
33
|
|
|
35
34
|
private
|
data/lib/livechat.rb
CHANGED
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.
|
|
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.
|
|
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.'
|