diff options
author | rinpatch <rinpatch@sdf.org> | 2019-04-17 12:22:32 +0300 |
---|---|---|
committer | rinpatch <rinpatch@sdf.org> | 2019-04-17 12:22:32 +0300 |
commit | 627e5a0a4992cc19fc65a7e93a09c470c8e2bf33 (patch) | |
tree | 0f38b475e8554863a1cbbd7750c19d4cd1336eb1 /lib/mix | |
parent | d6ab701a14f7c9fb4d59953648c425e04725fc62 (diff) | |
parent | 73df3046e014ae16e03f16a9c82921652cefcb54 (diff) | |
download | pleroma-627e5a0a4992cc19fc65a7e93a09c470c8e2bf33.tar.gz |
Merge branch 'develop' into feature/database-compaction
Diffstat (limited to 'lib/mix')
23 files changed, 807 insertions, 426 deletions
diff --git a/lib/mix/tasks/compact_database.ex b/lib/mix/tasks/compact_database.ex index 7de50812a..17b9721f7 100644 --- a/lib/mix/tasks/compact_database.ex +++ b/lib/mix/tasks/compact_database.ex @@ -6,9 +6,9 @@ defmodule Mix.Tasks.CompactDatabase do require Logger use Mix.Task - import Mix.Ecto import Ecto.Query - alias Pleroma.{Repo, Object, Activity} + alias Pleroma.Activity + alias Pleroma.Repo defp maybe_compact(%Activity{data: %{"object" => %{"id" => object_id}}} = activity) do data = @@ -33,7 +33,7 @@ defmodule Mix.Tasks.CompactDatabase do ) end - def run(args) do + def run(_args) do Application.ensure_all_started(:pleroma) max = Repo.aggregate(Activity, :max, :id) diff --git a/lib/mix/tasks/deactivate_user.ex b/lib/mix/tasks/deactivate_user.ex deleted file mode 100644 index e71ed1ec0..000000000 --- a/lib/mix/tasks/deactivate_user.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule Mix.Tasks.DeactivateUser do - use Mix.Task - alias Pleroma.User - - @moduledoc """ - Deactivates a user (local or remote) - - Usage: ``mix deactivate_user <nickname>`` - - Example: ``mix deactivate_user lain`` - """ - def run([nickname]) do - Mix.Task.run("app.start") - - with user <- User.get_by_nickname(nickname) do - User.deactivate(user) - end - end -end diff --git a/lib/mix/tasks/generate_config.ex b/lib/mix/tasks/generate_config.ex deleted file mode 100644 index e3cbbf131..000000000 --- a/lib/mix/tasks/generate_config.ex +++ /dev/null @@ -1,47 +0,0 @@ -defmodule Mix.Tasks.GenerateConfig do - use Mix.Task - - @moduledoc """ - Generate a new config - - ## Usage - ``mix generate_config`` - - This mix task is interactive, and will overwrite the config present at ``config/generated_config.exs``. - """ - - def run(_) do - IO.puts("Answer a few questions to generate a new config\n") - IO.puts("--- THIS WILL OVERWRITE YOUR config/generated_config.exs! ---\n") - domain = IO.gets("What is your domain name? (e.g. pleroma.soykaf.com): ") |> String.trim() - name = IO.gets("What is the name of your instance? (e.g. Pleroma/Soykaf): ") |> String.trim() - email = IO.gets("What's your admin email address: ") |> String.trim() - - secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) - dbpass = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) - - resultSql = EEx.eval_file("lib/mix/tasks/sample_psql.eex", dbpass: dbpass) - - result = - EEx.eval_file( - "lib/mix/tasks/sample_config.eex", - domain: domain, - email: email, - name: name, - secret: secret, - dbpass: dbpass - ) - - IO.puts( - "\nWriting config to config/generated_config.exs.\n\nCheck it and configure your database, then copy it to either config/dev.secret.exs or config/prod.secret.exs" - ) - - File.write("config/generated_config.exs", result) - - IO.puts( - "\nWriting setup_db.psql, please run it as postgre superuser, i.e.: sudo su postgres -c 'psql -f config/setup_db.psql'" - ) - - File.write("config/setup_db.psql", resultSql) - end -end diff --git a/lib/mix/tasks/generate_invite_token.ex b/lib/mix/tasks/generate_invite_token.ex deleted file mode 100644 index 418ef3790..000000000 --- a/lib/mix/tasks/generate_invite_token.ex +++ /dev/null @@ -1,32 +0,0 @@ -defmodule Mix.Tasks.GenerateInviteToken do - use Mix.Task - - @moduledoc """ - Generates invite token - - This is in the form of a URL to be used by the Invited user to register themselves. - - ## Usage - ``mix generate_invite_token`` - """ - def run([]) do - Mix.Task.run("app.start") - - with {:ok, token} <- Pleroma.UserInviteToken.create_token() do - IO.puts("Generated user invite token") - - IO.puts( - "Url: #{ - Pleroma.Web.Router.Helpers.redirect_url( - Pleroma.Web.Endpoint, - :registration_page, - token.token - ) - }" - ) - else - _ -> - IO.puts("Error creating token") - end - end -end diff --git a/lib/mix/tasks/generate_password_reset.ex b/lib/mix/tasks/generate_password_reset.ex deleted file mode 100644 index f7f4c4f59..000000000 --- a/lib/mix/tasks/generate_password_reset.ex +++ /dev/null @@ -1,33 +0,0 @@ -defmodule Mix.Tasks.GeneratePasswordReset do - use Mix.Task - alias Pleroma.User - - @moduledoc """ - Generate password reset link for user - - Usage: ``mix generate_password_reset <nickname>`` - - Example: ``mix generate_password_reset lain`` - """ - def run([nickname]) do - Mix.Task.run("app.start") - - with %User{local: true} = user <- User.get_by_nickname(nickname), - {:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do - IO.puts("Generated password reset token for #{user.nickname}") - - IO.puts( - "Url: #{ - Pleroma.Web.Router.Helpers.util_url( - Pleroma.Web.Endpoint, - :show_password_reset, - token.token - ) - }" - ) - else - _ -> - IO.puts("No local user #{nickname}") - end - end -end diff --git a/lib/mix/tasks/make_moderator.ex b/lib/mix/tasks/make_moderator.ex deleted file mode 100644 index 15586dc30..000000000 --- a/lib/mix/tasks/make_moderator.ex +++ /dev/null @@ -1,37 +0,0 @@ -defmodule Mix.Tasks.SetModerator do - @moduledoc """ - Set moderator to a local user - - Usage: ``mix set_moderator <nickname>`` - - Example: ``mix set_moderator lain`` - """ - - use Mix.Task - import Mix.Ecto - alias Pleroma.{Repo, User} - - def run([nickname | rest]) do - Application.ensure_all_started(:pleroma) - - moderator = - case rest do - [moderator] -> moderator == "true" - _ -> true - end - - with %User{local: true} = user <- User.get_by_nickname(nickname) do - info = - user.info - |> Map.put("is_moderator", !!moderator) - - cng = User.info_changeset(user, %{info: info}) - {:ok, user} = User.update_and_set_cache(cng) - - IO.puts("Moderator status of #{nickname}: #{user.info["is_moderator"]}") - else - _ -> - IO.puts("No local user #{nickname}") - end - end -end diff --git a/lib/mix/tasks/pleroma/common.ex b/lib/mix/tasks/pleroma/common.ex new file mode 100644 index 000000000..48c0c1346 --- /dev/null +++ b/lib/mix/tasks/pleroma/common.ex @@ -0,0 +1,28 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.Common do + @doc "Common functions to be reused in mix tasks" + def start_pleroma do + Mix.Task.run("app.start") + end + + def get_option(options, opt, prompt, defval \\ nil, defname \\ nil) do + Keyword.get(options, opt) || + case Mix.shell().prompt("#{prompt} [#{defname || defval}]") do + "\n" -> + case defval do + nil -> get_option(options, opt, prompt, defval) + defval -> defval + end + + opt -> + opt |> String.trim() + end + end + + def escape_sh_path(path) do + ~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(') + end +end diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex new file mode 100644 index 000000000..6cee8d630 --- /dev/null +++ b/lib/mix/tasks/pleroma/instance.ex @@ -0,0 +1,213 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.Instance do + use Mix.Task + alias Mix.Tasks.Pleroma.Common + + @shortdoc "Manages Pleroma instance" + @moduledoc """ + Manages Pleroma instance. + + ## Generate a new instance config. + + mix pleroma.instance gen [OPTION...] + + If any options are left unspecified, you will be prompted interactively + + ## Options + + - `-f`, `--force` - overwrite any output files + - `-o PATH`, `--output PATH` - the output file for the generated configuration + - `--output-psql PATH` - the output file for the generated PostgreSQL setup + - `--domain DOMAIN` - the domain of your instance + - `--instance-name INSTANCE_NAME` - the name of your instance + - `--admin-email ADMIN_EMAIL` - the email address of the instance admin + - `--notify-email NOTIFY_EMAIL` - email address for notifications + - `--dbhost HOSTNAME` - the hostname of the PostgreSQL database to use + - `--dbname DBNAME` - the name of the database to use + - `--dbuser DBUSER` - the user (aka role) to use for the database connection + - `--dbpass DBPASS` - the password to use for the database connection + - `--indexable Y/N` - Allow/disallow indexing site by search engines + """ + + def run(["gen" | rest]) do + {options, [], []} = + OptionParser.parse( + rest, + strict: [ + force: :boolean, + output: :string, + output_psql: :string, + domain: :string, + instance_name: :string, + admin_email: :string, + notify_email: :string, + dbhost: :string, + dbname: :string, + dbuser: :string, + dbpass: :string, + indexable: :string + ], + aliases: [ + o: :output, + f: :force + ] + ) + + paths = + [config_path, psql_path] = [ + Keyword.get(options, :output, "config/generated_config.exs"), + Keyword.get(options, :output_psql, "config/setup_db.psql") + ] + + will_overwrite = Enum.filter(paths, &File.exists?/1) + proceed? = Enum.empty?(will_overwrite) or Keyword.get(options, :force, false) + + if proceed? do + [domain, port | _] = + String.split( + Common.get_option( + options, + :domain, + "What domain will your instance use? (e.g pleroma.soykaf.com)" + ), + ":" + ) ++ [443] + + name = + Common.get_option( + options, + :instance_name, + "What is the name of your instance? (e.g. Pleroma/Soykaf)" + ) + + email = Common.get_option(options, :admin_email, "What is your admin email address?") + + notify_email = + Common.get_option( + options, + :notify_email, + "What email address do you want to use for sending email notifications?", + email + ) + + indexable = + Common.get_option( + options, + :indexable, + "Do you want search engines to index your site? (y/n)", + "y" + ) === "y" + + dbhost = + Common.get_option(options, :dbhost, "What is the hostname of your database?", "localhost") + + dbname = + Common.get_option(options, :dbname, "What is the name of your database?", "pleroma_dev") + + dbuser = + Common.get_option( + options, + :dbuser, + "What is the user used to connect to your database?", + "pleroma" + ) + + dbpass = + Common.get_option( + options, + :dbpass, + "What is the password used to connect to your database?", + :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64), + "autogenerated" + ) + + secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) + signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8) + {web_push_public_key, web_push_private_key} = :crypto.generate_key(:ecdh, :prime256v1) + + result_config = + EEx.eval_file( + "sample_config.eex" |> Path.expand(__DIR__), + domain: domain, + port: port, + email: email, + notify_email: notify_email, + name: name, + dbhost: dbhost, + dbname: dbname, + dbuser: dbuser, + dbpass: dbpass, + version: Pleroma.Mixfile.project() |> Keyword.get(:version), + secret: secret, + signing_salt: signing_salt, + web_push_public_key: Base.url_encode64(web_push_public_key, padding: false), + web_push_private_key: Base.url_encode64(web_push_private_key, padding: false) + ) + + result_psql = + EEx.eval_file( + "sample_psql.eex" |> Path.expand(__DIR__), + dbname: dbname, + dbuser: dbuser, + dbpass: dbpass + ) + + Mix.shell().info( + "Writing config to #{config_path}. You should rename it to config/prod.secret.exs or config/dev.secret.exs." + ) + + File.write(config_path, result_config) + Mix.shell().info("Writing #{psql_path}.") + File.write(psql_path, result_psql) + + write_robots_txt(indexable) + + Mix.shell().info( + "\n" <> + """ + To get started: + 1. Verify the contents of the generated files. + 2. Run `sudo -u postgres psql -f #{Common.escape_sh_path(psql_path)}`. + """ <> + if config_path in ["config/dev.secret.exs", "config/prod.secret.exs"] do + "" + else + "3. Run `mv #{Common.escape_sh_path(config_path)} 'config/prod.secret.exs'`." + end + ) + else + Mix.shell().error( + "The task would have overwritten the following files:\n" <> + (Enum.map(paths, &"- #{&1}\n") |> Enum.join("")) <> + "Rerun with `--force` to overwrite them." + ) + end + end + + defp write_robots_txt(indexable) do + robots_txt = + EEx.eval_file( + Path.expand("robots_txt.eex", __DIR__), + indexable: indexable + ) + + static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/") + + unless File.exists?(static_dir) do + File.mkdir_p!(static_dir) + end + + robots_txt_path = Path.join(static_dir, "robots.txt") + + if File.exists?(robots_txt_path) do + File.cp!(robots_txt_path, "#{robots_txt_path}.bak") + Mix.shell().info("Backing up existing robots.txt to #{robots_txt_path}.bak") + end + + File.write(robots_txt_path, robots_txt) + Mix.shell().info("Writing #{robots_txt_path}.") + end +end diff --git a/lib/mix/tasks/pleroma/relay.ex b/lib/mix/tasks/pleroma/relay.ex new file mode 100644 index 000000000..fbec473c5 --- /dev/null +++ b/lib/mix/tasks/pleroma/relay.ex @@ -0,0 +1,47 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.Relay do + use Mix.Task + alias Mix.Tasks.Pleroma.Common + alias Pleroma.Web.ActivityPub.Relay + + @shortdoc "Manages remote relays" + @moduledoc """ + Manages remote relays + + ## Follow a remote relay + + ``mix pleroma.relay follow <relay_url>`` + + Example: ``mix pleroma.relay follow https://example.org/relay`` + + ## Unfollow a remote relay + + ``mix pleroma.relay unfollow <relay_url>`` + + Example: ``mix pleroma.relay unfollow https://example.org/relay`` + """ + def run(["follow", target]) do + Common.start_pleroma() + + with {:ok, _activity} <- Relay.follow(target) do + # put this task to sleep to allow the genserver to push out the messages + :timer.sleep(500) + else + {:error, e} -> Mix.shell().error("Error while following #{target}: #{inspect(e)}") + end + end + + def run(["unfollow", target]) do + Common.start_pleroma() + + with {:ok, _activity} <- Relay.unfollow(target) do + # put this task to sleep to allow the genserver to push out the messages + :timer.sleep(500) + else + {:error, e} -> Mix.shell().error("Error while following #{target}: #{inspect(e)}") + end + end +end diff --git a/lib/mix/tasks/pleroma/robots_txt.eex b/lib/mix/tasks/pleroma/robots_txt.eex new file mode 100644 index 000000000..1af3c47ee --- /dev/null +++ b/lib/mix/tasks/pleroma/robots_txt.eex @@ -0,0 +1,2 @@ +User-Agent: * +Disallow: <%= if indexable, do: "", else: "/" %> diff --git a/lib/mix/tasks/pleroma/robotstxt.ex b/lib/mix/tasks/pleroma/robotstxt.ex new file mode 100644 index 000000000..2128e1cd6 --- /dev/null +++ b/lib/mix/tasks/pleroma/robotstxt.ex @@ -0,0 +1,32 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.RobotsTxt do + use Mix.Task + + @shortdoc "Generate robots.txt" + @moduledoc """ + Generates robots.txt + + ## Overwrite robots.txt to disallow all + + mix pleroma.robots_txt disallow_all + + This will write a robots.txt that will hide all paths on your instance + from search engines and other robots that obey robots.txt + + """ + def run(["disallow_all"]) do + static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/") + + if !File.exists?(static_dir) do + File.mkdir_p!(static_dir) + end + + robots_txt_path = Path.join(static_dir, "robots.txt") + robots_txt_content = "User-Agent: *\nDisallow: /\n" + + File.write!(robots_txt_path, robots_txt_content, [:write]) + end +end diff --git a/lib/mix/tasks/sample_config.eex b/lib/mix/tasks/pleroma/sample_config.eex index 462c34636..52bd57cb7 100644 --- a/lib/mix/tasks/sample_config.eex +++ b/lib/mix/tasks/pleroma/sample_config.eex @@ -1,12 +1,19 @@ +# Pleroma instance configuration + +# NOTE: This file should not be committed to a repo or otherwise made public +# without removing sensitive information. + use Mix.Config config :pleroma, Pleroma.Web.Endpoint, - url: [host: "<%= domain %>", scheme: "https", port: 443], - secret_key_base: "<%= secret %>" + url: [host: "<%= domain %>", scheme: "https", port: <%= port %>], + secret_key_base: "<%= secret %>", + signing_salt: "<%= signing_salt %>" config :pleroma, :instance, name: "<%= name %>", email: "<%= email %>", + notify_email: "<%= notify_email %>", limit: 5000, registrations_open: true, dedupe_media: false @@ -16,15 +23,20 @@ config :pleroma, :media_proxy, redirect_on_failure: true #base_url: "https://cache.pleroma.social" -# Configure your database config :pleroma, Pleroma.Repo, adapter: Ecto.Adapters.Postgres, - username: "pleroma", + username: "<%= dbuser %>", password: "<%= dbpass %>", - database: "pleroma_dev", - hostname: "localhost", + database: "<%= dbname %>", + hostname: "<%= dbhost %>", pool_size: 10 +# Configure web push notifications +config :web_push_encryption, :vapid_details, + subject: "mailto:<%= email %>", + public_key: "<%= web_push_public_key %>", + private_key: "<%= web_push_private_key %>" + # Enable Strict-Transport-Security once SSL is working: # config :pleroma, :http_security, # sts: true @@ -50,9 +62,9 @@ config :pleroma, Pleroma.Repo, # Configure Openstack Swift support if desired. -# -# Many openstack deployments are different, so config is left very open with -# no assumptions made on which provider you're using. This should allow very +# +# Many openstack deployments are different, so config is left very open with +# no assumptions made on which provider you're using. This should allow very # wide support without needing separate handlers for OVH, Rackspace, etc. # # config :pleroma, Pleroma.Uploaders.Swift, @@ -64,4 +76,3 @@ config :pleroma, Pleroma.Repo, # storage_url: "https://swift-endpoint.prodider.com/v1/AUTH_<tenant>/<container>", # object_url: "https://cdn-endpoint.provider.com/<container>" # - diff --git a/lib/mix/tasks/sample_psql.eex b/lib/mix/tasks/pleroma/sample_psql.eex index c89b34ef2..f0ac05e57 100644 --- a/lib/mix/tasks/sample_psql.eex +++ b/lib/mix/tasks/pleroma/sample_psql.eex @@ -1,6 +1,6 @@ -CREATE USER pleroma WITH ENCRYPTED PASSWORD '<%= dbpass %>'; -CREATE DATABASE pleroma_dev OWNER pleroma; -\c pleroma_dev; +CREATE USER <%= dbuser %> WITH ENCRYPTED PASSWORD '<%= dbpass %>'; +CREATE DATABASE <%= dbname %> OWNER <%= dbuser %>; +\c <%= dbname %>; --Extensions made by ecto.migrate that need superuser access CREATE EXTENSION IF NOT EXISTS citext; CREATE EXTENSION IF NOT EXISTS pg_trgm; diff --git a/lib/mix/tasks/migrate_local_uploads.ex b/lib/mix/tasks/pleroma/uploads.ex index 8f9e210c0..106fcf443 100644 --- a/lib/mix/tasks/migrate_local_uploads.ex +++ b/lib/mix/tasks/pleroma/uploads.ex @@ -1,16 +1,30 @@ -defmodule Mix.Tasks.MigrateLocalUploads do +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.Uploads do use Mix.Task - import Mix.Ecto - alias Pleroma.{Upload, Uploaders.Local, Uploaders.S3} + alias Mix.Tasks.Pleroma.Common + alias Pleroma.Upload + alias Pleroma.Uploaders.Local require Logger @log_every 50 - @shortdoc "Migrate uploads from local to remote storage" - def run([target_uploader | args]) do - delete? = Enum.member?(args, "--delete") - Application.ensure_all_started(:pleroma) + @shortdoc "Migrates uploads from local to remote storage" + @moduledoc """ + Manages uploads + + ## Migrate uploads from local to remote storage + mix pleroma.uploads migrate_local TARGET_UPLOADER [OPTIONS...] + Options: + - `--delete` - delete local uploads after migrating them to the target uploader + A list of available uploaders can be seen in config.exs + """ + def run(["migrate_local", target_uploader | args]) do + delete? = Enum.member?(args, "--delete") + Common.start_pleroma() local_path = Pleroma.Config.get!([Local, :uploads]) uploader = Module.concat(Pleroma.Uploaders, target_uploader) @@ -24,10 +38,10 @@ defmodule Mix.Tasks.MigrateLocalUploads do Pleroma.Config.put([Upload, :uploader], uploader) end - Logger.info("Migrating files from local #{local_path} to #{to_string(uploader)}") + Mix.shell().info("Migrating files from local #{local_path} to #{to_string(uploader)}") if delete? do - Logger.warn( + Mix.shell().info( "Attention: uploaded files will be deleted, hope you have backups! (--delete ; cancel with ^C)" ) @@ -54,7 +68,7 @@ defmodule Mix.Tasks.MigrateLocalUploads do File.exists?(root_path) -> file = Path.basename(id) - [hash, ext] = String.split(id, ".") + hash = Path.rootname(id) {%Pleroma.Upload{id: hash, name: file, path: file, tempfile: root_path}, root_path} true -> @@ -64,7 +78,7 @@ defmodule Mix.Tasks.MigrateLocalUploads do |> Enum.filter(& &1) total_count = length(uploads) - Logger.info("Found #{total_count} uploads") + Mix.shell().info("Found #{total_count} uploads") uploads |> Task.async_stream( @@ -76,22 +90,19 @@ defmodule Mix.Tasks.MigrateLocalUploads do :ok error -> - Logger.error("failed to upload #{inspect(upload.path)}: #{inspect(error)}") + Mix.shell().error("failed to upload #{inspect(upload.path)}: #{inspect(error)}") end end, timeout: 150_000 ) |> Stream.chunk_every(@log_every) + # credo:disable-for-next-line Credo.Check.Warning.UnusedEnumOperation |> Enum.reduce(0, fn done, count -> count = count + length(done) - Logger.info("Uploaded #{count}/#{total_count} files") + Mix.shell().info("Uploaded #{count}/#{total_count} files") count end) - Logger.info("Done!") - end - - def run(_) do - Logger.error("Usage: migrate_local_uploads S3|Swift [--delete]") + Mix.shell().info("Done!") end end diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex new file mode 100644 index 000000000..441168df2 --- /dev/null +++ b/lib/mix/tasks/pleroma/user.ex @@ -0,0 +1,429 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.User do + use Mix.Task + import Ecto.Changeset + alias Mix.Tasks.Pleroma.Common + alias Pleroma.User + alias Pleroma.UserInviteToken + + @shortdoc "Manages Pleroma users" + @moduledoc """ + Manages Pleroma users. + + ## Create a new user. + + mix pleroma.user new NICKNAME EMAIL [OPTION...] + + Options: + - `--name NAME` - the user's name (i.e., "Lain Iwakura") + - `--bio BIO` - the user's bio + - `--password PASSWORD` - the user's password + - `--moderator`/`--no-moderator` - whether the user is a moderator + - `--admin`/`--no-admin` - whether the user is an admin + - `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions + + ## Generate an invite link. + + mix pleroma.user invite [OPTION...] + + Options: + - `--expires_at DATE` - last day on which token is active (e.g. "2019-04-05") + - `--max_use NUMBER` - maximum numbers of token uses + + ## List generated invites + + mix pleroma.user invites + + ## Revoke invite + + mix pleroma.user revoke_invite TOKEN OR TOKEN_ID + + ## Delete the user's account. + + mix pleroma.user rm NICKNAME + + ## Delete the user's activities. + + mix pleroma.user delete_activities NICKNAME + + ## Deactivate or activate the user's account. + + mix pleroma.user toggle_activated NICKNAME + + ## Unsubscribe local users from user's account and deactivate it + + mix pleroma.user unsubscribe NICKNAME + + ## Create a password reset link. + + mix pleroma.user reset_password NICKNAME + + ## Set the value of the given user's settings. + + mix pleroma.user set NICKNAME [OPTION...] + + Options: + - `--locked`/`--no-locked` - whether the user's account is locked + - `--moderator`/`--no-moderator` - whether the user is a moderator + - `--admin`/`--no-admin` - whether the user is an admin + + ## Add tags to a user. + + mix pleroma.user tag NICKNAME TAGS + + ## Delete tags from a user. + + mix pleroma.user untag NICKNAME TAGS + """ + def run(["new", nickname, email | rest]) do + {options, [], []} = + OptionParser.parse( + rest, + strict: [ + name: :string, + bio: :string, + password: :string, + moderator: :boolean, + admin: :boolean, + assume_yes: :boolean + ], + aliases: [ + y: :assume_yes + ] + ) + + name = Keyword.get(options, :name, nickname) + bio = Keyword.get(options, :bio, "") + + {password, generated_password?} = + case Keyword.get(options, :password) do + nil -> + {:crypto.strong_rand_bytes(16) |> Base.encode64(), true} + + password -> + {password, false} + end + + moderator? = Keyword.get(options, :moderator, false) + admin? = Keyword.get(options, :admin, false) + assume_yes? = Keyword.get(options, :assume_yes, false) + + Mix.shell().info(""" + A user will be created with the following information: + - nickname: #{nickname} + - email: #{email} + - password: #{ + if(generated_password?, do: "[generated; a reset link will be created]", else: password) + } + - name: #{name} + - bio: #{bio} + - moderator: #{if(moderator?, do: "true", else: "false")} + - admin: #{if(admin?, do: "true", else: "false")} + """) + + proceed? = assume_yes? or Mix.shell().yes?("Continue?") + + unless not proceed? do + Common.start_pleroma() + + params = %{ + nickname: nickname, + email: email, + password: password, + password_confirmation: password, + name: name, + bio: bio + } + + changeset = User.register_changeset(%User{}, params, confirmed: true) + {:ok, _user} = User.register(changeset) + + Mix.shell().info("User #{nickname} created") + + if moderator? do + run(["set", nickname, "--moderator"]) + end + + if admin? do + run(["set", nickname, "--admin"]) + end + + if generated_password? do + run(["reset_password", nickname]) + end + else + Mix.shell().info("User will not be created.") + end + end + + def run(["rm", nickname]) do + Common.start_pleroma() + + with %User{local: true} = user <- User.get_by_nickname(nickname) do + User.delete(user) + Mix.shell().info("User #{nickname} deleted.") + else + _ -> + Mix.shell().error("No local user #{nickname}") + end + end + + def run(["toggle_activated", nickname]) do + Common.start_pleroma() + + with %User{} = user <- User.get_by_nickname(nickname) do + {:ok, user} = User.deactivate(user, !user.info.deactivated) + + Mix.shell().info( + "Activation status of #{nickname}: #{if(user.info.deactivated, do: "de", else: "")}activated" + ) + else + _ -> + Mix.shell().error("No user #{nickname}") + end + end + + def run(["reset_password", nickname]) do + Common.start_pleroma() + + with %User{local: true} = user <- User.get_by_nickname(nickname), + {:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do + Mix.shell().info("Generated password reset token for #{user.nickname}") + + IO.puts( + "URL: #{ + Pleroma.Web.Router.Helpers.util_url( + Pleroma.Web.Endpoint, + :show_password_reset, + token.token + ) + }" + ) + else + _ -> + Mix.shell().error("No local user #{nickname}") + end + end + + def run(["unsubscribe", nickname]) do + Common.start_pleroma() + + with %User{} = user <- User.get_by_nickname(nickname) do + Mix.shell().info("Deactivating #{user.nickname}") + User.deactivate(user) + + {:ok, friends} = User.get_friends(user) + + Enum.each(friends, fn friend -> + user = User.get_by_id(user.id) + + Mix.shell().info("Unsubscribing #{friend.nickname} from #{user.nickname}") + User.unfollow(user, friend) + end) + + :timer.sleep(500) + + user = User.get_by_id(user.id) + + if Enum.empty?(user.following) do + Mix.shell().info("Successfully unsubscribed all followers from #{user.nickname}") + end + else + _ -> + Mix.shell().error("No user #{nickname}") + end + end + + def run(["set", nickname | rest]) do + Common.start_pleroma() + + {options, [], []} = + OptionParser.parse( + rest, + strict: [ + moderator: :boolean, + admin: :boolean, + locked: :boolean + ] + ) + + with %User{local: true} = user <- User.get_by_nickname(nickname) do + user = + case Keyword.get(options, :moderator) do + nil -> user + value -> set_moderator(user, value) + end + + user = + case Keyword.get(options, :locked) do + nil -> user + value -> set_locked(user, value) + end + + _user = + case Keyword.get(options, :admin) do + nil -> user + value -> set_admin(user, value) + end + else + _ -> + Mix.shell().error("No local user #{nickname}") + end + end + + def run(["tag", nickname | tags]) do + Common.start_pleroma() + + with %User{} = user <- User.get_by_nickname(nickname) do + user = user |> User.tag(tags) + + Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}") + else + _ -> + Mix.shell().error("Could not change user tags for #{nickname}") + end + end + + def run(["untag", nickname | tags]) do + Common.start_pleroma() + + with %User{} = user <- User.get_by_nickname(nickname) do + user = user |> User.untag(tags) + + Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}") + else + _ -> + Mix.shell().error("Could not change user tags for #{nickname}") + end + end + + def run(["invite" | rest]) do + {options, [], []} = + OptionParser.parse(rest, + strict: [ + expires_at: :string, + max_use: :integer + ] + ) + + options = + options + |> Keyword.update(:expires_at, {:ok, nil}, fn + nil -> {:ok, nil} + val -> Date.from_iso8601(val) + end) + |> Enum.into(%{}) + + Common.start_pleroma() + + with {:ok, val} <- options[:expires_at], + options = Map.put(options, :expires_at, val), + {:ok, invite} <- UserInviteToken.create_invite(options) do + Mix.shell().info( + "Generated user invite token " <> String.replace(invite.invite_type, "_", " ") + ) + + url = + Pleroma.Web.Router.Helpers.redirect_url( + Pleroma.Web.Endpoint, + :registration_page, + invite.token + ) + + IO.puts(url) + else + error -> + Mix.shell().error("Could not create invite token: #{inspect(error)}") + end + end + + def run(["invites"]) do + Common.start_pleroma() + + Mix.shell().info("Invites list:") + + UserInviteToken.list_invites() + |> Enum.each(fn invite -> + expire_info = + with expires_at when not is_nil(expires_at) <- invite.expires_at do + " | Expires at: #{Date.to_string(expires_at)}" + end + + using_info = + with max_use when not is_nil(max_use) <- invite.max_use do + " | Max use: #{max_use} Left use: #{max_use - invite.uses}" + end + + Mix.shell().info( + "ID: #{invite.id} | Token: #{invite.token} | Token type: #{invite.invite_type} | Used: #{ + invite.used + }#{expire_info}#{using_info}" + ) + end) + end + + def run(["revoke_invite", token]) do + Common.start_pleroma() + + with {:ok, invite} <- UserInviteToken.find_by_token(token), + {:ok, _} <- UserInviteToken.update_invite(invite, %{used: true}) do + Mix.shell().info("Invite for token #{token} was revoked.") + else + _ -> Mix.shell().error("No invite found with token #{token}") + end + end + + def run(["delete_activities", nickname]) do + Common.start_pleroma() + + with %User{local: true} = user <- User.get_by_nickname(nickname) do + User.delete_user_activities(user) + Mix.shell().info("User #{nickname} statuses deleted.") + else + _ -> + Mix.shell().error("No local user #{nickname}") + end + end + + defp set_moderator(user, value) do + info_cng = User.Info.admin_api_update(user.info, %{is_moderator: value}) + + user_cng = + Ecto.Changeset.change(user) + |> put_embed(:info, info_cng) + + {:ok, user} = User.update_and_set_cache(user_cng) + + Mix.shell().info("Moderator status of #{user.nickname}: #{user.info.is_moderator}") + user + end + + defp set_admin(user, value) do + info_cng = User.Info.admin_api_update(user.info, %{is_admin: value}) + + user_cng = + Ecto.Changeset.change(user) + |> put_embed(:info, info_cng) + + {:ok, user} = User.update_and_set_cache(user_cng) + + Mix.shell().info("Admin status of #{user.nickname}: #{user.info.is_admin}") + user + end + + defp set_locked(user, value) do + info_cng = User.Info.user_upgrade(user.info, %{locked: value}) + + user_cng = + Ecto.Changeset.change(user) + |> put_embed(:info, info_cng) + + {:ok, user} = User.update_and_set_cache(user_cng) + + Mix.shell().info("Locked status of #{user.nickname}: #{user.info.locked}") + user + end +end diff --git a/lib/mix/tasks/reactivate_user.ex b/lib/mix/tasks/reactivate_user.ex deleted file mode 100644 index a30d3ac8b..000000000 --- a/lib/mix/tasks/reactivate_user.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule Mix.Tasks.ReactivateUser do - use Mix.Task - alias Pleroma.User - - @moduledoc """ - Reactivate a user - - Usage: ``mix reactivate_user <nickname>`` - - Example: ``mix reactivate_user lain`` - """ - def run([nickname]) do - Mix.Task.run("app.start") - - with user <- User.get_by_nickname(nickname) do - User.deactivate(user, false) - end - end -end diff --git a/lib/mix/tasks/register_user.ex b/lib/mix/tasks/register_user.ex deleted file mode 100644 index 1f5321093..000000000 --- a/lib/mix/tasks/register_user.ex +++ /dev/null @@ -1,30 +0,0 @@ -defmodule Mix.Tasks.RegisterUser do - @moduledoc """ - Manually register a local user - - Usage: ``mix register_user <name> <nickname> <email> <bio> <password>`` - - Example: ``mix register_user 仮面の告白 lain lain@example.org "blushy-crushy fediverse idol + pleroma dev" pleaseDontHeckLain`` - """ - - use Mix.Task - alias Pleroma.{Repo, User} - - @shortdoc "Register user" - def run([name, nickname, email, bio, password]) do - Mix.Task.run("app.start") - - params = %{ - name: name, - nickname: nickname, - email: email, - password: password, - password_confirmation: password, - bio: bio - } - - user = User.register_changeset(%User{}, params) - - Repo.insert!(user) - end -end diff --git a/lib/mix/tasks/relay_follow.ex b/lib/mix/tasks/relay_follow.ex deleted file mode 100644 index 85b1c024d..000000000 --- a/lib/mix/tasks/relay_follow.ex +++ /dev/null @@ -1,24 +0,0 @@ -defmodule Mix.Tasks.RelayFollow do - use Mix.Task - require Logger - alias Pleroma.Web.ActivityPub.Relay - - @shortdoc "Follows a remote relay" - @moduledoc """ - Follows a remote relay - - Usage: ``mix relay_follow <relay_url>`` - - Example: ``mix relay_follow https://example.org/relay`` - """ - def run([target]) do - Mix.Task.run("app.start") - - with {:ok, activity} <- Relay.follow(target) do - # put this task to sleep to allow the genserver to push out the messages - :timer.sleep(500) - else - {:error, e} -> Mix.shell().error("Error while following #{target}: #{inspect(e)}") - end - end -end diff --git a/lib/mix/tasks/relay_unfollow.ex b/lib/mix/tasks/relay_unfollow.ex deleted file mode 100644 index 237fb771c..000000000 --- a/lib/mix/tasks/relay_unfollow.ex +++ /dev/null @@ -1,23 +0,0 @@ -defmodule Mix.Tasks.RelayUnfollow do - use Mix.Task - require Logger - alias Pleroma.Web.ActivityPub.Relay - - @moduledoc """ - Unfollows a remote relay - - Usage: ``mix relay_follow <relay_url>`` - - Example: ``mix relay_follow https://example.org/relay`` - """ - def run([target]) do - Mix.Task.run("app.start") - - with {:ok, activity} <- Relay.follow(target) do - # put this task to sleep to allow the genserver to push out the messages - :timer.sleep(500) - else - {:error, e} -> Mix.shell().error("Error while following #{target}: #{inspect(e)}") - end - end -end diff --git a/lib/mix/tasks/rm_user.ex b/lib/mix/tasks/rm_user.ex deleted file mode 100644 index 50463046c..000000000 --- a/lib/mix/tasks/rm_user.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule Mix.Tasks.RmUser do - use Mix.Task - alias Pleroma.User - - @moduledoc """ - Permanently deletes a user - - Usage: ``mix rm_user [nickname]`` - - Example: ``mix rm_user lain`` - """ - def run([nickname]) do - Mix.Task.run("app.start") - - with %User{local: true} = user <- User.get_by_nickname(nickname) do - {:ok, _} = User.delete(user) - end - end -end diff --git a/lib/mix/tasks/set_admin.ex b/lib/mix/tasks/set_admin.ex deleted file mode 100644 index d5ccf261b..000000000 --- a/lib/mix/tasks/set_admin.ex +++ /dev/null @@ -1,32 +0,0 @@ -defmodule Mix.Tasks.SetAdmin do - use Mix.Task - alias Pleroma.User - - @doc """ - Sets admin status - Usage: set_admin nickname [true|false] - """ - def run([nickname | rest]) do - Application.ensure_all_started(:pleroma) - - status = - case rest do - [status] -> status == "true" - _ -> true - end - - with %User{local: true} = user <- User.get_by_nickname(nickname) do - info = - user.info - |> Map.put("is_admin", !!status) - - cng = User.info_changeset(user, %{info: info}) - {:ok, user} = User.update_and_set_cache(cng) - - IO.puts("Admin status of #{nickname}: #{user.info["is_admin"]}") - else - _ -> - IO.puts("No local user #{nickname}") - end - end -end diff --git a/lib/mix/tasks/set_locked.ex b/lib/mix/tasks/set_locked.ex deleted file mode 100644 index a154595ca..000000000 --- a/lib/mix/tasks/set_locked.ex +++ /dev/null @@ -1,39 +0,0 @@ -defmodule Mix.Tasks.SetLocked do - @moduledoc """ - Lock a local user - - The local user will then have to manually accept/reject followers. This can also be done by the user into their settings. - - Usage: ``mix set_locked <username>`` - - Example: ``mix set_locked lain`` - """ - - use Mix.Task - import Mix.Ecto - alias Pleroma.{Repo, User} - - def run([nickname | rest]) do - ensure_started(Repo, []) - - locked = - case rest do - [locked] -> locked == "true" - _ -> true - end - - with %User{local: true} = user <- User.get_by_nickname(nickname) do - info = - user.info - |> Map.put("locked", !!locked) - - cng = User.info_changeset(user, %{info: info}) - user = Repo.update!(cng) - - IO.puts("locked status of #{nickname}: #{user.info["locked"]}") - else - _ -> - IO.puts("No local user #{nickname}") - end - end -end diff --git a/lib/mix/tasks/unsubscribe_user.ex b/lib/mix/tasks/unsubscribe_user.ex deleted file mode 100644 index 62ea61a5c..000000000 --- a/lib/mix/tasks/unsubscribe_user.ex +++ /dev/null @@ -1,38 +0,0 @@ -defmodule Mix.Tasks.UnsubscribeUser do - use Mix.Task - alias Pleroma.{User, Repo} - require Logger - - @moduledoc """ - Deactivate and Unsubscribe local users from a user - - Usage: ``mix unsubscribe_user <nickname>`` - - Example: ``mix unsubscribe_user lain`` - """ - def run([nickname]) do - Mix.Task.run("app.start") - - with %User{} = user <- User.get_by_nickname(nickname) do - Logger.info("Deactivating #{user.nickname}") - User.deactivate(user) - - {:ok, friends} = User.get_friends(user) - - Enum.each(friends, fn friend -> - user = Repo.get(User, user.id) - - Logger.info("Unsubscribing #{friend.nickname} from #{user.nickname}") - User.unfollow(user, friend) - end) - - :timer.sleep(500) - - user = Repo.get(User, user.id) - - if length(user.following) == 0 do - Logger.info("Successfully unsubscribed all followers from #{user.nickname}") - end - end - end -end |