First
[anni] / lib / mix / tasks / pleroma / instance.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright Â© 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Mix.Tasks.Pleroma.Instance do
6   use Mix.Task
7   import Mix.Pleroma
8
9   alias Pleroma.Config
10
11   @shortdoc "Manages Pleroma instance"
12   @moduledoc File.read!("docs/administration/CLI_tasks/instance.md")
13
14   def run(["gen" | rest]) do
15     {options, [], []} =
16       OptionParser.parse(
17         rest,
18         strict: [
19           force: :boolean,
20           output: :string,
21           output_psql: :string,
22           domain: :string,
23           instance_name: :string,
24           admin_email: :string,
25           notify_email: :string,
26           dbhost: :string,
27           dbname: :string,
28           dbuser: :string,
29           dbpass: :string,
30           rum: :string,
31           indexable: :string,
32           db_configurable: :string,
33           uploads_dir: :string,
34           static_dir: :string,
35           listen_ip: :string,
36           listen_port: :string,
37           strip_uploads_location: :string,
38           read_uploads_description: :string,
39           anonymize_uploads: :string,
40           dedupe_uploads: :string
41         ],
42         aliases: [
43           o: :output,
44           f: :force
45         ]
46       )
47
48     paths =
49       [config_path, psql_path] = [
50         Keyword.get(options, :output, "config/generated_config.exs"),
51         Keyword.get(options, :output_psql, "config/setup_db.psql")
52       ]
53
54     will_overwrite = Enum.filter(paths, &File.exists?/1)
55     proceed? = Enum.empty?(will_overwrite) or Keyword.get(options, :force, false)
56
57     if proceed? do
58       [domain, port | _] =
59         String.split(
60           get_option(
61             options,
62             :domain,
63             "What domain will your instance use? (e.g pleroma.soykaf.com)"
64           ),
65           ":"
66         ) ++ [443]
67
68       name =
69         get_option(
70           options,
71           :instance_name,
72           "What is the name of your instance? (e.g. The Corndog Emporium)",
73           domain
74         )
75
76       email = get_option(options, :admin_email, "What is your admin email address?")
77
78       notify_email =
79         get_option(
80           options,
81           :notify_email,
82           "What email address do you want to use for sending email notifications?",
83           email
84         )
85
86       indexable =
87         get_option(
88           options,
89           :indexable,
90           "Do you want search engines to index your site? (y/n)",
91           "y"
92         ) === "y"
93
94       db_configurable? =
95         get_option(
96           options,
97           :db_configurable,
98           "Do you want to store the configuration in the database (allows controlling it from admin-fe)? (y/n)",
99           "n"
100         ) === "y"
101
102       dbhost = get_option(options, :dbhost, "What is the hostname of your database?", "localhost")
103
104       dbname = get_option(options, :dbname, "What is the name of your database?", "pleroma")
105
106       dbuser =
107         get_option(
108           options,
109           :dbuser,
110           "What is the user used to connect to your database?",
111           "pleroma"
112         )
113
114       dbpass =
115         get_option(
116           options,
117           :dbpass,
118           "What is the password used to connect to your database?",
119           :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64),
120           "autogenerated"
121         )
122
123       rum_enabled =
124         get_option(
125           options,
126           :rum,
127           "Would you like to use RUM indices?",
128           "n"
129         ) === "y"
130
131       listen_port =
132         get_option(
133           options,
134           :listen_port,
135           "What port will the app listen to (leave it if you are using the default setup with nginx)?",
136           4000
137         )
138
139       listen_ip =
140         get_option(
141           options,
142           :listen_ip,
143           "What ip will the app listen to (leave it if you are using the default setup with nginx)?",
144           "127.0.0.1"
145         )
146
147       uploads_dir =
148         get_option(
149           options,
150           :uploads_dir,
151           "What directory should media uploads go in (when using the local uploader)?",
152           Config.get([Pleroma.Uploaders.Local, :uploads])
153         )
154         |> Path.expand()
155
156       static_dir =
157         get_option(
158           options,
159           :static_dir,
160           "What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?",
161           Config.get([:instance, :static_dir])
162         )
163         |> Path.expand()
164
165       {strip_uploads_location_message, strip_uploads_location_default} =
166         if Pleroma.Utils.command_available?("exiftool") do
167           {"Do you want to strip location (GPS) data from uploaded images? This requires exiftool, it was detected as installed. (y/n)",
168            "y"}
169         else
170           {"Do you want to strip location (GPS) data from uploaded images? This requires exiftool, it was detected as not installed, please install it if you answer yes. (y/n)",
171            "n"}
172         end
173
174       strip_uploads_location =
175         get_option(
176           options,
177           :strip_uploads_location,
178           strip_uploads_location_message,
179           strip_uploads_location_default
180         ) === "y"
181
182       {read_uploads_description_message, read_uploads_description_default} =
183         if Pleroma.Utils.command_available?("exiftool") do
184           {"Do you want to read data from uploaded files so clients can use it to prefill fields like image description? This requires exiftool, it was detected as installed. (y/n)",
185            "y"}
186         else
187           {"Do you want to read data from uploaded files so clients can use it to prefill fields like image description? This requires exiftool, it was detected as not installed, please install it if you answer yes. (y/n)",
188            "n"}
189         end
190
191       read_uploads_description =
192         get_option(
193           options,
194           :read_uploads_description,
195           read_uploads_description_message,
196           read_uploads_description_default
197         ) === "y"
198
199       anonymize_uploads =
200         get_option(
201           options,
202           :anonymize_uploads,
203           "Do you want to anonymize the filenames of uploads? (y/n)",
204           "n"
205         ) === "y"
206
207       dedupe_uploads =
208         get_option(
209           options,
210           :dedupe_uploads,
211           "Do you want to deduplicate uploaded files? (y/n)",
212           "n"
213         ) === "y"
214
215       Config.put([:instance, :static_dir], static_dir)
216
217       secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
218       jwt_secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
219       signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
220       lv_signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
221       {web_push_public_key, web_push_private_key} = :crypto.generate_key(:ecdh, :prime256v1)
222       template_dir = Application.app_dir(:pleroma, "priv") <> "/templates"
223
224       result_config =
225         EEx.eval_file(
226           template_dir <> "/sample_config.eex",
227           domain: domain,
228           port: port,
229           email: email,
230           notify_email: notify_email,
231           name: name,
232           dbhost: dbhost,
233           dbname: dbname,
234           dbuser: dbuser,
235           dbpass: dbpass,
236           secret: secret,
237           jwt_secret: jwt_secret,
238           signing_salt: signing_salt,
239           lv_signing_salt: lv_signing_salt,
240           web_push_public_key: Base.url_encode64(web_push_public_key, padding: false),
241           web_push_private_key: Base.url_encode64(web_push_private_key, padding: false),
242           db_configurable?: db_configurable?,
243           static_dir: static_dir,
244           uploads_dir: uploads_dir,
245           rum_enabled: rum_enabled,
246           listen_ip: listen_ip,
247           listen_port: listen_port,
248           upload_filters:
249             upload_filters(%{
250               strip_location: strip_uploads_location,
251               read_description: read_uploads_description,
252               anonymize: anonymize_uploads,
253               dedupe: dedupe_uploads
254             })
255         )
256
257       result_psql =
258         EEx.eval_file(
259           template_dir <> "/sample_psql.eex",
260           dbname: dbname,
261           dbuser: dbuser,
262           dbpass: dbpass,
263           rum_enabled: rum_enabled
264         )
265
266       config_dir = Path.dirname(config_path)
267       psql_dir = Path.dirname(psql_path)
268
269       # Note: Distros requiring group read (0o750) on those directories should
270       # pre-create the directories.
271       [config_dir, psql_dir, static_dir, uploads_dir]
272       |> Enum.reject(&File.exists?/1)
273       |> Enum.each(fn dir ->
274         File.mkdir_p!(dir)
275         File.chmod!(dir, 0o700)
276       end)
277
278       shell_info("Writing config to #{config_path}.")
279
280       # Sadly no fchmod(2) equivalent in Elixir…
281       File.touch!(config_path)
282       File.chmod!(config_path, 0o640)
283       File.write(config_path, result_config)
284       shell_info("Writing the postgres script to #{psql_path}.")
285       File.write(psql_path, result_psql)
286
287       write_robots_txt(static_dir, indexable, template_dir)
288
289       shell_info(
290         "\n All files successfully written! Refer to the installation instructions for your platform for next steps."
291       )
292
293       if db_configurable? do
294         shell_info(
295           " Please transfer your config to the database after running database migrations. Refer to \"Transfering the config to/from the database\" section of the docs for more information."
296         )
297       end
298     else
299       shell_error(
300         "The task would have overwritten the following files:\n" <>
301           Enum.map_join(will_overwrite, &"- #{&1}\n") <> "Rerun with `--force` to overwrite them."
302       )
303     end
304   end
305
306   defp write_robots_txt(static_dir, indexable, template_dir) do
307     robots_txt =
308       EEx.eval_file(
309         template_dir <> "/robots_txt.eex",
310         indexable: indexable
311       )
312
313     robots_txt_path = Path.join(static_dir, "robots.txt")
314
315     if File.exists?(robots_txt_path) do
316       File.cp!(robots_txt_path, "#{robots_txt_path}.bak")
317       shell_info("Backing up existing robots.txt to #{robots_txt_path}.bak")
318     end
319
320     File.write(robots_txt_path, robots_txt)
321     shell_info("Writing #{robots_txt_path}.")
322   end
323
324   defp upload_filters(filters) when is_map(filters) do
325     enabled_filters =
326       if filters.strip_location do
327         [Pleroma.Upload.Filter.Exiftool.StripLocation]
328       else
329         []
330       end
331
332     enabled_filters =
333       if filters.read_description do
334         enabled_filters ++ [Pleroma.Upload.Filter.Exiftool.ReadDescription]
335       else
336         enabled_filters
337       end
338
339     enabled_filters =
340       if filters.anonymize do
341         enabled_filters ++ [Pleroma.Upload.Filter.AnonymizeFilename]
342       else
343         enabled_filters
344       end
345
346     enabled_filters =
347       if filters.dedupe do
348         enabled_filters ++ [Pleroma.Upload.Filter.Dedupe]
349       else
350         enabled_filters
351       end
352
353     enabled_filters
354   end
355
356   defp upload_filters(_), do: []
357 end