loading
Generated 2024-10-25T20:12:51+00:00

All Files ( 78.3% covered at 21.59 hits/line )

114 files in total.
1373 relevant lines, 1075 lines covered and 298 lines missed. ( 78.3% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/channels/application_cable/channel.rb 100.00 % 4 2 2 0 1.00
app/channels/application_cable/connection.rb 90.91 % 20 11 10 1 7.18
app/channels/heartbeat_channel.rb 100.00 % 2 1 1 0 1.00
app/channels/presence_channel.rb 93.33 % 27 15 14 1 11.80
app/channels/read_rooms_channel.rb 100.00 % 5 3 3 0 8.67
app/channels/room_channel.rb 87.50 % 14 8 7 1 18.50
app/channels/typing_notifications_channel.rb 100.00 % 14 8 8 0 3.38
app/channels/unread_rooms_channel.rb 100.00 % 5 3 3 0 8.67
app/controllers/accounts/logos_controller.rb 79.31 % 52 29 23 6 5.14
app/controllers/application_controller.rb 100.00 % 4 3 3 0 1.00
app/controllers/concerns/allow_browser.rb 100.00 % 9 5 5 0 1.00
app/controllers/concerns/authentication.rb 84.91 % 95 53 45 8 63.51
app/controllers/concerns/authentication/session_lookup.rb 100.00 % 7 4 4 0 130.75
app/controllers/concerns/authorization.rb 75.00 % 6 4 3 1 0.75
app/controllers/concerns/room_scoped.rb 100.00 % 13 8 8 0 9.88
app/controllers/concerns/set_current_request.rb 100.00 % 13 7 7 0 373.43
app/controllers/concerns/set_platform.rb 100.00 % 12 7 7 0 125.86
app/controllers/concerns/tracked_room_visit.rb 100.00 % 20 11 11 0 6.64
app/controllers/concerns/version_headers.rb 100.00 % 13 8 8 0 73.00
app/controllers/messages/boosts_controller.rb 100.00 % 41 21 21 0 1.62
app/controllers/messages_controller.rb 78.57 % 82 42 33 9 1.69
app/controllers/rooms/refreshes_controller.rb 100.00 % 15 9 9 0 8.67
app/controllers/rooms_controller.rb 67.86 % 51 28 19 9 7.96
app/controllers/sessions_controller.rb 65.22 % 40 23 15 8 3.09
app/controllers/users/avatars_controller.rb 79.17 % 42 24 19 5 22.29
app/controllers/users/sidebars_controller.rb 100.00 % 25 15 15 0 50.07
app/controllers/welcome_controller.rb 80.00 % 9 5 4 1 6.40
app/helpers/accounts_helper.rb 100.00 % 5 3 3 0 5.67
app/helpers/application_helper.rb 80.95 % 44 21 17 4 18.10
app/helpers/broadcasts_helper.rb 42.86 % 13 7 3 4 0.43
app/helpers/clipboard_helper.rb 66.67 % 8 3 2 1 0.67
app/helpers/content_filters.rb 100.00 % 3 2 2 0 1.00
app/helpers/content_filters/remove_solo_unfurled_link_text.rb 71.43 % 38 21 15 6 41.19
app/helpers/content_filters/sanitize_tags.rb 100.00 % 17 9 9 0 496.78
app/helpers/content_filters/style_unfurled_twitter_avatars.rb 78.57 % 24 14 11 3 20.93
app/helpers/drop_target_helper.rb 100.00 % 5 3 3 0 24.00
app/helpers/emoji_helper.rb 100.00 % 12 2 2 0 1.00
app/helpers/forms_helper.rb 40.00 % 8 5 2 3 0.40
app/helpers/messages_helper.rb 64.29 % 109 42 27 15 16.55
app/helpers/qr_code_helper.rb 50.00 % 8 4 2 2 0.50
app/helpers/rich_text_helper.rb 100.00 % 11 5 5 0 23.20
app/helpers/rooms/involvements_helper.rb 57.14 % 36 14 8 6 0.57
app/helpers/rooms_helper.rb 78.13 % 84 32 25 7 26.72
app/helpers/searches_helper.rb 66.67 % 12 3 2 1 0.67
app/helpers/time_helper.rb 100.00 % 5 3 3 0 62.67
app/helpers/translations_helper.rb 100.00 % 38 13 13 0 44.15
app/helpers/users/avatars_helper.rb 100.00 % 18 8 8 0 48.63
app/helpers/users/filter_helper.rb 60.00 % 10 5 3 2 0.60
app/helpers/users/profiles_helper.rb 50.00 % 25 8 4 4 0.50
app/helpers/users/sidebar_helper.rb 100.00 % 10 3 3 0 32.00
app/helpers/users_helper.rb 50.00 % 7 4 2 2 0.50
app/helpers/version_helper.rb 100.00 % 5 3 3 0 5.67
app/jobs/application_job.rb 100.00 % 7 1 1 0 1.00
app/jobs/room/push_message_job.rb 66.67 % 5 3 2 1 0.67
app/models/account.rb 100.00 % 5 3 3 0 1.00
app/models/account/joinable.rb 77.78 % 16 9 7 2 0.78
app/models/application_platform.rb 71.88 % 61 32 23 9 36.50
app/models/application_record.rb 100.00 % 3 2 2 0 1.00
app/models/boost.rb 100.00 % 6 4 4 0 25.25
app/models/current.rb 100.00 % 9 5 5 0 63.80
app/models/membership.rb 92.86 % 24 14 13 1 24.00
app/models/membership/connectable.rb 78.57 % 51 28 22 6 6.71
app/models/message.rb 90.48 % 38 21 19 2 125.33
app/models/message/attachment.rb 90.91 % 41 22 20 2 10.23
app/models/message/broadcasts.rb 100.00 % 6 4 4 0 2.50
app/models/message/mentionee.rb 88.89 % 16 9 8 1 1.89
app/models/message/pagination.rb 88.24 % 29 17 15 2 8.59
app/models/message/searchable.rb 100.00 % 28 16 16 0 2.00
app/models/push.rb 100.00 % 5 3 3 0 1.00
app/models/push/subscription.rb 75.00 % 7 4 3 1 0.75
app/models/room.rb 71.43 % 75 42 30 12 14.83
app/models/rooms/closed.rb 100.00 % 3 1 1 0 1.00
app/models/rooms/direct.rb 60.00 % 22 10 6 4 0.60
app/models/rooms/open.rb 80.00 % 9 5 4 1 0.80
app/models/search.rb 80.00 % 18 10 8 2 0.80
app/models/session.rb 90.00 % 19 10 9 1 26.60
app/models/user.rb 68.42 % 67 38 26 12 12.55
app/models/user/avatar.rb 100.00 % 17 9 9 0 59.89
app/models/user/bot.rb 52.78 % 68 36 19 17 0.64
app/models/user/mentionable.rb 62.50 % 15 8 5 3 0.63
app/models/user/role.rb 100.00 % 11 6 6 0 8.50
app/models/user/transferable.rb 75.00 % 15 8 6 2 0.75
app/models/webhook.rb 45.00 % 79 40 18 22 0.45
config/environment.rb 100.00 % 5 2 2 0 1.00
config/environments/test.rb 100.00 % 56 16 16 0 1.00
config/initializers/active_storage.rb 66.67 % 5 3 2 1 0.67
config/initializers/assets.rb 100.00 % 4 1 1 0 1.00
config/initializers/content_security_policy.rb 100.00 % 25 0 0 0 0.00
config/initializers/extensions.rb 100.00 % 3 2 2 0 3.50
config/initializers/filter_parameter_logging.rb 100.00 % 8 1 1 0 1.00
config/initializers/inflections.rb 100.00 % 16 0 0 0 0.00
config/initializers/permissions_policy.rb 100.00 % 11 0 0 0 0.00
config/initializers/sentry.rb 16.67 % 8 6 1 5 0.17
config/initializers/session_store.rb 100.00 % 4 1 1 0 1.00
config/initializers/sqlite3.rb 52.17 % 36 23 12 11 0.70
config/initializers/storage_paths.rb 100.00 % 5 3 3 0 1.33
config/initializers/time_formats.rb 100.00 % 2 1 1 0 292.00
config/initializers/vapid.rb 100.00 % 4 3 3 0 1.00
config/initializers/version.rb 100.00 % 2 2 2 0 1.00
config/initializers/web_push.rb 34.62 % 44 26 9 17 0.35
config/routes.rb 100.00 % 97 54 54 0 11.74
lib/rails_ext/action_text_attachables.rb 50.00 % 26 14 7 7 0.50
lib/rails_ext/actiontext_opengraph_embeds.rb 57.14 % 44 21 12 9 0.57
lib/rails_ext/filter.rb 85.71 % 24 14 12 2 61.71
lib/rails_ext/filters.rb 100.00 % 12 7 7 0 55.14
lib/rails_ext/string.rb 100.00 % 5 3 3 0 41.67
lib/web_push/notification.rb 53.85 % 29 13 7 6 0.54
lib/web_push/pool.rb 61.29 % 56 31 19 12 3.29
test/system/sending_messages_test.rb 100.00 % 70 41 41 0 1.10
test/system/unread_rooms_test.rb 100.00 % 27 17 17 0 1.00
test/test_helpers/mention_test_helper.rb 40.00 % 7 5 2 3 0.40
test/test_helpers/session_test_helper.rb 42.86 % 11 7 3 4 0.43
test/test_helpers/system_test_helper.rb 96.77 % 56 31 30 1 7.58
test/test_helpers/turbo_test_helper.rb 36.36 % 17 11 4 7 0.36

app/channels/application_cable/channel.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 module ApplicationCable
  2. 1 class Channel < ActionCable::Channel::Base
  3. end
  4. end

app/channels/application_cable/connection.rb

90.91% lines covered

11 relevant lines. 10 lines covered and 1 lines missed.
    
  1. 1 module ApplicationCable
  2. 1 class Connection < ActionCable::Connection::Base
  3. 1 include Authentication::SessionLookup
  4. 1 identified_by :current_user
  5. 1 def connect
  6. 24 self.current_user = find_verified_user
  7. end
  8. 1 private
  9. 1 def find_verified_user
  10. 24 if verified_session = find_session_by_cookie
  11. 24 verified_session.user
  12. else
  13. reject_unauthorized_connection
  14. end
  15. end
  16. end
  17. end

app/channels/heartbeat_channel.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. 1 class HeartbeatChannel < ApplicationCable::Channel
  2. end

app/channels/presence_channel.rb

93.33% lines covered

15 relevant lines. 14 lines covered and 1 lines missed.
    
  1. 1 class PresenceChannel < RoomChannel
  2. 1 on_subscribe :present, unless: :subscription_rejected?
  3. 1 on_unsubscribe :absent, unless: :subscription_rejected?
  4. 1 def present
  5. 24 membership.present
  6. 24 broadcast_read_room
  7. end
  8. 1 def absent
  9. 24 membership.disconnected
  10. end
  11. 1 def refresh
  12. membership.refresh_connection
  13. end
  14. 1 private
  15. 1 def membership
  16. 72 @room.memberships.find_by(user: current_user)
  17. end
  18. 1 def broadcast_read_room
  19. 24 ActionCable.server.broadcast "user_#{current_user.id}_reads", { room_id: membership.room_id }
  20. end
  21. end

app/channels/read_rooms_channel.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 class ReadRoomsChannel < ApplicationCable::Channel
  2. 1 def subscribed
  3. 24 stream_from "user_#{current_user.id}_reads"
  4. end
  5. end

app/channels/room_channel.rb

87.5% lines covered

8 relevant lines. 7 lines covered and 1 lines missed.
    
  1. 1 class RoomChannel < ApplicationCable::Channel
  2. 1 def subscribed
  3. 48 if @room = find_room
  4. 48 stream_for @room
  5. else
  6. reject
  7. end
  8. end
  9. 1 private
  10. 1 def find_room
  11. 48 current_user.rooms.find_by(id: params[:room_id])
  12. end
  13. end

app/channels/typing_notifications_channel.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 class TypingNotificationsChannel < RoomChannel
  2. 1 def start(data)
  3. 3 broadcast_to @room, action: :start, user: current_user_attributes
  4. end
  5. 1 def stop(data)
  6. 8 broadcast_to @room, action: :stop, user: current_user_attributes
  7. end
  8. 1 private
  9. 1 def current_user_attributes
  10. 11 current_user.slice(:id, :name)
  11. end
  12. end

app/channels/unread_rooms_channel.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 class UnreadRoomsChannel < ApplicationCable::Channel
  2. 1 def subscribed
  3. 24 stream_from "unread_rooms"
  4. end
  5. end

app/controllers/accounts/logos_controller.rb

79.31% lines covered

29 relevant lines. 23 lines covered and 6 lines missed.
    
  1. 1 class Accounts::LogosController < ApplicationController
  2. 1 include ActiveStorage::Streaming, ActionView::Helpers::AssetUrlHelper
  3. 1 allow_unauthenticated_access only: :show
  4. 1 before_action :ensure_can_administer, only: :destroy
  5. 1 def show
  6. 15 if stale?(etag: Current.account)
  7. 15 expires_in 5.minutes, public: true, stale_while_revalidate: 1.week
  8. 15 if Current.account&.logo&.attached?
  9. logo = Current.account.logo.variant(logo_variant).processed
  10. send_png_file ActiveStorage::Blob.service.path_for(logo.key)
  11. else
  12. 15 send_stock_icon
  13. end
  14. end
  15. end
  16. 1 def destroy
  17. Current.account.logo.destroy
  18. redirect_to edit_account_url
  19. end
  20. 1 private
  21. 1 LARGE_SQUARE_PNG_VARIANT = { resize_to_limit: [ 512, 512 ], format: :png }
  22. 1 SMALL_SQUARE_PNG_VARIANT = { resize_to_limit: [ 192, 192 ], format: :png }
  23. 1 def send_png_file(path)
  24. 15 send_file path, content_type: "image/png", disposition: :inline
  25. end
  26. 1 def send_stock_icon
  27. 15 if small_logo?
  28. send_png_file logo_path("app-icon-192.png")
  29. else
  30. 15 send_png_file logo_path("app-icon.png")
  31. end
  32. end
  33. 1 def logo_variant
  34. small_logo? ? SMALL_SQUARE_PNG_VARIANT : LARGE_SQUARE_PNG_VARIANT
  35. end
  36. 1 def small_logo?
  37. 15 params[:size] == "small"
  38. end
  39. 1 def logo_path(filename)
  40. 15 Rails.root.join("app/assets/images/logos/#{filename}")
  41. end
  42. end

app/controllers/application_controller.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 class ApplicationController < ActionController::Base
  2. 1 include AllowBrowser, Authentication, Authorization, SetCurrentRequest, SetPlatform, TrackedRoomVisit, VersionHeaders
  3. 1 include Turbo::Streams::Broadcasts, Turbo::Streams::StreamName
  4. end

app/controllers/concerns/allow_browser.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 module AllowBrowser
  2. 1 extend ActiveSupport::Concern
  3. 1 VERSIONS = { safari: 17.2, chrome: 120, firefox: 121, opera: 104, ie: false }
  4. 1 included do
  5. 1 allow_browser versions: VERSIONS, block: -> { render template: "sessions/incompatible_browser" }
  6. end
  7. end

app/controllers/concerns/authentication.rb

84.91% lines covered

53 relevant lines. 45 lines covered and 8 lines missed.
    
  1. 1 module Authentication
  2. 1 extend ActiveSupport::Concern
  3. 1 include SessionLookup
  4. 1 included do
  5. 1 before_action :require_authentication
  6. 1 before_action :deny_bots
  7. 1 helper_method :signed_in?
  8. 275 protect_from_forgery with: :exception, unless: -> { authenticated_by.bot_key? }
  9. end
  10. 1 class_methods do
  11. 1 def allow_unauthenticated_access(**options)
  12. 2 skip_before_action :require_authentication, **options
  13. end
  14. 1 def allow_bot_access(**options)
  15. skip_before_action :deny_bots, **options
  16. end
  17. 1 def require_unauthenticated_access(**options)
  18. skip_before_action :require_authentication, **options
  19. before_action :restore_authentication, :redirect_signed_in_user_to_root, **options
  20. end
  21. end
  22. 1 private
  23. 1 def signed_in?
  24. Current.user.present?
  25. end
  26. 1 def require_authentication
  27. 244 restore_authentication || bot_authentication || request_authentication
  28. end
  29. 1 def restore_authentication
  30. 244 if session = find_session_by_cookie
  31. 229 resume_session session
  32. end
  33. end
  34. 1 def bot_authentication
  35. 15 if params[:bot_key].present? && bot = User.authenticate_bot(params[:bot_key].strip)
  36. Current.user = bot
  37. set_authenticated_by(:bot_key)
  38. end
  39. end
  40. 1 def request_authentication
  41. 15 session[:return_to_after_authenticating] = request.url
  42. 15 redirect_to new_session_url
  43. end
  44. 1 def redirect_signed_in_user_to_root
  45. redirect_to root_url if signed_in?
  46. end
  47. 1 def start_new_session_for(user)
  48. 15 user.sessions.start!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
  49. 15 authenticated_as session
  50. end
  51. end
  52. 1 def resume_session(session)
  53. 229 session.resume user_agent: request.user_agent, ip_address: request.remote_ip
  54. 229 authenticated_as session
  55. end
  56. 1 def authenticated_as(session)
  57. 244 Current.user = session.user
  58. 244 set_authenticated_by(:session)
  59. 244 cookies.signed.permanent[:session_token] = { value: session.token, httponly: true, same_site: :lax }
  60. end
  61. 1 def post_authenticating_url
  62. 15 session.delete(:return_to_after_authenticating) || root_url
  63. end
  64. 1 def reset_authentication
  65. cookies.delete(:session_token)
  66. end
  67. 1 def deny_bots
  68. 274 head :forbidden if authenticated_by.bot_key?
  69. end
  70. 1 def set_authenticated_by(method)
  71. 244 @authenticated_by = method.to_s.inquiry
  72. end
  73. 1 def authenticated_by
  74. 548 @authenticated_by ||= "".inquiry
  75. end
  76. end

app/controllers/concerns/authentication/session_lookup.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 module Authentication::SessionLookup
  2. 1 def find_session_by_cookie
  3. 268 if token = cookies.signed[:session_token]
  4. 253 Session.find_by(token: token)
  5. end
  6. end
  7. end

app/controllers/concerns/authorization.rb

75.0% lines covered

4 relevant lines. 3 lines covered and 1 lines missed.
    
  1. 1 module Authorization
  2. 1 private
  3. 1 def ensure_can_administer
  4. head :forbidden unless Current.user.can_administer?
  5. end
  6. end

app/controllers/concerns/room_scoped.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 module RoomScoped
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 2 before_action :set_room
  5. end
  6. 1 private
  7. 1 def set_room
  8. 36 @membership = Current.user.memberships.find_by!(room_id: params[:room_id])
  9. 36 @room = @membership.room
  10. end
  11. end

app/controllers/concerns/set_current_request.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 module SetCurrentRequest
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 before_action do
  5. 289 Current.request = request
  6. end
  7. end
  8. 1 def default_url_options
  9. 2320 { host: Current.request_host, protocol: Current.request_protocol }.compact_blank
  10. end
  11. end

app/controllers/concerns/set_platform.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 module SetPlatform
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 helper_method :platform
  5. end
  6. 1 private
  7. 1 def platform
  8. 875 @platform ||= ApplicationPlatform.new(request.user_agent)
  9. end
  10. end

app/controllers/concerns/tracked_room_visit.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 1 module TrackedRoomVisit
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 helper_method :last_room_visited
  5. end
  6. 1 def remember_last_room_visited
  7. 35 cookies.permanent[:last_room] = @room.id
  8. end
  9. 1 def last_room_visited
  10. 15 Current.user.rooms.find_by(id: cookies[:last_room]) || default_room
  11. end
  12. 1 private
  13. 1 def default_room
  14. 15 Current.user.rooms.original
  15. end
  16. end

app/controllers/concerns/version_headers.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 module VersionHeaders
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 before_action :set_version_headers
  5. end
  6. 1 private
  7. 1 def set_version_headers
  8. 289 response.headers["X-Version"] = Rails.application.config.app_version
  9. 289 response.headers["X-Rev"] = Rails.application.config.git_revision
  10. end
  11. end

app/controllers/messages/boosts_controller.rb

100.0% lines covered

21 relevant lines. 21 lines covered and 0 lines missed.
    
  1. 1 class Messages::BoostsController < ApplicationController
  2. 1 before_action :set_message
  3. 1 def index
  4. end
  5. 1 def new
  6. end
  7. 1 def create
  8. 2 @boost = @message.boosts.create!(boost_params)
  9. 2 broadcast_create
  10. 2 redirect_to message_boosts_url(@message)
  11. end
  12. 1 def destroy
  13. 1 @boost = Current.user.boosts.find(params[:id])
  14. 1 @boost.destroy!
  15. 1 broadcast_remove
  16. end
  17. 1 private
  18. 1 def set_message
  19. 9 @message = Current.user.reachable_messages.find(params[:message_id])
  20. end
  21. 1 def boost_params
  22. 2 params.require(:boost).permit(:content)
  23. end
  24. 1 def broadcast_create
  25. 2 @boost.broadcast_append_to @boost.message.room, :messages,
  26. target: "boosts_message_#{@boost.message.client_message_id}", partial: "messages/boosts/boost", attributes: { maintain_scroll: true }
  27. end
  28. 1 def broadcast_remove
  29. 1 @boost.broadcast_remove_to @boost.message.room, :messages
  30. end
  31. end

app/controllers/messages_controller.rb

78.57% lines covered

42 relevant lines. 33 lines covered and 9 lines missed.
    
  1. 1 class MessagesController < ApplicationController
  2. 1 include ActiveStorage::SetCurrent, RoomScoped
  3. 1 before_action :set_room, except: :create
  4. 1 before_action :set_message, only: %i[ show edit update destroy ]
  5. 1 before_action :ensure_can_administer, only: %i[ edit update destroy ]
  6. 1 layout false, only: :index
  7. 1 def index
  8. @messages = find_paged_messages
  9. if @messages.any?
  10. fresh_when @messages
  11. else
  12. head :no_content
  13. end
  14. end
  15. 1 def create
  16. 4 set_room
  17. 4 @message = @room.messages.create_with_attachment!(message_params)
  18. 4 @message.broadcast_create
  19. 4 deliver_webhooks_to_bots
  20. rescue ActiveRecord::RecordNotFound
  21. render action: :room_not_found
  22. end
  23. 1 def show
  24. end
  25. 1 def edit
  26. end
  27. 1 def update
  28. 2 @message.update!(message_params)
  29. 2 @message.broadcast_replace_to @room, :messages, target: [ @message, :presentation ], partial: "messages/presentation", attributes: { maintain_scroll: true }
  30. 2 redirect_to room_message_url(@room, @message)
  31. end
  32. 1 def destroy
  33. 1 @message.destroy
  34. 1 @message.broadcast_remove_to @room, :messages
  35. end
  36. 1 private
  37. 1 def set_message
  38. 8 @message = @room.messages.find(params[:id])
  39. end
  40. 1 def ensure_can_administer
  41. 6 head :forbidden unless Current.user.can_administer?(@message)
  42. end
  43. 1 def find_paged_messages
  44. case
  45. when params[:before].present?
  46. @room.messages.with_creator.page_before(@room.messages.find(params[:before]))
  47. when params[:after].present?
  48. @room.messages.with_creator.page_after(@room.messages.find(params[:after]))
  49. else
  50. @room.messages.with_creator.last_page
  51. end
  52. end
  53. 1 def message_params
  54. 6 params.require(:message).permit(:body, :attachment, :client_message_id)
  55. end
  56. 1 def deliver_webhooks_to_bots
  57. 4 bots_eligible_for_webhook.excluding(@message.creator).each { |bot| bot.deliver_webhook_later(@message) }
  58. end
  59. 1 def bots_eligible_for_webhook
  60. 4 @room.direct? ? @room.users.active_bots : @message.mentionees.active_bots
  61. end
  62. end

app/controllers/rooms/refreshes_controller.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 class Rooms::RefreshesController < ApplicationController
  2. 1 include RoomScoped
  3. 1 before_action :set_last_updated_at
  4. 1 def show
  5. 24 @new_messages = @room.messages.with_creator.page_created_since(@last_updated_at)
  6. 24 @updated_messages = @room.messages.without(@new_messages).with_creator.page_updated_since(@last_updated_at)
  7. end
  8. 1 private
  9. 1 def set_last_updated_at
  10. 24 @last_updated_at = Time.at(0, params[:since].to_i, :millisecond)
  11. end
  12. end

app/controllers/rooms_controller.rb

67.86% lines covered

28 relevant lines. 19 lines covered and 9 lines missed.
    
  1. 1 class RoomsController < ApplicationController
  2. 1 before_action :set_room, only: %i[ edit update show destroy ]
  3. 1 before_action :ensure_can_administer, only: %i[ update destroy ]
  4. 1 before_action :remember_last_room_visited, only: :show
  5. 1 def index
  6. redirect_to room_url(Current.user.rooms.last)
  7. end
  8. 1 def show
  9. 35 @messages = find_messages
  10. end
  11. 1 def destroy
  12. @room.destroy
  13. broadcast_remove_room
  14. redirect_to root_url
  15. end
  16. 1 private
  17. 1 def set_room
  18. 35 if room = Current.user.rooms.find_by(id: params[:room_id] || params[:id])
  19. 35 @room = room
  20. else
  21. redirect_to root_url, alert: "Room not found or inaccessible"
  22. end
  23. end
  24. 1 def ensure_can_administer
  25. head :forbidden unless Current.user.can_administer?(@room)
  26. end
  27. 1 def find_messages
  28. 35 messages = @room.messages.with_creator
  29. 35 if show_first_message = messages.find_by(id: params[:message_id])
  30. @messages = messages.page_around(show_first_message)
  31. else
  32. 35 @messages = messages.last_page
  33. end
  34. end
  35. 1 def room_params
  36. params.require(:room).permit(:name)
  37. end
  38. 1 def broadcast_remove_room
  39. broadcast_remove_to :rooms, target: [ @room, :list ]
  40. end
  41. end

app/controllers/sessions_controller.rb

65.22% lines covered

23 relevant lines. 15 lines covered and 8 lines missed.
    
  1. 1 class SessionsController < ApplicationController
  2. 1 allow_unauthenticated_access only: %i[ new create ]
  3. 1 rate_limit to: 10, within: 3.minutes, only: :create, with: -> { render_rejection :too_many_requests }
  4. 1 before_action :ensure_user_exists, only: :new
  5. 1 def new
  6. end
  7. 1 def create
  8. 15 if user = User.active.authenticate_by(email_address: params[:email_address], password: params[:password])
  9. 15 start_new_session_for user
  10. 15 redirect_to post_authenticating_url
  11. else
  12. render_rejection :unauthorized
  13. end
  14. end
  15. 1 def destroy
  16. remove_push_subscription
  17. reset_authentication
  18. redirect_to root_url
  19. end
  20. 1 private
  21. 1 def ensure_user_exists
  22. 15 redirect_to first_run_url if User.none?
  23. end
  24. 1 def render_rejection(status)
  25. flash.now[:alert] = "Too many requests or unauthorized."
  26. render :new, status: status
  27. end
  28. 1 def remove_push_subscription
  29. if endpoint = params[:push_subscription_endpoint]
  30. Push::Subscription.destroy_by(endpoint: endpoint, user_id: Current.user.id)
  31. end
  32. end
  33. end

app/controllers/users/avatars_controller.rb

79.17% lines covered

24 relevant lines. 19 lines covered and 5 lines missed.
    
  1. 1 class Users::AvatarsController < ApplicationController
  2. 1 include ActiveStorage::Streaming
  3. 1 rescue_from(ActiveSupport::MessageVerifier::InvalidSignature) { head :not_found }
  4. 1 def show
  5. 75 @user = User.from_avatar_token(params[:user_id])
  6. 75 if stale?(etag: @user)
  7. 75 expires_in 30.minutes, public: true, stale_while_revalidate: 1.week
  8. 75 if @user.avatar.attached?
  9. avatar_variant = @user.avatar.variant(SQUARE_WEBP_VARIANT).processed
  10. send_webp_blob_file avatar_variant.key
  11. 75 elsif @user.bot?
  12. 15 render_default_bot
  13. else
  14. 60 render_initials
  15. end
  16. end
  17. end
  18. 1 def destroy
  19. Current.user.avatar.destroy
  20. redirect_to user_profile_url
  21. end
  22. 1 private
  23. 1 SQUARE_WEBP_VARIANT = { resize_to_limit: [ 512, 512 ], format: :webp }
  24. 1 def send_webp_blob_file(key)
  25. send_file ActiveStorage::Blob.service.path_for(key), content_type: "image/webp", disposition: :inline
  26. end
  27. 1 def render_default_bot
  28. 15 send_file Rails.root.join("app/assets/images/default-bot-avatar.svg"), content_type: "image/svg+xml", disposition: :inline
  29. end
  30. 1 def render_initials
  31. 60 render formats: :svg
  32. end
  33. end

app/controllers/users/sidebars_controller.rb

100.0% lines covered

15 relevant lines. 15 lines covered and 0 lines missed.
    
  1. 1 class Users::SidebarsController < ApplicationController
  2. 1 DIRECT_PLACEHOLDERS = 20
  3. 1 def show
  4. 59 all_memberships = Current.user.memberships.visible.with_ordered_room
  5. 59 @direct_memberships = extract_direct_memberships(all_memberships)
  6. 59 @other_memberships = all_memberships.without(@direct_memberships)
  7. 59 @direct_placeholder_users = find_direct_placeholder_users
  8. end
  9. 1 private
  10. 1 def extract_direct_memberships(all_memberships)
  11. 331 all_memberships.select { |m| m.room.direct? }.sort_by { |m| m.room.updated_at }.reverse
  12. end
  13. 1 def find_direct_placeholder_users
  14. 59 exclude_user_ids = user_ids_already_in_direct_rooms_with_current_user.including(Current.user.id)
  15. 59 User.active.where.not(id: exclude_user_ids).order(:created_at).limit(DIRECT_PLACEHOLDERS - exclude_user_ids.count)
  16. end
  17. 1 def user_ids_already_in_direct_rooms_with_current_user
  18. 59 Membership.where(room_id: Current.user.rooms.directs.pluck(:id)).pluck(:user_id).uniq
  19. end
  20. end

app/controllers/welcome_controller.rb

80.0% lines covered

5 relevant lines. 4 lines covered and 1 lines missed.
    
  1. 1 class WelcomeController < ApplicationController
  2. 1 def show
  3. 15 if Current.user.rooms.any?
  4. 15 redirect_to room_url(last_room_visited)
  5. else
  6. render
  7. end
  8. end
  9. end

app/helpers/accounts_helper.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module AccountsHelper
  2. 1 def account_logo_tag(style: nil)
  3. 15 tag.figure image_tag(fresh_account_logo_path, alt: "Account logo", size: 300), class: "account-logo avatar #{style}"
  4. end
  5. end

app/helpers/application_helper.rb

80.95% lines covered

21 relevant lines. 17 lines covered and 4 lines missed.
    
  1. 1 module ApplicationHelper
  2. 1 def page_title_tag
  3. 55 tag.title @page_title || "Campfire"
  4. end
  5. 1 def current_user_meta_tags
  6. 55 unless Current.user.nil?
  7. 40 safe_join [
  8. tag(:meta, name: "current-user-id", content: Current.user.id),
  9. tag(:meta, name: "current-user-name", content: Current.user.name)
  10. ]
  11. end
  12. end
  13. 1 def custom_styles_tag
  14. 55 if custom_styles = Current.account&.custom_styles
  15. tag.style(custom_styles.to_s.html_safe, data: { turbo_track: "reload" })
  16. end
  17. end
  18. 1 def body_classes
  19. 55 [ @body_class, admin_body_class, account_logo_body_class ].compact.join(" ")
  20. end
  21. 1 def link_back
  22. link_back_to request.referrer || root_path
  23. end
  24. 1 def link_back_to(destination)
  25. link_to destination, class: "btn" do
  26. image_tag("arrow-left.svg", aria: { hidden: "true" }, size: 20) +
  27. tag.span("Go Back", class: "for-screen-reader")
  28. end
  29. end
  30. 1 private
  31. 1 def admin_body_class
  32. 55 "admin" if Current.user&.can_administer?
  33. end
  34. 1 def account_logo_body_class
  35. 55 "account-has-logo" if Current.account&.logo&.attached?
  36. end
  37. end

app/helpers/broadcasts_helper.rb

42.86% lines covered

7 relevant lines. 3 lines covered and 4 lines missed.
    
  1. 1 module BroadcastsHelper
  2. 1 def broadcast_image_tag(image, options)
  3. image_tag(broadcast_image_path(image), options)
  4. end
  5. 1 def broadcast_image_path(image)
  6. if image.is_a?(Symbol) || image.is_a?(String)
  7. image_path(image)
  8. else
  9. polymorphic_url(image, only_path: true)
  10. end
  11. end
  12. end

app/helpers/clipboard_helper.rb

66.67% lines covered

3 relevant lines. 2 lines covered and 1 lines missed.
    
  1. 1 module ClipboardHelper
  2. 1 def button_to_copy_to_clipboard(url, &)
  3. tag.button class: "btn", data: {
  4. controller: "copy-to-clipboard", action: "copy-to-clipboard#copy",
  5. copy_to_clipboard_success_class: "btn--success", copy_to_clipboard_content_value: url
  6. }, &
  7. end
  8. end

app/helpers/content_filters.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 module ContentFilters
  2. 1 TextMessagePresentationFilters = ActionText::Content::Filters.new(RemoveSoloUnfurledLinkText, StyleUnfurledTwitterAvatars, SanitizeTags)
  3. end

app/helpers/content_filters/remove_solo_unfurled_link_text.rb

71.43% lines covered

21 relevant lines. 15 lines covered and 6 lines missed.
    
  1. 1 class ContentFilters::RemoveSoloUnfurledLinkText < ActionText::Content::Filter
  2. 1 def applicable?
  3. 95 normalize_tweet_url(solo_unfurled_url) == normalize_tweet_url(content.to_plain_text)
  4. end
  5. 1 def apply
  6. fragment.replace("div") { |node| node.tap { |n| n.inner_html = unfurled_links.first.to_s } }
  7. end
  8. 1 private
  9. 1 TWITTER_DOMAINS = %w[ x.com twitter.com ]
  10. 1 TWITTER_DOMAIN_MAPPING = { "x.com" => "twitter.com" }
  11. 1 def solo_unfurled_url
  12. 95 unfurled_links.first["href"] if unfurled_links.size == 1
  13. end
  14. 1 def unfurled_links
  15. 95 fragment.find_all("action-text-attachment[@content-type='#{ActionText::Attachment::OpengraphEmbed::OPENGRAPH_EMBED_CONTENT_TYPE}']")
  16. end
  17. 1 def normalize_tweet_url(url)
  18. 190 return url unless twitter_url?(url)
  19. uri = URI.parse(url)
  20. uri.dup.tap do |u|
  21. u.host = TWITTER_DOMAIN_MAPPING[uri.host&.downcase] || uri.host
  22. u.query = nil
  23. end.to_s
  24. rescue URI::InvalidURIError
  25. url
  26. end
  27. 1 def twitter_url?(url)
  28. 380 url.present? && TWITTER_DOMAINS.any? { |domain| url.strip.include?(domain) }
  29. end
  30. end

app/helpers/content_filters/sanitize_tags.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 class ContentFilters::SanitizeTags < ActionText::Content::Filter
  2. 1 def applicable?
  3. 95 true
  4. end
  5. 1 def apply
  6. 95 fragment.replace(not_allowed_tags_css_selector) { nil }
  7. end
  8. 1 private
  9. 1 ALLOWED_TAGS = %w[ a abbr acronym address b big blockquote br cite code dd del dfn div dl dt em h1 h2 h3 h4 h5 h6 hr i ins kbd li ol
  10. p pre samp small span strong sub sup time tt ul var ] + [ ActionText::Attachment.tag_name, "figure", "figcaption" ]
  11. 1 def not_allowed_tags_css_selector
  12. 4275 ALLOWED_TAGS.map { |tag| ":not(#{tag})" }.join("")
  13. end
  14. end

app/helpers/content_filters/style_unfurled_twitter_avatars.rb

78.57% lines covered

14 relevant lines. 11 lines covered and 3 lines missed.
    
  1. 1 class ContentFilters::StyleUnfurledTwitterAvatars < ActionText::Content::Filter
  2. 1 def applicable?
  3. 95 unfurled_twitter_avatars.present?
  4. end
  5. 1 def apply
  6. fragment.update do |source|
  7. div = source.at_css("div")
  8. div["class"] = UNFURLED_TWITTER_AVATAR_CSS_CLASS
  9. end
  10. end
  11. 1 private
  12. 1 UNFURLED_TWITTER_AVATAR_CSS_CLASS = "cf-twitter-avatar"
  13. 1 TWITTER_AVATAR_URL_PREFIX = "https://pbs.twimg.com/profile_images"
  14. 1 def unfurled_twitter_avatars
  15. 95 fragment.find_all("#{opengraph_css_selector}[url*='#{TWITTER_AVATAR_URL_PREFIX}']")
  16. end
  17. 1 def opengraph_css_selector
  18. 95 "action-text-attachment[@content-type='#{ActionText::Attachment::OpengraphEmbed::OPENGRAPH_EMBED_CONTENT_TYPE}']"
  19. end
  20. end

app/helpers/drop_target_helper.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module DropTargetHelper
  2. 1 def drop_target_actions
  3. 70 "dragenter->drop-target#dragenter dragover->drop-target#dragover drop->drop-target#drop"
  4. end
  5. end

app/helpers/emoji_helper.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 module EmojiHelper
  2. REACTIONS = {
  3. 1 "👍" => "Thumbs up",
  4. "👏" => "Clapping",
  5. "👋" => "Waving hand",
  6. "💪" => "Muscle",
  7. "❤️" => "Red heart",
  8. "😂" => "Face with tears of joy",
  9. "🎉" => "Party popper",
  10. "🔥" => "Fire"
  11. }
  12. end

app/helpers/forms_helper.rb

40.0% lines covered

5 relevant lines. 2 lines covered and 3 lines missed.
    
  1. 1 module FormsHelper
  2. 1 def auto_submit_form_with(**attributes, &)
  3. data = attributes.delete(:data) || {}
  4. data[:controller] = "auto-submit #{data[:controller]}".strip
  5. form_with **attributes, data: data, &
  6. end
  7. end

app/helpers/messages_helper.rb

64.29% lines covered

42 relevant lines. 27 lines covered and 15 lines missed.
    
  1. 1 module MessagesHelper
  2. 1 def message_area_tag(room, &)
  3. 35 tag.div id: "message-area", class: "message-area", contents: true, data: {
  4. controller: "messages presence drop-target",
  5. action: [ messages_actions, drop_target_actions, presence_actions ].join(" "),
  6. messages_first_of_day_class: "message--first-of-day",
  7. messages_formatted_class: "message--formatted",
  8. messages_me_class: "message--me",
  9. messages_mentioned_class: "message--mentioned",
  10. messages_threaded_class: "message--threaded",
  11. messages_page_url_value: room_messages_url(room)
  12. }, &
  13. end
  14. 1 def messages_tag(room, &)
  15. 35 tag.div id: dom_id(room, :messages), class: "messages", data: {
  16. controller: "maintain-scroll refresh-room",
  17. action: [ maintain_scroll_actions, refresh_room_actions ].join(" "),
  18. messages_target: "messages",
  19. refresh_room_loaded_at_value: room.updated_at.to_fs(:epoch),
  20. refresh_room_url_value: room_refresh_url(room)
  21. }, &
  22. end
  23. 1 def message_tag(message, &)
  24. 93 message_timestamp_milliseconds = message.created_at.to_fs(:epoch)
  25. 93 tag.div id: dom_id(message),
  26. class: "message #{"message--emoji" if message.plain_text_body.all_emoji?}",
  27. data: {
  28. controller: "reply",
  29. user_id: message.creator_id,
  30. message_id: message.id,
  31. message_timestamp: message_timestamp_milliseconds,
  32. message_updated_at: message.updated_at.to_fs(:epoch),
  33. sort_value: message_timestamp_milliseconds,
  34. messages_target: "message",
  35. search_results_target: "message",
  36. refresh_room_target: "message",
  37. reply_composer_outlet: "#composer"
  38. }, &
  39. rescue Exception => e
  40. Sentry.capture_exception(e, extra: { message: message })
  41. Rails.logger.error "Exception while rendering message #{message.class.name}##{message.id}, failed with: #{e.class} `#{e.message}`"
  42. render "messages/unrenderable"
  43. end
  44. 1 def message_timestamp(message, **attributes)
  45. 93 local_datetime_tag message.created_at, **attributes
  46. end
  47. 1 def message_presentation(message)
  48. 95 case message.content_type
  49. when "attachment"
  50. message_attachment_presentation(message)
  51. when "sound"
  52. message_sound_presentation(message)
  53. else
  54. 95 auto_link h(ContentFilters::TextMessagePresentationFilters.apply(message.body.body)), html: { target: "_blank" }
  55. end
  56. rescue Exception => e
  57. Sentry.capture_exception(e, extra: { message: message })
  58. Rails.logger.error "Exception while generating message representation for #{message.class.name}##{message.id}, failed with: #{e.class} `#{e.message}`"
  59. ""
  60. end
  61. 1 private
  62. 1 def messages_actions
  63. 35 "turbo:before-stream-render@document->messages#beforeStreamRender keydown.up@document->messages#editMyLastMessage"
  64. end
  65. 1 def maintain_scroll_actions
  66. 35 "turbo:before-stream-render@document->maintain-scroll#beforeStreamRender"
  67. end
  68. 1 def refresh_room_actions
  69. 35 "visibilitychange@document->refresh-room#visibilityChanged online@window->refresh-room#online"
  70. end
  71. 1 def presence_actions
  72. 35 "visibilitychange@document->presence#visibilityChanged"
  73. end
  74. 1 def message_attachment_presentation(message)
  75. Messages::AttachmentPresentation.new(message, context: self).render
  76. end
  77. 1 def message_sound_presentation(message)
  78. sound = message.sound
  79. tag.div class: "sound", data: { controller: "sound", action: "messages:play->sound#play", sound_url_value: asset_path(sound.asset_path) } do
  80. play_button + (sound.image ? sound_image_tag(sound.image) : sound.text)
  81. end
  82. end
  83. 1 def play_button
  84. tag.button "🔊", class: "btn btn--plain", data: { action: "sound#play" }
  85. end
  86. 1 def sound_image_tag(image)
  87. image_tag image.asset_path, width: image.width, height: image.height, class: "align--middle"
  88. end
  89. 1 def message_author_title(author)
  90. [ author.name, author.bio ].compact_blank.join(" – ")
  91. end
  92. end

app/helpers/qr_code_helper.rb

50.0% lines covered

4 relevant lines. 2 lines covered and 2 lines missed.
    
  1. 1 module QrCodeHelper
  2. 1 def link_to_zoom_qr_code(url, &)
  3. id = Base64.urlsafe_encode64(url)
  4. link_to qr_code_path(id), class: "btn", data: {
  5. lightbox_target: "image", action: "lightbox#open", lightbox_url_value: qr_code_path(id) }, &
  6. end
  7. end

app/helpers/rich_text_helper.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 module RichTextHelper
  2. 1 def rich_text_data_actions
  3. default_actions =
  4. 38 "trix-change->typing-notifications#start keydown->composer#submitByKeyboard"
  5. autocomplete_actions =
  6. 38 "trix-focus->rich-autocomplete#focus trix-change->rich-autocomplete#search trix-blur->rich-autocomplete#blur"
  7. 38 [ default_actions, autocomplete_actions ].join(" ")
  8. end
  9. end

app/helpers/rooms/involvements_helper.rb

57.14% lines covered

14 relevant lines. 8 lines covered and 6 lines missed.
    
  1. 1 module Rooms::InvolvementsHelper
  2. 1 def turbo_frame_for_involvement_tag(room, &)
  3. turbo_frame_tag dom_id(room, :involvement), data: {
  4. controller: "turbo-frame", action: "notifications:ready@window->turbo-frame#load", turbo_frame_url_param: room_involvement_path(room)
  5. }, &
  6. end
  7. 1 def button_to_change_involvement(room, involvement)
  8. button_to room_involvement_path(room, involvement: next_involvement_for(room, involvement: involvement)),
  9. method: :put,
  10. role: "checkbox", aria: { checked: true, labelledby: dom_id(room, :involvement_label) }, tabindex: 0,
  11. class: "btn #{involvement}" do
  12. image_tag("notification-bell-#{involvement}.svg", aria: { hidden: "true" }, size: 20) +
  13. tag.span(HUMANIZE_INVOLVEMENT[involvement], class: "for-screen-reader", id: dom_id(room, :involvement_label))
  14. end
  15. end
  16. 1 private
  17. HUMANIZE_INVOLVEMENT = {
  18. 1 "mentions" => "Notifying about @ mentions",
  19. "everything" => "Notifying about all messages",
  20. "nothing" => "Notifications are off",
  21. "invisible" => "Notifications are off and room invisible in sidebar"
  22. }
  23. 1 SHARED_INVOLVEMENT_ORDER = %w[ mentions everything nothing invisible ]
  24. 1 DIRECT_INVOLVEMENT_ORDER = %w[ everything nothing ]
  25. 1 def next_involvement_for(room, involvement:)
  26. if room.direct?
  27. DIRECT_INVOLVEMENT_ORDER[DIRECT_INVOLVEMENT_ORDER.index(involvement) + 1] || DIRECT_INVOLVEMENT_ORDER.first
  28. else
  29. SHARED_INVOLVEMENT_ORDER[SHARED_INVOLVEMENT_ORDER.index(involvement) + 1] || SHARED_INVOLVEMENT_ORDER.first
  30. end
  31. end
  32. end

app/helpers/rooms_helper.rb

78.13% lines covered

32 relevant lines. 25 lines covered and 7 lines missed.
    
  1. 1 module RoomsHelper
  2. 1 def link_to_room(room, **attributes, &)
  3. 202 link_to room_path(room), **attributes, data: {
  4. rooms_list_target: "room", room_id: room.id, badge_dot_target: "unread", sorted_list_target: "item"
  5. }.merge(attributes.delete(:data) || {}), &
  6. end
  7. 1 def link_to_edit_room(room, &)
  8. 35 link_to \
  9. [ :edit, @room ],
  10. class: "btn",
  11. style: "view-transition-name: edit-room-#{@room.id}",
  12. data: { room_id: @room.id },
  13. &
  14. end
  15. 1 def link_back_to_last_room_visited
  16. if last_room = last_room_visited
  17. link_back_to room_path(last_room)
  18. else
  19. link_back_to root_path
  20. end
  21. end
  22. 1 def button_to_delete_room(room, url: nil)
  23. button_to url || room_url(room), method: :delete, class: "btn btn--negative max-width", aria: { label: "Delete #{room.name}" },
  24. data: { turbo_confirm: "Are you sure you want to delete this room and all messages in it? This can’t be undone." } do
  25. image_tag("trash.svg", aria: { hidden: "true" }, size: 20) +
  26. tag.span(room_display_name(room), class: "overflow-ellipsis")
  27. end
  28. end
  29. 1 def button_to_jump_to_newest_message
  30. 35 tag.button \
  31. class: "message-area__return-to-latest btn",
  32. data: { action: "messages#returnToLatest", messages_target: "latest" },
  33. hidden: true do
  34. 35 image_tag("arrow-down.svg", aria: { hidden: "true" }, size: 20) +
  35. tag.span("Jump to newest message", class: "for-screen-reader")
  36. end
  37. end
  38. 1 def submit_room_button_tag
  39. button_tag class: "btn btn--reversed txt-large center", type: "submit" do
  40. image_tag("check.svg", aria: { hidden: "true" }, size: 20) +
  41. tag.span("Save", class: "for-screen-reader")
  42. end
  43. end
  44. 1 def composer_form_tag(room, &)
  45. 35 form_with model: Message.new, url: room_messages_path(room),
  46. id: "composer", class: "margin-block flex-item-grow contain", data: composer_data_options(room), &
  47. end
  48. 1 def room_display_name(room, for_user: Current.user)
  49. 163 if room.direct?
  50. 16 room.users.without(for_user).pluck(:name).to_sentence.presence || for_user&.name
  51. else
  52. 147 room.name
  53. end
  54. end
  55. 1 private
  56. 1 def composer_data_options(room)
  57. {
  58. 35 controller: "composer drop-target",
  59. action: composer_data_actions,
  60. composer_messages_outlet: "#message-area",
  61. composer_toolbar_class: "composer--rich-text", composer_room_id_value: room.id
  62. }
  63. end
  64. 1 def composer_data_actions
  65. 35 drag_and_drop_actions = "drop-target:drop@window->composer#dropFiles"
  66. trix_attachment_actions =
  67. 35 "trix-file-accept->composer#preventAttachment refresh-room:online@window->composer#online"
  68. remaining_actions =
  69. 35 "typing-notifications#stop paste->composer#pasteFiles turbo:submit-end->composer#submitEnd refresh-room:offline@window->composer#offline"
  70. 35 [ drop_target_actions, drag_and_drop_actions, trix_attachment_actions, remaining_actions ].join(" ")
  71. end
  72. end

app/helpers/searches_helper.rb

66.67% lines covered

3 relevant lines. 2 lines covered and 1 lines missed.
    
  1. 1 module SearchesHelper
  2. 1 def search_results_tag(&)
  3. tag.div id: "search-results", class: "messages searches__results", data: {
  4. controller: "search-results",
  5. search_results_target: "messages",
  6. search_results_me_class: "message--me",
  7. search_results_threaded_class: "message--threaded",
  8. search_results_mentioned_class: "message--mentioned",
  9. search_results_formatted_class: "message--formatted"
  10. }, &
  11. end
  12. end

app/helpers/time_helper.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module TimeHelper
  2. 1 def local_datetime_tag(datetime, style: :time, **attributes)
  3. 186 tag.time **attributes, datetime: datetime.iso8601, data: { local_time_target: style }
  4. end
  5. end

app/helpers/translations_helper.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. 1 module TranslationsHelper
  2. TRANSLATIONS = {
  3. 1 email_address: { "🇺🇸": "Enter your email address", "🇪🇸": "Introduce tu correo electrónico", "🇫🇷": "Entrez votre adresse courriel", "🇮🇳": "अपना ईमेल पता दर्ज करें", "🇩🇪": "Geben Sie Ihre E-Mail-Adresse ein", "🇧🇷": "Insira seu endereço de email" },
  4. password: { "🇺🇸": "Enter your password", "🇪🇸": "Introduce tu contraseña", "🇫🇷": "Saisissez votre mot de passe", "🇮🇳": "अपना पासवर्ड दर्ज करें", "🇩🇪": "Geben Sie Ihr Passwort ein", "🇧🇷": "Insira sua senha" },
  5. update_password: { "🇺🇸": "Change password", "🇪🇸": "Cambiar contraseña", "🇫🇷": "Changer le mot de passe", "🇮🇳": "पासवर्ड बदलें", "🇩🇪": "Passwort ändern", "🇧🇷": "Alterar senha" },
  6. user_name: { "🇺🇸": "Enter your name", "🇪🇸": "Introduce tu nombre", "🇫🇷": "Entrez votre nom", "🇮🇳": "अपना नाम दर्ज करें", "🇩🇪": "Geben Sie Ihren Namen ein", "🇧🇷": "Insira seu nome" },
  7. account_name: { "🇺🇸": "Name this account", "🇪🇸": "Nombre de esta cuenta", "🇫🇷": "Nommez ce compte", "🇮🇳": "इस खाते का नाम दें", "🇩🇪": "Benennen Sie dieses Konto", "🇧🇷": "Dê um nome a essa conta" },
  8. room_name: { "🇺🇸": "Name the room", "🇪🇸": "Nombrar la sala", "🇫🇷": "Nommez la salle", "🇮🇳": "कमरे का नाम दें", "🇩🇪": "Geben Sie dem Raum einen Namen", "🇧🇷": "Dê um nome a essa sala" },
  9. invite_message: { "🇺🇸": "Welcome to Campfire. To invite some people to chat with you, share the join link below.", "🇪🇸": "Bienvenido a Campfire. Para invitar a algunas personas a chatear contigo, comparte el enlace de unión que se encuentra a continuación.", "🇫🇷": "Bienvenue sur Campfire. Pour inviter des personnes à discuter avec vous, partagez le lien pour rejoindre ci-dessous.", "🇮🇳": "Campfire में आपका स्वागत है। अधिक लोगों को चैट के लिए आमंत्रित करने के लिए, नीचे जुड़ने का लिंक साझा करें।", "🇩🇪": "Willkommen bei Campfire. Um einige Personen zum Chatten einzuladen, teilen Sie den unten stehenden Beitrittslink.", "🇧🇷": "Boas vindas ao Campfire. Para convidar pessoas para conversarem com você, compartilhe o link de convite abaixo." },
  10. incompatible_browser_messsage: { "🇺🇸": "Upgrade to a supported web browser. Campfire requires a modern web browser. Please use one of the browsers listed below and make sure auto-updates are enabled.", "🇪🇸": "Actualiza a un navegador web compatible. Campfire requiere un navegador web moderno. Utiliza uno de los navegadores listados a continuación y asegúrate de que las actualizaciones automáticas estén habilitadas.", "🇫🇷": "Mettez à jour vers un navigateur web pris en charge. Campfire nécessite un navigateur web moderne. Veuillez utiliser l'un des navigateurs répertoriés ci-dessous et assurez-vous que les mises à jour automatiques sont activées.", "🇮🇳": "समर्थित वेब ब्राउज़र में अपग्रेड करें। Campfire को एक आधुनिक वेब ब्राउज़र की आवश्यकता है। कृपया नीचे सूचीबद्ध ब्राउज़रों में से कोई एक का उपयोग करें और सुनिश्चित करें कि स्वचालित अपडेट्स सक्षम हैं।", "🇩🇪": "Aktualisieren Sie auf einen unterstützten Webbrowser. Campfire erfordert einen modernen Webbrowser. Verwenden Sie bitte einen der unten aufgeführten Browser und stellen Sie sicher, dass automatische Updates aktiviert sind.", "🇧🇷": "Atualize para um navegador compatível. O Campfire requer um navegador moderno. Por favor, use um dos navegadores listados abaixo e certifique-se de que as atualizações automáticas estão ativadas." },
  11. bio: { "🇺🇸": "Enter a few words about yourself.", "🇪🇸": "Ingresa algunas palabras sobre ti mismo.", "🇫🇷": "Saisissez quelques mots à propos de vous-même.", "🇮🇳": "अपने बारे में कुछ शब्द लिखें.", "🇩🇪": "Geben Sie ein paar Worte über sich selbst ein.", "🇧🇷": "Insira alguma palavras sobre você." },
  12. webhook_url: { "🇺🇸": "Webhook URL", "🇪🇸": "URL del Webhook", "🇫🇷": "URL du webhook", "🇮🇳": "वेबहुक URL", "🇩🇪": "Webhook-URL", "🇧🇷": "URL do Webhook" },
  13. chat_bots: { "🇺🇸": "Chat bots. With Chat bots, other sites and services can post updates directly to Campfire.", "🇪🇸": "Bots de chat. Con los bots de chat, otros sitios y servicios pueden publicar actualizaciones directamente en Campfire.", "🇫🇷": "Bots de discussion. Avec les bots de discussion, d'autres sites et services peuvent publier des mises à jour directement sur Campfire.", "🇮🇳": "चैट बॉट। चैट बॉट के साथ, अन्य साइटों और सेवाएं सीधे कैम्पफायर में अपडेट पोस्ट कर सकती हैं।", "🇩🇪": "Chat-Bots. Mit Chat-Bots können andere Websites und Dienste Updates direkt in Campfire veröffentlichen.", "🇧🇷": "Chat bots. Com Chat bots, outros sites e serviços podem postar atualizações diretamente no Campfire." },
  14. bot_name: { "🇺🇸": "Name the bot", "🇪🇸": "Nombrar al bot", "🇫🇷": "Nommer le bot", "🇮🇳": "बॉट का नाम दें", "🇩🇪": "Benenne den Bot", "🇧🇷": "Dê um nome ao bot" },
  15. custom_styles: { "🇺🇸": "Add custom CSS styles. Use Caution: you could break things.", "🇪🇸": "Agrega estilos CSS personalizados. Usa precaución: podrías romper cosas.", "🇫🇷": "Ajoutez des styles CSS personnalisés. Utilisez avec précaution : vous pourriez casser des choses.", "🇮🇳": "कस्टम CSS स्टाइल जोड़ें। सावधानी बरतें: आप चीज़ों को तोड़ सकते हैं।", "🇩🇪": "Fügen Sie benutzerdefinierte CSS-Stile hinzu. Vorsicht: Sie könnten Dinge kaputt machen.", "🇧🇷": "Adicione estilos CSS personalizados. Use com cuidado: você pode quebrar coisas." }
  16. }
  17. 1 def translations_for(translation_key)
  18. 30 tag.dl(class: "language-list") do
  19. 30 TRANSLATIONS[translation_key].map do |language, translation|
  20. 180 concat tag.dt(language)
  21. 180 concat tag.dd(translation, class: "margin-none")
  22. end
  23. end
  24. end
  25. 1 def translation_button(translation_key)
  26. 30 tag.details(class: "position-relative", data: { controller: "popup", action: "keydown.esc->popup#close toggle->popup#toggle click@document->popup#closeOnClickOutside", popup_orientation_top_class: "popup-orientation-top" }) do
  27. 30 tag.summary(class: "btn", tabindex: -1) do
  28. 30 concat image_tag("globe.svg", size: 20, aria: { hidden: "true" }, class: "color-icon")
  29. 30 concat tag.span("Translate", class: "for-screen-reader")
  30. end +
  31. tag.div(class: "lanuage-list-menu shadow", data: { popup_target: "menu" }) do
  32. 30 translations_for(translation_key)
  33. end
  34. end
  35. end
  36. end

app/helpers/users/avatars_helper.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 require "zlib"
  2. 1 module Users::AvatarsHelper
  3. AVATAR_COLORS = %w[
  4. 1 #AF2E1B #CC6324 #3B4B59 #BFA07A #ED8008 #ED3F1C #BF1B1B #736B1E #D07B53
  5. #736356 #AD1D1D #BF7C2A #C09C6F #698F9C #7C956B #5D618F #3B3633 #67695E
  6. ]
  7. 1 def avatar_background_color(user)
  8. 60 AVATAR_COLORS[Zlib.crc32(user.to_param) % AVATAR_COLORS.size]
  9. end
  10. 1 def avatar_tag(user, **options)
  11. 162 link_to user_path(user), title: user.title, class: "btn avatar", data: { turbo_frame: "_top" } do
  12. 162 image_tag fresh_user_avatar_path(user), aria: { hidden: "true" }, size: 48, **options
  13. end
  14. end
  15. end

app/helpers/users/filter_helper.rb

60.0% lines covered

5 relevant lines. 3 lines covered and 2 lines missed.
    
  1. 1 module Users::FilterHelper
  2. 1 def user_filter_menu_tag(&)
  3. tag.menu class: "flex flex-column gap margin-none pad overflow-y constrain-height",
  4. data: { controller: "filter", filter_active_class: "filter--active", filter_selected_class: "selected" }, &
  5. end
  6. 1 def user_filter_search_tag
  7. tag.input type: "search", id: "search", autocorrect: "off", autocomplete: "off", "data-1p-ignore": "true", class: "input input--transparent full-width", placeholder: "Filter…", data: { action: "input->filter#filter" }
  8. end
  9. end

app/helpers/users/profiles_helper.rb

50.0% lines covered

8 relevant lines. 4 lines covered and 4 lines missed.
    
  1. 1 module Users::ProfilesHelper
  2. 1 def profile_form_with(model, **params, &)
  3. form_with \
  4. model: @user, url: user_profile_path, method: :patch,
  5. data: { controller: "form" },
  6. **params,
  7. &
  8. end
  9. 1 def profile_form_submit_button
  10. tag.button class: "btn btn--reversed center txt-large", type: "submit" do
  11. image_tag("check.svg", aria: { hidden: "true" }, size: 20) +
  12. tag.span("Save changes", class: "for-screen-reader")
  13. end
  14. end
  15. 1 def web_share_session_button(url, title, text, &)
  16. tag.button class: "btn", hidden: true, data: {
  17. controller: "web-share", action: "web-share#share",
  18. web_share_url_value: url,
  19. web_share_text_value: text,
  20. web_share_title_value: title
  21. }, &
  22. end
  23. end

app/helpers/users/sidebar_helper.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module Users::SidebarHelper
  2. 1 def sidebar_turbo_frame_tag(src: nil, &)
  3. 94 turbo_frame_tag :user_sidebar, src: src, target: "_top", data: {
  4. turbo_permanent: true,
  5. controller: "rooms-list read-rooms turbo-frame",
  6. rooms_list_unread_class: "unread",
  7. action: "presence:present@window->rooms-list#read read-rooms:read->rooms-list#read turbo:frame-load->rooms-list#loaded refresh-room:visible@window->turbo-frame#reload".html_safe # otherwise -> is escaped
  8. }, &
  9. end
  10. end

app/helpers/users_helper.rb

50.0% lines covered

4 relevant lines. 2 lines covered and 2 lines missed.
    
  1. 1 module UsersHelper
  2. 1 def button_to_direct_room_with(user)
  3. button_to rooms_directs_path(user_ids: [ user.id ]), class: "btn btn--primary full-width txt--large" do
  4. image_tag("messages.svg")
  5. end
  6. end
  7. end

app/helpers/version_helper.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module VersionHelper
  2. 1 def version_badge
  3. 15 tag.span(Rails.application.config.app_version, class: "version-badge")
  4. end
  5. end

app/jobs/application_job.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. 1 class ApplicationJob < ActiveJob::Base
  2. # Automatically retry jobs that encountered a deadlock
  3. # retry_on ActiveRecord::Deadlocked
  4. # Most jobs are safe to ignore if the underlying records are no longer available
  5. # discard_on ActiveJob::DeserializationError
  6. end

app/jobs/room/push_message_job.rb

66.67% lines covered

3 relevant lines. 2 lines covered and 1 lines missed.
    
  1. 1 class Room::PushMessageJob < ApplicationJob
  2. 1 def perform(room, message)
  3. Room::MessagePusher.new(room:, message:).push
  4. end
  5. end

app/models/account.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 class Account < ApplicationRecord
  2. 1 include Joinable
  3. 1 has_one_attached :logo
  4. end

app/models/account/joinable.rb

77.78% lines covered

9 relevant lines. 7 lines covered and 2 lines missed.
    
  1. 1 module Account::Joinable
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 before_create { self.join_code = generate_join_code }
  5. end
  6. 1 def reset_join_code
  7. update! join_code: generate_join_code
  8. end
  9. 1 private
  10. 1 def generate_join_code
  11. SecureRandom.alphanumeric(12).scan(/.{4}/).join("-")
  12. end
  13. end

app/models/application_platform.rb

71.88% lines covered

32 relevant lines. 23 lines covered and 9 lines missed.
    
  1. 1 class ApplicationPlatform < PlatformAgent
  2. 1 def ios?
  3. 140 match? /iPhone|iPad/
  4. end
  5. 1 def android?
  6. 105 match? /Android/
  7. end
  8. 1 def mac?
  9. match? /Macintosh/
  10. end
  11. 1 def chrome?
  12. 175 user_agent.browser.match? /Chrome/
  13. end
  14. 1 def firefox?
  15. 210 user_agent.browser.match? /Firefox|FxiOS/
  16. end
  17. 1 def safari?
  18. 140 user_agent.browser.match? /Safari/
  19. end
  20. 1 def edge?
  21. 105 user_agent.browser.match? /Edg/
  22. end
  23. 1 def apple_messages?
  24. # Apple Messages pretends to be Facebook and Twitter bots via spoofed user agent.
  25. # We want to avoid showing "Unsupported browser" message when a Campfire link
  26. # is shared via Messages.
  27. match?(/facebookexternalhit/i) && match?(/Twitterbot/i)
  28. end
  29. 1 def mobile?
  30. 105 ios? || android?
  31. end
  32. 1 def desktop?
  33. 105 !mobile?
  34. end
  35. 1 def windows?
  36. operating_system == "Windows"
  37. end
  38. 1 def operating_system
  39. 35 case user_agent.platform
  40. when /Android/ then "Android"
  41. when /iPad/ then "iPad"
  42. when /iPhone/ then "iPhone"
  43. when /Macintosh/ then "macOS"
  44. when /Windows/ then "Windows"
  45. when /CrOS/ then "ChromeOS"
  46. else
  47. 35 os =~ /Linux/ ? "Linux" : os
  48. end
  49. end
  50. end

app/models/application_record.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 class ApplicationRecord < ActiveRecord::Base
  2. 1 primary_abstract_class
  3. end

app/models/boost.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 class Boost < ApplicationRecord
  2. 1 belongs_to :message, touch: true
  3. 3 belongs_to :booster, class_name: "User", default: -> { Current.user }
  4. 96 scope :ordered, -> { order(:created_at) }
  5. end

app/models/current.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class Current < ActiveSupport::CurrentAttributes
  2. 1 attribute :user, :request
  3. 1 delegate :host, :protocol, to: :request, prefix: true, allow_nil: true
  4. 1 def account
  5. 315 Account.first
  6. end
  7. end

app/models/membership.rb

92.86% lines covered

14 relevant lines. 13 lines covered and 1 lines missed.
    
  1. 1 class Membership < ApplicationRecord
  2. 1 include Connectable
  3. 1 belongs_to :room
  4. 1 belongs_to :user
  5. 1 after_destroy_commit { user.reset_remote_connections }
  6. 1 enum involvement: %w[ invisible nothing mentions everything ].index_by(&:itself), _prefix: :involved_in
  7. 60 scope :with_ordered_room, -> { includes(:room).joins(:room).order("LOWER(rooms.name)") }
  8. 1 scope :without_direct_rooms, -> { joins(:room).where.not(room: { type: "Rooms::Direct" }) }
  9. 64 scope :visible, -> { where.not(involvement: :invisible) }
  10. 1 scope :unread, -> { where.not(unread_at: nil) }
  11. 1 def read
  12. update!(unread_at: nil)
  13. end
  14. 1 def unread?
  15. 202 unread_at.present?
  16. end
  17. end

app/models/membership/connectable.rb

78.57% lines covered

28 relevant lines. 22 lines covered and 6 lines missed.
    
  1. 1 module Membership::Connectable
  2. 1 extend ActiveSupport::Concern
  3. 1 CONNECTION_TTL = 60.seconds
  4. 1 included do
  5. 1 scope :connected, -> { where(connected_at: CONNECTION_TTL.ago..) }
  6. 5 scope :disconnected, -> { where(connected_at: [ nil, ...CONNECTION_TTL.ago ]) }
  7. end
  8. 1 class_methods do
  9. 1 def disconnect_all
  10. connected.update_all connected_at: nil, connections: 0, updated_at: Time.current
  11. end
  12. 1 def connect(membership, connections)
  13. 24 where(id: membership.id).update_all(connections: connections, connected_at: Time.current, unread_at: nil)
  14. end
  15. end
  16. 1 def connected?
  17. 48 connected_at? && connected_at >= CONNECTION_TTL.ago
  18. end
  19. 1 def present
  20. 24 self.class.connect(self, connected? ? connections + 1 : 1)
  21. end
  22. 1 def connected
  23. increment_connections
  24. touch :connected_at
  25. end
  26. 1 def disconnected
  27. 24 decrement_connections
  28. 24 update! connected_at: nil if connections < 1
  29. end
  30. 1 def refresh_connection
  31. increment_connections unless connected?
  32. touch :connected_at
  33. end
  34. 1 def increment_connections
  35. connected? ? increment!(:connections, touch: true) : update!(connections: 1)
  36. end
  37. 1 def decrement_connections
  38. 24 connected? ? decrement!(:connections, touch: true) : update!(connections: 0)
  39. end
  40. end

app/models/message.rb

90.48% lines covered

21 relevant lines. 19 lines covered and 2 lines missed.
    
  1. 1 class Message < ApplicationRecord
  2. 1 include Attachment, Broadcasts, Mentionee, Pagination, Searchable
  3. 1 belongs_to :room, touch: true
  4. 5 belongs_to :creator, class_name: "User", default: -> { Current.user }
  5. 1 has_many :boosts, dependent: :destroy
  6. 1 has_rich_text :body
  7. 5 before_create -> { self.client_message_id ||= Random.uuid } # Bots don't care
  8. 5 after_create_commit -> { room.receive(self) }
  9. 84 scope :ordered, -> { order(:created_at) }
  10. 84 scope :with_creator, -> { includes(:creator) }
  11. 1 def plain_text_body
  12. 293 body.to_plain_text.presence || attachment&.filename&.to_s || ""
  13. end
  14. 1 def to_key
  15. 1574 [ client_message_id ]
  16. end
  17. 1 def content_type
  18. case
  19. 191 when attachment? then "attachment"
  20. when sound.present? then "sound"
  21. 191 else "text"
  22. end.inquiry
  23. end
  24. 1 def sound
  25. 191 plain_text_body.match(/\A\/play (?<name>\w+)\z/) do |match|
  26. Sound.find_by_name match[:name]
  27. end
  28. end
  29. end

app/models/message/attachment.rb

90.91% lines covered

22 relevant lines. 20 lines covered and 2 lines missed.
    
  1. 1 module Message::Attachment
  2. 1 extend ActiveSupport::Concern
  3. 1 THUMBNAIL_MAX_WIDTH = 1200
  4. 1 THUMBNAIL_MAX_HEIGHT = 800
  5. 1 included do
  6. 1 has_one_attached :attachment do |attachable|
  7. 1 attachable.variant :thumb, resize_to_limit: [ THUMBNAIL_MAX_WIDTH, THUMBNAIL_MAX_HEIGHT ]
  8. end
  9. end
  10. 1 module ClassMethods
  11. 1 def create_with_attachment!(attributes)
  12. 4 create!(attributes).tap(&:process_attachment)
  13. end
  14. end
  15. 1 def attachment?
  16. 191 attachment.attached?
  17. end
  18. 1 def process_attachment
  19. 4 ensure_attachment_analyzed
  20. 4 process_attachment_thumbnail
  21. end
  22. 1 private
  23. 1 def ensure_attachment_analyzed
  24. 4 attachment&.analyze
  25. end
  26. 1 def process_attachment_thumbnail
  27. case
  28. 4 when attachment.video?
  29. attachment.preview(format: :webp).processed
  30. when attachment.representable?
  31. attachment.representation(:thumb).processed
  32. end
  33. end
  34. end

app/models/message/broadcasts.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 module Message::Broadcasts
  2. 1 def broadcast_create
  3. 4 broadcast_append_to room, :messages, target: [ room, :messages ]
  4. 4 ActionCable.server.broadcast("unread_rooms", { roomId: room.id })
  5. end
  6. end

app/models/message/mentionee.rb

88.89% lines covered

9 relevant lines. 8 lines covered and 1 lines missed.
    
  1. 1 module Message::Mentionee
  2. 1 extend ActiveSupport::Concern
  3. 1 def mentionees
  4. 4 room.users.where(id: mentioned_users.map(&:id))
  5. end
  6. 1 private
  7. 1 def mentioned_users
  8. 4 if body.body
  9. 4 body.body.attachables.grep(User).uniq
  10. else
  11. []
  12. end
  13. end
  14. end

app/models/message/pagination.rb

88.24% lines covered

17 relevant lines. 15 lines covered and 2 lines missed.
    
  1. 1 module Message::Pagination
  2. 1 extend ActiveSupport::Concern
  3. 1 PAGE_SIZE = 40
  4. 1 included do
  5. 60 scope :last_page, -> { ordered.last(PAGE_SIZE) }
  6. 25 scope :first_page, -> { ordered.first(PAGE_SIZE) }
  7. 1 scope :before, ->(message) { where("created_at < ?", message.created_at) }
  8. 1 scope :after, ->(message) { where("created_at > ?", message.created_at) }
  9. 1 scope :page_before, ->(message) { before(message).last_page }
  10. 1 scope :page_after, ->(message) { after(message).first_page }
  11. 25 scope :page_created_since, ->(time) { where("created_at > ?", time).first_page }
  12. 25 scope :page_updated_since, ->(time) { where("updated_at > ?", time).last_page }
  13. end
  14. 1 class_methods do
  15. 1 def page_around(message)
  16. page_before(message) + [ message ] + page_after(message)
  17. end
  18. 1 def paged?
  19. count > PAGE_SIZE
  20. end
  21. end
  22. end

app/models/message/searchable.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. 1 module Message::Searchable
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 after_create_commit :create_in_index
  5. 1 after_update_commit :update_in_index
  6. 1 after_destroy_commit :remove_from_index
  7. 1 scope :search, ->(query) { joins("join message_search_index idx on messages.id = idx.rowid").where("idx.body match ?", query).ordered }
  8. end
  9. 1 private
  10. 1 def create_in_index
  11. 4 execute_sql_with_binds "insert into message_search_index(rowid, body) values (?, ?)", id, plain_text_body
  12. end
  13. 1 def update_in_index
  14. 5 execute_sql_with_binds "update message_search_index set body = ? where rowid = ?", plain_text_body, id
  15. end
  16. 1 def remove_from_index
  17. 1 execute_sql_with_binds "delete from message_search_index where rowid = ?", id
  18. end
  19. 1 def execute_sql_with_binds(*statement)
  20. 10 self.class.connection.execute self.class.sanitize_sql(statement)
  21. end
  22. end

app/models/push.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module Push
  2. 1 def self.table_name_prefix
  3. 1 "push_"
  4. end
  5. end

app/models/push/subscription.rb

75.0% lines covered

4 relevant lines. 3 lines covered and 1 lines missed.
    
  1. 1 class Push::Subscription < ApplicationRecord
  2. 1 belongs_to :user
  3. 1 def notification(**params)
  4. WebPush::Notification.new(**params, badge: user.memberships.unread.count, endpoint: endpoint, p256dh_key: p256dh_key, auth_key: auth_key)
  5. end
  6. end

app/models/room.rb

71.43% lines covered

42 relevant lines. 30 lines covered and 12 lines missed.
    
  1. 1 class Room < ApplicationRecord
  2. 1 has_many :memberships, dependent: :delete_all do
  3. 1 def grant_to(users)
  4. room = proxy_association.owner
  5. Membership.insert_all(Array(users).collect { |user| { room_id: room.id, user_id: user.id, involvement: room.default_involvement } })
  6. end
  7. 1 def revoke_from(users)
  8. destroy_by user: users
  9. end
  10. 1 def revise(granted: [], revoked: [])
  11. transaction do
  12. grant_to(granted) if granted.present?
  13. revoke_from(revoked) if revoked.present?
  14. end
  15. end
  16. end
  17. 1 has_many :users, through: :memberships
  18. 1 has_many :messages, dependent: :destroy
  19. 1 belongs_to :creator, class_name: "User", default: -> { Current.user }
  20. 1 scope :opens, -> { where(type: "Rooms::Open") }
  21. 1 scope :closeds, -> { where(type: "Rooms::Closed") }
  22. 60 scope :directs, -> { where(type: "Rooms::Direct") }
  23. 1 scope :without_directs, -> { where.not(type: "Rooms::Direct") }
  24. 1 scope :ordered, -> { order("LOWER(name)") }
  25. 1 class << self
  26. 1 def create_for(attributes, users:)
  27. transaction do
  28. create!(attributes).tap do |room|
  29. room.memberships.grant_to users
  30. end
  31. end
  32. end
  33. 1 def original
  34. 50 order(:created_at).first
  35. end
  36. end
  37. 1 def receive(message)
  38. 4 unread_memberships(message)
  39. 4 push_later(message)
  40. end
  41. 1 def open?
  42. is_a?(Rooms::Open)
  43. end
  44. 1 def closed?
  45. is_a?(Rooms::Closed)
  46. end
  47. 1 def direct?
  48. 474 is_a?(Rooms::Direct)
  49. end
  50. 1 def default_involvement
  51. "mentions"
  52. end
  53. 1 private
  54. 1 def unread_memberships(message)
  55. 4 memberships.visible.disconnected.where.not(user: message.creator).update_all(unread_at: message.created_at, updated_at: Time.current)
  56. end
  57. 1 def push_later(message)
  58. 4 Room::PushMessageJob.perform_later(self, message)
  59. end
  60. end

app/models/rooms/closed.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. # Rooms where only a subset of all users on the account have been explicited granted membership.
  2. 1 class Rooms::Closed < Room
  3. end

app/models/rooms/direct.rb

60.0% lines covered

10 relevant lines. 6 lines covered and 4 lines missed.
    
  1. # Rooms for direct message chats between users. These act as a singleton, so a single set of users will
  2. # always refer to the same direct room.
  3. 1 class Rooms::Direct < Room
  4. 1 class << self
  5. 1 def find_or_create_for(users)
  6. find_for(users) || create_for({}, users: users)
  7. end
  8. 1 private
  9. # FIXME: Find a more performant algorithm that won't be a problem on accounts with 10K+ direct rooms,
  10. # which could be to store the membership id list as a hash on the room, and use that for lookup.
  11. 1 def find_for(users)
  12. all.joins(:users).detect do |room|
  13. Set.new(room.user_ids) == Set.new(users.pluck(:id))
  14. end
  15. end
  16. end
  17. 1 def default_involvement
  18. "everything"
  19. end
  20. end

app/models/rooms/open.rb

80.0% lines covered

5 relevant lines. 4 lines covered and 1 lines missed.
    
  1. # Rooms open to all users on the account. When a new user is added to the account, they're automatically granted membership.
  2. 1 class Rooms::Open < Room
  3. 1 after_save_commit :grant_access_to_all_users
  4. 1 private
  5. 1 def grant_access_to_all_users
  6. memberships.grant_to(User.active) if type_previously_changed?(to: "Rooms::Open")
  7. end
  8. end

app/models/search.rb

80.0% lines covered

10 relevant lines. 8 lines covered and 2 lines missed.
    
  1. 1 class Search < ApplicationRecord
  2. 1 belongs_to :user
  3. 1 after_create :trim_recent_searches
  4. 1 scope :ordered, -> { order(updated_at: :desc) }
  5. 1 class << self
  6. 1 def record(query)
  7. find_or_create_by(query: query).touch
  8. end
  9. end
  10. 1 private
  11. 1 def trim_recent_searches
  12. user.searches.excluding(user.searches.ordered.limit(10)).destroy_all
  13. end
  14. end

app/models/session.rb

90.0% lines covered

10 relevant lines. 9 lines covered and 1 lines missed.
    
  1. 1 class Session < ApplicationRecord
  2. 1 ACTIVITY_REFRESH_RATE = 1.hour
  3. 1 has_secure_token
  4. 1 belongs_to :user
  5. 16 before_create { self.last_active_at ||= Time.now }
  6. 1 def self.start!(user_agent:, ip_address:)
  7. 15 create! user_agent: user_agent, ip_address: ip_address
  8. end
  9. 1 def resume(user_agent:, ip_address:)
  10. 229 if last_active_at.before?(ACTIVITY_REFRESH_RATE.ago)
  11. update! user_agent: user_agent, ip_address: ip_address, last_active_at: Time.now
  12. end
  13. end
  14. end

app/models/user.rb

68.42% lines covered

38 relevant lines. 26 lines covered and 12 lines missed.
    
  1. 1 class User < ApplicationRecord
  2. 1 include Avatar, Bot, Mentionable, Role, Transferable
  3. 1 has_many :memberships, dependent: :delete_all
  4. 1 has_many :rooms, through: :memberships
  5. 1 has_many :reachable_messages, through: :rooms, source: :messages
  6. 1 has_many :messages, dependent: :destroy, foreign_key: :creator_id
  7. 1 has_many :push_subscriptions, class_name: "Push::Subscription", dependent: :delete_all
  8. 1 has_many :boosts, dependent: :destroy, foreign_key: :booster_id
  9. 1 has_many :searches, dependent: :delete_all
  10. 1 has_many :sessions, dependent: :destroy
  11. 79 scope :active, -> { where(active: true) }
  12. 1 has_secure_password validations: false
  13. 1 after_create_commit :grant_membership_to_open_rooms
  14. 1 scope :ordered, -> { order("LOWER(name)") }
  15. 1 scope :filtered_by, ->(query) { where("name like ?", "%#{query}%") }
  16. 1 def initials
  17. 120 name.scan(/\b\w/).join
  18. end
  19. 1 def title
  20. 255 [ name, bio ].compact_blank.join(" – ")
  21. end
  22. 1 def deactivate
  23. transaction do
  24. close_remote_connections
  25. memberships.without_direct_rooms.delete_all
  26. push_subscriptions.delete_all
  27. searches.delete_all
  28. sessions.delete_all
  29. update! active: false, email_address: deactived_email_address
  30. end
  31. end
  32. 1 def deactivated?
  33. !active?
  34. end
  35. 1 def reset_remote_connections
  36. close_remote_connections reconnect: true
  37. end
  38. 1 private
  39. 1 def grant_membership_to_open_rooms
  40. Membership.insert_all(Rooms::Open.pluck(:id).collect { |room_id| { room_id: room_id, user_id: id } })
  41. end
  42. 1 def deactived_email_address
  43. email_address&.gsub(/@/, "-deactivated-#{SecureRandom.uuid}@")
  44. end
  45. 1 def close_remote_connections(reconnect: false)
  46. ActionCable.server.remote_connections.where(current_user: self).disconnect reconnect: reconnect
  47. end
  48. end

app/models/user/avatar.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 module User::Avatar
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 has_one_attached :avatar
  5. end
  6. 1 class_methods do
  7. 1 def from_avatar_token(sid)
  8. 75 find_signed!(sid, purpose: :avatar)
  9. end
  10. end
  11. 1 def avatar_token
  12. 457 signed_id(purpose: :avatar)
  13. end
  14. end

app/models/user/bot.rb

52.78% lines covered

36 relevant lines. 19 lines covered and 17 lines missed.
    
  1. 1 module User::Bot
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 5 scope :active_bots, -> { active.where(role: :bot) }
  5. 1 scope :without_bots, -> { where.not(role: :bot) }
  6. 1 has_one :webhook, dependent: :delete
  7. end
  8. 1 module ClassMethods
  9. 1 def create_bot!(attributes)
  10. bot_token = generate_bot_token
  11. webhook_url = attributes.delete(:webhook_url)
  12. User.create!(**attributes, bot_token: bot_token, role: :bot).tap do |user|
  13. user.create_webhook!(url: webhook_url) if webhook_url
  14. end
  15. end
  16. 1 def authenticate_bot(bot_key)
  17. bot_id, bot_token = bot_key.split("-")
  18. active.find_by(id: bot_id, bot_token: bot_token)
  19. end
  20. 1 def generate_bot_token
  21. 1 SecureRandom.alphanumeric(12)
  22. end
  23. end
  24. 1 def update_bot!(attributes)
  25. transaction do
  26. update_webhook_url!(attributes.delete(:webhook_url))
  27. update!(attributes)
  28. end
  29. end
  30. 1 def bot_key
  31. "#{id}-#{bot_token}"
  32. end
  33. 1 def reset_bot_key
  34. update! bot_token: self.class.generate_bot_token
  35. end
  36. 1 def webhook_url
  37. webhook&.url
  38. end
  39. 1 def deliver_webhook_later(message)
  40. Bot::WebhookJob.perform_later(self, message) if webhook
  41. end
  42. 1 def deliver_webhook(message)
  43. webhook.deliver(message)
  44. end
  45. 1 private
  46. 1 def update_webhook_url!(url)
  47. if url.present?
  48. webhook&.update!(url: url) || create_webhook!(url: url)
  49. else
  50. webhook&.destroy
  51. end
  52. end
  53. end

app/models/user/mentionable.rb

62.5% lines covered

8 relevant lines. 5 lines covered and 3 lines missed.
    
  1. 1 module User::Mentionable
  2. 1 include ActionText::Attachable
  3. 1 def to_attachable_partial_path
  4. "users/mention"
  5. end
  6. 1 def to_trix_content_attachment_partial_path
  7. "users/mention"
  8. end
  9. 1 def attachable_plain_text_representation(caption)
  10. "@#{name}"
  11. end
  12. end

app/models/user/role.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 module User::Role
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 enum role: %i[ member administrator bot ]
  5. end
  6. 1 def can_administer?(record = nil)
  7. 46 administrator? || self == record&.creator || record&.new_record?
  8. end
  9. end

app/models/user/transferable.rb

75.0% lines covered

8 relevant lines. 6 lines covered and 2 lines missed.
    
  1. 1 module User::Transferable
  2. 1 extend ActiveSupport::Concern
  3. 1 TRANSFER_LINK_EXPIRY_DURATION = 4.hours
  4. 1 class_methods do
  5. 1 def find_by_transfer_id(id)
  6. find_signed(id, purpose: :transfer)
  7. end
  8. end
  9. 1 def transfer_id
  10. signed_id(purpose: :transfer, expires_in: TRANSFER_LINK_EXPIRY_DURATION)
  11. end
  12. end

app/models/webhook.rb

45.0% lines covered

40 relevant lines. 18 lines covered and 22 lines missed.
    
  1. 1 require "net/http"
  2. 1 require "uri"
  3. 1 class Webhook < ApplicationRecord
  4. 1 ENDPOINT_TIMEOUT = 7.seconds
  5. 1 belongs_to :user
  6. 1 def deliver(message)
  7. post(payload(message)).tap do |response|
  8. if text = extract_text_from(response)
  9. receive_text_reply_to(message.room, text: text)
  10. elsif attachment = extract_attachment_from(response)
  11. receive_attachment_reply_to(message.room, attachment: attachment)
  12. end
  13. end
  14. rescue Net::OpenTimeout, Net::ReadTimeout
  15. receive_text_reply_to message.room, text: "Failed to respond within #{ENDPOINT_TIMEOUT} seconds"
  16. end
  17. 1 private
  18. 1 def post(payload)
  19. http.request \
  20. Net::HTTP::Post.new(uri, "Content-Type" => "application/json").tap { |request| request.body = payload }
  21. end
  22. 1 def http
  23. Net::HTTP.new(uri.host, uri.port).tap do |http|
  24. http.use_ssl = (uri.scheme == "https")
  25. http.open_timeout = ENDPOINT_TIMEOUT
  26. http.read_timeout = ENDPOINT_TIMEOUT
  27. end
  28. end
  29. 1 def uri
  30. @uri ||= URI(url)
  31. end
  32. 1 def payload(message)
  33. {
  34. user: { id: message.creator.id, name: message.creator.name },
  35. room: { id: message.room.id, name: message.room.name, path: room_bot_messages_path(message) },
  36. message: { id: message.id, body: { html: message.body.body, plain: without_recipient_mentions(message.plain_text_body) }, path: message_path(message) }
  37. }.to_json
  38. end
  39. 1 def message_path(message)
  40. Rails.application.routes.url_helpers.room_at_message_path(message.room, message)
  41. end
  42. 1 def room_bot_messages_path(message)
  43. Rails.application.routes.url_helpers.room_bot_messages_path(message.room, user.bot_key)
  44. end
  45. 1 def extract_text_from(response)
  46. response.body.force_encoding("UTF-8") if response.code == "200" && response.content_type.in?(%w[ text/html text/plain ])
  47. end
  48. 1 def receive_text_reply_to(room, text:)
  49. room.messages.create!(body: text, creator: user).broadcast_create
  50. end
  51. 1 def extract_attachment_from(response)
  52. if response.content_type && mime_type = Mime::Type.lookup(response.content_type)
  53. ActiveStorage::Blob.create_and_upload! \
  54. io: StringIO.new(response.body), filename: "attachment.#{mime_type.symbol}", content_type: mime_type.to_s
  55. end
  56. end
  57. 1 def receive_attachment_reply_to(room, attachment:)
  58. room.messages.create_with_attachment!(attachment: attachment, creator: user).broadcast_create
  59. end
  60. 1 def without_recipient_mentions(body)
  61. body \
  62. .gsub(user.attachable_plain_text_representation(nil), "") # Remove mentions of the recipient user
  63. .gsub(/\A\p{Space}+|\p{Space}+\z/, "") # Remove leading and trailing whitespace uncluding unicode spaces
  64. end
  65. end

config/environment.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. # Load the Rails application.
  2. 1 require_relative "application"
  3. # Initialize the Rails application.
  4. 1 Rails.application.initialize!

config/environments/test.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. 1 require "active_support/core_ext/integer/time"
  2. # The test environment is used exclusively to run your application's
  3. # test suite. You never need to work with it otherwise. Remember that
  4. # your test database is "scratch space" for the test suite and is wiped
  5. # and recreated between test runs. Don't rely on the data there!
  6. 1 Rails.application.configure do
  7. # Settings specified here will take precedence over those in config/application.rb.
  8. # Turn false under Spring and add config.action_view.cache_template_loading = true.
  9. 1 config.cache_classes = true
  10. # Eager loading loads your whole application. When running a single test locally,
  11. # this probably isn't necessary. It's a good idea to do in a continuous integration
  12. # system, or in some way before deploying your code.
  13. 1 config.eager_load = ENV["CI"].present?
  14. # Configure public file server for tests with Cache-Control for performance.
  15. 1 config.public_file_server.enabled = true
  16. 1 config.public_file_server.headers = {
  17. "Cache-Control" => "public, max-age=#{1.hour.to_i}"
  18. }
  19. # Show full error reports and disable caching.
  20. 1 config.consider_all_requests_local = true
  21. 1 config.action_controller.perform_caching = false
  22. 1 config.cache_store = :null_store
  23. # Raise exceptions instead of rendering exception templates.
  24. 1 config.action_dispatch.show_exceptions = :none
  25. # Disable request forgery protection in test environment.
  26. 1 config.action_controller.allow_forgery_protection = false
  27. # Store uploaded files on the local file system in a temporary directory.
  28. 1 config.active_storage.service = :test
  29. # Print deprecation notices to the stderr.
  30. 1 config.active_support.deprecation = :stderr
  31. # Raise exceptions for disallowed deprecations.
  32. 1 config.active_support.disallowed_deprecation = :raise
  33. # Tell Active Support which deprecation messages to disallow.
  34. 1 config.active_support.disallowed_deprecation_warnings = []
  35. # Raises error for missing translations.
  36. # config.i18n.raise_on_missing_translations = true
  37. # Annotate rendered view with file names.
  38. # config.action_view.annotate_rendered_view_with_filenames = true
  39. # Load test helpers
  40. 1 config.autoload_paths += %w[ test/test_helpers ]
  41. end

config/initializers/active_storage.rb

66.67% lines covered

3 relevant lines. 2 lines covered and 1 lines missed.
    
  1. 1 ActiveSupport.on_load(:active_storage_blob) do
  2. 1 ActiveStorage::DiskController.after_action only: :show do
  3. response.set_header("Cache-Control", "max-age=3600, public")
  4. end
  5. end

config/initializers/assets.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. # Be sure to restart your server when you modify this file.
  2. # Version of your assets, change this if you want to expire all your assets.
  3. 1 Rails.application.config.assets.version = "1.0"

config/initializers/content_security_policy.rb

100.0% lines covered

0 relevant lines. 0 lines covered and 0 lines missed.
    
  1. # Be sure to restart your server when you modify this file.
  2. # Define an application-wide content security policy.
  3. # See the Securing Rails Applications Guide for more information:
  4. # https://guides.rubyonrails.org/security.html#content-security-policy-header
  5. # Rails.application.configure do
  6. # config.content_security_policy do |policy|
  7. # policy.default_src :self, :https
  8. # policy.font_src :self, :https, :data
  9. # policy.img_src :self, :https, :data
  10. # policy.object_src :none
  11. # policy.script_src :self, :https
  12. # policy.style_src :self, :https
  13. # # Specify URI for violation reports
  14. # # policy.report_uri "/csp-violation-report-endpoint"
  15. # end
  16. #
  17. # # Generate session nonces for permitted importmap and inline scripts
  18. # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
  19. # config.content_security_policy_nonce_directives = %w(script-src)
  20. #
  21. # # Report violations without enforcing the policy.
  22. # # config.content_security_policy_report_only = true
  23. # end

config/initializers/extensions.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 %w[ rails_ext ].each do |extensions_dir|
  2. 6 Dir["#{Rails.root}/lib/#{extensions_dir}/*"].each { |path| require "#{extensions_dir}/#{File.basename(path)}" }
  3. end

config/initializers/filter_parameter_logging.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. # Be sure to restart your server when you modify this file.
  2. # Configure parameters to be filtered from the log file. Use this to limit dissemination of
  3. # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
  4. # notations and behaviors.
  5. 1 Rails.application.config.filter_parameters += [
  6. :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :endpoint, "message.body"
  7. ]

config/initializers/inflections.rb

100.0% lines covered

0 relevant lines. 0 lines covered and 0 lines missed.
    
  1. # Be sure to restart your server when you modify this file.
  2. # Add new inflection rules using the following format. Inflections
  3. # are locale specific, and you may define rules for as many different
  4. # locales as you wish. All of these examples are active by default:
  5. # ActiveSupport::Inflector.inflections(:en) do |inflect|
  6. # inflect.plural /^(ox)$/i, "\\1en"
  7. # inflect.singular /^(ox)en/i, "\\1"
  8. # inflect.irregular "person", "people"
  9. # inflect.uncountable %w( fish sheep )
  10. # end
  11. # These inflection rules are supported but not enabled by default:
  12. # ActiveSupport::Inflector.inflections(:en) do |inflect|
  13. # inflect.acronym "RESTful"
  14. # end

config/initializers/permissions_policy.rb

100.0% lines covered

0 relevant lines. 0 lines covered and 0 lines missed.
    
  1. # Define an application-wide HTTP permissions policy. For further
  2. # information see https://developers.google.com/web/updates/2018/06/feature-policy
  3. #
  4. # Rails.application.config.permissions_policy do |f|
  5. # f.camera :none
  6. # f.gyroscope :none
  7. # f.microphone :none
  8. # f.usb :none
  9. # f.fullscreen :self
  10. # f.payment :self, "https://secure.example.com"
  11. # end

config/initializers/sentry.rb

16.67% lines covered

6 relevant lines. 1 lines covered and 5 lines missed.
    
  1. 1 if Rails.env.production? && ENV["SKIP_TELEMETRY"].blank?
  2. Sentry.init do |config|
  3. config.dsn = "https://975a8bf631edee43b6a8cf4823998d92@o33603.ingest.sentry.io/4506587182530560"
  4. config.breadcrumbs_logger = [ :active_support_logger, :http_logger ]
  5. config.send_default_pii = false
  6. config.release = ENV["GIT_REVISION"]
  7. end
  8. end

config/initializers/session_store.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. 1 Rails.application.config.session_store :cookie_store,
  2. key: "_campfire_session",
  3. # Persist session cookie as permament so re-opened browser windows maintain a CSRF token
  4. expire_after: 20.years

config/initializers/sqlite3.rb

52.17% lines covered

23 relevant lines. 12 lines covered and 11 lines missed.
    
  1. 1 module SQLite3Configuration
  2. 1 private
  3. 1 def configure_connection
  4. 2 super
  5. 2 if @config[:retries]
  6. 2 retries = self.class.type_cast_config_to_integer(@config[:retries])
  7. 2 raw_connection.busy_handler do |count|
  8. (count <= retries).tap { |result| sleep count * 0.001 if result }
  9. end
  10. end
  11. end
  12. end
  13. 1 module SQLite3DumpConfiguration
  14. 1 def structure_dump(filename, extra_flags)
  15. args = []
  16. args.concat(Array(extra_flags)) if extra_flags
  17. args << db_config.database
  18. ignore_tables = ActiveRecord::SchemaDumper.ignore_tables
  19. if ignore_tables.any?
  20. ignore_tables = connection.data_sources.select { |table| ignore_tables.any? { |pattern| pattern === table } }
  21. condition = ignore_tables.map { |table| connection.quote(table) }.join(", ")
  22. args << "SELECT sql || ';' FROM sqlite_master WHERE tbl_name NOT IN (#{condition}) ORDER BY tbl_name, type DESC, name"
  23. else
  24. args << ".schema --nosys"
  25. end
  26. run_cmd("sqlite3", args, filename)
  27. end
  28. end
  29. 1 ActiveSupport.on_load :active_record do
  30. 1 ActiveRecord::ConnectionAdapters::SQLite3Adapter.prepend SQLite3Configuration
  31. 1 ActiveRecord::Tasks::SQLiteDatabaseTasks.prepend SQLite3DumpConfiguration
  32. end

config/initializers/storage_paths.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 Rails.application.config.after_initialize do
  2. 1 %w[ db files ].each do |dir|
  3. 2 Rails.root.join("storage", dir).mkpath
  4. end
  5. end

config/initializers/time_formats.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. # Used to match JavaScripts (new Date).getTime() for sorting
  2. 292 Time::DATE_FORMATS[:epoch] = ->(time) { (time.to_f * 1000).to_i }

config/initializers/vapid.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 Rails.application.configure do
  2. 1 config.x.vapid.private_key = ENV.fetch("VAPID_PRIVATE_KEY", Rails.application.credentials.dig(:vapid, :private_key))
  3. 1 config.x.vapid.public_key = ENV.fetch("VAPID_PUBLIC_KEY", Rails.application.credentials.dig(:vapid, :public_key))
  4. end

config/initializers/version.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 Rails.application.config.app_version = ENV.fetch("APP_VERSION", "0")
  2. 1 Rails.application.config.git_revision = ENV["GIT_REVISION"]

config/initializers/web_push.rb

34.62% lines covered

26 relevant lines. 9 lines covered and 17 lines missed.
    
  1. 1 require "web-push"
  2. 1 require "web_push/pool"
  3. 1 require "web_push/notification"
  4. 1 Rails.application.configure do
  5. 1 config.x.web_push_pool = WebPush::Pool.new(
  6. invalid_subscription_handler: ->(subscription_id) do
  7. Rails.application.executor.wrap do
  8. Rails.logger.info "Destroying push subscription: #{subscription_id}"
  9. Push::Subscription.find_by(id: subscription_id)&.destroy
  10. end
  11. end
  12. )
  13. 1 at_exit { config.x.web_push_pool.shutdown }
  14. end
  15. 1 module WebPush::PersistentRequest
  16. 1 def perform
  17. if @options[:connection]
  18. http = @options[:connection]
  19. else
  20. http = Net::HTTP.new(uri.host, uri.port, *proxy_options)
  21. http.use_ssl = true
  22. http.ssl_timeout = @options[:ssl_timeout] unless @options[:ssl_timeout].nil?
  23. http.open_timeout = @options[:open_timeout] unless @options[:open_timeout].nil?
  24. http.read_timeout = @options[:read_timeout] unless @options[:read_timeout].nil?
  25. end
  26. req = Net::HTTP::Post.new(uri.request_uri, headers)
  27. req.body = body
  28. if http.is_a?(Net::HTTP::Persistent)
  29. response = http.request uri, req
  30. else
  31. resp = http.request(req)
  32. verify_response(resp)
  33. end
  34. resp
  35. end
  36. end
  37. 1 WebPush::Request.prepend WebPush::PersistentRequest

config/routes.rb

100.0% lines covered

54 relevant lines. 54 lines covered and 0 lines missed.
    
  1. 1 Rails.application.routes.draw do
  2. 1 root "welcome#show"
  3. 1 resource :first_run
  4. 1 resource :session do
  5. 1 scope module: "sessions" do
  6. 1 resources :transfers, only: %i[ show update ]
  7. end
  8. end
  9. 1 resource :account do
  10. 1 scope module: "accounts" do
  11. 1 resources :users
  12. 1 resources :bots do
  13. 1 scope module: "bots" do
  14. 1 resource :key, only: :update
  15. end
  16. end
  17. 1 resource :join_code, only: :create
  18. 1 resource :logo, only: %i[ show destroy ]
  19. 1 resource :custom_styles, only: %i[ edit update ]
  20. end
  21. end
  22. 1 direct :fresh_account_logo do |options|
  23. 125 route_for :account_logo, v: Current.account&.updated_at&.to_fs(:number), size: options[:size]
  24. end
  25. 1 get "join/:join_code", to: "users#new", as: :join
  26. 1 post "join/:join_code", to: "users#create"
  27. 1 resources :qr_code, only: :show
  28. 1 resources :users, only: :show do
  29. 1 scope module: "users" do
  30. 1 resource :avatar, only: %i[ show destroy ]
  31. 1 scope defaults: { user_id: "me" } do
  32. 1 resource :sidebar, only: :show
  33. 1 resource :profile
  34. 1 resources :push_subscriptions do
  35. 1 scope module: "push_subscriptions" do
  36. 1 resources :test_notifications, only: :create
  37. end
  38. end
  39. end
  40. end
  41. end
  42. 1 namespace :autocompletable do
  43. 1 resources :users, only: :index
  44. end
  45. 1 direct :fresh_user_avatar do |user, options|
  46. 457 route_for :user_avatar, user.avatar_token, v: user.updated_at.to_fs(:number)
  47. end
  48. 1 resources :rooms do
  49. 1 resources :messages
  50. 1 post ":bot_key/messages", to: "messages/by_bots#create", as: :bot_messages
  51. 1 scope module: "rooms" do
  52. 1 resource :refresh, only: :show
  53. 1 resource :settings, only: :show
  54. 1 resource :involvement, only: %i[ show update ]
  55. end
  56. 1 get "@:message_id", to: "rooms#show", as: :at_message
  57. end
  58. 1 namespace :rooms do
  59. 1 resources :opens
  60. 1 resources :closeds
  61. 1 resources :directs
  62. end
  63. 1 resources :messages do
  64. 1 scope module: "messages" do
  65. 1 resources :boosts
  66. end
  67. end
  68. 1 resources :searches, only: %i[ index create ] do
  69. 1 delete :clear, on: :collection
  70. end
  71. 1 resource :unfurl_link, only: :create
  72. 1 get "webmanifest" => "pwa#manifest"
  73. 1 get "service-worker" => "pwa#service_worker"
  74. 1 get "up" => "rails/health#show", as: :rails_health_check
  75. end

lib/rails_ext/action_text_attachables.rb

50.0% lines covered

14 relevant lines. 7 lines covered and 7 lines missed.
    
  1. 1 ActiveSupport.on_load(:action_text_content) do
  2. 1 class ActionText::Attachment
  3. 1 class << self
  4. 1 def from_node(node, attachable = nil)
  5. new(node, attachable || ActionText::Attachment::OpengraphEmbed.from_node(node) || attachable_from_possibly_expired_sgid(node["sgid"]) || ActionText::Attachable.from_node(node))
  6. end
  7. 1 private
  8. # Our @mentions use ActionText attachments, which are signed. If someone rotates SECRET_KEY_BASE, the existing attachments become invalid.
  9. # This allows ignoring invalid signatures for User attachments in ActionText.
  10. 1 ATTACHABLES_PERMITTED_WITH_INVALID_SIGNATURES = %w[ User ]
  11. 1 def attachable_from_possibly_expired_sgid(sgid)
  12. if message = sgid&.split("--")&.first
  13. encoded_message = JSON.parse Base64.strict_decode64(message)
  14. decoded_gid = Marshal.load Base64.urlsafe_decode64(encoded_message.dig("_rails", "message"))
  15. model = GlobalID.find(decoded_gid)
  16. model.model_name.to_s.in?(ATTACHABLES_PERMITTED_WITH_INVALID_SIGNATURES) ? model : nil
  17. end
  18. rescue ActiveRecord::RecordNotFound
  19. nil
  20. end
  21. end
  22. end
  23. end

lib/rails_ext/actiontext_opengraph_embeds.rb

57.14% lines covered

21 relevant lines. 12 lines covered and 9 lines missed.
    
  1. 1 class ActionText::Attachment::OpengraphEmbed
  2. 1 include ActiveModel::Model
  3. 1 OPENGRAPH_EMBED_CONTENT_TYPE = "application/vnd.actiontext.opengraph-embed"
  4. 1 class << self
  5. 1 def from_node(node)
  6. if node["content-type"]
  7. if matches = node["content-type"].match(OPENGRAPH_EMBED_CONTENT_TYPE)
  8. attachment = new(attributes_from_node(node))
  9. attachment if attachment.valid?
  10. end
  11. end
  12. end
  13. 1 private
  14. 1 def attributes_from_node(node)
  15. {
  16. href: node["href"],
  17. url: node["url"],
  18. filename: node["filename"],
  19. description: node["caption"]
  20. }
  21. end
  22. end
  23. 1 attr_accessor :href, :url, :filename, :description
  24. 1 def attachable_content_type
  25. OPENGRAPH_EMBED_CONTENT_TYPE
  26. end
  27. 1 def attachable_plain_text_representation(caption)
  28. ""
  29. end
  30. 1 def to_partial_path
  31. "action_text/attachables/opengraph_embed"
  32. end
  33. 1 def to_trix_content_attachment_partial_path
  34. "action_text/attachables/opengraph_embed"
  35. end
  36. end

lib/rails_ext/filter.rb

85.71% lines covered

14 relevant lines. 12 lines covered and 2 lines missed.
    
  1. 1 class ActionText::Content::Filter
  2. 1 class << self
  3. 1 def apply(content)
  4. 285 filter = new(content)
  5. 285 filter.applicable? ? ActionText::Content.new(filter.apply, canonicalize: false) : content
  6. end
  7. end
  8. 1 def initialize(content)
  9. 285 @content = content
  10. end
  11. 1 def applicable?
  12. raise NotImplementedError
  13. end
  14. 1 def apply
  15. raise NotImplementedError
  16. end
  17. 1 private
  18. 1 attr_reader :content
  19. 1 delegate :fragment, to: :content
  20. end

lib/rails_ext/filters.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 class ActionText::Content::Filters
  2. 1 def initialize(*filters)
  3. 1 @filters = filters
  4. end
  5. 1 def apply(content)
  6. 380 filters.reduce(content) { |content, filter| filter.apply(content) }
  7. end
  8. 1 private
  9. 1 attr_reader :filters
  10. end

lib/rails_ext/string.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 class String
  2. 1 def all_emoji?
  3. 123 self.match? /\A(\p{Emoji_Presentation}|\p{Extended_Pictographic}|\uFE0F)+\z/u
  4. end
  5. end

lib/web_push/notification.rb

53.85% lines covered

13 relevant lines. 7 lines covered and 6 lines missed.
    
  1. 1 class WebPush::Notification
  2. 1 def initialize(title:, body:, path:, badge:, endpoint:, p256dh_key:, auth_key:)
  3. @title, @body, @path, @badge = title, body, path, badge
  4. @endpoint, @p256dh_key, @auth_key = endpoint, p256dh_key, auth_key
  5. end
  6. 1 def deliver(connection: nil)
  7. WebPush.payload_send \
  8. message: encoded_message,
  9. endpoint: @endpoint, p256dh: @p256dh_key, auth: @auth_key,
  10. vapid: vapid_identification,
  11. connection: connection,
  12. urgency: "high"
  13. end
  14. 1 private
  15. 1 def vapid_identification
  16. { subject: "mailto:support@37signals.com" }.merge \
  17. Rails.configuration.x.vapid.symbolize_keys
  18. end
  19. 1 def encoded_message
  20. JSON.generate title: @title, options: { body: @body, icon: icon_path, data: { path: @path, badge: @badge } }
  21. end
  22. 1 def icon_path
  23. Rails.application.routes.url_helpers.account_logo_path
  24. end
  25. end

lib/web_push/pool.rb

61.29% lines covered

31 relevant lines. 19 lines covered and 12 lines missed.
    
  1. # This is in lib so we can use it in a thread pool without the Rails executor
  2. 1 class WebPush::Pool
  3. 1 attr_reader :delivery_pool, :invalidation_pool, :connection, :invalid_subscription_handler
  4. 1 def initialize(invalid_subscription_handler:)
  5. 9 @delivery_pool = Concurrent::ThreadPoolExecutor.new(max_threads: 50, queue_size: 10000)
  6. 9 @invalidation_pool = Concurrent::FixedThreadPool.new(1)
  7. 9 @connection = Net::HTTP::Persistent.new(name: "web_push", pool_size: 150)
  8. 9 @invalid_subscription_handler = invalid_subscription_handler
  9. end
  10. 1 def queue(payload, subscriptions)
  11. subscriptions.find_each do |subscription|
  12. deliver_later(payload, subscription)
  13. end
  14. end
  15. 1 def shutdown
  16. 8 connection.shutdown
  17. 8 shutdown_pool(delivery_pool)
  18. 8 shutdown_pool(invalidation_pool)
  19. end
  20. 1 private
  21. 1 def deliver_later(payload, subscription)
  22. # Ensure any AR operations happen before we post to the thread pool
  23. notification = subscription.notification(**payload)
  24. subscription_id = subscription.id
  25. delivery_pool.post do
  26. deliver(notification, subscription_id)
  27. rescue Exception => e
  28. Rails.logger.error "Error in WebPush::Pool.deliver: #{e.class} #{e.message}"
  29. end
  30. rescue Concurrent::RejectedExecutionError
  31. end
  32. 1 def deliver(notification, id)
  33. notification.deliver(connection: connection)
  34. rescue WebPush::ExpiredSubscription, OpenSSL::OpenSSLError => ex
  35. invalidate_subscription_later(id) if invalid_subscription_handler
  36. end
  37. 1 def invalidate_subscription_later(id)
  38. invalidation_pool.post do
  39. invalid_subscription_handler.call(id)
  40. rescue Exception => e
  41. Rails.logger.error "Error in WebPush::Pool.invalid_subscription_handler: #{e.class} #{e.message}"
  42. end
  43. end
  44. 1 def shutdown_pool(pool)
  45. 16 pool.shutdown
  46. 16 pool.kill unless pool.wait_for_termination(1)
  47. end
  48. end

test/system/sending_messages_test.rb

100.0% lines covered

41 relevant lines. 41 lines covered and 0 lines missed.
    
  1. 1 require "application_system_test_case"
  2. 1 class SendingMessagesTest < ApplicationSystemTestCase
  3. 1 setup do
  4. 3 sign_in "jz@37signals.com"
  5. 3 join_room rooms(:designers)
  6. end
  7. 1 test "sending messages between two users" do
  8. 1 using_session("Kevin") do
  9. 1 sign_in "kevin@37signals.com"
  10. 1 join_room rooms(:designers)
  11. end
  12. 1 join_room rooms(:designers)
  13. 1 send_message "Is this thing on?"
  14. 1 using_session("Kevin") do
  15. 1 join_room rooms(:designers)
  16. 1 assert_message_text "Is this thing on?"
  17. 1 send_message "👍👍"
  18. end
  19. 1 join_room rooms(:designers)
  20. 1 assert_message_text "👍👍"
  21. end
  22. 1 test "editing messages" do
  23. 1 using_session("Kevin") do
  24. 1 sign_in "kevin@37signals.com"
  25. 1 join_room rooms(:designers)
  26. end
  27. 1 within_message messages(:third) do
  28. 1 reveal_message_actions
  29. 1 find(".message__edit-btn").click
  30. 1 fill_in_rich_text_area "message_body", with: "Redacted!"
  31. 1 click_on "Save changes"
  32. end
  33. 1 using_session("Kevin") do
  34. 1 join_room rooms(:designers)
  35. 1 assert_message_text "Redacted!"
  36. end
  37. end
  38. 1 test "deleting messages" do
  39. 1 using_session("Kevin") do
  40. 1 sign_in "kevin@37signals.com"
  41. 1 join_room rooms(:designers)
  42. 1 assert_message_text "Third time's a charm."
  43. end
  44. 1 within_message messages(:third) do
  45. 1 reveal_message_actions
  46. 1 find(".message__edit-btn").click
  47. 1 accept_confirm do
  48. 1 click_on "Delete message"
  49. end
  50. end
  51. 1 using_session("Kevin") do
  52. 1 assert_message_text "Third time's a charm.", count: 0
  53. end
  54. end
  55. end

test/system/unread_rooms_test.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. 1 require "application_system_test_case"
  2. 1 class UnreadRoomsTest < ApplicationSystemTestCase
  3. 1 setup do
  4. 1 sign_in "jz@37signals.com"
  5. end
  6. 1 test "sending messages between two users" do
  7. 1 designers_room = rooms(:designers)
  8. 1 hq_room = rooms(:hq)
  9. 1 join_room hq_room
  10. 1 assert_room_read hq_room
  11. 1 using_session("Kevin") do
  12. 1 sign_in "kevin@37signals.com"
  13. 1 join_room designers_room
  14. 1 send_message("Hello!!")
  15. 1 send_message("Talking to myself?")
  16. end
  17. 1 assert_room_unread designers_room
  18. 1 join_room designers_room
  19. 1 assert_room_read designers_room
  20. end
  21. end

test/test_helpers/mention_test_helper.rb

40.0% lines covered

5 relevant lines. 2 lines covered and 3 lines missed.
    
  1. 1 module MentionTestHelper
  2. 1 def mention_attachment_for(name)
  3. user = users(name)
  4. attachment_body = ApplicationController.render partial: "users/mention", locals: { user: user }
  5. "<action-text-attachment sgid=\"#{user.attachable_sgid}\" content-type=\"application/vnd.campfire.mention\" content=\"#{attachment_body.gsub('"', '&quot;')}\"></action-text-attachment>"
  6. end
  7. end

test/test_helpers/session_test_helper.rb

42.86% lines covered

7 relevant lines. 3 lines covered and 4 lines missed.
    
  1. 1 module SessionTestHelper
  2. 1 def parsed_cookies
  3. ActionDispatch::Cookies::CookieJar.build(request, cookies.to_hash)
  4. end
  5. 1 def sign_in(user)
  6. user = users(user) unless user.is_a? User
  7. post session_url, params: { email_address: user.email_address, password: "secret123456" }
  8. assert cookies[:session_token].present?
  9. end
  10. end

test/test_helpers/system_test_helper.rb

96.77% lines covered

31 relevant lines. 30 lines covered and 1 lines missed.
    
  1. 1 module SystemTestHelper
  2. 1 def sign_in(email_address, password = "secret123456")
  3. 15 visit root_url
  4. 15 fill_in "email_address", with: email_address
  5. 15 fill_in "password", with: password
  6. 15 click_on "log_in"
  7. 15 assert_selector "a.btn", text: "Designers"
  8. end
  9. 1 def wait_for_cable_connection
  10. 20 assert_selector "turbo-cable-stream-source[connected]", count: 3, visible: false
  11. end
  12. 1 def join_room(room)
  13. 20 visit room_url(room)
  14. 20 wait_for_cable_connection
  15. 20 dismiss_pwa_install_prompt
  16. end
  17. 1 def send_message(message)
  18. 4 fill_in_rich_text_area "message_body", with: message
  19. 4 click_on "send"
  20. end
  21. 1 def within_message(message, &block)
  22. 9 within "#" + dom_id(message), &block
  23. end
  24. 1 def assert_message_text(text, **options)
  25. 8 assert_selector ".message__body", text: text, **options
  26. end
  27. 1 def assert_room_read(room)
  28. 2 assert_selector ".rooms a", class: "!unread", text: "#{room.name}", wait: 5
  29. end
  30. 1 def assert_room_unread(room)
  31. 1 assert_selector ".rooms a", class: "unread", text: "#{room.name}", wait: 5
  32. end
  33. 1 def reveal_message_actions
  34. 7 find(".message__options-btn").click
  35. rescue Capybara::ElementNotFound
  36. 7 find(".message__options-btn", visible: false).hover.click
  37. ensure
  38. 7 assert_selector ".message__boost-btn", visible: true
  39. end
  40. 1 def dismiss_pwa_install_prompt
  41. 20 if page.has_css?("[data-pwa-install-target~='dialog']", visible: :visible, wait: 5)
  42. click_on("Close")
  43. end
  44. end
  45. end

test/test_helpers/turbo_test_helper.rb

36.36% lines covered

11 relevant lines. 4 lines covered and 7 lines missed.
    
  1. 1 module TurboTestHelper
  2. 1 def assert_rendered_turbo_stream_broadcast(*streambles, action:, target:, &block)
  3. streams = find_broadcasts_for(*streambles)
  4. target = ActionView::RecordIdentifier.dom_id(*target)
  5. assert_select Nokogiri::HTML.fragment(streams), %(turbo-stream[action="#{action}"][target="#{target}"]), &block
  6. end
  7. 1 private
  8. 1 def find_broadcasts_for(*streambles)
  9. broadcasting = streambles.collect do |streamble|
  10. streamble.try(:to_gid_param) || streamble
  11. end.join(":")
  12. broadcasts = ActionCable.server.pubsub.broadcasts(broadcasting)
  13. broadcasts.collect { |b| JSON.parse(b) }.join("\n\n")
  14. end
  15. end