diff --git a/Dockerfile b/Dockerfile index b2eb0ac..65fab53 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,7 +45,8 @@ FROM base # Install packages needed for deployment RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y curl libsqlite3-0 libvips imagemagick fonts-liberation sqlite3 libsqlite3-dev && \ + apt-get install --no-install-recommends -y curl libsqlite3-0 libvips imagemagick fonts-liberation sqlite3 libsqlite3-dev \ + fonts-freefont-ttf fonts-dejavu fontconfig && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives # Copy built artifacts: gems, application, and node modules diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 7e80a28..efafe81 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -17,7 +17,11 @@ def create if resource.active_for_authentication? set_flash_message! :notice, :signed_up sign_up(resource_name, resource) - UserMailer.welcome_email(resource).deliver_now + begin + UserMailer.welcome_email(resource).deliver_later + rescue => e + Rails.logger.error("Failed to enqueue welcome email for user #{resource.id}: #{e.message}") + end respond_with resource, location: after_sign_up_path_for(resource) else set_flash_message! :notice, :"signed_up_but_#{resource.inactive_message}" @@ -29,6 +33,10 @@ def create set_minimum_password_length respond_with resource end + rescue => e + Rails.logger.error("Error during user registration: #{e.message}") + flash[:error] = "An error occurred during registration. Please try again." + redirect_to new_user_registration_path end def edit @@ -75,6 +83,10 @@ def update render :edit end end + rescue => e + Rails.logger.error("Error during user update: #{e.message}") + flash[:error] = "An error occurred while updating your profile. Please try again." + render :edit end private diff --git a/app/jobs/generate_open_graph_image_job.rb b/app/jobs/generate_open_graph_image_job.rb new file mode 100644 index 0000000..d950979 --- /dev/null +++ b/app/jobs/generate_open_graph_image_job.rb @@ -0,0 +1,12 @@ +class GenerateOpenGraphImageJob < ApplicationJob + queue_as :default + + def perform(user_id) + user = User.find_by(id: user_id) + return unless user + + OpenGraphImageGenerator.new(user).generate + rescue StandardError => e + Rails.logger.error("Failed to generate OG image for user #{user_id}: #{e.message}") + end +end \ No newline at end of file diff --git a/app/models/user.rb b/app/models/user.rb index c3f0152..c6d7fa3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -23,7 +23,7 @@ class User < ApplicationRecord before_validation :ensure_username_presence before_create :set_default_images - after_create :generate_open_graph_image, unless: -> { Rails.env.test? } + after_create :generate_open_graph_image_async, unless: -> { Rails.env.test? } before_save :process_avatar, if: :will_save_change_to_avatar? before_save :process_banner, if: :will_save_change_to_banner? @@ -62,6 +62,10 @@ def valid_url?(url) private + def generate_open_graph_image_async + GenerateOpenGraphImageJob.perform_later(self.id) + end + def ensure_username_presence if username.blank? self.username = email.present? ? email.split('@').first : "user#{SecureRandom.hex(4)}" diff --git a/app/services/open_graph_image_generator.rb b/app/services/open_graph_image_generator.rb index 357a7e5..b486c7d 100644 --- a/app/services/open_graph_image_generator.rb +++ b/app/services/open_graph_image_generator.rb @@ -2,7 +2,6 @@ class OpenGraphImageGenerator # Constants for sizes and paths AVATAR_SIZE = 400 BORDER_SIZE = 5 - FONT = 'Courier' # Use a standard block print font def initialize(user) @user = user @@ -14,91 +13,96 @@ def generate FileUtils.mkdir_p(output_dir) unless File.directory?(output_dir) output_path = output_dir.join("#{@user.username}_og.png") - image = MiniMagick::Image.open(template_path) + begin + image = MiniMagick::Image.open(template_path) - # Determine whether to use fallback avatar or download the provided one - if @user.avatar.blank? || !valid_image_url?(@user.avatar_url) - avatar = MiniMagick::Image.open(Rails.root.join('public', 'avatars', 'default_avatar.jpg')) - else - avatar = download_image(@user.avatar_url) - end + # Determine whether to use fallback avatar or download the provided one + if @user.avatar.blank? || !valid_image_url?(@user.avatar_url) + avatar = MiniMagick::Image.open(Rails.root.join('public', 'avatars', 'default_avatar.jpg')) + else + avatar = download_image(@user.avatar_url) + end - # Resize avatar and add border - avatar.resize "#{AVATAR_SIZE}x#{AVATAR_SIZE}" - avatar.combine_options do |c| - c.bordercolor 'white' - c.border BORDER_SIZE - end + # Resize avatar and add border + avatar.resize "#{AVATAR_SIZE}x#{AVATAR_SIZE}" + avatar.combine_options do |c| + c.bordercolor 'white' + c.border BORDER_SIZE + end - # Prepare text elements - tag_text = @user.parsed_tags.present? ? @user.parsed_tags.join(' | ') : "" - full_name = @user.full_name - username = "@#{@user.username}" + # Prepare text elements + tag_text = @user.parsed_tags.present? ? @user.parsed_tags.join(' | ') : "" + full_name = @user.full_name + username = "@#{@user.username}" - # Define point sizes - name_pointsize = 40 - username_pointsize = 28 - tag_pointsize = 20 + # Define point sizes + name_pointsize = 40 + username_pointsize = 28 + tag_pointsize = 20 - # Spacing between elements - spacing = 10 + # Spacing between elements + spacing = 10 - # Estimate text heights (approximated as 1.2 times point size) - name_text_height = name_pointsize * 1.2 - username_text_height = username_pointsize * 1.2 - tag_text_height = tag_pointsize * 1.2 if tag_text.present? + # Estimate text heights (approximated as 1.2 times point size) + name_text_height = name_pointsize * 1.2 + username_text_height = username_pointsize * 1.2 + tag_text_height = tag_pointsize * 1.2 if tag_text.present? - # Total content height calculation - total_height = (AVATAR_SIZE + 2 * BORDER_SIZE) + spacing + - name_text_height + spacing + - username_text_height + # Total content height calculation + total_height = (AVATAR_SIZE + 2 * BORDER_SIZE) + spacing + + name_text_height + spacing + + username_text_height - total_height += spacing + tag_text_height if tag_text.present? + total_height += spacing + tag_text_height if tag_text.present? - # Calculate starting y-position to center content vertically - template_height = image.height - content_start_y = ((template_height - total_height) / 2).to_i + # Calculate starting y-position to center content vertically + template_height = image.height + content_start_y = ((template_height - total_height) / 2).to_i - # Position elements - current_y = content_start_y + # Position elements + current_y = content_start_y - # Add avatar to the image, centered horizontally - image = image.composite(avatar) do |c| - c.gravity 'North' # Align from the top - c.geometry "+0+#{current_y}" - end + # Add avatar to the image, centered horizontally + image = image.composite(avatar) do |c| + c.gravity 'North' # Align from the top + c.geometry "+0+#{current_y}" + end - current_y += (AVATAR_SIZE + 2 * BORDER_SIZE) + spacing + current_y += (AVATAR_SIZE + 2 * BORDER_SIZE) + spacing - # Add text to the image - image.combine_options do |c| - c.gravity 'North' # Align from the top - c.font FONT # Set the font to 'Courier' + # Add text to the image + image.combine_options do |c| + c.gravity 'North' # Align from the top + c.font 'Arial' # Use a common system font - # Add full name - c.fill '#BEF264' - c.pointsize name_pointsize.to_s - c.draw "text 0,#{current_y} '#{escape_text(full_name)}'" + # Add full name + c.fill '#BEF264' + c.pointsize name_pointsize.to_s + c.draw "text 0,#{current_y} '#{escape_text(full_name)}'" - current_y += name_text_height + spacing + current_y += name_text_height + spacing - # Add username - c.pointsize username_pointsize.to_s - c.draw "text 0,#{current_y} '#{escape_text(username)}'" + # Add username + c.pointsize username_pointsize.to_s + c.draw "text 0,#{current_y} '#{escape_text(username)}'" - current_y += username_text_height + spacing + current_y += username_text_height + spacing - # Add tags if present - if tag_text.present? - c.fill 'white' - c.pointsize tag_pointsize.to_s - c.draw "text 0,#{current_y} '#{escape_text(tag_text)}'" + # Add tags if present + if tag_text.present? + c.fill 'white' + c.pointsize tag_pointsize.to_s + c.draw "text 0,#{current_y} '#{escape_text(tag_text)}'" + end end - end - # Save the generated image - image.write(output_path) - output_path + # Save the generated image + image.write(output_path) + output_path + rescue StandardError => e + Rails.logger.error("Failed to generate OG image for user #{@user.id}: #{e.message}") + nil # Return nil to indicate failure without raising an exception + end end private @@ -147,4 +151,4 @@ def download_image(url) def escape_text(text) text.gsub("'", "\\\\'") end -end +end \ No newline at end of file diff --git a/spec/controllers/users/registrations_controller_spec.rb b/spec/controllers/users/registrations_controller_spec.rb index c819767..3155b9f 100644 --- a/spec/controllers/users/registrations_controller_spec.rb +++ b/spec/controllers/users/registrations_controller_spec.rb @@ -70,10 +70,10 @@ end end - it "sends a welcome email after successful registration" do + it "enqueues a welcome email after successful registration" do expect { post :create, params: { user: attributes_for(:user, invite_code: "POWEROVERWHELMING") } - }.to change { ActionMailer::Base.deliveries.count }.by(1) + }.to have_enqueued_mail(UserMailer, :welcome_email) end it "handles tags correctly" do diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 9eca46b..b40b901 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -28,6 +28,9 @@ config.include FactoryBot::Syntax::Methods + # Add this line to include ActiveJob test helpers + config.include ActiveJob::TestHelper + # Precompile assets before running tests config.before(:suite) do Rails.application.load_tasks @@ -51,4 +54,4 @@ Capybara::Selenium::Driver.new(app, browser: :chrome, options: options) end -Capybara.javascript_driver = :chrome_headless +Capybara.javascript_driver = :chrome_headless \ No newline at end of file