turbo_chat 0.1.15 → 0.2.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/CHANGELOG.md +10 -0
- data/README.md +10 -0
- data/app/assets/javascripts/turbo_chat/realtime.js +1 -14
- data/app/assets/javascripts/turbo_chat/shared.js +1 -0
- data/app/controllers/turbo_chat/application_controller.rb +1 -1
- data/app/controllers/turbo_chat/chat_memberships_controller.rb +1 -1
- data/app/controllers/turbo_chat/chat_messages_controller.rb +1 -1
- data/app/controllers/turbo_chat/chats_controller/event_payload_support.rb +2 -2
- data/app/controllers/turbo_chat/chats_controller/invitation_support.rb +1 -1
- data/app/controllers/turbo_chat/chats_controller.rb +1 -1
- data/app/helpers/turbo_chat/application_helper/config_support.rb +1 -1
- data/app/helpers/turbo_chat/application_helper/mention_support/permission_support.rb +2 -2
- data/app/helpers/turbo_chat/application_helper/message_rendering.rb +9 -0
- data/app/helpers/turbo_chat/application_helper/participant_support.rb +2 -2
- data/app/models/turbo_chat/chat.rb +4 -0
- data/app/models/turbo_chat/chat_message/blocked_words_moderation.rb +3 -3
- data/app/models/turbo_chat/chat_message/broadcasting.rb +1 -1
- data/app/models/turbo_chat/chat_message/formatting.rb +5 -1
- data/app/models/turbo_chat/chat_message/mention_validation.rb +2 -2
- data/app/models/turbo_chat/chat_message.rb +1 -1
- data/db/migrate/20260302000015_add_kind_index_to_turbo_chat_chat_messages.rb +6 -0
- data/lib/turbo_chat/messages.rb +1 -1
- data/lib/turbo_chat/moderation/support.rb +1 -1
- data/lib/turbo_chat/permission/support.rb +4 -3
- data/lib/turbo_chat/permission.rb +1 -1
- data/lib/turbo_chat/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c3fc14508fac1357a1da08a00d2cd3b905271a1fd5a18dc0ccc71e8f6a8572c7
|
|
4
|
+
data.tar.gz: dc7b1fb742ebe03541e5ab140c312d1b2ed6f27efeb0cfbd3adca266bd9f7b28
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a452a6b3ba2b13ab03382150ca9f5780b13994781011a764f54a518939a095a46dec586319b9853f5e03bc4b7b0e70fc62af9749935540fcc5525534c2c910c6
|
|
7
|
+
data.tar.gz: be545c2e1dc54a1df52fa83e8785ab36ea48c138ce40338c12ebd122e5311d771c861688b1347fe3f3b1f77c646c5f1fa7e1207fd482293c5ef89858a6b39cc4
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `turbo_chat` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.2.0] - 2026-03-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Membership lookup cache (`Chat#membership_lookup`) for efficient per-message role resolution, eliminating N+1 queries when rendering participant roles in message lists.
|
|
9
|
+
- Memoized `actor_membership` in Permission to avoid redundant database queries within a single request.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Narrowed broad `rescue StandardError` clauses to specific exception types (`NoMethodError`, `TypeError`) across controllers, helpers, models, and permission modules for better error visibility during development.
|
|
13
|
+
- Removed duplicated `containerBottomPadding` function from `realtime.js`; now imports the shared version from `shared.js`.
|
|
14
|
+
|
|
5
15
|
## [0.1.15] - 2026-02-28
|
|
6
16
|
|
|
7
17
|
### Added
|
data/README.md
CHANGED
|
@@ -149,6 +149,10 @@ end
|
|
|
149
149
|
|
|
150
150
|
Flat aliases (for example `config.max_message_length`) still work for backward compatibility.
|
|
151
151
|
|
|
152
|
+
### Rate Limiting
|
|
153
|
+
|
|
154
|
+
TurboChat does not include built-in rate limiting. For production deployments, add request throttling for message creation in your host application using middleware such as [rack-attack](https://github.com/rack/rack-attack).
|
|
155
|
+
|
|
152
156
|
## Message Ingest API
|
|
153
157
|
|
|
154
158
|
Post messages as a specific participant, including external sources like WhatsApp:
|
|
@@ -368,6 +372,12 @@ bin/rails turbo_chat:install:migrations
|
|
|
368
372
|
bin/rails db:migrate
|
|
369
373
|
```
|
|
370
374
|
|
|
375
|
+
## Known Limitations
|
|
376
|
+
|
|
377
|
+
- **Stream subscriptions after member removal.** Turbo Stream subscriptions persist until the page is reloaded. A removed member may continue to see new messages until they navigate away. This is not a security issue because stream names are cryptographically signed, but it can be surprising.
|
|
378
|
+
- **Blocked word filtering.** Word-boundary matching can be bypassed with Unicode homoglyphs, zero-width characters, or leetspeak substitutions. Treat it as a first line of defense, not a guarantee.
|
|
379
|
+
- **Invitable participants cap.** The invite picker returns at most 100 non-member participants. Applications with larger user bases should provide a custom search endpoint.
|
|
380
|
+
|
|
371
381
|
## Dependencies
|
|
372
382
|
|
|
373
383
|
- Ruby `>= 3.1`
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
var parseMentionOptions = namespace.parseMentionOptions;
|
|
16
16
|
var setupMentionAutocomplete = namespace.setupMentionAutocomplete;
|
|
17
17
|
var scrollLastMessageIntoView = namespace.scrollLastMessageIntoView;
|
|
18
|
+
var containerBottomPadding = namespace.containerBottomPadding;
|
|
18
19
|
|
|
19
20
|
function hideOwnSignals(container) {
|
|
20
21
|
if (!container) {
|
|
@@ -41,20 +42,6 @@
|
|
|
41
42
|
});
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
function containerBottomPadding(container) {
|
|
45
|
-
if (!container || typeof window === "undefined") {
|
|
46
|
-
return 0;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
var cssPadding = window.getComputedStyle(container).paddingBottom;
|
|
50
|
-
var parsedPadding = parseFloat(cssPadding);
|
|
51
|
-
if (isNaN(parsedPadding) || parsedPadding <= 0) {
|
|
52
|
-
return 0;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return parsedPadding;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
45
|
function visibleSignalNode(container) {
|
|
59
46
|
if (!container) {
|
|
60
47
|
return null;
|
|
@@ -524,6 +524,7 @@
|
|
|
524
524
|
namespace.emptyMentionAutocomplete = emptyMentionAutocomplete;
|
|
525
525
|
namespace.setupMentionAutocomplete = setupMentionAutocomplete;
|
|
526
526
|
namespace.scrollLastMessageIntoView = scrollLastMessageIntoView;
|
|
527
|
+
namespace.containerBottomPadding = containerBottomPadding;
|
|
527
528
|
namespace.signalOverlayOffset = signalOverlayOffset;
|
|
528
529
|
namespace.prefersReducedMotion = prefersReducedMotion;
|
|
529
530
|
namespace.scrollMessageIntoView = scrollMessageIntoView;
|
|
@@ -68,7 +68,7 @@ module TurboChat
|
|
|
68
68
|
configuration = TurboChat.configuration
|
|
69
69
|
value = configuration.respond_to?(:disable_input) ? configuration.disable_input : false
|
|
70
70
|
ActiveModel::Type::Boolean.new.cast(value)
|
|
71
|
-
rescue
|
|
71
|
+
rescue NoMethodError, TypeError
|
|
72
72
|
false
|
|
73
73
|
end
|
|
74
74
|
end
|
|
@@ -149,7 +149,7 @@ module TurboChat
|
|
|
149
149
|
configuration = TurboChat.configuration
|
|
150
150
|
value = configuration.respond_to?(method_name) ? configuration.public_send(method_name) : default
|
|
151
151
|
ActiveModel::Type::Boolean.new.cast(value)
|
|
152
|
-
rescue
|
|
152
|
+
rescue NoMethodError, TypeError
|
|
153
153
|
default
|
|
154
154
|
end
|
|
155
155
|
end
|
|
@@ -16,7 +16,7 @@ module TurboChat
|
|
|
16
16
|
chatTitle: symbolized_payload[:chatTitle].presence || symbolized_payload[:chat_title].presence,
|
|
17
17
|
chatMembershipId: symbolized_payload[:chatMembershipId].presence || symbolized_payload[:chat_membership_id].presence
|
|
18
18
|
}.compact
|
|
19
|
-
rescue
|
|
19
|
+
rescue NoMethodError, TypeError
|
|
20
20
|
nil
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -37,7 +37,7 @@ module TurboChat
|
|
|
37
37
|
chatTitle: symbolized_payload[:chatTitle].presence || symbolized_payload[:chat_title].presence,
|
|
38
38
|
chatMembershipId: symbolized_payload[:chatMembershipId].presence || symbolized_payload[:chat_membership_id].presence
|
|
39
39
|
}.compact
|
|
40
|
-
rescue
|
|
40
|
+
rescue NoMethodError, TypeError
|
|
41
41
|
nil
|
|
42
42
|
end
|
|
43
43
|
|
|
@@ -11,7 +11,7 @@ module TurboChat
|
|
|
11
11
|
|
|
12
12
|
current_member_ids = @chat.chat_memberships.where(removed_at: nil, participant_type: participant_type).pluck(:participant_id)
|
|
13
13
|
participant_class.where.not(id: current_member_ids).order(id: :asc).limit(100).to_a
|
|
14
|
-
rescue
|
|
14
|
+
rescue NoMethodError, TypeError
|
|
15
15
|
[]
|
|
16
16
|
end
|
|
17
17
|
|
|
@@ -101,7 +101,7 @@ module TurboChat
|
|
|
101
101
|
configuration = TurboChat.configuration
|
|
102
102
|
value = configuration.respond_to?(:show_members) ? configuration.show_members : true
|
|
103
103
|
ActiveModel::Type::Boolean.new.cast(value)
|
|
104
|
-
rescue
|
|
104
|
+
rescue NoMethodError, TypeError
|
|
105
105
|
true
|
|
106
106
|
end
|
|
107
107
|
|
|
@@ -11,7 +11,7 @@ module TurboChat
|
|
|
11
11
|
return nil if participant.nil?
|
|
12
12
|
|
|
13
13
|
TurboChat.configuration.permission_adapter.new(participant, chat)
|
|
14
|
-
rescue
|
|
14
|
+
rescue NoMethodError, TypeError, ArgumentError
|
|
15
15
|
nil
|
|
16
16
|
end
|
|
17
17
|
|
|
@@ -19,7 +19,7 @@ module TurboChat
|
|
|
19
19
|
return true unless permission.respond_to?(method_name)
|
|
20
20
|
|
|
21
21
|
permission.public_send(method_name)
|
|
22
|
-
rescue
|
|
22
|
+
rescue NoMethodError, TypeError
|
|
23
23
|
false
|
|
24
24
|
end
|
|
25
25
|
end
|
|
@@ -27,6 +27,8 @@ module TurboChat
|
|
|
27
27
|
|
|
28
28
|
def render_chat_message_body(chat_message)
|
|
29
29
|
body = chat_message.body.to_s
|
|
30
|
+
# html_safe is safe here: decorate_plain_message_text guarantees the body
|
|
31
|
+
# is html_escaped before any markup injection (see method comment above).
|
|
30
32
|
return content_tag(:p, decorate_plain_message_text(body).html_safe, class: "chat-body") unless TurboChat.configuration.render_message_html
|
|
31
33
|
|
|
32
34
|
sanitized_html = sanitize(
|
|
@@ -103,6 +105,13 @@ module TurboChat
|
|
|
103
105
|
"##{red}#{green}#{blue}#{alpha_hex}".downcase
|
|
104
106
|
end
|
|
105
107
|
|
|
108
|
+
# Safety: builds an HTML-safe string via a strict pipeline order:
|
|
109
|
+
# 1. ERB::Util.html_escape escapes ALL user content first
|
|
110
|
+
# 2. Emoji aliases and mention highlights inject only controlled markup
|
|
111
|
+
# 3. Newlines are converted to <br> tags
|
|
112
|
+
# The result is safe to mark as html_safe because no raw user content
|
|
113
|
+
# reaches the output unescaped. Any change to this pipeline MUST
|
|
114
|
+
# preserve html_escape as the first step.
|
|
106
115
|
def decorate_plain_message_text(body)
|
|
107
116
|
formatted = ERB::Util.html_escape(body.to_s)
|
|
108
117
|
formatted = apply_emoji_aliases(formatted) if TurboChat.configuration.enable_emoji_aliases
|
|
@@ -57,7 +57,7 @@ module TurboChat
|
|
|
57
57
|
return false if permission.respond_to?(:can_post_message?) && !permission.can_post_message?
|
|
58
58
|
|
|
59
59
|
own_chat_message?(chat_message, participant: participant)
|
|
60
|
-
rescue
|
|
60
|
+
rescue NoMethodError, TypeError
|
|
61
61
|
false
|
|
62
62
|
end
|
|
63
63
|
|
|
@@ -67,7 +67,7 @@ module TurboChat
|
|
|
67
67
|
return nil unless respond_to?(:current_chat_participant, true)
|
|
68
68
|
|
|
69
69
|
current_chat_participant
|
|
70
|
-
rescue
|
|
70
|
+
rescue NoMethodError, TypeError
|
|
71
71
|
nil
|
|
72
72
|
end
|
|
73
73
|
|
|
@@ -65,6 +65,10 @@ module TurboChat
|
|
|
65
65
|
relation.ordered.preload(:participant)
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
def membership_lookup
|
|
69
|
+
@membership_lookup ||= chat_memberships.active.index_by { |m| [m.participant_type, m.participant_id] }
|
|
70
|
+
end
|
|
71
|
+
|
|
68
72
|
def last_message_at
|
|
69
73
|
chat_messages.message.maximum(:created_at)
|
|
70
74
|
end
|
|
@@ -57,7 +57,7 @@ module TurboChat
|
|
|
57
57
|
return [] unless configuration.respond_to?(:effective_blocked_words)
|
|
58
58
|
|
|
59
59
|
Array(configuration.effective_blocked_words)
|
|
60
|
-
rescue
|
|
60
|
+
rescue NoMethodError, TypeError
|
|
61
61
|
[]
|
|
62
62
|
end
|
|
63
63
|
|
|
@@ -66,7 +66,7 @@ module TurboChat
|
|
|
66
66
|
return "reject" unless configuration.respond_to?(:effective_blocked_words_action)
|
|
67
67
|
|
|
68
68
|
configuration.effective_blocked_words_action.to_s
|
|
69
|
-
rescue
|
|
69
|
+
rescue NoMethodError, TypeError
|
|
70
70
|
"reject"
|
|
71
71
|
end
|
|
72
72
|
|
|
@@ -92,7 +92,7 @@ module TurboChat
|
|
|
92
92
|
return false unless configuration.respond_to?(:emit_blocked_words_events)
|
|
93
93
|
|
|
94
94
|
ActiveModel::Type::Boolean.new.cast(configuration.emit_blocked_words_events)
|
|
95
|
-
rescue
|
|
95
|
+
rescue NoMethodError, TypeError
|
|
96
96
|
false
|
|
97
97
|
end
|
|
98
98
|
|
|
@@ -80,7 +80,7 @@ module TurboChat
|
|
|
80
80
|
value = configuration.respond_to?(:message_insert_position) ? configuration.message_insert_position : "append_end"
|
|
81
81
|
normalized = value.to_s.strip.downcase
|
|
82
82
|
%w[append_start start prepend].include?(normalized)
|
|
83
|
-
rescue
|
|
83
|
+
rescue NoMethodError, TypeError
|
|
84
84
|
false
|
|
85
85
|
end
|
|
86
86
|
end
|
|
@@ -46,7 +46,11 @@ module TurboChat
|
|
|
46
46
|
def participant_membership
|
|
47
47
|
return @participant_membership if instance_variable_defined?(:@participant_membership)
|
|
48
48
|
|
|
49
|
-
@participant_membership = chat.
|
|
49
|
+
@participant_membership = if chat.respond_to?(:membership_lookup) && chat.instance_variable_defined?(:@membership_lookup)
|
|
50
|
+
chat.membership_lookup[[participant_type, participant_id]]
|
|
51
|
+
else
|
|
52
|
+
chat.chat_memberships.active.find_by(participant: participant)
|
|
53
|
+
end
|
|
50
54
|
end
|
|
51
55
|
|
|
52
56
|
def formatted_time_for(timestamp)
|
|
@@ -36,7 +36,7 @@ module TurboChat
|
|
|
36
36
|
return nil unless adapter.respond_to?(:new)
|
|
37
37
|
|
|
38
38
|
adapter.new(participant, chat)
|
|
39
|
-
rescue
|
|
39
|
+
rescue NoMethodError, TypeError, ArgumentError
|
|
40
40
|
nil
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -53,7 +53,7 @@ module TurboChat
|
|
|
53
53
|
else
|
|
54
54
|
permission_gate_allowed?(permission, :can_mention_members?)
|
|
55
55
|
end
|
|
56
|
-
rescue
|
|
56
|
+
rescue NoMethodError, TypeError
|
|
57
57
|
false
|
|
58
58
|
end
|
|
59
59
|
|
data/lib/turbo_chat/messages.rb
CHANGED
|
@@ -125,7 +125,7 @@ module TurboChat
|
|
|
125
125
|
adapter.new(participant, chat)
|
|
126
126
|
rescue AuthorizationError
|
|
127
127
|
raise
|
|
128
|
-
rescue
|
|
128
|
+
rescue NoMethodError, TypeError, ArgumentError => error
|
|
129
129
|
raise AuthorizationError, "Unable to authorize message posting: #{error.message}"
|
|
130
130
|
end
|
|
131
131
|
private_class_method :permission_for
|
|
@@ -36,10 +36,11 @@ class TurboChat::Permission
|
|
|
36
36
|
def target_role_rank(target_membership) = target_membership&.effective_role_rank || -1
|
|
37
37
|
|
|
38
38
|
def actor_membership
|
|
39
|
-
return
|
|
40
|
-
return nil unless participant_present?
|
|
39
|
+
return @actor_membership if defined?(@actor_membership)
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
@actor_membership = if chat_present? && participant_present?
|
|
42
|
+
chat.chat_memberships.active.find_by(participant: participant)
|
|
43
|
+
end
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
def participant_present? = !participant.nil?
|
|
@@ -73,7 +73,7 @@ class TurboChat::Permission
|
|
|
73
73
|
configuration = TurboChat.configuration
|
|
74
74
|
value = configuration.respond_to?(:disable_input) ? configuration.disable_input : false
|
|
75
75
|
ActiveModel::Type::Boolean.new.cast(value)
|
|
76
|
-
rescue
|
|
76
|
+
rescue NoMethodError, TypeError
|
|
77
77
|
false
|
|
78
78
|
end
|
|
79
79
|
end
|
data/lib/turbo_chat/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: turbo_chat
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alexander Haumer
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02
|
|
11
|
+
date: 2026-03-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -113,6 +113,7 @@ files:
|
|
|
113
113
|
- db/migrate/20260218000012_add_custom_role_key_to_chat_memberships.rb
|
|
114
114
|
- db/migrate/20260218000013_add_invitation_accepted_to_turbo_chat_chat_memberships.rb
|
|
115
115
|
- db/migrate/20260223000014_add_source_fields_to_turbo_chat_chat_messages.rb
|
|
116
|
+
- db/migrate/20260302000015_add_kind_index_to_turbo_chat_chat_messages.rb
|
|
116
117
|
- lib/generators/turbo_chat/install/install_generator.rb
|
|
117
118
|
- lib/generators/turbo_chat/install/templates/turbo_chat.rb
|
|
118
119
|
- lib/tasks/turbo_chat_tasks.rake
|