mastodon.rake 14 KB

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