diff options
Diffstat (limited to 'lib')
89 files changed, 1611 insertions, 813 deletions
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..36432c291 --- /dev/null +++ b/lib/mix/tasks/pleroma/common.ex @@ -0,0 +1,24 @@ +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..3be856115 --- /dev/null +++ b/lib/mix/tasks/pleroma/instance.ex @@ -0,0 +1,156 @@ +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 + - `--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 + """ + + 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, + dbhost: :string, + dbname: :string, + dbuser: :string, + dbpass: :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) + + unless not proceed? do + domain = + Common.get_option( + options, + :domain, + "What domain will your instance use? (e.g pleroma.soykaf.com)" + ) + + name = + Common.get_option( + options, + :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?") + + 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) + {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, + email: email, + name: name, + dbhost: dbhost, + dbname: dbname, + dbuser: dbuser, + dbpass: dbpass, + version: Pleroma.Mixfile.project() |> Keyword.get(:version), + secret: secret, + 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) + + 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 +end diff --git a/lib/mix/tasks/pleroma/relay.ex b/lib/mix/tasks/pleroma/relay.ex new file mode 100644 index 000000000..03586d6c3 --- /dev/null +++ b/lib/mix/tasks/pleroma/relay.ex @@ -0,0 +1,43 @@ +defmodule Mix.Tasks.Pleroma.Relay do + use Mix.Task + alias Pleroma.Web.ActivityPub.Relay + alias Mix.Tasks.Pleroma.Common + + @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/sample_config.eex b/lib/mix/tasks/pleroma/sample_config.eex index 462c34636..0cd572797 100644 --- a/lib/mix/tasks/sample_config.eex +++ b/lib/mix/tasks/pleroma/sample_config.eex @@ -1,3 +1,8 @@ +# 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, @@ -16,15 +21,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 +60,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, diff --git a/lib/mix/tasks/sample_psql.eex b/lib/mix/tasks/pleroma/sample_psql.eex index b6f57948b..c89b34ef2 100644 --- a/lib/mix/tasks/sample_psql.eex +++ b/lib/mix/tasks/pleroma/sample_psql.eex @@ -4,3 +4,4 @@ CREATE DATABASE pleroma_dev OWNER pleroma; --Extensions made by ecto.migrate that need superuser access CREATE EXTENSION IF NOT EXISTS citext; CREATE EXTENSION IF NOT EXISTS pg_trgm; +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; diff --git a/lib/mix/tasks/migrate_local_uploads.ex b/lib/mix/tasks/pleroma/uploads.ex index 8f9e210c0..c6b840130 100644 --- a/lib/mix/tasks/migrate_local_uploads.ex +++ b/lib/mix/tasks/pleroma/uploads.ex @@ -1,16 +1,19 @@ -defmodule Mix.Tasks.MigrateLocalUploads do +defmodule Mix.Tasks.Pleroma.Uploads do use Mix.Task - import Mix.Ecto - alias Pleroma.{Upload, Uploaders.Local, Uploaders.S3} + alias Pleroma.{Upload, Uploaders.Local} + alias Mix.Tasks.Pleroma.Common require Logger @log_every 50 @shortdoc "Migrate uploads from local to remote storage" + @doc """ + Manages uploads + ## Migrate uploads from local to remote storage - def run([target_uploader | args]) do + """ + def run(["migrate_local", target_uploader | args]) do delete? = Enum.member?(args, "--delete") - Application.ensure_all_started(:pleroma) - + Common.start_pleroma() local_path = Pleroma.Config.get!([Local, :uploads]) uploader = Module.concat(Pleroma.Uploaders, target_uploader) @@ -24,10 +27,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 +57,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 +67,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,7 +79,7 @@ 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 @@ -84,14 +87,10 @@ defmodule Mix.Tasks.MigrateLocalUploads do |> Stream.chunk_every(@log_every) |> 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..2675b021d --- /dev/null +++ b/lib/mix/tasks/pleroma/user.ex @@ -0,0 +1,293 @@ +defmodule Mix.Tasks.Pleroma.User do + use Mix.Task + import Ecto.Changeset + alias Pleroma.{Repo, User} + alias Mix.Tasks.Pleroma.Common + + @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 + + ## Generate an invite link. + + mix pleroma.user invite + + ## Delete the user's account. + + mix pleroma.user rm 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 + """ + def run(["new", nickname, email | rest]) do + {options, [], []} = + OptionParser.parse( + rest, + strict: [ + name: :string, + bio: :string, + password: :string, + moderator: :boolean, + admin: :boolean + ] + ) + + 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) + + 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? = 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 + } + |> IO.inspect() + + user = User.register_changeset(%User{}, params) + Repo.insert!(user) + + 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 + User.deactivate(user, !user.info["deactivated"]) + Mix.shell().info("Activation status of #{nickname}: #{user.info["deactivated"]}") + 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 = Repo.get(User, user.id) + + Mix.shell().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 + 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 + case Keyword.get(options, :moderator) do + nil -> nil + value -> set_moderator(user, value) + end + + case Keyword.get(options, :locked) do + nil -> nil + value -> set_locked(user, value) + end + + case Keyword.get(options, :admin) do + nil -> nil + value -> set_admin(user, value) + end + else + _ -> + Mix.shell().error("No local user #{nickname}") + end + end + + def run(["invite"]) do + Common.start_pleroma() + + with {:ok, token} <- Pleroma.UserInviteToken.create_token() do + Mix.shell().info("Generated user invite token") + + url = + Pleroma.Web.Router.Helpers.redirect_url( + Pleroma.Web.Endpoint, + :registration_page, + token.token + ) + + IO.puts(url) + else + _ -> + Mix.shell().error("Could not create invite token.") + 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}") + 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_moderator}") + 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}") + 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 diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index cc68d9669..8705395a4 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -15,7 +15,6 @@ defmodule Pleroma.Application do # See http://elixir-lang.org/docs/stable/elixir/Application.html # for more information on OTP Applications - @env Mix.env() def start(_type, _args) do import Cachex.Spec @@ -66,7 +65,8 @@ defmodule Pleroma.Application do ), worker(Pleroma.Web.Federator.RetryQueue, []), worker(Pleroma.Web.Federator, []), - worker(Pleroma.Stats, []) + worker(Pleroma.Stats, []), + worker(Pleroma.Web.Push, []) ] ++ streamer_child() ++ chat_child() ++ diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex index 0a5e1d5ce..bedad99d6 100644 --- a/lib/pleroma/emoji.ex +++ b/lib/pleroma/emoji.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Emoji do """ use GenServer @ets __MODULE__.Ets - @ets_options [:set, :protected, :named_table, {:read_concurrency, true}] + @ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}] @doc false def start_link() do @@ -165,7 +165,7 @@ defmodule Pleroma.Emoji do defp load_from_file_stream(stream) do stream - |> Stream.map(&String.strip/1) + |> Stream.map(&String.trim/1) |> Stream.map(fn line -> case String.split(line, ~r/,\s*/) do [name, file] -> {name, file} diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex index 25ed38f34..c57bd3bf8 100644 --- a/lib/pleroma/filter.ex +++ b/lib/pleroma/filter.ex @@ -1,10 +1,10 @@ defmodule Pleroma.Filter do use Ecto.Schema import Ecto.{Changeset, Query} - alias Pleroma.{User, Repo, Activity} + alias Pleroma.{User, Repo} schema "filters" do - belongs_to(:user, Pleroma.User) + belongs_to(:user, User) field(:filter_id, :integer) field(:hide, :boolean, default: false) field(:whole_word, :boolean, default: true) @@ -26,7 +26,7 @@ defmodule Pleroma.Filter do Repo.one(query) end - def get_filters(%Pleroma.User{id: user_id} = user) do + def get_filters(%User{id: user_id} = _user) do query = from( f in Pleroma.Filter, @@ -38,9 +38,9 @@ defmodule Pleroma.Filter do def create(%Pleroma.Filter{user_id: user_id, filter_id: nil} = filter) do # If filter_id wasn't given, use the max filter_id for this user plus 1. - # XXX This could result in a race condition if a user tries to add two - # different filters for their account from two different clients at the - # same time, but that should be unlikely. + # XXX This could result in a race condition if a user tries to add two + # different filters for their account from two different clients at the + # same time, but that should be unlikely. max_id_query = from( diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index 1a5c07c8a..5b03e9aeb 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -114,7 +114,7 @@ defmodule Pleroma.Formatter do subs = subs ++ - Enum.map(mentions, fn {match, %User{ap_id: ap_id, info: info}, uuid} -> + Enum.map(mentions, fn {match, %User{id: id, ap_id: ap_id, info: info}, uuid} -> ap_id = if is_binary(info.source_data["url"]) do info.source_data["url"] @@ -125,7 +125,7 @@ defmodule Pleroma.Formatter do short_match = String.split(match, "@") |> tl() |> hd() {uuid, - "<span><a class='mention' href='#{ap_id}'>@<span>#{short_match}</span></a></span>"} + "<span><a data-user='#{id}' class='mention' href='#{ap_id}'>@<span>#{short_match}</span></a></span>"} end) {subs, uuid_text} @@ -147,7 +147,11 @@ defmodule Pleroma.Formatter do subs = subs ++ Enum.map(tags, fn {tag_text, tag, uuid} -> - url = "<a href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{tag_text}</a>" + url = + "<a data-tag='#{tag}' href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{ + tag_text + }</a>" + {uuid, url} end) diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex index 3b0569a99..4d582ef25 100644 --- a/lib/pleroma/gopher/server.ex +++ b/lib/pleroma/gopher/server.ex @@ -22,7 +22,7 @@ defmodule Pleroma.Gopher.Server do :gopher, 100, :ranch_tcp, - [port: port], + [ip: ip, port: port], __MODULE__.ProtocolHandler, [] ) diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index 1b920d7fd..8a0333461 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -45,7 +45,7 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do Meta.strip_comments() # links - Meta.allow_tag_with_uri_attributes("a", ["href"], @valid_schemes) + Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes) Meta.allow_tag_with_these_attributes("a", ["name", "title"]) # paragraphs and linebreaks @@ -86,7 +86,7 @@ defmodule Pleroma.HTML.Scrubber.Default do Meta.remove_cdata_sections_before_scrub() Meta.strip_comments() - Meta.allow_tag_with_uri_attributes("a", ["href"], @valid_schemes) + Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes) Meta.allow_tag_with_these_attributes("a", ["name", "title"]) Meta.allow_tag_with_these_attributes("abbr", ["title"]) @@ -166,7 +166,7 @@ defmodule Pleroma.HTML.Transform.MediaProxy do {"src", media_url} end - def scrub_attribute(tag, attribute), do: attribute + def scrub_attribute(_tag, attribute), do: attribute def scrub({"img", attributes, children}) do attributes = @@ -177,9 +177,9 @@ defmodule Pleroma.HTML.Transform.MediaProxy do {"img", attributes, children} end - def scrub({:comment, children}), do: "" + def scrub({:comment, _children}), do: "" def scrub({tag, attributes, children}), do: {tag, attributes, children} - def scrub({tag, children}), do: children + def scrub({_tag, children}), do: children def scrub(text), do: text end diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex new file mode 100644 index 000000000..7b11060b2 --- /dev/null +++ b/lib/pleroma/http/connection.ex @@ -0,0 +1,32 @@ +defmodule Pleroma.HTTP.Connection do + @moduledoc """ + Connection for http-requests. + """ + + @hackney_options [ + pool: :default, + timeout: 10000, + recv_timeout: 20000, + follow_redirect: true + ] + @adapter Application.get_env(:tesla, :adapter) + + @doc """ + Configure a client connection + + # Returns + + Tesla.Env.client + """ + @spec new(Keyword.t()) :: Tesla.Env.client() + def new(opts \\ []) do + Tesla.client([], {@adapter, hackney_options(opts)}) + end + + # fetch Hackney options + # + defp hackney_options(opts) do + options = Keyword.get(opts, :adapter, []) + @hackney_options ++ options + end +end diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex index e64266ae7..3c0256575 100644 --- a/lib/pleroma/http/http.ex +++ b/lib/pleroma/http/http.ex @@ -1,14 +1,42 @@ defmodule Pleroma.HTTP do - require HTTPoison + @moduledoc """ + """ + + alias Pleroma.HTTP.Connection + alias Pleroma.HTTP.RequestBuilder, as: Builder + + @doc """ + Builds and perform http request. + + # Arguments: + `method` - :get, :post, :put, :delete + `url` + `body` + `headers` - a keyworld list of headers, e.g. `[{"content-type", "text/plain"}]` + `options` - custom, per-request middleware or adapter options + + # Returns: + `{:ok, %Tesla.Env{}}` or `{:error, error}` + + """ def request(method, url, body \\ "", headers \\ [], options \\ []) do options = process_request_options(options) |> process_sni_options(url) - HTTPoison.request(method, url, body, headers, options) + %{} + |> Builder.method(method) + |> Builder.headers(headers) + |> Builder.opts(options) + |> Builder.url(url) + |> Builder.add_param(:body, :body, body) + |> Enum.into([]) + |> (&Tesla.request(Connection.new(), &1)).() end + defp process_sni_options(options, nil), do: options + defp process_sni_options(options, url) do uri = URI.parse(url) host = uri.host |> to_charlist() @@ -22,7 +50,7 @@ defmodule Pleroma.HTTP do def process_request_options(options) do config = Application.get_env(:pleroma, :http, []) proxy = Keyword.get(config, :proxy_url, nil) - options = options ++ [hackney: [pool: :default]] + options = options ++ [adapter: [pool: :default]] case proxy do nil -> options @@ -30,8 +58,19 @@ defmodule Pleroma.HTTP do end end - def get(url, headers \\ [], options \\ []), do: request(:get, url, "", headers, options) + @doc """ + Performs GET request. + + See `Pleroma.HTTP.request/5` + """ + def get(url, headers \\ [], options \\ []), + do: request(:get, url, "", headers, options) + + @doc """ + Performs POST request. + See `Pleroma.HTTP.request/5` + """ def post(url, body, headers \\ [], options \\ []), do: request(:post, url, body, headers, options) end diff --git a/lib/pleroma/http/request_builder.ex b/lib/pleroma/http/request_builder.ex new file mode 100644 index 000000000..5aee2b8ae --- /dev/null +++ b/lib/pleroma/http/request_builder.ex @@ -0,0 +1,126 @@ +defmodule Pleroma.HTTP.RequestBuilder do + @moduledoc """ + Helper functions for building Tesla requests + """ + + @doc """ + Specify the request method when building a request + + ## Parameters + + - request (Map) - Collected request options + - m (atom) - Request method + + ## Returns + + Map + """ + @spec method(map(), atom) :: map() + def method(request, m) do + Map.put_new(request, :method, m) + end + + @doc """ + Specify the request method when building a request + + ## Parameters + + - request (Map) - Collected request options + - u (String) - Request URL + + ## Returns + + Map + """ + @spec url(map(), String.t()) :: map() + def url(request, u) do + Map.put_new(request, :url, u) + end + + @doc """ + Add headers to the request + """ + @spec headers(map(), list(tuple)) :: map() + def headers(request, h) do + Map.put_new(request, :headers, h) + end + + @doc """ + Add custom, per-request middleware or adapter options to the request + """ + @spec opts(map(), Keyword.t()) :: map() + def opts(request, options) do + Map.put_new(request, :opts, options) + end + + @doc """ + Add optional parameters to the request + + ## Parameters + + - request (Map) - Collected request options + - definitions (Map) - Map of parameter name to parameter location. + - options (KeywordList) - The provided optional parameters + + ## Returns + + Map + """ + @spec add_optional_params(map(), %{optional(atom) => atom}, keyword()) :: map() + def add_optional_params(request, _, []), do: request + + def add_optional_params(request, definitions, [{key, value} | tail]) do + case definitions do + %{^key => location} -> + request + |> add_param(location, key, value) + |> add_optional_params(definitions, tail) + + _ -> + add_optional_params(request, definitions, tail) + end + end + + @doc """ + Add optional parameters to the request + + ## Parameters + + - request (Map) - Collected request options + - location (atom) - Where to put the parameter + - key (atom) - The name of the parameter + - value (any) - The value of the parameter + + ## Returns + + Map + """ + @spec add_param(map(), atom, atom, any()) :: map() + def add_param(request, :body, :body, value), do: Map.put(request, :body, value) + + def add_param(request, :body, key, value) do + request + |> Map.put_new_lazy(:body, &Tesla.Multipart.new/0) + |> Map.update!( + :body, + &Tesla.Multipart.add_field(&1, key, Poison.encode!(value), + headers: [{:"Content-Type", "application/json"}] + ) + ) + end + + def add_param(request, :file, name, path) do + request + |> Map.put_new_lazy(:body, &Tesla.Multipart.new/0) + |> Map.update!(:body, &Tesla.Multipart.add_file(&1, path, name: name)) + end + + def add_param(request, :form, name, value) do + request + |> Map.update(:body, %{name => value}, &Map.put(&1, name, value)) + end + + def add_param(request, location, key, value) do + Map.update(request, location, [{key, value}], &(&1 ++ [{key, value}])) + end +end diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex index 891c73f5a..c5bf3e083 100644 --- a/lib/pleroma/list.ex +++ b/lib/pleroma/list.ex @@ -23,7 +23,7 @@ defmodule Pleroma.List do |> validate_required([:following]) end - def for_user(user, opts) do + def for_user(user, _opts) do query = from( l in Pleroma.List, @@ -46,7 +46,7 @@ defmodule Pleroma.List do Repo.one(query) end - def get_following(%Pleroma.List{following: following} = list) do + def get_following(%Pleroma.List{following: following} = _list) do q = from( u in User, diff --git a/lib/pleroma/mime.ex b/lib/pleroma/mime.ex index 99063ded6..2cb3d8bd1 100644 --- a/lib/pleroma/mime.ex +++ b/lib/pleroma/mime.ex @@ -33,10 +33,10 @@ defmodule Pleroma.MIME do {:ok, check_mime_type(head)} end - def mime_type(<<_::binary>>), do: {:ok, @default} - def bin_mime_type(_), do: :error + def mime_type(<<_::binary>>), do: {:ok, @default} + defp fix_extension(filename, content_type) do parts = String.split(filename, ".") diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index a3aeb1221..47f6b6ee7 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -110,6 +110,7 @@ defmodule Pleroma.Notification do notification = %Notification{user_id: user.id, activity: activity} {:ok, notification} = Repo.insert(notification) Pleroma.Web.Streamer.stream("user", notification) + Pleroma.Web.Push.send(notification) notification end end @@ -117,7 +118,7 @@ defmodule Pleroma.Notification do def get_notified_from_activity(activity, local_only \\ true) def get_notified_from_activity( - %Activity{data: %{"to" => _, "type" => type} = data} = activity, + %Activity{data: %{"to" => _, "type" => type} = _data} = activity, local_only ) when type in ["Create", "Like", "Announce", "Follow"] do @@ -130,18 +131,18 @@ defmodule Pleroma.Notification do User.get_users_from_set(recipients, local_only) end - def get_notified_from_activity(_, local_only), do: [] + def get_notified_from_activity(_, _local_only), do: [] defp maybe_notify_to_recipients( recipients, - %Activity{data: %{"to" => to, "type" => type}} = activity + %Activity{data: %{"to" => to, "type" => _type}} = _activity ) do recipients ++ to end defp maybe_notify_mentioned_recipients( recipients, - %Activity{data: %{"to" => to, "type" => type} = data} = activity + %Activity{data: %{"to" => _to, "type" => type} = data} = _activity ) when type == "Create" do object = Object.normalize(data["object"]) diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index 03a75dfbd..31c8dd5bd 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -1,6 +1,6 @@ defmodule Pleroma.Object do use Ecto.Schema - alias Pleroma.{Repo, Object, Activity} + alias Pleroma.{Repo, Object, User, Activity} import Ecto.{Query, Changeset} schema "objects" do @@ -31,6 +31,13 @@ defmodule Pleroma.Object do def normalize(ap_id) when is_binary(ap_id), do: Object.get_by_ap_id(ap_id) def normalize(_), do: nil + # Owned objects can only be mutated by their owner + def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}), + do: actor == ap_id + + # Legacy objects can be mutated by anybody + def authorize_mutation(%Object{}, %User{}), do: true + if Mix.env() == :test do def get_cached_by_ap_id(ap_id) do get_by_ap_id(ap_id) diff --git a/lib/pleroma/plugs/authentication_plug.ex b/lib/pleroma/plugs/authentication_plug.ex index 3ac301b97..b240ff29f 100644 --- a/lib/pleroma/plugs/authentication_plug.ex +++ b/lib/pleroma/plugs/authentication_plug.ex @@ -26,14 +26,7 @@ defmodule Pleroma.Plugs.AuthenticationPlug do end end - def call( - %{ - assigns: %{ - auth_credentials: %{password: password} - } - } = conn, - _ - ) do + def call(%{assigns: %{auth_credentials: %{password: _}}} = conn, _) do Pbkdf2.dummy_checkpw() conn end diff --git a/lib/pleroma/plugs/basic_auth_decoder_plug.ex b/lib/pleroma/plugs/basic_auth_decoder_plug.ex index fc8fcee98..f7ebf7db2 100644 --- a/lib/pleroma/plugs/basic_auth_decoder_plug.ex +++ b/lib/pleroma/plugs/basic_auth_decoder_plug.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Plugs.BasicAuthDecoderPlug do options end - def call(conn, opts) do + def call(conn, _opts) do with ["Basic " <> header] <- get_req_header(conn, "authorization"), {:ok, userinfo} <- Base.decode64(header), [username, password] <- String.split(userinfo, ":", parts: 2) do diff --git a/lib/pleroma/plugs/federating_plug.ex b/lib/pleroma/plugs/federating_plug.ex index 4108d90af..f0442ca15 100644 --- a/lib/pleroma/plugs/federating_plug.ex +++ b/lib/pleroma/plugs/federating_plug.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.FederatingPlug do options end - def call(conn, opts) do + def call(conn, _opts) do if Keyword.get(Application.get_env(:pleroma, :instance), :federating) do conn else diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index 4c32653ea..f34f2364b 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -4,11 +4,11 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do def init(opts), do: opts - def call(conn, options) do + def call(conn, _options) do if Config.get([:http_security, :enabled]) do - conn = - merge_resp_headers(conn, headers()) - |> maybe_send_sts_header(Config.get([:http_security, :sts])) + conn + |> merge_resp_headers(headers()) + |> maybe_send_sts_header(Config.get([:http_security, :sts])) else conn end @@ -42,7 +42,7 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do "script-src 'self'", "connect-src 'self' " <> String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"), "manifest-src 'self'", - if @protocol == "https" do + if protocol == "https" do "upgrade-insecure-requests" end ] diff --git a/lib/pleroma/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex index 630f15eec..8b99a74d1 100644 --- a/lib/pleroma/plugs/oauth_plug.ex +++ b/lib/pleroma/plugs/oauth_plug.ex @@ -1,30 +1,70 @@ defmodule Pleroma.Plugs.OAuthPlug do import Plug.Conn - alias Pleroma.User - alias Pleroma.Repo - alias Pleroma.Web.OAuth.Token + import Ecto.Query - def init(options) do - options - end + alias Pleroma.{ + User, + Repo, + Web.OAuth.Token + } + + @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i") + + def init(options), do: options def call(%{assigns: %{user: %User{}}} = conn, _), do: conn def call(conn, _) do - token = - case get_req_header(conn, "authorization") do - ["Bearer " <> header] -> header - _ -> get_session(conn, :oauth_token) - end - - with token when not is_nil(token) <- token, - %Token{user_id: user_id} <- Repo.get_by(Token, token: token), - %User{} = user <- Repo.get(User, user_id), - false <- !!user.info.deactivated do + with {:ok, token} <- fetch_token(conn), + {:ok, user} <- fetch_user(token) do conn + |> assign(:token, token) |> assign(:user, user) else _ -> conn end end + + # Gets user by token + # + @spec fetch_user(String.t()) :: {:ok, User.t()} | nil + defp fetch_user(token) do + query = from(q in Token, where: q.token == ^token, preload: [:user]) + + with %Token{user: %{info: %{deactivated: false} = _} = user} <- Repo.one(query) do + {:ok, user} + end + end + + # Gets token from session by :oauth_token key + # + @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} + defp fetch_token_from_session(conn) do + case get_session(conn, :oauth_token) do + nil -> :no_token_found + token -> {:ok, token} + end + end + + # Gets token from headers + # + @spec fetch_token(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} + defp fetch_token(%Plug.Conn{} = conn) do + headers = get_req_header(conn, "authorization") + + with :no_token_found <- fetch_token(headers), + do: fetch_token_from_session(conn) + end + + @spec fetch_token(Keyword.t()) :: :no_token_found | {:ok, String.t()} + defp fetch_token([]), do: :no_token_found + + defp fetch_token([token | tail]) do + trimmed_token = String.trim(token) + + case Regex.run(@realm_reg, trimmed_token) do + [_, match] -> {:ok, String.trim(match)} + _ -> fetch_token(tail) + end + end end diff --git a/lib/pleroma/plugs/session_authentication_plug.ex b/lib/pleroma/plugs/session_authentication_plug.ex index 904a27952..aed619432 100644 --- a/lib/pleroma/plugs/session_authentication_plug.ex +++ b/lib/pleroma/plugs/session_authentication_plug.ex @@ -1,6 +1,5 @@ defmodule Pleroma.Plugs.SessionAuthenticationPlug do import Plug.Conn - alias Pleroma.User def init(options) do options diff --git a/lib/pleroma/plugs/uploaded_media.ex b/lib/pleroma/plugs/uploaded_media.ex index 994cc8bf6..7e1e84126 100644 --- a/lib/pleroma/plugs/uploaded_media.ex +++ b/lib/pleroma/plugs/uploaded_media.ex @@ -8,10 +8,6 @@ defmodule Pleroma.Plugs.UploadedMedia do @behaviour Plug # no slashes @path "media" - @cache_control %{ - default: "public, max-age=1209600", - error: "public, must-revalidate, max-age=160" - } def init(_opts) do static_plug_opts = diff --git a/lib/pleroma/plugs/user_fetcher_plug.ex b/lib/pleroma/plugs/user_fetcher_plug.ex index 9cbaaf40a..e24785ad1 100644 --- a/lib/pleroma/plugs/user_fetcher_plug.ex +++ b/lib/pleroma/plugs/user_fetcher_plug.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Plugs.UserFetcherPlug do options end - def call(conn, options) do + def call(conn, _options) do with %{auth_credentials: %{username: username}} <- conn.assigns, {:ok, %User{} = user} <- user_fetcher(username) do conn diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex index ad9dc82fe..7f328d00d 100644 --- a/lib/pleroma/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy.ex @@ -56,7 +56,7 @@ defmodule Pleroma.ReverseProxy do @hackney Application.get_env(:pleroma, :hackney, :hackney) @httpoison Application.get_env(:pleroma, :httpoison, HTTPoison) - @default_hackney_options [{:follow_redirect, true}] + @default_hackney_options [] @inline_content_types [ "image/gif", @@ -85,7 +85,9 @@ defmodule Pleroma.ReverseProxy do | {:redirect_on_failure, boolean()} @spec call(Plug.Conn.t(), url :: String.t(), [option()]) :: Plug.Conn.t() - def call(conn = %{method: method}, url, opts \\ []) when method in @methods do + def call(_conn, _url, _opts \\ []) + + def call(conn = %{method: method}, url, opts) when method in @methods do hackney_opts = @default_hackney_options |> Keyword.merge(Keyword.get(opts, :http, [])) @@ -240,24 +242,23 @@ defmodule Pleroma.ReverseProxy do end defp build_req_headers(headers, opts) do - headers = - headers - |> downcase_headers() - |> Enum.filter(fn {k, _} -> k in @keep_req_headers end) - |> (fn headers -> - headers = headers ++ Keyword.get(opts, :req_headers, []) - - if Keyword.get(opts, :keep_user_agent, false) do - List.keystore( - headers, - "user-agent", - 0, - {"user-agent", Pleroma.Application.user_agent()} - ) - else - headers - end - end).() + headers + |> downcase_headers() + |> Enum.filter(fn {k, _} -> k in @keep_req_headers end) + |> (fn headers -> + headers = headers ++ Keyword.get(opts, :req_headers, []) + + if Keyword.get(opts, :keep_user_agent, false) do + List.keystore( + headers, + "user-agent", + 0, + {"user-agent", Pleroma.Application.user_agent()} + ) + else + headers + end + end).() end defp build_resp_headers(headers, opts) do @@ -268,7 +269,7 @@ defmodule Pleroma.ReverseProxy do |> (fn headers -> headers ++ Keyword.get(opts, :resp_headers, []) end).() end - defp build_resp_cache_headers(headers, opts) do + defp build_resp_cache_headers(headers, _opts) do has_cache? = Enum.any?(headers, fn {k, _} -> k in @resp_cache_headers end) if has_cache? do diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index bf2c60102..07031ac58 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -128,19 +128,18 @@ defmodule Pleroma.Upload do opts end - opts = - if Pleroma.Config.get([:instance, :dedupe_media]) == true && - !Enum.member?(opts.filters, Pleroma.Upload.Filter.Dedupe) do - Logger.warn(""" - Pleroma: configuration `:instance, :dedupe_media` is deprecated, please instead set: + if Pleroma.Config.get([:instance, :dedupe_media]) == true && + !Enum.member?(opts.filters, Pleroma.Upload.Filter.Dedupe) do + Logger.warn(""" + Pleroma: configuration `:instance, :dedupe_media` is deprecated, please instead set: - :pleroma, Pleroma.Upload, [filters: [Pleroma.Upload.Filter.Dedupe]] - """) + :pleroma, Pleroma.Upload, [filters: [Pleroma.Upload.Filter.Dedupe]] + """) - Map.put(opts, :filters, opts.filters ++ [Pleroma.Upload.Filter.Dedupe]) - else - opts - end + Map.put(opts, :filters, opts.filters ++ [Pleroma.Upload.Filter.Dedupe]) + else + opts + end end defp prepare_upload(%Plug.Upload{} = file, opts) do @@ -216,7 +215,5 @@ defmodule Pleroma.Upload do |> Path.join() end - defp url_from_spec({:url, url}) do - url - end + defp url_from_spec(_base_url, {:url, url}), do: url end diff --git a/lib/pleroma/upload/filter/anonymize_filename.ex b/lib/pleroma/upload/filter/anonymize_filename.ex index a83e764e5..39eed7af3 100644 --- a/lib/pleroma/upload/filter/anonymize_filename.ex +++ b/lib/pleroma/upload/filter/anonymize_filename.ex @@ -1,10 +1,23 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilename do - @moduledoc "Replaces the original filename with a randomly generated string." + @moduledoc """ + Replaces the original filename with a pre-defined text or randomly generated string. + + Should be used after `Pleroma.Upload.Filter.Dedupe`. + """ @behaviour Pleroma.Upload.Filter def filter(upload) do extension = List.last(String.split(upload.name, ".")) - string = Base.url_encode64(:crypto.strong_rand_bytes(10), padding: false) - {:ok, %Pleroma.Upload{upload | name: string <> "." <> extension}} + name = Pleroma.Config.get([__MODULE__, :text], random(extension)) + {:ok, %Pleroma.Upload{upload | name: name}} + end + + defp random(extension) do + string = + 10 + |> :crypto.strong_rand_bytes() + |> Base.url_encode64(padding: false) + + string <> "." <> extension end end diff --git a/lib/pleroma/upload/filter/dedupe.ex b/lib/pleroma/upload/filter/dedupe.ex index 28091a627..0657b2c8d 100644 --- a/lib/pleroma/upload/filter/dedupe.ex +++ b/lib/pleroma/upload/filter/dedupe.ex @@ -1,10 +1,11 @@ defmodule Pleroma.Upload.Filter.Dedupe do @behaviour Pleroma.Upload.Filter + alias Pleroma.Upload - def filter(upload = %Pleroma.Upload{name: name, tempfile: path}) do + def filter(upload = %Upload{name: name}) do extension = String.split(name, ".") |> List.last() shasum = :crypto.hash(:sha256, File.read!(upload.tempfile)) |> Base.encode16(case: :lower) filename = shasum <> "." <> extension - {:ok, %Pleroma.Upload{upload | id: shasum, path: filename}} + {:ok, %Upload{upload | id: shasum, path: filename}} end end diff --git a/lib/pleroma/upload/filter/mogrify.ex b/lib/pleroma/upload/filter/mogrify.ex index d6ed471ed..f106bd4b1 100644 --- a/lib/pleroma/upload/filter/mogrify.ex +++ b/lib/pleroma/upload/filter/mogrify.ex @@ -1,5 +1,5 @@ defmodule Pleroma.Upload.Filter.Mogrify do - @behaviour Pleroma.Uploader.Filter + @behaviour Pleroma.Upload.Filter @type conversion :: action :: String.t() | {action :: String.t(), opts :: String.t()} @type conversions :: conversion() | [conversion()] diff --git a/lib/pleroma/uploaders/local.ex b/lib/pleroma/uploaders/local.ex index 434a6b515..2994bcd51 100644 --- a/lib/pleroma/uploaders/local.ex +++ b/lib/pleroma/uploaders/local.ex @@ -1,8 +1,6 @@ defmodule Pleroma.Uploaders.Local do @behaviour Pleroma.Uploaders.Uploader - alias Pleroma.Web - def get_file(_) do {:ok, {:static_dir, upload_path()}} end diff --git a/lib/pleroma/uploaders/mdii.ex b/lib/pleroma/uploaders/mdii.ex index 35d36d3e4..f06755056 100644 --- a/lib/pleroma/uploaders/mdii.ex +++ b/lib/pleroma/uploaders/mdii.ex @@ -12,15 +12,15 @@ defmodule Pleroma.Uploaders.MDII do end def put_file(upload) do - cgi = Pleroma.Config.get([Pleroma.Uploaders.MDII, :cgi]) - files = Pleroma.Config.get([Pleroma.Uploaders.MDII, :files]) + cgi = Config.get([Pleroma.Uploaders.MDII, :cgi]) + files = Config.get([Pleroma.Uploaders.MDII, :files]) {:ok, file_data} = File.read(upload.tempfile) extension = String.split(upload.name, ".") |> List.last() query = "#{cgi}?#{extension}" - with {:ok, %{status_code: 200, body: body}} <- @httpoison.post(query, file_data) do + with {:ok, %{status: 200, body: body}} <- @httpoison.post(query, file_data) do remote_file_name = String.split(body) |> List.first() public_url = "#{files}/#{remote_file_name}.#{extension}" {:ok, {:url, public_url}} diff --git a/lib/pleroma/uploaders/swift/keystone.ex b/lib/pleroma/uploaders/swift/keystone.ex index e578b3c61..4aed977b1 100644 --- a/lib/pleroma/uploaders/swift/keystone.ex +++ b/lib/pleroma/uploaders/swift/keystone.ex @@ -25,10 +25,10 @@ defmodule Pleroma.Uploaders.Swift.Keystone do ["Content-Type": "application/json"], hackney: [:insecure] ) do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> + {:ok, %Tesla.Env{status: 200, body: body}} -> body["access"]["token"]["id"] - {:ok, %HTTPoison.Response{status_code: _}} -> + {:ok, %Tesla.Env{status: _}} -> "" end end diff --git a/lib/pleroma/uploaders/swift/swift.ex b/lib/pleroma/uploaders/swift/swift.ex index 1e865f101..d4e758bbb 100644 --- a/lib/pleroma/uploaders/swift/swift.ex +++ b/lib/pleroma/uploaders/swift/swift.ex @@ -9,14 +9,13 @@ defmodule Pleroma.Uploaders.Swift.Client do end def upload_file(filename, body, content_type) do - object_url = Pleroma.Config.get!([Pleroma.Uploaders.Swift, :object_url]) token = Pleroma.Uploaders.Swift.Keystone.get_token() case put("#{filename}", body, "X-Auth-Token": token, "Content-Type": content_type) do - {:ok, %HTTPoison.Response{status_code: 201}} -> + {:ok, %Tesla.Env{status: 201}} -> {:ok, {:file, filename}} - {:ok, %HTTPoison.Response{status_code: 401}} -> + {:ok, %Tesla.Env{status: 401}} -> {:error, "Unauthorized, Bad Token"} {:error, _} -> diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 76712b4bf..6a267ee58 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -4,9 +4,13 @@ defmodule Pleroma.User do import Ecto.{Changeset, Query} alias Pleroma.{Repo, User, Object, Web, Activity, Notification} alias Comeonin.Pbkdf2 + alias Pleroma.Formatter + alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils alias Pleroma.Web.{OStatus, Websub, OAuth} alias Pleroma.Web.ActivityPub.{Utils, ActivityPub} + @type t :: %__MODULE__{} + schema "users" do field(:bio, :string) field(:email, :string) @@ -21,6 +25,7 @@ defmodule Pleroma.User do field(:local, :boolean, default: true) field(:follower_address, :string) field(:search_distance, :float, virtual: true) + field(:tags, {:array, :string}, default: []) field(:last_refreshed_at, :naive_datetime) has_many(:notifications, Notification) embeds_one(:info, Pleroma.User.Info) @@ -60,10 +65,6 @@ defmodule Pleroma.User do |> validate_required([:following]) end - def info_changeset(struct, params \\ %{}) do - raise "NOT VALID ANYMORE" - end - def user_info(%User{} = user) do oneself = if user.local, do: 1, else: 0 @@ -175,6 +176,7 @@ defmodule Pleroma.User do |> validate_format(:email, @email_regex) |> validate_length(:bio, max: 1000) |> validate_length(:name, min: 1, max: 100) + |> put_change(:info, %Pleroma.User.Info{}) if changeset.valid? do hashed = Pbkdf2.hashpwsalt(changeset.changes[:password]) @@ -217,7 +219,7 @@ defmodule Pleroma.User do end end - def maybe_follow(%User{} = follower, %User{info: info} = followed) do + def maybe_follow(%User{} = follower, %User{info: _info} = followed) do if not following?(follower, followed) do follow(follower, followed) else @@ -279,6 +281,7 @@ defmodule Pleroma.User do end end + @spec following?(User.t(), User.t()) :: boolean def following?(%User{} = follower, %User{} = followed) do Enum.member?(follower.following, followed.follower_address) end @@ -802,4 +805,56 @@ defmodule Pleroma.User do :error end end + + def parse_bio(bio, user \\ %User{info: %{source_data: %{}}}) do + mentions = Formatter.parse_mentions(bio) + tags = Formatter.parse_tags(bio) + + emoji = + (user.info.source_data["tag"] || []) + |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end) + |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} -> + {String.trim(name, ":"), url} + end) + + CommonUtils.format_input(bio, mentions, tags, "text/plain") |> Formatter.emojify(emoji) + end + + def tag(user_identifiers, tags) when is_list(user_identifiers) do + Repo.transaction(fn -> + for user_identifier <- user_identifiers, do: tag(user_identifier, tags) + end) + end + + def tag(nickname, tags) when is_binary(nickname), + do: tag(User.get_by_nickname(nickname), tags) + + def tag(%User{} = user, tags), + do: update_tags(user, Enum.uniq(user.tags ++ normalize_tags(tags))) + + def untag(user_identifiers, tags) when is_list(user_identifiers) do + Repo.transaction(fn -> + for user_identifier <- user_identifiers, do: untag(user_identifier, tags) + end) + end + + def untag(nickname, tags) when is_binary(nickname), + do: untag(User.get_by_nickname(nickname), tags) + + def untag(%User{} = user, tags), do: update_tags(user, user.tags -- normalize_tags(tags)) + + defp update_tags(%User{} = user, new_tags) do + {:ok, updated_user} = + user + |> change(%{tags: new_tags}) + |> Repo.update() + + updated_user + end + + defp normalize_tags(tags) do + [tags] + |> List.flatten() + |> Enum.map(&String.downcase(&1)) + end end diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 94d403bf7..d81b45b8d 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -4,7 +4,7 @@ defmodule Pleroma.User.Info do embedded_schema do field(:banner, :map, default: %{}) - field(:background, :string, default: nil) + field(:background, :map, default: %{}) field(:source_data, :map, default: %{}) field(:note_count, :integer, default: 0) field(:follower_count, :integer, default: 0) @@ -24,6 +24,7 @@ defmodule Pleroma.User.Info do field(:topic, :string, default: nil) field(:hub, :string, default: nil) field(:salmon, :string, default: nil) + field(:hide_network, :boolean, default: false) # Found in the wild # ap_id -> Where is this used? @@ -134,7 +135,9 @@ defmodule Pleroma.User.Info do :locked, :no_rich_text, :default_scope, - :banner + :banner, + :hide_network, + :background ]) end @@ -146,6 +149,11 @@ defmodule Pleroma.User.Info do ]) end + def mastodon_settings_update(info, params) do + info + |> cast(params, [:settings]) + end + def set_source_data(info, source_data) do params = %{source_data: source_data} diff --git a/lib/pleroma/user_invite_token.ex b/lib/pleroma/user_invite_token.ex index 48ee1019a..ce804f78e 100644 --- a/lib/pleroma/user_invite_token.ex +++ b/lib/pleroma/user_invite_token.ex @@ -3,7 +3,8 @@ defmodule Pleroma.UserInviteToken do import Ecto.Changeset - alias Pleroma.{User, UserInviteToken, Repo} + alias Pleroma.UserInviteToken + alias Pleroma.Repo schema "user_invite_tokens" do field(:token, :string) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 7e207c620..31455343c 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -574,7 +574,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do def upload(file, opts \\ []) do with {:ok, data} <- Upload.store(file, opts) do - Repo.insert(%Object{data: data}) + obj_data = + if opts[:actor] do + Map.put(data, "actor", opts[:actor]) + else + data + end + + Repo.insert(%Object{data: obj_data}) end end @@ -762,13 +769,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do Logger.info("Fetching #{id} via AP") with true <- String.starts_with?(id, "http"), - {:ok, %{body: body, status_code: code}} when code in 200..299 <- + {:ok, %{body: body, status: code}} when code in 200..299 <- @httpoison.get( id, - [Accept: "application/activity+json"], - follow_redirect: true, - timeout: 10000, - recv_timeout: 20000 + [{:Accept, "application/activity+json"}] ), {:ok, data} <- Jason.decode(body), :ok <- Transmogrifier.contain_origin_from_id(id, data) do @@ -795,7 +799,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end # guard - def entire_thread_visible_for_user?(nil, user), do: false + def entire_thread_visible_for_user?(nil, _user), do: false # child def entire_thread_visible_for_user?( diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 3570a75cb..0317f3c8c 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -141,7 +141,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do json(conn, "error") end - def relay(conn, params) do + def relay(conn, _params) do with %User{} = user <- Relay.get_actor(), {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do conn diff --git a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex new file mode 100644 index 000000000..6fa48454a --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex @@ -0,0 +1,40 @@ +defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do + alias Pleroma.Object + + @behaviour Pleroma.Web.ActivityPub.MRF + + @reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless]) + def filter_by_summary( + %{"summary" => parent_summary} = _parent, + %{"summary" => child_summary} = child + ) + when not is_nil(child_summary) and byte_size(child_summary) > 0 and + not is_nil(parent_summary) and byte_size(parent_summary) > 0 do + if (child_summary == parent_summary and not Regex.match?(@reply_prefix, child_summary)) or + (Regex.match?(@reply_prefix, parent_summary) && + Regex.replace(@reply_prefix, parent_summary, "") == child_summary) do + Map.put(child, "summary", "re: " <> child_summary) + else + child + end + end + + def filter_by_summary(_parent, child), do: child + + def filter(%{"type" => activity_type} = object) when activity_type == "Create" do + child = object["object"] + in_reply_to = Object.normalize(child["inReplyTo"]) + + child = + if(in_reply_to, + do: filter_by_summary(in_reply_to.data, child), + else: child + ) + + object = Map.put(object, "object", child) + + {:ok, object} + end + + def filter(object), do: {:ok, object} +end diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index 86dcf5080..12fc3b181 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -23,7 +23,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do defp check_media_removal( %{host: actor_host} = _actor_info, - %{"type" => "Create", "object" => %{"attachement" => child_attachment}} = object + %{"type" => "Create", "object" => %{"attachment" => child_attachment}} = object ) when length(child_attachment) > 0 do object = diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 17b063609..e6af4b211 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -37,9 +37,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do @doc """ Checks that an imported AP object's actor matches the domain it came from. """ - def contain_origin(id, %{"actor" => nil}), do: :error + def contain_origin(_id, %{"actor" => nil}), do: :error - def contain_origin(id, %{"actor" => actor} = params) do + def contain_origin(id, %{"actor" => _actor} = params) do id_uri = URI.parse(id) actor_uri = URI.parse(get_actor(params)) @@ -50,9 +50,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def contain_origin_from_id(id, %{"id" => nil}), do: :error + def contain_origin_from_id(_id, %{"id" => nil}), do: :error - def contain_origin_from_id(id, %{"id" => other_id} = params) do + def contain_origin_from_id(id, %{"id" => other_id} = _params) do id_uri = URI.parse(id) other_uri = URI.parse(other_id) @@ -266,6 +266,32 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def fix_content_map(object), do: object + defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do + with true <- id =~ "follows", + %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id), + %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do + {:ok, activity} + else + _ -> {:error, nil} + end + end + + defp mastodon_follow_hack(_, _), do: {:error, nil} + + defp get_follow_activity(follow_object, followed) do + with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object), + {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do + {:ok, activity} + else + # Can't find the activity. This might a Mastodon 2.3 "Accept" + {:activity, nil} -> + mastodon_follow_hack(follow_object, followed) + + _ -> + {:error, nil} + end + end + # disallow objects with bogus IDs def handle_incoming(%{"id" => nil}), do: :error def handle_incoming(%{"id" => ""}), do: :error @@ -331,34 +357,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do - with true <- id =~ "follows", - %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id), - %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do - {:ok, activity} - else - _ -> {:error, nil} - end - end - - defp mastodon_follow_hack(_), do: {:error, nil} - - defp get_follow_activity(follow_object, followed) do - with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object), - {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do - {:ok, activity} - else - # Can't find the activity. This might a Mastodon 2.3 "Accept" - {:activity, nil} -> - mastodon_follow_hack(follow_object, followed) - - _ -> - {:error, nil} - end - end - def handle_incoming( - %{"type" => "Accept", "object" => follow_object, "actor" => actor, "id" => id} = data + %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data ) do with actor <- get_actor(data), %User{} = followed <- User.get_or_fetch_by_ap_id(actor), @@ -374,7 +374,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do local: false }) do if not User.following?(follower, followed) do - {:ok, follower} = User.follow(follower, followed) + {:ok, _follower} = User.follow(follower, followed) end {:ok, activity} @@ -384,7 +384,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end def handle_incoming( - %{"type" => "Reject", "object" => follow_object, "actor" => actor, "id" => id} = data + %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data ) do with actor <- get_actor(data), %User{} = followed <- User.get_or_fetch_by_ap_id(actor), @@ -408,7 +408,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end def handle_incoming( - %{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = data + %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data ) do with actor <- get_actor(data), %User{} = actor <- User.get_or_fetch_by_ap_id(actor), @@ -421,7 +421,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end def handle_incoming( - %{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data + %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data ) do with actor <- get_actor(data), %User{} = actor <- User.get_or_fetch_by_ap_id(actor), @@ -492,7 +492,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do %{ "type" => "Undo", "object" => %{"type" => "Announce", "object" => object_id}, - "actor" => actor, + "actor" => _actor, "id" => id } = data ) do @@ -520,7 +520,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do User.unfollow(follower, followed) {:ok, activity} else - e -> :error + _e -> :error end end @@ -539,12 +539,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do User.unblock(blocker, blocked) {:ok, activity} else - e -> :error + _e -> :error end end def handle_incoming( - %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = data + %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data ) do with true <- Pleroma.Config.get([:activitypub, :accept_blocks]), %User{local: true} = blocked = User.get_cached_by_ap_id(blocked), @@ -554,7 +554,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do User.block(blocker, blocked) {:ok, activity} else - e -> :error + _e -> :error end end @@ -562,7 +562,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do %{ "type" => "Undo", "object" => %{"type" => "Like", "object" => object_id}, - "actor" => actor, + "actor" => _actor, "id" => id } = data ) do diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 549148989..074622f2b 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -292,7 +292,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do """ def make_follow_data( %User{ap_id: follower_id}, - %User{ap_id: followed_id} = followed, + %User{ap_id: followed_id} = _followed, activity_id ) do data = %{ diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index aaa777602..869934172 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -82,7 +82,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do query = from(user in query, select: [:ap_id]) following = Repo.all(query) - collection(following, "#{user.ap_id}/following", page) + collection(following, "#{user.ap_id}/following", page, !user.info.hide_network) |> Map.merge(Utils.make_json_ld_header()) end @@ -95,7 +95,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do "id" => "#{user.ap_id}/following", "type" => "OrderedCollection", "totalItems" => length(following), - "first" => collection(following, "#{user.ap_id}/following", 1) + "first" => collection(following, "#{user.ap_id}/following", 1, !user.info.hide_network) } |> Map.merge(Utils.make_json_ld_header()) end @@ -105,7 +105,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do query = from(user in query, select: [:ap_id]) followers = Repo.all(query) - collection(followers, "#{user.ap_id}/followers", page) + collection(followers, "#{user.ap_id}/followers", page, !user.info.hide_network) |> Map.merge(Utils.make_json_ld_header()) end @@ -118,7 +118,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do "id" => "#{user.ap_id}/followers", "type" => "OrderedCollection", "totalItems" => length(followers), - "first" => collection(followers, "#{user.ap_id}/followers", 1) + "first" => collection(followers, "#{user.ap_id}/followers", 1, !user.info.hide_network) } |> Map.merge(Utils.make_json_ld_header()) end @@ -172,7 +172,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do end end - def collection(collection, iri, page, total \\ nil) do + def collection(collection, iri, page, show_items \\ true, total \\ nil) do offset = (page - 1) * 10 items = Enum.slice(collection, offset, 10) items = Enum.map(items, fn user -> user.ap_id end) @@ -183,7 +183,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do "type" => "OrderedCollectionPage", "partOf" => iri, "totalItems" => total, - "orderedItems" => items + "orderedItems" => if(show_items, do: items, else: []) } if offset < total do diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 2c67d9cda..06c3c7c81 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -3,6 +3,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.{User, Repo} alias Pleroma.Web.ActivityPub.Relay + import Pleroma.Web.ControllerHelper, only: [json_response: 3] + require Logger action_fallback(:errors) @@ -40,6 +42,16 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do |> json(new_user.nickname) end + def tag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do + with {:ok, _} <- User.tag(nicknames, tags), + do: json_response(conn, :no_content, "") + end + + def untag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do + with {:ok, _} <- User.untag(nicknames, tags), + do: json_response(conn, :no_content, "") + end + def right_add(conn, %{"permission_group" => permission_group, "nickname" => nickname}) when permission_group in ["moderator", "admin"] do user = User.get_by_nickname(nickname) @@ -51,13 +63,19 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do info_cng = User.Info.admin_api_update(user.info, info) cng = - Ecto.Changeset.change(user) + user + |> Ecto.Changeset.change() |> Ecto.Changeset.put_embed(:info, info_cng) - {:ok, user} = User.update_and_set_cache(cng) + {:ok, _user} = User.update_and_set_cache(cng) + + json(conn, info) + end + def right_add(conn, _) do conn - |> json(info) + |> put_status(404) + |> json(%{error: "No such permission_group"}) end def right_get(conn, %{"nickname" => nickname}) do @@ -70,12 +88,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do }) end - def right_add(conn, _) do - conn - |> put_status(404) - |> json(%{error: "No such permission_group"}) - end - def right_delete( %{assigns: %{user: %User{:nickname => admin_nickname}}} = conn, %{ @@ -101,10 +113,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng) - {:ok, user} = User.update_and_set_cache(cng) + {:ok, _user} = User.update_and_set_cache(cng) - conn - |> json(info) + json(conn, info) end end @@ -115,32 +126,28 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end def relay_follow(conn, %{"relay_url" => target}) do - {status, message} = Relay.follow(target) - - if status == :ok do - conn - |> json(target) + with {:ok, _message} <- Relay.follow(target) do + json(conn, target) else - conn - |> put_status(500) - |> json(target) + _ -> + conn + |> put_status(500) + |> json(target) end end def relay_unfollow(conn, %{"relay_url" => target}) do - {status, message} = Relay.unfollow(target) - - if status == :ok do - conn - |> json(target) + with {:ok, _message} <- Relay.unfollow(target) do + json(conn, target) else - conn - |> put_status(500) - |> json(target) + _ -> + conn + |> put_status(500) + |> json(target) end end - @shortdoc "Get a account registeration invite token (base64 string)" + @doc "Get a account registeration invite token (base64 string)" def get_invite_token(conn, _params) do {:ok, token} = Pleroma.UserInviteToken.create_token() @@ -148,7 +155,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do |> json(token.token) end - @shortdoc "Get a password reset token (base64 string) for given nickname" + @doc "Get a password reset token (base64 string) for given nickname" def get_password_reset(conn, %{"nickname" => nickname}) do (%User{local: true} = user) = User.get_by_nickname(nickname) {:ok, token} = Pleroma.PasswordResetToken.create_token(user) diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 728f24c7e..8f2625cef 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -122,7 +122,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do |> Formatter.finalize() end - def format_input(text, mentions, tags, "text/html") do + def format_input(text, mentions, _tags, "text/html") do text |> Formatter.html_escape("text/html") |> String.replace(~r/\r?\n/, "<br>") @@ -236,7 +236,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do end end - def emoji_from_profile(%{info: info} = user) do + def emoji_from_profile(%{info: _info} = user) do (Formatter.get_emoji(user.bio) ++ Formatter.get_emoji(user.name)) |> Enum.map(fn {shortcode, url} -> %{ diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex new file mode 100644 index 000000000..ddf958811 --- /dev/null +++ b/lib/pleroma/web/controller_helper.ex @@ -0,0 +1,9 @@ +defmodule Pleroma.Web.ControllerHelper do + use Pleroma.Web, :controller + + def json_response(conn, status, json) do + conn + |> put_status(status) + |> json(json) + end +end diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex index ac3d7c132..a9c7aecd5 100644 --- a/lib/pleroma/web/federator/federator.ex +++ b/lib/pleroma/web/federator/federator.ex @@ -13,7 +13,6 @@ defmodule Pleroma.Web.Federator do @websub Application.get_env(:pleroma, :websub) @ostatus Application.get_env(:pleroma, :ostatus) - @httpoison Application.get_env(:pleroma, :httpoison) @max_jobs 20 def init(args) do @@ -134,7 +133,7 @@ defmodule Pleroma.Web.Federator do def handle( :publish_single_websub, - %{xml: xml, topic: topic, callback: callback, secret: secret} = params + %{xml: _xml, topic: _topic, callback: _callback, secret: _secret} = params ) do case Websub.publish_one(params) do {:ok, _} -> @@ -151,7 +150,7 @@ defmodule Pleroma.Web.Federator do end if Mix.env() == :test do - def enqueue(type, payload, priority \\ 1) do + def enqueue(type, payload, _priority \\ 1) do if Pleroma.Config.get([:instance, :federating]) do handle(type, payload) end diff --git a/lib/pleroma/web/federator/retry_queue.ex b/lib/pleroma/web/federator/retry_queue.ex index 06c094f26..510b4315d 100644 --- a/lib/pleroma/web/federator/retry_queue.ex +++ b/lib/pleroma/web/federator/retry_queue.ex @@ -1,13 +1,8 @@ defmodule Pleroma.Web.Federator.RetryQueue do use GenServer - alias Pleroma.Web.{WebFinger, Websub} - alias Pleroma.Web.ActivityPub.ActivityPub + require Logger - @websub Application.get_env(:pleroma, :websub) - @ostatus Application.get_env(:pleroma, :websub) - @httpoison Application.get_env(:pleroma, :websub) - @instance Application.get_env(:pleroma, :websub) # initial timeout, 5 min @initial_timeout 30_000 @max_retries 5 @@ -17,7 +12,15 @@ defmodule Pleroma.Web.Federator.RetryQueue do end def start_link() do - GenServer.start_link(__MODULE__, %{delivered: 0, dropped: 0}, name: __MODULE__) + enabled = Pleroma.Config.get([:retry_queue, :enabled], false) + + if enabled do + Logger.info("Starting retry queue") + GenServer.start_link(__MODULE__, %{delivered: 0, dropped: 0}, name: __MODULE__) + else + Logger.info("Retry queue disabled") + :ignore + end end def enqueue(data, transport, retries \\ 0) do @@ -38,7 +41,7 @@ defmodule Pleroma.Web.Federator.RetryQueue do Process.send_after( __MODULE__, {:send, data, transport, retries}, - growth_function(retries) + timeout ) {:noreply, state} @@ -54,7 +57,7 @@ defmodule Pleroma.Web.Federator.RetryQueue do {:ok, _} -> {:noreply, %{state | delivered: delivery_count + 1}} - {:error, reason} -> + {:error, _reason} -> enqueue(data, transport, retries) {:noreply, state} end diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index d19d55044..5c8602322 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -2,13 +2,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do use Pleroma.Web, :controller alias Pleroma.{Repo, Object, Activity, User, Notification, Stats} alias Pleroma.Web - alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView, FilterView} + + alias Pleroma.Web.MastodonAPI.{ + StatusView, + AccountView, + MastodonView, + ListView, + FilterView, + PushSubscriptionView + } + alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.CommonAPI alias Pleroma.Web.OAuth.{Authorization, Token, App} alias Pleroma.Web.MediaProxy - alias Comeonin.Pbkdf2 + import Ecto.Query require Logger @@ -55,7 +64,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do user_params = %{} |> add_if_present(params, "display_name", :name) - |> add_if_present(params, "note", :bio) + |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end) |> add_if_present(params, "avatar", :avatar, fn value -> with %Plug.Upload{} <- value, {:ok, object} <- ActivityPub.upload(value, type: :avatar) do @@ -428,38 +437,33 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array. - def relationships(%{assigns: %{user: user}} = conn, _) do - conn - |> json([]) - end + def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, []) - def update_media(%{assigns: %{user: _}} = conn, data) do + def update_media(%{assigns: %{user: user}} = conn, data) do with %Object{} = object <- Repo.get(Object, data["id"]), + true <- Object.authorize_mutation(object, user), true <- is_binary(data["description"]), description <- data["description"] do new_data = %{object.data | "name" => description} - change = Object.change(object, %{data: new_data}) - {:ok, _} = Repo.update(change) - - data = - new_data - |> Map.put("id", object.id) + {:ok, _} = + object + |> Object.change(%{data: new_data}) + |> Repo.update() - render(conn, StatusView, "attachment.json", %{attachment: data}) + attachment_data = Map.put(new_data, "id", object.id) + render(conn, StatusView, "attachment.json", %{attachment: attachment_data}) end end - def upload(%{assigns: %{user: _}} = conn, %{"file" => file} = data) do - with {:ok, object} <- ActivityPub.upload(file, description: Map.get(data, "description")) do - change = Object.change(object, %{data: object.data}) - {:ok, object} = Repo.update(change) - - objdata = - object.data - |> Map.put("id", object.id) - - render(conn, StatusView, "attachment.json", %{attachment: objdata}) + def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do + with {:ok, object} <- + ActivityPub.upload(file, + actor: User.ap_id(user), + description: Map.get(data, "description") + ) do + attachment_data = Map.put(object.data, "id", object.id) + render(conn, StatusView, "attachment.json", %{attachment: attachment_data}) end end @@ -502,17 +506,30 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity}) end - # TODO: Pagination - def followers(conn, %{"id" => id}) do + def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do with %User{} = user <- Repo.get(User, id), {:ok, followers} <- User.get_followers(user) do + followers = + cond do + for_user && user.id == for_user.id -> followers + user.info.hide_network -> [] + true -> followers + end + render(conn, AccountView, "accounts.json", %{users: followers, as: :user}) end end - def following(conn, %{"id" => id}) do + def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do with %User{} = user <- Repo.get(User, id), {:ok, followers} <- User.get_friends(user) do + followers = + cond do + for_user && user.id == for_user.id -> followers + user.info.hide_network -> [] + true -> followers + end + render(conn, AccountView, "accounts.json", %{users: followers, as: :user}) end end @@ -830,7 +847,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do - with %Pleroma.List{title: title, following: following} <- Pleroma.List.get(id, user) do + with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do params = params |> Map.put("type", "Create") @@ -959,15 +976,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do - with new_info <- Map.put(user.info, "settings", settings), - change <- User.info_changeset(user, %{info: new_info}), - {:ok, _user} <- User.update_and_set_cache(change) do - conn - |> json(%{}) + info_cng = User.Info.mastodon_settings_update(user.info, settings) + + with changeset <- User.update_changeset(user), + changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng), + {:ok, _user} <- User.update_and_set_cache(changeset) do + json(conn, %{}) else e -> - conn - |> json(%{error: inspect(e)}) + json(conn, %{error: inspect(e)}) end end @@ -1149,6 +1166,33 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do json(conn, %{}) end + def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do + Pleroma.Web.Push.Subscription.delete_if_exists(user, token) + {:ok, subscription} = Pleroma.Web.Push.Subscription.create(user, token, params) + view = PushSubscriptionView.render("push_subscription.json", subscription: subscription) + json(conn, view) + end + + def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do + subscription = Pleroma.Web.Push.Subscription.get(user, token) + view = PushSubscriptionView.render("push_subscription.json", subscription: subscription) + json(conn, view) + end + + def update_push_subscription( + %{assigns: %{user: user, token: token}} = conn, + params + ) do + {:ok, subscription} = Pleroma.Web.Push.Subscription.update(user, token, params) + view = PushSubscriptionView.render("push_subscription.json", subscription: subscription) + json(conn, view) + end + + def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do + {:ok, _response} = Pleroma.Web.Push.Subscription.delete(user, token) + json(conn, %{}) + end + def errors(conn, _) do conn |> put_status(500) @@ -1168,8 +1212,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do user = user.nickname url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user) - with {:ok, %{status_code: 200, body: body}} <- - @httpoison.get(url, [], timeout: timeout, recv_timeout: timeout), + with {:ok, %{status: 200, body: body}} <- + @httpoison.get( + url, + [], + adapter: [ + timeout: timeout, + recv_timeout: timeout + ] + ), {:ok, data} <- Jason.decode(body) do data2 = Enum.slice(data, 0, limit) diff --git a/lib/pleroma/web/mastodon_api/mastodon_socket.ex b/lib/pleroma/web/mastodon_api/mastodon_socket.ex index f3c13d1aa..755ac5730 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_socket.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_socket.ex @@ -5,12 +5,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonSocket do alias Pleroma.{User, Repo} transport( - :streaming, + :websocket, Phoenix.Transports.WebSocket.Raw, # We never receive data. timeout: :infinity ) + @spec connect(params :: map(), Phoenix.Socket.t()) :: {:ok, Phoenix.Socket.t()} | :error def connect(%{"access_token" => token} = params, socket) do with %Token{user_id: user_id} <- Repo.get_by(Token, token: token), %User{} = user <- Repo.get(User, user_id), @@ -52,16 +53,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonSocket do _ -> stream end - with socket = - socket - |> assign(:topic, topic) do - Pleroma.Web.Streamer.add_socket(topic, socket) - {:ok, socket} - else - _e -> :error - end + socket = + socket + |> assign(:topic, topic) + + Pleroma.Web.Streamer.add_socket(topic, socket) + {:ok, socket} end + def connect(_params, _socket), do: :error + def id(_), do: nil def handle(:text, message, _state) do diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index bcfa8836e..ebcf9230b 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -58,6 +58,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do note: "", privacy: user_info.default_scope, sensitive: false + }, + + # Pleroma extension + pleroma: %{ + tags: user.tags } } end diff --git a/lib/pleroma/web/mastodon_api/views/mastodon_view.ex b/lib/pleroma/web/mastodon_api/views/mastodon_view.ex index 370fad374..1fd05d9f1 100644 --- a/lib/pleroma/web/mastodon_api/views/mastodon_view.ex +++ b/lib/pleroma/web/mastodon_api/views/mastodon_view.ex @@ -1,5 +1,4 @@ defmodule Pleroma.Web.MastodonAPI.MastodonView do use Pleroma.Web, :view import Phoenix.HTML - import Phoenix.HTML.Form end diff --git a/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex b/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex new file mode 100644 index 000000000..c8b95d14c --- /dev/null +++ b/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex @@ -0,0 +1,11 @@ +defmodule Pleroma.Web.MastodonAPI.PushSubscriptionView do + use Pleroma.Web, :view + + def render("push_subscription.json", %{subscription: subscription}) do + %{ + id: to_string(subscription.id), + endpoint: subscription.endpoint, + alerts: Map.get(subscription.data, "alerts") + } + end +end diff --git a/lib/pleroma/web/media_proxy/controller.ex b/lib/pleroma/web/media_proxy/controller.ex index 81ea5d510..63140feb9 100644 --- a/lib/pleroma/web/media_proxy/controller.ex +++ b/lib/pleroma/web/media_proxy/controller.ex @@ -2,15 +2,14 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do use Pleroma.Web, :controller alias Pleroma.{Web.MediaProxy, ReverseProxy} - @default_proxy_opts [max_body_length: 25 * 1_048_576] + @default_proxy_opts [max_body_length: 25 * 1_048_576, http: [follow_redirect: true]] def remote(conn, params = %{"sig" => sig64, "url" => url64}) do - with config <- Pleroma.Config.get([:media_proxy]), + with config <- Pleroma.Config.get([:media_proxy], []), true <- Keyword.get(config, :enabled, false), {:ok, url} <- MediaProxy.decode_url(sig64, url64), - filename <- Path.basename(URI.parse(url).path), :ok <- filename_matches(Map.has_key?(params, "filename"), conn.request_path, url) do - ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_length)) + ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts)) else false -> send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404)) @@ -24,11 +23,17 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do end def filename_matches(has_filename, path, url) do - filename = MediaProxy.filename(url) + filename = + url + |> MediaProxy.filename() + |> URI.decode() - cond do - has_filename && filename && Path.basename(path) != filename -> {:wrong_filename, filename} - true -> :ok + path = URI.decode(path) + + if has_filename && filename && Path.basename(path) != filename do + {:wrong_filename, filename} + else + :ok end end end diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index 28aacb0b1..902ab1b77 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -14,7 +14,14 @@ defmodule Pleroma.Web.MediaProxy do url else secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base] - base64 = Base.url_encode64(url, @base64_opts) + + # The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice. + base64 = + url + |> URI.decode() + |> URI.encode() + |> Base.url_encode64(@base64_opts) + sig = :crypto.hmac(:sha, secret, base64) sig64 = sig |> Base.url_encode64(@base64_opts) diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index 2ea75cf16..277dc6ba1 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -71,23 +71,28 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do %{} end - features = [ - "pleroma_api", - "mastodon_api", - "mastodon_api_streaming", - if Keyword.get(media_proxy, :enabled) do - "media_proxy" - end, - if Keyword.get(gopher, :enabled) do - "gopher" - end, - if Keyword.get(chat, :enabled) do - "chat" - end, - if Keyword.get(suggestions, :enabled) do - "suggestions" - end - ] + features = + [ + "pleroma_api", + "mastodon_api", + "mastodon_api_streaming", + if Keyword.get(media_proxy, :enabled) do + "media_proxy" + end, + if Keyword.get(gopher, :enabled) do + "gopher" + end, + if Keyword.get(chat, :enabled) do + "chat" + end, + if Keyword.get(suggestions, :enabled) do + "suggestions" + end, + if Keyword.get(instance, :allow_relay) do + "relay" + end + ] + |> Enum.filter(& &1) response = %{ version: "2.0", diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index d03c8b05a..20c2e799b 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -121,7 +121,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do def token_exchange( conn, - %{"grant_type" => "password", "name" => name, "password" => password} = params + %{"grant_type" => "password", "name" => name, "password" => _password} = params ) do params = params diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex index 6a27f1730..c6440c20e 100644 --- a/lib/pleroma/web/ostatus/ostatus.ex +++ b/lib/pleroma/web/ostatus/ostatus.ex @@ -346,13 +346,10 @@ defmodule Pleroma.Web.OStatus do def fetch_activity_from_atom_url(url) do with true <- String.starts_with?(url, "http"), - {:ok, %{body: body, status_code: code}} when code in 200..299 <- + {:ok, %{body: body, status: code}} when code in 200..299 <- @httpoison.get( url, - [Accept: "application/atom+xml"], - follow_redirect: true, - timeout: 10000, - recv_timeout: 20000 + [{:Accept, "application/atom+xml"}] ) do Logger.debug("Got document from #{url}, handling...") handle_incoming(body) @@ -367,8 +364,7 @@ defmodule Pleroma.Web.OStatus do Logger.debug("Trying to fetch #{url}") with true <- String.starts_with?(url, "http"), - {:ok, %{body: body}} <- - @httpoison.get(url, [], follow_redirect: true, timeout: 10000, recv_timeout: 20000), + {:ok, %{body: body}} <- @httpoison.get(url, []), {:ok, atom_url} <- get_atom_url(body) do fetch_activity_from_atom_url(atom_url) else @@ -379,19 +375,14 @@ defmodule Pleroma.Web.OStatus do end def fetch_activity_from_url(url) do - try do - with {:ok, activities} when length(activities) > 0 <- fetch_activity_from_atom_url(url) do - {:ok, activities} - else - _e -> - with {:ok, activities} <- fetch_activity_from_html_url(url) do - {:ok, activities} - end - end - rescue - e -> - Logger.debug("Couldn't get #{url}: #{inspect(e)}") - {:error, "Couldn't get #{url}: #{inspect(e)}"} + with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url) do + {:ok, activities} + else + _e -> fetch_activity_from_html_url(url) end + rescue + e -> + Logger.debug("Couldn't get #{url}: #{inspect(e)}") + {:error, "Couldn't get #{url}: #{inspect(e)}"} end end diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index af6e22c2b..9dfcf0f95 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -157,7 +157,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do conn, "activity+json", %Activity{data: %{"type" => "Create"}} = activity, - user + _user ) do object = Object.normalize(activity.data["object"]) @@ -166,7 +166,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do |> json(ObjectView.render("object.json", %{object: object})) end - defp represent_activity(conn, "activity+json", _, _) do + defp represent_activity(_conn, "activity+json", _, _) do {:error, :not_found} end diff --git a/lib/pleroma/web/push/push.ex b/lib/pleroma/web/push/push.ex new file mode 100644 index 000000000..5a873ec19 --- /dev/null +++ b/lib/pleroma/web/push/push.ex @@ -0,0 +1,116 @@ +defmodule Pleroma.Web.Push do + use GenServer + + alias Pleroma.{Repo, User} + alias Pleroma.Web.Push.Subscription + + require Logger + import Ecto.Query + + @types ["Create", "Follow", "Announce", "Like"] + + @gcm_api_key nil + + def start_link() do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + def init(:ok) do + case Application.get_env(:web_push_encryption, :vapid_details) do + nil -> + Logger.warn( + "VAPID key pair is not found. Please, add VAPID configuration to config. Run `mix web_push.gen.keypair` mix task to create a key pair" + ) + + :ignore + + _ -> + {:ok, %{}} + end + end + + def send(notification) do + if Application.get_env(:web_push_encryption, :vapid_details) do + GenServer.cast(Pleroma.Web.Push, {:send, notification}) + end + end + + def handle_cast( + {:send, %{activity: %{data: %{"type" => type}}, user_id: user_id} = notification}, + state + ) + when type in @types do + actor = User.get_cached_by_ap_id(notification.activity.data["actor"]) + body = notification |> format(actor) |> Jason.encode!() + + Subscription + |> where(user_id: ^user_id) + |> Repo.all() + |> Enum.each(fn record -> + subscription = %{ + keys: %{ + p256dh: record.key_p256dh, + auth: record.key_auth + }, + endpoint: record.endpoint + } + + case WebPushEncryption.send_web_push(body, subscription, @gcm_api_key) do + {:ok, %{status_code: code}} when 400 <= code and code < 500 -> + Logger.debug("Removing subscription record") + Repo.delete!(record) + :ok + + {:ok, %{status_code: code}} when 200 <= code and code < 300 -> + :ok + + {:ok, %{status_code: code}} -> + Logger.error("Web Push Nonification failed with code: #{code}") + :error + + _ -> + Logger.error("Web Push Nonification failed with unknown error") + :error + end + end) + + {:noreply, state} + end + + def handle_cast({:send, _}, state) do + Logger.warn("Unknown notification type") + {:noreply, state} + end + + def format(%{activity: %{data: %{"type" => "Create"}}}, actor) do + %{ + title: "New Mention", + body: "@#{actor.nickname} has mentiond you", + icon: User.avatar_url(actor) + } + end + + def format(%{activity: %{data: %{"type" => "Follow"}}}, actor) do + %{ + title: "New Follower", + body: "@#{actor.nickname} has followed you", + icon: User.avatar_url(actor) + } + end + + def format(%{activity: %{data: %{"type" => "Announce"}}}, actor) do + %{ + title: "New Announce", + body: "@#{actor.nickname} has announced your post", + icon: User.avatar_url(actor) + } + end + + def format(%{activity: %{data: %{"type" => "Like"}}}, actor) do + %{ + title: "New Like", + body: "@#{actor.nickname} has liked your post", + icon: User.avatar_url(actor) + } + end +end diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex new file mode 100644 index 000000000..cfab7a98e --- /dev/null +++ b/lib/pleroma/web/push/subscription.ex @@ -0,0 +1,66 @@ +defmodule Pleroma.Web.Push.Subscription do + use Ecto.Schema + import Ecto.Changeset + alias Pleroma.{Repo, User} + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.Push.Subscription + + schema "push_subscriptions" do + belongs_to(:user, User) + belongs_to(:token, Token) + field(:endpoint, :string) + field(:key_p256dh, :string) + field(:key_auth, :string) + field(:data, :map, default: %{}) + + timestamps() + end + + @supported_alert_types ~w[follow favourite mention reblog] + + defp alerts(%{"data" => %{"alerts" => alerts}}) do + alerts = Map.take(alerts, @supported_alert_types) + %{"alerts" => alerts} + end + + def create( + %User{} = user, + %Token{} = token, + %{ + "subscription" => %{ + "endpoint" => endpoint, + "keys" => %{"auth" => key_auth, "p256dh" => key_p256dh} + } + } = params + ) do + Repo.insert(%Subscription{ + user_id: user.id, + token_id: token.id, + endpoint: endpoint, + key_auth: key_auth, + key_p256dh: key_p256dh, + data: alerts(params) + }) + end + + def get(%User{id: user_id}, %Token{id: token_id}) do + Repo.get_by(Subscription, user_id: user_id, token_id: token_id) + end + + def update(user, token, params) do + get(user, token) + |> change(data: alerts(params)) + |> Repo.update() + end + + def delete(user, token) do + Repo.delete(get(user, token)) + end + + def delete_if_exists(user, token) do + case get(user, token) do + nil -> {:ok, nil} + sub -> Repo.delete(sub) + end + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index d6a9d5779..9c06fac4f 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -1,8 +1,6 @@ defmodule Pleroma.Web.Router do use Pleroma.Web, :router - alias Pleroma.{Repo, User, Web.Router} - pipeline :api do plug(:accepts, ["json"]) plug(:fetch_session) @@ -98,6 +96,8 @@ defmodule Pleroma.Web.Router do pipe_through(:admin_api) delete("/user", AdminAPIController, :user_delete) post("/user", AdminAPIController, :user_create) + put("/users/tag", AdminAPIController, :tag_users) + delete("/users/tag", AdminAPIController, :untag_users) get("/permission_group/:nickname", AdminAPIController, :right_get) get("/permission_group/:nickname/:permission_group", AdminAPIController, :right_get) @@ -198,6 +198,11 @@ defmodule Pleroma.Web.Router do put("/filters/:id", MastodonAPIController, :update_filter) delete("/filters/:id", MastodonAPIController, :delete_filter) + post("/push/subscription", MastodonAPIController, :create_push_subscription) + get("/push/subscription", MastodonAPIController, :get_push_subscription) + put("/push/subscription", MastodonAPIController, :update_push_subscription) + delete("/push/subscription", MastodonAPIController, :delete_push_subscription) + get("/suggestions", MastodonAPIController, :suggestions) get("/endorsements", MastodonAPIController, :empty_array) @@ -324,6 +329,7 @@ defmodule Pleroma.Web.Router do post("/statusnet/media/upload", TwitterAPI.Controller, :upload) post("/media/upload", TwitterAPI.Controller, :upload_json) + post("/media/metadata/create", TwitterAPI.Controller, :update_media) post("/favorites/create/:id", TwitterAPI.Controller, :favorite) post("/favorites/create", TwitterAPI.Controller, :favorite) diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex index b98ece6c9..0e2cfddd0 100644 --- a/lib/pleroma/web/salmon/salmon.ex +++ b/lib/pleroma/web/salmon/salmon.ex @@ -158,14 +158,11 @@ defmodule Pleroma.Web.Salmon do end defp send_to_user(%{info: %{salmon: salmon}}, feed, poster) do - with {:ok, %{status_code: code}} <- + with {:ok, %{status: code}} <- poster.( salmon, feed, - [{"Content-Type", "application/magic-envelope+xml"}], - timeout: 10000, - recv_timeout: 20000, - hackney: [pool: :default] + [{"Content-Type", "application/magic-envelope+xml"}] ) do Logger.debug(fn -> "Pushed to #{salmon}, code #{code}" end) else diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index 99b8b7063..29c44e9d5 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -61,8 +61,6 @@ defmodule Pleroma.Web.Streamer do end def handle_cast(%{action: :stream, topic: "list", item: item}, topics) do - author = User.get_cached_by_ap_id(item.data["actor"]) - # filter the recipient list if the activity is not public, see #270. recipient_lists = case ActivityPub.is_public?(item) do diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index b0ed8387e..0ed519ea6 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -6,9 +6,8 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Web.WebFinger alias Pleroma.Web.CommonAPI alias Comeonin.Pbkdf2 - alias Pleroma.{Formatter, Emoji} alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.{Repo, PasswordResetToken, User} + alias Pleroma.{Repo, PasswordResetToken, User, Emoji} def show_password_reset(conn, %{"token" => token}) do with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}), @@ -157,13 +156,17 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do |> send_resp(200, response) _ -> + vapid_public_key = + Keyword.get(Application.get_env(:web_push_encryption, :vapid_details), :public_key) + data = %{ name: Keyword.get(instance, :name), description: Keyword.get(instance, :description), server: Web.base_url(), textlimit: to_string(Keyword.get(instance, :limit)), closed: if(Keyword.get(instance, :registrations_open), do: "0", else: "1"), - private: if(Keyword.get(instance, :public, true), do: "0", else: "1") + private: if(Keyword.get(instance, :public, true), do: "0", else: "1"), + vapidPublicKey: vapid_public_key } pleroma_fe = %{ diff --git a/lib/pleroma/web/twitter_api/representers/activity_representer.ex b/lib/pleroma/web/twitter_api/representers/activity_representer.ex index fbd33f07e..2808192b0 100644 --- a/lib/pleroma/web/twitter_api/representers/activity_representer.ex +++ b/lib/pleroma/web/twitter_api/representers/activity_representer.ex @@ -141,7 +141,7 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do end def to_map( - %Activity{data: %{"object" => %{"content" => content} = object}} = activity, + %Activity{data: %{"object" => %{"content" => _content} = object}} = activity, %{user: user} = opts ) do created_at = object["published"] |> Utils.date_to_asctime() @@ -165,7 +165,7 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do tags = if possibly_sensitive, do: Enum.uniq(["nsfw" | tags]), else: tags - {summary, content} = ActivityView.render_content(object) + {_summary, content} = ActivityView.render_content(object) html = HTML.filter_tags(content, User.html_filter_policy(opts[:for])) @@ -173,7 +173,7 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do video = if object["type"] == "Video" do - vid = [object] + [object] else [] end diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index 39a2974bb..79ea48d86 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -2,18 +2,15 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do alias Pleroma.{UserInviteToken, User, Activity, Repo, Object} alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.TwitterAPI.UserView - alias Pleroma.Web.{OStatus, CommonAPI} - alias Pleroma.Web.MediaProxy + alias Pleroma.Web.CommonAPI import Ecto.Query - @httpoison Application.get_env(:pleroma, :httpoison) - def create_status(%User{} = user, %{"status" => _} = data) do CommonAPI.post(user, data) end def delete(%User{} = user, id) do - with %Activity{data: %{"type" => type}} <- Repo.get(Activity, id), + with %Activity{data: %{"type" => _type}} <- Repo.get(Activity, id), {:ok, activity} <- CommonAPI.delete(id, user) do {:ok, activity} end @@ -37,7 +34,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do def unfollow(%User{} = follower, params) do with {:ok, %User{} = unfollowed} <- get_user(params), - {:ok, follower, follow_activity} <- User.unfollow(follower, unfollowed), + {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed), {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed) do {:ok, follower, unfollowed} else @@ -93,8 +90,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do end end - def upload(%Plug.Upload{} = file, format \\ "xml") do - {:ok, object} = ActivityPub.upload(file) + def upload(%Plug.Upload{} = file, %User{} = user, format \\ "xml") do + {:ok, object} = ActivityPub.upload(file, actor: User.ap_id(user)) url = List.first(object.data["url"]) href = url["href"] @@ -132,7 +129,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do params = %{ nickname: params["nickname"], name: params["fullname"], - bio: params["bio"], + bio: User.parse_bio(params["bio"]), email: params["email"], password: params["password"], password_confirmation: params["confirm"] @@ -244,10 +241,6 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do _activities = Repo.all(q) end - defp make_date do - DateTime.utc_now() |> DateTime.to_iso8601() - end - # DEPRECATED mostly, context objects are now created at insertion time. def context_to_conversation_id(context) do with %Object{id: id} <- Object.get_cached_by_ap_id(context) do diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index ff644dd79..786849aa3 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -1,10 +1,8 @@ defmodule Pleroma.Web.TwitterAPI.Controller do use Pleroma.Web, :controller - alias Pleroma.Formatter alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView, NotificationView} alias Pleroma.Web.CommonAPI - alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils - alias Pleroma.{Repo, Activity, User, Notification} + alias Pleroma.{Repo, Activity, Object, User, Notification} alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils alias Ecto.Changeset @@ -155,7 +153,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do |> render(NotificationView, "notification.json", %{notifications: notifications, for: user}) end - def notifications_read(%{assigns: %{user: user}} = conn, _) do + def notifications_read(%{assigns: %{user: _user}} = conn, _) do bad_request_reply(conn, "You need to specify latest_id") end @@ -226,16 +224,51 @@ defmodule Pleroma.Web.TwitterAPI.Controller do end end - def upload(conn, %{"media" => media}) do - response = TwitterAPI.upload(media) + @doc """ + Updates metadata of uploaded media object. + Derived from [Twitter API endpoint](https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create). + """ + def update_media(%{assigns: %{user: user}} = conn, %{"media_id" => id} = data) do + object = Repo.get(Object, id) + description = get_in(data, ["alt_text", "text"]) || data["name"] || data["description"] + + {conn, status, response_body} = + cond do + !object -> + {halt(conn), :not_found, ""} + + !Object.authorize_mutation(object, user) -> + {halt(conn), :forbidden, "You can only update your own uploads."} + + !is_binary(description) -> + {conn, :not_modified, ""} + + true -> + new_data = Map.put(object.data, "name", description) + + {:ok, _} = + object + |> Object.change(%{data: new_data}) + |> Repo.update() + + {conn, :no_content, ""} + end + + conn + |> put_status(status) + |> json(response_body) + end + + def upload(%{assigns: %{user: user}} = conn, %{"media" => media}) do + response = TwitterAPI.upload(media, user) conn |> put_resp_content_type("application/atom+xml") |> send_resp(200, response) end - def upload_json(conn, %{"media" => media}) do - response = TwitterAPI.upload(media, "json") + def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do + response = TwitterAPI.upload(media, user, "json") conn |> json_reply(200, response) @@ -340,18 +373,32 @@ defmodule Pleroma.Web.TwitterAPI.Controller do end end - def followers(conn, params) do - with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params), + def followers(%{assigns: %{user: for_user}} = conn, params) do + with {:ok, user} <- TwitterAPI.get_user(for_user, params), {:ok, followers} <- User.get_followers(user) do + followers = + cond do + for_user && user.id == for_user.id -> followers + user.info.hide_network -> [] + true -> followers + end + render(conn, UserView, "index.json", %{users: followers, for: conn.assigns[:user]}) else _e -> bad_request_reply(conn, "Can't get followers") end end - def friends(conn, params) do + def friends(%{assigns: %{user: for_user}} = conn, params) do with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params), {:ok, friends} <- User.get_friends(user) do + friends = + cond do + for_user && user.id == for_user.id -> friends + user.info.hide_network -> [] + true -> friends + end + render(conn, UserView, "index.json", %{users: friends, for: conn.assigns[:user]}) else _e -> bad_request_reply(conn, "Can't get friends") @@ -367,7 +414,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do end end - def approve_friend_request(conn, %{"user_id" => uid} = params) do + def approve_friend_request(conn, %{"user_id" => uid} = _params) do with followed <- conn.assigns[:user], uid when is_number(uid) <- String.to_integer(uid), %User{} = follower <- Repo.get(User, uid), @@ -387,7 +434,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do end end - def deny_friend_request(conn, %{"user_id" => uid} = params) do + def deny_friend_request(conn, %{"user_id" => uid} = _params) do with followed <- conn.assigns[:user], uid when is_number(uid) <- String.to_integer(uid), %User{} = follower <- Repo.get(User, uid), @@ -429,7 +476,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do defp build_info_cng(user, params) do info_params = - ["no_rich_text", "locked"] + ["no_rich_text", "locked", "hide_network"] |> Enum.reduce(%{}, fn key, res -> if value = params[key] do Map.put(res, key, value == "true") @@ -448,27 +495,16 @@ defmodule Pleroma.Web.TwitterAPI.Controller do User.Info.profile_update(user.info, info_params) end - defp add_profile_emoji(user, params) do + defp parse_profile_bio(user, params) do if bio = params["description"] do - mentions = Formatter.parse_mentions(bio) - tags = Formatter.parse_tags(bio) - - emoji = - (user.info.source_data["tag"] || []) - |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end) - |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} -> - {String.trim(name, ":"), url} - end) - - bio_html = CommonUtils.format_input(bio, mentions, tags, "text/plain") - Map.put(params, "bio", bio_html |> Formatter.emojify(emoji)) + Map.put(params, "bio", User.parse_bio(bio, user)) else params end end def update_profile(%{assigns: %{user: user}} = conn, params) do - params = add_profile_emoji(user, params) + params = parse_profile_bio(user, params) info_cng = build_info_cng(user, params) with changeset <- User.update_changeset(user, params), diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex index b78024ed7..b3459af9a 100644 --- a/lib/pleroma/web/twitter_api/views/user_view.ex +++ b/lib/pleroma/web/twitter_api/views/user_view.ex @@ -77,7 +77,12 @@ defmodule Pleroma.Web.TwitterAPI.UserView do "locked" => user.info.locked, "default_scope" => user.info.default_scope, "no_rich_text" => user.info.no_rich_text, - "fields" => fields + "fields" => fields, + + # Pleroma extension + "pleroma" => %{ + "tags" => user.tags + } } if assigns[:token] do diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex index eaee3a8c6..0ff3b8b5f 100644 --- a/lib/pleroma/web/web_finger/web_finger.ex +++ b/lib/pleroma/web/web_finger/web_finger.ex @@ -220,8 +220,8 @@ defmodule Pleroma.Web.WebFinger do end def find_lrdd_template(domain) do - with {:ok, %{status_code: status_code, body: body}} when status_code in 200..299 <- - @httpoison.get("http://#{domain}/.well-known/host-meta", [], follow_redirect: true) do + with {:ok, %{status: status, body: body}} when status in 200..299 <- + @httpoison.get("http://#{domain}/.well-known/host-meta", []) do get_template_from_xml(body) else _ -> @@ -259,7 +259,7 @@ defmodule Pleroma.Web.WebFinger do [Accept: "application/xrd+xml,application/jrd+json"], follow_redirect: true ), - {:ok, %{status_code: status_code, body: body}} when status_code in 200..299 <- response do + {:ok, %{status: status, body: body}} when status in 200..299 <- response do doc = XML.parse_document(body) if doc != :error do diff --git a/lib/pleroma/web/websub/websub.ex b/lib/pleroma/web/websub/websub.ex index 905d8d658..8cb07006f 100644 --- a/lib/pleroma/web/websub/websub.ex +++ b/lib/pleroma/web/websub/websub.ex @@ -173,7 +173,7 @@ defmodule Pleroma.Web.Websub do def gather_feed_data(topic, getter \\ &@httpoison.get/1) do with {:ok, response} <- getter.(topic), - status_code when status_code in 200..299 <- response.status_code, + status when status in 200..299 <- response.status, body <- response.body, doc <- XML.parse_document(body), uri when not is_nil(uri) <- XML.string_from_xpath("/feed/author[1]/uri", doc), @@ -221,7 +221,7 @@ defmodule Pleroma.Web.Websub do task = Task.async(websub_checker) - with {:ok, %{status_code: 202}} <- + with {:ok, %{status: 202}} <- poster.(websub.hub, {:form, data}, "Content-type": "application/x-www-form-urlencoded"), {:ok, websub} <- Task.yield(task, timeout) do {:ok, websub} @@ -257,17 +257,14 @@ defmodule Pleroma.Web.Websub do signature = sign(secret || "", xml) Logger.info(fn -> "Pushing #{topic} to #{callback}" end) - with {:ok, %{status_code: code}} <- + with {:ok, %{status: code}} <- @httpoison.post( callback, xml, [ {"Content-Type", "application/atom+xml"}, {"X-Hub-Signature", "sha1=#{signature}"} - ], - timeout: 10000, - recv_timeout: 20000, - hackney: [pool: :default] + ] ) do Logger.info(fn -> "Pushed to #{callback}, code #{code}" end) {:ok, code} diff --git a/lib/pleroma/web/xml/xml.ex b/lib/pleroma/web/xml/xml.ex index da3f68ecb..63d3302e0 100644 --- a/lib/pleroma/web/xml/xml.ex +++ b/lib/pleroma/web/xml/xml.ex @@ -28,12 +28,12 @@ defmodule Pleroma.Web.XML do |> :xmerl_scan.string() doc - catch - :exit, _error -> + rescue + _e -> Logger.debug("Couldn't parse XML: #{inspect(text)}") :error - rescue - e -> + catch + :exit, _error -> Logger.debug("Couldn't parse XML: #{inspect(text)}") :error end |