diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/devise/ldap_authenticatable.rb | 49 | ||||
-rw-r--r-- | lib/mastodon/premailer_webpack_strategy.rb | 19 | ||||
-rw-r--r-- | lib/mastodon/version.rb | 4 | ||||
-rw-r--r-- | lib/paperclip/gif_transcoder.rb | 2 | ||||
-rw-r--r-- | lib/paperclip/lazy_thumbnail.rb | 28 | ||||
-rw-r--r-- | lib/tasks/assets.rake | 8 | ||||
-rw-r--r-- | lib/tasks/mastodon.rake | 432 |
7 files changed, 501 insertions, 41 deletions
diff --git a/lib/devise/ldap_authenticatable.rb b/lib/devise/ldap_authenticatable.rb new file mode 100644 index 000000000..531abdbbe --- /dev/null +++ b/lib/devise/ldap_authenticatable.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +if ENV['LDAP_ENABLED'] == 'true' + require 'net/ldap' + require 'devise/strategies/authenticatable' + + module Devise + module Strategies + class LdapAuthenticatable < Authenticatable + def authenticate! + if params[:user] + ldap = Net::LDAP.new( + host: Devise.ldap_host, + port: Devise.ldap_port, + base: Devise.ldap_base, + encryption: { + method: Devise.ldap_method, + tls_options: OpenSSL::SSL::SSLContext::DEFAULT_PARAMS, + }, + auth: { + method: :simple, + username: Devise.ldap_bind_dn, + password: Devise.ldap_password, + }, + connect_timeout: 10 + ) + + if (user_info = ldap.bind_as(base: Devise.ldap_base, filter: "(#{Devise.ldap_uid}=#{email})", password: password)) + user = User.ldap_get_user(user_info.first) + success!(user) + else + return fail(:invalid_login) + end + end + end + + def email + params[:user][:email] + end + + def password + params[:user][:password] + end + end + end + end + + Warden::Strategies.add(:ldap_authenticatable, Devise::Strategies::LdapAuthenticatable) +end diff --git a/lib/mastodon/premailer_webpack_strategy.rb b/lib/mastodon/premailer_webpack_strategy.rb index 84d83cc66..56ef09c1a 100644 --- a/lib/mastodon/premailer_webpack_strategy.rb +++ b/lib/mastodon/premailer_webpack_strategy.rb @@ -2,16 +2,21 @@ module PremailerWebpackStrategy def load(url) - public_path_host = ENV['ASSET_HOST'] || ENV['LOCAL_DOMAIN'] - url = url.gsub(/\A\/\/#{public_path_host}/, '') + asset_host = ENV['CDN_HOST'] || ENV['WEB_DOMAIN'] || ENV['LOCAL_DOMAIN'] if Webpacker.dev_server.running? - url = File.join("#{Webpacker.dev_server.protocol}://#{Webpacker.dev_server.host_with_port}", url) - HTTP.get(url).to_s - else - url = url[1..-1] if url.start_with?('/') - File.read(Rails.root.join('public', url)) + asset_host = "#{Webpacker.dev_server.protocol}://#{Webpacker.dev_server.host_with_port}" + url = File.join(asset_host, url) end + + css = if url.start_with?('http') + HTTP.get(url).to_s + else + url = url[1..-1] if url.start_with?('/') + File.read(Rails.root.join('public', url)) + end + + css.gsub(/url\(\//, "url(#{asset_host}/") end module_function :load diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 098ab46ac..3ba96731d 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -9,7 +9,7 @@ module Mastodon end def minor - 2 + 3 end def patch @@ -21,7 +21,7 @@ module Mastodon end def flags - 'rc1' + '' end def to_a diff --git a/lib/paperclip/gif_transcoder.rb b/lib/paperclip/gif_transcoder.rb index 629e37581..62787983c 100644 --- a/lib/paperclip/gif_transcoder.rb +++ b/lib/paperclip/gif_transcoder.rb @@ -16,7 +16,7 @@ module Paperclip final_file = Paperclip::Transcoder.make(file, options, attachment) - attachment.instance.file_file_name = 'media.mp4' + attachment.instance.file_file_name = File.basename(attachment.instance.file_file_name, '.*') + '.mp4' attachment.instance.file_content_type = 'video/mp4' attachment.instance.type = MediaAttachment.types[:gifv] diff --git a/lib/paperclip/lazy_thumbnail.rb b/lib/paperclip/lazy_thumbnail.rb new file mode 100644 index 000000000..aafa21343 --- /dev/null +++ b/lib/paperclip/lazy_thumbnail.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Paperclip + class LazyThumbnail < Paperclip::Thumbnail + def make + return File.open(@file.path) unless needs_convert? + + min_side = [@current_geometry.width, @current_geometry.height].min + options[:geometry] = "#{min_side.to_i}x#{min_side.to_i}#" if @target_geometry.square? && min_side < @target_geometry.width + + Paperclip::Thumbnail.make(file, options, attachment) + end + + private + + def needs_convert? + needs_different_geometry? || needs_different_format? + end + + def needs_different_geometry? + !@target_geometry.nil? && @current_geometry.width != @target_geometry.width && @current_geometry.height != @target_geometry.height + end + + def needs_different_format? + @format.present? && @current_format != @format + end + end +end diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake index f60c1b9f2..b642510a1 100644 --- a/lib/tasks/assets.rake +++ b/lib/tasks/assets.rake @@ -1,15 +1,13 @@ # frozen_string_literal: true def render_static_page(action, dest:, **opts) - I18n.with_locale(ENV['DEFAULT_LOCALE'] || I18n.default_locale) do - html = ApplicationController.render(action, opts) - File.write(dest, html) - end + html = ApplicationController.render(action, opts) + File.write(dest, html) end namespace :assets do desc 'Generate static pages' - task :generate_static_pages do + task generate_static_pages: :environment do render_static_page 'errors/500', layout: 'error', dest: Rails.root.join('public', 'assets', '500.html') end end diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index 486c035de..d6c9e2d01 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -4,6 +4,370 @@ require 'optparse' require 'colorize' namespace :mastodon do + desc 'Configure the instance for production use' + task :setup do + prompt = TTY::Prompt.new + env = {} + + begin + prompt.say('Your instance is identified by its domain name. Changing it afterward will break things.') + env['LOCAL_DOMAIN'] = prompt.ask('Domain name:') do |q| + q.required true + q.modify :strip + q.validate(/\A[a-z0-9\.\-]+\z/i) + q.messages[:valid?] = 'Invalid domain. If you intend to use unicode characters, enter punycode here' + end + + prompt.say "\n" + + prompt.say('Single user mode disables registrations and redirects the landing page to your public profile.') + env['SINGLE_USER_MODE'] = prompt.yes?('Do you want to enable single user mode?', default: false) + + %w(SECRET_KEY_BASE OTP_SECRET).each do |key| + env[key] = SecureRandom.hex(64) + end + + vapid_key = Webpush.generate_key + + env['VAPID_PRIVATE_KEY'] = vapid_key.private_key + env['VAPID_PUBLIC_KEY'] = vapid_key.public_key + + prompt.say "\n" + + using_docker = prompt.yes?('Are you using Docker to run Mastodon?') + db_connection_works = false + + prompt.say "\n" + + loop do + env['DB_HOST'] = prompt.ask('PostgreSQL host:') do |q| + q.required true + q.default using_docker ? 'db' : '/var/run/postgresql' + q.modify :strip + end + + env['DB_PORT'] = prompt.ask('PostgreSQL port:') do |q| + q.required true + q.default 5432 + q.convert :int + end + + env['DB_NAME'] = prompt.ask('Name of PostgreSQL database:') do |q| + q.required true + q.default using_docker ? 'postgres' : 'mastodon_production' + q.modify :strip + end + + env['DB_USER'] = prompt.ask('Name of PostgreSQL user:') do |q| + q.required true + q.default using_docker ? 'postgres' : 'mastodon' + q.modify :strip + end + + env['DB_PASS'] = prompt.ask('Password of PostgreSQL user:') do |q| + q.echo false + end + + # The chosen database may not exist yet. Connect to default database + # to avoid "database does not exist" error. + db_options = { + adapter: :postgresql, + database: 'postgres', + host: env['DB_HOST'], + port: env['DB_PORT'], + user: env['DB_USER'], + password: env['DB_PASS'], + } + + begin + ActiveRecord::Base.establish_connection(db_options) + ActiveRecord::Base.connection + prompt.ok 'Database configuration works! 🎆' + db_connection_works = true + break + rescue StandardError => e + prompt.error 'Database connection could not be established with this configuration, try again.' + prompt.error e.message + break unless prompt.yes?('Try again?') + end + end + + prompt.say "\n" + + loop do + env['REDIS_HOST'] = prompt.ask('Redis host:') do |q| + q.required true + q.default using_docker ? 'redis' : 'localhost' + q.modify :strip + end + + env['REDIS_PORT'] = prompt.ask('Redis port:') do |q| + q.required true + q.default 6379 + q.convert :int + end + + redis_options = { + host: env['REDIS_HOST'], + port: env['REDIS_PORT'], + driver: :hiredis, + } + + begin + redis = Redis.new(redis_options) + redis.ping + prompt.ok 'Redis configuration works! 🎆' + break + rescue StandardError => e + prompt.error 'Redis connection could not be established with this configuration, try again.' + prompt.error e.message + break unless prompt.yes?('Try again?') + end + end + + prompt.say "\n" + + if prompt.yes?('Do you want to store uploaded files on the cloud?', default: false) + case prompt.select('Provider', ['Amazon S3', 'Wasabi', 'Minio']) + when 'Amazon S3' + env['S3_ENABLED'] = 'true' + env['S3_PROTOCOL'] = 'https' + + env['S3_BUCKET'] = prompt.ask('S3 bucket name:') do |q| + q.required true + q.default "files.#{env['LOCAL_DOMAIN']}" + q.modify :strip + end + + env['S3_REGION'] = prompt.ask('S3 region:') do |q| + q.required true + q.default 'us-east-1' + q.modify :strip + end + + env['S3_HOSTNAME'] = prompt.ask('S3 hostname:') do |q| + q.required true + q.default 's3-us-east-1.amazonaws.com' + q.modify :strip + end + + env['AWS_ACCESS_KEY_ID'] = prompt.ask('S3 access key:') do |q| + q.required true + q.modify :strip + end + + env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('S3 secret key:') do |q| + q.required true + q.modify :strip + end + when 'Wasabi' + env['S3_ENABLED'] = 'true' + env['S3_PROTOCOL'] = 'https' + env['S3_REGION'] = 'us-east-1' + env['S3_HOSTNAME'] = 's3.wasabisys.com' + env['S3_ENDPOINT'] = 'https://s3.wasabisys.com/' + + env['S3_BUCKET'] = prompt.ask('Wasabi bucket name:') do |q| + q.required true + q.default "files.#{env['LOCAL_DOMAIN']}" + q.modify :strip + end + + env['AWS_ACCESS_KEY_ID'] = prompt.ask('Wasabi access key:') do |q| + q.required true + q.modify :strip + end + + env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('Wasabi secret key:') do |q| + q.required true + q.modify :strip + end + when 'Minio' + env['S3_ENABLED'] = 'true' + env['S3_PROTOCOL'] = 'https' + env['S3_REGION'] = 'us-east-1' + + env['S3_ENDPOINT'] = prompt.ask('Minio endpoint URL:') do |q| + q.required true + q.modify :strip + end + + env['S3_PROTOCOL'] = env['S3_ENDPOINT'].start_with?('https') ? 'https' : 'http' + env['S3_HOSTNAME'] = env['S3_ENDPOINT'].gsub(/\Ahttps?:\/\//, '') + + env['S3_BUCKET'] = prompt.ask('Minio bucket name:') do |q| + q.required true + q.default "files.#{env['LOCAL_DOMAIN']}" + q.modify :strip + end + + env['AWS_ACCESS_KEY_ID'] = prompt.ask('Minio access key:') do |q| + q.required true + q.modify :strip + end + + env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('Minio secret key:') do |q| + q.required true + q.modify :strip + end + end + + if prompt.yes?('Do you want to access the uploaded files from your own domain?') + env['S3_CLOUDFRONT_HOST'] = prompt.ask('Domain for uploaded files:') do |q| + q.required true + q.default "files.#{env['LOCAL_DOMAIN']}" + q.modify :strip + end + end + end + + prompt.say "\n" + + loop do + env['SMTP_SERVER'] = prompt.ask('SMTP server:') do |q| + q.required true + q.default 'smtp.mailgun.org' + q.modify :strip + end + + env['SMTP_PORT'] = prompt.ask('SMTP port:') do |q| + q.required true + q.default 587 + q.convert :int + end + + env['SMTP_LOGIN'] = prompt.ask('SMTP username:') do |q| + q.modify :strip + end + + env['SMTP_PASSWORD'] = prompt.ask('SMTP password:') do |q| + q.echo false + end + + env['SMTP_FROM_ADDRESS'] = prompt.ask('E-mail address to send e-mails "from":') do |q| + q.required true + q.default "Mastodon <notifications@#{env['LOCAL_DOMAIN']}>" + q.modify :strip + end + + break unless prompt.yes?('Send a test e-mail with this configuration right now?') + + send_to = prompt.ask('Send test e-mail to:', required: true) + + begin + ActionMailer::Base.smtp_settings = { + :port => env['SMTP_PORT'], + :address => env['SMTP_SERVER'], + :user_name => env['SMTP_LOGIN'].presence, + :password => env['SMTP_PASSWORD'].presence, + :domain => env['LOCAL_DOMAIN'], + :authentication => :plain, + :enable_starttls_auto => true, + } + + ActionMailer::Base.default_options = { + from: env['SMTP_FROM_ADDRESS'], + } + + mail = ActionMailer::Base.new.mail to: send_to, subject: 'Test', body: 'Mastodon SMTP configuration works!' + mail.deliver + rescue StandardError => e + prompt.error 'E-mail could not be sent with this configuration, try again.' + prompt.error e.message + break unless prompt.yes?('Try again?') + end + end + + prompt.say "\n" + prompt.say 'This configuration will be written to .env.production' + + if prompt.yes?('Save configuration?') + cmd = TTY::Command.new(printer: :quiet) + + File.write(Rails.root.join('.env.production'), "# Generated with mastodon:setup on #{Time.now.utc}\n\n" + env.each_pair.map { |key, value| "#{key}=#{value}" }.join("\n") + "\n") + + if using_docker + prompt.ok 'Below is your configuration, save it to an .env.production file outside Docker:' + prompt.say "\n" + prompt.say File.read(Rails.root.join('.env.production')) + prompt.say "\n" + prompt.ok 'It is also saved within this container so you can proceed with this wizard.' + end + + prompt.say "\n" + prompt.say 'Now that configuration is saved, the database schema must be loaded.' + prompt.warn 'If the database already exists, this will erase its contents.' + + if prompt.yes?('Prepare the database now?') + prompt.say 'Running `RAILS_ENV=production rails db:setup` ...' + prompt.say "\n" + + if cmd.run!({ RAILS_ENV: 'production' }, :rails, 'db:setup').failure? + prompt.say "\n" + prompt.error 'That failed! Perhaps your configuration is not right' + else + prompt.say "\n" + prompt.ok 'Done!' + end + end + + prompt.say "\n" + prompt.say 'The final step is compiling CSS/JS assets.' + prompt.say 'This may take a while and consume a lot of RAM.' + + if prompt.yes?('Compile the assets now?') + prompt.say 'Running `RAILS_ENV=production rails assets:precompile` ...' + prompt.say "\n" + + if cmd.run!({ RAILS_ENV: 'production' }, :rails, 'assets:precompile').failure? + prompt.say "\n" + prompt.error 'That failed! Maybe you need swap space?' + else + prompt.say "\n" + prompt.say 'Done!' + end + end + + prompt.say "\n" + prompt.ok 'All done! You can now power on the Mastodon server 🐘' + prompt.say "\n" + + if db_connection_works && prompt.yes?('Do you want to create an admin user straight away?') + env.each_pair do |key, value| + ENV[key] = value.to_s + end + + require_relative '../../config/environment' + disable_log_stdout! + + username = prompt.ask('Username:') do |q| + q.required true + q.default 'admin' + q.validate(/\A[a-z0-9_]+\z/i) + q.modify :strip + end + + email = prompt.ask('E-mail:') do |q| + q.required true + q.modify :strip + end + + password = SecureRandom.hex(16) + + user = User.new(admin: true, email: email, password: password, confirmed_at: Time.now.utc, account_attributes: { username: username }) + user.save(validate: false) + + prompt.ok "You can login with the password: #{password}" + prompt.warn 'You can change your password once you login.' + end + else + prompt.warn 'Nothing saved. Bye!' + end + rescue TTY::Reader::InputInterrupt + prompt.ok 'Aborting. Bye!' + end + end + desc 'Execute daily tasks (deprecated)' task :daily do # No-op @@ -67,32 +431,40 @@ namespace :mastodon do desc 'Add a user by providing their email, username and initial password.' \ 'The user will receive a confirmation email, then they must reset their password before logging in.' task add_user: :environment do - print 'Enter email: ' - email = STDIN.gets.chomp - - print 'Enter username: ' - username = STDIN.gets.chomp - - print 'Create user and send them confirmation mail [y/N]: ' - confirm = STDIN.gets.chomp - puts - - if confirm.casecmp('y').zero? - password = SecureRandom.hex - user = User.new(email: email, password: password, account_attributes: { username: username }) - if user.save - puts 'User added and confirmation mail sent to user\'s email address.' - puts "Here is the random password generated for the user: #{password}" - else - puts 'Following errors occured while creating new user:' - user.errors.each do |key, val| - puts "#{key}: #{val}" + disable_log_stdout! + + prompt = TTY::Prompt.new + + begin + email = prompt.ask('E-mail:', required: true) do |q| + q.modify :strip + end + + username = prompt.ask('Username:', required: true) do |q| + q.modify :strip + end + + role = prompt.select('Role:', %w(user moderator admin)) + + if prompt.yes?('Proceed to create the user?') + user = User.new(email: email, password: SecureRandom.hex, admin: role == 'admin', moderator: role == 'moderator', account_attributes: { username: username }) + + if user.save + prompt.ok 'User created and confirmation mail sent to the user\'s email address.' + prompt.ok "Here is the random password generated for the user: #{password}" + else + prompt.warn 'User was not created because of the following errors:' + + user.errors.each do |key, val| + prompt.error "#{key}: #{val}" + end end + else + prompt.ok 'Aborting. Bye!' end - else - puts 'Aborted by user.' + rescue TTY::Reader::InputInterrupt + prompt.ok 'Aborting. Bye!' end - puts end namespace :media do @@ -112,6 +484,8 @@ namespace :mastodon do time_ago = ENV.fetch('NUM_DAYS') { 7 }.to_i.days.ago MediaAttachment.where.not(remote_url: '').where.not(file_file_name: nil).where('created_at < ?', time_ago).find_each do |media| + next unless media.file.exists? + media.file.destroy media.save end @@ -130,9 +504,13 @@ namespace :mastodon do accounts = accounts.where(domain: ENV['DOMAIN']) if ENV['DOMAIN'].present? accounts.find_each do |account| - account.reset_avatar! - account.reset_header! - account.save + begin + account.reset_avatar! + account.reset_header! + account.save + rescue Paperclip::Error + puts "Error resetting avatar and header for account #{username}@#{domain}" + end end end end @@ -388,6 +766,7 @@ namespace :mastodon do if [404, 410].include?(res.code) if options[:force] + SuspendAccountService.new.call(account) account.destroy else progress_bar.pause @@ -400,6 +779,7 @@ namespace :mastodon do if confirm.casecmp('n').zero? next else + SuspendAccountService.new.call(account) account.destroy end end |