All Files ( 95.94% covered at 7.29 hits/line )
188 files in total.
3101 relevant lines,
2975 lines covered and
126 lines missed.
(
95.94%
)
- 1
module ApplicationCable
- 1
class Channel < ActionCable::Channel::Base
end
end
- 1
module ApplicationCable
- 1
class Connection < ActionCable::Connection::Base
- 1
include Authentication::SessionLookup
- 1
identified_by :current_user
- 1
def connect
- 3
self.current_user = find_verified_user
end
- 1
private
- 1
def find_verified_user
- 3
if verified_session = find_session_by_cookie
- 1
verified_session.user
else
- 2
reject_unauthorized_connection
end
end
end
end
- 1
class PresenceChannel < RoomChannel
- 1
on_subscribe :present, unless: :subscription_rejected?
- 1
on_unsubscribe :absent, unless: :subscription_rejected?
- 1
def present
- 3
membership.present
- 3
broadcast_read_room
end
- 1
def absent
- 1
membership.disconnected
end
- 1
def refresh
membership.refresh_connection
end
- 1
private
- 1
def membership
- 7
@room.memberships.find_by(user: current_user)
end
- 1
def broadcast_read_room
- 3
ActionCable.server.broadcast "user_#{current_user.id}_reads", { room_id: membership.room_id }
end
end
- 1
class RoomChannel < ApplicationCable::Channel
- 1
def subscribed
- 6
if @room = find_room
- 3
stream_for @room
else
- 3
reject
end
end
- 1
private
- 1
def find_room
- 6
current_user.rooms.find_by(id: params[:room_id])
end
end
- 1
class Accounts::Bots::KeysController < ApplicationController
- 1
before_action :ensure_can_administer
- 1
def update
- 1
User.active_bots.find(params[:bot_id]).reset_bot_key
- 1
redirect_to account_bots_url
end
end
- 1
class Accounts::BotsController < ApplicationController
- 1
before_action :ensure_can_administer
- 1
before_action :set_bot, only: %i[ edit update destroy ]
- 1
def index
- 1
@bots = User.active_bots.ordered
end
- 1
def new
- 1
@bot = User.active_bots.new
end
- 1
def create
- 1
User.create_bot! bot_params
- 1
redirect_to account_bots_url
end
- 1
def edit
end
- 1
def update
- 2
@bot.update_bot! bot_params
- 2
redirect_to account_bots_url
end
- 1
def destroy
- 1
@bot.deactivate
- 1
redirect_to account_bots_url
end
- 1
private
- 1
def set_bot
- 4
@bot = User.active_bots.find(params[:id])
end
- 1
def bot_params
- 3
params.require(:user).permit(:name, :avatar, :webhook_url)
end
end
- 1
class Accounts::CustomStylesController < ApplicationController
- 1
before_action :ensure_can_administer, :set_account
- 1
def edit
end
- 1
def update
- 1
@account.update!(account_params)
- 1
redirect_to edit_account_custom_styles_url, notice: "✓"
end
- 1
private
- 1
def set_account
- 2
@account = Current.account
end
- 1
def account_params
- 1
params.require(:account).permit(:custom_styles)
end
end
- 1
class Accounts::JoinCodesController < ApplicationController
- 1
before_action :ensure_can_administer
- 1
def create
- 1
Current.account.reset_join_code
- 1
redirect_to edit_account_url
end
end
- 1
class Accounts::LogosController < ApplicationController
- 1
include ActiveStorage::Streaming, ActionView::Helpers::AssetUrlHelper
- 1
allow_unauthenticated_access only: :show
- 1
before_action :ensure_can_administer, only: :destroy
- 1
def show
- 4
if stale?(etag: Current.account)
- 4
expires_in 5.minutes, public: true, stale_while_revalidate: 1.week
- 4
if Current.account&.logo&.attached?
- 2
logo = Current.account.logo.variant(logo_variant).processed
- 2
send_png_file ActiveStorage::Blob.service.path_for(logo.key)
else
- 2
send_stock_icon
end
end
end
- 1
def destroy
- 1
Current.account.logo.destroy
- 1
redirect_to edit_account_url
end
- 1
private
- 1
LARGE_SQUARE_PNG_VARIANT = { resize_to_limit: [ 512, 512 ], format: :png }
- 1
SMALL_SQUARE_PNG_VARIANT = { resize_to_limit: [ 192, 192 ], format: :png }
- 1
def send_png_file(path)
- 4
send_file path, content_type: "image/png", disposition: :inline
end
- 1
def send_stock_icon
- 2
if small_logo?
- 1
send_png_file logo_path("app-icon-192.png")
else
- 1
send_png_file logo_path("app-icon.png")
end
end
- 1
def logo_variant
- 2
small_logo? ? SMALL_SQUARE_PNG_VARIANT : LARGE_SQUARE_PNG_VARIANT
end
- 1
def small_logo?
- 4
params[:size] == "small"
end
- 1
def logo_path(filename)
- 2
Rails.root.join("app/assets/images/logos/#{filename}")
end
end
- 1
class Accounts::UsersController < ApplicationController
- 1
before_action :ensure_can_administer, :set_user, only: %i[ update destroy ]
- 1
def index
set_page_and_extract_portion_from User.active.ordered.without_bots, per_page: 500
end
- 1
def update
- 1
@user.update(role_params)
- 1
redirect_to edit_account_url
end
- 1
def destroy
- 1
@user.deactivate
- 1
redirect_to edit_account_url
end
- 1
private
- 1
def set_user
- 2
@user = User.active.find(params[:user_id] || params[:id])
end
- 1
def role_params
- 1
{ role: params.require(:user)[:role].presence_in(%w[ member administrator ]) || "member" }
end
end
- 1
class AccountsController < ApplicationController
- 1
before_action :ensure_can_administer, only: :update
- 1
before_action :set_account
- 1
def edit
- 1
set_page_and_extract_portion_from User.active.ordered, per_page: 500
end
- 1
def update
- 1
@account.update!(account_params)
- 1
redirect_to edit_account_url, notice: "✓"
end
- 1
private
- 1
def set_account
- 2
@account = Current.account
end
- 1
def account_params
- 1
params.require(:account).permit(:name, :logo)
end
end
- 1
class ApplicationController < ActionController::Base
- 1
include AllowBrowser, Authentication, Authorization, SetCurrentRequest, SetPlatform, TrackedRoomVisit, VersionHeaders
- 1
include Turbo::Streams::Broadcasts, Turbo::Streams::StreamName
end
- 1
class Autocompletable::UsersController < ApplicationController
- 1
def index
- 4
set_page_and_extract_portion_from find_autocompletable_users.with_attached_avatar.ordered, per_page: 20
end
- 1
private
- 1
def find_autocompletable_users
- 4
params[:query].present? ? users_scope.active.filtered_by(params[:query]) : users_scope.active
end
- 1
def users_scope
- 4
params[:room_id].present? ? Current.user.rooms.find(params[:room_id]).users : User.all
end
end
- 1
module AllowBrowser
- 1
extend ActiveSupport::Concern
- 1
VERSIONS = { safari: 17.2, chrome: 120, firefox: 121, opera: 104, ie: false }
- 1
included do
- 2
allow_browser versions: VERSIONS, block: -> { render template: "sessions/incompatible_browser" }
end
end
- 1
module Authentication
- 1
extend ActiveSupport::Concern
- 1
include SessionLookup
- 1
included do
- 1
before_action :require_authentication
- 1
before_action :deny_bots
- 1
helper_method :signed_in?
- 232
protect_from_forgery with: :exception, unless: -> { authenticated_by.bot_key? }
end
- 1
class_methods do
- 1
def allow_unauthenticated_access(**options)
- 5
skip_before_action :require_authentication, **options
end
- 1
def allow_bot_access(**options)
- 1
skip_before_action :deny_bots, **options
end
- 1
def require_unauthenticated_access(**options)
- 1
skip_before_action :require_authentication, **options
- 1
before_action :restore_authentication, :redirect_signed_in_user_to_root, **options
end
end
- 1
private
- 1
def signed_in?
- 5
Current.user.present?
end
- 1
def require_authentication
- 104
restore_authentication || bot_authentication || request_authentication
end
- 1
def restore_authentication
- 109
if session = find_session_by_cookie
- 99
resume_session session
end
end
- 1
def bot_authentication
- 6
if params[:bot_key].present? && bot = User.authenticate_bot(params[:bot_key].strip)
- 6
Current.user = bot
- 6
set_authenticated_by(:bot_key)
end
end
- 1
def request_authentication
session[:return_to_after_authenticating] = request.url
redirect_to new_session_url
end
- 1
def redirect_signed_in_user_to_root
- 5
redirect_to root_url if signed_in?
end
- 1
def start_new_session_for(user)
- 111
user.sessions.start!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
- 111
authenticated_as session
end
end
- 1
def resume_session(session)
- 99
session.resume user_agent: request.user_agent, ip_address: request.remote_ip
- 99
authenticated_as session
end
- 1
def authenticated_as(session)
- 210
Current.user = session.user
- 210
set_authenticated_by(:session)
- 210
cookies.signed.permanent[:session_token] = { value: session.token, httponly: true, same_site: :lax }
end
- 1
def post_authenticating_url
- 109
session.delete(:return_to_after_authenticating) || root_url
end
- 1
def reset_authentication
- 2
cookies.delete(:session_token)
end
- 1
def deny_bots
- 227
head :forbidden if authenticated_by.bot_key?
end
- 1
def set_authenticated_by(method)
- 216
@authenticated_by = method.to_s.inquiry
end
- 1
def authenticated_by
- 458
@authenticated_by ||= "".inquiry
end
end
- 1
module Authentication::SessionLookup
- 1
def find_session_by_cookie
- 112
if token = cookies.signed[:session_token]
- 101
Session.find_by(token: token)
end
end
end
- 1
module Authorization
- 1
private
- 1
def ensure_can_administer
- 20
head :forbidden unless Current.user.can_administer?
end
end
- 1
module RoomScoped
- 1
extend ActiveSupport::Concern
- 1
included do
- 3
before_action :set_room
end
- 1
private
- 1
def set_room
- 25
@membership = Current.user.memberships.find_by!(room_id: params[:room_id])
- 25
@room = @membership.room
end
end
- 1
module SetCurrentRequest
- 1
extend ActiveSupport::Concern
- 1
included do
- 1
before_action do
- 232
Current.request = request
end
end
- 1
def default_url_options
- 1104
{ host: Current.request_host, protocol: Current.request_protocol }.compact_blank
end
end
- 1
module SetPlatform
- 1
extend ActiveSupport::Concern
- 1
included do
- 1
helper_method :platform
end
- 1
private
- 1
def platform
- 63
@platform ||= ApplicationPlatform.new(request.user_agent)
end
end
- 1
module TrackedRoomVisit
- 1
extend ActiveSupport::Concern
- 1
included do
- 1
helper_method :last_room_visited
end
- 1
def remember_last_room_visited
- 4
cookies.permanent[:last_room] = @room.id
end
- 1
def last_room_visited
- 8
Current.user.rooms.find_by(id: cookies[:last_room]) || default_room
end
- 1
private
- 1
def default_room
- 7
Current.user.rooms.original
end
end
- 1
module VersionHeaders
- 1
extend ActiveSupport::Concern
- 1
included do
- 1
before_action :set_version_headers
end
- 1
private
- 1
def set_version_headers
- 232
response.headers["X-Version"] = Rails.application.config.app_version
- 232
response.headers["X-Rev"] = Rails.application.config.git_revision
end
end
- 1
class FirstRunsController < ApplicationController
- 1
allow_unauthenticated_access
- 1
before_action :prevent_repeats
- 1
def show
- 1
@user = User.new
end
- 1
def create
- 1
user = FirstRun.create!(user_params)
- 1
start_new_session_for user
- 1
redirect_to root_url
end
- 1
private
- 1
def prevent_repeats
- 3
redirect_to root_url if Account.any?
end
- 1
def user_params
- 1
params.require(:user).permit(:name, :avatar, :email_address, :password)
end
end
- 1
class Messages::BoostsController < ApplicationController
- 1
before_action :set_message
- 1
def index
end
- 1
def new
end
- 1
def create
- 1
@boost = @message.boosts.create!(boost_params)
- 1
broadcast_create
- 1
redirect_to message_boosts_url(@message)
end
- 1
def destroy
- 1
@boost = Current.user.boosts.find(params[:id])
- 1
@boost.destroy!
- 1
broadcast_remove
end
- 1
private
- 1
def set_message
- 2
@message = Current.user.reachable_messages.find(params[:message_id])
end
- 1
def boost_params
- 1
params.require(:boost).permit(:content)
end
- 1
def broadcast_create
- 1
@boost.broadcast_append_to @boost.message.room, :messages,
target: "boosts_message_#{@boost.message.client_message_id}", partial: "messages/boosts/boost", attributes: { maintain_scroll: true }
end
- 1
def broadcast_remove
- 1
@boost.broadcast_remove_to @boost.message.room, :messages
end
end
- 1
class Messages::ByBotsController < MessagesController
- 1
allow_bot_access only: :create
- 1
def create
- 5
super
- 5
head :created, location: message_url(@message)
end
- 1
private
- 1
def message_params
- 5
if params[:attachment]
- 1
params.permit(:attachment)
else
- 8
reading(request.body) { |body| { body: body } }
end
end
- 1
def reading(io)
- 4
io.rewind
- 4
yield io.read.force_encoding("UTF-8")
ensure
- 4
io.rewind
end
end
- 1
class MessagesController < ApplicationController
- 1
include ActiveStorage::SetCurrent, RoomScoped
- 1
before_action :set_room, except: :create
- 1
before_action :set_message, only: %i[ show edit update destroy ]
- 1
before_action :ensure_can_administer, only: %i[ edit update destroy ]
- 1
layout false, only: :index
- 1
def index
- 4
@messages = find_paged_messages
- 4
if @messages.any?
- 3
fresh_when @messages
else
- 1
head :no_content
end
end
- 1
def create
- 8
set_room
- 8
@message = @room.messages.create_with_attachment!(message_params)
- 8
@message.broadcast_create
- 8
deliver_webhooks_to_bots
rescue ActiveRecord::RecordNotFound
render action: :room_not_found
end
- 1
def show
end
- 1
def edit
end
- 1
def update
- 2
@message.update!(message_params)
- 2
@message.broadcast_replace_to @room, :messages, target: [ @message, :presentation ], partial: "messages/presentation", attributes: { maintain_scroll: true }
- 2
redirect_to room_message_url(@room, @message)
end
- 1
def destroy
- 2
@message.destroy
- 2
@message.broadcast_remove_to @room, :messages
end
- 1
private
- 1
def set_message
- 7
@message = @room.messages.find(params[:id])
end
- 1
def ensure_can_administer
- 6
head :forbidden unless Current.user.can_administer?(@message)
end
- 1
def find_paged_messages
case
- 4
when params[:before].present?
- 1
@room.messages.with_creator.page_before(@room.messages.find(params[:before]))
when params[:after].present?
- 1
@room.messages.with_creator.page_after(@room.messages.find(params[:after]))
else
- 2
@room.messages.with_creator.last_page
end
end
- 1
def message_params
- 5
params.require(:message).permit(:body, :attachment, :client_message_id)
end
- 1
def deliver_webhooks_to_bots
- 9
bots_eligible_for_webhook.excluding(@message.creator).each { |bot| bot.deliver_webhook_later(@message) }
end
- 1
def bots_eligible_for_webhook
- 8
@room.direct? ? @room.users.active_bots : @message.mentionees.active_bots
end
end
- 1
class QrCodeController < ApplicationController
- 1
allow_unauthenticated_access
- 1
def show
- 1
url = Base64.urlsafe_decode64(params[:id])
- 1
qr_code = RQRCode::QRCode.new(url).as_svg(viewbox: true, fill: :white, color: :black)
- 1
expires_in 1.year, public: true
- 1
render plain: qr_code, content_type: "image/svg+xml"
end
end
- 1
class Rooms::ClosedsController < RoomsController
- 1
before_action :force_room_type, only: %i[ edit update ]
- 1
DEFAULT_ROOM_NAME = "New room"
- 1
def show
redirect_to room_url(@room)
end
- 1
def new
- 1
@room = Rooms::Closed.new(name: DEFAULT_ROOM_NAME)
- 1
@users = User.active.ordered
end
- 1
def create
- 1
room = Rooms::Closed.create_for(room_params, users: grantees)
- 1
broadcast_create_room(room)
- 1
redirect_to room_url(room)
end
- 1
def edit
selected_user_ids = @room.users.pluck(:id)
@selected_users, @unselected_users = User.active.ordered.partition { |user| selected_user_ids.include?(user.id) }
end
- 1
def update
- 3
@room.update! room_params
- 3
@room.memberships.revise(granted: grantees, revoked: revokees)
- 3
broadcast_update_room
- 3
redirect_to room_url(@room)
end
- 1
private
# Allows us to edit an open room and turn it into a closed one on saving.
- 1
def force_room_type
- 3
@room = @room.becomes!(Rooms::Closed)
end
- 1
def grantees
- 4
User.where(id: grantee_ids)
end
- 1
def revokees
- 3
@room.users.where.not(id: grantee_ids)
end
- 1
def grantee_ids
- 7
params.fetch(:user_ids, [])
end
- 1
def broadcast_create_room(room)
- 1
each_user_and_html_for(room) do |user, html|
- 3
broadcast_prepend_to user, :rooms, target: :shared_rooms, html: html
end
end
- 1
def broadcast_update_room
- 3
each_user_and_html_for(@room) do |user, html|
- 7
broadcast_replace_to user, :rooms, target: [ @room, :list ], html: html
end
end
- 1
def each_user_and_html_for(room)
# Optimization to avoid rendering the same partial for every user
- 4
html = render_to_string(partial: "users/sidebars/rooms/shared", locals: { room: room })
- 14
room.users.each { |user| yield user, html }
end
end
- 1
class Rooms::DirectsController < RoomsController
- 1
def new
@room = Rooms::Direct.new
end
- 1
def create
- 3
room = Rooms::Direct.find_or_create_for(selected_users)
- 3
broadcast_create_room(room)
- 3
redirect_to room_url(room)
end
- 1
def edit
end
- 1
private
- 1
def selected_users
- 3
User.where(id: selected_users_ids.including(Current.user.id))
end
- 1
def selected_users_ids
- 3
params.fetch(:user_ids, [])
end
- 1
def broadcast_create_room(room)
- 3
room.memberships.each do |membership|
- 6
membership.broadcast_prepend_to membership.user, :rooms, target: :direct_rooms, partial: "users/sidebars/rooms/direct"
end
end
# All users in a direct room can administer it
- 1
def ensure_can_administer
- 1
true
end
end
- 1
class Rooms::InvolvementsController < ApplicationController
- 1
include RoomScoped
- 1
def show
- 1
@involvement = @membership.involvement
end
- 1
def update
- 4
@membership.update! involvement: params[:involvement]
- 4
broadcast_visibility_changes
- 4
redirect_to room_involvement_url(@room)
end
- 1
private
- 1
def broadcast_visibility_changes
case
- 4
when @room.direct?
# Do nothing
when @membership.involved_in_invisible?
- 1
broadcast_remove_to @membership.user, :rooms, target: [ @room, :list ]
when @membership.involvement_previously_was.inquiry.invisible?
- 1
broadcast_prepend_to @membership.user, :rooms, target: :shared_rooms, partial: "users/sidebars/rooms/shared", locals: { room: @room }
end
end
end
- 1
class Rooms::OpensController < RoomsController
- 1
before_action :force_room_type, only: %i[ edit update ]
- 1
DEFAULT_ROOM_NAME = "New room"
- 1
def show
- 2
redirect_to room_url(@room)
end
- 1
def new
- 1
@room = Rooms::Open.new(name: DEFAULT_ROOM_NAME)
- 1
@users = User.active.ordered
end
- 1
def create
- 1
room = Rooms::Open.create_for(room_params, users: Current.user)
- 1
broadcast_create_room(room)
- 1
redirect_to room_url(room)
end
- 1
def edit
@users = User.active.ordered
end
- 1
def update
- 2
@room.update! room_params
- 2
broadcast_update_room
- 2
redirect_to room_url(@room)
end
- 1
private
# Allows us to edit a closed room and turn it into an open one on saving.
- 1
def force_room_type
- 2
@room = @room.becomes!(Rooms::Open)
end
- 1
def broadcast_create_room(room)
- 1
broadcast_prepend_to :rooms, target: :shared_rooms, partial: "users/sidebars/rooms/shared", locals: { room: room }
end
- 1
def broadcast_update_room
- 2
broadcast_replace_to :rooms, target: [ @room, :list ], partial: "users/sidebars/rooms/shared", locals: { room: @room }
end
end
- 1
class Rooms::RefreshesController < ApplicationController
- 1
include RoomScoped
- 1
before_action :set_last_updated_at
- 1
def show
- 1
@new_messages = @room.messages.with_creator.page_created_since(@last_updated_at)
- 1
@updated_messages = @room.messages.without(@new_messages).with_creator.page_updated_since(@last_updated_at)
end
- 1
private
- 1
def set_last_updated_at
- 1
@last_updated_at = Time.at(0, params[:since].to_i, :millisecond)
end
end
- 1
class RoomsController < ApplicationController
- 1
before_action :set_room, only: %i[ edit update show destroy ]
- 1
before_action :ensure_can_administer, only: %i[ update destroy ]
- 1
before_action :remember_last_room_visited, only: :show
- 1
def index
- 1
redirect_to room_url(Current.user.rooms.last)
end
- 1
def show
- 2
@messages = find_messages
end
- 1
def destroy
- 3
@room.destroy
- 3
broadcast_remove_room
- 3
redirect_to root_url
end
- 1
private
- 1
def set_room
- 16
if room = Current.user.rooms.find_by(id: params[:room_id] || params[:id])
- 15
@room = room
else
- 1
redirect_to root_url, alert: "Room not found or inaccessible"
end
end
- 1
def ensure_can_administer
- 10
head :forbidden unless Current.user.can_administer?(@room)
end
- 1
def find_messages
- 2
messages = @room.messages.with_creator
- 2
if show_first_message = messages.find_by(id: params[:message_id])
@messages = messages.page_around(show_first_message)
else
- 2
@messages = messages.last_page
end
end
- 1
def room_params
- 7
params.require(:room).permit(:name)
end
- 1
def broadcast_remove_room
- 3
broadcast_remove_to :rooms, target: [ @room, :list ]
end
end
- 1
class SearchesController < ApplicationController
- 1
before_action :set_messages
- 1
def index
- 3
@query = query if query.present?
- 3
@recent_searches = Current.user.searches.ordered
- 3
@return_to_room = last_room_visited
end
- 1
def create
- 1
Current.user.searches.record(query)
- 1
redirect_to searches_url(q: query)
end
- 1
def clear
- 1
Current.user.searches.destroy_all
- 1
redirect_to searches_url
end
- 1
private
- 1
def set_messages
- 5
if query.present?
- 3
@messages = Current.user.reachable_messages.search(query).last(100)
else
- 2
@messages = Message.none
end
end
- 1
def query
- 15
params[:q]&.gsub(/[^[:word:]]/, " ")
end
end
- 1
class Sessions::TransfersController < ApplicationController
- 1
allow_unauthenticated_access
- 1
def show
end
- 1
def update
- 1
if user = User.active.find_by_transfer_id(params[:id])
- 1
start_new_session_for user
- 1
redirect_to post_authenticating_url
else
head :bad_request
end
end
end
- 1
class SessionsController < ApplicationController
- 1
allow_unauthenticated_access only: %i[ new create ]
- 1
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { render_rejection :too_many_requests }
- 1
before_action :ensure_user_exists, only: :new
- 1
def new
end
- 1
def create
- 109
if user = User.active.authenticate_by(email_address: params[:email_address], password: params[:password])
- 108
start_new_session_for user
- 108
redirect_to post_authenticating_url
else
- 1
render_rejection :unauthorized
end
end
- 1
def destroy
- 2
remove_push_subscription
- 2
reset_authentication
- 2
redirect_to root_url
end
- 1
private
- 1
def ensure_user_exists
- 3
redirect_to first_run_url if User.none?
end
- 1
def render_rejection(status)
- 1
flash.now[:alert] = "Too many requests or unauthorized."
- 1
render :new, status: status
end
- 1
def remove_push_subscription
- 2
if endpoint = params[:push_subscription_endpoint]
- 1
Push::Subscription.destroy_by(endpoint: endpoint, user_id: Current.user.id)
end
end
end
- 1
class UnfurlLinksController < ApplicationController
- 1
def create
- 5
opengraph = Opengraph::Metadata.from_url(url_param)
- 4
if opengraph.valid?
- 3
render json: opengraph
else
- 1
head :no_content
end
end
- 1
private
- 1
def url_param
- 5
params.require(:url)
end
end
- 1
class Users::AvatarsController < ApplicationController
- 1
include ActiveStorage::Streaming
- 2
rescue_from(ActiveSupport::MessageVerifier::InvalidSignature) { head :not_found }
- 1
def show
- 3
@user = User.from_avatar_token(params[:user_id])
- 2
if stale?(etag: @user)
- 2
expires_in 30.minutes, public: true, stale_while_revalidate: 1.week
- 2
if @user.avatar.attached?
- 1
avatar_variant = @user.avatar.variant(SQUARE_WEBP_VARIANT).processed
- 1
send_webp_blob_file avatar_variant.key
- 1
elsif @user.bot?
render_default_bot
else
- 1
render_initials
end
end
end
- 1
def destroy
Current.user.avatar.destroy
redirect_to user_profile_url
end
- 1
private
- 1
SQUARE_WEBP_VARIANT = { resize_to_limit: [ 512, 512 ], format: :webp }
- 1
def send_webp_blob_file(key)
- 1
send_file ActiveStorage::Blob.service.path_for(key), content_type: "image/webp", disposition: :inline
end
- 1
def render_default_bot
send_file Rails.root.join("app/assets/images/default-bot-avatar.svg"), content_type: "image/svg+xml", disposition: :inline
end
- 1
def render_initials
- 1
render formats: :svg
end
end
- 1
class Users::ProfilesController < ApplicationController
- 1
before_action :set_user
- 1
def show
@direct_memberships, @shared_memberships =
- 7
Current.user.memberships.with_ordered_room.partition { |m| m.room.direct? }
end
- 1
def update
- 2
@user.update user_params
- 2
redirect_to user_profile_url, notice: update_notice
end
- 1
private
- 1
def set_user
- 3
@user = Current.user
end
- 1
def user_params
- 2
params.require(:user).permit(:name, :avatar, :email_address, :password, :bio).compact
end
- 1
def update_notice
- 2
params[:user][:avatar] ? "It may take up to 30 minutes to change everywhere." : "✓"
end
end
- 1
class Users::PushSubscriptionsController < ApplicationController
- 1
before_action :set_push_subscriptions
- 1
def index
end
- 1
def create
- 2
if subscription = @push_subscriptions.find_by(push_subscription_params)
- 1
subscription.touch
else
- 1
@push_subscriptions.create! push_subscription_params.merge(user_agent: request.user_agent)
end
- 2
head :ok
end
- 1
def destroy
- 1
@push_subscriptions.destroy_by(id: params[:id])
- 1
redirect_to user_push_subscriptions_url
end
- 1
private
- 1
def set_push_subscriptions
- 3
@push_subscriptions = Current.user.push_subscriptions
end
- 1
def push_subscription_params
- 3
params.require(:push_subscription).permit(:endpoint, :p256dh_key, :auth_key)
end
end
- 1
class Users::SidebarsController < ApplicationController
- 1
DIRECT_PLACEHOLDERS = 20
- 1
def show
- 3
all_memberships = Current.user.memberships.visible.with_ordered_room
- 3
@direct_memberships = extract_direct_memberships(all_memberships)
- 3
@other_memberships = all_memberships.without(@direct_memberships)
- 3
@direct_placeholder_users = find_direct_placeholder_users
end
- 1
private
- 1
def extract_direct_memberships(all_memberships)
- 27
all_memberships.select { |m| m.room.direct? }.sort_by { |m| m.room.updated_at }.reverse
end
- 1
def find_direct_placeholder_users
- 3
exclude_user_ids = user_ids_already_in_direct_rooms_with_current_user.including(Current.user.id)
- 3
User.active.where.not(id: exclude_user_ids).order(:created_at).limit(DIRECT_PLACEHOLDERS - exclude_user_ids.count)
end
- 1
def user_ids_already_in_direct_rooms_with_current_user
- 3
Membership.where(room_id: Current.user.rooms.directs.pluck(:id)).pluck(:user_id).uniq
end
end
- 1
class UsersController < ApplicationController
- 1
require_unauthenticated_access only: %i[ new create ]
- 1
before_action :set_user, only: :show
- 1
before_action :verify_join_code, only: %i[ new create ]
- 1
def new
- 1
@user = User.new
end
- 1
def create
- 2
@user = User.create!(user_params)
- 1
start_new_session_for @user
- 1
redirect_to root_url
rescue ActiveRecord::RecordNotUnique
- 1
redirect_to new_session_url(email_address: user_params[:email_address])
end
- 1
def show
end
- 1
private
- 1
def set_user
- 1
@user = User.find(params[:id])
end
- 1
def verify_join_code
- 4
head :not_found if Current.account.join_code != params[:join_code]
end
- 1
def user_params
- 3
params.require(:user).permit(:name, :avatar, :email_address, :password)
end
end
- 1
class WelcomeController < ApplicationController
- 1
def show
- 2
if Current.user.rooms.any?
- 2
redirect_to room_url(last_room_visited)
else
render
end
end
end
- 1
module AccountsHelper
- 1
def account_logo_tag(style: nil)
- 4
tag.figure image_tag(fresh_account_logo_path, alt: "Account logo", size: 300), class: "account-logo avatar #{style}"
end
end
- 1
module ApplicationHelper
- 1
def page_title_tag
- 26
tag.title @page_title || "Campfire"
end
- 1
def current_user_meta_tags
- 26
unless Current.user.nil?
- 19
safe_join [
tag(:meta, name: "current-user-id", content: Current.user.id),
tag(:meta, name: "current-user-name", content: Current.user.name)
]
end
end
- 1
def custom_styles_tag
- 26
if custom_styles = Current.account&.custom_styles
tag.style(custom_styles.to_s.html_safe, data: { turbo_track: "reload" })
end
end
- 1
def body_classes
- 26
[ @body_class, admin_body_class, account_logo_body_class ].compact.join(" ")
end
- 1
def link_back
- 2
link_back_to request.referrer || root_path
end
- 1
def link_back_to(destination)
- 9
link_to destination, class: "btn" do
- 9
image_tag("arrow-left.svg", aria: { hidden: "true" }, size: 20) +
tag.span("Go Back", class: "for-screen-reader")
end
end
- 1
private
- 1
def admin_body_class
- 26
"admin" if Current.user&.can_administer?
end
- 1
def account_logo_body_class
- 26
"account-has-logo" if Current.account&.logo&.attached?
end
end
- 1
module BroadcastsHelper
- 1
def broadcast_image_tag(image, options)
- 2
image_tag(broadcast_image_path(image), options)
end
- 1
def broadcast_image_path(image)
- 2
if image.is_a?(Symbol) || image.is_a?(String)
image_path(image)
else
- 2
polymorphic_url(image, only_path: true)
end
end
end
- 1
module ClipboardHelper
- 1
def button_to_copy_to_clipboard(url, &)
- 5
tag.button class: "btn", data: {
controller: "copy-to-clipboard", action: "copy-to-clipboard#copy",
copy_to_clipboard_success_class: "btn--success", copy_to_clipboard_content_value: url
}, &
end
end
- 1
module ContentFilters
- 1
TextMessagePresentationFilters = ActionText::Content::Filters.new(RemoveSoloUnfurledLinkText, StyleUnfurledTwitterAvatars, SanitizeTags)
end
- 1
class ContentFilters::RemoveSoloUnfurledLinkText < ActionText::Content::Filter
- 1
def applicable?
- 41
normalize_tweet_url(solo_unfurled_url) == normalize_tweet_url(content.to_plain_text)
end
- 1
def apply
- 9
fragment.replace("div") { |node| node.tap { |n| n.inner_html = unfurled_links.first.to_s } }
end
- 1
private
- 1
TWITTER_DOMAINS = %w[ x.com twitter.com ]
- 1
TWITTER_DOMAIN_MAPPING = { "x.com" => "twitter.com" }
- 1
def solo_unfurled_url
- 41
unfurled_links.first["href"] if unfurled_links.size == 1
end
- 1
def unfurled_links
- 48
fragment.find_all("action-text-attachment[@content-type='#{ActionText::Attachment::OpengraphEmbed::OPENGRAPH_EMBED_CONTENT_TYPE}']")
end
- 1
def normalize_tweet_url(url)
- 82
return url unless twitter_url?(url)
- 4
uri = URI.parse(url)
- 4
uri.dup.tap do |u|
- 4
u.host = TWITTER_DOMAIN_MAPPING[uri.host&.downcase] || uri.host
- 4
u.query = nil
end.to_s
rescue URI::InvalidURIError
url
end
- 1
def twitter_url?(url)
- 170
url.present? && TWITTER_DOMAINS.any? { |domain| url.strip.include?(domain) }
end
end
- 1
class ContentFilters::SanitizeTags < ActionText::Content::Filter
- 1
def applicable?
- 41
true
end
- 1
def apply
- 42
fragment.replace(not_allowed_tags_css_selector) { nil }
end
- 1
private
- 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
p pre samp small span strong sub sup time tt ul var ] + [ ActionText::Attachment.tag_name, "figure", "figcaption" ]
- 1
def not_allowed_tags_css_selector
- 1845
ALLOWED_TAGS.map { |tag| ":not(#{tag})" }.join("")
end
end
- 1
class ContentFilters::StyleUnfurledTwitterAvatars < ActionText::Content::Filter
- 1
def applicable?
- 43
unfurled_twitter_avatars.present?
end
- 1
def apply
- 1
fragment.update do |source|
- 1
div = source.at_css("div")
- 1
div["class"] = UNFURLED_TWITTER_AVATAR_CSS_CLASS
end
end
- 1
private
- 1
UNFURLED_TWITTER_AVATAR_CSS_CLASS = "cf-twitter-avatar"
- 1
TWITTER_AVATAR_URL_PREFIX = "https://pbs.twimg.com/profile_images"
- 1
def unfurled_twitter_avatars
- 43
fragment.find_all("#{opengraph_css_selector}[url*='#{TWITTER_AVATAR_URL_PREFIX}']")
end
- 1
def opengraph_css_selector
- 43
"action-text-attachment[@content-type='#{ActionText::Attachment::OpengraphEmbed::OPENGRAPH_EMBED_CONTENT_TYPE}']"
end
end
- 1
module DropTargetHelper
- 1
def drop_target_actions
- 4
"dragenter->drop-target#dragenter dragover->drop-target#dragover drop->drop-target#drop"
end
end
- 1
module EmojiHelper
REACTIONS = {
- 1
"👍" => "Thumbs up",
"👏" => "Clapping",
"👋" => "Waving hand",
"💪" => "Muscle",
"❤️" => "Red heart",
"😂" => "Face with tears of joy",
"🎉" => "Party popper",
"🔥" => "Fire"
}
end
- 1
module FormsHelper
- 1
def auto_submit_form_with(**attributes, &)
- 1
data = attributes.delete(:data) || {}
- 1
data[:controller] = "auto-submit #{data[:controller]}".strip
- 1
form_with **attributes, data: data, &
end
end
- 1
class Messages::AttachmentPresentation
- 1
def initialize(message, context:)
- 2
@message, @context = message, context
end
- 1
def render
- 2
if message.attachment.attached?
- 2
if message.attachment.previewable? || message.attachment.variable?
- 2
render_preview
else
render_link
end
end
end
- 1
private
- 1
attr_reader :message, :context
- 1
delegate :tag, :link_to, :broadcast_image_tag, :rails_blob_path, :url_for, to: :context
- 1
def render_preview
- 2
if message.attachment.video?
video_preview_tag
else
- 2
lightboxed_image_preview_tag
end
end
- 1
def video_preview_tag
width, height = preview_dimensions
inline_media_dimension_constraints(width, height) do
tag.video \
src: rails_blob_path(message.attachment), poster: url_for(message.attachment.preview(format: :webp, resize_to_limit: [ Message::THUMBNAIL_MAX_WIDTH, Message::THUMBNAIL_MAX_HEIGHT ])),
controls: true, preload: :none, width: "100%", height: "100%", class: "message__attachment"
end
end
- 1
def lightboxed_image_preview_tag
- 2
width, height = preview_dimensions
- 2
inline_media_dimension_constraints(width, height) do
- 2
lightbox_link do
- 2
broadcast_image_tag message.attachment.representation(:thumb), width: width, height: height, class: "message__attachment", loading: "lazy"
end
end
end
- 1
def inline_media_dimension_constraints(width, height, &)
- 2
if width && height
- 2
aspect_ratio = (width / height.to_f)
- 2
tag.div class: "max-inline-size center flex overflow-clip", style: "width: #{width / 2}px; aspect-ratio: #{aspect_ratio};", &
else
tag.div class: "max-inline-size center overflow-clip", &
end
end
- 1
def preview_dimensions
- 2
width = message.attachment.metadata[:width]
- 2
height = message.attachment.metadata[:height]
case
- 2
when width.nil? || height.nil?
[ nil, nil ]
when width <= Message::THUMBNAIL_MAX_WIDTH && height <= Message::THUMBNAIL_MAX_HEIGHT
[ width, height ]
else
- 2
width_factor = Message::THUMBNAIL_MAX_WIDTH.to_f / width
- 2
height_factor = Message::THUMBNAIL_MAX_HEIGHT.to_f / height
- 2
scale_factor = [ width_factor, height_factor ].min
- 2
[ width * scale_factor, height * scale_factor ]
end
end
- 1
def render_link
tag.div class: "flex-inline align-center gap-half" do
broadcast_image_tag("common-file-text.svg", size: 22, class: "colorize--black", aria: { hidden: "true" }) +
tag.span(filename) + download_link + share_button
end
end
- 1
def lightbox_link(&)
- 2
link_to rails_blob_path(message.attachment), class: "flex", data: {
lightbox_target: "image", action: "lightbox#open", lightbox_url_value: download_url }, &
end
- 1
def download_link
link_to download_url, class: "btn message__action-btn hide-in-ios-pwa", style: "--width: auto;" do
broadcast_image_tag("download.svg", aria: { hidden: "true" }, size: 20) + tag.span("Download #{ filename }", class: "for-screen-reader")
end
end
- 1
def share_button
tag.button class: "btn message__action-btn", style: "--width: auto;", data: { controller: "web-share", action: "web-share#share", web_share_files_value: download_url } do
broadcast_image_tag("share.svg", aria: { hidden: "true" }, size: 20) + tag.span("Share #{ filename }", class: "for-screen-reader")
end
end
- 1
def filename
message.attachment.filename.to_s
end
- 1
def download_url
- 2
rails_blob_path message.attachment, disposition: "attachment", only_path: true
end
end
- 1
module MessagesHelper
- 1
def message_area_tag(room, &)
- 2
tag.div id: "message-area", class: "message-area", contents: true, data: {
controller: "messages presence drop-target",
action: [ messages_actions, drop_target_actions, presence_actions ].join(" "),
messages_first_of_day_class: "message--first-of-day",
messages_formatted_class: "message--formatted",
messages_me_class: "message--me",
messages_mentioned_class: "message--mentioned",
messages_threaded_class: "message--threaded",
messages_page_url_value: room_messages_url(room)
}, &
end
- 1
def messages_tag(room, &)
- 2
tag.div id: dom_id(room, :messages), class: "messages", data: {
controller: "maintain-scroll refresh-room",
action: [ maintain_scroll_actions, refresh_room_actions ].join(" "),
messages_target: "messages",
refresh_room_loaded_at_value: room.updated_at.to_fs(:epoch),
refresh_room_url_value: room_refresh_url(room)
}, &
end
- 1
def message_tag(message, &)
- 37
message_timestamp_milliseconds = message.created_at.to_fs(:epoch)
- 37
tag.div id: dom_id(message),
class: "message #{"message--emoji" if message.plain_text_body.all_emoji?}",
data: {
controller: "reply",
user_id: message.creator_id,
message_id: message.id,
message_timestamp: message_timestamp_milliseconds,
message_updated_at: message.updated_at.to_fs(:epoch),
sort_value: message_timestamp_milliseconds,
messages_target: "message",
search_results_target: "message",
refresh_room_target: "message",
reply_composer_outlet: "#composer"
}, &
rescue Exception => e
Sentry.capture_exception(e, extra: { message: message })
Rails.logger.error "Exception while rendering message #{message.class.name}##{message.id}, failed with: #{e.class} `#{e.message}`"
render "messages/unrenderable"
end
- 1
def message_timestamp(message, **attributes)
- 37
local_datetime_tag message.created_at, **attributes
end
- 1
def message_presentation(message)
- 37
case message.content_type
when "attachment"
- 2
message_attachment_presentation(message)
when "sound"
message_sound_presentation(message)
else
- 35
auto_link h(ContentFilters::TextMessagePresentationFilters.apply(message.body.body)), html: { target: "_blank" }
end
rescue Exception => e
Sentry.capture_exception(e, extra: { message: message })
Rails.logger.error "Exception while generating message representation for #{message.class.name}##{message.id}, failed with: #{e.class} `#{e.message}`"
""
end
- 1
private
- 1
def messages_actions
- 2
"turbo:before-stream-render@document->messages#beforeStreamRender keydown.up@document->messages#editMyLastMessage"
end
- 1
def maintain_scroll_actions
- 2
"turbo:before-stream-render@document->maintain-scroll#beforeStreamRender"
end
- 1
def refresh_room_actions
- 2
"visibilitychange@document->refresh-room#visibilityChanged online@window->refresh-room#online"
end
- 1
def presence_actions
- 2
"visibilitychange@document->presence#visibilityChanged"
end
- 1
def message_attachment_presentation(message)
- 2
Messages::AttachmentPresentation.new(message, context: self).render
end
- 1
def message_sound_presentation(message)
sound = message.sound
tag.div class: "sound", data: { controller: "sound", action: "messages:play->sound#play", sound_url_value: asset_path(sound.asset_path) } do
play_button + (sound.image ? sound_image_tag(sound.image) : sound.text)
end
end
- 1
def play_button
tag.button "🔊", class: "btn btn--plain", data: { action: "sound#play" }
end
- 1
def sound_image_tag(image)
image_tag image.asset_path, width: image.width, height: image.height, class: "align--middle"
end
- 1
def message_author_title(author)
[ author.name, author.bio ].compact_blank.join(" – ")
end
end
- 1
module QrCodeHelper
- 1
def link_to_zoom_qr_code(url, &)
- 3
id = Base64.urlsafe_encode64(url)
- 3
link_to qr_code_path(id), class: "btn", data: {
lightbox_target: "image", action: "lightbox#open", lightbox_url_value: qr_code_path(id) }, &
end
end
- 1
module RichTextHelper
- 1
def rich_text_data_actions
default_actions =
- 2
"trix-change->typing-notifications#start keydown->composer#submitByKeyboard"
autocomplete_actions =
- 2
"trix-focus->rich-autocomplete#focus trix-change->rich-autocomplete#search trix-blur->rich-autocomplete#blur"
- 2
[ default_actions, autocomplete_actions ].join(" ")
end
end
- 1
module Rooms::InvolvementsHelper
- 1
def turbo_frame_for_involvement_tag(room, &)
- 1
turbo_frame_tag dom_id(room, :involvement), data: {
controller: "turbo-frame", action: "notifications:ready@window->turbo-frame#load", turbo_frame_url_param: room_involvement_path(room)
}, &
end
- 1
def button_to_change_involvement(room, involvement)
- 7
button_to room_involvement_path(room, involvement: next_involvement_for(room, involvement: involvement)),
method: :put,
role: "checkbox", aria: { checked: true, labelledby: dom_id(room, :involvement_label) }, tabindex: 0,
class: "btn #{involvement}" do
- 7
image_tag("notification-bell-#{involvement}.svg", aria: { hidden: "true" }, size: 20) +
tag.span(HUMANIZE_INVOLVEMENT[involvement], class: "for-screen-reader", id: dom_id(room, :involvement_label))
end
end
- 1
private
HUMANIZE_INVOLVEMENT = {
- 1
"mentions" => "Notifying about @ mentions",
"everything" => "Notifying about all messages",
"nothing" => "Notifications are off",
"invisible" => "Notifications are off and room invisible in sidebar"
}
- 1
SHARED_INVOLVEMENT_ORDER = %w[ mentions everything nothing invisible ]
- 1
DIRECT_INVOLVEMENT_ORDER = %w[ everything nothing ]
- 1
def next_involvement_for(room, involvement:)
- 7
if room.direct?
- 2
DIRECT_INVOLVEMENT_ORDER[DIRECT_INVOLVEMENT_ORDER.index(involvement) + 1] || DIRECT_INVOLVEMENT_ORDER.first
else
- 5
SHARED_INVOLVEMENT_ORDER[SHARED_INVOLVEMENT_ORDER.index(involvement) + 1] || SHARED_INVOLVEMENT_ORDER.first
end
end
end
- 1
module RoomsHelper
- 1
def link_to_room(room, **attributes, &)
- 32
link_to room_path(room), **attributes, data: {
rooms_list_target: "room", room_id: room.id, badge_dot_target: "unread", sorted_list_target: "item"
}.merge(attributes.delete(:data) || {}), &
end
- 1
def link_to_edit_room(room, &)
- 2
link_to \
[ :edit, @room ],
class: "btn",
style: "view-transition-name: edit-room-#{@room.id}",
data: { room_id: @room.id },
&
end
- 1
def link_back_to_last_room_visited
- 3
if last_room = last_room_visited
- 3
link_back_to room_path(last_room)
else
link_back_to root_path
end
end
- 1
def button_to_delete_room(room, url: nil)
button_to url || room_url(room), method: :delete, class: "btn btn--negative max-width", aria: { label: "Delete #{room.name}" },
data: { turbo_confirm: "Are you sure you want to delete this room and all messages in it? This can’t be undone." } do
image_tag("trash.svg", aria: { hidden: "true" }, size: 20) +
tag.span(room_display_name(room), class: "overflow-ellipsis")
end
end
- 1
def button_to_jump_to_newest_message
- 2
tag.button \
class: "message-area__return-to-latest btn",
data: { action: "messages#returnToLatest", messages_target: "latest" },
hidden: true do
- 2
image_tag("arrow-down.svg", aria: { hidden: "true" }, size: 20) +
tag.span("Jump to newest message", class: "for-screen-reader")
end
end
- 1
def submit_room_button_tag
- 2
button_tag class: "btn btn--reversed txt-large center", type: "submit" do
- 2
image_tag("check.svg", aria: { hidden: "true" }, size: 20) +
tag.span("Save", class: "for-screen-reader")
end
end
- 1
def composer_form_tag(room, &)
- 2
form_with model: Message.new, url: room_messages_path(room),
id: "composer", class: "margin-block flex-item-grow contain", data: composer_data_options(room), &
end
- 1
def room_display_name(room, for_user: Current.user)
- 48
if room.direct?
- 7
room.users.without(for_user).pluck(:name).to_sentence.presence || for_user&.name
else
- 41
room.name
end
end
- 1
private
- 1
def composer_data_options(room)
{
- 2
controller: "composer drop-target",
action: composer_data_actions,
composer_messages_outlet: "#message-area",
composer_toolbar_class: "composer--rich-text", composer_room_id_value: room.id
}
end
- 1
def composer_data_actions
- 2
drag_and_drop_actions = "drop-target:drop@window->composer#dropFiles"
trix_attachment_actions =
- 2
"trix-file-accept->composer#preventAttachment refresh-room:online@window->composer#online"
remaining_actions =
- 2
"typing-notifications#stop paste->composer#pasteFiles turbo:submit-end->composer#submitEnd refresh-room:offline@window->composer#offline"
- 2
[ drop_target_actions, drag_and_drop_actions, trix_attachment_actions, remaining_actions ].join(" ")
end
end
- 1
module SearchesHelper
- 1
def search_results_tag(&)
- 3
tag.div id: "search-results", class: "messages searches__results", data: {
controller: "search-results",
search_results_target: "messages",
search_results_me_class: "message--me",
search_results_threaded_class: "message--threaded",
search_results_mentioned_class: "message--mentioned",
search_results_formatted_class: "message--formatted"
}, &
end
end
- 1
module TimeHelper
- 1
def local_datetime_tag(datetime, style: :time, **attributes)
- 74
tag.time **attributes, datetime: datetime.iso8601, data: { local_time_target: style }
end
end
- 1
module TranslationsHelper
TRANSLATIONS = {
- 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" },
password: { "🇺🇸": "Enter your password", "🇪🇸": "Introduce tu contraseña", "🇫🇷": "Saisissez votre mot de passe", "🇮🇳": "अपना पासवर्ड दर्ज करें", "🇩🇪": "Geben Sie Ihr Passwort ein", "🇧🇷": "Insira sua senha" },
update_password: { "🇺🇸": "Change password", "🇪🇸": "Cambiar contraseña", "🇫🇷": "Changer le mot de passe", "🇮🇳": "पासवर्ड बदलें", "🇩🇪": "Passwort ändern", "🇧🇷": "Alterar senha" },
user_name: { "🇺🇸": "Enter your name", "🇪🇸": "Introduce tu nombre", "🇫🇷": "Entrez votre nom", "🇮🇳": "अपना नाम दर्ज करें", "🇩🇪": "Geben Sie Ihren Namen ein", "🇧🇷": "Insira seu nome" },
account_name: { "🇺🇸": "Name this account", "🇪🇸": "Nombre de esta cuenta", "🇫🇷": "Nommez ce compte", "🇮🇳": "इस खाते का नाम दें", "🇩🇪": "Benennen Sie dieses Konto", "🇧🇷": "Dê um nome a essa conta" },
room_name: { "🇺🇸": "Name the room", "🇪🇸": "Nombrar la sala", "🇫🇷": "Nommez la salle", "🇮🇳": "कमरे का नाम दें", "🇩🇪": "Geben Sie dem Raum einen Namen", "🇧🇷": "Dê um nome a essa sala" },
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." },
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." },
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ê." },
webhook_url: { "🇺🇸": "Webhook URL", "🇪🇸": "URL del Webhook", "🇫🇷": "URL du webhook", "🇮🇳": "वेबहुक URL", "🇩🇪": "Webhook-URL", "🇧🇷": "URL do Webhook" },
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." },
bot_name: { "🇺🇸": "Name the bot", "🇪🇸": "Nombrar al bot", "🇫🇷": "Nommer le bot", "🇮🇳": "बॉट का नाम दें", "🇩🇪": "Benenne den Bot", "🇧🇷": "Dê um nome ao bot" },
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." }
}
- 1
def translations_for(translation_key)
- 26
tag.dl(class: "language-list") do
- 26
TRANSLATIONS[translation_key].map do |language, translation|
- 156
concat tag.dt(language)
- 156
concat tag.dd(translation, class: "margin-none")
end
end
end
- 1
def translation_button(translation_key)
- 26
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
- 26
tag.summary(class: "btn", tabindex: -1) do
- 26
concat image_tag("globe.svg", size: 20, aria: { hidden: "true" }, class: "color-icon")
- 26
concat tag.span("Translate", class: "for-screen-reader")
end +
tag.div(class: "lanuage-list-menu shadow", data: { popup_target: "menu" }) do
- 26
translations_for(translation_key)
end
end
end
end
- 1
require "zlib"
- 1
module Users::AvatarsHelper
AVATAR_COLORS = %w[
- 1
#AF2E1B #CC6324 #3B4B59 #BFA07A #ED8008 #ED3F1C #BF1B1B #736B1E #D07B53
#736356 #AD1D1D #BF7C2A #C09C6F #698F9C #7C956B #5D618F #3B3633 #67695E
]
- 1
def avatar_background_color(user)
- 1
AVATAR_COLORS[Zlib.crc32(user.to_param) % AVATAR_COLORS.size]
end
- 1
def avatar_tag(user, **options)
- 69
link_to user_path(user), title: user.title, class: "btn avatar", data: { turbo_frame: "_top" } do
- 69
image_tag fresh_user_avatar_path(user), aria: { hidden: "true" }, size: 48, **options
end
end
end
- 1
module Users::FilterHelper
- 1
def user_filter_menu_tag(&)
- 2
tag.menu class: "flex flex-column gap margin-none pad overflow-y constrain-height",
data: { controller: "filter", filter_active_class: "filter--active", filter_selected_class: "selected" }, &
end
- 1
def user_filter_search_tag
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" }
end
end
- 1
module Users::ProfilesHelper
- 1
def profile_form_with(model, **params, &)
- 3
form_with \
model: @user, url: user_profile_path, method: :patch,
data: { controller: "form" },
**params,
&
end
- 1
def profile_form_submit_button
- 3
tag.button class: "btn btn--reversed center txt-large", type: "submit" do
- 3
image_tag("check.svg", aria: { hidden: "true" }, size: 20) +
tag.span("Save changes", class: "for-screen-reader")
end
end
- 1
def web_share_session_button(url, title, text, &)
- 3
tag.button class: "btn", hidden: true, data: {
controller: "web-share", action: "web-share#share",
web_share_url_value: url,
web_share_text_value: text,
web_share_title_value: title
}, &
end
end
- 1
module Users::SidebarHelper
- 1
def sidebar_turbo_frame_tag(src: nil, &)
- 5
turbo_frame_tag :user_sidebar, src: src, target: "_top", data: {
turbo_permanent: true,
controller: "rooms-list read-rooms turbo-frame",
rooms_list_unread_class: "unread",
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
}, &
end
end
- 1
module UsersHelper
- 1
def button_to_direct_room_with(user)
button_to rooms_directs_path(user_ids: [ user.id ]), class: "btn btn--primary full-width txt--large" do
image_tag("messages.svg")
end
end
end
- 1
module VersionHelper
- 1
def version_badge
- 5
tag.span(Rails.application.config.app_version, class: "version-badge")
end
end
- 1
class ApplicationJob < ActiveJob::Base
# Automatically retry jobs that encountered a deadlock
# retry_on ActiveRecord::Deadlocked
# Most jobs are safe to ignore if the underlying records are no longer available
# discard_on ActiveJob::DeserializationError
end
- 1
class Bot::WebhookJob < ApplicationJob
- 1
def perform(bot, message)
- 1
bot.deliver_webhook(message)
end
end
- 1
class Room::PushMessageJob < ApplicationJob
- 1
def perform(room, message)
- 6
Room::MessagePusher.new(room:, message:).push
end
end
- 1
class Account < ApplicationRecord
- 1
include Joinable
- 1
has_one_attached :logo
end
- 1
module Account::Joinable
- 1
extend ActiveSupport::Concern
- 1
included do
- 7
before_create { self.join_code = generate_join_code }
end
- 1
def reset_join_code
- 2
update! join_code: generate_join_code
end
- 1
private
- 1
def generate_join_code
- 8
SecureRandom.alphanumeric(12).scan(/.{4}/).join("-")
end
end
- 1
class ApplicationPlatform < PlatformAgent
- 1
def ios?
match? /iPhone|iPad/
end
- 1
def android?
match? /Android/
end
- 1
def mac?
match? /Macintosh/
end
- 1
def chrome?
- 21
user_agent.browser.match? /Chrome/
end
- 1
def firefox?
- 14
user_agent.browser.match? /Firefox|FxiOS/
end
- 1
def safari?
- 14
user_agent.browser.match? /Safari/
end
- 1
def edge?
- 7
user_agent.browser.match? /Edg/
end
- 1
def apple_messages?
# Apple Messages pretends to be Facebook and Twitter bots via spoofed user agent.
# We want to avoid showing "Unsupported browser" message when a Campfire link
# is shared via Messages.
- 1
match?(/facebookexternalhit/i) && match?(/Twitterbot/i)
end
- 1
def mobile?
ios? || android?
end
- 1
def desktop?
!mobile?
end
- 1
def windows?
operating_system == "Windows"
end
- 1
def operating_system
- 2
case user_agent.platform
when /Android/ then "Android"
when /iPad/ then "iPad"
when /iPhone/ then "iPhone"
when /Macintosh/ then "macOS"
when /Windows/ then "Windows"
when /CrOS/ then "ChromeOS"
else
- 2
os =~ /Linux/ ? "Linux" : os
end
end
end
- 1
class ApplicationRecord < ActiveRecord::Base
- 1
primary_abstract_class
end
- 1
class Boost < ApplicationRecord
- 1
belongs_to :message, touch: true
- 2
belongs_to :booster, class_name: "User", default: -> { Current.user }
- 38
scope :ordered, -> { order(:created_at) }
end
- 1
class Current < ActiveSupport::CurrentAttributes
- 1
attribute :user, :request
- 1
delegate :host, :protocol, to: :request, prefix: true, allow_nil: true
- 1
def account
- 136
Account.first
end
end
- 1
class FirstRun
- 1
ACCOUNT_NAME = "Campfire"
- 1
FIRST_ROOM_NAME = "All Talk"
- 1
def self.create!(user_params)
- 4
account = Account.create!(name: ACCOUNT_NAME)
- 4
room = Rooms::Open.new(name: FIRST_ROOM_NAME)
- 4
administrator = room.creator = User.new(user_params.merge(role: :administrator))
- 4
room.save!
- 4
room.memberships.grant_to administrator
- 4
administrator
end
end
- 1
class Membership < ApplicationRecord
- 1
include Connectable
- 1
belongs_to :room
- 1
belongs_to :user
- 8
after_destroy_commit { user.reset_remote_connections }
- 1
enum involvement: %w[ invisible nothing mentions everything ].index_by(&:itself), _prefix: :involved_in
- 5
scope :with_ordered_room, -> { includes(:room).joins(:room).order("LOWER(rooms.name)") }
- 6
scope :without_direct_rooms, -> { joins(:room).where.not(room: { type: "Rooms::Direct" }) }
- 60
scope :visible, -> { where.not(involvement: :invisible) }
- 15
scope :unread, -> { where.not(unread_at: nil) }
- 1
def read
update!(unread_at: nil)
end
- 1
def unread?
- 30
unread_at.present?
end
end
- 1
module Membership::Connectable
- 1
extend ActiveSupport::Concern
- 1
CONNECTION_TTL = 60.seconds
- 1
included do
- 4
scope :connected, -> { where(connected_at: CONNECTION_TTL.ago..) }
- 60
scope :disconnected, -> { where(connected_at: [ nil, ...CONNECTION_TTL.ago ]) }
end
- 1
class_methods do
- 1
def disconnect_all
connected.update_all connected_at: nil, connections: 0, updated_at: Time.current
end
- 1
def connect(membership, connections)
- 3
where(id: membership.id).update_all(connections: connections, connected_at: Time.current, unread_at: nil)
end
end
- 1
def connected?
- 35
connected_at? && connected_at >= CONNECTION_TTL.ago
end
- 1
def present
- 3
self.class.connect(self, connected? ? connections + 1 : 1)
end
- 1
def connected
- 14
increment_connections
- 14
touch :connected_at
end
- 1
def disconnected
- 6
decrement_connections
- 6
update! connected_at: nil if connections < 1
end
- 1
def refresh_connection
- 1
increment_connections unless connected?
- 1
touch :connected_at
end
- 1
def increment_connections
- 15
connected? ? increment!(:connections, touch: true) : update!(connections: 1)
end
- 1
def decrement_connections
- 6
connected? ? decrement!(:connections, touch: true) : update!(connections: 0)
end
end
- 1
class Message < ApplicationRecord
- 1
include Attachment, Broadcasts, Mentionee, Pagination, Searchable
- 1
belongs_to :room, touch: true
- 9
belongs_to :creator, class_name: "User", default: -> { Current.user }
- 1
has_many :boosts, dependent: :destroy
- 1
has_rich_text :body
- 45
before_create -> { self.client_message_id ||= Random.uuid } # Bots don't care
- 45
after_create_commit -> { room.receive(self) }
- 32
scope :ordered, -> { order(:created_at) }
- 9
scope :with_creator, -> { includes(:creator) }
- 1
def plain_text_body
- 193
body.to_plain_text.presence || attachment&.filename&.to_s || ""
end
- 1
def to_key
- 610
[ client_message_id ]
end
- 1
def content_type
case
- 78
when attachment? then "attachment"
when sound.present? then "sound"
- 70
else "text"
end.inquiry
end
- 1
def sound
- 70
plain_text_body.match(/\A\/play (?<name>\w+)\z/) do |match|
Sound.find_by_name match[:name]
end
end
end
- 1
module Message::Attachment
- 1
extend ActiveSupport::Concern
- 1
THUMBNAIL_MAX_WIDTH = 1200
- 1
THUMBNAIL_MAX_HEIGHT = 800
- 1
included do
- 1
has_one_attached :attachment do |attachable|
- 1
attachable.variant :thumb, resize_to_limit: [ THUMBNAIL_MAX_WIDTH, THUMBNAIL_MAX_HEIGHT ]
end
end
- 1
module ClassMethods
- 1
def create_with_attachment!(attributes)
- 12
create!(attributes).tap(&:process_attachment)
end
end
- 1
def attachment?
- 74
attachment.attached?
end
- 1
def process_attachment
- 12
ensure_attachment_analyzed
- 12
process_attachment_thumbnail
end
- 1
private
- 1
def ensure_attachment_analyzed
- 12
attachment&.analyze
end
- 1
def process_attachment_thumbnail
case
- 12
when attachment.video?
- 1
attachment.preview(format: :webp).processed
when attachment.representable?
- 4
attachment.representation(:thumb).processed
end
end
end
- 1
module Message::Broadcasts
- 1
def broadcast_create
- 11
broadcast_append_to room, :messages, target: [ room, :messages ]
- 11
ActionCable.server.broadcast("unread_rooms", { roomId: room.id })
end
end
- 1
module Message::Mentionee
- 1
extend ActiveSupport::Concern
- 1
def mentionees
- 16
room.users.where(id: mentioned_users.map(&:id))
end
- 1
private
- 1
def mentioned_users
- 16
if body.body
- 15
body.body.attachables.grep(User).uniq
else
- 1
[]
end
end
end
- 1
module Message::Pagination
- 1
extend ActiveSupport::Concern
- 1
PAGE_SIZE = 40
- 1
included do
- 7
scope :last_page, -> { ordered.last(PAGE_SIZE) }
- 3
scope :first_page, -> { ordered.first(PAGE_SIZE) }
- 2
scope :before, ->(message) { where("created_at < ?", message.created_at) }
- 2
scope :after, ->(message) { where("created_at > ?", message.created_at) }
- 2
scope :page_before, ->(message) { before(message).last_page }
- 2
scope :page_after, ->(message) { after(message).first_page }
- 2
scope :page_created_since, ->(time) { where("created_at > ?", time).first_page }
- 2
scope :page_updated_since, ->(time) { where("updated_at > ?", time).last_page }
end
- 1
class_methods do
- 1
def page_around(message)
page_before(message) + [ message ] + page_after(message)
end
- 1
def paged?
count > PAGE_SIZE
end
end
end
- 1
module Message::Searchable
- 1
extend ActiveSupport::Concern
- 1
included do
- 1
after_create_commit :create_in_index
- 1
after_update_commit :update_in_index
- 1
after_destroy_commit :remove_from_index
- 10
scope :search, ->(query) { joins("join message_search_index idx on messages.id = idx.rowid").where("idx.body match ?", query).ordered }
end
- 1
private
- 1
def create_in_index
- 44
execute_sql_with_binds "insert into message_search_index(rowid, body) values (?, ?)", id, plain_text_body
end
- 1
def update_in_index
- 20
execute_sql_with_binds "update message_search_index set body = ? where rowid = ?", plain_text_body, id
end
- 1
def remove_from_index
- 110
execute_sql_with_binds "delete from message_search_index where rowid = ?", id
end
- 1
def execute_sql_with_binds(*statement)
- 174
self.class.connection.execute self.class.sanitize_sql(statement)
end
end
- 1
require "nokogiri"
- 1
class Opengraph::Document
- 1
attr_accessor :html
- 1
def initialize(html)
- 20
@html = Nokogiri::HTML(html)
end
- 1
def opengraph_attributes
- 20
@opengraph_attributes ||= extract_opengraph_attributes
end
- 1
private
- 1
def extract_opengraph_attributes
- 20
opengraph_tags = html.xpath("//*/meta[starts-with(@property, \"og:\") or starts-with(@name, \"og:\")]").map do |tag|
- 61
key = tag.key?("property") ? "property" : "name"
- 61
[ tag[key].gsub("og:", "").to_sym, sanitize_content(tag["content"]) ] if tag["content"].present?
end
- 20
Hash[opengraph_tags.compact].slice(*Opengraph::Metadata::ATTRIBUTES)
end
- 1
def sanitize_content(content)
- 61
html.meta_encoding ? content : content.encode("UTF-8", "binary", invalid: :replace, undef: :replace, replace: "")
end
end
- 1
require "net/http"
- 1
require "restricted_http/private_network_guard"
- 1
class Opengraph::Fetch
- 1
ALLOWED_DOCUMENT_CONTENT_TYPE = "text/html"
- 1
MAX_BODY_SIZE = 5.megabytes
- 1
MAX_REDIRECTS = 10
- 1
class TooManyRedirectsError < StandardError; end
- 1
class RedirectDeniedError < StandardError; end
- 1
def fetch_document(url, ip: RestrictedHTTP::PrivateNetworkGuard.resolve(url.host))
- 29
request(url, Net::HTTP::Get, ip: ip) do |response|
- 25
return body_if_acceptable(response)
end
end
- 1
def fetch_content_type(url, ip: RestrictedHTTP::PrivateNetworkGuard.resolve(url.host))
- 10
request(url, Net::HTTP::Head, ip: ip) do |response|
- 10
return response["Content-Type"]
end
end
- 1
private
- 1
def request(url, request_class, ip:)
- 39
MAX_REDIRECTS.times do
- 50
Net::HTTP.start(url.host, url.port, ipaddr: ip, use_ssl: url.scheme == "https") do |http|
- 50
http.request request_class.new(url) do |response|
- 48
if response.is_a?(Net::HTTPRedirection)
- 13
url, ip = resolve_redirect(response["location"])
else
- 35
yield response
end
end
end
end
- 1
raise TooManyRedirectsError
end
- 1
def resolve_redirect(location)
- 13
url = URI.parse(location)
- 13
raise RedirectDeniedError unless url.is_a?(URI::HTTP)
- 13
[ url, RestrictedHTTP::PrivateNetworkGuard.resolve(url.host) ]
end
- 1
def body_if_acceptable(response)
- 25
size_restricted_body(response) if response_valid?(response)
end
- 1
def size_restricted_body(response)
# We've already checked the Content-Length header, to try to avoid reading
# the body of any large responses. But that header could be wrong or
# missing. To be on the safe side, we'll read the body in chunks, and bail
# if it runs over our size limit.
- 19
"".tap do |body|
- 19
response.read_body do |chunk|
- 19
return nil if body.bytesize + chunk.bytesize > MAX_BODY_SIZE
- 17
body << chunk
end
end
end
- 1
def response_valid?(response)
- 25
status_valid?(response) && content_type_valid?(response) && content_length_valid?(response)
end
- 1
def status_valid?(response)
- 25
response.is_a?(Net::HTTPOK)
end
- 1
def content_type_valid?(response)
- 24
response.content_type == ALLOWED_DOCUMENT_CONTENT_TYPE
end
- 1
def content_length_valid?(response)
- 21
response.content_length.to_i <= MAX_BODY_SIZE
end
end
- 1
require "restricted_http/private_network_guard"
- 1
class Opengraph::Location
- 1
include ActiveModel::Validations
- 1
attr_accessor :url, :parsed_url
- 1
validate :validate_url, :validate_url_is_public
- 1
def initialize(url)
- 72
@url = url
end
- 1
def read_html
- 25
fetch_html if valid? && !url.match(FILES_AND_MEDIA_URL_REGEX)
end
- 1
def fetch_content_type
- 13
Opengraph::Fetch.new.fetch_content_type(parsed_url, ip: resolved_ip) if valid?
rescue => e
Rails.logger.warn "Failed to fetch #{parsed_url} at #{resolved_ip} (#{e})"
nil
end
- 1
def resolved_ip
- 99
return @resolved_ip if defined? @resolved_ip
- 71
@resolved_ip = RestrictedHTTP::PrivateNetworkGuard.resolve(parsed_url.host) rescue nil
end
- 1
private
- 1
FILES_AND_MEDIA_URL_REGEX = /\bhttps?:\/\/\S+\.(?:zip|tar|tar\.gz|tar\.bz2|tar\.xz|gz|bz2|rar|7z|dmg|exe|msi|pkg|deb|iso|jpg|jpeg|png|gif|bmp|mp4|mov|avi|mkv|wmv|flv|heic|heif|mp3|wav|ogg|aac|wma|webm|ogv|mpg|mpeg)\b/
- 1
def validate_url
- 71
errors.add :url, "is invalid" unless parsed_url.is_a?(URI::HTTP)
end
- 1
def validate_url_is_public
- 71
errors.add :url, "is not public" unless resolved_ip
end
- 1
def parsed_url
- 170
return @parsed_url if defined? @parsed_url
- 71
@parsed_url = URI.parse(url) rescue nil
end
- 1
def fetch_html
- 19
Opengraph::Fetch.new.fetch_document(parsed_url, ip: resolved_ip)
rescue => e
Rails.logger.warn "Failed to fetch #{parsed_url} at #{resolved_ip} (#{e})"
nil
end
end
- 1
class Opengraph::Metadata
- 1
include ActiveModel::Model
- 1
include ActiveModel::Validations::Callbacks
- 1
include ActionView::Helpers::SanitizeHelper
- 1
include Fetching
- 1
ATTRIBUTES = %i[ title url image description ]
- 1
attr_accessor *ATTRIBUTES
- 1
before_validation :sanitize_fields
- 1
validates_presence_of :title, :url, :description
- 1
validate :ensure_valid_image_url
- 1
private
- 1
def sanitize_fields
- 17
self.title = sanitize(strip_tags(title))
- 17
self.description = sanitize(strip_tags(description))
end
- 1
def ensure_valid_image_url
- 17
if image.present?
- 8
errors.add :image, "url is invalid" unless Opengraph::Location.new(image).valid?
end
end
end
- 1
module Opengraph::Metadata::Fetching
- 1
extend ActiveSupport::Concern
- 1
module ClassMethods
- 1
def from_url(url)
- 17
document = fetch_document(url)
- 17
attributes = document.opengraph_attributes
- 17
new attributes.merge(url: valid_canonical_url(attributes[:url], url), image: valid_image_content_type(attributes[:image]))
end
- 1
private
- 1
TWITTER_HOSTS = %w[ twitter.com www.twitter.com x.com www.x.com ]
- 1
FX_TWITTER_HOST = "fxtwitter.com"
- 1
ALLOWED_IMAGE_CONTENT_TYPES = %w[ image/jpeg image/png image/gif image/webp ]
- 1
def fetch_document(untrusted_url)
- 17
tweet_url?(untrusted_url) ? fetch_fxtwitter_document(untrusted_url) : fetch_non_fxtwitter_document(untrusted_url)
end
- 1
def fetch_fxtwitter_document(untrusted_url)
- 2
fxtwitter_url = replace_twitter_domain_for_opengraph_support(untrusted_url)
- 2
Opengraph::Location.new(fxtwitter_url).then do |location|
# fxtwitter.com HTML response does not include character encoding, resulting in emojis and quotes not
# being encoded properly.
- 2
Opengraph::Document.new(location.read_html.force_encoding("UTF-8"))
end
end
- 1
def fetch_non_fxtwitter_document(untrusted_url)
- 15
Opengraph::Location.new(untrusted_url).then do |location|
- 15
Opengraph::Document.new(location.read_html)
end
end
- 1
def valid_canonical_url(url, fallback)
- 17
Opengraph::Location.new(url).valid? ? url : fallback
end
- 1
def valid_image_content_type(image)
- 17
return unless image.present?
- 13
content_type = Opengraph::Location.new(URI.parse(image)).fetch_content_type&.downcase
- 13
content_type.in?(ALLOWED_IMAGE_CONTENT_TYPES) ? image : nil
rescue => e
Rails.logger.warn "Failed to fetch image content tpye: #{image} (#{e})"
nil
end
# Twitter.com and X.com do not support Opengraph at the moment.
# Piggybacking on fxtwitter.com allows us to have twitter unfurling
# without relying on fxtwitter.com's future availability.
- 1
def replace_twitter_domain_for_opengraph_support(url)
- 2
uri = URI.parse(url)
- 2
uri.host = FX_TWITTER_HOST if uri.host.in?(TWITTER_HOSTS)
- 2
uri.to_s
rescue URI::InvalidURIError
nil
end
- 1
def tweet_url?(url)
- 17
uri = URI.parse(url)
- 17
uri.host.in?(TWITTER_HOSTS) && uri.path.present? && uri.path != "/"
rescue URI::InvalidURIError
nil
end
end
end
- 1
module Push
- 1
def self.table_name_prefix
- 1
"push_"
end
end
- 1
class Push::Subscription < ApplicationRecord
- 1
belongs_to :user
- 1
def notification(**params)
- 14
WebPush::Notification.new(**params, badge: user.memberships.unread.count, endpoint: endpoint, p256dh_key: p256dh_key, auth_key: auth_key)
end
end
- 1
class Room < ApplicationRecord
- 1
has_many :memberships, dependent: :delete_all do
- 1
def grant_to(users)
- 23
room = proxy_association.owner
- 72
Membership.insert_all(Array(users).collect { |user| { room_id: room.id, user_id: user.id, involvement: room.default_involvement } })
end
- 1
def revoke_from(users)
- 4
destroy_by user: users
end
- 1
def revise(granted: [], revoked: [])
- 4
transaction do
- 4
grant_to(granted) if granted.present?
- 4
revoke_from(revoked) if revoked.present?
end
end
end
- 1
has_many :users, through: :memberships
- 1
has_many :messages, dependent: :destroy
- 5
belongs_to :creator, class_name: "User", default: -> { Current.user }
- 4
scope :opens, -> { where(type: "Rooms::Open") }
- 3
scope :closeds, -> { where(type: "Rooms::Closed") }
- 4
scope :directs, -> { where(type: "Rooms::Direct") }
- 2
scope :without_directs, -> { where.not(type: "Rooms::Direct") }
- 2
scope :ordered, -> { order("LOWER(name)") }
- 1
class << self
- 1
def create_for(attributes, users:)
- 6
transaction do
- 6
create!(attributes).tap do |room|
- 6
room.memberships.grant_to users
end
end
end
- 1
def original
- 10
order(:created_at).first
end
end
- 1
def receive(message)
- 44
unread_memberships(message)
- 44
push_later(message)
end
- 1
def open?
- 2
is_a?(Rooms::Open)
end
- 1
def closed?
- 1
is_a?(Rooms::Closed)
end
- 1
def direct?
- 117
is_a?(Rooms::Direct)
end
- 1
def default_involvement
- 45
"mentions"
end
- 1
private
- 1
def unread_memberships(message)
- 44
memberships.visible.disconnected.where.not(user: message.creator).update_all(unread_at: message.created_at, updated_at: Time.current)
end
- 1
def push_later(message)
- 44
Room::PushMessageJob.perform_later(self, message)
end
end
- 1
class Room::MessagePusher
- 1
attr_reader :room, :message
- 1
def initialize(room:, message:)
- 6
@room, @message = room, message
end
- 1
def push
- 6
build_payload.tap do |payload|
- 6
push_to_users_involved_in_everything(payload)
- 6
push_to_users_involved_in_mentions(payload)
end
end
- 1
private
- 1
def build_payload
- 6
if room.direct?
build_direct_payload
else
- 6
build_shared_payload
end
end
- 1
def build_direct_payload
{
title: message.creator.name,
body: message.plain_text_body,
path: Rails.application.routes.url_helpers.room_path(room)
}
end
- 1
def build_shared_payload
{
- 6
title: room.name,
body: "#{message.creator.name}: #{message.plain_text_body}",
path: Rails.application.routes.url_helpers.room_path(room)
}
end
- 1
def push_to_users_involved_in_everything(payload)
- 6
enqueue_payload_for_delivery payload, push_subscriptions_for_users_involved_in_everything
end
- 1
def push_to_users_involved_in_mentions(payload)
- 6
enqueue_payload_for_delivery payload, push_subscriptions_for_mentionable_users(message.mentionees)
end
- 1
def push_subscriptions_for_users_involved_in_everything
- 6
relevant_subscriptions.merge(Membership.involved_in_everything)
end
- 1
def push_subscriptions_for_mentionable_users(mentionees)
- 6
relevant_subscriptions.merge(Membership.involved_in_mentions).where(user_id: mentionees.ids)
end
- 1
def relevant_subscriptions
- 12
Push::Subscription
.joins(user: :memberships)
.merge(Membership.visible.disconnected.where(room: room).where.not(user: message.creator))
end
- 1
def enqueue_payload_for_delivery(payload, subscriptions)
- 12
Rails.configuration.x.web_push_pool.queue(payload, subscriptions)
end
end
# Rooms where only a subset of all users on the account have been explicited granted membership.
- 1
class Rooms::Closed < Room
end
# Rooms for direct message chats between users. These act as a singleton, so a single set of users will
# always refer to the same direct room.
- 1
class Rooms::Direct < Room
- 1
class << self
- 1
def find_or_create_for(users)
- 7
find_for(users) || create_for({}, users: users)
end
- 1
private
# FIXME: Find a more performant algorithm that won't be a problem on accounts with 10K+ direct rooms,
# which could be to store the membership id list as a hash on the room, and use that for lookup.
- 1
def find_for(users)
- 7
all.joins(:users).detect do |room|
- 39
Set.new(room.user_ids) == Set.new(users.pluck(:id))
end
end
end
- 1
def default_involvement
- 4
"everything"
end
end
# Rooms open to all users on the account. When a new user is added to the account, they're automatically granted membership.
- 1
class Rooms::Open < Room
- 1
after_save_commit :grant_access_to_all_users
- 1
private
- 1
def grant_access_to_all_users
- 28
memberships.grant_to(User.active) if type_previously_changed?(to: "Rooms::Open")
end
end
- 1
class Search < ApplicationRecord
- 1
belongs_to :user
- 1
after_create :trim_recent_searches
- 5
scope :ordered, -> { order(updated_at: :desc) }
- 1
class << self
- 1
def record(query)
- 1
find_or_create_by(query: query).touch
end
end
- 1
private
- 1
def trim_recent_searches
- 1
user.searches.excluding(user.searches.ordered.limit(10)).destroy_all
end
end
- 1
class Session < ApplicationRecord
- 1
ACTIVITY_REFRESH_RATE = 1.hour
- 1
has_secure_token
- 1
belongs_to :user
- 112
before_create { self.last_active_at ||= Time.now }
- 1
def self.start!(user_agent:, ip_address:)
- 111
create! user_agent: user_agent, ip_address: ip_address
end
- 1
def resume(user_agent:, ip_address:)
- 99
if last_active_at.before?(ACTIVITY_REFRESH_RATE.ago)
update! user_agent: user_agent, ip_address: ip_address, last_active_at: Time.now
end
end
end
- 1
class User < ApplicationRecord
- 1
include Avatar, Bot, Mentionable, Role, Transferable
- 1
has_many :memberships, dependent: :delete_all
- 1
has_many :rooms, through: :memberships
- 1
has_many :reachable_messages, through: :rooms, source: :messages
- 1
has_many :messages, dependent: :destroy, foreign_key: :creator_id
- 1
has_many :push_subscriptions, class_name: "Push::Subscription", dependent: :delete_all
- 1
has_many :boosts, dependent: :destroy, foreign_key: :booster_id
- 1
has_many :searches, dependent: :delete_all
- 1
has_many :sessions, dependent: :destroy
- 157
scope :active, -> { where(active: true) }
- 1
has_secure_password validations: false
- 1
after_create_commit :grant_membership_to_open_rooms
- 8
scope :ordered, -> { order("LOWER(name)") }
- 4
scope :filtered_by, ->(query) { where("name like ?", "%#{query}%") }
- 1
def initials
- 2
name.scan(/\b\w/).join
end
- 1
def title
- 106
[ name, bio ].compact_blank.join(" – ")
end
- 1
def deactivate
- 4
transaction do
- 4
close_remote_connections
- 4
memberships.without_direct_rooms.delete_all
- 4
push_subscriptions.delete_all
- 4
searches.delete_all
- 4
sessions.delete_all
- 4
update! active: false, email_address: deactived_email_address
end
end
- 1
def deactivated?
- 1
!active?
end
- 1
def reset_remote_connections
- 6
close_remote_connections reconnect: true
end
- 1
private
- 1
def grant_membership_to_open_rooms
- 29
Membership.insert_all(Rooms::Open.pluck(:id).collect { |room_id| { room_id: room_id, user_id: id } })
end
- 1
def deactived_email_address
- 4
email_address&.gsub(/@/, "-deactivated-#{SecureRandom.uuid}@")
end
- 1
def close_remote_connections(reconnect: false)
- 10
ActionCable.server.remote_connections.where(current_user: self).disconnect reconnect: reconnect
end
end
- 1
module User::Avatar
- 1
extend ActiveSupport::Concern
- 1
included do
- 1
has_one_attached :avatar
end
- 1
class_methods do
- 1
def from_avatar_token(sid)
- 3
find_signed!(sid, purpose: :avatar)
end
end
- 1
def avatar_token
- 97
signed_id(purpose: :avatar)
end
end
- 1
module User::Bot
- 1
extend ActiveSupport::Concern
- 1
included do
- 18
scope :active_bots, -> { active.where(role: :bot) }
- 1
scope :without_bots, -> { where.not(role: :bot) }
- 1
has_one :webhook, dependent: :delete
end
- 1
module ClassMethods
- 1
def create_bot!(attributes)
- 4
bot_token = generate_bot_token
- 4
webhook_url = attributes.delete(:webhook_url)
- 4
User.create!(**attributes, bot_token: bot_token, role: :bot).tap do |user|
- 4
user.create_webhook!(url: webhook_url) if webhook_url
end
end
- 1
def authenticate_bot(bot_key)
- 7
bot_id, bot_token = bot_key.split("-")
- 7
active.find_by(id: bot_id, bot_token: bot_token)
end
- 1
def generate_bot_token
- 8
SecureRandom.alphanumeric(12)
end
end
- 1
def update_bot!(attributes)
- 2
transaction do
- 2
update_webhook_url!(attributes.delete(:webhook_url))
- 2
update!(attributes)
end
end
- 1
def bot_key
- 20
"#{id}-#{bot_token}"
end
- 1
def reset_bot_key
- 2
update! bot_token: self.class.generate_bot_token
end
- 1
def webhook_url
- 2
webhook&.url
end
- 1
def deliver_webhook_later(message)
- 2
Bot::WebhookJob.perform_later(self, message) if webhook
end
- 1
def deliver_webhook(message)
- 1
webhook.deliver(message)
end
- 1
private
- 1
def update_webhook_url!(url)
- 2
if url.present?
webhook&.update!(url: url) || create_webhook!(url: url)
else
- 2
webhook&.destroy
end
end
end
- 1
module User::Mentionable
- 1
include ActionText::Attachable
- 1
def to_attachable_partial_path
- 3
"users/mention"
end
- 1
def to_trix_content_attachment_partial_path
"users/mention"
end
- 1
def attachable_plain_text_representation(caption)
- 25
"@#{name}"
end
end
- 1
module User::Role
- 1
extend ActiveSupport::Concern
- 1
included do
- 1
enum role: %i[ member administrator bot ]
end
- 1
def can_administer?(record = nil)
- 86
administrator? || self == record&.creator || record&.new_record?
end
end
- 1
module User::Transferable
- 1
extend ActiveSupport::Concern
- 1
TRANSFER_LINK_EXPIRY_DURATION = 4.hours
- 1
class_methods do
- 1
def find_by_transfer_id(id)
- 1
find_signed(id, purpose: :transfer)
end
end
- 1
def transfer_id
- 3
signed_id(purpose: :transfer, expires_in: TRANSFER_LINK_EXPIRY_DURATION)
end
end
- 1
require "net/http"
- 1
require "uri"
- 1
class Webhook < ApplicationRecord
- 1
ENDPOINT_TIMEOUT = 7.seconds
- 1
belongs_to :user
- 1
def deliver(message)
- 7
post(payload(message)).tap do |response|
- 6
if text = extract_text_from(response)
- 1
receive_text_reply_to(message.room, text: text)
- 5
elsif attachment = extract_attachment_from(response)
- 1
receive_attachment_reply_to(message.room, attachment: attachment)
end
end
rescue Net::OpenTimeout, Net::ReadTimeout
- 1
receive_text_reply_to message.room, text: "Failed to respond within #{ENDPOINT_TIMEOUT} seconds"
end
- 1
private
- 1
def post(payload)
- 6
http.request \
- 6
Net::HTTP::Post.new(uri, "Content-Type" => "application/json").tap { |request| request.body = payload }
end
- 1
def http
- 6
Net::HTTP.new(uri.host, uri.port).tap do |http|
- 6
http.use_ssl = (uri.scheme == "https")
- 6
http.open_timeout = ENDPOINT_TIMEOUT
- 6
http.read_timeout = ENDPOINT_TIMEOUT
end
end
- 1
def uri
- 24
@uri ||= URI(url)
end
- 1
def payload(message)
{
- 7
user: { id: message.creator.id, name: message.creator.name },
room: { id: message.room.id, name: message.room.name, path: room_bot_messages_path(message) },
message: { id: message.id, body: { html: message.body.body, plain: without_recipient_mentions(message.plain_text_body) }, path: message_path(message) }
}.to_json
end
- 1
def message_path(message)
- 7
Rails.application.routes.url_helpers.room_at_message_path(message.room, message)
end
- 1
def room_bot_messages_path(message)
- 7
Rails.application.routes.url_helpers.room_bot_messages_path(message.room, user.bot_key)
end
- 1
def extract_text_from(response)
- 6
response.body.force_encoding("UTF-8") if response.code == "200" && response.content_type.in?(%w[ text/html text/plain ])
end
- 1
def receive_text_reply_to(room, text:)
- 2
room.messages.create!(body: text, creator: user).broadcast_create
end
- 1
def extract_attachment_from(response)
- 5
if response.content_type && mime_type = Mime::Type.lookup(response.content_type)
- 1
ActiveStorage::Blob.create_and_upload! \
io: StringIO.new(response.body), filename: "attachment.#{mime_type.symbol}", content_type: mime_type.to_s
end
end
- 1
def receive_attachment_reply_to(room, attachment:)
- 1
room.messages.create_with_attachment!(attachment: attachment, creator: user).broadcast_create
end
- 1
def without_recipient_mentions(body)
- 7
body \
.gsub(user.attachable_plain_text_representation(nil), "") # Remove mentions of the recipient user
.gsub(/\A\p{Space}+|\p{Space}+\z/, "") # Remove leading and trailing whitespace uncluding unicode spaces
end
end
# Load the Rails application.
- 1
require_relative "application"
# Initialize the Rails application.
- 1
Rails.application.initialize!
- 1
require "active_support/core_ext/integer/time"
# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!
- 1
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# Turn false under Spring and add config.action_view.cache_template_loading = true.
- 1
config.cache_classes = true
# Eager loading loads your whole application. When running a single test locally,
# this probably isn't necessary. It's a good idea to do in a continuous integration
# system, or in some way before deploying your code.
- 1
config.eager_load = ENV["CI"].present?
# Configure public file server for tests with Cache-Control for performance.
- 1
config.public_file_server.enabled = true
- 1
config.public_file_server.headers = {
"Cache-Control" => "public, max-age=#{1.hour.to_i}"
}
# Show full error reports and disable caching.
- 1
config.consider_all_requests_local = true
- 1
config.action_controller.perform_caching = false
- 1
config.cache_store = :null_store
# Raise exceptions instead of rendering exception templates.
- 1
config.action_dispatch.show_exceptions = :none
# Disable request forgery protection in test environment.
- 1
config.action_controller.allow_forgery_protection = false
# Store uploaded files on the local file system in a temporary directory.
- 1
config.active_storage.service = :test
# Print deprecation notices to the stderr.
- 1
config.active_support.deprecation = :stderr
# Raise exceptions for disallowed deprecations.
- 1
config.active_support.disallowed_deprecation = :raise
# Tell Active Support which deprecation messages to disallow.
- 1
config.active_support.disallowed_deprecation_warnings = []
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
# Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true
# Load test helpers
- 1
config.autoload_paths += %w[ test/test_helpers ]
end
- 1
ActiveSupport.on_load(:active_storage_blob) do
- 1
ActiveStorage::DiskController.after_action only: :show do
response.set_header("Cache-Control", "max-age=3600, public")
end
end
# Be sure to restart your server when you modify this file.
# Version of your assets, change this if you want to expire all your assets.
- 1
Rails.application.config.assets.version = "1.0"
# Be sure to restart your server when you modify this file.
# Define an application-wide content security policy.
# See the Securing Rails Applications Guide for more information:
# https://guides.rubyonrails.org/security.html#content-security-policy-header
# Rails.application.configure do
# config.content_security_policy do |policy|
# policy.default_src :self, :https
# policy.font_src :self, :https, :data
# policy.img_src :self, :https, :data
# policy.object_src :none
# policy.script_src :self, :https
# policy.style_src :self, :https
# # Specify URI for violation reports
# # policy.report_uri "/csp-violation-report-endpoint"
# end
#
# # Generate session nonces for permitted importmap and inline scripts
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
# config.content_security_policy_nonce_directives = %w(script-src)
#
# # Report violations without enforcing the policy.
# # config.content_security_policy_report_only = true
# end
- 1
%w[ rails_ext ].each do |extensions_dir|
- 6
Dir["#{Rails.root}/lib/#{extensions_dir}/*"].each { |path| require "#{extensions_dir}/#{File.basename(path)}" }
end
# Be sure to restart your server when you modify this file.
# Configure parameters to be filtered from the log file. Use this to limit dissemination of
# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
# notations and behaviors.
- 1
Rails.application.config.filter_parameters += [
:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :endpoint, "message.body"
]
# Be sure to restart your server when you modify this file.
# Add new inflection rules using the following format. Inflections
# are locale specific, and you may define rules for as many different
# locales as you wish. All of these examples are active by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.plural /^(ox)$/i, "\\1en"
# inflect.singular /^(ox)en/i, "\\1"
# inflect.irregular "person", "people"
# inflect.uncountable %w( fish sheep )
# end
# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.acronym "RESTful"
# end
# Define an application-wide HTTP permissions policy. For further
# information see https://developers.google.com/web/updates/2018/06/feature-policy
#
# Rails.application.config.permissions_policy do |f|
# f.camera :none
# f.gyroscope :none
# f.microphone :none
# f.usb :none
# f.fullscreen :self
# f.payment :self, "https://secure.example.com"
# end
- 1
if Rails.env.production? && ENV["SKIP_TELEMETRY"].blank?
Sentry.init do |config|
config.dsn = "https://975a8bf631edee43b6a8cf4823998d92@o33603.ingest.sentry.io/4506587182530560"
config.breadcrumbs_logger = [ :active_support_logger, :http_logger ]
config.send_default_pii = false
config.release = ENV["GIT_REVISION"]
end
end
- 1
Rails.application.config.session_store :cookie_store,
key: "_campfire_session",
# Persist session cookie as permament so re-opened browser windows maintain a CSRF token
expire_after: 20.years
- 1
module SQLite3Configuration
- 1
private
- 1
def configure_connection
- 2
super
- 2
if @config[:retries]
- 2
retries = self.class.type_cast_config_to_integer(@config[:retries])
- 2
raw_connection.busy_handler do |count|
(count <= retries).tap { |result| sleep count * 0.001 if result }
end
end
end
end
- 1
module SQLite3DumpConfiguration
- 1
def structure_dump(filename, extra_flags)
args = []
args.concat(Array(extra_flags)) if extra_flags
args << db_config.database
ignore_tables = ActiveRecord::SchemaDumper.ignore_tables
if ignore_tables.any?
ignore_tables = connection.data_sources.select { |table| ignore_tables.any? { |pattern| pattern === table } }
condition = ignore_tables.map { |table| connection.quote(table) }.join(", ")
args << "SELECT sql || ';' FROM sqlite_master WHERE tbl_name NOT IN (#{condition}) ORDER BY tbl_name, type DESC, name"
else
args << ".schema --nosys"
end
run_cmd("sqlite3", args, filename)
end
end
- 1
ActiveSupport.on_load :active_record do
- 1
ActiveRecord::ConnectionAdapters::SQLite3Adapter.prepend SQLite3Configuration
- 1
ActiveRecord::Tasks::SQLiteDatabaseTasks.prepend SQLite3DumpConfiguration
end
- 1
Rails.application.config.after_initialize do
- 1
%w[ db files ].each do |dir|
- 2
Rails.root.join("storage", dir).mkpath
end
end
# Used to match JavaScripts (new Date).getTime() for sorting
- 90
Time::DATE_FORMATS[:epoch] = ->(time) { (time.to_f * 1000).to_i }
- 1
Rails.application.configure do
- 1
config.x.vapid.private_key = ENV.fetch("VAPID_PRIVATE_KEY", Rails.application.credentials.dig(:vapid, :private_key))
- 1
config.x.vapid.public_key = ENV.fetch("VAPID_PUBLIC_KEY", Rails.application.credentials.dig(:vapid, :public_key))
end
- 1
Rails.application.config.app_version = ENV.fetch("APP_VERSION", "0")
- 1
Rails.application.config.git_revision = ENV["GIT_REVISION"]
- 1
require "web-push"
- 1
require "web_push/pool"
- 1
require "web_push/notification"
- 1
Rails.application.configure do
- 1
config.x.web_push_pool = WebPush::Pool.new(
invalid_subscription_handler: ->(subscription_id) do
- 2
Rails.application.executor.wrap do
- 2
Rails.logger.info "Destroying push subscription: #{subscription_id}"
- 2
Push::Subscription.find_by(id: subscription_id)&.destroy
end
end
)
- 1
at_exit { config.x.web_push_pool.shutdown }
end
- 1
module WebPush::PersistentRequest
- 1
def perform
if @options[:connection]
http = @options[:connection]
else
http = Net::HTTP.new(uri.host, uri.port, *proxy_options)
http.use_ssl = true
http.ssl_timeout = @options[:ssl_timeout] unless @options[:ssl_timeout].nil?
http.open_timeout = @options[:open_timeout] unless @options[:open_timeout].nil?
http.read_timeout = @options[:read_timeout] unless @options[:read_timeout].nil?
end
req = Net::HTTP::Post.new(uri.request_uri, headers)
req.body = body
if http.is_a?(Net::HTTP::Persistent)
response = http.request uri, req
else
resp = http.request(req)
verify_response(resp)
end
resp
end
end
- 1
WebPush::Request.prepend WebPush::PersistentRequest
- 1
Rails.application.routes.draw do
- 1
root "welcome#show"
- 1
resource :first_run
- 1
resource :session do
- 1
scope module: "sessions" do
- 1
resources :transfers, only: %i[ show update ]
end
end
- 1
resource :account do
- 1
scope module: "accounts" do
- 1
resources :users
- 1
resources :bots do
- 1
scope module: "bots" do
- 1
resource :key, only: :update
end
end
- 1
resource :join_code, only: :create
- 1
resource :logo, only: %i[ show destroy ]
- 1
resource :custom_styles, only: %i[ edit update ]
end
end
- 1
direct :fresh_account_logo do |options|
- 57
route_for :account_logo, v: Current.account&.updated_at&.to_fs(:number), size: options[:size]
end
- 1
get "join/:join_code", to: "users#new", as: :join
- 1
post "join/:join_code", to: "users#create"
- 1
resources :qr_code, only: :show
- 1
resources :users, only: :show do
- 1
scope module: "users" do
- 1
resource :avatar, only: %i[ show destroy ]
- 1
scope defaults: { user_id: "me" } do
- 1
resource :sidebar, only: :show
- 1
resource :profile
- 1
resources :push_subscriptions do
- 1
scope module: "push_subscriptions" do
- 1
resources :test_notifications, only: :create
end
end
end
end
end
- 1
namespace :autocompletable do
- 1
resources :users, only: :index
end
- 1
direct :fresh_user_avatar do |user, options|
- 95
route_for :user_avatar, user.avatar_token, v: user.updated_at.to_fs(:number)
end
- 1
resources :rooms do
- 1
resources :messages
- 1
post ":bot_key/messages", to: "messages/by_bots#create", as: :bot_messages
- 1
scope module: "rooms" do
- 1
resource :refresh, only: :show
- 1
resource :settings, only: :show
- 1
resource :involvement, only: %i[ show update ]
end
- 1
get "@:message_id", to: "rooms#show", as: :at_message
end
- 1
namespace :rooms do
- 1
resources :opens
- 1
resources :closeds
- 1
resources :directs
end
- 1
resources :messages do
- 1
scope module: "messages" do
- 1
resources :boosts
end
end
- 1
resources :searches, only: %i[ index create ] do
- 1
delete :clear, on: :collection
end
- 1
resource :unfurl_link, only: :create
- 1
get "webmanifest" => "pwa#manifest"
- 1
get "service-worker" => "pwa#service_worker"
- 1
get "up" => "rails/health#show", as: :rails_health_check
end
- 1
ActiveSupport.on_load(:action_text_content) do
- 1
class ActionText::Attachment
- 1
class << self
- 1
def from_node(node, attachable = nil)
- 39
new(node, attachable || ActionText::Attachment::OpengraphEmbed.from_node(node) || attachable_from_possibly_expired_sgid(node["sgid"]) || ActionText::Attachable.from_node(node))
end
- 1
private
# Our @mentions use ActionText attachments, which are signed. If someone rotates SECRET_KEY_BASE, the existing attachments become invalid.
# This allows ignoring invalid signatures for User attachments in ActionText.
- 1
ATTACHABLES_PERMITTED_WITH_INVALID_SIGNATURES = %w[ User ]
- 1
def attachable_from_possibly_expired_sgid(sgid)
- 24
if message = sgid&.split("--")&.first
- 23
encoded_message = JSON.parse Base64.strict_decode64(message)
- 23
decoded_gid = Marshal.load Base64.urlsafe_decode64(encoded_message.dig("_rails", "message"))
- 23
model = GlobalID.find(decoded_gid)
- 23
model.model_name.to_s.in?(ATTACHABLES_PERMITTED_WITH_INVALID_SIGNATURES) ? model : nil
end
rescue ActiveRecord::RecordNotFound
nil
end
end
end
end
- 1
class ActionText::Attachment::OpengraphEmbed
- 1
include ActiveModel::Model
- 1
OPENGRAPH_EMBED_CONTENT_TYPE = "application/vnd.actiontext.opengraph-embed"
- 1
class << self
- 1
def from_node(node)
- 36
if node["content-type"]
- 33
if matches = node["content-type"].match(OPENGRAPH_EMBED_CONTENT_TYPE)
- 12
attachment = new(attributes_from_node(node))
- 12
attachment if attachment.valid?
end
end
end
- 1
private
- 1
def attributes_from_node(node)
{
- 12
href: node["href"],
url: node["url"],
filename: node["filename"],
description: node["caption"]
}
end
end
- 1
attr_accessor :href, :url, :filename, :description
- 1
def attachable_content_type
OPENGRAPH_EMBED_CONTENT_TYPE
end
- 1
def attachable_plain_text_representation(caption)
- 12
""
end
- 1
def to_partial_path
"action_text/attachables/opengraph_embed"
end
- 1
def to_trix_content_attachment_partial_path
"action_text/attachables/opengraph_embed"
end
end
- 1
class ActionText::Content::Filter
- 1
class << self
- 1
def apply(content)
- 125
filter = new(content)
- 125
filter.applicable? ? ActionText::Content.new(filter.apply, canonicalize: false) : content
end
end
- 1
def initialize(content)
- 125
@content = content
end
- 1
def applicable?
raise NotImplementedError
end
- 1
def apply
raise NotImplementedError
end
- 1
private
- 1
attr_reader :content
- 1
delegate :fragment, to: :content
end
- 1
class ActionText::Content::Filters
- 1
def initialize(*filters)
- 1
@filters = filters
end
- 1
def apply(content)
- 164
filters.reduce(content) { |content, filter| filter.apply(content) }
end
- 1
private
- 1
attr_reader :filters
end
- 1
class String
- 1
def all_emoji?
- 44
self.match? /\A(\p{Emoji_Presentation}|\p{Extended_Pictographic}|\uFE0F)+\z/u
end
end
- 1
require "resolv"
- 1
module RestrictedHTTP
- 1
class Violation < StandardError; end
- 1
module PrivateNetworkGuard
- 1
extend self
- 1
LOCAL_IP = IPAddr.new("0.0.0.0/8") # "This" network
- 1
def resolve(hostname)
- 85
Resolv.getaddress(hostname).tap do |ip|
- 77
raise Violation.new("Attempt to access private IP via #{hostname}") if ip && private_ip?(ip)
end
end
- 1
def private_ip?(ip)
- 77
IPAddr.new(ip).then do |ipaddr|
- 77
ipaddr.private? || ipaddr.loopback? || LOCAL_IP.include?(ipaddr)
end
rescue IPAddr::InvalidAddressError
true
end
end
end
- 1
class WebPush::Notification
- 1
def initialize(title:, body:, path:, badge:, endpoint:, p256dh_key:, auth_key:)
- 14
@title, @body, @path, @badge = title, body, path, badge
- 14
@endpoint, @p256dh_key, @auth_key = endpoint, p256dh_key, auth_key
end
- 1
def deliver(connection: nil)
- 14
WebPush.payload_send \
message: encoded_message,
endpoint: @endpoint, p256dh: @p256dh_key, auth: @auth_key,
vapid: vapid_identification,
connection: connection,
urgency: "high"
end
- 1
private
- 1
def vapid_identification
- 14
{ subject: "mailto:support@37signals.com" }.merge \
Rails.configuration.x.vapid.symbolize_keys
end
- 1
def encoded_message
- 14
JSON.generate title: @title, options: { body: @body, icon: icon_path, data: { path: @path, badge: @badge } }
end
- 1
def icon_path
- 14
Rails.application.routes.url_helpers.account_logo_path
end
end
# This is in lib so we can use it in a thread pool without the Rails executor
- 1
class WebPush::Pool
- 1
attr_reader :delivery_pool, :invalidation_pool, :connection, :invalid_subscription_handler
- 1
def initialize(invalid_subscription_handler:)
- 224
@delivery_pool = Concurrent::ThreadPoolExecutor.new(max_threads: 50, queue_size: 10000)
- 224
@invalidation_pool = Concurrent::FixedThreadPool.new(1)
- 224
@connection = Net::HTTP::Persistent.new(name: "web_push", pool_size: 150)
- 224
@invalid_subscription_handler = invalid_subscription_handler
end
- 1
def queue(payload, subscriptions)
- 12
subscriptions.find_each do |subscription|
- 14
deliver_later(payload, subscription)
end
end
- 1
def shutdown
- 223
connection.shutdown
- 223
shutdown_pool(delivery_pool)
- 223
shutdown_pool(invalidation_pool)
end
- 1
private
- 1
def deliver_later(payload, subscription)
# Ensure any AR operations happen before we post to the thread pool
- 14
notification = subscription.notification(**payload)
- 14
subscription_id = subscription.id
- 14
delivery_pool.post do
- 14
deliver(notification, subscription_id)
rescue Exception => e
Rails.logger.error "Error in WebPush::Pool.deliver: #{e.class} #{e.message}"
end
rescue Concurrent::RejectedExecutionError
end
- 1
def deliver(notification, id)
- 14
notification.deliver(connection: connection)
rescue WebPush::ExpiredSubscription, OpenSSL::OpenSSLError => ex
- 2
invalidate_subscription_later(id) if invalid_subscription_handler
end
- 1
def invalidate_subscription_later(id)
- 2
invalidation_pool.post do
- 2
invalid_subscription_handler.call(id)
rescue Exception => e
Rails.logger.error "Error in WebPush::Pool.invalid_subscription_handler: #{e.class} #{e.message}"
end
end
- 1
def shutdown_pool(pool)
- 446
pool.shutdown
- 446
pool.kill unless pool.wait_for_termination(1)
end
end
- 1
require "test_helper"
- 1
class PresenceChannelTest < ActionCable::Channel::TestCase
- 1
setup do
- 6
stub_connection(current_user: users(:david))
end
- 1
test "subscribes" do
- 1
room = users(:david).rooms.first
- 1
subscribe room_id: room.id
- 1
assert subscription.confirmed?
- 1
assert_has_stream_for room
end
- 1
test "rejects subscription to a room that the user is not a member of" do
- 1
subscribe room_id: Rooms::Closed.create!(name: "New Room", creator: users(:david)).id
- 1
assert subscription.rejected?
end
- 1
test "rejects subscription to non-existent room" do
- 1
subscribe room_id: -1
- 1
assert subscription.rejected?
end
- 1
test "rejects subscription without a room" do
- 1
subscribe room_id: -1
- 1
assert subscription.rejected?
end
- 1
test "subscribing marks the membership as connected" do
- 1
membership = users(:david).memberships.first
- 3
assert_changes -> { membership.reload.connected? }, from: false, to: true do
- 1
subscribe room_id: membership.room_id
end
end
- 1
test "unsubscribing marks the membership as disconnected" do
- 1
membership = users(:david).memberships.first
- 1
subscribe room_id: membership.room_id
- 3
assert_changes -> { membership.reload.connected? }, from: true, to: false do
- 1
unsubscribe
end
end
end
- 1
require "test_helper"
- 1
class Accounts::Bots::KeysControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 1
sign_in :david
end
- 1
test "update" do
- 3
assert_changes -> { users(:bender).reload.bot_token } do
- 1
put account_bot_key_url(users(:bender))
- 1
assert_redirected_to account_bots_url
end
end
end
- 1
require "test_helper"
- 1
class Accounts::BotsControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 5
sign_in :david
end
- 1
test "index" do
- 1
get account_bots_url
- 1
assert_response :ok
end
- 1
test "create" do
- 1
get new_account_bot_url
- 1
assert_response :ok
- 1
post account_bots_url, params: { user: { name: "Bender's Friend" } }
- 1
assert_redirected_to account_bots_url
- 1
assert_equal "Bender's Friend", User.bot.last.name
end
- 1
test "update" do
- 1
get edit_account_bot_url(users(:bender))
- 1
assert_response :ok
- 1
put account_bot_url(users(:bender)), params: { user: { name: "Bender's New Friend" } }
- 1
assert_redirected_to account_bots_url
- 1
assert_equal "Bender's New Friend", users(:bender).reload.name
end
- 1
test "destroy" do
- 3
assert_difference -> { User.active_bots.count }, -1 do
- 1
delete account_bot_url(users(:bender))
end
- 1
assert users(:bender).reload.deactivated?
end
- 1
test "remove webhook" do
- 3
assert_difference -> { Webhook.count }, -1 do
- 1
put account_bot_url(users(:bender)), params: { user: { name: "Bender's New Friend", webook_url: "" } }
- 1
assert_redirected_to account_bots_url
end
end
end
- 1
require "test_helper"
- 1
class Accounts::CustomStylesControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 3
sign_in :david
end
- 1
test "edit" do
- 1
get edit_account_custom_styles_url
- 1
assert_response :ok
end
- 1
test "update" do
- 1
assert users(:david).administrator?
- 1
put account_custom_styles_url, params: { account: { custom_styles: ":root { --color-text: red; }" } }
- 1
assert_redirected_to edit_account_custom_styles_url
- 1
assert_equal accounts(:signal).custom_styles, ":root { --color-text: red; }"
end
- 1
test "non-admins cannot update" do
- 1
sign_in :kevin
- 1
assert users(:kevin).member?
- 1
put account_custom_styles_url, params: { account: { custom_styles: ":root { --color-text: red; }" } }
- 1
assert_response :forbidden
end
end
- 1
require "test_helper"
- 1
class Accounts::JoinCodesControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 2
sign_in :david
end
- 1
test "create new join code" do
- 3
assert_changes -> { accounts(:signal).reload.join_code } do
- 1
post account_join_code_url
- 1
assert_redirected_to edit_account_url
end
end
- 1
test "only administrators can create new join codes" do
- 1
sign_in :jz
- 1
post account_join_code_url
- 1
assert_response :forbidden
end
end
- 1
require "test_helper"
- 1
require "vips"
- 1
class Accounts::LogosControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 5
sign_in :david
end
- 1
test "show stock" do
- 1
get account_logo_url
- 1
assert_valid_png_response size: 512
end
- 1
test "show stock small size" do
- 1
get account_logo_url(size: :small)
- 1
assert_valid_png_response size: 192
end
- 1
test "show custom" do
- 1
accounts(:signal).update! logo: fixture_file_upload("moon.jpg", "image/jpeg")
- 1
get account_logo_url
- 1
assert_valid_png_response size: 512
end
- 1
test "show custom small size" do
- 1
accounts(:signal).update! logo: fixture_file_upload("moon.jpg", "image/jpeg")
- 1
get account_logo_url(size: :small)
- 1
assert_valid_png_response size: 192
end
- 1
test "destroy" do
- 1
accounts(:signal).update! logo: fixture_file_upload("moon.jpg", "image/jpeg")
- 1
delete account_logo_url
- 1
assert_redirected_to edit_account_url
- 1
assert_not accounts(:signal).reload.logo.attached?
end
- 1
private
- 1
def assert_valid_png_response(size:)
- 4
assert_equal @response.headers["content-type"], "image/png"
- 4
image = ::Vips::Image.new_from_buffer(@response.body, "")
- 4
assert_equal size, image.width
- 4
assert_equal size, image.height
end
end
- 1
require "test_helper"
- 1
class Accounts::UsersControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 3
sign_in :david
end
- 1
test "update" do
- 1
assert users(:david).administrator?
- 1
put account_user_url(users(:david)), params: { user: { role: "administrator" } }
- 1
assert_redirected_to edit_account_url
- 1
assert users(:david).reload.administrator?
end
- 1
test "destroy" do
- 3
assert_difference -> { User.active.count }, -1 do
- 1
delete account_user_url(users(:david))
end
- 1
assert_redirected_to edit_account_url
- 1
assert_nil User.active.find_by(id: users(:david).id)
end
- 1
test "non-admins cannot perform actions" do
- 1
sign_in :kevin
- 1
put account_user_url(users(:david)), params: { user: { role: "administrator" } }
- 1
assert_response :forbidden
- 1
delete account_user_url(users(:david))
- 1
assert_response :forbidden
end
end
- 1
require "test_helper"
- 1
class AccountsControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 3
sign_in :david
end
- 1
test "edit" do
- 1
get edit_account_url
- 1
assert_response :ok
end
- 1
test "update" do
- 1
assert users(:david).administrator?
- 1
put account_url, params: { account: { name: "Different" } }
- 1
assert_redirected_to edit_account_url
- 1
assert_equal accounts(:signal).name, "Different"
end
- 1
test "non-admins cannot update" do
- 1
sign_in :kevin
- 1
assert users(:kevin).member?
- 1
put account_url, params: { account: { name: "Different" } }
- 1
assert_response :forbidden
end
end
- 1
require "test_helper"
- 1
class Autocompletable::UsersControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 4
sign_in :david
end
- 1
test "search returns matching users" do
- 1
get autocompletable_users_url(format: :json), params: { query: "da" }
- 1
assert_response :success
- 1
assert_equal "David", response.parsed_body.first["name"]
end
- 1
test "search results escape HTML in names" do
- 1
users(:david).update!(name: "David <script>alert(123)</script>")
- 1
get autocompletable_users_url(format: :json), params: { query: "da" }
- 1
assert_response :success
- 1
assert_equal "David <script>alert(123)</script>", response.parsed_body.first["name"]
end
- 1
test "room search returns matching users" do
- 1
get autocompletable_users_url(room_id: rooms(:hq).id, format: :json), params: { query: "da" }
- 1
assert_response :success
- 1
assert_equal "David", response.parsed_body.first["name"]
end
- 1
test "room search is scoped by membership" do
- 1
sign_in :kevin
- 1
assert_not_includes users(:kevin).rooms, rooms(:watercooler)
- 1
assert_raises ActiveRecord::RecordNotFound do
- 1
get autocompletable_users_url(room_id: rooms(:watercooler).id, format: :json), params: { query: "da" }
end
end
end
- 1
require "test_helper"
- 1
class FirstRunsControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 3
Account.destroy_all
- 3
User.destroy_all
- 3
Room.destroy_all
end
- 1
test "new is permitted when no other users exit" do
- 1
get first_run_url
- 1
assert_response :success
end
- 1
test "new is not permitted when account exist" do
- 1
Account.create!(name: "Chat")
- 1
get first_run_url
- 1
assert_redirected_to root_url
end
- 1
test "create" do
- 3
assert_difference -> { Room.count }, 1 do
- 3
assert_difference -> { User.count }, 1 do
- 1
post first_run_url, params: { account: { name: "37signals" }, user: { name: "New Person", email_address: "new@37signals.com", password: "secret123456" } }
end
end
- 1
assert_redirected_to root_url
- 1
assert parsed_cookies.signed[:session_token]
end
end
- 1
require "test_helper"
- 1
class Messages::BoostsControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 2
sign_in :david
- 2
@message = messages(:first)
end
- 1
test "create" do
- 1
assert_turbo_stream_broadcasts [ @message.room, :messages ], count: 1 do
- 3
assert_difference -> { @message.boosts.count }, 1 do
- 1
post message_boosts_url(@message, format: :turbo_stream), params: { boost: { content: "Morning!" } }
- 1
assert_redirected_to message_boosts_url(@message)
end
end
end
- 1
test "destroy" do
- 1
assert_turbo_stream_broadcasts [ @message.room, :messages ], count: 1 do
- 3
assert_difference -> { @message.boosts.count }, -1 do
- 1
delete message_boost_url(@message, boosts(:first), format: :turbo_stream)
- 1
assert_response :success
end
end
end
end
- 1
require "test_helper"
- 1
class Messages::ByBotsControlleTest < ActionDispatch::IntegrationTest
- 1
setup do
- 6
@room = rooms(:watercooler)
end
- 1
test "create" do
- 3
assert_difference -> { Message.count }, +1 do
- 1
post room_bot_messages_url(@room, users(:bender).bot_key), params: "Hello Bot World!"
- 1
assert_equal "Hello Bot World!", Message.last.plain_text_body
end
end
- 1
test "create with UTF-8 content" do
- 3
assert_difference -> { Message.count }, +1 do
- 1
post room_bot_messages_url(@room, users(:bender).bot_key), params: "Hello 👋!"
- 1
assert_equal "Hello 👋!", Message.last.plain_text_body
end
end
- 1
test "create file" do
- 3
assert_difference -> { Message.count }, +1 do
- 1
post room_bot_messages_url(@room, users(:bender).bot_key), params: { attachment: fixture_file_upload("moon.jpg", "image/jpeg") }
- 1
assert Message.last.attachment.present?
end
end
- 1
test "create does not trigger a webhook to the sending bot if it mentions itself" do
- 1
body = "<div>Hey #{mention_attachment_for(:bender)}</div>"
- 1
assert_no_enqueued_jobs only: Bot::WebhookJob do
- 1
post room_bot_messages_url(@room, users(:bender).bot_key), params: body
end
end
- 1
test "create does not trigger a webhook to the sending bot in a direct room" do
- 1
assert_no_enqueued_jobs only: Bot::WebhookJob do
- 1
post room_bot_messages_url(rooms(:bender_and_kevin), users(:bender).bot_key), params: "Talking to myself again!"
end
end
- 1
test "denied index" do
- 1
get room_messages_url(@room, bot_key: users(:bender).bot_key, format: :json)
- 1
assert_response :forbidden
end
end
- 1
require "test_helper"
- 1
class MessagesControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 14
host! "once.campfire.test"
- 14
sign_in :david
- 14
@room = rooms(:watercooler)
- 14
@messages = @room.messages.ordered.to_a
end
- 1
test "index returns the last page by default" do
- 1
get room_messages_url(@room)
- 1
assert_response :success
- 1
ensure_messages_present @messages.last
end
- 1
test "index returns a page before the specified message" do
- 1
get room_messages_url(@room, before: @messages.third)
- 1
assert_response :success
- 1
ensure_messages_present @messages.first, @messages.second
- 1
ensure_messages_not_present @messages.third, @messages.fourth, @messages.fifth
end
- 1
test "index returns a page after the specified message" do
- 1
get room_messages_url(@room, after: @messages.third)
- 1
assert_response :success
- 1
ensure_messages_present @messages.fourth, @messages.fifth
- 1
ensure_messages_not_present @messages.first, @messages.second, @messages.third
end
- 1
test "index returns no_content when there are no messages" do
- 1
@room.messages.destroy_all
- 1
get room_messages_url(@room)
- 1
assert_response :no_content
end
- 1
test "get renders a single message belonging to the user" do
- 1
message = @room.messages.where(creator: users(:david)).first
- 1
get room_message_url(@room, message)
- 1
assert_response :success
end
- 1
test "creating a message broadcasts the message to the room" do
- 1
post room_messages_url(@room, format: :turbo_stream), params: { message: { body: "New one", client_message_id: 999 } }
- 1
assert_rendered_turbo_stream_broadcast @room, :messages, action: "append", target: [ @room, :messages ] do
- 1
assert_select ".message__body", text: /New one/
- 1
assert_copy_link_button room_at_message_url(@room, Message.last, host: "once.campfire.test")
end
end
- 1
test "creating a message broadcasts unread room" do
- 1
assert_broadcasts "unread_rooms", 1 do
- 1
post room_messages_url(@room, format: :turbo_stream), params: { message: { body: "New one", client_message_id: 999 } }
end
end
- 1
test "update updates a message belonging to the user" do
- 1
message = @room.messages.where(creator: users(:david)).first
- 1
Turbo::StreamsChannel.expects(:broadcast_replace_to).once
- 1
put room_message_url(@room, message), params: { message: { body: "Updated body" } }
- 1
assert_redirected_to room_message_url(@room, message)
- 1
assert_equal "Updated body", message.reload.plain_text_body
end
- 1
test "admin updates a message belonging to another user" do
- 1
message = @room.messages.where(creator: users(:jason)).first
- 1
Turbo::StreamsChannel.expects(:broadcast_replace_to).once
- 1
put room_message_url(@room, message), params: { message: { body: "Updated body" } }
- 1
assert_redirected_to room_message_url(@room, message)
- 1
assert_equal "Updated body", message.reload.plain_text_body
end
- 1
test "destroy destroys a message belonging to the user" do
- 1
message = @room.messages.where(creator: users(:david)).first
- 3
assert_difference -> { Message.count }, -1 do
- 1
Turbo::StreamsChannel.expects(:broadcast_remove_to).once
- 1
delete room_message_url(@room, message, format: :turbo_stream)
- 1
assert_response :success
end
end
- 1
test "admin destroy destroys a message belonging to another user" do
- 1
assert users(:david).administrator?
- 1
message = @room.messages.where(creator: users(:jason)).first
- 3
assert_difference -> { Message.count }, -1 do
- 1
Turbo::StreamsChannel.expects(:broadcast_remove_to).once
- 1
delete room_message_url(@room, message, format: :turbo_stream)
- 1
assert_response :success
end
end
- 1
test "ensure non-admin can't update a message belonging to another user" do
- 1
sign_in :jz
- 1
assert_not users(:jz).administrator?
- 1
room = rooms(:designers)
- 1
message = room.messages.where(creator: users(:jason)).first
- 1
put room_message_url(room, message), params: { message: { body: "Updated body" } }
- 1
assert_response :forbidden
end
- 1
test "ensure non-admin can't destroy a message belonging to another user" do
- 1
sign_in :jz
- 1
assert_not users(:jz).administrator?
- 1
room = rooms(:designers)
- 1
message = room.messages.where(creator: users(:jason)).first
- 1
delete room_message_url(room, message, format: :turbo_stream)
- 1
assert_response :forbidden
end
- 1
test "mentioning a bot triggers a webhook" do
- 1
WebMock.stub_request(:post, webhooks(:bender).url).to_return(status: 200)
- 1
assert_enqueued_jobs 1, only: Bot::WebhookJob do
- 1
post room_messages_url(@room, format: :turbo_stream), params: { message: {
body: "<div>Hey #{mention_attachment_for(:bender)}</div>", client_message_id: 999 } }
end
end
- 1
private
- 1
def ensure_messages_present(*messages, count: 1)
- 5
messages.each do |message|
- 11
assert_select "#" + dom_id(message), count:
end
end
- 1
def ensure_messages_not_present(*messages)
- 2
ensure_messages_present *messages, count: 0
end
- 1
def assert_copy_link_button(url)
- 1
assert_select ".btn[title='Copy link'][data-copy-to-clipboard-content-value='#{url}']"
end
end
- 1
require "test_helper"
- 1
class QrCodeControllerTest < ActionDispatch::IntegrationTest
- 1
test "show renders a QR code as a cacheable SVG image" do
- 1
id = Base64.urlsafe_encode64("http://example.com")
- 1
get qr_code_path(id)
- 1
assert_response :success
- 1
assert_includes response.content_type, "image/svg+xml"
- 1
assert_equal 1.year, response.cache_control[:max_age].to_i
- 1
assert response.cache_control[:public]
end
end
- 1
require "test_helper"
- 1
class Rooms::ClosedsControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 7
sign_in :david
end
- 1
test "show redirects to get general show" do
- 1
get rooms_open_url(users(:david).rooms.closeds.last)
- 1
assert_redirected_to room_url(users(:david).rooms.closeds.last)
end
- 1
test "new" do
- 1
get new_rooms_closed_url
- 1
assert_response :success
end
- 1
test "create" do
- 1
assert_turbo_stream_broadcasts [ users(:david), :rooms ], count: 1 do
- 1
assert_turbo_stream_broadcasts [ users(:kevin), :rooms ], count: 1 do
- 1
assert_turbo_stream_broadcasts [ users(:jason), :rooms ], count: 1 do
- 1
post rooms_closeds_url, params: { room: { name: "My New Room" }, user_ids: [ users(:david).id, users(:kevin).id, users(:jason).id ] }
end
end
end
- 1
new_room = Room.last
- 1
assert_equal new_room.memberships.count, 3
- 1
assert_redirected_to room_url(Room.last)
end
- 1
test "update with membership revisions" do
- 3
assert_difference -> { rooms(:designers).reload.users.count }, -1 do
- 1
put rooms_closed_url(rooms(:designers)), params: {
room: { name: "New Name" }, user_ids: rooms(:designers).users.without(users(:jason)).collect(&:id)
}
end
- 1
assert_redirected_to room_url(rooms(:designers))
- 1
assert rooms(:designers).reload.name, "New Name"
end
- 1
test "update an open room to be closed" do
- 1
put rooms_closed_url(rooms(:pets)), params: { room: { name: "Doesn't matter" }, user_ids: [ users(:david).id, users(:jason).id ] }
- 1
assert_equal rooms(:pets).memberships.count, 2
end
- 1
test "only admins or creators can update" do
- 1
sign_in :jz
- 1
assert_turbo_stream_broadcasts :rooms, count: 0 do
- 1
put rooms_closed_url(rooms(:designers)), params: { room: { name: "New Name" } }
end
- 1
assert_response :forbidden
- 1
assert rooms(:designers).reload.name, "Designers"
end
- 1
test "remove yourself" do
- 3
assert_difference -> { users(:david).rooms.count }, -1 do
- 1
put rooms_closed_url(rooms(:designers), params: { room: { name: "Designers" }, user_ids: [ users(:jason).id, users(:jz).id ] })
- 1
assert_redirected_to room_url(rooms(:designers))
- 1
follow_redirect!
- 1
assert_redirected_to root_url
end
end
end
- 1
require "test_helper"
- 1
class Rooms::DirectsControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 3
sign_in :david
end
- 1
test "create" do
- 1
post rooms_directs_url, params: { user_ids: [ users(:jz).id ] }
- 1
room = Room.last
- 1
assert_redirected_to room_url(room)
- 1
assert room.users.include?(users(:david))
- 1
assert room.users.include?(users(:jz))
end
- 1
test "create only once per user set" do
- 3
assert_difference -> { Room.all.count }, +1 do
- 1
post rooms_directs_url, params: { user_ids: [ users(:jz).id ] }
- 1
post rooms_directs_url, params: { user_ids: [ users(:jz).id ] }
end
end
- 1
test "destroy only allowed for all room users" do
- 1
sign_in :kevin
- 3
assert_difference -> { Room.count }, -1 do
- 1
delete rooms_direct_url(rooms(:david_and_kevin))
- 1
assert_redirected_to root_url
end
end
end
- 1
require "test_helper"
- 1
class Rooms::InvolvementsControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 4
sign_in :david
end
- 1
test "show" do
- 1
get room_involvement_url(rooms(:designers))
- 1
assert_response :success
end
- 1
test "update involvement sends turbo update when becoming visible and when going invisible" do
- 1
assert_turbo_stream_broadcasts [ users(:david), :rooms ], count: 1 do
- 3
assert_changes -> { memberships(:david_watercooler).reload.involvement }, from: "everything", to: "invisible" do
- 1
put room_involvement_url(rooms(:watercooler)), params: { involvement: "invisible" }
- 1
assert_redirected_to room_involvement_url(rooms(:watercooler))
end
end
- 1
assert_turbo_stream_broadcasts [ users(:david), :rooms ], count: 2 do
- 3
assert_changes -> { memberships(:david_watercooler).reload.involvement }, from: "invisible", to: "everything" do
- 1
put room_involvement_url(rooms(:watercooler)), params: { involvement: "everything" }
- 1
assert_redirected_to room_involvement_url(rooms(:watercooler))
end
end
end
- 1
test "updating involvement does not send turbo update changing visible states" do
- 1
assert_no_turbo_stream_broadcasts [ users(:david), :rooms ] do
- 3
assert_changes -> { memberships(:david_watercooler).reload.involvement }, from: "everything", to: "mentions" do
- 1
put room_involvement_url(rooms(:watercooler)), params: { involvement: "mentions" }
- 1
assert_redirected_to room_involvement_url(rooms(:watercooler))
end
end
end
- 1
test "updating involvement does not send turbo update for direct rooms" do
- 1
assert_no_turbo_stream_broadcasts [ users(:david), :rooms ] do
- 3
assert_changes -> { memberships(:david_david_and_jason).reload.involvement }, from: "everything", to: "nothing" do
- 1
put room_involvement_url(rooms(:david_and_jason)), params: { involvement: "nothing" }
- 1
assert_redirected_to room_involvement_url(rooms(:david_and_jason))
end
end
end
end
- 1
require "test_helper"
- 1
class Rooms::OpensControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 6
sign_in :david
end
- 1
test "show redirects to get general show" do
- 1
get rooms_open_url(users(:david).rooms.opens.last)
- 1
assert_redirected_to room_url(users(:david).rooms.opens.last)
end
- 1
test "new" do
- 1
get new_rooms_open_url
- 1
assert_response :success
end
- 1
test "create" do
- 1
assert_turbo_stream_broadcasts :rooms, count: 1 do
- 1
post rooms_opens_url, params: { room: { name: "My New Room" } }
end
- 1
assert_equal Room.last.memberships.count, User.count
- 1
assert_redirected_to room_url(Room.last)
end
- 1
test "only admins or creators can update" do
- 1
sign_in :jz
- 1
assert_turbo_stream_broadcasts :rooms, count: 0 do
- 1
put rooms_open_url(rooms(:hq)), params: { room: { name: "New Name" } }
end
- 1
assert_response :forbidden
- 1
assert rooms(:hq).reload.name, "HQ"
end
- 1
test "update" do
- 1
assert_turbo_stream_broadcasts :rooms, count: 1 do
- 1
put rooms_open_url(rooms(:pets)), params: { room: { name: "New Name" } }
end
- 1
assert_redirected_to room_url(rooms(:pets))
- 1
assert rooms(:pets).reload.name, "New Name"
end
- 1
test "update a closed room to be open" do
- 1
put rooms_open_url(rooms(:designers)), params: { room: { name: "Doesn't matter" } }
- 1
assert_equal rooms(:designers).memberships.count, User.count
end
end
- 1
require "test_helper"
- 1
class Rooms::RefreshesControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 1
sign_in :david
end
- 1
test "refresh includes new messages since the last known" do
- 1
travel_to 1.day.ago do
- 1
@old_message = rooms(:hq).messages.create!(creator: users(:jason), body: "Old message", client_message_id: "old")
end
- 1
travel_to 1.minute.ago do
- 1
@new_message = rooms(:hq).messages.create!(creator: users(:jason), body: "New message", client_message_id: "new")
- 1
@old_message.touch
end
- 1
get room_refresh_url(rooms(:hq), format: :turbo_stream), params: { since: 10.minutes.ago.to_fs(:epoch) }
- 1
assert_response :success
- 1
assert_select "turbo-stream[action='append']" do
- 1
assert_select "#" + dom_id(@new_message)
- 1
assert_select "template", count: 1
end
- 1
assert_select "turbo-stream[action='replace']" do
- 1
assert_select "#" + dom_id(@old_message)
- 1
assert_select "template", count: 1
end
end
end
- 1
require "test_helper"
- 1
class RoomsControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 5
sign_in :david
end
- 1
test "index redirects to the user's last room" do
- 1
get rooms_url
- 1
assert_redirected_to room_url(users(:david).rooms.last)
end
- 1
test "show" do
- 1
get room_url(users(:david).rooms.last)
- 1
assert_response :success
end
- 1
test "shows records the last room visited in a cookie" do
- 1
get room_url(users(:david).rooms.last)
- 1
assert response.cookies[:last_room] = users(:david).rooms.last.id
end
- 1
test "destroy" do
- 1
assert_turbo_stream_broadcasts :rooms, count: 1 do
- 3
assert_difference -> { Room.count }, -1 do
- 1
delete room_url(rooms(:designers))
end
end
end
- 1
test "destroy only allowed for creators or those who can administer" do
- 1
sign_in :jz
- 3
assert_no_difference -> { Room.count } do
- 1
delete room_url(rooms(:designers))
- 1
assert_response :forbidden
end
- 1
rooms(:designers).update! creator: users(:jz)
- 3
assert_difference -> { Room.count }, -1 do
- 1
delete room_url(rooms(:designers))
end
end
end
- 1
require "test_helper"
- 1
class SearchesControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 5
sign_in :david
- 5
@message = rooms(:designers).messages.create! body: "Hello world!", client_message_id: "search", creator: users(:david)
end
- 1
test "index initial view" do
- 1
get searches_url
- 1
assert_response :success
- 1
assert_select ".message", count: 0
end
- 1
test "finding reachable messages" do
- 1
get searches_url, params: { q: "hello" }
- 1
assert_response :success
- 1
assert_select ".message", text: /Hello world!/
end
- 1
test "unreachable messages are not found" do
- 1
memberships(:david_designers).destroy!
- 1
get searches_url, params: { q: "hello" }
- 1
assert_response :success
- 1
assert_select ".message", count: 0
end
- 1
test "create saves the search term" do
- 3
assert_difference -> { users(:david).searches.count }, +1 do
- 1
post searches_url, params: { q: "hello" }
end
- 1
assert_redirected_to searches_url(q: "hello")
- 1
assert users(:david).searches.exists?(query: "hello")
end
- 1
test "clear search history" do
- 1
assert users(:david).searches.any?
- 1
delete clear_searches_url
- 1
assert users(:david).searches.none?
end
end
- 1
require "test_helper"
- 1
class Sessions::TransfersControllerTest < ActionDispatch::IntegrationTest
- 1
test "show renders when not signed in" do
- 1
get session_transfer_url("some-token")
- 1
assert_response :success
end
- 1
test "update establishes a session when the code is valid" do
- 1
user = users(:david)
- 1
put session_transfer_url(user.transfer_id)
- 1
assert_redirected_to root_url
- 1
assert parsed_cookies.signed[:session_token]
end
end
- 1
require "test_helper"
- 1
class SessionsControllerTest < ActionDispatch::IntegrationTest
- 1
ALLOWED_BROWSER = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15"
- 1
DISALLOWED_BROWSER = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0"
- 1
test "new" do
- 1
get new_session_url
- 1
assert_response :success
end
- 1
test "new redirects to first run when no users exist" do
- 1
User.destroy_all
- 1
get new_session_url
- 1
assert_redirected_to first_run_url
end
- 1
test "new denied with incompatible browser" do
- 1
get new_session_url, env: { "HTTP_USER_AGENT" => DISALLOWED_BROWSER }
- 1
assert_select "h1", /Upgrade to a supported web browser/
end
- 1
test "new allowed with compatible browser" do
- 1
get new_session_url, env: { "HTTP_USER_AGENT" => ALLOWED_BROWSER }
- 1
assert_select "h1", text: /Upgrade to a supported web browser/, count: 0
end
- 1
test "create with valid credentials" do
- 1
post session_url, params: { email_address: "david@37signals.com", password: "secret123456" }
- 1
assert_redirected_to root_url
- 1
assert parsed_cookies.signed[:session_token]
end
- 1
test "create with invalid credentials" do
- 1
post session_url, params: { email_address: "david@37signals.com", password: "wrong" }
- 1
assert_response :unauthorized
- 1
assert_nil parsed_cookies.signed[:session_token]
end
- 1
test "destroy" do
- 1
sign_in :david
- 1
delete session_url
- 1
assert_redirected_to root_url
- 1
assert_not cookies[:session_token].present?
end
- 1
test "destroy removes the push subscription for the device" do
- 1
sign_in :david
- 3
assert_difference -> { users(:david).push_subscriptions.count }, -1 do
- 1
delete session_url, params: { push_subscription_endpoint: push_subscriptions(:david_chrome).endpoint }
end
- 1
assert_redirected_to root_url
- 1
assert_not cookies[:session_token].present?
end
end
- 1
require "test_helper"
- 1
class UnfurlLinksControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 5
sign_in :david
end
- 1
test "create" do
- 1
stub_successful_request
- 1
post unfurl_link_url, params: { url: "https://www.example.com" }
- 1
assert_response :success
- 1
json_response = JSON.parse(response.body)
- 1
assert_equal "Hey!", json_response["title"]
- 1
assert_equal "https://example.com", json_response["url"]
- 1
assert_equal "https://example.com/image.png", json_response["image"]
- 1
assert_equal "desc..", json_response["description"]
end
- 1
test "create with missing opengraph meta tags" do
- 1
WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: "<html><head></head></html>", headers: {})
- 1
post unfurl_link_url, params: { url: "https://www.example.com" }
- 1
assert_response :no_content
end
- 1
test "create with a missing URL" do
- 1
assert_raise ActionController::ParameterMissing do
- 1
post unfurl_link_url, params: { url: "" }
assert_response :bad_request
end
end
- 1
test "create for twitter.com" do
- 1
stub_successful_request url: "https://fxtwitter.com/dhh/status/834146806594433025"
- 1
post unfurl_link_url, params: { url: "https://twitter.com/dhh/status/834146806594433025" }
- 1
assert_response :success
- 1
assert_equal "Hey!", JSON.parse(response.body)["title"]
end
- 1
test "create for x.com" do
- 1
stub_successful_request url: "https://fxtwitter.com/dhh/status/834146806594433025"
- 1
post unfurl_link_url, params: { url: "https://x.com/dhh/status/834146806594433025" }
- 1
assert_response :success
- 1
assert_equal "Hey!", JSON.parse(response.body)["title"]
end
- 1
private
- 1
def stub_successful_request(url: "https://www.example.com/")
- 3
WebMock.stub_request(:get, url).to_return(
status: 200,
body: "<html><head><meta property=\"og:url\" content=\"https://example.com\"><meta property=\"og:title\" content=\"Hey!\"><meta property=\"og:description\" content=\"desc..\"><meta property=\"og:image\" content=\"https://example.com/image.png\"></head></html>",
headers: { content_type: "text/html" }
)
- 3
WebMock.stub_request(:head, "https://example.com/image.png").to_return(
status: 200,
headers: { content_type: "image/png" }
)
end
end
- 1
require "test_helper"
- 1
class Users::AvatarsControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 3
sign_in :david
end
- 1
test "show initials" do
- 1
get user_avatar_url(users(:kevin).avatar_token)
- 1
assert_select "text", text: "K"
end
- 1
test "show image" do
- 1
users(:kevin).update! avatar: fixture_file_upload("moon.jpg", "image/jpeg")
- 1
get user_avatar_url(users(:kevin).avatar_token)
- 1
assert_response :success
- 1
assert_equal "image/webp", @response.content_type
end
- 1
test "show image with invalid token responds 404" do
- 1
get user_avatar_url("not-a-valid-token")
- 1
assert_response :not_found
end
end
- 1
require "test_helper"
- 1
class Users::ProfilesControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 3
sign_in :david
end
- 1
test "show" do
- 1
get user_profile_url
- 1
assert_response :success
end
- 1
test "update" do
- 1
put user_profile_url, params: { user: { name: "John Doe", bio: "Acrobat" } }
- 1
assert_redirected_to user_profile_url
- 1
assert_equal "John Doe", users(:david).reload.name
- 1
assert_equal "Acrobat", users(:david).bio
- 1
assert_equal "david@37signals.com", users(:david).email_address
end
- 1
test "updates are limited to the current user" do
- 1
put user_profile_url(users(:jason)), params: { user: { name: "John Doe" } }
- 1
assert_equal "Jason", users(:jason).reload.name
end
end
- 1
require "test_helper"
- 1
class Users::PushSubscriptionsControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 3
sign_in :david
end
- 1
test "create new push subscription" do
- 1
subscription_params = { "endpoint" => "https://apple", "p256dh_key" => "123", "auth_key" => "456" }
- 1
post user_push_subscriptions_url,
params: { push_subscription: subscription_params }, headers: { "HTTP_USER_AGENT" => "Mozilla/5.0" }
- 1
assert_response :ok
- 1
assert_equal subscription_params, users(:david).push_subscriptions.last.attributes.slice("endpoint", "p256dh_key", "auth_key")
- 1
assert_equal "Mozilla/5.0", users(:david).push_subscriptions.last.user_agent
end
- 1
test "touch existing subscription" do
- 3
assert_no_difference -> { users(:david).push_subscriptions.count } do
- 3
assert_changes -> { push_subscriptions(:david_chrome).reload.updated_at } do
- 1
post user_push_subscriptions_url(params: {
push_subscription: push_subscriptions(:david_chrome).attributes.slice("endpoint", "p256dh_key", "auth_key")
})
end
end
- 1
assert_response :ok
end
- 1
test "destroy a push subscription via dev mode" do
- 3
assert_difference -> { Push::Subscription.count }, -1 do
- 1
delete user_push_subscription_url(push_subscriptions(:david_chrome))
- 1
assert_redirected_to user_push_subscriptions_url
end
end
end
- 1
require "test_helper"
- 1
class Users::SidebarsControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 3
sign_in :david
end
- 1
test "show" do
- 1
get user_sidebar_url
- 1
users(:david).rooms.opens.each do |room|
- 2
assert_match /#{room.name}/, @response.body
end
end
- 1
test "unread directs" do
- 1
rooms(:david_and_jason).messages.create! client_message_id: 999, body: "Hello", creator: users(:jason)
- 1
get user_sidebar_url
- 7
assert_select ".unread", count: users(:david).memberships.select { |m| m.room.direct? && m.unread? }.count
end
- 1
test "unread other" do
- 1
rooms(:watercooler).messages.create! client_message_id: 999, body: "Hello", creator: users(:jason)
- 1
get user_sidebar_url
- 7
assert_select ".unread", count: users(:david).memberships.reject { |m| m.room.direct? || !m.unread? }.count
end
end
- 1
require "test_helper"
- 1
class UsersControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 6
@join_code = accounts(:signal).join_code
end
- 1
test "show" do
- 1
sign_in :david
- 1
get user_url(users(:david))
- 1
assert_response :ok
end
- 1
test "new" do
- 1
get join_url(@join_code)
- 1
assert_response :success
end
- 1
test "new does not allow a signed in user" do
- 1
sign_in :david
- 1
get join_url(@join_code)
- 1
assert_redirected_to root_url
end
- 1
test "new requires a join code" do
- 1
get join_url("not")
- 1
assert_response :not_found
end
- 1
test "create" do
- 3
assert_difference -> { User.count }, 1 do
- 1
post join_url(@join_code), params: { user: { name: "New Person", email_address: "new@37signals.com", password: "secret123456" } }
end
- 1
assert_redirected_to root_url
- 1
user = User.last
- 1
assert_equal user.id, Session.find_by(token: parsed_cookies.signed[:session_token]).user.id
- 1
assert_equal Rooms::Open.all, user.rooms
end
- 1
test "creating a new user with an existing email address will redirect to login screen" do
- 3
assert_no_difference -> { User.count } do
- 1
post join_url(@join_code), params: { user: { name: "Another David", email_address: users(:david).email_address, password: "secret123456" } }
end
- 1
assert_redirected_to new_session_url(email_address: users(:david).email_address)
end
end
- 1
require "test_helper"
- 1
class WelcomeControllerTest < ActionDispatch::IntegrationTest
- 1
setup do
- 2
sign_in :david
end
- 1
test "redirects to the first created visible room the user has access to" do
- 1
get root_url
- 1
assert_redirected_to room_url(users(:david).rooms.original)
end
- 1
test "redirects to the last room visited, if we have one" do
- 1
cookies[:last_room] = rooms(:watercooler).id
- 1
get root_url
- 1
assert_redirected_to room_url(rooms(:watercooler))
end
end
- 1
require "test_helper"
- 1
class ContentFiltersTest < ActionView::TestCase
- 1
test "entire message contains an unfurled URL" do
- 1
text = "https://basecamp.com/"
- 1
message = Message.create! room: rooms(:pets), body: unfurled_message_body_for_basecamp(text), client_message_id: "0015", creator: users(:jason)
- 1
filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
- 1
assert_not_equal message.body.body.to_html, filtered.to_html
- 1
assert_match /<div><action-text-attachment/, filtered.to_html
end
- 1
test "message includes additional text besides an unfurled URL" do
- 1
text = "Hello https://basecamp.com/"
- 1
message = Message.create! room: rooms(:pets), body: unfurled_message_body_for_basecamp(text), client_message_id: "0015", creator: users(:jason)
- 1
filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
- 1
assert_equal message.body.body.to_html, filtered.to_html
- 1
assert_match %r{<div>Hello https://basecamp\.com/<action-text-attachment}, filtered.to_html
end
- 1
test "unfurled tweet without any image" do
- 1
text = "<div>https://twitter.com/37signals/status/1750290547908952568<action-text-attachment content-type=\"application/vnd.actiontext.opengraph-embed\" url=\"https://pbs.twimg.com/profile_images/1671940407633010689/9P5gi6LF_200x200.jpg\" href=\"https://twitter.com/37signals/status/1750290547908952568\" filename=\"37signals (@37signals)\" caption=\"We're back up on all apps, everyone. Really sorry for the disruption to your day.\" content=\"<actiontext-opengraph-embed>\n <div class="og-embed">\n <div class="og-embed__content">\n <div class="og-embed__title">37signals (@37signals)</div>\n <div class="og-embed__description">We're back up on all apps, everyone. Really sorry for the disruption to your day.</div>\n </div>\n <div class="og-embed__image">\n <img src="https://pbs.twimg.com/profile_images/1671940407633010689/9P5gi6LF_200x200.jpg" class="image" alt="" />\n </div>\n </div>\n </actiontext-opengraph-embed>\"></action-text-attachment></div>"
- 1
message = Message.create! room: rooms(:pets), body: unfurled_message_body_for_basecamp(text), client_message_id: "0015", creator: users(:jason)
- 1
filtered = ContentFilters::StyleUnfurledTwitterAvatars.apply(message.body.body)
- 1
assert_match %r{<div class="cf-twitter-avatar">}, filtered.to_html
end
- 1
test "unfurled tweet containing an image" do
- 1
text = "<div>https://twitter.com/dhh/status/1748445489648050505<action-text-attachment content-type=\"application/vnd.actiontext.opengraph-embed\" url=\"https://pbs.twimg.com/media/GEO5l04bsAA9f6H.jpg\" href=\"https://twitter.com/dhh/status/1748445489648050505\" filename=\"DHH (@dhh)\" caption=\"We pay homage to the glorious MIT License with the ONCE license. May all our future legalese be as succinct!\" content=\"<actiontext-opengraph-embed>\n <div class="og-embed">\n <div class="og-embed__content">\n <div class="og-embed__title">DHH (@dhh)</div>\n <div class="og-embed__description">We pay homage to the glorious MIT License with the ONCE license. May all our future legalese be as succinct!</div>\n </div>\n <div class="og-embed__image">\n <img src="https://pbs.twimg.com/media/GEO5l04bsAA9f6H.jpg" class="image" alt="" />\n </div>\n </div>\n </actiontext-opengraph-embed>\"></action-text-attachment></div>"
- 1
message = Message.create! room: rooms(:pets), body: unfurled_message_body_for_basecamp(text), client_message_id: "0015", creator: users(:jason)
- 1
filtered = ContentFilters::StyleUnfurledTwitterAvatars.apply(message.body.body)
- 1
assert_no_match %r{<div class="cf-twitter-avatar">}, filtered.to_html
end
- 1
test "entire message contains an unfurled URL from x.com but unfurls to twitter.com" do
- 1
text = "https://x.com/dhh/status/1752476663303323939"
- 1
message = Message.create! room: rooms(:pets), body: unfurled_message_body_for_twitter(text), client_message_id: "0015", creator: users(:jason)
- 1
filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
- 1
assert_not_equal message.body.body.to_html, filtered.to_html
- 1
assert_match /<div><action-text-attachment/, filtered.to_html
end
- 1
test "entire message contains an unfurled URL from x.com with query params" do
- 1
text = "https://x.com/dhh/status/1752476663303323939?s=20"
- 1
message = Message.create! room: rooms(:pets), body: unfurled_message_body_for_twitter(text), client_message_id: "0015", creator: users(:jason)
- 1
filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
- 1
assert_not_equal message.body.body.to_html, filtered.to_html
- 1
assert_match /<div><action-text-attachment/, filtered.to_html
end
- 1
test "message contains a forbidden tag" do
- 1
exploit_image_tag = 'Hello <img src="https://ssecurityrise.com/tests/billionlaughs-cache.svg">World'
- 1
message = Message.create! room: rooms(:pets), body: exploit_image_tag, client_message_id: "0015", creator: users(:jason)
- 1
filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
- 1
assert_equal "Hello World", filtered.to_html
end
- 1
test "message with a mention attachment" do
- 1
message = Message.create! room: rooms(:pets), body: "<div>Hey #{mention_attachment_for(:david)}</div>", creator: users(:jason)
- 1
filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
- 1
expected = /<action-text-attachment sgid="#{users(:david).attachable_sgid}" content-type="application\/vnd\.campfire\.mention" content="(.*?)"><\/action-text-attachment>/m
- 1
assert_match expected, filtered.to_html
end
- 1
private
- 1
def unfurled_message_body_for_basecamp(text)
- 4
"<div>#{text}#{unfurled_link_trix_attachment_for_basecamp}</div>"
end
- 1
def unfurled_link_trix_attachment_for_basecamp
- 4
<<~BASECAMP
<action-text-attachment content-type=\"application/vnd.actiontext.opengraph-embed\" url=\"https://basecamp.com/assets/general/opengraph.png\" href=\"https://basecamp.com/\" filename=\"Project management software, online collaboration\" caption=\"Trusted by millions, Basecamp puts everything you need to get work done in one place. It’s the calm, organized way to manage projects, work with clients, and communicate company-wide.\" content=\"<actiontext-opengraph-embed>\n <div class="og-embed">\n <div class="og-embed__content">\n <div class="og-embed__title">Project management software, online collaboration</div>\n <div class="og-embed__description">Trusted by millions, Basecamp puts everything you need to get work done in one place. It’s the calm, organized way to manage projects, work with clients, and communicate company-wide.</div>\n </div>\n <div class="og-embed__image">\n <img src="https://basecamp.com/assets/general/opengraph.png" class="image" alt="" />\n </div>\n </div>\n </actiontext-opengraph-embed>\"></action-text-attachment>
BASECAMP
end
- 1
def unfurled_message_body_for_twitter(text)
- 2
"<div>#{text}#{unfurled_link_trix_attachment_for_twitter}</div>"
end
- 1
def unfurled_link_trix_attachment_for_twitter
- 2
<<~TWEET
<action-text-attachment content-type=\"application/vnd.actiontext.opengraph-embed\" url=\"https://pbs.twimg.com/ext_tw_video_thumb/1752476502791503873/pu/img/WEAqUgarUxWjPNHD.jpg\" href=\"https://twitter.com/dhh/status/1752476663303323939\" filename=\"DHH (@dhh)\" caption=\"We're playing with adding easy extension points to ONCE/Campfire. Here's one experiment for allowing any type of CSS to be easily added.\" content=\"<actiontext-opengraph-embed>\n <div class="og-embed">\n <div class="og-embed__content">\n <div class="og-embed__title">DHH (@dhh)</div>\n <div class="og-embed__description">We're playing with adding easy extension points to ONCE/Campfire. Here's one experiment for allowing any type of CSS to be easily added.</div>\n </div>\n <div class="og-embed__image">\n <img src="https://pbs.twimg.com/ext_tw_video_thumb/1752476502791503873/pu/img/WEAqUgarUxWjPNHD.jpg" class="image" alt="" />\n </div>\n </div>\n </actiontext-opengraph-embed>\"><figure class=\"attachment attachment--content attachment--og\">\n \n <div class=\"og-embed gap\">\n <div class=\"og-embed__content\">\n <div class=\"og-embed__title\">\n <a href=\"https://twitter.com/dhh/status/1752476663303323939\">DHH (@dhh)</a>\n </div>\n <div class=\"og-embed__description\">We're playing with adding easy extension points to ONCE/Campfire. Here's one experiment for allowing any type of CSS to be easily added.</div>\n </div>\n <div class=\"og-embed__image\">\n <img src=\"https://pbs.twimg.com/ext_tw_video_thumb/1752476502791503873/pu/img/WEAqUgarUxWjPNHD.jpg\" class=\"image center\" alt=\"\">\n </div>\n </div>\n \n</figure></action-text-attachment>
TWEET
end
end
- 1
require "test_helper"
- 1
class Account::JoinableTest < ActiveSupport::TestCase
- 1
test "new accounts get a joinable code" do
- 1
Account.destroy_all
- 1
account = Account.create!(name: "Chat")
- 1
assert_match /\w{4}-\w{4}-\w{4}/, account.join_code
end
- 1
test "accounts can reset join code" do
- 3
assert_changes -> { accounts(:signal).reload.join_code } do
- 1
accounts(:signal).reset_join_code
end
end
end
- 1
require "test_helper"
- 1
class AccountTest < ActiveSupport::TestCase
end
- 1
require "test_helper"
- 1
class ActionTextAttachmentTest < ActiveSupport::TestCase
- 1
setup do
- 3
@user = users(:david)
end
- 1
test "lookup user attachable with invalid sgid" do
- 1
message, signature = @user.attachable_sgid.split("--")
- 1
html = %Q(<action-text-attachment sgid="#{message}--invalid"></action-text-attachment>)
- 1
node = ActionText::Fragment.wrap(html).find_all(ActionText::Attachment.tag_name).first
- 1
attachment = ActionText::Attachment.from_node(node)
- 1
assert_equal @user, attachment.attachable
end
- 1
test "lookup attachable with nil sgid" do
- 1
html = %Q(<action-text-attachment></action-text-attachment>)
- 1
node = ActionText::Fragment.wrap(html).find_all(ActionText::Attachment.tag_name).first
- 1
attachment = ActionText::Attachment.from_node(node)
- 1
assert_kind_of ActionText::Attachables::MissingAttachable, attachment.attachable
end
- 1
test "lookup invalid sgid for an attachable requiring a valid sgid" do
# Make room instance attachable for testing purposes
- 2
room = rooms(:pets).tap { |r| r.extend ActionText::Attachable }
- 1
message, signature = rooms(:pets).attachable_sgid.split("--")
- 1
html = %Q(<action-text-attachment sgid="#{message}--invalid"></action-text-attachment>)
- 1
node = ActionText::Fragment.wrap(html).find_all(ActionText::Attachment.tag_name).first
- 1
attachment = ActionText::Attachment.from_node(node)
- 1
assert_kind_of ActionText::Attachables::MissingAttachable, attachment.attachable
end
end
- 1
require "test_helper"
- 1
class FirstRunTest < ActiveSupport::TestCase
- 1
setup do
- 3
Account.destroy_all
- 3
Room.destroy_all
- 3
User.destroy_all
end
- 1
test "creating makes first user an administrator" do
- 1
user = create_first_run_user
- 1
assert user.administrator?
end
- 1
test "first user has access to first room" do
- 1
user = create_first_run_user
- 1
assert user.rooms.one?
end
- 1
test "first room is an open room" do
- 1
create_first_run_user
- 1
assert Room.first.open?
end
- 1
private
- 1
def create_first_run_user
- 3
FirstRun.create!({ name: "User", email_address: "user@example.com", password: "secret123456" })
end
end
- 1
require "test_helper"
- 1
class MembershipTest < ActiveSupport::TestCase
- 1
setup do
- 9
@membership = memberships(:david_watercooler)
end
- 1
test "connected scope" do
- 1
@membership.connected
- 1
assert Membership.connected.exists?(@membership.id)
- 1
@membership.disconnected
- 1
assert_not Membership.connected.exists?(@membership.id)
- 1
travel_to Membership::Connectable::CONNECTION_TTL.from_now + 1
- 1
assert_not Membership.connected.exists?(@membership.id)
end
- 1
test "disconnected scope" do
- 1
@membership.disconnected
- 1
assert Membership.disconnected.exists?(@membership.id)
- 1
@membership.connected
- 1
assert_not Membership.disconnected.exists?(@membership.id)
- 1
travel_to Membership::Connectable::CONNECTION_TTL.from_now + 1
- 1
assert Membership.disconnected.exists?(@membership.id)
end
- 1
test "connected? is false when connection is stale" do
- 1
@membership.connected
- 1
travel_to Membership::Connectable::CONNECTION_TTL.from_now + 1
- 1
assert_not @membership.connected?
end
- 1
test "connecting" do
- 1
@membership.connected
- 1
assert @membership.connected?
- 1
assert_equal 1, @membership.connections
- 1
@membership.connected
- 1
assert_equal 2, @membership.connections
end
- 1
test "connecting resets stale connection count" do
- 3
2.times { @membership.connected }
- 1
assert_equal 2, @membership.connections
- 1
travel_to Membership::Connectable::CONNECTION_TTL.from_now + 1
- 1
@membership.connected
- 1
assert_equal 1, @membership.connections
end
- 1
test "disconnecting" do
- 3
2.times { @membership.connected }
- 1
@membership.disconnected
- 1
assert @membership.connected?
- 1
assert_equal 1, @membership.connections
- 1
@membership.disconnected
- 1
assert_not @membership.connected?
- 1
assert_equal 0, @membership.connections
end
- 1
test "disconnecting resets stale connection count" do
- 3
2.times { @membership.connected }
- 1
assert_equal 2, @membership.connections
- 1
travel_to Membership::Connectable::CONNECTION_TTL.from_now + 1
- 1
@membership.disconnected
- 1
assert_equal 0, @membership.connections
end
- 1
test "refreshing the connection" do
- 1
@membership.connected
- 1
travel_to Membership::Connectable::CONNECTION_TTL.from_now + 1
- 1
assert_not @membership.connected?
- 1
@membership.refresh_connection
- 1
assert @membership.connected?
end
- 1
test "removing a membership resets the user's connections" do
- 1
@membership.user.expects :reset_remote_connections
- 1
@membership.destroy
end
end
- 1
require "test_helper"
- 1
class Message::AttachmentTest < ActiveSupport::TestCase
- 1
include ActiveJob::TestHelper
- 1
include ActionDispatch::TestProcess
- 1
test "creating a message creates image thumbnail" do
- 1
message = create_attachment_message("moon.jpg", "image/jpeg")
- 1
assert message.attachment.representation(:thumb).image.present?
end
- 1
test "creating a message creates video preview" do
- 1
message = create_attachment_message("alpha-centuri.mov", "video/quicktime")
- 1
assert message.reload.attachment.preview(format: :webp).image.attached?
end
- 1
test "creating a blank message with attachment will use filename as plain text body" do
- 1
message = create_attachment_message("moon.jpg", "image/jpeg")
- 1
assert_equal message.plain_text_body, "moon.jpg"
end
- 1
private
- 1
def create_attachment_message(file, content_type)
- 3
rooms(:hq).messages.create_with_attachment! \
creator: users(:david),
client_message_id: "message",
attachment: fixture_file_upload(file, content_type)
end
end
- 1
require "test_helper"
- 1
class Message::SearchableTest < ActiveSupport::TestCase
- 1
test "message body is indexed and searchable" do
- 1
message = rooms(:designers).messages.create! body: "My hovercraft is full of eels", client_message_id: "earth", creator: users(:david)
- 1
assert_equal [ message ], rooms(:designers).messages.search("eel")
- 1
message.update! body: "My hovercraft is full of sharks"
- 1
assert_equal [ message ], rooms(:designers).messages.search("sharks")
- 1
message.destroy!
- 1
assert_equal [], rooms(:designers).messages.search("sharks")
end
- 1
test "search results are returned in message order" do
- 1
messages = [ "first cat", "second cat", "third cat", "cat cat cat" ].map do |body|
- 4
rooms(:designers).messages.create! body: body, client_message_id: body, creator: users(:david)
end
- 1
assert_equal messages, rooms(:designers).messages.search("cat")
end
- 1
test "rich text body is converted to plain text for indexing" do
- 1
message = rooms(:designers).messages.create! body: "<span>My hovercraft is full of eels</span>", client_message_id: "earth", creator: users(:david)
- 1
assert_equal [], rooms(:designers).messages.search("span")
- 1
assert_equal [ message ], rooms(:designers).messages.search("eel")
end
end
- 1
require "test_helper"
- 1
class MessageTest < ActiveSupport::TestCase
- 1
include ActionCable::TestHelper, ActiveJob::TestHelper
- 1
test "creating a message enqueues to push later" do
- 1
assert_enqueued_jobs 1, only: [ Room::PushMessageJob ] do
- 1
create_new_message_in rooms(:designers)
end
end
- 1
test "all emoji" do
- 1
assert Message.new(body: "😄🤘").plain_text_body.all_emoji?
- 1
assert_not Message.new(body: "Haha! 😄🤘").plain_text_body.all_emoji?
- 1
assert_not Message.new(body: "🔥\nmultiple lines\n💯").plain_text_body.all_emoji?
- 1
assert_not Message.new(body: "🔥 💯").plain_text_body.all_emoji?
end
- 1
test "mentionees" do
- 1
message = Message.new room: rooms(:pets), body: "<div>Hey #{mention_attachment_for(:david)}</div>", creator: users(:jason), client_message_id: "earth"
- 1
assert_equal [ users(:david) ], message.mentionees
- 1
message_with_duplicate_mentions = Message.new room: rooms(:pets), body: "<div>Hey #{mention_attachment_for(:david)} #{mention_attachment_for(:david)}</div>", creator: users(:jason), client_message_id: "earth"
- 1
assert_equal [ users(:david) ], message.mentionees
- 1
message_mentioning_a_non_member = Message.new room: rooms(:pets), body: "<div>Hey #{mention_attachment_for(:kevin)}</div>", creator: users(:jason), client_message_id: "earth"
- 1
assert_equal [], message_mentioning_a_non_member.mentionees
end
- 1
private
- 1
def create_new_message_in(room)
- 1
room.messages.create!(creator: users(:jason), body: "Hello", client_message_id: "123")
end
end
- 1
require "test_helper"
- 1
class Opengraph::DocumentTest < ActiveSupport::TestCase
- 1
test "extract opengraph tags using property attribute" do
- 1
document = Opengraph::Document.new("<html><head><meta property=\"og:url\" content=\"https://example.com\"><meta property=\"og:title\" content=\"Hey!\"><meta property=\"og:description\" content=\"desc..\"><meta property=\"og:image\" content=\"https://example.com/image.png\"></head></html>")
- 1
attributes = document.opengraph_attributes
- 1
assert_equal "https://example.com", attributes[:url]
- 1
assert_equal "Hey!", attributes[:title]
- 1
assert_equal "desc..", attributes[:description]
- 1
assert_equal "https://example.com/image.png", attributes[:image]
end
- 1
test "extract opengraph tags using name attribute" do
- 1
document = Opengraph::Document.new("<html><head><meta name=\"og:url\" content=\"https://example.com\"><meta name=\"og:title\" content=\"Hey!\"><meta name=\"og:description\" content=\"desc..\"><meta name=\"og:image\" content=\"https://example.com/image.png\"></head></html>")
- 1
attributes = document.opengraph_attributes
- 1
assert_equal "https://example.com", attributes[:url]
- 1
assert_equal "Hey!", attributes[:title]
- 1
assert_equal "desc..", attributes[:description]
- 1
assert_equal "https://example.com/image.png", attributes[:image]
end
- 1
test "document containing missing meta encoding tag and non-UTF8 characters" do
- 1
document = Opengraph::Document.new("<html><head><meta name=\"og:url\" content=\"https://example.com\"><meta name=\"og:title\" content=\"Hey!\"><meta name=\"og:description\" content=\"Hello â\u0080\u0099World\"><meta name=\"og:image\" content=\"https://example.com/image.png\"></head></html>")
- 1
attributes = document.opengraph_attributes
- 1
assert_equal "https://example.com", attributes[:url]
- 1
assert_equal "Hey!", attributes[:title]
- 1
assert_equal "Hello World", attributes[:description]
- 1
assert_equal "https://example.com/image.png", attributes[:image]
end
end
- 1
require "test_helper"
- 1
require "restricted_http/private_network_guard"
- 1
class Opengraph::FetchTest < ActiveSupport::TestCase
- 1
setup do
- 11
@fetch = Opengraph::Fetch.new
- 11
@url = URI.parse("https://www.example.com")
end
- 1
test "#fetch_document fetches valid HTML" do
- 1
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 200, body: "<body>ok<body>", headers: { content_type: "text/html" })
- 1
assert_equal "<body>ok<body>", @fetch.fetch_document(@url)
end
- 1
test "#fetch_document discards other content types" do
- 1
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 200, body: "I'm not HTML!", headers: { content_type: "text/plain" })
- 1
assert_nil @fetch.fetch_document(@url)
end
- 1
test "#fetch_document follows redirects" do
- 1
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 302, headers: { location: "https://www.other.com/" })
- 1
WebMock.stub_request(:get, "https://www.other.com/")
.to_return(status: 200, body: "<body>ok<body>", headers: { content_type: "text/html" })
- 1
assert_equal "<body>ok<body>", @fetch.fetch_document(@url)
end
- 1
test "#fetch_document does not follow redirects to private networks" do
- 1
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 302, headers: { location: "https://www.other.com/" })
- 1
WebMock.stub_request(:get, "https://www.other.com/")
.to_return(status: 200, body: "<body>ok<body>", headers: { content_type: "text/html" })
- 1
Resolv.stubs(:getaddress).with("www.other.com").returns("127.0.0.1")
- 1
assert_raises RestrictedHTTP::Violation do
- 1
@fetch.fetch_document(@url, ip: "1.2.3.4")
end
end
- 1
test "#fetch_document resolves hostnames once to avoid DNS rebinding" do
# Allow but interrupt a real connection to demonstrate that we connect
# to a resolved IP, not a hostname to re-resolve.
- 1
WebMock.disable_net_connect! allow: [ @url.host ]
- 1
Resolv.stubs(:getaddress).with(@url.host).returns("1.2.3.4", "127.0.0.1")
- 1
TCPSocket.expects(:open).with(@url.host, 443, nil, nil).never
- 1
TCPSocket.expects(:open).with("1.2.3.4", 443, nil, nil).throws(:dns_not_rebound)
- 1
assert_throws :dns_not_rebound do
- 1
@fetch.fetch_document(@url)
end
end
- 1
test "#fetch_document resolves redirect location hostnames once to avoid DNS rebinding" do
# Stub the initial URL to redirect to a DNS-rebound location
- 1
WebMock.stub_request(:get, "https://www.other.com/")
.to_return(status: 302, headers: { location: @url.to_s })
# Allow but interrupt a real connection to demonstrate that we connect
# to a resolved IP, not a hostname to re-resolve.
- 1
WebMock.disable_net_connect! allow: [ @url.host ]
- 1
Resolv.stubs(:getaddress).with(@url.host).returns("1.2.3.4", "127.0.0.1")
- 1
TCPSocket.expects(:open).with(@url.host, 443, nil, nil).never
- 1
TCPSocket.expects(:open).with("1.2.3.4", 443, nil, nil).throws(:dns_not_rebound)
- 1
assert_throws :dns_not_rebound do
- 1
@fetch.fetch_document(URI.parse("https://www.other.com/"), ip: "1.2.3.4")
end
end
- 1
test "#fetch_document is empty following redirects that never finish" do
- 1
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 302, headers: { location: "https://www.example.com/" })
- 1
assert_raises Opengraph::Fetch::TooManyRedirectsError do
- 1
@fetch.fetch_document(@url)
end
end
- 1
test "#fetch_document ignores large responses" do
- 1
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 200, body: "too large", headers: { content_length: 1.gigabyte, content_type: "text/html" })
- 1
assert_nil @fetch.fetch_document(@url)
end
- 1
test "#fetch_document ignores large responses that were missing their content length" do
- 1
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 200, body: large_body_content, headers: { content_type: "text/html" })
- 1
assert_nil @fetch.fetch_document(@url)
end
- 1
test "#fetch_document ignores large responses that were lying about their content length" do
- 1
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 200, body: large_body_content, headers: { content_length: 1.megabyte, content_type: "text/html" })
- 1
assert_nil @fetch.fetch_document(@url)
end
- 1
test "fetch content type" do
- 1
WebMock.stub_request(:head, "https://example.com/image.png").to_return(status: 200, headers: { content_type: "image/png" })
- 1
url = URI.parse("https://example.com/image.png")
- 1
assert_equal "image/png", @fetch.fetch_content_type(url)
end
- 1
private
- 1
def large_body_content
- 2
"x" * (Opengraph::Fetch::MAX_BODY_SIZE + 1)
end
end
- 1
require "test_helper"
- 1
require "restricted_http/private_network_guard"
- 1
class Opengraph::LocationTest < ActiveSupport::TestCase
- 1
test "url validations" do
- 1
assert Opengraph::Location.new("https://www.example.com").valid?
- 1
assert Opengraph::Location.new("http://www.example.com").valid?
- 1
assert_not Opengraph::Location.new("~/etc/password").valid?
- 1
assert_not Opengraph::Location.new("ftp://speedtest.tele2.net").valid?
- 1
assert_not Opengraph::Location.new("httpfake").valid?
- 1
assert_not Opengraph::Location.new(" foo").valid?
- 1
assert_not Opengraph::Location.new("https/incorrect").valid?
end
- 1
test "private network urls" do
- 1
Resolv.stubs(:getaddress).with("www.example.com").returns("172.16.0.0")
- 1
location = Opengraph::Location.new("https://www.example.com")
- 1
assert_not location.valid?
- 1
assert_equal [ "is not public" ], location.errors[:url]
end
- 1
test "avoid reading file urls when expecting HTML" do
- 1
large_file = Opengraph::Location.new("https://www.example.com/100gb.zip")
- 1
assert_nil Opengraph::Location.new("http://www.example.com/video.mp4").read_html
- 1
assert_nil Opengraph::Location.new("http://www.example.com/archive.tar").read_html
- 1
assert_nil Opengraph::Location.new("https://www.example.com/large.heic").read_html
- 1
assert_nil Opengraph::Location.new("https://www.example.com/image.jpeg").read_html
- 1
assert_nil Opengraph::Location.new("https://www.example.com/malware.exe").read_html
- 1
assert_nil Opengraph::Location.new("https://www.example.com/massiveOS.iso").read_html
end
- 1
test "read valid HTML" do
- 1
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 200, body: "<body>ok<body>", headers: { content_type: "text/html" })
- 1
location = Opengraph::Location.new("https://www.example.com")
- 1
assert_equal "<body>ok<body>", location.read_html
end
- 1
test "read ignores invalid responses" do
- 1
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 200, body: "too large", headers: { content_length: 1.gigabyte, content_type: "text/html" })
- 1
location = Opengraph::Location.new("https://www.example.com")
- 1
assert_nil location.read_html
end
end
- 1
require "test_helper"
- 1
class Opengraph::MetadataTest < ActiveSupport::TestCase
- 1
test "successful fetch" do
- 1
body = <<~HTML
<html>
<head>
<meta property="og:url" content="https://example.com">
<meta property="og:title" content="Hey!">
<meta property="og:description" content="Hello">
<meta property="og:image" content="https://example.com/image.png">
</head>
</html>
HTML
- 1
WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: body, headers: { content_type: "text/html" })
- 1
WebMock.stub_request(:head, "https://example.com/image.png").to_return(status: 200, headers: { content_type: "image/png" })
- 1
metadata = Opengraph::Metadata.from_url("https://www.example.com")
- 1
assert metadata.valid?
- 1
assert_equal "https://example.com", metadata.url
- 1
assert_equal "Hey!", metadata.title
- 1
assert_equal "Hello", metadata.description
- 1
assert_equal "https://example.com/image.png", metadata.image
end
- 1
test "missing opengraph meta tags" do
- 1
WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: "<html><head></head></html>", headers: { content_type: "text/html" })
- 1
opengraph = Opengraph::Metadata.from_url("https://www.example.com")
- 1
assert_not opengraph.valid?
- 1
assert_equal [ "Title can't be blank", "Description can't be blank" ], opengraph.errors.full_messages
end
- 1
test "URL uses the provided value if the returned value is missing" do
- 1
body = <<~HTML
<html>
<head>
<meta property="og:title" content="Hey!">
<meta property="og:description" content="Hello">
<meta property="og:image" content="https://example.com/image.png">
</head>
</html>
HTML
- 1
WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: body, headers: { content_type: "text/html" })
- 1
WebMock.stub_request(:head, "https://example.com/image.png").to_return(status: 200, headers: { content_type: "image/png" })
- 1
metadata = Opengraph::Metadata.from_url("https://www.example.com")
- 1
assert metadata.valid?
- 1
assert_equal "https://www.example.com", metadata.url
end
- 1
test "URL uses the provided value if the returned value is invalid" do
- 1
body = <<~HTML
<html>
<head>
<meta property="og:url" content="/foo">
<meta property="og:title" content="Hey!">
<meta property="og:description" content="Hello">
<meta property="og:image" content="https://example.com/image.png">
</head>
</html>
HTML
- 1
WebMock.stub_request(:get, "https://www.example.com/foo").to_return(status: 200, body: body, headers: { content_type: "text/html" })
- 1
WebMock.stub_request(:head, "https://example.com/image.png").to_return(status: 200, headers: { content_type: "image/png" })
- 1
metadata = Opengraph::Metadata.from_url("https://www.example.com/foo")
- 1
assert metadata.valid?
- 1
assert_equal "https://www.example.com/foo", metadata.url
end
- 1
test "missing response body" do
- 1
WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 403, body: "", headers: { content_type: "text/html" })
- 1
assert_not Opengraph::Metadata.from_url("https://www.example.com").valid?
end
- 1
test "non html response" do
- 1
WebMock.stub_request(:get, "https://www.example.com/image").to_return(status: 200, body: "[blob]", headers: { content_type: "image/jpeg" })
- 1
assert_not Opengraph::Metadata.from_url("https://www.example.com/image").valid?
end
- 1
test "relative and invalid image URLs are ignored" do
- 1
body = <<~HTML
<html>
<head>
<meta property="og:url" content="https://example.com">
<meta property="og:title" content="Hey!">
<meta property="og:description" content="Hello">
<meta property="og:image" content="%s">
</head>
</html>
HTML
- 1
[ "/image.png", "foo", "https/incorrect", "~/etc/password" ].each do |invalid_image_url|
- 4
WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: body % invalid_image_url, headers: { content_type: "text/html" })
- 4
opengraph = Opengraph::Metadata.from_url("https://www.example.com")
- 4
assert opengraph.valid?
- 4
assert_nil opengraph.image
end
end
- 1
test "sanitize title and description" do
- 1
body = <<~HTML
<html>
<head>
<meta property="og:title" content="Hey!<script>alert('hi')</script>">
<meta property="og:description" content="Hello<script>alert('hi')</script>">
<meta property="og:image" content="https://example.com/image.png">
</head>
</html>
HTML
- 1
WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: body, headers: { content_type: "text/html" })
- 1
WebMock.stub_request(:head, "https://example.com/image.png").to_return(status: 200, headers: { content_type: "image/png" })
- 1
metadata = Opengraph::Metadata.from_url("https://www.example.com")
- 1
assert metadata.valid?
- 1
assert_equal "Hey!", metadata.title
- 1
assert_equal "Hello", metadata.description
end
- 1
test "remove encoded tags from title and description" do
- 1
body = <<~HTML
<html>
<head>
<meta property="og:title" content="Hey!</script><img src=a onerror=prompt(1)>">
<meta property="og:description" content="Hello</script><img src=a onerror=prompt(2)></script>">
<meta property="og:image" content="https://example.com/image.png">
</head>
</html>
HTML
- 1
WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: body, headers: { content_type: "text/html" })
- 1
WebMock.stub_request(:head, "https://example.com/image.png").to_return(status: 200, headers: { content_type: "image/png" })
- 1
metadata = Opengraph::Metadata.from_url("https://www.example.com")
- 1
assert metadata.valid?
- 1
assert_equal "Hey!", metadata.title
- 1
assert_equal "Hello", metadata.description
end
- 1
test "does not allow SVG content type for preview image" do
- 1
body = <<~HTML
<html>
<head>
<meta property="og:url" content="https://example.com">
<meta property="og:title" content="Hey!">
<meta property="og:description" content="Hello">
<meta property="og:image" content="https://example.com/image.svg">
</head>
</html>
HTML
- 1
WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: body, headers: { content_type: "text/html" })
- 1
WebMock.stub_request(:head, "https://example.com/image.svg").to_return(status: 200, headers: { content_type: "image/svg+xml" })
- 1
metadata = Opengraph::Metadata.from_url("https://www.example.com")
- 1
assert metadata.valid?
- 1
assert_nil metadata.image
end
end
- 1
require "test_helper"
- 1
class Room::PushTest < ActiveSupport::TestCase
- 1
include ActiveJob::TestHelper
- 1
test "deliver new message to other room users with push subscriptions" do
- 1
task_count = Push::Subscription.count - users(:david).push_subscriptions.count
- 1
perform_enqueued_jobs only: Room::PushMessageJob do
- 1
WebPush.expects(:payload_send).times(task_count)
- 1
rooms(:hq).messages.create! body: "This is from earth", client_message_id: "earth", creator: users(:david)
end
- 1
wait_for_web_push_delivery_pool_tasks(task_count)
end
- 1
test "notifies subscribed users" do
- 1
perform_enqueued_jobs only: Room::PushMessageJob do
- 1
WebPush.expects(:payload_send).times(2)
- 1
rooms(:designers).messages.create! body: "This is from earth", client_message_id: "earth", creator: users(:david)
end
- 1
wait_for_web_push_delivery_pool_tasks(2)
- 1
perform_enqueued_jobs only: Room::PushMessageJob do
- 1
WebPush.expects(:payload_send).times(3)
- 1
rooms(:designers).messages.create! body: "Hey #{mention_attachment_for(:kevin)}", client_message_id: "earth", creator: users(:david)
end
- 1
wait_for_web_push_delivery_pool_tasks(5)
end
- 1
test "does not notify for connected rooms" do
- 1
memberships(:kevin_designers).connected
- 1
perform_enqueued_jobs only: Room::PushMessageJob do
- 1
WebPush.expects(:payload_send).times(2)
- 1
rooms(:designers).messages.create! body: "Hey @kevin", client_message_id: "earth", creator: users(:david)
end
- 1
wait_for_web_push_delivery_pool_tasks(2)
end
- 1
test "does not notify for invisible rooms" do
- 1
memberships(:kevin_designers).update! involvement: "invisible"
- 1
perform_enqueued_jobs only: Room::PushMessageJob do
- 1
WebPush.expects(:payload_send).times(2)
- 1
rooms(:designers).messages.create! body: "Hey @kevin", client_message_id: "earth", creator: users(:david)
end
- 1
wait_for_web_push_delivery_pool_tasks(2)
end
- 1
test "destroys invalid subscriptions" do
- 1
memberships(:kevin_designers).update! involvement: "invisible"
- 3
assert_difference -> { Push::Subscription.count }, -2 do
- 1
perform_enqueued_jobs only: Room::PushMessageJob do
- 1
WebPush.expects(:payload_send).times(2).raises(WebPush::ExpiredSubscription.new(Struct.new(:body).new, "example.com"))
- 1
rooms(:designers).messages.create! body: "Hey @kevin", client_message_id: "earth", creator: users(:david)
end
- 1
wait_for_web_push_delivery_pool_tasks(2)
- 1
wait_for_invalidation_pool_tasks(2)
end
end
- 1
private
- 1
def wait_for_web_push_delivery_pool_tasks(count)
- 6
wait_for_pool_tasks(Rails.configuration.x.web_push_pool.delivery_pool, count)
end
- 1
def wait_for_invalidation_pool_tasks(count)
- 1
wait_for_pool_tasks(Rails.configuration.x.web_push_pool.invalidation_pool, count)
end
- 1
def wait_for_pool_tasks(pool, count)
- 7
start = Time.now
- 7
timeout = 0.2
- 7
while pool.completed_task_count < count
- 1
raise "Timeout waiting for pool tasks to complete" if Time.now - start > timeout
- 1
sleep timeout / 10.0
end
end
end
- 1
require "test_helper"
- 1
class RoomTest < ActiveSupport::TestCase
- 1
test "grant membership to user" do
- 1
rooms(:watercooler).memberships.grant_to(users(:kevin))
- 1
assert rooms(:watercooler).users.include?(users(:kevin))
end
- 1
test "revoke membership from user" do
- 1
rooms(:watercooler).memberships.revoke_from(users(:david))
- 1
assert_not rooms(:watercooler).users.include?(users(:david))
end
- 1
test "revise memberships" do
- 1
rooms(:watercooler).memberships.revise(granted: users(:kevin), revoked: users(:david))
- 1
assert rooms(:watercooler).users.include?(users(:kevin))
- 1
assert_not rooms(:watercooler).users.include?(users(:david))
end
- 1
test "create for users by giving them immediate membership" do
- 1
room = Rooms::Closed.create_for({ name: "Hello!", creator: users(:david) }, users: [ users(:kevin), users(:david) ])
- 1
assert room.users.include?(users(:kevin))
- 1
assert room.users.include?(users(:david))
end
- 1
test "type" do
- 1
assert Rooms::Open.new.open?
- 1
assert_not Rooms::Open.new.direct?
- 1
assert Rooms::Direct.new.direct?
- 1
assert Rooms::Closed.new.closed?
end
- 1
test "default involvement for new users" do
- 1
room = Rooms::Closed.create_for({ name: "Hello!", creator: users(:david) }, users: [ users(:kevin), users(:david) ])
- 3
assert room.memberships.all? { |m| m.involved_in_mentions? }
end
end
- 1
require "test_helper"
- 1
class Rooms::DirectTest < ActiveSupport::TestCase
- 1
test "create room for same users" do
- 1
room = Rooms::Direct.find_or_create_for([ users(:david), users(:kevin) ])
- 1
assert room.users.include?(users(:david))
- 1
assert room.users.include?(users(:kevin))
- 1
assert_not room.users.include?(users(:jason))
end
- 1
test "only one room will exist for the same users" do
- 1
room1 = Rooms::Direct.find_or_create_for([ users(:david), users(:kevin) ])
- 1
room2 = Rooms::Direct.find_or_create_for([ users(:kevin), users(:david) ])
- 1
assert_equal room1, room2
end
- 1
test "default involvement for new users" do
- 1
room = Rooms::Direct.find_or_create_for([ users(:david), users(:kevin) ])
- 3
assert room.memberships.all? { |m| m.involved_in_everything? }
end
end
- 1
require "test_helper"
- 1
class Rooms::OpenTest < ActiveSupport::TestCase
- 1
test "grants access to all users after creation" do
- 1
room = Rooms::Open.create!(name: "My open room with everyone!", creator: users(:david))
- 1
assert_equal User.count, room.users.count
end
- 1
test "grants access to all users after becoming open" do
- 1
room = rooms(:watercooler).becomes!(Rooms::Open)
- 1
room.save!
- 1
assert_equal User.count, room.users.count
end
end
- 1
require "test_helper"
- 1
class User::BotTest < ActiveSupport::TestCase
- 1
test "create bot" do
- 1
token = "5M0aLYwQyBXOXa5Wsz6NZb11EE4tW2"
- 1
SecureRandom.stubs(:alphanumeric).returns(token)
- 1
uuid = "3574925f-479d-44f8-82b7-fc039af5367c"
- 1
Random.stubs(:uuid).returns(uuid)
- 1
bot = User.create_bot!(name: "Bender")
- 1
assert_equal "#{bot.id}-#{token}", bot.bot_key
end
- 1
test "reset bot key" do
- 1
first_token = "5M0aLYwQyBXOXa5Wsz6NZb11EE4tW2"
- 1
SecureRandom.stubs(:alphanumeric).returns(first_token)
- 1
bot = User.create_bot!(name: "Bender")
- 1
assert_equal "#{bot.id}-#{first_token}", bot.bot_key
- 1
second_token = "R4kme9anwWRuz3sSoBXiB8Li8ioZPP"
- 1
SecureRandom.stubs(:alphanumeric).returns(second_token)
- 1
bot.reset_bot_key
- 1
assert_equal "#{bot.id}-#{second_token}", bot.bot_key
end
- 1
test "authenticate" do
- 1
bot = User.create_bot!(name: "Bender")
- 1
assert User.authenticate_bot(bot.bot_key)
end
- 1
test "deliver message by webhook" do
- 1
WebMock.stub_request(:post, webhooks(:bender).url).to_return(status: 200)
- 1
perform_enqueued_jobs only: Bot::WebhookJob do
- 1
users(:bender).deliver_webhook_later(messages(:first))
end
end
end
- 1
require "test_helper"
- 1
class User::RoleTest < ActiveSupport::TestCase
- 1
test "creating subsequent users makes them members" do
- 1
assert User.create!(name: "User", email_address: "user@example.com", password: "secret123456").member?
end
- 1
test "can_administer?" do
- 1
assert User.new(role: :administrator).can_administer?
- 1
assert_not User.new(role: :member).can_administer?
- 1
assert_not User.new.can_administer?
end
- 1
test "can administer a record" do
- 1
member = User.new(role: :member)
- 1
assert member.can_administer?(Room.new(creator: member))
- 1
another_member = User.new(role: :member)
- 1
assert another_member.can_administer?(Room.new(creator: member))
- 1
assert_not another_member.can_administer?(rooms(:designers))
end
end
- 1
require "test_helper"
- 1
class UserTest < ActiveSupport::TestCase
- 1
test "user does not prevent very long passwords" do
- 1
users(:david).update(password: "secret" * 50)
- 1
assert users(:david).valid?
end
- 1
test "creating users grants membership to the open rooms" do
- 3
assert_difference -> { Membership.count }, +Rooms::Open.count do
- 1
create_new_user
end
end
- 1
test "deactivating a user deletes push subscriptions, searches, memberships for non-direct rooms, and changes their email address" do
- 3
assert_difference -> { Membership.count }, -users(:david).memberships.without_direct_rooms.count do
- 3
assert_difference -> { Push::Subscription.count }, -users(:david).push_subscriptions.count do
- 3
assert_difference -> { Search.count }, -users(:david).searches.count do
- 1
SecureRandom.stubs(:uuid).returns("2e7de450-cf04-4fa8-9b02-ff5ab2d733e7")
- 1
users(:david).deactivate
- 1
assert_equal "david-deactivated-2e7de450-cf04-4fa8-9b02-ff5ab2d733e7@37signals.com", users(:david).reload.email_address
end
end
end
end
- 1
test "deactivating a user deletes their sessions" do
- 3
assert_changes -> { users(:david).sessions.count }, from: 1, to: 0 do
- 1
users(:david).deactivate
end
end
- 1
private
- 1
def create_new_user
- 1
User.create!(name: "User", email_address: "user@example.com", password: "secret123456")
end
end
- 1
require "test_helper"
- 1
class WebhookTest < ActiveSupport::TestCase
- 1
test "payload" do
- 1
message = messages(:first)
- 1
message_path = Rails.application.routes.url_helpers.room_at_message_path(message.room, message)
- 1
bot_messages_path = Rails.application.routes.url_helpers.room_bot_messages_path(message.room, users(:bender).bot_key)
- 1
WebMock.stub_request(:post, webhooks(:bender).url).
with(body: hash_including(
user: { id: message.creator.id, name: message.creator.name },
room: { id: message.room.id, name: message.room.name, path: bot_messages_path },
message: { id: message.id, body: { html: "First post!", plain: "First post!" }, path: message_path },
))
- 1
response = webhooks(:bender).deliver(messages(:first))
- 1
assert_equal 200, response.code.to_i
end
- 1
test "delivery" do
- 1
WebMock.stub_request(:post, webhooks(:bender).url).to_return(status: 200, body: "", headers: {})
- 1
response = webhooks(:bender).deliver(messages(:first))
- 1
assert_equal 200, response.code.to_i
end
- 1
test "delivery with OK text reply" do
- 1
WebMock.stub_request(:post, webhooks(:bender).url).to_return(status: 200, body: "Hello back!", headers: { "Content-Type" => "text/plain" })
- 1
response = webhooks(:bender).deliver(messages(:first))
- 1
reply_message = Message.last
- 1
assert_equal "Hello back!", reply_message.body.to_plain_text
end
- 1
test "delivery with OK attachment reply" do
- 1
WebMock.stub_request(:post, webhooks(:bender).url).to_return(status: 200, body: file_fixture("moon.jpg"), headers: { "Content-Type" => "image/jpeg" })
- 1
response = webhooks(:bender).deliver(messages(:first))
- 1
reply_message = Message.last
- 1
assert reply_message.attachment.present?
end
- 1
test "delivery with error reply" do
- 3
assert_no_difference -> { Message.count } do
- 1
WebMock.stub_request(:post, webhooks(:bender).url).to_return(status: 500, body: "Internal Error!", headers: {})
- 1
response = webhooks(:bender).deliver(messages(:first))
end
end
- 1
test "delivery that times out" do
- 1
Webhook.any_instance.stubs(:post).raises(Net::OpenTimeout)
- 1
response = webhooks(:bender).deliver(messages(:first))
- 1
reply_message = Message.last
- 1
assert_equal "Failed to respond within 7 seconds", reply_message.body.to_plain_text
end
end
- 1
module MentionTestHelper
- 1
def mention_attachment_for(name)
- 8
user = users(name)
- 8
attachment_body = ApplicationController.render partial: "users/mention", locals: { user: user }
- 8
"<action-text-attachment sgid=\"#{user.attachable_sgid}\" content-type=\"application/vnd.campfire.mention\" content=\"#{attachment_body.gsub('"', '"')}\"></action-text-attachment>"
end
end
- 1
module SessionTestHelper
- 1
def parsed_cookies
- 5
ActionDispatch::Cookies::CookieJar.build(request, cookies.to_hash)
end
- 1
def sign_in(user)
- 107
user = users(user) unless user.is_a? User
- 107
post session_url, params: { email_address: user.email_address, password: "secret123456" }
- 107
assert cookies[:session_token].present?
end
end
- 1
module TurboTestHelper
- 1
def assert_rendered_turbo_stream_broadcast(*streambles, action:, target:, &block)
- 1
streams = find_broadcasts_for(*streambles)
- 1
target = ActionView::RecordIdentifier.dom_id(*target)
- 1
assert_select Nokogiri::HTML.fragment(streams), %(turbo-stream[action="#{action}"][target="#{target}"]), &block
end
- 1
private
- 1
def find_broadcasts_for(*streambles)
- 1
broadcasting = streambles.collect do |streamble|
- 2
streamble.try(:to_gid_param) || streamble
end.join(":")
- 1
broadcasts = ActionCable.server.pubsub.broadcasts(broadcasting)
- 2
broadcasts.collect { |b| JSON.parse(b) }.join("\n\n")
end
end