mastodon.rake 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  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 interfere 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. env['SMTP_ENABLE_STARTTLS'] = 'auto'
  228. else
  229. env['SMTP_SERVER'] = prompt.ask('SMTP server:') do |q|
  230. q.required true
  231. q.default 'smtp.mailgun.org'
  232. q.modify :strip
  233. end
  234. env['SMTP_PORT'] = prompt.ask('SMTP port:') do |q|
  235. q.required true
  236. q.default 587
  237. q.convert :int
  238. end
  239. env['SMTP_LOGIN'] = prompt.ask('SMTP username:') do |q|
  240. q.modify :strip
  241. end
  242. env['SMTP_PASSWORD'] = prompt.ask('SMTP password:') do |q|
  243. q.echo false
  244. end
  245. env['SMTP_AUTH_METHOD'] = prompt.ask('SMTP authentication:') do |q|
  246. q.required
  247. q.default 'plain'
  248. q.modify :strip
  249. end
  250. env['SMTP_OPENSSL_VERIFY_MODE'] = prompt.select('SMTP OpenSSL verify mode:', %w(none peer client_once fail_if_no_peer_cert))
  251. env['SMTP_ENABLE_STARTTLS'] = prompt.select('Enable STARTTLS:', %w(auto always never))
  252. end
  253. env['SMTP_FROM_ADDRESS'] = prompt.ask('E-mail address to send e-mails "from":') do |q|
  254. q.required true
  255. q.default "Mastodon <notifications@#{env['LOCAL_DOMAIN']}>"
  256. q.modify :strip
  257. end
  258. break unless prompt.yes?('Send a test e-mail with this configuration right now?')
  259. send_to = prompt.ask('Send test e-mail to:', required: true)
  260. begin
  261. enable_starttls = nil
  262. enable_starttls_auto = nil
  263. case env['SMTP_ENABLE_STARTTLS']
  264. when 'always'
  265. enable_starttls = true
  266. when 'never'
  267. enable_starttls = false
  268. when 'auto'
  269. enable_starttls_auto = true
  270. else
  271. enable_starttls_auto = ENV['SMTP_ENABLE_STARTTLS_AUTO'] != 'false'
  272. end
  273. ActionMailer::Base.smtp_settings = {
  274. port: env['SMTP_PORT'],
  275. address: env['SMTP_SERVER'],
  276. user_name: env['SMTP_LOGIN'].presence,
  277. password: env['SMTP_PASSWORD'].presence,
  278. domain: env['LOCAL_DOMAIN'],
  279. authentication: env['SMTP_AUTH_METHOD'] == 'none' ? nil : env['SMTP_AUTH_METHOD'] || :plain,
  280. openssl_verify_mode: env['SMTP_OPENSSL_VERIFY_MODE'],
  281. enable_starttls: enable_starttls,
  282. enable_starttls_auto: enable_starttls_auto,
  283. }
  284. ActionMailer::Base.default_options = {
  285. from: env['SMTP_FROM_ADDRESS'],
  286. }
  287. mail = ActionMailer::Base.new.mail to: send_to, subject: 'Test', body: 'Mastodon SMTP configuration works!'
  288. mail.deliver
  289. break
  290. rescue StandardError => e
  291. prompt.error 'E-mail could not be sent with this configuration, try again.'
  292. prompt.error e.message
  293. break unless prompt.yes?('Try again?')
  294. end
  295. end
  296. prompt.say "\n"
  297. prompt.say 'This configuration will be written to .env.production'
  298. if prompt.yes?('Save configuration?')
  299. incompatible_syntax = false
  300. env_contents = env.each_pair.map do |key, value|
  301. if value.is_a?(String) && value =~ /[\s\#\\"]/
  302. incompatible_syntax = true
  303. if value =~ /[']/
  304. value = value.to_s.gsub(/[\\"\$]/) { |x| "\\#{x}" }
  305. "#{key}=\"#{value}\""
  306. else
  307. "#{key}='#{value}'"
  308. end
  309. else
  310. "#{key}=#{value}"
  311. end
  312. end.join("\n")
  313. generated_header = "# Generated with mastodon:setup on #{Time.now.utc}\n\n".dup
  314. if incompatible_syntax
  315. generated_header << "# Some variables in this file will be interpreted differently whether you are\n"
  316. generated_header << "# using docker-compose or not.\n\n"
  317. end
  318. File.write(Rails.root.join('.env.production'), "#{generated_header}#{env_contents}\n")
  319. if using_docker
  320. prompt.ok 'Below is your configuration, save it to an .env.production file outside Docker:'
  321. prompt.say "\n"
  322. prompt.say "#{generated_header}#{env.each_pair.map { |key, value| "#{key}=#{value}" }.join("\n")}"
  323. prompt.say "\n"
  324. prompt.ok 'It is also saved within this container so you can proceed with this wizard.'
  325. end
  326. prompt.say "\n"
  327. prompt.say 'Now that configuration is saved, the database schema must be loaded.'
  328. prompt.warn 'If the database already exists, this will erase its contents.'
  329. if prompt.yes?('Prepare the database now?')
  330. prompt.say 'Running `RAILS_ENV=production rails db:setup` ...'
  331. prompt.say "\n\n"
  332. if !system(env.transform_values(&:to_s).merge({ 'RAILS_ENV' => 'production', 'SAFETY_ASSURED' => '1' }), 'rails db:setup')
  333. prompt.error 'That failed! Perhaps your configuration is not right'
  334. else
  335. prompt.ok 'Done!'
  336. end
  337. end
  338. unless using_docker
  339. prompt.say "\n"
  340. prompt.say 'The final step is compiling CSS/JS assets.'
  341. prompt.say 'This may take a while and consume a lot of RAM.'
  342. if prompt.yes?('Compile the assets now?')
  343. prompt.say 'Running `RAILS_ENV=production rails assets:precompile` ...'
  344. prompt.say "\n\n"
  345. if !system(env.transform_values(&:to_s).merge({ 'RAILS_ENV' => 'production' }), 'rails assets:precompile')
  346. prompt.error 'That failed! Maybe you need swap space?'
  347. else
  348. prompt.say 'Done!'
  349. end
  350. end
  351. end
  352. prompt.say "\n"
  353. prompt.ok 'All done! You can now power on the Mastodon server 🐘'
  354. prompt.say "\n"
  355. if db_connection_works && prompt.yes?('Do you want to create an admin user straight away?')
  356. env.each_pair do |key, value|
  357. ENV[key] = value.to_s
  358. end
  359. require_relative '../../config/environment'
  360. disable_log_stdout!
  361. username = prompt.ask('Username:') do |q|
  362. q.required true
  363. q.default 'admin'
  364. q.validate(/\A[a-z0-9_]+\z/i)
  365. q.modify :strip
  366. end
  367. email = prompt.ask('E-mail:') do |q|
  368. q.required true
  369. q.modify :strip
  370. end
  371. password = SecureRandom.hex(16)
  372. owner_role = UserRole.find_by(name: 'Owner')
  373. user = User.new(email: email, password: password, confirmed_at: Time.now.utc, account_attributes: { username: username }, bypass_invite_request_check: true, role: owner_role)
  374. user.save(validate: false)
  375. Setting.site_contact_username = username
  376. prompt.ok "You can login with the password: #{password}"
  377. prompt.warn 'You can change your password once you login.'
  378. end
  379. else
  380. prompt.warn 'Nothing saved. Bye!'
  381. end
  382. rescue TTY::Reader::InputInterrupt
  383. prompt.ok 'Aborting. Bye!'
  384. end
  385. end
  386. namespace :webpush do
  387. desc 'Generate VAPID key'
  388. task :generate_vapid_key do
  389. vapid_key = Webpush.generate_key
  390. puts "VAPID_PRIVATE_KEY=#{vapid_key.private_key}"
  391. puts "VAPID_PUBLIC_KEY=#{vapid_key.public_key}"
  392. end
  393. end
  394. end
  395. def disable_log_stdout!
  396. dev_null = Logger.new('/dev/null')
  397. Rails.logger = dev_null
  398. ActiveRecord::Base.logger = dev_null
  399. HttpLog.configuration.logger = dev_null
  400. Paperclip.options[:log] = false
  401. end