livechat 0.7.1 → 0.8.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 +173 -0
- data/CHANGELOG.md +45 -0
- data/README.md +6 -0
- data/app/controllers/concerns/livechat/request_context.rb +85 -0
- data/app/controllers/livechat/application_controller.rb +10 -73
- data/app/controllers/livechat/conversations_controller.rb +1 -3
- data/app/controllers/livechat/dashboard_controller.rb +30 -0
- data/app/controllers/livechat/messages_controller.rb +1 -3
- data/app/views/layouts/livechat/application.html.erb +11 -12
- data/app/views/livechat/conversations/index.html.erb +91 -89
- data/app/views/livechat/conversations/show.html.erb +6 -4
- data/app/views/livechat/shared/_dashboard.html.erb +18 -0
- data/config/routes.rb +9 -4
- data/lib/generators/livechat/install/install_generator.rb +2 -6
- data/lib/generators/livechat/install/templates/create_livechat_tables.rb.tt +5 -3
- data/lib/generators/livechat/install/templates/initializer.rb +8 -0
- data/lib/generators/livechat/migration_helpers.rb +43 -0
- data/lib/livechat/configuration.rb +21 -1
- data/lib/livechat/dashboard.css +224 -231
- data/lib/livechat/version.rb +1 -1
- data/lib/livechat.rb +7 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 275ee37e54a4e5a5b30abb69aea78e691d05c78e2b6e25124bb51b10b442881e
|
|
4
|
+
data.tar.gz: 7769b4a9dfe5851ca2d7f1bece6c3c4197c9573a4be6e87673df1f40ce5be8c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '08038c4cea2d6942c3e5270dd610385e891178018436aa6feeb4191d0f08129937087662217d56ab298f2284bfb16463a093c5e872ede78d76e8fb1ca28ccb0f'
|
|
7
|
+
data.tar.gz: f788355751c85357edb0c7e79db9b9c5c6e775efe79e30c5f6b7249fe9a1fc2ad5adb247da7a4adea70b78a13a6e917b20d723dd935c3b3a3f706db48aa7befe
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Instructions for coding agents. Two audiences:
|
|
4
|
+
|
|
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
|
+
- **[Working on the gem itself](#working-on-the-gem-itself)** — you are working in this repository.
|
|
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.
|
|
9
|
+
|
|
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
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Installing into a Rails app
|
|
15
|
+
|
|
16
|
+
### 1. Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bundle add livechat
|
|
20
|
+
bin/rails generate livechat:install
|
|
21
|
+
bin/rails db:migrate
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The generator writes `config/initializers/livechat.rb`, one migration (`livechat_conversations`, `livechat_messages`), and `mount_livechat at: "/livechat"` into `config/routes.rb`. Read the initializer it wrote — every option is documented there in comments, and it is the source of truth over any summary of it, including this file.
|
|
25
|
+
|
|
26
|
+
Every `config.…` line below belongs inside the `Livechat.configure do |config|` block in that initializer. Uncomment and edit in place rather than appending a second `configure` block.
|
|
27
|
+
|
|
28
|
+
### 2. Wire the three things the generator cannot
|
|
29
|
+
|
|
30
|
+
**a. The widget tag.** Nothing appears until this is on the page:
|
|
31
|
+
|
|
32
|
+
```erb
|
|
33
|
+
<%# app/views/layouts/application.html.erb, before </body> %>
|
|
34
|
+
<%= livechat_tag %>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The helper is injected into ActionView by the engine — no include, no import, no asset pipeline entry. It renders the launcher bubble bottom-right.
|
|
38
|
+
|
|
39
|
+
**b. `authorize_agent` — do this before deploying.** The inbox at `/livechat` defaults to **development only**. It fails closed, so shipping without this is not an open inbox — it is a 403 reading "Forbidden. Set Livechat.config.authorize_agent to grant access."
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
config.authorize_agent = ->(request) { request.env["warden"]&.user&.admin? }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**c. Visitor identity**, if the app has users. Without it every visitor is a cookie-tracked guest, and nobody in the inbox has a name.
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
config.current_user = ->(request) { request.env["warden"]&.user }
|
|
49
|
+
config.visitor_label = ->(user) { user.name } # what the inbox shows
|
|
50
|
+
config.agent_label = ->(user) { user.name } # signed onto each reply
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
> **`current_user`, `enabled` and `authorize_agent` receive the raw `request`, not a controller.** Writing `->(request) { current_user }` is the most common mistake here — that method does not exist in this scope. Resolve the user *from the request*: Warden env, a signed cookie, `Current.user` if middleware already set it. Note the different shapes: `visitor_label` and `agent_label` receive the **user**, while `agent_display_name` receives the already-stored **label string**.
|
|
54
|
+
|
|
55
|
+
Rails 8 built-in auth:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
config.current_user = lambda do |request|
|
|
59
|
+
token = request.cookies["session_token"]
|
|
60
|
+
Session.find_signed(token)&.user if token
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 3. Verify
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
bin/rails routes | grep livechat # engine mounted
|
|
68
|
+
bin/rails livechat:seed_demo # optional sample conversations, idempotent
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Then in the running app: load any page, confirm the bubble appears bottom-right, send a message, and answer it at `/livechat`.
|
|
72
|
+
|
|
73
|
+
### Opening the widget
|
|
74
|
+
|
|
75
|
+
| Way | How |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| The launcher bubble | On by default. `config.show_launcher = false` to remove it |
|
|
78
|
+
| Your own element | `<%= livechat_button %>`, or any element with `data-livechat-open` |
|
|
79
|
+
| JavaScript | `window.Livechat.open()` |
|
|
80
|
+
|
|
81
|
+
A visitor has **one conversation**, not a queue of tickets — writing again reopens the same thread. Signed-in visitors keep it across devices (keyed by user id); guests are tracked by cookie.
|
|
82
|
+
|
|
83
|
+
### Email notifications need two settings, not one
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
config.mailer_from = "support@example.com" # required, or nothing sends
|
|
87
|
+
config.agent_emails = ["team@example.com"] # array, or a callable returning one
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Setting `agent_emails` alone sends nothing: `mailer_from` is what switches email on (`Livechat.config.emails_enabled?` is `mailer_from.present?`). Notification is one email per unread stretch, not one per message.
|
|
91
|
+
|
|
92
|
+
### Realtime is opt-in
|
|
93
|
+
|
|
94
|
+
Polling is the default transport, on purpose — a host with no Action Cable works untouched. Turning on push requires the host to actually mount a cable:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
config.action_cable = true
|
|
98
|
+
config.action_cable_url = "/cable" # keep in sync with the mount in routes.rb
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Leave it off unless the app already has Action Cable working. Polling is not a degraded mode here.
|
|
102
|
+
|
|
103
|
+
### Attachments
|
|
104
|
+
|
|
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
|
+
|
|
107
|
+
### Do not
|
|
108
|
+
|
|
109
|
+
- **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.
|
|
110
|
+
- **Do not build your own inbox.** Use the mounted one; `config.agent_layout = "admin/application"` renders it inside an existing admin shell.
|
|
111
|
+
- **Do not add an Action Cable mount to "make chat realtime"** unless you also set `config.action_cable = true`. Polling is the default and is not broken.
|
|
112
|
+
- **Do not set config outside the initializer.** `rate_limit` in particular is read when the controller class loads; assigning config per-request mutates it process-wide.
|
|
113
|
+
- **Do not expose attachments by blob URL** — the engine's gated route exists so a leaked signed URL cannot hand over a customer's file.
|
|
114
|
+
|
|
115
|
+
### Configuration worth knowing
|
|
116
|
+
|
|
117
|
+
Everything is optional; a fresh install works with zero config. Full list with comments is in the generated initializer.
|
|
118
|
+
|
|
119
|
+
| Option | Default | Note |
|
|
120
|
+
| --- | --- | --- |
|
|
121
|
+
| `authorize_agent` | development only | **Who can read the inbox. Set before deploying.** |
|
|
122
|
+
| `enabled` | everyone | Per-request gate for the widget and its endpoints |
|
|
123
|
+
| `current_user` | `nil` | Receives the request; nil means guest-by-cookie |
|
|
124
|
+
| `visitor_label`, `agent_label` | name/email/to_s | Receive the user |
|
|
125
|
+
| `agent_display_name` | the label unchanged | Receives the label; return "Support team" to keep agents anonymous |
|
|
126
|
+
| `app_name`, `greeting`, `reply_time_text`, `launcher_label` | localized defaults | Widget copy |
|
|
127
|
+
| `avatar_url`, `accent_color` | `nil` | Header avatar (URL or callable) and brand hex |
|
|
128
|
+
| `show_launcher` | `true` | `false` = open only from your own elements |
|
|
129
|
+
| `mailer_from` | `nil` | **Required for any email at all** |
|
|
130
|
+
| `agent_emails` | `nil` | Array or callable |
|
|
131
|
+
| `attach_files` | `true` | Needs Active Storage; inert without it |
|
|
132
|
+
| `storage_service` | app default | A `storage.yml` key for a dedicated bucket |
|
|
133
|
+
| `max_attachments`, `max_attachment_size` | `5`, `10.megabytes` | Enforced server-side |
|
|
134
|
+
| `allowed_attachment_types` | `nil` | Content-type allowlist |
|
|
135
|
+
| `action_cable`, `action_cable_url` | `false`, `"/cable"` | Opt-in push |
|
|
136
|
+
| `rate_limit` | `{ to: 30, within: 60 }` | Rails 7.2+; ignored on 7.1. `nil` disables |
|
|
137
|
+
| `mount_path` | `"/livechat"` | Keep in sync with `mount_livechat at:` |
|
|
138
|
+
| `on_visitor_message`, `on_agent_message` | no-ops | Run inline after save — Slack, Noticed, push |
|
|
139
|
+
|
|
140
|
+
Turbo Drive and strict nonce-based CSP work out of the box. 26 locales ship with the gem, RTL included.
|
|
141
|
+
|
|
142
|
+
### Common failure modes
|
|
143
|
+
|
|
144
|
+
| Symptom | Cause |
|
|
145
|
+
| --- | --- |
|
|
146
|
+
| `/livechat` returns 403 "Set Livechat.config.authorize_agent to grant access" | Exactly what it says: still at the development-only default |
|
|
147
|
+
| No bubble on the page | `livechat_tag` missing from the rendered layout, `config.enabled` false, or `show_launcher = false` with no opener of your own |
|
|
148
|
+
| No notification emails | `mailer_from` not set — `agent_emails` alone does nothing |
|
|
149
|
+
| Messages only appear on refresh | Expected: polling is the default. `config.action_cable = true` (with `/cable` mounted) for push |
|
|
150
|
+
| No attachment button | Active Storage not installed, or `attach_files = false` |
|
|
151
|
+
| `undefined local variable current_user` in the initializer | A gate lambda treated its argument as a controller. It is a `request` |
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Working on the gem itself
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
bundle exec rake test # minitest, dummy app under test/dummy
|
|
159
|
+
bundle exec rubocop # must be clean
|
|
160
|
+
BUNDLE_GEMFILE=gemfiles/rails_7.1.gemfile bundle exec rake test # 7.1, 7.2, 8.0, 8.1 in gemfiles/
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Layout: `app/` controllers, models, inbox views, mailer · `lib/livechat/` config, widget JS/CSS, seeds, engine, channels · `lib/generators/livechat/install/` the one generator · `config/locales/` 26 locales · `test/` minitest, `test/dummy` the host app.
|
|
164
|
+
|
|
165
|
+
Conventions this codebase holds to — follow them rather than the first thing that works:
|
|
166
|
+
|
|
167
|
+
- **Polling is the baseline, Action Cable is opt-in.** Nothing may require a cable to be mounted. The channel lives under `lib/` and is required only when `ActionCable` is defined, so eager-loading an app without it cannot fail.
|
|
168
|
+
- **Active Storage is optional at runtime.** Attachment code checks for it rather than assuming it; an app without Active Storage gets a working widget, not an exception.
|
|
169
|
+
- **The widget is plain ES5-style JS served by the engine**, no build step, no framework, config read from a JSON block so a Turbo visit re-reads the current page's settings.
|
|
170
|
+
- **Visitor scoping is never by conversation id.** The widget's endpoints resolve the thread from the signed-in id or the guest cookie, so no id in a request can address someone else's conversation. Keep it that way.
|
|
171
|
+
- **The dummy app pins `config.active_job.queue_adapter = :test`.** Do not remove it or let it drift back to the `:async` default. Attaching a file enqueues Active Storage's analysis job, and `:async` runs it on a background thread that checks out its own connection — writes no test transaction covers, landing in the middle of whatever runs next. That is a suite that fails order-dependently in a test which never created a row, and it is miserable to trace back.
|
|
172
|
+
- Every user-facing change bumps `lib/livechat/version.rb` and adds a `CHANGELOG.md` entry that says what it costs, not only what it adds.
|
|
173
|
+
- Commit messages are prose that explains the tradeoff — read `git log` before writing one.
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
- **`config.agent_layout` now works on its own.** The inbox's stylesheet and
|
|
6
|
+
script were declared in the gem's layout, so replacing that layout dropped
|
|
7
|
+
both: unstyled inbox with its thread polling and keyboard submit dead. They
|
|
8
|
+
move into the views, so every layout gets them with nothing asked of the host.
|
|
9
|
+
- **The dashboard stylesheet no longer claims selectors it does not own.** It
|
|
10
|
+
styled bare `*`, `body` and `a`, and its `.container`, `.card` and `.tabs` are
|
|
11
|
+
names other frameworks use too. Component rules now nest inside a
|
|
12
|
+
`.lvc-dashboard` wrapper the views render, and every custom property is
|
|
13
|
+
`--lvc-` prefixed — that collision ran both ways, so a host defining `--bg`
|
|
14
|
+
recoloured the inbox just as easily.
|
|
15
|
+
- **Added `config.base_controller_class`.** Name the controller your own admin
|
|
16
|
+
inherits from and the inbox adopts its layout, helpers, authentication and
|
|
17
|
+
request context. It reparents the inbox only — the widget, visitor API and
|
|
18
|
+
attachment proxy stay on the engine's public controller, so it can never
|
|
19
|
+
demand a staff session from a visitor starting a chat. Default is unchanged.
|
|
20
|
+
- **Migrations follow the host's `primary_key_type`,** including the engine's own
|
|
21
|
+
`messages -> conversations` foreign key, which has to match or a bigint column
|
|
22
|
+
ends up pointing at a uuid table. A uuid-keyed app has a uuid
|
|
23
|
+
`active_storage_attachments.record_id`, so bigint tables here could never hold
|
|
24
|
+
a message attachment: `attach` raised `NotNullViolation`.
|
|
25
|
+
- **Dropped the `id: /\d+/` constraints** on the attachment and conversation
|
|
26
|
+
routes, which were what forced the tables to be bigint. Ordering already
|
|
27
|
+
disambiguates: every fixed-name route is declared first.
|
|
28
|
+
- **`t.references :conversation` no longer creates its own index.**
|
|
29
|
+
`(conversation_id)` is a leftmost prefix of the `(conversation_id, id)` index
|
|
30
|
+
the migration already adds, so it answered no query the wider one could not.
|
|
31
|
+
Existing installs keep theirs until they drop it:
|
|
32
|
+
`remove_index :livechat_messages, :conversation_id`.
|
|
33
|
+
- A `BackboneTest` now fails the build on any of the above regressing.
|
|
34
|
+
|
|
35
|
+
## 0.7.2
|
|
36
|
+
|
|
37
|
+
- Adds `AGENTS.md`: install and integration instructions written for coding
|
|
38
|
+
agents — the request-shaped config lambdas, the two settings email needs, why
|
|
39
|
+
polling is the default and Action Cable is opt-in, and the mistakes agents
|
|
40
|
+
actually make. It ships inside the gem, so
|
|
41
|
+
`cat "$(bundle show livechat)/AGENTS.md"` works from a host app.
|
|
42
|
+
- The dummy app pins `queue_adapter = :test` for the test suite. Attaching a
|
|
43
|
+
file enqueues Active Storage's analysis job, and the default `:async` adapter
|
|
44
|
+
runs it on a background thread with its own database connection — writes no
|
|
45
|
+
test transaction covers, which is how a suite starts failing order-dependently
|
|
46
|
+
in a test that never created a row. No effect on the gem itself.
|
|
47
|
+
|
|
3
48
|
## 0.7.1
|
|
4
49
|
|
|
5
50
|
- Removed the translucent border and background from the customer-facing
|
data/README.md
CHANGED
|
@@ -52,6 +52,11 @@ demo messages instead of duplicating conversations.
|
|
|
52
52
|
|
|
53
53
|
Ruby >= 3.2 · Rails >= 7.1 · Active Storage only if you want file attachments.
|
|
54
54
|
|
|
55
|
+
Installing with a coding agent? Point it at [AGENTS.md](AGENTS.md) — the same
|
|
56
|
+
steps in the order an agent needs them, plus the gates it tends to get wrong and
|
|
57
|
+
the things it should not do. It ships inside the gem, so
|
|
58
|
+
`cat "$(bundle show livechat)/AGENTS.md"` works from any app that bundles it.
|
|
59
|
+
|
|
55
60
|
## What you get
|
|
56
61
|
|
|
57
62
|
| | |
|
|
@@ -121,6 +126,7 @@ Everything is optional — a fresh install works with zero config. In
|
|
|
121
126
|
| Option | Default | What it does |
|
|
122
127
|
| --- | --- | --- |
|
|
123
128
|
| `authorize_agent` | development only | **Who can read the inbox.** Override before deploying |
|
|
129
|
+
| `base_controller_class` | `ActionController::Base` | Controller the inbox inherits — name your admin's and it adopts its layout, helpers and auth |
|
|
124
130
|
| `enabled` | everyone | Who sees the widget. `false` hides it and rejects posts |
|
|
125
131
|
| `current_user` | `nil` | Identify the visitor. Receives the request |
|
|
126
132
|
| `app_name` | Rails app name | Shown in the widget header |
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Livechat
|
|
4
|
+
# Who is asking — visitor or agent — and the gates that answer it.
|
|
5
|
+
#
|
|
6
|
+
# A concern rather than inherited behaviour because the engine has two
|
|
7
|
+
# controller roots: the widget's endpoints hang off ActionController::Base, and
|
|
8
|
+
# the inbox hangs off whatever the host set as `base_controller_class`.
|
|
9
|
+
module RequestContext
|
|
10
|
+
extend ActiveSupport::Concern
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def livechat_agent_layout
|
|
15
|
+
Livechat.config.agent_layout
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def current_visitor
|
|
19
|
+
return @current_visitor if defined?(@current_visitor)
|
|
20
|
+
|
|
21
|
+
@current_visitor = Livechat.config.current_user.call(request)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def current_visitor_id
|
|
25
|
+
current_visitor.respond_to?(:id) ? current_visitor.id.to_s : nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def require_enabled
|
|
29
|
+
head :forbidden unless Livechat.enabled?(request)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Server-side gate for the inbox. Default: development only.
|
|
33
|
+
def require_agent
|
|
34
|
+
return if Livechat.agent?(request)
|
|
35
|
+
|
|
36
|
+
render plain: 'Forbidden. Set Livechat.config.authorize_agent to grant access.',
|
|
37
|
+
status: :forbidden
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Replies are attributed to whoever config.current_user resolves. When an
|
|
41
|
+
# inbox is protected by something user-less (HTTP basic, a VPN), replies
|
|
42
|
+
# still work — anonymously, as "Support".
|
|
43
|
+
def current_agent_id
|
|
44
|
+
current_visitor_id || '0'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def current_agent_label
|
|
48
|
+
label = (Livechat.config.agent_label.call(current_visitor).presence if current_visitor)
|
|
49
|
+
label || I18n.t(:team, scope: :livechat, default: 'Support')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def render_rate_limited
|
|
53
|
+
message = I18n.t('livechat.error_rate_limited',
|
|
54
|
+
default: 'Too many messages. Please wait a moment and try again.')
|
|
55
|
+
render json: { errors: [message] }, status: :too_many_requests
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Guests get a permanent random token — their key to the conversation
|
|
59
|
+
# across visits. Signed-in visitors are keyed by id and only need the
|
|
60
|
+
# cookie to carry a guest history into their account.
|
|
61
|
+
def visitor_token
|
|
62
|
+
cookies[:livechat_vid]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def ensure_visitor_token
|
|
66
|
+
visitor_token.presence || begin
|
|
67
|
+
token = SecureRandom.base58(24)
|
|
68
|
+
cookies.permanent[:livechat_vid] = { value: token, httponly: true, same_site: :lax }
|
|
69
|
+
token
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# A guest who signed in keeps their thread: adopt cookie-token
|
|
74
|
+
# conversations into the account, then the cookie no longer matters.
|
|
75
|
+
def claim_guest_conversations
|
|
76
|
+
return unless current_visitor_id && visitor_token
|
|
77
|
+
|
|
78
|
+
Conversation.claim!(
|
|
79
|
+
visitor_token: visitor_token,
|
|
80
|
+
visitor_id: current_visitor_id,
|
|
81
|
+
visitor_label: Livechat.config.visitor_label.call(current_visitor).presence
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -1,80 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Livechat
|
|
4
|
+
# Root of the engine's PUBLIC surface: widget.js, the visitor API and the
|
|
5
|
+
# attachment proxy (which serves visitors as well as agents). These stay on a
|
|
6
|
+
# plain ActionController::Base deliberately — a visitor starting a chat must
|
|
7
|
+
# not be routed through a host's admin controller, which would demand a staff
|
|
8
|
+
# session for the widget.
|
|
9
|
+
#
|
|
10
|
+
# The inbox's root is DashboardController, and that is where
|
|
11
|
+
# `config.base_controller_class` applies.
|
|
4
12
|
class ApplicationController < ActionController::Base
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
private
|
|
8
|
-
|
|
9
|
-
def livechat_agent_layout
|
|
10
|
-
Livechat.config.agent_layout
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def current_visitor
|
|
14
|
-
return @current_visitor if defined?(@current_visitor)
|
|
15
|
-
|
|
16
|
-
@current_visitor = Livechat.config.current_user.call(request)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def current_visitor_id
|
|
20
|
-
current_visitor.respond_to?(:id) ? current_visitor.id.to_s : nil
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def require_enabled
|
|
24
|
-
head :forbidden unless Livechat.enabled?(request)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Server-side gate for the inbox. Default: development only.
|
|
28
|
-
def require_agent
|
|
29
|
-
return if Livechat.agent?(request)
|
|
13
|
+
include RequestContext
|
|
30
14
|
|
|
31
|
-
|
|
32
|
-
status: :forbidden
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Replies are attributed to whoever config.current_user resolves. When an
|
|
36
|
-
# inbox is protected by something user-less (HTTP basic, a VPN), replies
|
|
37
|
-
# still work — anonymously, as "Support".
|
|
38
|
-
def current_agent_id
|
|
39
|
-
current_visitor_id || '0'
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def current_agent_label
|
|
43
|
-
label = (Livechat.config.agent_label.call(current_visitor).presence if current_visitor)
|
|
44
|
-
label || I18n.t(:team, scope: :livechat, default: 'Support')
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def render_rate_limited
|
|
48
|
-
message = I18n.t('livechat.error_rate_limited',
|
|
49
|
-
default: 'Too many messages. Please wait a moment and try again.')
|
|
50
|
-
render json: { errors: [message] }, status: :too_many_requests
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Guests get a permanent random token — their key to the conversation
|
|
54
|
-
# across visits. Signed-in visitors are keyed by id and only need the
|
|
55
|
-
# cookie to carry a guest history into their account.
|
|
56
|
-
def visitor_token
|
|
57
|
-
cookies[:livechat_vid]
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def ensure_visitor_token
|
|
61
|
-
visitor_token.presence || begin
|
|
62
|
-
token = SecureRandom.base58(24)
|
|
63
|
-
cookies.permanent[:livechat_vid] = { value: token, httponly: true, same_site: :lax }
|
|
64
|
-
token
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# A guest who signed in keeps their thread: adopt cookie-token
|
|
69
|
-
# conversations into the account, then the cookie no longer matters.
|
|
70
|
-
def claim_guest_conversations
|
|
71
|
-
return unless current_visitor_id && visitor_token
|
|
72
|
-
|
|
73
|
-
Conversation.claim!(
|
|
74
|
-
visitor_token: visitor_token,
|
|
75
|
-
visitor_id: current_visitor_id,
|
|
76
|
-
visitor_label: Livechat.config.visitor_label.call(current_visitor).presence
|
|
77
|
-
)
|
|
78
|
-
end
|
|
15
|
+
protect_from_forgery with: :exception
|
|
79
16
|
end
|
|
80
17
|
end
|
|
@@ -4,10 +4,8 @@ module Livechat
|
|
|
4
4
|
# The inbox. Every action is gated by config.authorize_agent; any
|
|
5
5
|
# authorized teammate can read and answer any thread — it's a shared line,
|
|
6
6
|
# not an assignment queue.
|
|
7
|
-
class ConversationsController <
|
|
8
|
-
before_action :require_agent
|
|
7
|
+
class ConversationsController < DashboardController
|
|
9
8
|
before_action :set_conversation, except: %i[index index_poll]
|
|
10
|
-
layout :livechat_agent_layout
|
|
11
9
|
|
|
12
10
|
PER_PAGE = 50
|
|
13
11
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Livechat
|
|
4
|
+
# Root of the AGENT surface: the inbox and agent replies.
|
|
5
|
+
#
|
|
6
|
+
# Inherits from `config.base_controller_class` — by default a plain
|
|
7
|
+
# ActionController::Base, which is why `authorize_agent` exists. Point it at
|
|
8
|
+
# the controller your own admin already inherits from and the inbox picks up
|
|
9
|
+
# that stack wholesale: your layout, your helpers, your authentication, and
|
|
10
|
+
# whatever request context your before_actions establish.
|
|
11
|
+
#
|
|
12
|
+
# Only the inbox hangs off it. The widget's endpoints stay on
|
|
13
|
+
# ApplicationController, so wiring an admin base controller here can never
|
|
14
|
+
# demand a staff session from a visitor starting a chat.
|
|
15
|
+
class DashboardController < Livechat.base_controller
|
|
16
|
+
include RequestContext
|
|
17
|
+
|
|
18
|
+
# A host base controller brings its own layout, and declaring one here would
|
|
19
|
+
# override it. So the gem only claims the layout when it owns the decision:
|
|
20
|
+
# no host base controller, or a host that named an `agent_layout` explicitly.
|
|
21
|
+
layout :livechat_agent_layout unless superclass != ActionController::Base &&
|
|
22
|
+
Livechat.config.agent_layout == Configuration::DEFAULT_AGENT_LAYOUT
|
|
23
|
+
|
|
24
|
+
before_action :require_agent
|
|
25
|
+
|
|
26
|
+
# A host base controller has configured CSRF already; declaring it twice
|
|
27
|
+
# would run the check twice.
|
|
28
|
+
protect_from_forgery with: :exception if superclass == ActionController::Base
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Livechat
|
|
4
|
-
class MessagesController <
|
|
5
|
-
before_action :require_agent
|
|
6
|
-
|
|
4
|
+
class MessagesController < DashboardController
|
|
7
5
|
def create
|
|
8
6
|
conversation = Conversation.find(params[:conversation_id])
|
|
9
7
|
message = conversation.post_agent_message!(
|
|
@@ -5,19 +5,18 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<%= csrf_meta_tags %>
|
|
7
7
|
<%= csp_meta_tag %>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
keyboard submit work under strict script-src CSPs. %>
|
|
12
|
-
<%= javascript_include_tag "#{dashboard_script_path}?v=#{Livechat::Widget.dashboard_fingerprint}",
|
|
13
|
-
defer: true, nonce: true %>
|
|
8
|
+
<%# The inbox's stylesheet and script are declared by the views
|
|
9
|
+
(livechat/shared/_dashboard), not here, so they survive a host replacing
|
|
10
|
+
this layout via config.agent_layout or config.base_controller_class. %>
|
|
14
11
|
</head>
|
|
15
|
-
<body class="<%= yield(:body_class) %>">
|
|
16
|
-
<div class="
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
<body class="lvc-page <%= yield(:body_class) %>">
|
|
13
|
+
<div class="lvc-dashboard">
|
|
14
|
+
<div class="container">
|
|
15
|
+
<% if flash[:alert].present? %>
|
|
16
|
+
<p class="flash alert"><%= flash[:alert] %></p>
|
|
17
|
+
<% end %>
|
|
18
|
+
<%= yield %>
|
|
19
|
+
</div>
|
|
21
20
|
</div>
|
|
22
21
|
</body>
|
|
23
22
|
</html>
|