mastodon.rake 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. # frozen_string_literal: true
  2. require 'tty-prompt'
  3. namespace :mastodon do
  4. desc 'Configure the instance for production use'
  5. task :setup do
  6. prompt = TTY::Prompt.new
  7. env = {}
  8. # When the application code gets loaded, it runs `lib/mastodon/redis_configuration.rb`.
  9. # This happens before application environment configuration and sets REDIS_URL etc.
  10. # These variables are then used even when REDIS_HOST etc. are changed, so clear them
  11. # out so they don't interfer with our new configuration.
  12. ENV.delete('REDIS_URL')
  13. ENV.delete('CACHE_REDIS_URL')
  14. ENV.delete('SIDEKIQ_REDIS_URL')
  15. begin
  16. prompt.say('Your instance is identified by its domain name. Changing it afterward will break things.')
  17. env['LOCAL_DOMAIN'] = prompt.ask('Domain name:') do |q|
  18. q.required true
  19. q.modify :strip
  20. q.validate(/\A[a-z0-9\.\-]+\z/i)
  21. q.messages[:valid?] = 'Invalid domain. If you intend to use unicode characters, enter punycode here'
  22. end
  23. prompt.say "\n"
  24. prompt.say('Single user mode disables registrations and redirects the landing page to your public profile.')
  25. env['SINGLE_USER_MODE'] = prompt.yes?('Do you want to enable single user mode?', default: false)
  26. %w(SECRET_KEY_BASE OTP_SECRET).each do |key|
  27. env[key] = SecureRandom.hex(64)
  28. end
  29. vapid_key = Webpush.generate_key
  30. env['VAPID_PRIVATE_KEY'] = vapid_key.private_key
  31. env['VAPID_PUBLIC_KEY'] = vapid_key.public_key
  32. prompt.say "\n"
  33. using_docker = prompt.yes?('Are you using Docker to run Mastodon?')
  34. db_connection_works = false
  35. prompt.say "\n"
  36. loop do
  37. env['DB_HOST'] = prompt.ask('PostgreSQL host:') do |q|
  38. q.required true
  39. q.default using_docker ? 'db' : '/var/run/postgresql'
  40. q.modify :strip
  41. end
  42. env['DB_PORT'] = prompt.ask('PostgreSQL port:') do |q|
  43. q.required true
  44. q.default 5432
  45. q.convert :int
  46. end
  47. env['DB_NAME'] = prompt.ask('Name of PostgreSQL database:') do |q|
  48. q.required true
  49. q.default using_docker ? 'postgres' : 'mastodon_production'
  50. q.modify :strip
  51. end
  52. env['DB_USER'] = prompt.ask('Name of PostgreSQL user:') do |q|
  53. q.required true
  54. q.default using_docker ? 'postgres' : 'mastodon'
  55. q.modify :strip
  56. end
  57. env['DB_PASS'] = prompt.ask('Password of PostgreSQL user:') do |q|
  58. q.echo false
  59. end
  60. # The chosen database may not exist yet. Connect to default database
  61. # to avoid "database does not exist" error.
  62. db_options = {
  63. adapter: :postgresql,
  64. database: 'postgres',
  65. host: env['DB_HOST'],
  66. port: env['DB_PORT'],
  67. user: env['DB_USER'],
  68. password: env['DB_PASS'],
  69. }
  70. begin
  71. ActiveRecord::Base.establish_connection(db_options)
  72. ActiveRecord::Base.connection
  73. prompt.ok 'Database configuration works! 🎆'
  74. db_connection_works = true
  75. break
  76. rescue StandardError => e
  77. prompt.error 'Database connection could not be established with this configuration, try again.'
  78. prompt.error e.message
  79. break unless prompt.yes?('Try again?')
  80. end
  81. end
  82. prompt.say "\n"
  83. loop do
  84. env['REDIS_HOST'] = prompt.ask('Redis host:') do |q|
  85. q.required true
  86. q.default using_docker ? 'redis' : 'localhost'
  87. q.modify :strip
  88. end
  89. env['REDIS_PORT'] = prompt.ask('Redis port:') do |q|
  90. q.required true
  91. q.default 6379
  92. q.convert :int
  93. end
  94. env['REDIS_PASSWORD'] = prompt.ask('Redis password:') do |q|
  95. q.required false
  96. q.default nil
  97. q.modify :strip
  98. end
  99. redis_options = {
  100. host: env['REDIS_HOST'],
  101. port: env['REDIS_PORT'],
  102. password: env['REDIS_PASSWORD'],
  103. driver: :hiredis,
  104. }
  105. begin
  106. redis = Redis.new(redis_options)
  107. redis.ping
  108. prompt.ok 'Redis configuration works! 🎆'
  109. break
  110. rescue StandardError => e
  111. prompt.error 'Redis connection could not be established with this configuration, try again.'
  112. prompt.error e.message
  113. break unless prompt.yes?('Try again?')
  114. end
  115. end
  116. prompt.say "\n"
  117. if prompt.yes?('Do you want to store uploaded files on the cloud?', default: false)
  118. case prompt.select('Provider', ['Amazon S3', 'Wasabi', 'Minio', 'Google Cloud Storage'])
  119. when 'Amazon S3'
  120. env['S3_ENABLED'] = 'true'
  121. env['S3_PROTOCOL'] = 'https'
  122. env['S3_BUCKET'] = prompt.ask('S3 bucket name:') do |q|
  123. q.required true
  124. q.default "files.#{env['LOCAL_DOMAIN']}"
  125. q.modify :strip
  126. end
  127. env['S3_REGION'] = prompt.ask('S3 region:') do |q|
  128. q.required true
  129. q.default 'us-east-1'
  130. q.modify :strip
  131. end
  132. env['S3_HOSTNAME'] = prompt.ask('S3 hostname:') do |q|
  133. q.required true
  134. q.default 's3-us-east-1.amazonaws.com'
  135. q.modify :strip
  136. end
  137. env['AWS_ACCESS_KEY_ID'] = prompt.ask('S3 access key:') do |q|
  138. q.required true
  139. q.modify :strip
  140. end
  141. env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('S3 secret key:') do |q|
  142. q.required true
  143. q.modify :strip
  144. end
  145. when 'Wasabi'
  146. env['S3_ENABLED'] = 'true'
  147. env['S3_PROTOCOL'] = 'https'
  148. env['S3_REGION'] = 'us-east-1'
  149. env['S3_HOSTNAME'] = 's3.wasabisys.com'
  150. env['S3_ENDPOINT'] = 'https://s3.wasabisys.com/'
  151. env['S3_BUCKET'] = prompt.ask('Wasabi bucket name:') do |q|
  152. q.required true
  153. q.default "files.#{env['LOCAL_DOMAIN']}"
  154. q.modify :strip
  155. end
  156. env['AWS_ACCESS_KEY_ID'] = prompt.ask('Wasabi access key:') do |q|
  157. q.required true
  158. q.modify :strip
  159. end
  160. env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('Wasabi secret key:') do |q|
  161. q.required true
  162. q.modify :strip
  163. end
  164. when 'Minio'
  165. env['S3_ENABLED'] = 'true'
  166. env['S3_PROTOCOL'] = 'https'
  167. env['S3_REGION'] = 'us-east-1'
  168. env['S3_ENDPOINT'] = prompt.ask('Minio endpoint URL:') do |q|
  169. q.required true
  170. q.modify :strip
  171. end
  172. env['S3_PROTOCOL'] = env['S3_ENDPOINT'].start_with?('https') ? 'https' : 'http'
  173. env['S3_HOSTNAME'] = env['S3_ENDPOINT'].gsub(/\Ahttps?:\/\//, '')
  174. env['S3_BUCKET'] = prompt.ask('Minio bucket name:') do |q|
  175. q.required true
  176. q.default "files.#{env['LOCAL_DOMAIN']}"
  177. q.modify :strip
  178. end
  179. env['AWS_ACCESS_KEY_ID'] = prompt.ask('Minio access key:') do |q|
  180. q.required true
  181. q.modify :strip
  182. end
  183. env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('Minio secret key:') do |q|
  184. q.required true
  185. q.modify :strip
  186. end
  187. when 'Google Cloud Storage'
  188. env['S3_ENABLED'] = 'true'
  189. env['S3_PROTOCOL'] = 'https'
  190. env['S3_HOSTNAME'] = 'storage.googleapis.com'
  191. env['S3_ENDPOINT'] = 'https://storage.googleapis.com'
  192. env['S3_MULTIPART_THRESHOLD'] = 50.megabytes
  193. env['S3_BUCKET'] = prompt.ask('GCS bucket name:') do |q|
  194. q.required true
  195. q.default "files.#{env['LOCAL_DOMAIN']}"
  196. q.modify :strip
  197. end
  198. env['S3_REGION'] = prompt.ask('GCS region:') do |q|
  199. q.required true
  200. q.default 'us-west1'
  201. q.modify :strip
  202. end
  203. env['AWS_ACCESS_KEY_ID'] = prompt.ask('GCS access key:') do |q|
  204. q.required true
  205. q.modify :strip
  206. end
  207. env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('GCS secret key:') do |q|
  208. q.required true
  209. q.modify :strip
  210. end
  211. end
  212. if prompt.yes?('Do you want to access the uploaded files from your own domain?')
  213. env['S3_ALIAS_HOST'] = prompt.ask('Domain for uploaded files:') do |q|
  214. q.required true
  215. q.default "files.#{env['LOCAL_DOMAIN']}"
  216. q.modify :strip
  217. end
  218. end
  219. end
  220. prompt.say "\n"
  221. loop do
  222. if prompt.yes?('Do you want to send e-mails from localhost?', default: false)
  223. env['SMTP_SERVER'] = 'localhost'
  224. env['SMTP_PORT'] = 25
  225. env['SMTP_AUTH_METHOD'] = 'none'
  226. env['SMTP_OPENSSL_VERIFY_MODE'] = 'none'
  227. else
  228. env['SMTP_SERVER'] = prompt.ask('SMTP server:') do |q|
  229. q.required true
  230. q.default 'smtp.mailgun.org'
  231. q.modify :strip
  232. end
  233. env['SMTP_PORT'] = prompt.ask('SMTP port:') do |q|
  234. q.required true
  235. q.default 587
  236. q.convert :int
  237. end
  238. env['SMTP_LOGIN'] = prompt.ask('SMTP username:') do |q|
  239. q.modify :strip
  240. end
  241. env['SMTP_PASSWORD'] = prompt.ask('SMTP password:') do |q|
  242. q.echo false
  243. end
  244. env['SMTP_AUTH_METHOD'] = prompt.ask('SMTP authentication:') do |q|
  245. q.required
  246. q.default 'plain'
  247. q.modify :strip
  248. end
  249. env['SMTP_OPENSSL_VERIFY_MODE'] = prompt.select('SMTP OpenSSL verify mode:', %w(none peer client_once fail_if_no_peer_cert))
  250. end
  251. env['SMTP_FROM_ADDRESS'] = prompt.ask('E-mail address to send e-mails "from":') do |q|
  252. q.required true
  253. q.default "Mastodon <notifications@#{env['LOCAL_DOMAIN']}>"
  254. q.modify :strip
  255. end
  256. break unless prompt.yes?('Send a test e-mail with this configuration right now?')
  257. send_to = prompt.ask('Send test e-mail to:', required: true)
  258. begin
  259. ActionMailer::Base.smtp_settings = {
  260. port: env['SMTP_PORT'],
  261. address: env['SMTP_SERVER'],
  262. user_name: env['SMTP_LOGIN'].presence,
  263. password: env['SMTP_PASSWORD'].presence,
  264. domain: env['LOCAL_DOMAIN'],
  265. authentication: env['SMTP_AUTH_METHOD'] == 'none' ? nil : env['SMTP_AUTH_METHOD'] || :plain,
  266. openssl_verify_mode: env['SMTP_OPENSSL_VERIFY_MODE'],
  267. enable_starttls_auto: true,
  268. }
  269. ActionMailer::Base.default_options = {
  270. from: env['SMTP_FROM_ADDRESS'],
  271. }
  272. mail = ActionMailer::Base.new.mail to: send_to, subject: 'Test', body: 'Mastodon SMTP configuration works!'
  273. mail.deliver
  274. break
  275. rescue StandardError => e
  276. prompt.error 'E-mail could not be sent with this configuration, try again.'
  277. prompt.error e.message
  278. break unless prompt.yes?('Try again?')
  279. end
  280. end
  281. prompt.say "\n"
  282. prompt.say 'This configuration will be written to .env.production'
  283. if prompt.yes?('Save configuration?')
  284. incompatible_syntax = false
  285. env_contents = env.each_pair.map do |key, value|
  286. if value.is_a?(String) && value =~ /[\s\#\\"]/
  287. incompatible_syntax = true
  288. if value =~ /[']/
  289. value = value.to_s.gsub(/[\\"\$]/) { |x| "\\#{x}" }
  290. "#{key}=\"#{value}\""
  291. else
  292. "#{key}='#{value}'"
  293. end
  294. else
  295. "#{key}=#{value}"
  296. end
  297. end.join("\n")
  298. generated_header = "# Generated with mastodon:setup on #{Time.now.utc}\n\n".dup
  299. if incompatible_syntax
  300. generated_header << "# Some variables in this file will be interpreted differently whether you are\n"
  301. generated_header << "# using docker-compose or not.\n\n"
  302. end
  303. File.write(Rails.root.join('.env.production'), "#{generated_header}#{env_contents}\n")
  304. if using_docker
  305. prompt.ok 'Below is your configuration, save it to an .env.production file outside Docker:'
  306. prompt.say "\n"
  307. prompt.say "#{generated_header}#{env.each_pair.map { |key, value| "#{key}=#{value}" }.join("\n")}"
  308. prompt.say "\n"
  309. prompt.ok 'It is also saved within this container so you can proceed with this wizard.'
  310. end
  311. prompt.say "\n"
  312. prompt.say 'Now that configuration is saved, the database schema must be loaded.'
  313. prompt.warn 'If the database already exists, this will erase its contents.'
  314. if prompt.yes?('Prepare the database now?')
  315. prompt.say 'Running `RAILS_ENV=production rails db:setup` ...'
  316. prompt.say "\n\n"
  317. if !system(env.transform_values(&:to_s).merge({ 'RAILS_ENV' => 'production', 'SAFETY_ASSURED' => '1' }), 'rails db:setup')
  318. prompt.error 'That failed! Perhaps your configuration is not right'
  319. else
  320. prompt.ok 'Done!'
  321. end
  322. end
  323. unless using_docker
  324. prompt.say "\n"
  325. prompt.say 'The final step is compiling CSS/JS assets.'
  326. prompt.say 'This may take a while and consume a lot of RAM.'
  327. if prompt.yes?('Compile the assets now?')
  328. prompt.say 'Running `RAILS_ENV=production rails assets:precompile` ...'
  329. prompt.say "\n\n"
  330. if !system(env.transform_values(&:to_s).merge({ 'RAILS_ENV' => 'production' }), 'rails assets:precompile')
  331. prompt.error 'That failed! Maybe you need swap space?'
  332. else
  333. prompt.say 'Done!'
  334. end
  335. end
  336. end
  337. prompt.say "\n"
  338. prompt.ok 'All done! You can now power on the Mastodon server 🐘'
  339. prompt.say "\n"
  340. if db_connection_works && prompt.yes?('Do you want to create an admin user straight away?')
  341. env.each_pair do |key, value|
  342. ENV[key] = value.to_s
  343. end
  344. require_relative '../../config/environment'
  345. disable_log_stdout!
  346. username = prompt.ask('Username:') do |q|
  347. q.required true
  348. q.default 'admin'
  349. q.validate(/\A[a-z0-9_]+\z/i)
  350. q.modify :strip
  351. end
  352. email = prompt.ask('E-mail:') do |q|
  353. q.required true
  354. q.modify :strip
  355. end
  356. password = SecureRandom.hex(16)
  357. user = User.new(admin: true, email: email, password: password, confirmed_at: Time.now.utc, account_attributes: { username: username }, bypass_invite_request_check: true)
  358. user.save(validate: false)
  359. prompt.ok "You can login with the password: #{password}"
  360. prompt.warn 'You can change your password once you login.'
  361. end
  362. else
  363. prompt.warn 'Nothing saved. Bye!'
  364. end
  365. rescue TTY::Reader::InputInterrupt
  366. prompt.ok 'Aborting. Bye!'
  367. end
  368. end
  369. namespace :webpush do
  370. desc 'Generate VAPID key'
  371. task :generate_vapid_key do
  372. vapid_key = Webpush.generate_key
  373. puts "VAPID_PRIVATE_KEY=#{vapid_key.private_key}"
  374. puts "VAPID_PUBLIC_KEY=#{vapid_key.public_key}"
  375. end
  376. end
  377. end
  378. def disable_log_stdout!
  379. dev_null = Logger.new('/dev/null')
  380. Rails.logger = dev_null
  381. ActiveRecord::Base.logger = dev_null
  382. HttpLog.configuration.logger = dev_null
  383. Paperclip.options[:log] = false
  384. end