turbo_chat 0.1.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 +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +741 -0
- data/app/assets/config/chat_gem_manifest.js +6 -0
- data/app/assets/javascripts/chat_gem/application.js +4 -0
- data/app/assets/javascripts/chat_gem/lifecycle_events.js +93 -0
- data/app/assets/javascripts/chat_gem/messages.js +442 -0
- data/app/assets/javascripts/chat_gem/realtime.js +398 -0
- data/app/assets/javascripts/chat_gem/shared.js +488 -0
- data/app/assets/stylesheets/chat_gem/application.css +741 -0
- data/app/controllers/chat_gem/application_controller.rb +41 -0
- data/app/controllers/chat_gem/chat_memberships_controller.rb +81 -0
- data/app/controllers/chat_gem/chat_messages_controller.rb +144 -0
- data/app/controllers/chat_gem/chats_controller/event_payload_support.rb +58 -0
- data/app/controllers/chat_gem/chats_controller/invitation_support.rb +31 -0
- data/app/controllers/chat_gem/chats_controller.rb +125 -0
- data/app/helpers/chat_gem/application_helper/config_support.rb +41 -0
- data/app/helpers/chat_gem/application_helper/mention_support/entry_builder.rb +55 -0
- data/app/helpers/chat_gem/application_helper/mention_support/permission_support.rb +28 -0
- data/app/helpers/chat_gem/application_helper/mention_support/token_builder.rb +49 -0
- data/app/helpers/chat_gem/application_helper/mention_support.rb +80 -0
- data/app/helpers/chat_gem/application_helper/message_rendering.rb +165 -0
- data/app/helpers/chat_gem/application_helper/participant_support.rb +81 -0
- data/app/helpers/chat_gem/application_helper.rb +12 -0
- data/app/models/chat_gem/application_record.rb +5 -0
- data/app/models/chat_gem/chat.rb +127 -0
- data/app/models/chat_gem/chat_membership.rb +136 -0
- data/app/models/chat_gem/chat_message/blocked_words_moderation.rb +120 -0
- data/app/models/chat_gem/chat_message/body_length_validation.rb +20 -0
- data/app/models/chat_gem/chat_message/broadcasting.rb +61 -0
- data/app/models/chat_gem/chat_message/formatting.rb +81 -0
- data/app/models/chat_gem/chat_message/mention_validation.rb +85 -0
- data/app/models/chat_gem/chat_message/signals.rb +61 -0
- data/app/models/chat_gem/chat_message.rb +40 -0
- data/app/views/chat_gem/chat_messages/_chat_message.html.erb +1 -0
- data/app/views/chat_gem/chat_messages/_form.html.erb +22 -0
- data/app/views/chat_gem/chat_messages/_message.html.erb +83 -0
- data/app/views/chat_gem/chat_messages/_signal.html.erb +3 -0
- data/app/views/chat_gem/chat_messages/_signals.html.erb +24 -0
- data/app/views/chat_gem/chat_messages/index.html.erb +1 -0
- data/app/views/chat_gem/chats/index.html.erb +51 -0
- data/app/views/chat_gem/chats/new.html.erb +13 -0
- data/app/views/chat_gem/chats/show.html.erb +95 -0
- data/app/views/layouts/chat_gem/application.html.erb +20 -0
- data/config/routes.rb +16 -0
- data/db/migrate/20260215000000_create_chat_gem_chats.rb +8 -0
- data/db/migrate/20260215000001_create_chat_gem_chat_memberships.rb +19 -0
- data/db/migrate/20260215000002_create_chat_gem_chat_messages.rb +14 -0
- data/db/migrate/20260218000011_add_closed_at_to_chat_gem_chats.rb +6 -0
- data/db/migrate/20260218000012_add_custom_role_key_to_chat_memberships.rb +6 -0
- data/db/migrate/20260218000013_add_invitation_accepted_to_chat_gem_chat_memberships.rb +5 -0
- data/lib/chat_gem/configuration.rb +242 -0
- data/lib/chat_gem/engine.rb +29 -0
- data/lib/chat_gem/model_extensions/chat_participant.rb +45 -0
- data/lib/chat_gem/moderation.rb +194 -0
- data/lib/chat_gem/permission.rb +193 -0
- data/lib/chat_gem/signals.rb +26 -0
- data/lib/chat_gem/version.rb +3 -0
- data/lib/chat_gem.rb +24 -0
- data/lib/generators/chat_gem/install/install_generator.rb +18 -0
- data/lib/generators/chat_gem/install/templates/chat_gem.rb +36 -0
- data/lib/generators/turbo_chat/install/install_generator.rb +18 -0
- data/lib/generators/turbo_chat/install/templates/turbo_chat.rb +36 -0
- data/lib/tasks/chat_gem_tasks.rake +1 -0
- data/lib/tasks/turbo_chat_tasks.rake +10 -0
- data/lib/turbo_chat/version.rb +5 -0
- data/lib/turbo_chat.rb +24 -0
- data/turbo_chat.gemspec +31 -0
- metadata +155 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
module ChatGem
|
|
2
|
+
module Moderation
|
|
3
|
+
class AuthorizationError < StandardError; end
|
|
4
|
+
class InvalidActionError < StandardError; end
|
|
5
|
+
|
|
6
|
+
class << self
|
|
7
|
+
def mute_member!(actor:, membership:)
|
|
8
|
+
updated_membership = authorize_and_update_membership!(
|
|
9
|
+
actor: actor,
|
|
10
|
+
membership: membership,
|
|
11
|
+
action: :can_mute_member?,
|
|
12
|
+
attributes: { muted: true }
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
emit_moderation_event("chat_gem.moderation.member_muted", actor: actor, membership: updated_membership)
|
|
16
|
+
updated_membership
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def unmute_member!(actor:, membership:)
|
|
20
|
+
updated_membership = authorize_and_update_membership!(
|
|
21
|
+
actor: actor,
|
|
22
|
+
membership: membership,
|
|
23
|
+
action: :can_mute_member?,
|
|
24
|
+
attributes: { muted: false }
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
emit_moderation_event("chat_gem.moderation.member_unmuted", actor: actor, membership: updated_membership)
|
|
28
|
+
updated_membership
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def timeout_member!(actor:, membership:, until_time:)
|
|
32
|
+
authorize_member_action!(actor: actor, membership: membership, action: :can_timeout_member?)
|
|
33
|
+
raise InvalidActionError, "timeout must be in the future" if until_time.nil? || until_time <= Time.current
|
|
34
|
+
|
|
35
|
+
membership.update!(timed_out_until: until_time)
|
|
36
|
+
emit_moderation_event(
|
|
37
|
+
"chat_gem.moderation.member_timed_out",
|
|
38
|
+
actor: actor,
|
|
39
|
+
membership: membership,
|
|
40
|
+
extra: { timed_out_until: membership.timed_out_until }
|
|
41
|
+
)
|
|
42
|
+
membership
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def clear_timeout!(actor:, membership:)
|
|
46
|
+
updated_membership = authorize_and_update_membership!(
|
|
47
|
+
actor: actor,
|
|
48
|
+
membership: membership,
|
|
49
|
+
action: :can_timeout_member?,
|
|
50
|
+
attributes: { timed_out_until: nil }
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
emit_moderation_event("chat_gem.moderation.member_timeout_cleared", actor: actor, membership: updated_membership)
|
|
54
|
+
updated_membership
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def ban_member!(actor:, membership:)
|
|
58
|
+
updated_membership = authorize_and_update_membership!(
|
|
59
|
+
actor: actor,
|
|
60
|
+
membership: membership,
|
|
61
|
+
action: :can_ban_member?,
|
|
62
|
+
attributes: {
|
|
63
|
+
removed_at: Time.current,
|
|
64
|
+
muted: false,
|
|
65
|
+
timed_out_until: nil
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
emit_moderation_event("chat_gem.moderation.member_banned", actor: actor, membership: updated_membership)
|
|
70
|
+
updated_membership
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def delete_message!(actor:, message:)
|
|
74
|
+
permission = permission_for(actor, message.chat)
|
|
75
|
+
authorize_permission!(permission.can_delete_message?(message), "Not allowed to delete message")
|
|
76
|
+
|
|
77
|
+
payload = moderation_message_payload(message)
|
|
78
|
+
message.destroy!
|
|
79
|
+
emit_moderation_event("chat_gem.moderation.message_deleted", actor: actor, payload: payload)
|
|
80
|
+
true
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def close_chat!(actor:, chat:)
|
|
84
|
+
authorize_chat_action!(actor: actor, chat: chat, gate: :can_close_chat?, error_message: "Not allowed to close chat")
|
|
85
|
+
chat.close!
|
|
86
|
+
emit_moderation_event("chat_gem.moderation.chat_closed", actor: actor, payload: moderation_chat_payload(chat))
|
|
87
|
+
chat
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def reopen_chat!(actor:, chat:)
|
|
91
|
+
authorize_chat_action!(actor: actor, chat: chat, gate: :can_reopen_chat?, error_message: "Not allowed to reopen chat")
|
|
92
|
+
chat.reopen!
|
|
93
|
+
emit_moderation_event("chat_gem.moderation.chat_reopened", actor: actor, payload: moderation_chat_payload(chat))
|
|
94
|
+
chat
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def authorize_member_action!(actor:, membership:, action:)
|
|
100
|
+
raise InvalidActionError, "membership is required" if membership.nil?
|
|
101
|
+
|
|
102
|
+
permission = permission_for(actor, membership.chat)
|
|
103
|
+
return if permission.public_send(action, membership)
|
|
104
|
+
|
|
105
|
+
raise AuthorizationError, "Not allowed to #{action.to_s.delete_prefix('can_').delete_suffix('?').tr('_', ' ')}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def authorize_and_update_membership!(actor:, membership:, action:, attributes:)
|
|
109
|
+
authorize_member_action!(actor: actor, membership: membership, action: action)
|
|
110
|
+
membership.update!(attributes)
|
|
111
|
+
membership
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def authorize_chat_action!(actor:, chat:, gate:, error_message:)
|
|
115
|
+
permission = permission_for(actor, chat)
|
|
116
|
+
authorize_permission!(permission.public_send(gate), error_message)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def authorize_permission!(allowed, error_message)
|
|
120
|
+
raise AuthorizationError, error_message unless allowed
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def permission_for(actor, chat)
|
|
124
|
+
ChatGem.configuration.permission_adapter.new(actor, chat)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def emit_moderation_event(name, actor:, membership: nil, payload: nil, extra: {})
|
|
128
|
+
return unless moderation_events_enabled?
|
|
129
|
+
return unless defined?(ActiveSupport::Notifications)
|
|
130
|
+
|
|
131
|
+
base_payload =
|
|
132
|
+
if payload.present?
|
|
133
|
+
payload
|
|
134
|
+
else
|
|
135
|
+
moderation_membership_payload(membership)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
ActiveSupport::Notifications.instrument(
|
|
139
|
+
name,
|
|
140
|
+
base_payload.merge(actor_payload(actor)).merge(extra)
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def moderation_events_enabled?
|
|
145
|
+
config = ChatGem.configuration
|
|
146
|
+
return false unless config.respond_to?(:emit_moderation_events)
|
|
147
|
+
|
|
148
|
+
ActiveModel::Type::Boolean.new.cast(config.emit_moderation_events)
|
|
149
|
+
rescue StandardError
|
|
150
|
+
false
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def moderation_membership_payload(membership)
|
|
154
|
+
return {} if membership.nil?
|
|
155
|
+
|
|
156
|
+
{
|
|
157
|
+
chat_id: membership.chat_id,
|
|
158
|
+
membership_id: membership.id,
|
|
159
|
+
participant_type: membership.participant_type,
|
|
160
|
+
participant_id: membership.participant_id
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def moderation_message_payload(message)
|
|
165
|
+
return {} if message.nil?
|
|
166
|
+
|
|
167
|
+
{
|
|
168
|
+
chat_id: message.chat_id,
|
|
169
|
+
message_id: message.id,
|
|
170
|
+
participant_type: message.participant_type,
|
|
171
|
+
participant_id: message.participant_id
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def moderation_chat_payload(chat)
|
|
176
|
+
return {} if chat.nil?
|
|
177
|
+
|
|
178
|
+
{
|
|
179
|
+
chat_id: chat.id,
|
|
180
|
+
closed_at: chat.closed_at
|
|
181
|
+
}
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def actor_payload(actor)
|
|
185
|
+
return { actor_type: nil, actor_id: nil } if actor.nil?
|
|
186
|
+
|
|
187
|
+
{
|
|
188
|
+
actor_type: actor.class.base_class.name,
|
|
189
|
+
actor_id: actor.id
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
module ChatGem
|
|
2
|
+
class Permission
|
|
3
|
+
ROLE_MENTION_TOKEN_PATTERN = /\A@[A-Z][A-Z0-9_]{0,31}\z/.freeze
|
|
4
|
+
MEMBER_MENTION_TOKEN_PATTERN = /\A@[a-z0-9_]{1,32}\z/i.freeze
|
|
5
|
+
|
|
6
|
+
attr_reader :participant, :chat
|
|
7
|
+
|
|
8
|
+
def initialize(participant, chat = nil)
|
|
9
|
+
@participant = participant
|
|
10
|
+
@chat = chat
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def can_view_chat?
|
|
14
|
+
return false unless participant_present?
|
|
15
|
+
return true unless chat_present?
|
|
16
|
+
return false unless actor_membership_active?
|
|
17
|
+
|
|
18
|
+
role_permission?(:view_chat)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def can_create_chat?
|
|
22
|
+
participant_present?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def can_post_message?
|
|
26
|
+
return false unless can_view_chat?
|
|
27
|
+
return false unless role_permission?(:post_message)
|
|
28
|
+
return false if chat_closed?
|
|
29
|
+
return false if actor_membership_muted?
|
|
30
|
+
return false if actor_membership_timed_out?
|
|
31
|
+
|
|
32
|
+
true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def can_mute_member?(target_membership = nil)
|
|
36
|
+
can_moderate_member?(:mute_member, target_membership)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def can_timeout_member?(target_membership = nil)
|
|
40
|
+
can_moderate_member?(:timeout_member, target_membership)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def can_ban_member?(target_membership = nil)
|
|
44
|
+
can_moderate_member?(:ban_member, target_membership)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def can_invite_member?
|
|
48
|
+
can_view_chat? && role_permission?(:invite_member)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def can_delete_message?(message = nil)
|
|
52
|
+
return false unless can_view_chat?
|
|
53
|
+
return false unless role_permission?(:delete_message)
|
|
54
|
+
return true if message.nil?
|
|
55
|
+
return false unless message_in_chat?(message)
|
|
56
|
+
|
|
57
|
+
target_membership = membership_for(message.participant)
|
|
58
|
+
can_act_on_target_membership?(target_membership)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def can_edit_message?(message = nil)
|
|
62
|
+
return false unless can_post_message?
|
|
63
|
+
return true if message.nil?
|
|
64
|
+
return false unless message_in_chat?(message)
|
|
65
|
+
|
|
66
|
+
participant_type, participant_id = actor_identity
|
|
67
|
+
return false if participant_type.blank? || participant_id.blank?
|
|
68
|
+
|
|
69
|
+
message.participant_type.to_s == participant_type &&
|
|
70
|
+
message.participant_id.to_s == participant_id.to_s
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def can_close_chat?
|
|
74
|
+
can_view_chat? && role_permission?(:close_chat)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def can_reopen_chat?
|
|
78
|
+
can_view_chat? && role_permission?(:reopen_chat)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def can_mention_members?
|
|
82
|
+
can_view_chat? && role_permission?(:mention_member)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def can_mention_all?
|
|
86
|
+
can_view_chat? && role_permission?(:mention_all)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def can_mention_roles?
|
|
90
|
+
can_view_chat? && role_permission?(:mention_role)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def can_mention_token?(token)
|
|
94
|
+
mention = token.to_s.strip
|
|
95
|
+
return false if mention.blank?
|
|
96
|
+
return false unless mention.start_with?("@")
|
|
97
|
+
|
|
98
|
+
return can_mention_all? if mention.casecmp("@all").zero?
|
|
99
|
+
return can_mention_roles? if ROLE_MENTION_TOKEN_PATTERN.match?(mention)
|
|
100
|
+
return can_mention_members? if MEMBER_MENTION_TOKEN_PATTERN.match?(mention)
|
|
101
|
+
|
|
102
|
+
false
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def can_moderate_member?(permission, target_membership)
|
|
108
|
+
return false unless can_view_chat?
|
|
109
|
+
return false unless role_permission?(permission)
|
|
110
|
+
return true if target_membership.nil?
|
|
111
|
+
return false unless membership_in_chat?(target_membership)
|
|
112
|
+
return false unless target_membership.active?
|
|
113
|
+
|
|
114
|
+
can_act_on_target_membership?(target_membership)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def can_act_on_target_membership?(target_membership)
|
|
118
|
+
return true if target_membership.nil?
|
|
119
|
+
return false unless actor_membership_active?
|
|
120
|
+
return false if target_membership.participant == participant
|
|
121
|
+
|
|
122
|
+
actor_rank > target_role_rank(target_membership)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def membership_for(target_participant)
|
|
126
|
+
return nil unless chat_present?
|
|
127
|
+
return nil if target_participant.nil?
|
|
128
|
+
|
|
129
|
+
chat.chat_memberships.active.find_by(participant: target_participant)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def role_permission?(permission)
|
|
133
|
+
role_permissions.include?(permission.to_sym)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def role_permissions
|
|
137
|
+
actor_membership&.effective_role_permissions || []
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def actor_rank
|
|
141
|
+
actor_membership&.effective_role_rank || -1
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def target_role_rank(target_membership)
|
|
145
|
+
target_membership&.effective_role_rank || -1
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def actor_membership
|
|
149
|
+
return nil unless chat_present?
|
|
150
|
+
return nil unless participant_present?
|
|
151
|
+
|
|
152
|
+
chat.chat_memberships.active.find_by(participant: participant)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def participant_present?
|
|
156
|
+
!participant.nil?
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def chat_present?
|
|
160
|
+
!chat.nil?
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def actor_identity
|
|
164
|
+
participant_type = participant.class.base_class.name
|
|
165
|
+
participant_id = participant.id
|
|
166
|
+
[participant_type, participant_id]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def actor_membership_active?
|
|
170
|
+
actor_membership&.active?
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def actor_membership_muted?
|
|
174
|
+
actor_membership&.muted?
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def actor_membership_timed_out?
|
|
178
|
+
actor_membership&.timed_out_until&.future?
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def chat_closed?
|
|
182
|
+
chat&.closed?
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def message_in_chat?(message)
|
|
186
|
+
message.chat_id == chat&.id
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def membership_in_chat?(target_membership)
|
|
190
|
+
target_membership.chat_id == chat&.id
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module ChatGem
|
|
2
|
+
module Signals
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def start!(chat:, participant:, signal_type: :typing)
|
|
6
|
+
ChatGem::ChatMessage.replace_signal!(chat: chat, participant: participant, signal_type: signal_type)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def replace!(chat:, participant:, signal_type: :typing)
|
|
10
|
+
ChatGem::ChatMessage.replace_signal!(chat: chat, participant: participant, signal_type: signal_type)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def clear!(chat:, participant:)
|
|
14
|
+
ChatGem::ChatMessage.clear_signals!(chat: chat, participant: participant)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def with(chat:, participant:, signal_type: :typing, &block)
|
|
18
|
+
ChatGem::ChatMessage.with_signal(
|
|
19
|
+
chat: chat,
|
|
20
|
+
participant: participant,
|
|
21
|
+
signal_type: signal_type,
|
|
22
|
+
&block
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/chat_gem.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require "chat_gem/version"
|
|
2
|
+
require "turbo-rails"
|
|
3
|
+
require "chat_gem/configuration"
|
|
4
|
+
require "chat_gem/model_extensions/chat_participant"
|
|
5
|
+
require "chat_gem/permission"
|
|
6
|
+
require "chat_gem/moderation"
|
|
7
|
+
require "chat_gem/signals"
|
|
8
|
+
require "chat_gem/engine"
|
|
9
|
+
|
|
10
|
+
module ChatGem
|
|
11
|
+
class << self
|
|
12
|
+
def configuration
|
|
13
|
+
@configuration ||= ChatGem::Configuration.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def configure
|
|
17
|
+
yield(configuration)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.table_name_prefix
|
|
22
|
+
"chat_gem_"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require "rails/generators/base"
|
|
2
|
+
|
|
3
|
+
module ChatGem
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
desc "Installs ChatGem initializer and copies engine migrations"
|
|
8
|
+
|
|
9
|
+
def copy_initializer
|
|
10
|
+
template "chat_gem.rb", "config/initializers/chat_gem.rb"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def install_migrations
|
|
14
|
+
rake "railties:install:migrations FROM=chat_gem"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
ChatGem.configure do |config|
|
|
2
|
+
config.permission_adapter = ChatGem::Permission
|
|
3
|
+
config.max_chat_participants = 10
|
|
4
|
+
config.max_message_length = 1000
|
|
5
|
+
config.message_history_limit = 200
|
|
6
|
+
config.enable_mentions = true
|
|
7
|
+
config.mention_filter_exclude_self = true
|
|
8
|
+
config.mention_filter_hide_roles = true
|
|
9
|
+
config.enable_emoji_aliases = true
|
|
10
|
+
config.emoji_aliases = ChatGem::Configuration::DEFAULT_EMOJI_ALIASES.dup
|
|
11
|
+
config.blocked_words = []
|
|
12
|
+
config.blocked_words_action = :reject
|
|
13
|
+
config.mention_mark_hex_color = nil
|
|
14
|
+
config.mention_highlight_hex_color = nil
|
|
15
|
+
config.own_message_hex_color = nil
|
|
16
|
+
config.other_message_hex_color = nil
|
|
17
|
+
config.role_message_hex_colors = {}
|
|
18
|
+
config.show_timestamp = true
|
|
19
|
+
config.show_role = false
|
|
20
|
+
config.active_chat_window = 5.minutes
|
|
21
|
+
config.emit_typing_events = false
|
|
22
|
+
config.emit_message_events = false
|
|
23
|
+
config.emit_mention_events = false
|
|
24
|
+
config.emit_invitation_events = false
|
|
25
|
+
config.emit_chat_lifecycle_events = false
|
|
26
|
+
config.emit_moderation_events = false
|
|
27
|
+
config.emit_blocked_words_events = false
|
|
28
|
+
config.show_self_signals = false
|
|
29
|
+
config.replace_signals_on_message_submit = false
|
|
30
|
+
config.message_css_class_resolver = nil
|
|
31
|
+
config.render_message_html = false
|
|
32
|
+
config.message_html_tags = %w[a b br code em i li ol p pre strong ul]
|
|
33
|
+
config.message_html_attributes = %w[href target rel class]
|
|
34
|
+
config.timestamp_formatter = ->(timestamp, _chat_message) { I18n.l(timestamp.in_time_zone, format: :long) }
|
|
35
|
+
config.role_formatter = ->(role, _chat_message) { role.to_s.humanize }
|
|
36
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require "rails/generators/base"
|
|
2
|
+
|
|
3
|
+
module TurboChat
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
desc "Installs TurboChat initializer and copies engine migrations"
|
|
8
|
+
|
|
9
|
+
def copy_initializer
|
|
10
|
+
template "turbo_chat.rb", "config/initializers/turbo_chat.rb"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def install_migrations
|
|
14
|
+
rake "railties:install:migrations FROM=chat_gem"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
TurboChat.configure do |config|
|
|
2
|
+
config.permission_adapter = TurboChat::Permission
|
|
3
|
+
config.max_chat_participants = 10
|
|
4
|
+
config.max_message_length = 1000
|
|
5
|
+
config.message_history_limit = 200
|
|
6
|
+
config.enable_mentions = true
|
|
7
|
+
config.mention_filter_exclude_self = true
|
|
8
|
+
config.mention_filter_hide_roles = true
|
|
9
|
+
config.enable_emoji_aliases = true
|
|
10
|
+
config.emoji_aliases = TurboChat::Configuration::DEFAULT_EMOJI_ALIASES.dup
|
|
11
|
+
config.blocked_words = []
|
|
12
|
+
config.blocked_words_action = :reject
|
|
13
|
+
config.mention_mark_hex_color = nil
|
|
14
|
+
config.mention_highlight_hex_color = nil
|
|
15
|
+
config.own_message_hex_color = nil
|
|
16
|
+
config.other_message_hex_color = nil
|
|
17
|
+
config.role_message_hex_colors = {}
|
|
18
|
+
config.show_timestamp = true
|
|
19
|
+
config.show_role = false
|
|
20
|
+
config.active_chat_window = 5.minutes
|
|
21
|
+
config.emit_typing_events = false
|
|
22
|
+
config.emit_message_events = false
|
|
23
|
+
config.emit_mention_events = false
|
|
24
|
+
config.emit_invitation_events = false
|
|
25
|
+
config.emit_chat_lifecycle_events = false
|
|
26
|
+
config.emit_moderation_events = false
|
|
27
|
+
config.emit_blocked_words_events = false
|
|
28
|
+
config.show_self_signals = false
|
|
29
|
+
config.replace_signals_on_message_submit = false
|
|
30
|
+
config.message_css_class_resolver = nil
|
|
31
|
+
config.render_message_html = false
|
|
32
|
+
config.message_html_tags = %w[a b br code em i li ol p pre strong ul]
|
|
33
|
+
config.message_html_attributes = %w[href target rel class]
|
|
34
|
+
config.timestamp_formatter = ->(timestamp, _chat_message) { I18n.l(timestamp.in_time_zone, format: :long) }
|
|
35
|
+
config.role_formatter = ->(role, _chat_message) { role.to_s.humanize }
|
|
36
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Add engine-specific tasks here.
|
data/lib/turbo_chat.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require_relative "chat_gem"
|
|
2
|
+
require_relative "turbo_chat/version"
|
|
3
|
+
|
|
4
|
+
module TurboChat
|
|
5
|
+
class << self
|
|
6
|
+
def configuration
|
|
7
|
+
ChatGem.configuration
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def configure(&block)
|
|
11
|
+
ChatGem.configure(&block)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def table_name_prefix
|
|
15
|
+
ChatGem.table_name_prefix
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def const_missing(name)
|
|
19
|
+
return ChatGem.const_get(name) if ChatGem.const_defined?(name)
|
|
20
|
+
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/turbo_chat.gemspec
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require_relative "lib/turbo_chat/version"
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "turbo_chat"
|
|
5
|
+
spec.version = TurboChat::VERSION
|
|
6
|
+
spec.authors = ["Alexander Haumer"]
|
|
7
|
+
spec.homepage = "https://github.com/Haumer/ruby_llm_chat"
|
|
8
|
+
spec.summary = "Lightweight mountable chat engine for Rails"
|
|
9
|
+
spec.description = "A mountable Rails engine with chats, messages, memberships, and Turbo Stream updates."
|
|
10
|
+
spec.license = "MIT"
|
|
11
|
+
|
|
12
|
+
spec.files = Dir.chdir(__dir__) do
|
|
13
|
+
Dir[
|
|
14
|
+
"{app,config,db,lib}/**/*",
|
|
15
|
+
"MIT-LICENSE",
|
|
16
|
+
"README.md",
|
|
17
|
+
"turbo_chat.gemspec"
|
|
18
|
+
]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
spec.required_ruby_version = ">= 3.1"
|
|
22
|
+
|
|
23
|
+
spec.add_dependency "rails", ">= 7.0", "< 8.0"
|
|
24
|
+
spec.add_dependency "turbo-rails", ">= 1.4", "< 3.0"
|
|
25
|
+
|
|
26
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
27
|
+
spec.metadata["source_code_uri"] = "#{spec.homepage}/tree/main"
|
|
28
|
+
spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
|
|
29
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/README.md"
|
|
30
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
31
|
+
end
|