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.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README.md +741 -0
  4. data/app/assets/config/chat_gem_manifest.js +6 -0
  5. data/app/assets/javascripts/chat_gem/application.js +4 -0
  6. data/app/assets/javascripts/chat_gem/lifecycle_events.js +93 -0
  7. data/app/assets/javascripts/chat_gem/messages.js +442 -0
  8. data/app/assets/javascripts/chat_gem/realtime.js +398 -0
  9. data/app/assets/javascripts/chat_gem/shared.js +488 -0
  10. data/app/assets/stylesheets/chat_gem/application.css +741 -0
  11. data/app/controllers/chat_gem/application_controller.rb +41 -0
  12. data/app/controllers/chat_gem/chat_memberships_controller.rb +81 -0
  13. data/app/controllers/chat_gem/chat_messages_controller.rb +144 -0
  14. data/app/controllers/chat_gem/chats_controller/event_payload_support.rb +58 -0
  15. data/app/controllers/chat_gem/chats_controller/invitation_support.rb +31 -0
  16. data/app/controllers/chat_gem/chats_controller.rb +125 -0
  17. data/app/helpers/chat_gem/application_helper/config_support.rb +41 -0
  18. data/app/helpers/chat_gem/application_helper/mention_support/entry_builder.rb +55 -0
  19. data/app/helpers/chat_gem/application_helper/mention_support/permission_support.rb +28 -0
  20. data/app/helpers/chat_gem/application_helper/mention_support/token_builder.rb +49 -0
  21. data/app/helpers/chat_gem/application_helper/mention_support.rb +80 -0
  22. data/app/helpers/chat_gem/application_helper/message_rendering.rb +165 -0
  23. data/app/helpers/chat_gem/application_helper/participant_support.rb +81 -0
  24. data/app/helpers/chat_gem/application_helper.rb +12 -0
  25. data/app/models/chat_gem/application_record.rb +5 -0
  26. data/app/models/chat_gem/chat.rb +127 -0
  27. data/app/models/chat_gem/chat_membership.rb +136 -0
  28. data/app/models/chat_gem/chat_message/blocked_words_moderation.rb +120 -0
  29. data/app/models/chat_gem/chat_message/body_length_validation.rb +20 -0
  30. data/app/models/chat_gem/chat_message/broadcasting.rb +61 -0
  31. data/app/models/chat_gem/chat_message/formatting.rb +81 -0
  32. data/app/models/chat_gem/chat_message/mention_validation.rb +85 -0
  33. data/app/models/chat_gem/chat_message/signals.rb +61 -0
  34. data/app/models/chat_gem/chat_message.rb +40 -0
  35. data/app/views/chat_gem/chat_messages/_chat_message.html.erb +1 -0
  36. data/app/views/chat_gem/chat_messages/_form.html.erb +22 -0
  37. data/app/views/chat_gem/chat_messages/_message.html.erb +83 -0
  38. data/app/views/chat_gem/chat_messages/_signal.html.erb +3 -0
  39. data/app/views/chat_gem/chat_messages/_signals.html.erb +24 -0
  40. data/app/views/chat_gem/chat_messages/index.html.erb +1 -0
  41. data/app/views/chat_gem/chats/index.html.erb +51 -0
  42. data/app/views/chat_gem/chats/new.html.erb +13 -0
  43. data/app/views/chat_gem/chats/show.html.erb +95 -0
  44. data/app/views/layouts/chat_gem/application.html.erb +20 -0
  45. data/config/routes.rb +16 -0
  46. data/db/migrate/20260215000000_create_chat_gem_chats.rb +8 -0
  47. data/db/migrate/20260215000001_create_chat_gem_chat_memberships.rb +19 -0
  48. data/db/migrate/20260215000002_create_chat_gem_chat_messages.rb +14 -0
  49. data/db/migrate/20260218000011_add_closed_at_to_chat_gem_chats.rb +6 -0
  50. data/db/migrate/20260218000012_add_custom_role_key_to_chat_memberships.rb +6 -0
  51. data/db/migrate/20260218000013_add_invitation_accepted_to_chat_gem_chat_memberships.rb +5 -0
  52. data/lib/chat_gem/configuration.rb +242 -0
  53. data/lib/chat_gem/engine.rb +29 -0
  54. data/lib/chat_gem/model_extensions/chat_participant.rb +45 -0
  55. data/lib/chat_gem/moderation.rb +194 -0
  56. data/lib/chat_gem/permission.rb +193 -0
  57. data/lib/chat_gem/signals.rb +26 -0
  58. data/lib/chat_gem/version.rb +3 -0
  59. data/lib/chat_gem.rb +24 -0
  60. data/lib/generators/chat_gem/install/install_generator.rb +18 -0
  61. data/lib/generators/chat_gem/install/templates/chat_gem.rb +36 -0
  62. data/lib/generators/turbo_chat/install/install_generator.rb +18 -0
  63. data/lib/generators/turbo_chat/install/templates/turbo_chat.rb +36 -0
  64. data/lib/tasks/chat_gem_tasks.rake +1 -0
  65. data/lib/tasks/turbo_chat_tasks.rake +10 -0
  66. data/lib/turbo_chat/version.rb +5 -0
  67. data/lib/turbo_chat.rb +24 -0
  68. data/turbo_chat.gemspec +31 -0
  69. 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
@@ -0,0 +1,3 @@
1
+ module ChatGem
2
+ VERSION = "0.1.0"
3
+ 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.
@@ -0,0 +1,10 @@
1
+ namespace :turbo_chat do
2
+ namespace :install do
3
+ desc "Install TurboChat migrations (alias for chat_gem:install:migrations)"
4
+ task :migrations do
5
+ task = Rake::Task["chat_gem:install:migrations"]
6
+ task.reenable
7
+ task.invoke
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ require_relative "../chat_gem/version"
2
+
3
+ module TurboChat
4
+ VERSION = ChatGem::VERSION
5
+ end
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
@@ -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