@ -4,6 +4,362 @@ require 'optparse'
require 'colorize'
require 'colorize'
namespace :mastodon do
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 PAPERCLIP_SECRET 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 " )
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)'
desc 'Execute daily tasks (deprecated)'
task :daily do
task :daily do
# No-op
# No-op
@ -67,32 +423,40 @@ namespace :mastodon do
desc 'Add a user by providing their email, username and initial password.' \
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.'
'The user will receive a confirmation email, then they must reset their password before logging in.'
task add_user : :environment do
task add_user : :environment do
print 'Enter email: '
disable_log_stdout!
email = STDIN . gets . chomp
print 'Enter username: '
prompt = TTY :: Prompt . new
username = STDIN . gets . chomp
print 'Create user and send them confirmation mail [y/N]: '
begin
confirm = STDIN . gets . chomp
email = prompt . ask ( 'E-mail:' , required : true ) do | q |
puts
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 confirm . casecmp ( 'y' ) . zero?
password = SecureRandom . hex
user = User . new ( email : email , password : password , account_attributes : { username : username } )
if user . save
if user . save
puts 'User added and confirmation mail sent to user\'s email address.'
prompt . ok 'User created and confirmation mail sent to the user\'s email address.'
puts " Here is the random password generated for the user: #{ password } "
prompt . ok " Here is the random password generated for the user: #{ password } "
else
else
puts 'Following errors occured while creating new user:'
prompt . warn 'User was not created because of the following errors:'
user . errors . each do | key , val |
user . errors . each do | key , val |
puts " #{ key } : #{ val } "
prompt . error " #{ key } : #{ val } "
end
end
end
end
else
else
puts 'Aborted by user.'
prompt . ok 'Aborting. Bye!'
end
rescue TTY :: Reader :: InputInterrupt
prompt . ok 'Aborting. Bye!'
end
end
puts
end
end
namespace :media do
namespace :media do