aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorEgor Kislitsyn <egor@kislitsyn.com>2018-12-06 19:55:58 +0700
committerEgor Kislitsyn <egor@kislitsyn.com>2018-12-06 19:55:58 +0700
commit8b4397c704147bcc5ca12ab60dde32f2b6e11a41 (patch)
treef1d26585be6673b495d3f4b8bea4deb5f0283d42 /lib
parent04a48286e69704bf83429b85dbcdb70863bdcff1 (diff)
parent52ce368562de919f1806dfd5235642caf0666e16 (diff)
downloadpleroma-8b4397c704147bcc5ca12ab60dde32f2b6e11a41.tar.gz
Merge branch 'develop' into feature/compat/push-subscriptions
# Conflicts: # lib/mix/tasks/sample_config.eex # lib/pleroma/web/twitter_api/controllers/util_controller.ex # mix.exs # mix.lock
Diffstat (limited to 'lib')
-rw-r--r--lib/mix/tasks/deactivate_user.ex8
-rw-r--r--lib/mix/tasks/generate_config.ex10
-rw-r--r--lib/mix/tasks/generate_invite_token.ex9
-rw-r--r--lib/mix/tasks/generate_password_reset.ex8
-rw-r--r--lib/mix/tasks/make_moderator.ex9
-rw-r--r--lib/mix/tasks/reactivate_user.ex19
-rw-r--r--lib/mix/tasks/register_user.ex8
-rw-r--r--lib/mix/tasks/relay_follow.ex17
-rw-r--r--lib/mix/tasks/relay_unfollow.ex18
-rw-r--r--lib/mix/tasks/rm_user.ex10
-rw-r--r--lib/mix/tasks/sample_config.eex10
-rw-r--r--lib/mix/tasks/sample_psql.eex7
-rw-r--r--lib/mix/tasks/set_admin.ex32
-rw-r--r--lib/mix/tasks/set_locked.ex11
-rw-r--r--lib/mix/tasks/unsubscribe_user.ex38
-rw-r--r--lib/pleroma/activity.ex6
-rw-r--r--lib/pleroma/application.ex41
-rw-r--r--lib/pleroma/config.ex42
-rw-r--r--lib/pleroma/emoji.ex194
-rw-r--r--lib/pleroma/filter.ex28
-rw-r--r--lib/pleroma/formatter.ex170
-rw-r--r--lib/pleroma/gopher/server.ex15
-rw-r--r--lib/pleroma/html.ex26
-rw-r--r--lib/pleroma/http/http.ex1
-rw-r--r--lib/pleroma/list.ex19
-rw-r--r--lib/pleroma/notification.ex78
-rw-r--r--lib/pleroma/object.ex20
-rw-r--r--lib/pleroma/plugs/federating_plug.ex18
-rw-r--r--lib/pleroma/plugs/http_security_plug.ex63
-rw-r--r--lib/pleroma/plugs/user_is_admin_plug.ex19
-rw-r--r--lib/pleroma/upload.ex122
-rw-r--r--lib/pleroma/uploaders/mdii.ex26
-rw-r--r--lib/pleroma/uploaders/s3.ex20
-rw-r--r--lib/pleroma/uploaders/swift/keystone.ex11
-rw-r--r--lib/pleroma/uploaders/swift/swift.ex6
-rw-r--r--lib/pleroma/user.ex109
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex97
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub_controller.ex49
-rw-r--r--lib/pleroma/web/activity_pub/mrf/normalize_markup.ex4
-rw-r--r--lib/pleroma/web/activity_pub/mrf/reject_non_public.ex10
-rw-r--r--lib/pleroma/web/activity_pub/mrf/simple_policy.ex88
-rw-r--r--lib/pleroma/web/activity_pub/mrf/user_allowlist.ex23
-rw-r--r--lib/pleroma/web/activity_pub/relay.ex14
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex230
-rw-r--r--lib/pleroma/web/activity_pub/utils.ex85
-rw-r--r--lib/pleroma/web/activity_pub/views/object_view.ex43
-rw-r--r--lib/pleroma/web/activity_pub/views/user_view.ex2
-rw-r--r--lib/pleroma/web/admin_api/admin_api_controller.ex158
-rw-r--r--lib/pleroma/web/channels/user_socket.ex7
-rw-r--r--lib/pleroma/web/common_api/common_api.ex25
-rw-r--r--lib/pleroma/web/common_api/utils.ex63
-rw-r--r--lib/pleroma/web/endpoint.ex19
-rw-r--r--lib/pleroma/web/federator/federator.ex80
-rw-r--r--lib/pleroma/web/federator/retry_queue.ex71
-rw-r--r--lib/pleroma/web/mastodon_api/mastodon_api_controller.ex150
-rw-r--r--lib/pleroma/web/mastodon_api/mastodon_socket.ex33
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex11
-rw-r--r--lib/pleroma/web/mastodon_api/views/status_view.ex23
-rw-r--r--lib/pleroma/web/media_proxy/controller.ex44
-rw-r--r--lib/pleroma/web/media_proxy/media_proxy.ex7
-rw-r--r--lib/pleroma/web/nodeinfo/nodeinfo_controller.ex73
-rw-r--r--lib/pleroma/web/oauth/authorization.ex10
-rw-r--r--lib/pleroma/web/oauth/oauth_controller.ex51
-rw-r--r--lib/pleroma/web/oauth/token.ex10
-rw-r--r--lib/pleroma/web/ostatus/ostatus.ex15
-rw-r--r--lib/pleroma/web/ostatus/ostatus_controller.ex20
-rw-r--r--lib/pleroma/web/router.ex115
-rw-r--r--lib/pleroma/web/streamer.ex44
-rw-r--r--lib/pleroma/web/templates/layout/app.html.eex4
-rw-r--r--lib/pleroma/web/templates/mastodon_api/mastodon/login.html.eex11
-rw-r--r--lib/pleroma/web/twitter_api/controllers/util_controller.ex58
-rw-r--r--lib/pleroma/web/twitter_api/representers/activity_representer.ex8
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api.ex25
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api_controller.ex70
-rw-r--r--lib/pleroma/web/twitter_api/views/activity_view.ex12
-rw-r--r--lib/pleroma/web/twitter_api/views/user_view.ex18
-rw-r--r--lib/pleroma/web/web_finger/web_finger_controller.ex2
-rw-r--r--lib/pleroma/web/websub/websub.ex25
-rw-r--r--lib/pleroma/web/websub/websub_controller.ex9
79 files changed, 2382 insertions, 782 deletions
diff --git a/lib/mix/tasks/deactivate_user.ex b/lib/mix/tasks/deactivate_user.ex
index 96b3db6e4..e71ed1ec0 100644
--- a/lib/mix/tasks/deactivate_user.ex
+++ b/lib/mix/tasks/deactivate_user.ex
@@ -2,7 +2,13 @@ defmodule Mix.Tasks.DeactivateUser do
use Mix.Task
alias Pleroma.User
- @shortdoc "Toggle deactivation status for a 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")
diff --git a/lib/mix/tasks/generate_config.ex b/lib/mix/tasks/generate_config.ex
index 58ce3113b..be085d187 100644
--- a/lib/mix/tasks/generate_config.ex
+++ b/lib/mix/tasks/generate_config.ex
@@ -1,7 +1,15 @@
defmodule Mix.Tasks.GenerateConfig do
use Mix.Task
- @shortdoc "Generates a new config"
+ @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")
diff --git a/lib/mix/tasks/generate_invite_token.ex b/lib/mix/tasks/generate_invite_token.ex
index c4daa9a6c..418ef3790 100644
--- a/lib/mix/tasks/generate_invite_token.ex
+++ b/lib/mix/tasks/generate_invite_token.ex
@@ -1,7 +1,14 @@
defmodule Mix.Tasks.GenerateInviteToken do
use Mix.Task
- @shortdoc "Generate invite token for user"
+ @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")
diff --git a/lib/mix/tasks/generate_password_reset.ex b/lib/mix/tasks/generate_password_reset.ex
index 6bf640150..f7f4c4f59 100644
--- a/lib/mix/tasks/generate_password_reset.ex
+++ b/lib/mix/tasks/generate_password_reset.ex
@@ -2,7 +2,13 @@ defmodule Mix.Tasks.GeneratePasswordReset do
use Mix.Task
alias Pleroma.User
- @shortdoc "Generate password reset link for 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")
diff --git a/lib/mix/tasks/make_moderator.ex b/lib/mix/tasks/make_moderator.ex
index a454a958e..15586dc30 100644
--- a/lib/mix/tasks/make_moderator.ex
+++ b/lib/mix/tasks/make_moderator.ex
@@ -1,9 +1,16 @@
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}
- @shortdoc "Set moderator status"
def run([nickname | rest]) do
Application.ensure_all_started(:pleroma)
diff --git a/lib/mix/tasks/reactivate_user.ex b/lib/mix/tasks/reactivate_user.ex
new file mode 100644
index 000000000..a30d3ac8b
--- /dev/null
+++ b/lib/mix/tasks/reactivate_user.ex
@@ -0,0 +1,19 @@
+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
index e74721c49..1f5321093 100644
--- a/lib/mix/tasks/register_user.ex
+++ b/lib/mix/tasks/register_user.ex
@@ -1,4 +1,12 @@
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}
diff --git a/lib/mix/tasks/relay_follow.ex b/lib/mix/tasks/relay_follow.ex
index ac6f20924..85b1c024d 100644
--- a/lib/mix/tasks/relay_follow.ex
+++ b/lib/mix/tasks/relay_follow.ex
@@ -4,12 +4,21 @@ defmodule Mix.Tasks.RelayFollow do
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")
- :ok = Relay.follow(target)
-
- # put this task to sleep to allow the genserver to push out the messages
- :timer.sleep(500)
+ 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
index 4621ace83..237fb771c 100644
--- a/lib/mix/tasks/relay_unfollow.ex
+++ b/lib/mix/tasks/relay_unfollow.ex
@@ -3,13 +3,21 @@ defmodule Mix.Tasks.RelayUnfollow do
require Logger
alias Pleroma.Web.ActivityPub.Relay
- @shortdoc "Follows a remote 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")
- :ok = Relay.unfollow(target)
-
- # put this task to sleep to allow the genserver to push out the messages
- :timer.sleep(500)
+ 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
index 27521b745..50463046c 100644
--- a/lib/mix/tasks/rm_user.ex
+++ b/lib/mix/tasks/rm_user.ex
@@ -2,12 +2,18 @@ defmodule Mix.Tasks.RmUser do
use Mix.Task
alias Pleroma.User
- @shortdoc "Permanently delete a 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
- User.delete(user)
+ {:ok, _} = User.delete(user)
end
end
end
diff --git a/lib/mix/tasks/sample_config.eex b/lib/mix/tasks/sample_config.eex
index f2272b10a..47b6be729 100644
--- a/lib/mix/tasks/sample_config.eex
+++ b/lib/mix/tasks/sample_config.eex
@@ -31,6 +31,10 @@ config :web_push_encryption, :vapid_details,
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
+
# Configure S3 support if desired.
# The public S3 endpoint is different depending on region and provider,
# consult your S3 provider's documentation for details on what to use.
@@ -52,9 +56,9 @@ config :web_push_encryption, :vapid_details,
# 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/sample_psql.eex
index bc22f166c..b6f57948b 100644
--- a/lib/mix/tasks/sample_psql.eex
+++ b/lib/mix/tasks/sample_psql.eex
@@ -1,8 +1,5 @@
-CREATE USER pleroma WITH ENCRYPTED PASSWORD '<%= dbpass %>' CREATEDB;
--- in case someone runs this second time accidentally
-ALTER USER pleroma WITH ENCRYPTED PASSWORD '<%= dbpass %>' CREATEDB;
-CREATE DATABASE pleroma_dev;
-ALTER DATABASE pleroma_dev OWNER TO pleroma;
+CREATE USER pleroma WITH ENCRYPTED PASSWORD '<%= dbpass %>';
+CREATE DATABASE pleroma_dev OWNER pleroma;
\c pleroma_dev;
--Extensions made by ecto.migrate that need superuser access
CREATE EXTENSION IF NOT EXISTS citext;
diff --git a/lib/mix/tasks/set_admin.ex b/lib/mix/tasks/set_admin.ex
new file mode 100644
index 000000000..d5ccf261b
--- /dev/null
+++ b/lib/mix/tasks/set_admin.ex
@@ -0,0 +1,32 @@
+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
index 2b3b18b09..a154595ca 100644
--- a/lib/mix/tasks/set_locked.ex
+++ b/lib/mix/tasks/set_locked.ex
@@ -1,9 +1,18 @@
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}
- @shortdoc "Set locked status"
def run([nickname | rest]) do
ensure_started(Repo, [])
diff --git a/lib/mix/tasks/unsubscribe_user.ex b/lib/mix/tasks/unsubscribe_user.ex
new file mode 100644
index 000000000..62ea61a5c
--- /dev/null
+++ b/lib/mix/tasks/unsubscribe_user.ex
@@ -0,0 +1,38 @@
+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/activity.ex b/lib/pleroma/activity.ex
index bed96861f..c065f3b6c 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -82,4 +82,10 @@ defmodule Pleroma.Activity do
def normalize(obj) when is_map(obj), do: Activity.get_by_ap_id(obj["id"])
def normalize(ap_id) when is_binary(ap_id), do: Activity.get_by_ap_id(ap_id)
def normalize(_), do: nil
+
+ def get_in_reply_to_activity(%Activity{data: %{"object" => %{"inReplyTo" => ap_id}}}) do
+ get_create_activity_by_object_ap_id(ap_id)
+ end
+
+ def get_in_reply_to_activity(_), do: nil
end
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 565e938fd..d0f23527f 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -1,8 +1,15 @@
defmodule Pleroma.Application do
use Application
+ @name "Pleroma"
+ @version Mix.Project.config()[:version]
+ def name, do: @name
+ def version, do: @version
+ def named_version(), do: @name <> " " <> @version
+
# 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 Supervisor.Spec
import Cachex.Spec
@@ -12,18 +19,35 @@ defmodule Pleroma.Application do
[
# Start the Ecto repository
supervisor(Pleroma.Repo, []),
+ worker(Pleroma.Emoji, []),
# Start the endpoint when the application starts
supervisor(Pleroma.Web.Endpoint, []),
# Start your own worker by calling: Pleroma.Worker.start_link(arg1, arg2, arg3)
# worker(Pleroma.Worker, [arg1, arg2, arg3]),
- worker(Cachex, [
- :user_cache,
+ worker(
+ Cachex,
+ [
+ :user_cache,
+ [
+ default_ttl: 25000,
+ ttl_interval: 1000,
+ limit: 2500
+ ]
+ ],
+ id: :cachex_user
+ ),
+ worker(
+ Cachex,
[
- default_ttl: 25000,
- ttl_interval: 1000,
- limit: 2500
- ]
- ]),
+ :object_cache,
+ [
+ default_ttl: 25000,
+ ttl_interval: 1000,
+ limit: 2500
+ ]
+ ],
+ id: :cachex_object
+ ),
worker(
Cachex,
[
@@ -40,11 +64,12 @@ defmodule Pleroma.Application do
id: :cachex_idem
),
worker(Pleroma.Web.Federator, []),
+ worker(Pleroma.Web.Federator.RetryQueue, []),
worker(Pleroma.Gopher.Server, []),
worker(Pleroma.Stats, []),
worker(Pleroma.Web.Push, [])
] ++
- if Mix.env() == :test,
+ if @env == :test,
do: [],
else:
[worker(Pleroma.Web.Streamer, [])] ++
diff --git a/lib/pleroma/config.ex b/lib/pleroma/config.ex
new file mode 100644
index 000000000..15f771b6e
--- /dev/null
+++ b/lib/pleroma/config.ex
@@ -0,0 +1,42 @@
+defmodule Pleroma.Config do
+ defmodule Error do
+ defexception [:message]
+ end
+
+ def get(key), do: get(key, nil)
+
+ def get([key], default), do: get(key, default)
+
+ def get([parent_key | keys], default) do
+ Application.get_env(:pleroma, parent_key)
+ |> get_in(keys) || default
+ end
+
+ def get(key, default) do
+ Application.get_env(:pleroma, key, default)
+ end
+
+ def get!(key) do
+ value = get(key, nil)
+
+ if value == nil do
+ raise(Error, message: "Missing configuration value: #{inspect(key)}")
+ else
+ value
+ end
+ end
+
+ def put([key], value), do: put(key, value)
+
+ def put([parent_key | keys], value) do
+ parent =
+ Application.get_env(:pleroma, parent_key)
+ |> put_in(keys, value)
+
+ Application.put_env(:pleroma, parent_key, parent)
+ end
+
+ def put(key, value) do
+ Application.put_env(:pleroma, key, value)
+ end
+end
diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex
new file mode 100644
index 000000000..0a5e1d5ce
--- /dev/null
+++ b/lib/pleroma/emoji.ex
@@ -0,0 +1,194 @@
+defmodule Pleroma.Emoji do
+ @moduledoc """
+ The emojis are loaded from:
+
+ * the built-in Finmojis (if enabled in configuration),
+ * the files: `config/emoji.txt` and `config/custom_emoji.txt`
+ * glob paths
+
+ This GenServer stores in an ETS table the list of the loaded emojis, and also allows to reload the list at runtime.
+ """
+ use GenServer
+ @ets __MODULE__.Ets
+ @ets_options [:set, :protected, :named_table, {:read_concurrency, true}]
+
+ @doc false
+ def start_link() do
+ GenServer.start_link(__MODULE__, [], name: __MODULE__)
+ end
+
+ @doc "Reloads the emojis from disk."
+ @spec reload() :: :ok
+ def reload() do
+ GenServer.call(__MODULE__, :reload)
+ end
+
+ @doc "Returns the path of the emoji `name`."
+ @spec get(String.t()) :: String.t() | nil
+ def get(name) do
+ case :ets.lookup(@ets, name) do
+ [{_, path}] -> path
+ _ -> nil
+ end
+ end
+
+ @doc "Returns all the emojos!!"
+ @spec get_all() :: [{String.t(), String.t()}, ...]
+ def get_all() do
+ :ets.tab2list(@ets)
+ end
+
+ @doc false
+ def init(_) do
+ @ets = :ets.new(@ets, @ets_options)
+ GenServer.cast(self(), :reload)
+ {:ok, nil}
+ end
+
+ @doc false
+ def handle_cast(:reload, state) do
+ load()
+ {:noreply, state}
+ end
+
+ @doc false
+ def handle_call(:reload, _from, state) do
+ load()
+ {:reply, :ok, state}
+ end
+
+ @doc false
+ def terminate(_, _) do
+ :ok
+ end
+
+ @doc false
+ def code_change(_old_vsn, state, _extra) do
+ load()
+ {:ok, state}
+ end
+
+ defp load() do
+ emojis =
+ (load_finmoji(Keyword.get(Application.get_env(:pleroma, :instance), :finmoji_enabled)) ++
+ load_from_file("config/emoji.txt") ++
+ load_from_file("config/custom_emoji.txt") ++
+ load_from_globs(
+ Keyword.get(Application.get_env(:pleroma, :emoji, []), :shortcode_globs, [])
+ ))
+ |> Enum.reject(fn value -> value == nil end)
+
+ true = :ets.insert(@ets, emojis)
+ :ok
+ end
+
+ @finmoji [
+ "a_trusted_friend",
+ "alandislands",
+ "association",
+ "auroraborealis",
+ "baby_in_a_box",
+ "bear",
+ "black_gold",
+ "christmasparty",
+ "crosscountryskiing",
+ "cupofcoffee",
+ "education",
+ "fashionista_finns",
+ "finnishlove",
+ "flag",
+ "forest",
+ "four_seasons_of_bbq",
+ "girlpower",
+ "handshake",
+ "happiness",
+ "headbanger",
+ "icebreaker",
+ "iceman",
+ "joulutorttu",
+ "kaamos",
+ "kalsarikannit_f",
+ "kalsarikannit_m",
+ "karjalanpiirakka",
+ "kicksled",
+ "kokko",
+ "lavatanssit",
+ "losthopes_f",
+ "losthopes_m",
+ "mattinykanen",
+ "meanwhileinfinland",
+ "moominmamma",
+ "nordicfamily",
+ "out_of_office",
+ "peacemaker",
+ "perkele",
+ "pesapallo",
+ "polarbear",
+ "pusa_hispida_saimensis",
+ "reindeer",
+ "sami",
+ "sauna_f",
+ "sauna_m",
+ "sauna_whisk",
+ "sisu",
+ "stuck",
+ "suomimainittu",
+ "superfood",
+ "swan",
+ "the_cap",
+ "the_conductor",
+ "the_king",
+ "the_voice",
+ "theoriginalsanta",
+ "tomoffinland",
+ "torillatavataan",
+ "unbreakable",
+ "waiting",
+ "white_nights",
+ "woollysocks"
+ ]
+ defp load_finmoji(true) do
+ Enum.map(@finmoji, fn finmoji ->
+ {finmoji, "/finmoji/128px/#{finmoji}-128.png"}
+ end)
+ end
+
+ defp load_finmoji(_), do: []
+
+ defp load_from_file(file) do
+ if File.exists?(file) do
+ load_from_file_stream(File.stream!(file))
+ else
+ []
+ end
+ end
+
+ defp load_from_file_stream(stream) do
+ stream
+ |> Stream.map(&String.strip/1)
+ |> Stream.map(fn line ->
+ case String.split(line, ~r/,\s*/) do
+ [name, file] -> {name, file}
+ _ -> nil
+ end
+ end)
+ |> Enum.to_list()
+ end
+
+ defp load_from_globs(globs) do
+ static_path = Path.join(:code.priv_dir(:pleroma), "static")
+
+ paths =
+ Enum.map(globs, fn glob ->
+ Path.join(static_path, glob)
+ |> Path.wildcard()
+ end)
+ |> Enum.concat()
+
+ Enum.map(paths, fn path ->
+ shortcode = Path.basename(path, Path.extname(path))
+ external_path = Path.join("/", Path.relative_to(path, static_path))
+ {shortcode, external_path}
+ end)
+ end
+end
diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex
index fe904df3a..25ed38f34 100644
--- a/lib/pleroma/filter.ex
+++ b/lib/pleroma/filter.ex
@@ -36,6 +36,34 @@ defmodule Pleroma.Filter do
Repo.all(query)
end
+ 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.
+
+ max_id_query =
+ from(
+ f in Pleroma.Filter,
+ where: f.user_id == ^user_id,
+ select: max(f.filter_id)
+ )
+
+ filter_id =
+ case Repo.one(max_id_query) do
+ # Start allocating from 1
+ nil ->
+ 1
+
+ max_id ->
+ max_id + 1
+ end
+
+ filter
+ |> Map.put(:filter_id, filter_id)
+ |> Repo.insert()
+ end
+
def create(%Pleroma.Filter{} = filter) do
Repo.insert(filter)
end
diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex
index 62f54a3f2..26bb17377 100644
--- a/lib/pleroma/formatter.ex
+++ b/lib/pleroma/formatter.ex
@@ -2,6 +2,7 @@ defmodule Pleroma.Formatter do
alias Pleroma.User
alias Pleroma.Web.MediaProxy
alias Pleroma.HTML
+ alias Pleroma.Emoji
@tag_regex ~r/\#\w+/u
def parse_tags(text, data \\ %{}) do
@@ -28,119 +29,10 @@ defmodule Pleroma.Formatter do
|> Enum.filter(fn {_match, user} -> user end)
end
- @finmoji [
- "a_trusted_friend",
- "alandislands",
- "association",
- "auroraborealis",
- "baby_in_a_box",
- "bear",
- "black_gold",
- "christmasparty",
- "crosscountryskiing",
- "cupofcoffee",
- "education",
- "fashionista_finns",
- "finnishlove",
- "flag",
- "forest",
- "four_seasons_of_bbq",
- "girlpower",
- "handshake",
- "happiness",
- "headbanger",
- "icebreaker",
- "iceman",
- "joulutorttu",
- "kaamos",
- "kalsarikannit_f",
- "kalsarikannit_m",
- "karjalanpiirakka",
- "kicksled",
- "kokko",
- "lavatanssit",
- "losthopes_f",
- "losthopes_m",
- "mattinykanen",
- "meanwhileinfinland",
- "moominmamma",
- "nordicfamily",
- "out_of_office",
- "peacemaker",
- "perkele",
- "pesapallo",
- "polarbear",
- "pusa_hispida_saimensis",
- "reindeer",
- "sami",
- "sauna_f",
- "sauna_m",
- "sauna_whisk",
- "sisu",
- "stuck",
- "suomimainittu",
- "superfood",
- "swan",
- "the_cap",
- "the_conductor",
- "the_king",
- "the_voice",
- "theoriginalsanta",
- "tomoffinland",
- "torillatavataan",
- "unbreakable",
- "waiting",
- "white_nights",
- "woollysocks"
- ]
-
- @finmoji_with_filenames Enum.map(@finmoji, fn finmoji ->
- {finmoji, "/finmoji/128px/#{finmoji}-128.png"}
- end)
-
- @emoji_from_file (with {:ok, default} <- File.read("config/emoji.txt") do
- custom =
- with {:ok, custom} <- File.read("config/custom_emoji.txt") do
- custom
- else
- _e -> ""
- end
-
- (default <> "\n" <> custom)
- |> String.trim()
- |> String.split(~r/\n+/)
- |> Enum.map(fn line ->
- [name, file] = String.split(line, ~r/,\s*/)
- {name, file}
- end)
- else
- _ -> []
- end)
-
- @emoji_from_globs (
- static_path = Path.join(:code.priv_dir(:pleroma), "static")
-
- globs =
- Application.get_env(:pleroma, :emoji, [])
- |> Keyword.get(:shortcode_globs, [])
-
- paths =
- Enum.map(globs, fn glob ->
- Path.join(static_path, glob)
- |> Path.wildcard()
- end)
- |> Enum.concat()
-
- Enum.map(paths, fn path ->
- shortcode = Path.basename(path, Path.extname(path))
- external_path = Path.join("/", Path.relative_to(path, static_path))
- {shortcode, external_path}
- end)
- )
-
- @emoji @finmoji_with_filenames ++ @emoji_from_globs ++ @emoji_from_file
+ def emojify(text) do
+ emojify(text, Emoji.get_all())
+ end
- def emojify(text, emoji \\ @emoji)
def emojify(text, nil), do: text
def emojify(text, emoji) do
@@ -160,39 +52,22 @@ defmodule Pleroma.Formatter do
end
def get_emoji(text) when is_binary(text) do
- Enum.filter(@emoji, fn {emoji, _} -> String.contains?(text, ":#{emoji}:") end)
+ Enum.filter(Emoji.get_all(), fn {emoji, _} -> String.contains?(text, ":#{emoji}:") end)
end
def get_emoji(_), do: []
- def get_custom_emoji() do
- @emoji
- end
-
@link_regex ~r/[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+/ui
- # IANA got a list https://www.iana.org/assignments/uri-schemes/ but
- # Stuff like ipfs isn’t in it
- # There is very niche stuff
- @uri_schemes [
- "https://",
- "http://",
- "dat://",
- "dweb://",
- "gopher://",
- "ipfs://",
- "ipns://",
- "irc:",
- "ircs:",
- "magnet:",
- "mailto:",
- "mumble:",
- "ssb://",
- "xmpp:"
- ]
+ @uri_schemes Application.get_env(:pleroma, :uri_schemes, [])
+ @valid_schemes Keyword.get(@uri_schemes, :valid_schemes, [])
# TODO: make it use something other than @link_regex
- def html_escape(text) do
+ def html_escape(text, "text/html") do
+ HTML.filter_tags(text)
+ end
+
+ def html_escape(text, "text/plain") do
Regex.split(@link_regex, text, include_captures: true)
|> Enum.map_every(2, fn chunk ->
{:safe, part} = Phoenix.HTML.html_escape(chunk)
@@ -203,14 +78,10 @@ defmodule Pleroma.Formatter do
@doc "changes scheme:... urls to html links"
def add_links({subs, text}) do
- additionnal_schemes =
- Application.get_env(:pleroma, :uri_schemes, [])
- |> Keyword.get(:additionnal_schemes, [])
-
links =
text
|> String.split([" ", "\t", "<br>"])
- |> Enum.filter(fn word -> String.starts_with?(word, @uri_schemes ++ additionnal_schemes) end)
+ |> Enum.filter(fn word -> String.starts_with?(word, @valid_schemes) end)
|> Enum.filter(fn word -> Regex.match?(@link_regex, word) end)
|> Enum.map(fn url -> {Ecto.UUID.generate(), url} end)
|> Enum.sort_by(fn {_, url} -> -String.length(url) end)
@@ -222,13 +93,7 @@ defmodule Pleroma.Formatter do
subs =
subs ++
Enum.map(links, fn {uuid, url} ->
- {:safe, link} = Phoenix.HTML.Link.link(url, to: url)
-
- link =
- link
- |> IO.iodata_to_binary()
-
- {uuid, link}
+ {uuid, "<a href=\"#{url}\">#{url}</a>"}
end)
{subs, uuid_text}
@@ -250,7 +115,12 @@ defmodule Pleroma.Formatter do
subs =
subs ++
Enum.map(mentions, fn {match, %User{ap_id: ap_id, info: info}, uuid} ->
- ap_id = info["source_data"]["url"] || ap_id
+ ap_id =
+ if is_binary(info["source_data"]["url"]) do
+ info["source_data"]["url"]
+ else
+ ap_id
+ end
short_match = String.split(match, "@") |> tl() |> hd()
diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex
index d34037f4f..e6361a82c 100644
--- a/lib/pleroma/gopher/server.ex
+++ b/lib/pleroma/gopher/server.ex
@@ -1,16 +1,16 @@
defmodule Pleroma.Gopher.Server do
use GenServer
require Logger
- @gopher Application.get_env(:pleroma, :gopher)
def start_link() do
- ip = Keyword.get(@gopher, :ip, {0, 0, 0, 0})
- port = Keyword.get(@gopher, :port, 1234)
+ config = Pleroma.Config.get(:gopher, [])
+ ip = Keyword.get(config, :ip, {0, 0, 0, 0})
+ port = Keyword.get(config, :port, 1234)
GenServer.start_link(__MODULE__, [ip, port], [])
end
def init([ip, port]) do
- if Keyword.get(@gopher, :enabled, false) do
+ if Pleroma.Config.get([:gopher, :enabled], false) do
Logger.info("Starting gopher server on #{port}")
:ranch.start_listener(
@@ -37,9 +37,6 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
alias Pleroma.Repo
alias Pleroma.HTML
- @instance Application.get_env(:pleroma, :instance)
- @gopher Application.get_env(:pleroma, :gopher)
-
def start_link(ref, socket, transport, opts) do
pid = spawn_link(__MODULE__, :init, [ref, socket, transport, opts])
{:ok, pid}
@@ -62,7 +59,7 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
def link(name, selector, type \\ 1) do
address = Pleroma.Web.Endpoint.host()
- port = Keyword.get(@gopher, :port, 1234)
+ port = Pleroma.Config.get([:gopher, :port], 1234)
"#{type}#{name}\t#{selector}\t#{address}\t#{port}\r\n"
end
@@ -85,7 +82,7 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
end
def response("") do
- info("Welcome to #{Keyword.get(@instance, :name, "Pleroma")}!") <>
+ info("Welcome to #{Pleroma.Config.get([:instance, :name], "Pleroma")}!") <>
link("Public Timeline", "/main/public") <>
link("Federated Timeline", "/main/all") <> ".\r\n"
end
diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex
index cf18f070c..1b920d7fd 100644
--- a/lib/pleroma/html.ex
+++ b/lib/pleroma/html.ex
@@ -1,14 +1,12 @@
defmodule Pleroma.HTML do
alias HtmlSanitizeEx.Scrubber
- @markup Application.get_env(:pleroma, :markup)
-
defp get_scrubbers(scrubber) when is_atom(scrubber), do: [scrubber]
defp get_scrubbers(scrubbers) when is_list(scrubbers), do: scrubbers
defp get_scrubbers(_), do: [Pleroma.HTML.Scrubber.Default]
def get_scrubbers() do
- Keyword.get(@markup, :scrub_policy)
+ Pleroma.Config.get([:markup, :scrub_policy])
|> get_scrubbers
end
@@ -36,11 +34,13 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
paragraphs, breaks and links are allowed through the filter.
"""
+ @markup Application.get_env(:pleroma, :markup)
+ @uri_schemes Application.get_env(:pleroma, :uri_schemes, [])
+ @valid_schemes Keyword.get(@uri_schemes, :valid_schemes, [])
+
require HtmlSanitizeEx.Scrubber.Meta
alias HtmlSanitizeEx.Scrubber.Meta
- @valid_schemes ["http", "https"]
-
Meta.remove_cdata_sections_before_scrub()
Meta.strip_comments()
@@ -56,11 +56,11 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
Meta.allow_tag_with_these_attributes("span", [])
# allow inline images for custom emoji
- @markup Application.get_env(:pleroma, :markup)
@allow_inline_images Keyword.get(@markup, :allow_inline_images)
if @allow_inline_images do
- Meta.allow_tag_with_uri_attributes("img", ["src"], @valid_schemes)
+ # restrict img tags to http/https only, because of MediaProxy.
+ Meta.allow_tag_with_uri_attributes("img", ["src"], ["http", "https"])
Meta.allow_tag_with_these_attributes("img", [
"width",
@@ -79,7 +79,9 @@ defmodule Pleroma.HTML.Scrubber.Default do
require HtmlSanitizeEx.Scrubber.Meta
alias HtmlSanitizeEx.Scrubber.Meta
- @valid_schemes ["http", "https"]
+ @markup Application.get_env(:pleroma, :markup)
+ @uri_schemes Application.get_env(:pleroma, :uri_schemes, [])
+ @valid_schemes Keyword.get(@uri_schemes, :valid_schemes, [])
Meta.remove_cdata_sections_before_scrub()
Meta.strip_comments()
@@ -87,6 +89,8 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_uri_attributes("a", ["href"], @valid_schemes)
Meta.allow_tag_with_these_attributes("a", ["name", "title"])
+ Meta.allow_tag_with_these_attributes("abbr", ["title"])
+
Meta.allow_tag_with_these_attributes("b", [])
Meta.allow_tag_with_these_attributes("blockquote", [])
Meta.allow_tag_with_these_attributes("br", [])
@@ -103,11 +107,11 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes("u", [])
Meta.allow_tag_with_these_attributes("ul", [])
- @markup Application.get_env(:pleroma, :markup)
@allow_inline_images Keyword.get(@markup, :allow_inline_images)
if @allow_inline_images do
- Meta.allow_tag_with_uri_attributes("img", ["src"], @valid_schemes)
+ # restrict img tags to http/https only, because of MediaProxy.
+ Meta.allow_tag_with_uri_attributes("img", ["src"], ["http", "https"])
Meta.allow_tag_with_these_attributes("img", [
"width",
@@ -173,6 +177,8 @@ defmodule Pleroma.HTML.Transform.MediaProxy do
{"img", attributes, children}
end
+ def scrub({:comment, children}), do: ""
+
def scrub({tag, attributes, children}), do: {tag, attributes, children}
def scrub({tag, children}), do: children
def scrub(text), do: text
diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex
index c19bccf60..e64266ae7 100644
--- a/lib/pleroma/http/http.ex
+++ b/lib/pleroma/http/http.ex
@@ -22,6 +22,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]]
case proxy do
nil -> options
diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex
index 53d98665b..891c73f5a 100644
--- a/lib/pleroma/list.ex
+++ b/lib/pleroma/list.ex
@@ -69,6 +69,25 @@ defmodule Pleroma.List do
Repo.all(query)
end
+ # Get lists to which the account belongs.
+ def get_lists_account_belongs(%User{} = owner, account_id) do
+ user = Repo.get(User, account_id)
+
+ query =
+ from(
+ l in Pleroma.List,
+ where:
+ l.user_id == ^owner.id and
+ fragment(
+ "? = ANY(?)",
+ ^user.follower_address,
+ l.following
+ )
+ )
+
+ Repo.all(query)
+ end
+
def rename(%Pleroma.List{} = list, title) do
list
|> title_changeset(%{title: title})
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index 6163413c8..a40b8f8c9 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -1,6 +1,6 @@
defmodule Pleroma.Notification do
use Ecto.Schema
- alias Pleroma.{User, Activity, Notification, Repo}
+ alias Pleroma.{User, Activity, Notification, Repo, Object}
import Ecto.Query
schema "notifications" do
@@ -42,6 +42,20 @@ defmodule Pleroma.Notification do
Repo.all(query)
end
+ def set_read_up_to(%{id: user_id} = _user, id) do
+ query =
+ from(
+ n in Notification,
+ where: n.user_id == ^user_id,
+ where: n.id <= ^id,
+ update: [
+ set: [seen: true]
+ ]
+ )
+
+ Repo.update_all(query, [])
+ end
+
def get(%{id: user_id} = _user, id) do
query =
from(
@@ -81,7 +95,7 @@ defmodule Pleroma.Notification do
def create_notifications(%Activity{id: _, data: %{"to" => _, "type" => type}} = activity)
when type in ["Create", "Like", "Announce", "Follow"] do
- users = User.get_notified_from_activity(activity)
+ users = get_notified_from_activity(activity)
notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
{:ok, notifications}
@@ -100,4 +114,64 @@ defmodule Pleroma.Notification do
notification
end
end
+
+ def get_notified_from_activity(activity, local_only \\ true)
+
+ def get_notified_from_activity(
+ %Activity{data: %{"to" => _, "type" => type} = data} = activity,
+ local_only
+ )
+ when type in ["Create", "Like", "Announce", "Follow"] do
+ recipients =
+ []
+ |> maybe_notify_to_recipients(activity)
+ |> maybe_notify_mentioned_recipients(activity)
+ |> Enum.uniq()
+
+ User.get_users_from_set(recipients, local_only)
+ end
+
+ def get_notified_from_activity(_, local_only), do: []
+
+ defp maybe_notify_to_recipients(
+ recipients,
+ %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
+ )
+ when type == "Create" do
+ object = Object.normalize(data["object"])
+
+ object_data =
+ cond do
+ !is_nil(object) ->
+ object.data
+
+ is_map(data["object"]) ->
+ data["object"]
+
+ true ->
+ %{}
+ end
+
+ tagged_mentions = maybe_extract_mentions(object_data)
+
+ recipients ++ tagged_mentions
+ end
+
+ defp maybe_notify_mentioned_recipients(recipients, _), do: recipients
+
+ defp maybe_extract_mentions(%{"tag" => tag}) do
+ tag
+ |> Enum.filter(fn x -> is_map(x) end)
+ |> Enum.filter(fn x -> x["type"] == "Mention" end)
+ |> Enum.map(fn x -> x["href"] end)
+ end
+
+ defp maybe_extract_mentions(_), do: []
end
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index 1bcff5a7b..03a75dfbd 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}
+ alias Pleroma.{Repo, Object, Activity}
import Ecto.{Query, Changeset}
schema "objects" do
@@ -31,13 +31,15 @@ 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
- def get_cached_by_ap_id(ap_id) do
- if Mix.env() == :test do
+ if Mix.env() == :test do
+ def get_cached_by_ap_id(ap_id) do
get_by_ap_id(ap_id)
- else
+ end
+ else
+ def get_cached_by_ap_id(ap_id) do
key = "object:#{ap_id}"
- Cachex.fetch!(:user_cache, key, fn _ ->
+ Cachex.fetch!(:object_cache, key, fn _ ->
object = get_by_ap_id(ap_id)
if object do
@@ -52,4 +54,12 @@ defmodule Pleroma.Object do
def context_mapping(context) do
Object.change(%Object{}, %{data: %{"id" => context}})
end
+
+ def delete(%Object{data: %{"id" => id}} = object) do
+ with Repo.delete(object),
+ Repo.delete_all(Activity.all_non_create_by_object_ap_id_q(id)),
+ {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
+ {:ok, object}
+ end
+ end
end
diff --git a/lib/pleroma/plugs/federating_plug.ex b/lib/pleroma/plugs/federating_plug.ex
new file mode 100644
index 000000000..4108d90af
--- /dev/null
+++ b/lib/pleroma/plugs/federating_plug.ex
@@ -0,0 +1,18 @@
+defmodule Pleroma.Web.FederatingPlug do
+ import Plug.Conn
+
+ def init(options) do
+ options
+ end
+
+ def call(conn, opts) do
+ if Keyword.get(Application.get_env(:pleroma, :instance), :federating) do
+ conn
+ else
+ conn
+ |> put_status(404)
+ |> Phoenix.Controller.render(Pleroma.Web.ErrorView, "404.json")
+ |> halt()
+ end
+ end
+end
diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex
new file mode 100644
index 000000000..4c32653ea
--- /dev/null
+++ b/lib/pleroma/plugs/http_security_plug.ex
@@ -0,0 +1,63 @@
+defmodule Pleroma.Plugs.HTTPSecurityPlug do
+ alias Pleroma.Config
+ import Plug.Conn
+
+ def init(opts), do: opts
+
+ 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]))
+ else
+ conn
+ end
+ end
+
+ defp headers do
+ referrer_policy = Config.get([:http_security, :referrer_policy])
+
+ [
+ {"x-xss-protection", "1; mode=block"},
+ {"x-permitted-cross-domain-policies", "none"},
+ {"x-frame-options", "DENY"},
+ {"x-content-type-options", "nosniff"},
+ {"referrer-policy", referrer_policy},
+ {"x-download-options", "noopen"},
+ {"content-security-policy", csp_string() <> ";"}
+ ]
+ end
+
+ defp csp_string do
+ protocol = Config.get([Pleroma.Web.Endpoint, :protocol])
+
+ [
+ "default-src 'none'",
+ "base-uri 'self'",
+ "frame-ancestors 'none'",
+ "img-src 'self' data: https:",
+ "media-src 'self' https:",
+ "style-src 'self' 'unsafe-inline'",
+ "font-src 'self'",
+ "script-src 'self'",
+ "connect-src 'self' " <> String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
+ "manifest-src 'self'",
+ if @protocol == "https" do
+ "upgrade-insecure-requests"
+ end
+ ]
+ |> Enum.join("; ")
+ end
+
+ defp maybe_send_sts_header(conn, true) do
+ max_age_sts = Config.get([:http_security, :sts_max_age])
+ max_age_ct = Config.get([:http_security, :ct_max_age])
+
+ merge_resp_headers(conn, [
+ {"strict-transport-security", "max-age=#{max_age_sts}; includeSubDomains"},
+ {"expect-ct", "enforce, max-age=#{max_age_ct}"}
+ ])
+ end
+
+ defp maybe_send_sts_header(conn, _), do: conn
+end
diff --git a/lib/pleroma/plugs/user_is_admin_plug.ex b/lib/pleroma/plugs/user_is_admin_plug.ex
new file mode 100644
index 000000000..5312f1499
--- /dev/null
+++ b/lib/pleroma/plugs/user_is_admin_plug.ex
@@ -0,0 +1,19 @@
+defmodule Pleroma.Plugs.UserIsAdminPlug do
+ import Plug.Conn
+ alias Pleroma.User
+
+ def init(options) do
+ options
+ end
+
+ def call(%{assigns: %{user: %User{info: %{"is_admin" => true}}}} = conn, _) do
+ conn
+ end
+
+ def call(conn, _) do
+ conn
+ |> put_resp_content_type("application/json")
+ |> send_resp(403, Jason.encode!(%{error: "User is not admin."}))
+ |> halt
+ end
+end
diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex
index f188a5f32..238630bf3 100644
--- a/lib/pleroma/upload.ex
+++ b/lib/pleroma/upload.ex
@@ -1,64 +1,74 @@
defmodule Pleroma.Upload do
alias Ecto.UUID
- @storage_backend Application.get_env(:pleroma, Pleroma.Upload)
- |> Keyword.fetch!(:uploader)
+ def check_file_size(path, nil), do: true
- def store(%Plug.Upload{} = file, should_dedupe) do
+ def check_file_size(path, size_limit) do
+ {:ok, %{size: size}} = File.stat(path)
+ size <= size_limit
+ end
+
+ def store(file, should_dedupe, size_limit \\ nil)
+
+ def store(%Plug.Upload{} = file, should_dedupe, size_limit) do
content_type = get_content_type(file.path)
- uuid = get_uuid(file, should_dedupe)
- name = get_name(file, uuid, content_type, should_dedupe)
-
- strip_exif_data(content_type, file.path)
-
- {:ok, url_path} =
- @storage_backend.put_file(name, uuid, file.path, content_type, should_dedupe)
-
- %{
- "type" => "Document",
- "url" => [
- %{
- "type" => "Link",
- "mediaType" => content_type,
- "href" => url_path
- }
- ],
- "name" => name
- }
+ with uuid <- get_uuid(file, should_dedupe),
+ name <- get_name(file, uuid, content_type, should_dedupe),
+ true <- check_file_size(file.path, size_limit) do
+ strip_exif_data(content_type, file.path)
+
+ {:ok, url_path} = uploader().put_file(name, uuid, file.path, content_type, should_dedupe)
+
+ %{
+ "type" => "Document",
+ "url" => [
+ %{
+ "type" => "Link",
+ "mediaType" => content_type,
+ "href" => url_path
+ }
+ ],
+ "name" => name
+ }
+ else
+ _e -> nil
+ end
end
- def store(%{"img" => "data:image/" <> image_data}, should_dedupe) do
+ def store(%{"img" => "data:image/" <> image_data}, should_dedupe, size_limit) do
parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
data = Base.decode64!(parsed["data"], ignore: :whitespace)
- tmp_path = tempfile_for_image(data)
-
- uuid = UUID.generate()
-
- content_type = get_content_type(tmp_path)
- strip_exif_data(content_type, tmp_path)
-
- name =
- create_name(
- String.downcase(Base.encode16(:crypto.hash(:sha256, data))),
- parsed["filetype"],
- content_type
- )
-
- {:ok, url_path} = @storage_backend.put_file(name, uuid, tmp_path, content_type, should_dedupe)
-
- %{
- "type" => "Image",
- "url" => [
- %{
- "type" => "Link",
- "mediaType" => content_type,
- "href" => url_path
- }
- ],
- "name" => name
- }
+ with tmp_path <- tempfile_for_image(data),
+ uuid <- UUID.generate(),
+ true <- check_file_size(tmp_path, size_limit) do
+ content_type = get_content_type(tmp_path)
+ strip_exif_data(content_type, tmp_path)
+
+ name =
+ create_name(
+ String.downcase(Base.encode16(:crypto.hash(:sha256, data))),
+ parsed["filetype"],
+ content_type
+ )
+
+ {:ok, url_path} = uploader().put_file(name, uuid, tmp_path, content_type, should_dedupe)
+
+ %{
+ "type" => "Image",
+ "url" => [
+ %{
+ "type" => "Link",
+ "mediaType" => content_type,
+ "href" => url_path
+ }
+ ],
+ "name" => name
+ }
+ else
+ _e -> nil
+ end
end
@doc """
@@ -152,7 +162,13 @@ defmodule Pleroma.Upload do
"audio/mpeg"
<<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00>> ->
- "audio/ogg"
+ case IO.binread(f, 27) do
+ <<_::size(160), 0x80, 0x74, 0x68, 0x65, 0x6F, 0x72, 0x61>> ->
+ "video/ogg"
+
+ _ ->
+ "audio/ogg"
+ end
<<0x52, 0x49, 0x46, 0x46, _, _, _, _>> ->
"audio/wav"
@@ -167,4 +183,8 @@ defmodule Pleroma.Upload do
_e -> "application/octet-stream"
end
end
+
+ defp uploader() do
+ Pleroma.Config.get!([Pleroma.Upload, :uploader])
+ end
end
diff --git a/lib/pleroma/uploaders/mdii.ex b/lib/pleroma/uploaders/mdii.ex
new file mode 100644
index 000000000..a9d52b0dc
--- /dev/null
+++ b/lib/pleroma/uploaders/mdii.ex
@@ -0,0 +1,26 @@
+defmodule Pleroma.Uploaders.MDII do
+ alias Pleroma.Config
+
+ @behaviour Pleroma.Uploaders.Uploader
+
+ @httpoison Application.get_env(:pleroma, :httpoison)
+
+ def put_file(name, uuid, path, content_type, should_dedupe) do
+ cgi = Pleroma.Config.get([Pleroma.Uploaders.MDII, :cgi])
+ files = Pleroma.Config.get([Pleroma.Uploaders.MDII, :files])
+
+ {:ok, file_data} = File.read(path)
+
+ extension = String.split(name, ".") |> List.last()
+ query = "#{cgi}?#{extension}"
+
+ with {:ok, %{status_code: 200, body: body}} <- @httpoison.post(query, file_data) do
+ File.rm!(path)
+ remote_file_name = String.split(body) |> List.first()
+ public_url = "#{files}/#{remote_file_name}.#{extension}"
+ {:ok, public_url}
+ else
+ _ -> Pleroma.Uploaders.Local.put_file(name, uuid, path, content_type, should_dedupe)
+ end
+ end
+end
diff --git a/lib/pleroma/uploaders/s3.ex b/lib/pleroma/uploaders/s3.ex
index ce0ed3e34..40a836460 100644
--- a/lib/pleroma/uploaders/s3.ex
+++ b/lib/pleroma/uploaders/s3.ex
@@ -1,16 +1,19 @@
defmodule Pleroma.Uploaders.S3 do
+ alias Pleroma.Web.MediaProxy
+
@behaviour Pleroma.Uploaders.Uploader
def put_file(name, uuid, path, content_type, _should_dedupe) do
settings = Application.get_env(:pleroma, Pleroma.Uploaders.S3)
bucket = Keyword.fetch!(settings, :bucket)
public_endpoint = Keyword.fetch!(settings, :public_endpoint)
+ force_media_proxy = Keyword.fetch!(settings, :force_media_proxy)
{:ok, file_data} = File.read(path)
File.rm!(path)
- s3_name = "#{uuid}/#{name}"
+ s3_name = "#{uuid}/#{encode(name)}"
{:ok, _} =
ExAws.S3.put_object(bucket, s3_name, file_data, [
@@ -19,6 +22,19 @@ defmodule Pleroma.Uploaders.S3 do
])
|> ExAws.request()
- {:ok, "#{public_endpoint}/#{bucket}/#{s3_name}"}
+ url_base = "#{public_endpoint}/#{bucket}/#{s3_name}"
+
+ public_url =
+ if force_media_proxy do
+ MediaProxy.url(url_base)
+ else
+ url_base
+ end
+
+ {:ok, public_url}
+ end
+
+ defp encode(name) do
+ String.replace(name, ~r/[^0-9a-zA-Z!.*'()_-]/, "-")
end
end
diff --git a/lib/pleroma/uploaders/swift/keystone.ex b/lib/pleroma/uploaders/swift/keystone.ex
index a79214319..e578b3c61 100644
--- a/lib/pleroma/uploaders/swift/keystone.ex
+++ b/lib/pleroma/uploaders/swift/keystone.ex
@@ -1,11 +1,9 @@
defmodule Pleroma.Uploaders.Swift.Keystone do
use HTTPoison.Base
- @settings Application.get_env(:pleroma, Pleroma.Uploaders.Swift)
-
def process_url(url) do
Enum.join(
- [Keyword.fetch!(@settings, :auth_url), url],
+ [Pleroma.Config.get!([Pleroma.Uploaders.Swift, :auth_url]), url],
"/"
)
end
@@ -16,9 +14,10 @@ defmodule Pleroma.Uploaders.Swift.Keystone do
end
def get_token() do
- username = Keyword.fetch!(@settings, :username)
- password = Keyword.fetch!(@settings, :password)
- tenant_id = Keyword.fetch!(@settings, :tenant_id)
+ settings = Pleroma.Config.get(Pleroma.Uploaders.Swift)
+ username = Keyword.fetch!(settings, :username)
+ password = Keyword.fetch!(settings, :password)
+ tenant_id = Keyword.fetch!(settings, :tenant_id)
case post(
"/tokens",
diff --git a/lib/pleroma/uploaders/swift/swift.ex b/lib/pleroma/uploaders/swift/swift.ex
index 819dfebda..fa08ca966 100644
--- a/lib/pleroma/uploaders/swift/swift.ex
+++ b/lib/pleroma/uploaders/swift/swift.ex
@@ -1,17 +1,15 @@
defmodule Pleroma.Uploaders.Swift.Client do
use HTTPoison.Base
- @settings Application.get_env(:pleroma, Pleroma.Uploaders.Swift)
-
def process_url(url) do
Enum.join(
- [Keyword.fetch!(@settings, :storage_url), url],
+ [Pleroma.Config.get!([Pleroma.Uploaders.Swift, :storage_url]), url],
"/"
)
end
def upload_file(filename, body, content_type) do
- object_url = Keyword.fetch!(@settings, :object_url)
+ 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
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 487bfce32..6e1d5559d 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -4,7 +4,7 @@ defmodule Pleroma.User do
import Ecto.{Changeset, Query}
alias Pleroma.{Repo, User, Object, Web, Activity, Notification}
alias Comeonin.Pbkdf2
- alias Pleroma.Web.{OStatus, Websub}
+ alias Pleroma.Web.{OStatus, Websub, OAuth}
alias Pleroma.Web.ActivityPub.{Utils, ActivityPub}
schema "users" do
@@ -42,6 +42,10 @@ defmodule Pleroma.User do
end
end
+ def profile_url(%User{info: %{"source_data" => %{"url" => url}}}), do: url
+ def profile_url(%User{ap_id: ap_id}), do: ap_id
+ def profile_url(_), do: nil
+
def ap_id(%User{nickname: nickname}) do
"#{Web.base_url()}/users/#{nickname}"
end
@@ -132,6 +136,9 @@ defmodule Pleroma.User do
|> validate_required([:password, :password_confirmation])
|> validate_confirmation(:password)
+ OAuth.Token.delete_user_tokens(struct)
+ OAuth.Authorization.delete_user_authorizations(struct)
+
if changeset.valid? do
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
@@ -184,33 +191,16 @@ defmodule Pleroma.User do
def needs_update?(_), do: true
- def maybe_direct_follow(%User{} = follower, %User{info: info} = followed) do
- user_config = Application.get_env(:pleroma, :user)
- deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
-
- user_info = user_info(followed)
-
- should_direct_follow =
- cond do
- # if the account is locked, don't pre-create the relationship
- user_info[:locked] == true ->
- false
-
- # if the users are blocking each other, we shouldn't even be here, but check for it anyway
- deny_follow_blocked and
- (User.blocks?(follower, followed) or User.blocks?(followed, follower)) ->
- false
-
- # if OStatus, then there is no three-way handshake to follow
- User.ap_enabled?(followed) != true ->
- true
+ def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{"locked" => true}}) do
+ {:ok, follower}
+ end
- # if there are no other reasons not to, just pre-create the relationship
- true ->
- true
- end
+ def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
+ follow(follower, followed)
+ end
- if should_direct_follow do
+ def maybe_direct_follow(%User{} = follower, %User{} = followed) do
+ if !User.ap_enabled?(followed) do
follow(follower, followed)
else
{:ok, follower}
@@ -305,6 +295,7 @@ defmodule Pleroma.User do
def invalidate_cache(user) do
Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
Cachex.del(:user_cache, "nickname:#{user.nickname}")
+ Cachex.del(:user_cache, "user_info:#{user.id}")
end
def get_cached_by_ap_id(ap_id) do
@@ -473,36 +464,25 @@ defmodule Pleroma.User do
update_and_set_cache(cs)
end
- def get_notified_from_activity_query(to) do
+ def get_users_from_set_query(ap_ids, false) do
from(
u in User,
- where: u.ap_id in ^to,
- where: u.local == true
+ where: u.ap_id in ^ap_ids
)
end
- def get_notified_from_activity(%Activity{recipients: to, data: %{"type" => "Announce"} = data}) do
- object = Object.normalize(data["object"])
- actor = User.get_cached_by_ap_id(data["actor"])
-
- # ensure that the actor who published the announced object appears only once
- to =
- if actor.nickname != nil do
- to ++ [object.data["actor"]]
- else
- to
- end
- |> Enum.uniq()
-
- query = get_notified_from_activity_query(to)
+ def get_users_from_set_query(ap_ids, true) do
+ query = get_users_from_set_query(ap_ids, false)
- Repo.all(query)
+ from(
+ u in query,
+ where: u.local == true
+ )
end
- def get_notified_from_activity(%Activity{recipients: to}) do
- query = get_notified_from_activity_query(to)
-
- Repo.all(query)
+ def get_users_from_set(ap_ids, local_only \\ true) do
+ get_users_from_set_query(ap_ids, local_only)
+ |> Repo.all()
end
def get_recipients_from_activity(%Activity{recipients: to}) do
@@ -518,7 +498,7 @@ defmodule Pleroma.User do
Repo.all(query)
end
- def search(query, resolve) do
+ def search(query, resolve \\ false) do
# strip the beginning @ off if there is a query
query = String.trim_leading(query, "@")
@@ -632,8 +612,8 @@ defmodule Pleroma.User do
)
end
- def deactivate(%User{} = user) do
- new_info = Map.put(user.info, "deactivated", true)
+ def deactivate(%User{} = user, status \\ true) do
+ new_info = Map.put(user.info, "deactivated", status)
cs = User.info_changeset(user, %{info: new_info})
update_and_set_cache(cs)
end
@@ -666,7 +646,7 @@ defmodule Pleroma.User do
end
end)
- :ok
+ {:ok, user}
end
def html_filter_policy(%User{info: %{"no_rich_text" => true}}) do
@@ -753,6 +733,7 @@ defmodule Pleroma.User do
Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
end
+ def ap_enabled?(%User{local: true}), do: true
def ap_enabled?(%User{info: info}), do: info["ap_enabled"]
def ap_enabled?(_), do: false
@@ -763,4 +744,28 @@ defmodule Pleroma.User do
get_or_fetch_by_nickname(uri_or_nickname)
end
end
+
+ # wait a period of time and return newest version of the User structs
+ # this is because we have synchronous follow APIs and need to simulate them
+ # with an async handshake
+ def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
+ with %User{} = a <- Repo.get(User, a.id),
+ %User{} = b <- Repo.get(User, b.id) do
+ {:ok, a, b}
+ else
+ _e ->
+ :error
+ end
+ end
+
+ def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
+ with :ok <- :timer.sleep(timeout),
+ %User{} = a <- Repo.get(User, a.id),
+ %User{} = b <- Repo.get(User, b.id) do
+ {:ok, a, b}
+ else
+ _e ->
+ :error
+ end
+ end
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 361e93e91..ed579e336 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -10,8 +10,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
@httpoison Application.get_env(:pleroma, :httpoison)
- @instance Application.get_env(:pleroma, :instance)
-
# For Announce activities, we filter the recipients based on following status for any actors
# that match actual users. See issue #164 for more information about why this is necessary.
defp get_recipients(%{"type" => "Announce"} = data) do
@@ -44,7 +42,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp check_actor_is_active(actor) do
if not is_nil(actor) do
with user <- User.get_cached_by_ap_id(actor),
- nil <- user.info["deactivated"] do
+ false <- !!user.info["deactivated"] do
:ok
else
_e -> :reject
@@ -273,8 +271,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
"to" => [user.follower_address, "https://www.w3.org/ns/activitystreams#Public"]
}
- with Repo.delete(object),
- Repo.delete_all(Activity.all_non_create_by_object_ap_id_q(id)),
+ with {:ok, _} <- Object.delete(object),
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity),
{:ok, _actor} <- User.decrease_note_count(user) do
@@ -575,9 +572,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Enum.reverse()
end
- def upload(file) do
- data = Upload.store(file, Application.get_env(:pleroma, :instance)[:dedupe_media])
- Repo.insert(%Object{data: data})
+ def upload(file, size_limit \\ nil) do
+ with data <-
+ Upload.store(file, Application.get_env(:pleroma, :instance)[:dedupe_media], size_limit),
+ false <- is_nil(data) do
+ Repo.insert(%Object{data: data})
+ end
end
def user_data_from_user_object(data) do
@@ -628,9 +628,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
def fetch_and_prepare_user_from_ap_id(ap_id) do
- with {:ok, %{status_code: 200, body: body}} <-
- @httpoison.get(ap_id, [Accept: "application/activity+json"], follow_redirect: true),
- {:ok, data} <- Jason.decode(body) do
+ with {:ok, data} <- fetch_and_contain_remote_object_from_id(ap_id) do
user_data_from_user_object(data)
else
e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
@@ -657,14 +655,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
- @quarantined_instances Keyword.get(@instance, :quarantined_instances, [])
-
def should_federate?(inbox, public) do
if public do
true
else
inbox_info = URI.parse(inbox)
- inbox_info.host not in @quarantined_instances
+ !Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host)
end
end
@@ -683,7 +679,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
(Pleroma.Web.Salmon.remote_users(activity) ++ followers)
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
|> Enum.map(fn %{info: %{"source_data" => data}} ->
- (data["endpoints"] && data["endpoints"]["sharedInbox"]) || data["inbox"]
+ (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
end)
|> Enum.uniq()
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
@@ -734,28 +730,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
else
Logger.info("Fetching #{id} via AP")
- with true <- String.starts_with?(id, "http"),
- {:ok, %{body: body, status_code: code}} when code in 200..299 <-
- @httpoison.get(
- id,
- [Accept: "application/activity+json"],
- follow_redirect: true,
- timeout: 10000,
- recv_timeout: 20000
- ),
- {:ok, data} <- Jason.decode(body),
+ with {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
nil <- Object.normalize(data),
params <- %{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
- "actor" => data["attributedTo"],
+ "actor" => data["actor"] || data["attributedTo"],
"object" => data
},
:ok <- Transmogrifier.contain_origin(id, params),
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, Object.normalize(activity.data["object"])}
else
+ {:error, {:reject, nil}} ->
+ {:reject, nil}
+
object = %Object{} ->
{:ok, object}
@@ -770,6 +760,27 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
+ def fetch_and_contain_remote_object_from_id(id) 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 <-
+ @httpoison.get(
+ id,
+ [Accept: "application/activity+json"],
+ follow_redirect: true,
+ timeout: 10000,
+ recv_timeout: 20000
+ ),
+ {:ok, data} <- Jason.decode(body),
+ :ok <- Transmogrifier.contain_origin_from_id(id, data) do
+ {:ok, data}
+ else
+ e ->
+ {:error, e}
+ end
+ end
+
def is_public?(activity) do
"https://www.w3.org/ns/activitystreams#Public" in (activity.data["to"] ++
(activity.data["cc"] || []))
@@ -784,4 +795,38 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
y = activity.data["to"] ++ (activity.data["cc"] || [])
visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y))
end
+
+ # guard
+ def entire_thread_visible_for_user?(nil, user), do: false
+
+ # child
+ def entire_thread_visible_for_user?(
+ %Activity{data: %{"object" => %{"inReplyTo" => parent_id}}} = tail,
+ user
+ )
+ when is_binary(parent_id) do
+ parent = Activity.get_in_reply_to_activity(tail)
+ visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user)
+ end
+
+ # root
+ def entire_thread_visible_for_user?(tail, user), do: visible_for_user?(tail, user)
+
+ # filter out broken threads
+ def contain_broken_threads(%Activity{} = activity, %User{} = user) do
+ entire_thread_visible_for_user?(activity, user)
+ end
+
+ # do post-processing on a specific activity
+ def contain_activity(%Activity{} = activity, %User{} = user) do
+ contain_broken_threads(activity, user)
+ end
+
+ # do post-processing on a timeline
+ def contain_timeline(timeline, user) do
+ timeline
+ |> Enum.filter(fn activity ->
+ contain_activity(activity, user)
+ end)
+ end
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index 52b2a467e..3570a75cb 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -4,12 +4,27 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
alias Pleroma.Web.ActivityPub.{ObjectView, UserView}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
+ alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Federator
require Logger
action_fallback(:errors)
+ plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay])
+ plug(:relay_active? when action in [:relay])
+
+ def relay_active?(conn, _) do
+ if Keyword.get(Application.get_env(:pleroma, :instance), :allow_relay) do
+ conn
+ else
+ conn
+ |> put_status(404)
+ |> json(%{error: "not found"})
+ |> halt
+ end
+ end
+
def user(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
@@ -87,25 +102,43 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
outbox(conn, %{"nickname" => nickname, "max_id" => nil})
end
- # TODO: Ensure that this inbox is a recipient of the message
+ def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
+ with %User{} = user <- User.get_cached_by_nickname(nickname),
+ true <- Utils.recipient_in_message(user.ap_id, params),
+ params <- Utils.maybe_splice_recipient(user.ap_id, params) do
+ Federator.enqueue(:incoming_ap_doc, params)
+ json(conn, "ok")
+ end
+ end
+
def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
Federator.enqueue(:incoming_ap_doc, params)
json(conn, "ok")
end
+ # only accept relayed Creates
+ def inbox(conn, %{"type" => "Create"} = params) do
+ Logger.info(
+ "Signature missing or not from author, relayed Create message, fetching object from source"
+ )
+
+ ActivityPub.fetch_object_from_id(params["object"]["id"])
+
+ json(conn, "ok")
+ end
+
def inbox(conn, params) do
headers = Enum.into(conn.req_headers, %{})
- if !String.contains?(headers["signature"] || "", params["actor"]) do
- Logger.info("Signature not from author, relayed message, fetching from source")
- ActivityPub.fetch_object_from_id(params["object"]["id"])
- else
- Logger.info("Signature error - make sure you are forwarding the HTTP Host header!")
- Logger.info("Could not validate #{params["actor"]}")
+ if String.contains?(headers["signature"], params["actor"]) do
+ Logger.info(
+ "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
+ )
+
Logger.info(inspect(conn.req_headers))
end
- json(conn, "ok")
+ json(conn, "error")
end
def relay(conn, params) do
diff --git a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex
index b4f91f3cc..c53cb1ad2 100644
--- a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex
+++ b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex
@@ -3,10 +3,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do
@behaviour Pleroma.Web.ActivityPub.MRF
- @mrf_normalize_markup Application.get_env(:pleroma, :mrf_normalize_markup)
-
def filter(%{"type" => activity_type} = object) when activity_type == "Create" do
- scrub_policy = Keyword.get(@mrf_normalize_markup, :scrub_policy)
+ scrub_policy = Pleroma.Config.get([:mrf_normalize_markup, :scrub_policy])
child = object["object"]
diff --git a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
index 129d04617..627284083 100644
--- a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
+++ b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
@@ -2,10 +2,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF
- @mrf_rejectnonpublic Application.get_env(:pleroma, :mrf_rejectnonpublic)
- @allow_followersonly Keyword.get(@mrf_rejectnonpublic, :allow_followersonly)
- @allow_direct Keyword.get(@mrf_rejectnonpublic, :allow_direct)
-
@impl true
def filter(%{"type" => "Create"} = object) do
user = User.get_cached_by_ap_id(object["actor"])
@@ -20,6 +16,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
true -> "direct"
end
+ policy = Pleroma.Config.get(:mrf_rejectnonpublic)
+
case visibility do
"public" ->
{:ok, object}
@@ -28,14 +26,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
{:ok, object}
"followers" ->
- with true <- @allow_followersonly do
+ with true <- Keyword.get(policy, :allow_followersonly) do
{:ok, object}
else
_e -> {:reject, nil}
end
"direct" ->
- with true <- @allow_direct do
+ with true <- Keyword.get(policy, :allow_direct) do
{:ok, object}
else
_e -> {:reject, nil}
diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
index 319721d48..86dcf5080 100644
--- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
@@ -2,60 +2,76 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF
- @mrf_policy Application.get_env(:pleroma, :mrf_simple)
-
- @accept Keyword.get(@mrf_policy, :accept)
- defp check_accept(%{host: actor_host} = actor_info, object)
- when length(@accept) > 0 and not (actor_host in @accept) do
- {:reject, nil}
+ defp check_accept(%{host: actor_host} = _actor_info, object) do
+ accepts = Pleroma.Config.get([:mrf_simple, :accept])
+
+ cond do
+ accepts == [] -> {:ok, object}
+ actor_host == Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object}
+ Enum.member?(accepts, actor_host) -> {:ok, object}
+ true -> {:reject, nil}
+ end
end
- defp check_accept(actor_info, object), do: {:ok, object}
-
- @reject Keyword.get(@mrf_policy, :reject)
- defp check_reject(%{host: actor_host} = actor_info, object) when actor_host in @reject do
- {:reject, nil}
+ defp check_reject(%{host: actor_host} = _actor_info, object) do
+ if Enum.member?(Pleroma.Config.get([:mrf_simple, :reject]), actor_host) do
+ {:reject, nil}
+ else
+ {:ok, object}
+ end
end
- defp check_reject(actor_info, object), do: {:ok, object}
+ defp check_media_removal(
+ %{host: actor_host} = _actor_info,
+ %{"type" => "Create", "object" => %{"attachement" => child_attachment}} = object
+ )
+ when length(child_attachment) > 0 do
+ object =
+ if Enum.member?(Pleroma.Config.get([:mrf_simple, :media_removal]), actor_host) do
+ child_object = Map.delete(object["object"], "attachment")
+ Map.put(object, "object", child_object)
+ else
+ object
+ end
- @media_removal Keyword.get(@mrf_policy, :media_removal)
- defp check_media_removal(%{host: actor_host} = actor_info, %{"type" => "Create"} = object)
- when actor_host in @media_removal do
- child_object = Map.delete(object["object"], "attachment")
- object = Map.put(object, "object", child_object)
{:ok, object}
end
- defp check_media_removal(actor_info, object), do: {:ok, object}
+ defp check_media_removal(_actor_info, object), do: {:ok, object}
- @media_nsfw Keyword.get(@mrf_policy, :media_nsfw)
defp check_media_nsfw(
- %{host: actor_host} = actor_info,
+ %{host: actor_host} = _actor_info,
%{
"type" => "Create",
"object" => %{"attachment" => child_attachment} = child_object
} = object
)
- when actor_host in @media_nsfw and length(child_attachment) > 0 do
- tags = (child_object["tag"] || []) ++ ["nsfw"]
- child_object = Map.put(child_object, "tags", tags)
- child_object = Map.put(child_object, "sensitive", true)
- object = Map.put(object, "object", child_object)
+ when length(child_attachment) > 0 do
+ object =
+ if Enum.member?(Pleroma.Config.get([:mrf_simple, :media_nsfw]), actor_host) do
+ tags = (child_object["tag"] || []) ++ ["nsfw"]
+ child_object = Map.put(child_object, "tags", tags)
+ child_object = Map.put(child_object, "sensitive", true)
+ Map.put(object, "object", child_object)
+ else
+ object
+ end
+
{:ok, object}
end
- defp check_media_nsfw(actor_info, object), do: {:ok, object}
-
- @ftl_removal Keyword.get(@mrf_policy, :federated_timeline_removal)
- defp check_ftl_removal(%{host: actor_host} = actor_info, object)
- when actor_host in @ftl_removal do
- user = User.get_by_ap_id(object["actor"])
+ defp check_media_nsfw(_actor_info, object), do: {:ok, object}
- # flip to/cc relationship to make the post unlisted
+ defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do
object =
- if "https://www.w3.org/ns/activitystreams#Public" in object["to"] and
- user.follower_address in object["cc"] do
+ with true <-
+ Enum.member?(
+ Pleroma.Config.get([:mrf_simple, :federated_timeline_removal]),
+ actor_host
+ ),
+ user <- User.get_cached_by_ap_id(object["actor"]),
+ true <- "https://www.w3.org/ns/activitystreams#Public" in object["to"],
+ true <- user.follower_address in object["cc"] do
to =
List.delete(object["to"], "https://www.w3.org/ns/activitystreams#Public") ++
[user.follower_address]
@@ -68,14 +84,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|> Map.put("to", to)
|> Map.put("cc", cc)
else
- object
+ _ -> object
end
{:ok, object}
end
- defp check_ftl_removal(actor_info, object), do: {:ok, object}
-
@impl true
def filter(object) do
actor_info = URI.parse(object["actor"])
diff --git a/lib/pleroma/web/activity_pub/mrf/user_allowlist.ex b/lib/pleroma/web/activity_pub/mrf/user_allowlist.ex
new file mode 100644
index 000000000..3503d8692
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/user_allowlist.ex
@@ -0,0 +1,23 @@
+defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do
+ alias Pleroma.Config
+
+ @behaviour Pleroma.Web.ActivityPub.MRF
+
+ defp filter_by_list(object, []), do: {:ok, object}
+
+ defp filter_by_list(%{"actor" => actor} = object, allow_list) do
+ if actor in allow_list do
+ {:ok, object}
+ else
+ {:reject, nil}
+ end
+ end
+
+ @impl true
+ def filter(object) do
+ actor_info = URI.parse(object["actor"])
+ allow_list = Config.get([:mrf_user_allowlist, String.to_atom(actor_info.host)], [])
+
+ filter_by_list(object, allow_list)
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex
index d30853d62..fcdc6b1c0 100644
--- a/lib/pleroma/web/activity_pub/relay.ex
+++ b/lib/pleroma/web/activity_pub/relay.ex
@@ -12,11 +12,12 @@ defmodule Pleroma.Web.ActivityPub.Relay do
%User{} = target_user <- User.get_or_fetch_by_ap_id(target_instance),
{:ok, activity} <- ActivityPub.follow(local_user, target_user) do
Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")
+ {:ok, activity}
else
- e -> Logger.error("error: #{inspect(e)}")
+ e ->
+ Logger.error("error: #{inspect(e)}")
+ {:error, e}
end
-
- :ok
end
def unfollow(target_instance) do
@@ -24,11 +25,12 @@ defmodule Pleroma.Web.ActivityPub.Relay do
%User{} = target_user <- User.get_or_fetch_by_ap_id(target_instance),
{:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do
Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}")
+ {:ok, activity}
else
- e -> Logger.error("error: #{inspect(e)}")
+ e ->
+ Logger.error("error: #{inspect(e)}")
+ {:error, e}
end
-
- :ok
end
def publish(%Activity{data: %{"type" => "Create"}} = activity) do
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 48c3aec97..5864855b0 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -21,18 +21,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
if is_binary(Enum.at(actor, 0)) do
Enum.at(actor, 0)
else
- Enum.find(actor, fn %{"type" => type} -> type == "Person" end)
+ Enum.find(actor, fn %{"type" => type} -> type in ["Person", "Service", "Application"] end)
|> Map.get("id")
end
end
- def get_actor(%{"actor" => actor}) when is_map(actor) do
- actor["id"]
+ def get_actor(%{"actor" => %{"id" => id}}) when is_bitstring(id) do
+ id
+ end
+
+ def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do
+ get_actor(%{"actor" => actor})
end
@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" => actor} = params) do
id_uri = URI.parse(id)
actor_uri = URI.parse(get_actor(params))
@@ -44,6 +50,19 @@ 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" => other_id} = params) do
+ id_uri = URI.parse(id)
+ other_uri = URI.parse(other_id)
+
+ if id_uri.host == other_uri.host do
+ :ok
+ else
+ :error
+ end
+ end
+
@doc """
Modifies an incoming AP object (mastodon format) to our internal format.
"""
@@ -51,6 +70,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
object
|> fix_actor
|> fix_attachments
+ |> fix_url
|> fix_context
|> fix_in_reply_to
|> fix_emoji
@@ -96,9 +116,25 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
object
end
- def fix_in_reply_to(%{"inReplyTo" => in_reply_to_id} = object)
- when not is_nil(in_reply_to_id) do
- case ActivityPub.fetch_object_from_id(in_reply_to_id) do
+ def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object)
+ when not is_nil(in_reply_to) do
+ in_reply_to_id =
+ cond do
+ is_bitstring(in_reply_to) ->
+ in_reply_to
+
+ is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
+ in_reply_to["id"]
+
+ is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
+ Enum.at(in_reply_to, 0)
+
+ # Maybe I should output an error too?
+ true ->
+ ""
+ end
+
+ case fetch_obj_helper(in_reply_to_id) do
{:ok, replied_object} ->
with %Activity{} = activity <-
Activity.get_create_activity_by_object_ap_id(replied_object.data["id"]) do
@@ -110,12 +146,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("context", replied_object.data["context"] || object["conversation"])
else
e ->
- Logger.error("Couldn't fetch #{object["inReplyTo"]} #{inspect(e)}")
+ Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
object
end
e ->
- Logger.error("Couldn't fetch #{object["inReplyTo"]} #{inspect(e)}")
+ Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
object
end
end
@@ -130,9 +166,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("conversation", context)
end
- def fix_attachments(object) do
+ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
attachments =
- (object["attachment"] || [])
+ attachment
|> Enum.map(fn data ->
url = [%{"type" => "Link", "mediaType" => data["mediaType"], "href" => data["url"]}]
Map.put(data, "url", url)
@@ -142,21 +178,41 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("attachment", attachments)
end
- def fix_emoji(object) do
- tags = object["tag"] || []
+ def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
+ Map.put(object, "attachment", [attachment])
+ |> fix_attachments()
+ end
+
+ def fix_attachments(object), do: object
+
+ def fix_url(%{"url" => url} = object) when is_map(url) do
+ object
+ |> Map.put("url", url["href"])
+ end
+
+ def fix_url(%{"url" => url} = object) when is_list(url) do
+ first_element = Enum.at(url, 0)
+
+ url_string =
+ cond do
+ is_bitstring(first_element) -> first_element
+ is_map(first_element) -> first_element["href"] || ""
+ true -> ""
+ end
+
+ object
+ |> Map.put("url", url_string)
+ end
+
+ def fix_url(object), do: object
+
+ def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
emoji =
emoji
|> Enum.reduce(%{}, fn data, mapping ->
- name = data["name"]
-
- name =
- if String.starts_with?(name, ":") do
- name |> String.slice(1..-2)
- else
- name
- end
+ name = String.trim(data["name"], ":")
mapping |> Map.put(name, data["icon"]["url"])
end)
@@ -168,18 +224,37 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("emoji", emoji)
end
- def fix_tag(object) do
+ def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
+ name = String.trim(tag["name"], ":")
+ emoji = %{name => tag["icon"]["url"]}
+
+ object
+ |> Map.put("emoji", emoji)
+ end
+
+ def fix_emoji(object), do: object
+
+ def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
tags =
- (object["tag"] || [])
+ tag
|> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
|> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
- combined = (object["tag"] || []) ++ tags
+ combined = tag ++ tags
+
+ object
+ |> Map.put("tag", combined)
+ end
+
+ def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
+ combined = [tag, String.slice(hashtag, 1..-1)]
object
|> Map.put("tag", combined)
end
+ def fix_tag(object), do: object
+
# content map usually only has one language so this will do for now.
def fix_content_map(%{"contentMap" => content_map} = object) do
content_groups = Map.to_list(content_map)
@@ -201,7 +276,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
# - tags
# - emoji
def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
- when objtype in ["Article", "Note", "Video"] do
+ when objtype in ["Article", "Note", "Video", "Page"] do
actor = get_actor(data)
data =
@@ -285,8 +360,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def handle_incoming(
%{"type" => "Accept", "object" => follow_object, "actor" => actor, "id" => id} = data
) do
- with %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
+ with actor <- get_actor(data),
+ %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
+ {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, activity} <-
ActivityPub.accept(%{
@@ -309,8 +386,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def handle_incoming(
%{"type" => "Reject", "object" => follow_object, "actor" => actor, "id" => id} = data
) do
- with %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
+ with actor <- get_actor(data),
+ %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
+ {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, activity} <-
ActivityPub.accept(%{
@@ -329,11 +408,11 @@ 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 %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
- {:ok, object} <-
- get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
+ with actor <- get_actor(data),
+ %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
+ {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
{:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
{:ok, activity}
else
@@ -342,11 +421,11 @@ 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 %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
- {:ok, object} <-
- get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
+ with actor <- get_actor(data),
+ %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
+ {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
{:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false) do
{:ok, activity}
else
@@ -388,15 +467,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
- # TODO: Make secure.
+ # TODO: We presently assume that any actor on the same origin domain as the object being
+ # deleted has the rights to delete that object. A better way to validate whether or not
+ # the object should be deleted is to refetch the object URI, which should return either
+ # an error or a tombstone. This would allow us to verify that a deletion actually took
+ # place.
def handle_incoming(
- %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = _data
+ %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data
) do
object_id = Utils.get_ap_id(object_id)
- with %User{} = _actor <- User.get_or_fetch_by_ap_id(actor),
- {:ok, object} <-
- get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
+ with actor <- get_actor(data),
+ %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
+ {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
+ :ok <- contain_origin(actor.ap_id, object.data),
{:ok, activity} <- ActivityPub.delete(object, false) do
{:ok, activity}
else
@@ -410,11 +494,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"object" => %{"type" => "Announce", "object" => object_id},
"actor" => actor,
"id" => id
- } = _data
+ } = data
) do
- with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
- {:ok, object} <-
- get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
+ with actor <- get_actor(data),
+ %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
+ {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
{:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
{:ok, activity}
else
@@ -440,9 +524,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
- @ap_config Application.get_env(:pleroma, :activitypub)
- @accept_blocks Keyword.get(@ap_config, :accept_blocks)
-
def handle_incoming(
%{
"type" => "Undo",
@@ -451,7 +532,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"id" => id
} = _data
) do
- with true <- @accept_blocks,
+ with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
%User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
%User{} = blocker <- User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
@@ -465,7 +546,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def handle_incoming(
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = data
) do
- with true <- @accept_blocks,
+ with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
%User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
%User{} = blocker = User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
@@ -483,11 +564,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"object" => %{"type" => "Like", "object" => object_id},
"actor" => actor,
"id" => id
- } = _data
+ } = data
) do
- with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
- {:ok, object} <-
- get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
+ with actor <- get_actor(data),
+ %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
+ {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
{:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
{:ok, activity}
else
@@ -497,6 +578,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def handle_incoming(_), do: :error
+ def fetch_obj_helper(id) when is_bitstring(id), do: ActivityPub.fetch_object_from_id(id)
+ def fetch_obj_helper(obj) when is_map(obj), do: ActivityPub.fetch_object_from_id(obj["id"])
+
def get_obj_helper(id) do
if object = Object.normalize(id), do: {:ok, object}, else: nil
end
@@ -523,6 +607,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> prepare_attachments
|> set_conversation
|> set_reply_to_uri
+ |> strip_internal_fields
+ |> strip_internal_tags
end
# @doc
@@ -538,7 +624,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
data =
data
|> Map.put("object", object)
- |> Map.put("@context", "https://www.w3.org/ns/activitystreams")
+ |> Map.merge(Utils.make_json_ld_header())
{:ok, data}
end
@@ -557,7 +643,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
data =
data
|> Map.put("object", object)
- |> Map.put("@context", "https://www.w3.org/ns/activitystreams")
+ |> Map.merge(Utils.make_json_ld_header())
{:ok, data}
end
@@ -575,7 +661,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
data =
data
|> Map.put("object", object)
- |> Map.put("@context", "https://www.w3.org/ns/activitystreams")
+ |> Map.merge(Utils.make_json_ld_header())
{:ok, data}
end
@@ -585,14 +671,14 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
data =
data
|> maybe_fix_object_url
- |> Map.put("@context", "https://www.w3.org/ns/activitystreams")
+ |> Map.merge(Utils.make_json_ld_header())
{:ok, data}
end
def maybe_fix_object_url(data) do
if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
- case ActivityPub.fetch_object_from_id(data["object"]) do
+ case fetch_obj_helper(data["object"]) do
{:ok, relative_object} ->
if relative_object.data["external_url"] do
_data =
@@ -627,12 +713,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
def add_mention_tags(object) do
- recipients = object["to"] ++ (object["cc"] || [])
-
mentions =
- recipients
- |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
- |> Enum.filter(& &1)
+ object
+ |> Utils.get_notified_from_object()
|> Enum.map(fn user ->
%{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
end)
@@ -692,6 +775,29 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("attachment", attachments)
end
+ defp strip_internal_fields(object) do
+ object
+ |> Map.drop([
+ "likes",
+ "like_count",
+ "announcements",
+ "announcement_count",
+ "emoji",
+ "context_id"
+ ])
+ end
+
+ defp strip_internal_tags(%{"tag" => tags} = object) do
+ tags =
+ tags
+ |> Enum.filter(fn x -> is_map(x) end)
+
+ object
+ |> Map.put("tag", tags)
+ end
+
+ defp strip_internal_tags(object), do: object
+
defp user_upgrade_task(user) do
old_follower_address = User.ap_followers(user)
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 0664b5a2e..549148989 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -1,11 +1,13 @@
defmodule Pleroma.Web.ActivityPub.Utils do
- alias Pleroma.{Repo, Web, Object, Activity, User}
+ alias Pleroma.{Repo, Web, Object, Activity, User, Notification}
alias Pleroma.Web.Router.Helpers
alias Pleroma.Web.Endpoint
alias Ecto.{Changeset, UUID}
import Ecto.Query
require Logger
+ @supported_object_types ["Article", "Note", "Video", "Page"]
+
# Some implementations send the actor URI as the actor field, others send the entire actor object,
# so figure out what the actor's URI is based on what we have.
def get_ap_id(object) do
@@ -19,22 +21,58 @@ defmodule Pleroma.Web.ActivityPub.Utils do
Map.put(params, "actor", get_ap_id(params["actor"]))
end
+ defp recipient_in_collection(ap_id, coll) when is_binary(coll), do: ap_id == coll
+ defp recipient_in_collection(ap_id, coll) when is_list(coll), do: ap_id in coll
+ defp recipient_in_collection(_, _), do: false
+
+ def recipient_in_message(ap_id, params) do
+ cond do
+ recipient_in_collection(ap_id, params["to"]) ->
+ true
+
+ recipient_in_collection(ap_id, params["cc"]) ->
+ true
+
+ recipient_in_collection(ap_id, params["bto"]) ->
+ true
+
+ recipient_in_collection(ap_id, params["bcc"]) ->
+ true
+
+ # if the message is unaddressed at all, then assume it is directly addressed
+ # to the recipient
+ !params["to"] && !params["cc"] && !params["bto"] && !params["bcc"] ->
+ true
+
+ true ->
+ false
+ end
+ end
+
+ defp extract_list(target) when is_binary(target), do: [target]
+ defp extract_list(lst) when is_list(lst), do: lst
+ defp extract_list(_), do: []
+
+ def maybe_splice_recipient(ap_id, params) do
+ need_splice =
+ !recipient_in_collection(ap_id, params["to"]) &&
+ !recipient_in_collection(ap_id, params["cc"])
+
+ cc_list = extract_list(params["cc"])
+
+ if need_splice do
+ params
+ |> Map.put("cc", [ap_id | cc_list])
+ else
+ params
+ end
+ end
+
def make_json_ld_header do
%{
"@context" => [
"https://www.w3.org/ns/activitystreams",
- "https://w3id.org/security/v1",
- %{
- "manuallyApprovesFollowers" => "as:manuallyApprovesFollowers",
- "sensitive" => "as:sensitive",
- "Hashtag" => "as:Hashtag",
- "ostatus" => "http://ostatus.org#",
- "atomUri" => "ostatus:atomUri",
- "inReplyToAtomUri" => "ostatus:inReplyToAtomUri",
- "conversation" => "ostatus:conversation",
- "toot" => "http://joinmastodon.org/ns#",
- "Emoji" => "toot:Emoji"
- }
+ "#{Web.base_url()}/schemas/litepub-0.1.jsonld"
]
}
end
@@ -59,6 +97,21 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"#{Web.base_url()}/#{type}/#{UUID.generate()}"
end
+ def get_notified_from_object(%{"type" => type} = object) when type in @supported_object_types do
+ fake_create_activity = %{
+ "to" => object["to"],
+ "cc" => object["cc"],
+ "type" => "Create",
+ "object" => object
+ }
+
+ Notification.get_notified_from_activity(%Activity{data: fake_create_activity}, false)
+ end
+
+ def get_notified_from_object(object) do
+ Notification.get_notified_from_activity(%Activity{data: object}, false)
+ end
+
def create_context(context) do
context = context || generate_id("contexts")
changeset = Object.context_mapping(context)
@@ -128,7 +181,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
Inserts a full object if it is contained in an activity.
"""
def insert_full_object(%{"object" => %{"type" => type} = object_data})
- when is_map(object_data) and type in ["Article", "Note", "Video"] do
+ when is_map(object_data) and type in @supported_object_types do
with {:ok, _} <- Object.create(object_data) do
:ok
end
@@ -247,11 +300,11 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"actor" => follower_id,
"to" => [followed_id],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
- "object" => followed_id
+ "object" => followed_id,
+ "state" => "pending"
}
data = if activity_id, do: Map.put(data, "id", activity_id), else: data
- data = if User.locked?(followed), do: Map.put(data, "state", "pending"), else: data
data
end
diff --git a/lib/pleroma/web/activity_pub/views/object_view.ex b/lib/pleroma/web/activity_pub/views/object_view.ex
index cc0b0556b..ff664636c 100644
--- a/lib/pleroma/web/activity_pub/views/object_view.ex
+++ b/lib/pleroma/web/activity_pub/views/object_view.ex
@@ -1,27 +1,34 @@
defmodule Pleroma.Web.ActivityPub.ObjectView do
use Pleroma.Web, :view
+ alias Pleroma.{Object, Activity}
alias Pleroma.Web.ActivityPub.Transmogrifier
- def render("object.json", %{object: object}) do
- base = %{
- "@context" => [
- "https://www.w3.org/ns/activitystreams",
- "https://w3id.org/security/v1",
- %{
- "manuallyApprovesFollowers" => "as:manuallyApprovesFollowers",
- "sensitive" => "as:sensitive",
- "Hashtag" => "as:Hashtag",
- "ostatus" => "http://ostatus.org#",
- "atomUri" => "ostatus:atomUri",
- "inReplyToAtomUri" => "ostatus:inReplyToAtomUri",
- "conversation" => "ostatus:conversation",
- "toot" => "http://joinmastodon.org/ns#",
- "Emoji" => "toot:Emoji"
- }
- ]
- }
+ def render("object.json", %{object: %Object{} = object}) do
+ base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
additional = Transmogrifier.prepare_object(object.data)
Map.merge(base, additional)
end
+
+ def render("object.json", %{object: %Activity{data: %{"type" => "Create"}} = activity}) do
+ base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
+ object = Object.normalize(activity.data["object"])
+
+ additional =
+ Transmogrifier.prepare_object(activity.data)
+ |> Map.put("object", Transmogrifier.prepare_object(object.data))
+
+ Map.merge(base, additional)
+ end
+
+ def render("object.json", %{object: %Activity{} = activity}) do
+ base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
+ object = Object.normalize(activity.data["object"])
+
+ additional =
+ Transmogrifier.prepare_object(activity.data)
+ |> Map.put("object", object.data["id"])
+
+ Map.merge(base, additional)
+ end
end
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index 16419e1b7..eb335813d 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -17,7 +17,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do
public_key = :public_key.pem_encode([public_key])
%{
- "@context" => "https://www.w3.org/ns/activitystreams",
"id" => user.ap_id,
"type" => "Application",
"following" => "#{user.ap_id}/following",
@@ -36,6 +35,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"sharedInbox" => "#{Pleroma.Web.Endpoint.url()}/inbox"
}
}
+ |> Map.merge(Utils.make_json_ld_header())
end
def render("user.json", %{user: user}) do
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
new file mode 100644
index 000000000..bcdb4ba37
--- /dev/null
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -0,0 +1,158 @@
+defmodule Pleroma.Web.AdminAPI.AdminAPIController do
+ use Pleroma.Web, :controller
+ alias Pleroma.{User, Repo}
+ alias Pleroma.Web.ActivityPub.Relay
+
+ require Logger
+
+ action_fallback(:errors)
+
+ def user_delete(conn, %{"nickname" => nickname}) do
+ user = User.get_by_nickname(nickname)
+
+ if user.local == true do
+ User.delete(user)
+ else
+ User.delete(user)
+ end
+
+ conn
+ |> json(nickname)
+ end
+
+ def user_create(
+ conn,
+ %{"nickname" => nickname, "email" => email, "password" => password}
+ ) do
+ new_user = %{
+ nickname: nickname,
+ name: nickname,
+ email: email,
+ password: password,
+ password_confirmation: password,
+ bio: "."
+ }
+
+ User.register_changeset(%User{}, new_user)
+ |> Repo.insert!()
+
+ conn
+ |> json(new_user.nickname)
+ end
+
+ def right_add(conn, %{"permission_group" => permission_group, "nickname" => nickname})
+ when permission_group in ["moderator", "admin"] do
+ user = User.get_by_nickname(nickname)
+
+ info =
+ user.info
+ |> Map.put("is_" <> permission_group, true)
+
+ cng = User.info_changeset(user, %{info: info})
+ {:ok, user} = User.update_and_set_cache(cng)
+
+ conn
+ |> json(user.info)
+ end
+
+ def right_get(conn, %{"nickname" => nickname}) do
+ user = User.get_by_nickname(nickname)
+
+ conn
+ |> json(user.info)
+ 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,
+ %{
+ "permission_group" => permission_group,
+ "nickname" => nickname
+ }
+ )
+ when permission_group in ["moderator", "admin"] do
+ if admin_nickname == nickname do
+ conn
+ |> put_status(403)
+ |> json(%{error: "You can't revoke your own admin status."})
+ else
+ user = User.get_by_nickname(nickname)
+
+ info =
+ user.info
+ |> Map.put("is_" <> permission_group, false)
+
+ cng = User.info_changeset(user, %{info: info})
+ {:ok, user} = User.update_and_set_cache(cng)
+
+ conn
+ |> json(user.info)
+ end
+ end
+
+ def right_delete(conn, _) do
+ conn
+ |> put_status(404)
+ |> json(%{error: "No such permission_group"})
+ end
+
+ def relay_follow(conn, %{"relay_url" => target}) do
+ {status, message} = Relay.follow(target)
+
+ if status == :ok do
+ conn
+ |> json(target)
+ else
+ 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)
+ else
+ conn
+ |> put_status(500)
+ |> json(target)
+ end
+ end
+
+ @shortdoc "Get a account registeration invite token (base64 string)"
+ def get_invite_token(conn, _params) do
+ {:ok, token} = Pleroma.UserInviteToken.create_token()
+
+ conn
+ |> json(token.token)
+ end
+
+ @shortdoc "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)
+
+ conn
+ |> json(token.token)
+ end
+
+ def errors(conn, {:param_cast, _}) do
+ conn
+ |> put_status(400)
+ |> json("Invalid parameters")
+ end
+
+ def errors(conn, _) do
+ conn
+ |> put_status(500)
+ |> json("Something went wrong")
+ end
+end
diff --git a/lib/pleroma/web/channels/user_socket.ex b/lib/pleroma/web/channels/user_socket.ex
index 21b22b409..07ddee169 100644
--- a/lib/pleroma/web/channels/user_socket.ex
+++ b/lib/pleroma/web/channels/user_socket.ex
@@ -4,9 +4,7 @@ defmodule Pleroma.Web.UserSocket do
## Channels
# channel "room:*", Pleroma.Web.RoomChannel
- if Application.get_env(:pleroma, :chat) |> Keyword.get(:enabled) do
- channel("chat:*", Pleroma.Web.ChatChannel)
- end
+ channel("chat:*", Pleroma.Web.ChatChannel)
## Transports
transport(:websocket, Phoenix.Transports.WebSocket)
@@ -24,7 +22,8 @@ defmodule Pleroma.Web.UserSocket do
# See `Phoenix.Token` documentation for examples in
# performing token verification on connect.
def connect(%{"token" => token}, socket) do
- with {:ok, user_id} <- Phoenix.Token.verify(socket, "user socket", token, max_age: 84600),
+ with true <- Pleroma.Config.get([:chat, :enabled]),
+ {:ok, user_id} <- Phoenix.Token.verify(socket, "user socket", token, max_age: 84600),
%User{} = user <- Pleroma.Repo.get(User, user_id) do
{:ok, assign(socket, :user_name, user.nickname)}
else
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index 125c57d05..77e4dbbd7 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -36,7 +36,6 @@ defmodule Pleroma.Web.CommonAPI do
def favorite(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
- false <- activity.data["actor"] == user.ap_id,
object <- Object.normalize(activity.data["object"]["id"]) do
ActivityPub.like(user, object)
else
@@ -47,7 +46,6 @@ defmodule Pleroma.Web.CommonAPI do
def unfavorite(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
- false <- activity.data["actor"] == user.ap_id,
object <- Object.normalize(activity.data["object"]["id"]) do
ActivityPub.unlike(user, object)
else
@@ -72,22 +70,37 @@ defmodule Pleroma.Web.CommonAPI do
def get_visibility(_), do: "public"
- @instance Application.get_env(:pleroma, :instance)
- @limit Keyword.get(@instance, :limit)
+ defp get_content_type(content_type) do
+ if Enum.member?(Pleroma.Config.get([:instance, :allowed_post_formats]), content_type) do
+ content_type
+ else
+ "text/plain"
+ end
+ end
+
def post(user, %{"status" => status} = data) do
visibility = get_visibility(data)
+ limit = Pleroma.Config.get([:instance, :limit])
with status <- String.trim(status),
- length when length in 1..@limit <- String.length(status),
attachments <- attachments_from_ids(data["media_ids"]),
mentions <- Formatter.parse_mentions(status),
inReplyTo <- get_replied_to_activity(data["in_reply_to_status_id"]),
{to, cc} <- to_for_user_and_mentions(user, mentions, inReplyTo, visibility),
tags <- Formatter.parse_tags(status, data),
content_html <-
- make_content_html(status, mentions, attachments, tags, data["no_attachment_links"]),
+ make_content_html(
+ status,
+ mentions,
+ attachments,
+ tags,
+ get_content_type(data["content_type"]),
+ data["no_attachment_links"]
+ ),
context <- make_context(inReplyTo),
cw <- data["spoiler_text"],
+ full_payload <- String.trim(status <> (data["spoiler_text"] || "")),
+ length when length in 1..limit <- String.length(full_payload),
object <-
make_note_data(
user.ap_id,
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 358ca22ac..728f24c7e 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -2,6 +2,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
alias Pleroma.{Repo, Object, Formatter, Activity}
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Endpoint
+ alias Pleroma.Web.MediaProxy
alias Pleroma.User
alias Calendar.Strftime
alias Comeonin.Pbkdf2
@@ -18,6 +19,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
+ def get_replied_to_activity(""), do: nil
+
def get_replied_to_activity(id) when not is_nil(id) do
Repo.get(Activity, id)
end
@@ -31,21 +34,29 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do
- to = ["https://www.w3.org/ns/activitystreams#Public"]
-
mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
- cc = [user.follower_address | mentioned_users]
+
+ to = ["https://www.w3.org/ns/activitystreams#Public" | mentioned_users]
+ cc = [user.follower_address]
if inReplyTo do
- {to, Enum.uniq([inReplyTo.data["actor"] | cc])}
+ {Enum.uniq([inReplyTo.data["actor"] | to]), cc}
else
{to, cc}
end
end
def to_for_user_and_mentions(user, mentions, inReplyTo, "unlisted") do
- {to, cc} = to_for_user_and_mentions(user, mentions, inReplyTo, "public")
- {cc, to}
+ mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
+
+ to = [user.follower_address | mentioned_users]
+ cc = ["https://www.w3.org/ns/activitystreams#Public"]
+
+ if inReplyTo do
+ {Enum.uniq([inReplyTo.data["actor"] | to]), cc}
+ else
+ {to, cc}
+ end
end
def to_for_user_and_mentions(user, mentions, inReplyTo, "private") do
@@ -63,9 +74,16 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
- def make_content_html(status, mentions, attachments, tags, no_attachment_links \\ false) do
+ def make_content_html(
+ status,
+ mentions,
+ attachments,
+ tags,
+ content_type,
+ no_attachment_links \\ false
+ ) do
status
- |> format_input(mentions, tags)
+ |> format_input(mentions, tags, content_type)
|> maybe_add_attachments(attachments, no_attachment_links)
end
@@ -81,8 +99,9 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def add_attachments(text, attachments) do
attachment_text =
Enum.map(attachments, fn
- %{"url" => [%{"href" => href} | _]} ->
- name = URI.decode(Path.basename(href))
+ %{"url" => [%{"href" => href} | _]} = attachment ->
+ name = attachment["name"] || URI.decode(Path.basename(href))
+ href = MediaProxy.url(href)
"<a href=\"#{href}\" class='attachment'>#{shortname(name)}</a>"
_ ->
@@ -92,9 +111,9 @@ defmodule Pleroma.Web.CommonAPI.Utils do
Enum.join([text | attachment_text], "<br>")
end
- def format_input(text, mentions, tags) do
+ def format_input(text, mentions, tags, "text/plain") do
text
- |> Formatter.html_escape()
+ |> Formatter.html_escape("text/plain")
|> String.replace(~r/\r?\n/, "<br>")
|> (&{[], &1}).()
|> Formatter.add_links()
@@ -103,6 +122,26 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> Formatter.finalize()
end
+ def format_input(text, mentions, tags, "text/html") do
+ text
+ |> Formatter.html_escape("text/html")
+ |> String.replace(~r/\r?\n/, "<br>")
+ |> (&{[], &1}).()
+ |> Formatter.add_user_links(mentions)
+ |> Formatter.finalize()
+ end
+
+ def format_input(text, mentions, tags, "text/markdown") do
+ text
+ |> Earmark.as_html!()
+ |> Formatter.html_escape("text/html")
+ |> String.replace(~r/\r?\n/, "")
+ |> (&{[], &1}).()
+ |> Formatter.add_user_links(mentions)
+ |> Formatter.add_hashtag_links(tags)
+ |> Formatter.finalize()
+ end
+
def add_tag_links(text, tags) do
tags =
tags
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index 955bd61f3..8728c908b 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -1,9 +1,7 @@
defmodule Pleroma.Web.Endpoint do
use Phoenix.Endpoint, otp_app: :pleroma
- if Application.get_env(:pleroma, :chat) |> Keyword.get(:enabled) do
- socket("/socket", Pleroma.Web.UserSocket)
- end
+ socket("/socket", Pleroma.Web.UserSocket)
socket("/api/v1", Pleroma.Web.MastodonAPI.MastodonSocket)
@@ -11,13 +9,17 @@ defmodule Pleroma.Web.Endpoint do
#
# You should set gzip to true if you are running phoenix.digest
# when deploying your static files in production.
+ plug(CORSPlug)
+ plug(Pleroma.Plugs.HTTPSecurityPlug)
+
plug(Plug.Static, at: "/media", from: Pleroma.Uploaders.Local.upload_path(), gzip: false)
plug(
Plug.Static,
at: "/",
from: :pleroma,
- only: ~w(index.html static finmoji emoji packs sounds images instance sw.js favicon.png)
+ only:
+ ~w(index.html static finmoji emoji packs sounds images instance sw.js favicon.png schemas)
)
# Code reloading can be explicitly enabled under the
@@ -42,14 +44,19 @@ defmodule Pleroma.Web.Endpoint do
plug(Plug.MethodOverride)
plug(Plug.Head)
+ cookie_name =
+ if Application.get_env(:pleroma, Pleroma.Web.Endpoint) |> Keyword.get(:secure_cookie_flag),
+ do: "__Host-pleroma_key",
+ else: "pleroma_key"
+
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
plug(
Plug.Session,
store: :cookie,
- key: "_pleroma_key",
- signing_salt: "CqaoopA2",
+ key: cookie_name,
+ signing_salt: {Pleroma.Config, :get, [[__MODULE__, :signing_salt], "CqaoopA2"]},
http_only: true,
secure:
Application.get_env(:pleroma, Pleroma.Web.Endpoint) |> Keyword.get(:secure_cookie_flag),
diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex
index 078f3ec11..ac3d7c132 100644
--- a/lib/pleroma/web/federator/federator.ex
+++ b/lib/pleroma/web/federator/federator.ex
@@ -3,17 +3,17 @@ defmodule Pleroma.Web.Federator do
alias Pleroma.User
alias Pleroma.Activity
alias Pleroma.Web.{WebFinger, Websub}
+ alias Pleroma.Web.Federator.RetryQueue
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.OStatus
require Logger
@websub Application.get_env(:pleroma, :websub)
@ostatus Application.get_env(:pleroma, :ostatus)
@httpoison Application.get_env(:pleroma, :httpoison)
- @instance Application.get_env(:pleroma, :instance)
- @federating Keyword.get(@instance, :federating)
@max_jobs 20
def init(args) do
@@ -65,15 +65,17 @@ defmodule Pleroma.Web.Federator do
{:ok, actor} = WebFinger.ensure_keys_present(actor)
if ActivityPub.is_public?(activity) do
- Logger.info(fn -> "Sending #{activity.data["id"]} out via WebSub" end)
- Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
+ if OStatus.is_representable?(activity) do
+ Logger.info(fn -> "Sending #{activity.data["id"]} out via WebSub" end)
+ Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
- Logger.info(fn -> "Sending #{activity.data["id"]} out via Salmon" end)
- Pleroma.Web.Salmon.publish(actor, activity)
+ Logger.info(fn -> "Sending #{activity.data["id"]} out via Salmon" end)
+ Pleroma.Web.Salmon.publish(actor, activity)
+ end
- if Mix.env() != :test do
+ if Keyword.get(Application.get_env(:pleroma, :instance), :allow_relay) do
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
- Pleroma.Web.ActivityPub.Relay.publish(activity)
+ Relay.publish(activity)
end
end
@@ -100,44 +102,46 @@ defmodule Pleroma.Web.Federator do
params = Utils.normalize_params(params)
+ # NOTE: we use the actor ID to do the containment, this is fine because an
+ # actor shouldn't be acting on objects outside their own AP server.
with {:ok, _user} <- ap_enabled_actor(params["actor"]),
nil <- Activity.normalize(params["id"]),
- {:ok, _activity} <- Transmogrifier.handle_incoming(params) do
+ :ok <- Transmogrifier.contain_origin_from_id(params["actor"], params),
+ {:ok, activity} <- Transmogrifier.handle_incoming(params) do
+ {:ok, activity}
else
%Activity{} ->
Logger.info("Already had #{params["id"]}")
+ :error
_e ->
# Just drop those for now
Logger.info("Unhandled activity")
Logger.info(Poison.encode!(params, pretty: 2))
+ :error
end
end
def handle(:publish_single_ap, params) do
- ActivityPub.publish_one(params)
- end
-
- def handle(:publish_single_websub, %{xml: xml, topic: topic, callback: callback, secret: secret}) do
- signature = @websub.sign(secret || "", xml)
- Logger.debug(fn -> "Pushing #{topic} to #{callback}" end)
-
- with {:ok, %{status_code: 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.debug(fn -> "Pushed to #{callback}, code #{code}" end)
- else
- e ->
- Logger.debug(fn -> "Couldn't push to #{callback}, #{inspect(e)}" end)
+ case ActivityPub.publish_one(params) do
+ {:ok, _} ->
+ :ok
+
+ {:error, _} ->
+ RetryQueue.enqueue(params, ActivityPub)
+ end
+ end
+
+ def handle(
+ :publish_single_websub,
+ %{xml: xml, topic: topic, callback: callback, secret: secret} = params
+ ) do
+ case Websub.publish_one(params) do
+ {:ok, _} ->
+ :ok
+
+ {:error, _} ->
+ RetryQueue.enqueue(params, Websub)
end
end
@@ -146,11 +150,15 @@ defmodule Pleroma.Web.Federator do
{:error, "Don't know what to do with this"}
end
- def enqueue(type, payload, priority \\ 1) do
- if @federating do
- if Mix.env() == :test do
+ if Mix.env() == :test do
+ def enqueue(type, payload, priority \\ 1) do
+ if Pleroma.Config.get([:instance, :federating]) do
handle(type, payload)
- else
+ end
+ end
+ else
+ def enqueue(type, payload, priority \\ 1) do
+ if Pleroma.Config.get([:instance, :federating]) do
GenServer.cast(__MODULE__, {:enqueue, type, payload, priority})
end
end
diff --git a/lib/pleroma/web/federator/retry_queue.ex b/lib/pleroma/web/federator/retry_queue.ex
new file mode 100644
index 000000000..06c094f26
--- /dev/null
+++ b/lib/pleroma/web/federator/retry_queue.ex
@@ -0,0 +1,71 @@
+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
+
+ def init(args) do
+ {:ok, args}
+ end
+
+ def start_link() do
+ GenServer.start_link(__MODULE__, %{delivered: 0, dropped: 0}, name: __MODULE__)
+ end
+
+ def enqueue(data, transport, retries \\ 0) do
+ GenServer.cast(__MODULE__, {:maybe_enqueue, data, transport, retries + 1})
+ end
+
+ def get_retry_params(retries) do
+ if retries > @max_retries do
+ {:drop, "Max retries reached"}
+ else
+ {:retry, growth_function(retries)}
+ end
+ end
+
+ def handle_cast({:maybe_enqueue, data, transport, retries}, %{dropped: drop_count} = state) do
+ case get_retry_params(retries) do
+ {:retry, timeout} ->
+ Process.send_after(
+ __MODULE__,
+ {:send, data, transport, retries},
+ growth_function(retries)
+ )
+
+ {:noreply, state}
+
+ {:drop, message} ->
+ Logger.debug(message)
+ {:noreply, %{state | dropped: drop_count + 1}}
+ end
+ end
+
+ def handle_info({:send, data, transport, retries}, %{delivered: delivery_count} = state) do
+ case transport.publish_one(data) do
+ {:ok, _} ->
+ {:noreply, %{state | delivered: delivery_count + 1}}
+
+ {:error, reason} ->
+ enqueue(data, transport, retries)
+ {:noreply, state}
+ end
+ end
+
+ def handle_info(unknown, state) do
+ Logger.debug("RetryQueue: don't know what to do with #{inspect(unknown)}, ignoring")
+ {:noreply, state}
+ end
+
+ defp growth_function(retries) do
+ round(@initial_timeout * :math.pow(retries, 3))
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index de5b2696f..f5b23971e 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -35,6 +35,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def update_credentials(%{assigns: %{user: user}} = conn, params) do
original_user = user
+ avatar_upload_limit =
+ Application.get_env(:pleroma, :instance)
+ |> Keyword.fetch(:avatar_upload_limit)
+
+ banner_upload_limit =
+ Application.get_env(:pleroma, :instance)
+ |> Keyword.fetch(:banner_upload_limit)
+
params =
if bio = params["note"] do
Map.put(params, "bio", bio)
@@ -52,7 +60,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
user =
if avatar = params["avatar"] do
with %Plug.Upload{} <- avatar,
- {:ok, object} <- ActivityPub.upload(avatar),
+ {:ok, object} <- ActivityPub.upload(avatar, avatar_upload_limit),
change = Ecto.Changeset.change(user, %{avatar: object.data}),
{:ok, user} = User.update_and_set_cache(change) do
user
@@ -66,7 +74,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
user =
if banner = params["header"] do
with %Plug.Upload{} <- banner,
- {:ok, object} <- ActivityPub.upload(banner),
+ {:ok, object} <- ActivityPub.upload(banner, banner_upload_limit),
new_info <- Map.put(user.info, "banner", object.data),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) do
@@ -124,22 +132,23 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
- @instance Application.get_env(:pleroma, :instance)
@mastodon_api_level "2.5.0"
def masto_instance(conn, _params) do
+ instance = Pleroma.Config.get(:instance)
+
response = %{
uri: Web.base_url(),
- title: Keyword.get(@instance, :name),
- description: Keyword.get(@instance, :description),
- version: "#{@mastodon_api_level} (compatible; #{Keyword.get(@instance, :version)})",
- email: Keyword.get(@instance, :email),
+ title: Keyword.get(instance, :name),
+ description: Keyword.get(instance, :description),
+ version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
+ email: Keyword.get(instance, :email),
urls: %{
streaming_api: String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws")
},
stats: Stats.get_stats(),
thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
- max_toot_chars: Keyword.get(@instance, :limit)
+ max_toot_chars: Keyword.get(instance, :limit)
}
json(conn, response)
@@ -150,7 +159,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
defp mastodonized_emoji do
- Pleroma.Formatter.get_custom_emoji()
+ Pleroma.Emoji.get_all()
|> Enum.map(fn {shortcode, relative_url} ->
url = to_string(URI.merge(Web.base_url(), relative_url))
@@ -223,6 +232,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
activities =
ActivityPub.fetch_activities([user.ap_id | user.following], params)
+ |> ActivityPub.contain_timeline(user)
|> Enum.reverse()
conn
@@ -268,9 +278,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
- def dm_timeline(%{assigns: %{user: user}} = conn, _params) do
+ def dm_timeline(%{assigns: %{user: user}} = conn, params) do
query =
- ActivityPub.fetch_activities_query([user.ap_id], %{"type" => "Create", visibility: "direct"})
+ ActivityPub.fetch_activities_query(
+ [user.ap_id],
+ Map.merge(params, %{"type" => "Create", visibility: "direct"})
+ )
activities = Repo.all(query)
@@ -282,7 +295,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
true <- ActivityPub.visible_for_user?(activity, user) do
- render(conn, StatusView, "status.json", %{activity: activity, for: user})
+ try_render(conn, StatusView, "status.json", %{activity: activity, for: user})
end
end
@@ -345,7 +358,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
{:ok, activity} =
Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
- render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
+ try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end
def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
@@ -361,28 +374,28 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
- render(conn, StatusView, "status.json", %{activity: announce, for: user, as: :activity})
+ try_render(conn, StatusView, "status.json", %{activity: announce, for: user, as: :activity})
end
end
def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
- render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
+ try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end
end
def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
- render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
+ try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end
end
def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
- render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
+ try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end
end
@@ -434,6 +447,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
render(conn, AccountView, "relationships.json", %{user: user, targets: targets})
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 update_media(%{assigns: %{user: _}} = conn, data) do
with %Object{} = object <- Repo.get(Object, data["id"]),
true <- is_binary(data["description"]),
@@ -499,6 +518,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> Map.put("type", "Create")
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
+ |> Map.put("tag", String.downcase(params["tag"]))
activities =
ActivityPub.fetch_public_activities(params)
@@ -574,7 +594,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id),
{:ok, follower} <- User.maybe_direct_follow(follower, followed),
- {:ok, _activity} <- ActivityPub.follow(follower, followed) do
+ {:ok, _activity} <- ActivityPub.follow(follower, followed),
+ {:ok, follower, followed} <-
+ User.wait_and_refresh(
+ Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
+ follower,
+ followed
+ ) do
render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
else
{:error, message} ->
@@ -765,6 +791,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
+ def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
+ lists = Pleroma.List.get_lists_account_belongs(user, account_id)
+ res = ListView.render("lists.json", lists: lists)
+ json(conn, res)
+ end
+
def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
{:ok, _list} <- Pleroma.List.delete(list) do
@@ -859,6 +891,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
if user && token do
mastodon_emoji = mastodonized_emoji()
+ limit = Pleroma.Config.get([:instance, :limit])
+
accounts =
Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
@@ -878,7 +912,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
auto_play_gif: false,
display_sensitive_media: false,
reduce_motion: false,
- max_toot_chars: Keyword.get(@instance, :limit)
+ max_toot_chars: limit
},
rights: %{
delete_others_notice: !!user.info["is_moderator"]
@@ -938,7 +972,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
push_subscription: nil,
accounts: accounts,
custom_emojis: mastodon_emoji,
- char_limit: Keyword.get(@instance, :limit)
+ char_limit: limit
}
|> Jason.encode!()
@@ -964,9 +998,29 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
+ def login(conn, %{"code" => code}) do
+ with {:ok, app} <- get_or_make_app(),
+ %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
+ {:ok, token} <- Token.exchange_token(app, auth) do
+ conn
+ |> put_session(:oauth_token, token.token)
+ |> redirect(to: "/web/getting-started")
+ end
+ end
+
def login(conn, _) do
- conn
- |> render(MastodonView, "login.html", %{error: false})
+ with {:ok, app} <- get_or_make_app() do
+ path =
+ o_auth_path(conn, :authorize,
+ response_type: "code",
+ client_id: app.client_id,
+ redirect_uri: ".",
+ scope: app.scopes
+ )
+
+ conn
+ |> redirect(to: path)
+ end
end
defp get_or_make_app() do
@@ -985,22 +1039,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
- def login_post(conn, %{"authorization" => %{"name" => name, "password" => password}}) do
- with %User{} = user <- User.get_by_nickname_or_email(name),
- true <- Pbkdf2.checkpw(password, user.password_hash),
- {:ok, app} <- get_or_make_app(),
- {:ok, auth} <- Authorization.create_authorization(app, user),
- {:ok, token} <- Token.exchange_token(app, auth) do
- conn
- |> put_session(:oauth_token, token.token)
- |> redirect(to: "/web/getting-started")
- else
- _e ->
- conn
- |> render(MastodonView, "login.html", %{error: "Wrong username or password"})
- end
- end
-
def logout(conn, _) do
conn
|> clear_session
@@ -1173,18 +1211,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> json("Something went wrong")
end
- @suggestions Application.get_env(:pleroma, :suggestions)
-
def suggestions(%{assigns: %{user: user}} = conn, _) do
- if Keyword.get(@suggestions, :enabled, false) do
- api = Keyword.get(@suggestions, :third_party_engine, "")
- timeout = Keyword.get(@suggestions, :timeout, 5000)
- limit = Keyword.get(@suggestions, :limit, 23)
+ suggestions = Pleroma.Config.get(:suggestions)
+
+ if Keyword.get(suggestions, :enabled, false) do
+ api = Keyword.get(suggestions, :third_party_engine, "")
+ timeout = Keyword.get(suggestions, :timeout, 5000)
+ limit = Keyword.get(suggestions, :limit, 23)
- host =
- Application.get_env(:pleroma, Pleroma.Web.Endpoint)
- |> Keyword.get(:url)
- |> Keyword.get(:host)
+ host = Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
user = user.nickname
url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user)
@@ -1220,4 +1255,23 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
json(conn, [])
end
end
+
+ def try_render(conn, renderer, target, params)
+ when is_binary(target) do
+ res = render(conn, renderer, target, params)
+
+ if res == nil do
+ conn
+ |> put_status(501)
+ |> json(%{error: "Can't display this activity"})
+ else
+ res
+ end
+ end
+
+ def try_render(conn, _, _, _) do
+ conn
+ |> put_status(501)
+ |> json(%{error: "Can't display this activity"})
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/mastodon_socket.ex b/lib/pleroma/web/mastodon_api/mastodon_socket.ex
index bc628ba56..f3c13d1aa 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_socket.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_socket.ex
@@ -11,9 +11,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonSocket do
timeout: :infinity
)
- def connect(params, socket) do
- with token when not is_nil(token) <- params["access_token"],
- %Token{user_id: user_id} <- Repo.get_by(Token, token: token),
+ 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),
stream
when stream in [
@@ -26,15 +25,37 @@ defmodule Pleroma.Web.MastodonAPI.MastodonSocket do
"list",
"hashtag"
] <- params["stream"] do
- topic = if stream == "list", do: "list:#{params["list"]}", else: stream
- socket_stream = if stream == "hashtag", do: "hashtag:#{params["tag"]}", else: stream
+ topic =
+ case stream do
+ "hashtag" -> "hashtag:#{params["tag"]}"
+ "list" -> "list:#{params["list"]}"
+ _ -> stream
+ end
socket =
socket
|> assign(:topic, topic)
|> assign(:user, user)
- Pleroma.Web.Streamer.add_socket(socket_stream, socket)
+ Pleroma.Web.Streamer.add_socket(topic, socket)
+ {:ok, socket}
+ else
+ _e -> :error
+ end
+ end
+
+ def connect(%{"stream" => stream} = params, socket)
+ when stream in ["public", "public:local", "hashtag"] do
+ topic =
+ case stream do
+ "hashtag" -> "hashtag:#{params["tag"]}"
+ _ -> stream
+ end
+
+ with socket =
+ socket
+ |> assign(:topic, topic) do
+ Pleroma.Web.Streamer.add_socket(topic, socket)
{:ok, socket}
else
_e -> :error
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index 96795c420..b68845e16 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -72,6 +72,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
end
def render("relationship.json", %{user: user, target: target}) do
+ follow_activity = Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(user, target)
+
+ requested =
+ if follow_activity do
+ follow_activity.data["state"] == "pending"
+ else
+ false
+ end
+
%{
id: to_string(target.id),
following: User.following?(user, target),
@@ -79,7 +88,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
blocking: User.blocks?(user, target),
muting: false,
muting_notifications: false,
- requested: false,
+ requested: requested,
domain_blocking: false,
showing_reblogs: false,
endorsed: false
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index ef46ba4fc..2d9a915f0 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -34,6 +34,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
"status.json",
Map.put(opts, :replied_to_activities, replied_to_activities)
)
+ |> Enum.filter(fn x -> not is_nil(x) end)
end
def render(
@@ -60,7 +61,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
in_reply_to_id: nil,
in_reply_to_account_id: nil,
reblog: reblogged,
- content: reblogged[:content],
+ content: reblogged[:content] || "",
created_at: created_at,
reblogs_count: 0,
replies_count: 0,
@@ -158,10 +159,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
end
+ def render("status.json", _) do
+ nil
+ end
+
def render("attachment.json", %{attachment: attachment}) do
[attachment_url | _] = attachment["url"]
media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
- href = attachment_url["href"]
+ href = attachment_url["href"] |> MediaProxy.url()
type =
cond do
@@ -175,9 +180,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
%{
id: to_string(attachment["id"] || hash_id),
- url: MediaProxy.url(href),
+ url: href,
remote_url: href,
- preview_url: MediaProxy.url(href),
+ preview_url: href,
text_url: href,
type: type,
description: attachment["name"]
@@ -225,24 +230,24 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
if !!name and name != "" do
"<p><a href=\"#{object["id"]}\">#{name}</a></p>#{object["content"]}"
else
- object["content"]
+ object["content"] || ""
end
content
end
- def render_content(%{"type" => "Article"} = object) do
+ def render_content(%{"type" => object_type} = object) when object_type in ["Article", "Page"] do
summary = object["name"]
content =
- if !!summary and summary != "" do
+ if !!summary and summary != "" and is_bitstring(object["url"]) do
"<p><a href=\"#{object["url"]}\">#{summary}</a></p>#{object["content"]}"
else
- object["content"]
+ object["content"] || ""
end
content
end
- def render_content(object), do: object["content"]
+ def render_content(object), do: object["content"] || ""
end
diff --git a/lib/pleroma/web/media_proxy/controller.ex b/lib/pleroma/web/media_proxy/controller.ex
index 8195a665e..bb257c262 100644
--- a/lib/pleroma/web/media_proxy/controller.ex
+++ b/lib/pleroma/web/media_proxy/controller.ex
@@ -11,15 +11,47 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
error: "public, must-revalidate, max-age=160"
}
- def remote(conn, %{"sig" => sig, "url" => url}) do
+ # Content-types that will not be returned as content-disposition attachments
+ # Override with :media_proxy, :safe_content_types in the configuration
+ @safe_content_types [
+ "image/gif",
+ "image/jpeg",
+ "image/jpg",
+ "image/png",
+ "image/svg+xml",
+ "audio/mpeg",
+ "audio/mp3",
+ "video/webm",
+ "video/mp4"
+ ]
+
+ def remote(conn, params = %{"sig" => sig, "url" => url}) do
config = Application.get_env(:pleroma, :media_proxy, [])
with true <- Keyword.get(config, :enabled, false),
{:ok, url} <- Pleroma.Web.MediaProxy.decode_url(sig, url),
- {:ok, content_type, body} <- proxy_request(url) do
+ filename <- Path.basename(URI.parse(url).path),
+ true <-
+ if(Map.get(params, "filename"),
+ do: filename == Path.basename(conn.request_path),
+ else: true
+ ),
+ {:ok, content_type, body} <- proxy_request(url),
+ safe_content_type <-
+ Enum.member?(
+ Keyword.get(config, :safe_content_types, @safe_content_types),
+ content_type
+ ) do
conn
|> put_resp_content_type(content_type)
|> set_cache_header(:default)
+ |> put_resp_header(
+ "content-security-policy",
+ "default-src 'none'; style-src 'unsafe-inline'; media-src data:; img-src 'self' data:"
+ )
+ |> put_resp_header("x-xss-protection", "1; mode=block")
+ |> put_resp_header("x-content-type-options", "nosniff")
+ |> put_attachement_header(safe_content_type, filename)
|> send_resp(200, body)
else
false ->
@@ -92,6 +124,12 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
# TODO: the body is passed here as well because some hosts do not provide a content-type.
# At some point we may want to use magic numbers to discover the content-type and reply a proper one.
defp proxy_request_content_type(headers, _body) do
- headers["Content-Type"] || headers["content-type"] || "image/jpeg"
+ headers["Content-Type"] || headers["content-type"] || "application/octet-stream"
+ end
+
+ defp put_attachement_header(conn, true, _), do: conn
+
+ defp put_attachement_header(conn, false, filename) do
+ put_resp_header(conn, "content-disposition", "attachment; filename='#{filename}'")
end
end
diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex
index 37718f48b..0fc0a07b2 100644
--- a/lib/pleroma/web/media_proxy/media_proxy.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy.ex
@@ -3,6 +3,8 @@ defmodule Pleroma.Web.MediaProxy do
def url(nil), do: nil
+ def url(""), do: nil
+
def url(url = "/" <> _), do: url
def url(url) do
@@ -15,7 +17,10 @@ defmodule Pleroma.Web.MediaProxy do
base64 = Base.url_encode64(url, @base64_opts)
sig = :crypto.hmac(:sha, secret, base64)
sig64 = sig |> Base.url_encode64(@base64_opts)
- Keyword.get(config, :base_url, Pleroma.Web.base_url()) <> "/proxy/#{sig64}/#{base64}"
+ filename = if path = URI.parse(url).path, do: "/" <> Path.basename(path), else: ""
+
+ Keyword.get(config, :base_url, Pleroma.Web.base_url()) <>
+ "/proxy/#{sig64}/#{base64}#{filename}"
end
end
diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
index 9c4827426..2ea75cf16 100644
--- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
+++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
@@ -4,6 +4,10 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
alias Pleroma.Stats
alias Pleroma.Web
alias Pleroma.{User, Repo}
+ alias Pleroma.Config
+ alias Pleroma.Web.ActivityPub.MRF
+
+ plug(Pleroma.Web.FederatingPlug)
def schemas(conn, _params) do
response = %{
@@ -27,16 +31,69 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
gopher = Application.get_env(:pleroma, :gopher)
stats = Stats.get_stats()
+ mrf_simple =
+ Application.get_env(:pleroma, :mrf_simple)
+ |> Enum.into(%{})
+
+ mrf_policies =
+ MRF.get_policies()
+ |> Enum.map(fn policy -> to_string(policy) |> String.split(".") |> List.last() end)
+
+ quarantined = Keyword.get(instance, :quarantined_instances)
+
+ quarantined =
+ if is_list(quarantined) do
+ quarantined
+ else
+ []
+ end
+
staff_accounts =
User.moderator_user_query()
|> Repo.all()
|> Enum.map(fn u -> u.ap_id end)
+ mrf_user_allowlist =
+ Config.get([:mrf_user_allowlist], [])
+ |> Enum.into(%{}, fn {k, v} -> {k, length(v)} end)
+
+ mrf_transparency = Keyword.get(instance, :mrf_transparency)
+
+ federation_response =
+ if mrf_transparency do
+ %{
+ mrf_policies: mrf_policies,
+ mrf_simple: mrf_simple,
+ mrf_user_allowlist: mrf_user_allowlist,
+ quarantined_instances: quarantined
+ }
+ else
+ %{}
+ 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
+ ]
+
response = %{
version: "2.0",
software: %{
- name: "pleroma",
- version: Keyword.get(instance, :version)
+ name: Pleroma.Application.name(),
+ version: Pleroma.Application.version()
},
protocols: ["ostatus", "activitypub"],
services: %{
@@ -53,7 +110,6 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
metadata: %{
nodeName: Keyword.get(instance, :name),
nodeDescription: Keyword.get(instance, :description),
- mediaProxy: Keyword.get(media_proxy, :enabled),
private: !Keyword.get(instance, :public, true),
suggestions: %{
enabled: Keyword.get(suggestions, :enabled, false),
@@ -63,8 +119,15 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
web: Keyword.get(suggestions, :web, "")
},
staffAccounts: staff_accounts,
- chat: Keyword.get(chat, :enabled),
- gopher: Keyword.get(gopher, :enabled)
+ federation: federation_response,
+ postFormats: Keyword.get(instance, :allowed_post_formats),
+ uploadLimits: %{
+ general: Keyword.get(instance, :upload_limit),
+ avatar: Keyword.get(instance, :avatar_upload_limit),
+ banner: Keyword.get(instance, :banner_upload_limit),
+ background: Keyword.get(instance, :background_upload_limit)
+ },
+ features: features
}
}
diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex
index 23e8eb7b1..2cad4550a 100644
--- a/lib/pleroma/web/oauth/authorization.ex
+++ b/lib/pleroma/web/oauth/authorization.ex
@@ -4,7 +4,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
alias Pleroma.{User, Repo}
alias Pleroma.Web.OAuth.{Authorization, App}
- import Ecto.{Changeset}
+ import Ecto.{Changeset, Query}
schema "oauth_authorizations" do
field(:token, :string)
@@ -45,4 +45,12 @@ defmodule Pleroma.Web.OAuth.Authorization do
end
def use_token(%Authorization{used: true}), do: {:error, "already used"}
+
+ def delete_user_authorizations(%User{id: user_id}) do
+ from(
+ a in Pleroma.Web.OAuth.Authorization,
+ where: a.user_id == ^user_id
+ )
+ |> Repo.delete_all()
+ end
end
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index 5441ee0a8..d03c8b05a 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -33,25 +33,35 @@ defmodule Pleroma.Web.OAuth.OAuthController do
true <- Pbkdf2.checkpw(password, user.password_hash),
%App{} = app <- Repo.get_by(App, client_id: client_id),
{:ok, auth} <- Authorization.create_authorization(app, user) do
- if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" do
- render(conn, "results.html", %{
- auth: auth
- })
- else
- connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?"
- url = "#{redirect_uri}#{connector}"
- url_params = %{:code => auth.token}
-
- url_params =
- if params["state"] do
- Map.put(url_params, :state, params["state"])
- else
- url_params
- end
-
- url = "#{url}#{Plug.Conn.Query.encode(url_params)}"
-
- redirect(conn, external: url)
+ # Special case: Local MastodonFE.
+ redirect_uri =
+ if redirect_uri == "." do
+ mastodon_api_url(conn, :login)
+ else
+ redirect_uri
+ end
+
+ cond do
+ redirect_uri == "urn:ietf:wg:oauth:2.0:oob" ->
+ render(conn, "results.html", %{
+ auth: auth
+ })
+
+ true ->
+ connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?"
+ url = "#{redirect_uri}#{connector}"
+ url_params = %{:code => auth.token}
+
+ url_params =
+ if params["state"] do
+ Map.put(url_params, :state, params["state"])
+ else
+ url_params
+ end
+
+ url = "#{url}#{Plug.Conn.Query.encode(url_params)}"
+
+ redirect(conn, external: url)
end
end
end
@@ -133,8 +143,11 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
end
+ # XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be
+ # decoding it. Investigate sometime.
defp fix_padding(token) do
token
+ |> URI.decode()
|> Base.url_decode64!(padding: false)
|> Base.url_encode64()
end
diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex
index 343fc0c45..a77d5af35 100644
--- a/lib/pleroma/web/oauth/token.ex
+++ b/lib/pleroma/web/oauth/token.ex
@@ -1,6 +1,8 @@
defmodule Pleroma.Web.OAuth.Token do
use Ecto.Schema
+ import Ecto.Query
+
alias Pleroma.{User, Repo}
alias Pleroma.Web.OAuth.{Token, App, Authorization}
@@ -35,4 +37,12 @@ defmodule Pleroma.Web.OAuth.Token do
Repo.insert(token)
end
+
+ def delete_user_tokens(%User{id: user_id}) do
+ from(
+ t in Pleroma.Web.OAuth.Token,
+ where: t.user_id == ^user_id
+ )
+ |> Repo.delete_all()
+ end
end
diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex
index 916c894eb..1d0019d3b 100644
--- a/lib/pleroma/web/ostatus/ostatus.ex
+++ b/lib/pleroma/web/ostatus/ostatus.ex
@@ -11,6 +11,21 @@ defmodule Pleroma.Web.OStatus do
alias Pleroma.Web.OStatus.{FollowHandler, UnfollowHandler, NoteHandler, DeleteHandler}
alias Pleroma.Web.ActivityPub.Transmogrifier
+ def is_representable?(%Activity{data: data}) do
+ object = Object.normalize(data["object"])
+
+ cond do
+ is_nil(object) ->
+ false
+
+ object.data["type"] == "Note" ->
+ true
+
+ true ->
+ false
+ end
+ end
+
def feed_path(user) do
"#{user.ap_id}/feed.atom"
end
diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex
index 09d1b1110..af6e22c2b 100644
--- a/lib/pleroma/web/ostatus/ostatus_controller.ex
+++ b/lib/pleroma/web/ostatus/ostatus_controller.ex
@@ -1,7 +1,7 @@
defmodule Pleroma.Web.OStatus.OStatusController do
use Pleroma.Web, :controller
- alias Pleroma.{User, Activity}
+ alias Pleroma.{User, Activity, Object}
alias Pleroma.Web.OStatus.{FeedRepresenter, ActivityRepresenter}
alias Pleroma.Repo
alias Pleroma.Web.{OStatus, Federator}
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Pleroma.Web.ActivityPub.ActivityPubController
alias Pleroma.Web.ActivityPub.ActivityPub
+ plug(Pleroma.Web.FederatingPlug when action in [:salmon_incoming])
action_fallback(:errors)
def feed_redirect(conn, %{"nickname" => nickname}) do
@@ -135,7 +136,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
"html" ->
conn
|> put_resp_content_type("text/html")
- |> send_file(200, "priv/static/index.html")
+ |> send_file(200, Application.app_dir(:pleroma, "priv/static/index.html"))
_ ->
represent_activity(conn, format, activity, user)
@@ -152,10 +153,21 @@ defmodule Pleroma.Web.OStatus.OStatusController do
end
end
- defp represent_activity(conn, "activity+json", activity, user) do
+ defp represent_activity(
+ conn,
+ "activity+json",
+ %Activity{data: %{"type" => "Create"}} = activity,
+ user
+ ) do
+ object = Object.normalize(activity.data["object"])
+
conn
|> put_resp_header("content-type", "application/activity+json")
- |> json(ObjectView.render("object.json", %{object: activity}))
+ |> json(ObjectView.render("object.json", %{object: object}))
+ end
+
+ defp represent_activity(conn, "activity+json", _, _) do
+ {:error, :not_found}
end
defp represent_activity(conn, _, activity, user) do
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 04dc80444..d8af81992 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -3,12 +3,6 @@ defmodule Pleroma.Web.Router do
alias Pleroma.{Repo, User, Web.Router}
- @instance Application.get_env(:pleroma, :instance)
- @federating Keyword.get(@instance, :federating)
- @allow_relay Keyword.get(@instance, :allow_relay)
- @public Keyword.get(@instance, :public)
- @registrations_open Keyword.get(@instance, :registrations_open)
-
pipeline :api do
plug(:accepts, ["json"])
plug(:fetch_session)
@@ -37,6 +31,21 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
end
+ pipeline :admin_api do
+ plug(:accepts, ["json"])
+ plug(:fetch_session)
+ plug(Pleroma.Plugs.OAuthPlug)
+ plug(Pleroma.Plugs.BasicAuthDecoderPlug)
+ plug(Pleroma.Plugs.UserFetcherPlug)
+ plug(Pleroma.Plugs.SessionAuthenticationPlug)
+ plug(Pleroma.Plugs.LegacyAuthenticationPlug)
+ plug(Pleroma.Plugs.AuthenticationPlug)
+ plug(Pleroma.Plugs.UserEnabledPlug)
+ plug(Pleroma.Plugs.SetUserSessionIdPlug)
+ plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
+ plug(Pleroma.Plugs.UserIsAdminPlug)
+ end
+
pipeline :mastodon_html do
plug(:accepts, ["html"])
plug(:fetch_session)
@@ -85,6 +94,23 @@ defmodule Pleroma.Web.Router do
get("/emoji", UtilController, :emoji)
end
+ scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
+ pipe_through(:admin_api)
+ delete("/user", AdminAPIController, :user_delete)
+ post("/user", AdminAPIController, :user_create)
+
+ get("/permission_group/:nickname", AdminAPIController, :right_get)
+ get("/permission_group/:nickname/:permission_group", AdminAPIController, :right_get)
+ post("/permission_group/:nickname/:permission_group", AdminAPIController, :right_add)
+ delete("/permission_group/:nickname/:permission_group", AdminAPIController, :right_delete)
+
+ post("/relay", AdminAPIController, :relay_follow)
+ delete("/relay", AdminAPIController, :relay_unfollow)
+
+ get("/invite_token", AdminAPIController, :get_invite_token)
+ get("/password_reset", AdminAPIController, :get_password_reset)
+ end
+
scope "/", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_html)
get("/ostatus_subscribe", UtilController, :remote_follow)
@@ -119,6 +145,7 @@ defmodule Pleroma.Web.Router do
post("/accounts/:id/unblock", MastodonAPIController, :unblock)
post("/accounts/:id/mute", MastodonAPIController, :relationship_noop)
post("/accounts/:id/unmute", MastodonAPIController, :relationship_noop)
+ get("/accounts/:id/lists", MastodonAPIController, :account_lists)
get("/follow_requests", MastodonAPIController, :follow_requests)
post("/follow_requests/:id/authorize", MastodonAPIController, :authorize_follow_request)
@@ -247,11 +274,7 @@ defmodule Pleroma.Web.Router do
end
scope "/api", Pleroma.Web do
- if @public do
- pipe_through(:api)
- else
- pipe_through(:authenticated_api)
- end
+ pipe_through(:api)
get("/statuses/public_timeline", TwitterAPI.Controller, :public_timeline)
@@ -264,7 +287,12 @@ defmodule Pleroma.Web.Router do
get("/statuses/networkpublic_timeline", TwitterAPI.Controller, :public_and_external_timeline)
end
- scope "/api", Pleroma.Web do
+ scope "/api", Pleroma.Web, as: :twitter_api_search do
+ pipe_through(:api)
+ get("/pleroma/search_user", TwitterAPI.Controller, :search_user)
+ end
+
+ scope "/api", Pleroma.Web, as: :authenticated_twitter_api do
pipe_through(:authenticated_api)
get("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
@@ -284,8 +312,13 @@ defmodule Pleroma.Web.Router do
get("/statuses/friends_timeline", TwitterAPI.Controller, :friends_timeline)
get("/statuses/mentions", TwitterAPI.Controller, :mentions_timeline)
get("/statuses/mentions_timeline", TwitterAPI.Controller, :mentions_timeline)
+ get("/statuses/dm_timeline", TwitterAPI.Controller, :dm_timeline)
get("/qvitter/statuses/notifications", TwitterAPI.Controller, :notifications)
+ # XXX: this is really a pleroma API, but we want to keep the pleroma namespace clean
+ # for now.
+ post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
+
post("/statuses/update", TwitterAPI.Controller, :status_update)
post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet)
post("/statuses/unretweet/:id", TwitterAPI.Controller, :unretweet)
@@ -335,12 +368,10 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/feed", OStatus.OStatusController, :feed)
get("/users/:nickname", OStatus.OStatusController, :feed_redirect)
- if @federating do
- post("/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming)
- post("/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request)
- get("/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation)
- post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
- end
+ post("/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming)
+ post("/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request)
+ get("/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation)
+ post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
end
pipeline :activitypub do
@@ -357,31 +388,27 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/outbox", ActivityPubController, :outbox)
end
- if @federating do
- if @allow_relay do
- scope "/relay", Pleroma.Web.ActivityPub do
- pipe_through(:ap_relay)
- get("/", ActivityPubController, :relay)
- end
- end
+ scope "/relay", Pleroma.Web.ActivityPub do
+ pipe_through(:ap_relay)
+ get("/", ActivityPubController, :relay)
+ end
- scope "/", Pleroma.Web.ActivityPub do
- pipe_through(:activitypub)
- post("/users/:nickname/inbox", ActivityPubController, :inbox)
- post("/inbox", ActivityPubController, :inbox)
- end
+ scope "/", Pleroma.Web.ActivityPub do
+ pipe_through(:activitypub)
+ post("/users/:nickname/inbox", ActivityPubController, :inbox)
+ post("/inbox", ActivityPubController, :inbox)
+ end
- scope "/.well-known", Pleroma.Web do
- pipe_through(:well_known)
+ scope "/.well-known", Pleroma.Web do
+ pipe_through(:well_known)
- get("/host-meta", WebFinger.WebFingerController, :host_meta)
- get("/webfinger", WebFinger.WebFingerController, :webfinger)
- get("/nodeinfo", Nodeinfo.NodeinfoController, :schemas)
- end
+ get("/host-meta", WebFinger.WebFingerController, :host_meta)
+ get("/webfinger", WebFinger.WebFingerController, :webfinger)
+ get("/nodeinfo", Nodeinfo.NodeinfoController, :schemas)
+ end
- scope "/nodeinfo", Pleroma.Web do
- get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
- end
+ scope "/nodeinfo", Pleroma.Web do
+ get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
end
scope "/", Pleroma.Web.MastodonAPI do
@@ -394,12 +421,12 @@ defmodule Pleroma.Web.Router do
end
pipeline :remote_media do
- plug(:accepts, ["html"])
end
scope "/proxy/", Pleroma.Web.MediaProxy do
pipe_through(:remote_media)
get("/:sig/:url", MediaProxyController, :remote)
+ get("/:sig/:url/:filename", MediaProxyController, :remote)
end
scope "/", Fallback do
@@ -414,11 +441,9 @@ defmodule Fallback.RedirectController do
use Pleroma.Web, :controller
def redirector(conn, _params) do
- if Mix.env() != :test do
- conn
- |> put_resp_content_type("text/html")
- |> send_file(200, "priv/static/index.html")
- end
+ conn
+ |> put_resp_content_type("text/html")
+ |> send_file(200, Application.app_dir(:pleroma, "priv/static/index.html"))
end
def registration_page(conn, params) do
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index 6b6d40346..306598157 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -73,7 +73,8 @@ defmodule Pleroma.Web.Streamer do
Pleroma.List.get_lists_from_activity(item)
|> Enum.filter(fn list ->
owner = Repo.get(User, list.user_id)
- author.follower_address in owner.following
+
+ ActivityPub.visible_for_user?(item, owner)
end)
end
@@ -169,16 +170,33 @@ defmodule Pleroma.Web.Streamer do
|> Jason.encode!()
end
+ defp represent_update(%Activity{} = activity) do
+ %{
+ event: "update",
+ payload:
+ Pleroma.Web.MastodonAPI.StatusView.render(
+ "status.json",
+ activity: activity
+ )
+ |> Jason.encode!()
+ }
+ |> Jason.encode!()
+ end
+
def push_to_socket(topics, topic, %Activity{data: %{"type" => "Announce"}} = item) do
Enum.each(topics[topic] || [], fn socket ->
# Get the current user so we have up-to-date blocks etc.
- user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
- blocks = user.info["blocks"] || []
+ if socket.assigns[:user] do
+ user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
+ blocks = user.info["blocks"] || []
- parent = Object.normalize(item.data["object"])
+ parent = Object.normalize(item.data["object"])
- unless is_nil(parent) or item.actor in blocks or parent.data["actor"] in blocks do
- send(socket.transport_pid, {:text, represent_update(item, user)})
+ unless is_nil(parent) or item.actor in blocks or parent.data["actor"] in blocks do
+ send(socket.transport_pid, {:text, represent_update(item, user)})
+ end
+ else
+ send(socket.transport_pid, {:text, represent_update(item)})
end
end)
end
@@ -186,11 +204,15 @@ defmodule Pleroma.Web.Streamer do
def push_to_socket(topics, topic, item) do
Enum.each(topics[topic] || [], fn socket ->
# Get the current user so we have up-to-date blocks etc.
- user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
- blocks = user.info["blocks"] || []
-
- unless item.actor in blocks do
- send(socket.transport_pid, {:text, represent_update(item, user)})
+ if socket.assigns[:user] do
+ user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
+ blocks = user.info["blocks"] || []
+
+ unless item.actor in blocks do
+ send(socket.transport_pid, {:text, represent_update(item, user)})
+ end
+ else
+ send(socket.transport_pid, {:text, represent_update(item)})
end
end)
end
diff --git a/lib/pleroma/web/templates/layout/app.html.eex b/lib/pleroma/web/templates/layout/app.html.eex
index 2a8dede80..2e96c1509 100644
--- a/lib/pleroma/web/templates/layout/app.html.eex
+++ b/lib/pleroma/web/templates/layout/app.html.eex
@@ -2,7 +2,9 @@
<html>
<head>
<meta charset=utf-8 />
- <title>Pleroma</title>
+ <title>
+ <%= Application.get_env(:pleroma, :instance)[:name] %>
+ </title>
<style>
body {
background-color: #282c37;
diff --git a/lib/pleroma/web/templates/mastodon_api/mastodon/login.html.eex b/lib/pleroma/web/templates/mastodon_api/mastodon/login.html.eex
deleted file mode 100644
index 34cd7ed89..000000000
--- a/lib/pleroma/web/templates/mastodon_api/mastodon/login.html.eex
+++ /dev/null
@@ -1,11 +0,0 @@
-<h2>Login to Mastodon Frontend</h2>
-<%= if @error do %>
- <h2><%= @error %></h2>
-<% end %>
-<%= form_for @conn, mastodon_api_path(@conn, :login), [as: "authorization"], fn f -> %>
-<%= text_input f, :name, placeholder: "Username or email" %>
-<br>
-<%= password_input f, :password, placeholder: "Password" %>
-<br>
-<%= submit "Log in" %>
-<% end %>
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index f06020a3e..092779010 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
alias Pleroma.Web.WebFinger
alias Pleroma.Web.CommonAPI
alias Comeonin.Pbkdf2
- alias Pleroma.Formatter
+ alias Pleroma.{Formatter, Emoji}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.{Repo, PasswordResetToken, User}
@@ -134,19 +134,20 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
- @instance Application.get_env(:pleroma, :instance)
- @instance_fe Application.get_env(:pleroma, :fe)
- @instance_chat Application.get_env(:pleroma, :chat)
def config(conn, _params) do
+ instance = Pleroma.Config.get(:instance)
+ instance_fe = Pleroma.Config.get(:fe)
+ instance_chat = Pleroma.Config.get(:chat)
+
case get_format(conn) do
"xml" ->
response = """
<config>
<site>
- <name>#{Keyword.get(@instance, :name)}</name>
+ <name>#{Keyword.get(instance, :name)}</name>
<site>#{Web.base_url()}</site>
- <textlimit>#{Keyword.get(@instance, :limit)}</textlimit>
- <closed>#{!Keyword.get(@instance, :registrations_open)}</closed>
+ <textlimit>#{Keyword.get(instance, :limit)}</textlimit>
+ <closed>#{!Keyword.get(instance, :registrations_open)}</closed>
</site>
</config>
"""
@@ -160,30 +161,33 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
Keyword.get(Application.get_env(:web_push_encryption, :vapid_details), :public_key)
data = %{
- name: Keyword.get(@instance, :name),
- description: Keyword.get(@instance, :description),
+ 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"),
+ 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"),
vapidPublicKey: vapid_public_key
}
pleroma_fe = %{
- theme: Keyword.get(@instance_fe, :theme),
- background: Keyword.get(@instance_fe, :background),
- logo: Keyword.get(@instance_fe, :logo),
- logoMask: Keyword.get(@instance_fe, :logo_mask),
- logoMargin: Keyword.get(@instance_fe, :logo_margin),
- redirectRootNoLogin: Keyword.get(@instance_fe, :redirect_root_no_login),
- redirectRootLogin: Keyword.get(@instance_fe, :redirect_root_login),
- chatDisabled: !Keyword.get(@instance_chat, :enabled),
- showInstanceSpecificPanel: Keyword.get(@instance_fe, :show_instance_panel),
- scopeOptionsEnabled: Keyword.get(@instance_fe, :scope_options_enabled),
- collapseMessageWithSubject: Keyword.get(@instance_fe, :collapse_message_with_subject)
+ theme: Keyword.get(instance_fe, :theme),
+ background: Keyword.get(instance_fe, :background),
+ logo: Keyword.get(instance_fe, :logo),
+ logoMask: Keyword.get(instance_fe, :logo_mask),
+ logoMargin: Keyword.get(instance_fe, :logo_margin),
+ redirectRootNoLogin: Keyword.get(instance_fe, :redirect_root_no_login),
+ redirectRootLogin: Keyword.get(instance_fe, :redirect_root_login),
+ chatDisabled: !Keyword.get(instance_chat, :enabled),
+ showInstanceSpecificPanel: Keyword.get(instance_fe, :show_instance_panel),
+ scopeOptionsEnabled: Keyword.get(instance_fe, :scope_options_enabled),
+ formattingOptionsEnabled: Keyword.get(instance_fe, :formatting_options_enabled),
+ collapseMessageWithSubject: Keyword.get(instance_fe, :collapse_message_with_subject),
+ hidePostStats: Keyword.get(instance_fe, :hide_post_stats),
+ hideUserStats: Keyword.get(instance_fe, :hide_user_stats)
}
- managed_config = Keyword.get(@instance, :managed_config)
+ managed_config = Keyword.get(instance, :managed_config)
data =
if managed_config do
@@ -197,7 +201,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
def version(conn, _params) do
- version = Keyword.get(@instance, :version)
+ version = Pleroma.Application.named_version()
case get_format(conn) do
"xml" ->
@@ -213,7 +217,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
def emoji(conn, _params) do
- json(conn, Enum.into(Formatter.get_custom_emoji(), %{}))
+ json(conn, Enum.into(Emoji.get_all(), %{}))
end
def follow_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
@@ -226,7 +230,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
|> Enum.map(fn account ->
with %User{} = follower <- User.get_cached_by_ap_id(user.ap_id),
%User{} = followed <- User.get_or_fetch(account),
- {:ok, follower} <- User.follow(follower, followed) do
+ {:ok, follower} <- User.maybe_direct_follow(follower, followed) do
ActivityPub.follow(follower, followed)
else
err -> Logger.debug("follow_import: following #{account} failed with #{inspect(err)}")
diff --git a/lib/pleroma/web/twitter_api/representers/activity_representer.ex b/lib/pleroma/web/twitter_api/representers/activity_representer.ex
index b21bbb205..fbd33f07e 100644
--- a/lib/pleroma/web/twitter_api/representers/activity_representer.ex
+++ b/lib/pleroma/web/twitter_api/representers/activity_representer.ex
@@ -180,6 +180,10 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
attachments = (object["attachment"] || []) ++ video
+ reply_parent = Activity.get_in_reply_to_activity(activity)
+
+ reply_user = reply_parent && User.get_cached_by_ap_id(reply_parent.actor)
+
%{
"id" => activity.id,
"uri" => activity.data["object"]["id"],
@@ -190,6 +194,10 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
"is_post_verb" => true,
"created_at" => created_at,
"in_reply_to_status_id" => object["inReplyToStatusId"],
+ "in_reply_to_screen_name" => reply_user && reply_user.nickname,
+ "in_reply_to_profileurl" => User.profile_url(reply_user),
+ "in_reply_to_ostatus_uri" => reply_user && reply_user.ap_id,
+ "in_reply_to_user_id" => reply_user && reply_user.id,
"statusnet_conversation_id" => conversation_id,
"attachments" => attachments |> ObjectRepresenter.enum_to_list(opts),
"attentions" => attentions,
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index dbad08e66..5bfb83b1e 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -3,11 +3,10 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.TwitterAPI.UserView
alias Pleroma.Web.{OStatus, CommonAPI}
+ alias Pleroma.Web.MediaProxy
import Ecto.Query
- @instance Application.get_env(:pleroma, :instance)
@httpoison Application.get_env(:pleroma, :httpoison)
- @registrations_open Keyword.get(@instance, :registrations_open)
def create_status(%User{} = user, %{"status" => _} = data) do
CommonAPI.post(user, data)
@@ -23,7 +22,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
def follow(%User{} = follower, params) do
with {:ok, %User{} = followed} <- get_user(params),
{:ok, follower} <- User.maybe_direct_follow(follower, followed),
- {:ok, activity} <- ActivityPub.follow(follower, followed) do
+ {:ok, activity} <- ActivityPub.follow(follower, followed),
+ {:ok, follower, followed} <-
+ User.wait_and_refresh(
+ Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
+ follower,
+ followed
+ ) do
{:ok, follower, followed, activity}
else
err -> err
@@ -92,7 +97,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
{:ok, object} = ActivityPub.upload(file)
url = List.first(object.data["url"])
- href = url["href"]
+ href = url["href"] |> MediaProxy.url()
type = url["mediaType"]
case format do
@@ -133,18 +138,20 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
password_confirmation: params["confirm"]
}
+ registrations_open = Pleroma.Config.get([:instance, :registrations_open])
+
# no need to query DB if registration is open
token =
- unless @registrations_open || is_nil(tokenString) do
+ unless registrations_open || is_nil(tokenString) do
Repo.get_by(UserInviteToken, %{token: tokenString})
end
cond do
- @registrations_open || (!is_nil(token) && !token.used) ->
+ registrations_open || (!is_nil(token) && !token.used) ->
changeset = User.register_changeset(%User{}, params)
with {:ok, user} <- Repo.insert(changeset) do
- !@registrations_open && UserInviteToken.mark_as_used(token.token)
+ !registrations_open && UserInviteToken.mark_as_used(token.token)
{:ok, user}
else
{:error, changeset} ->
@@ -155,10 +162,10 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
{:error, %{error: errors}}
end
- !@registrations_open && is_nil(token) ->
+ !registrations_open && is_nil(token) ->
{:error, "Invalid token"}
- !@registrations_open && token.used ->
+ !registrations_open && token.used ->
{:error, "Expired token"}
end
end
diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
index cd2bb5b57..cd0e2121c 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
require Logger
+ plug(:only_if_public_instance when action in [:public_timeline, :public_and_external_timeline])
action_fallback(:errors)
def verify_credentials(%{assigns: %{user: user}} = conn, _params) do
@@ -79,7 +80,9 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
|> Map.put("blocking_user", user)
|> Map.put("user", user)
- activities = ActivityPub.fetch_activities([user.ap_id | user.following], params)
+ activities =
+ ActivityPub.fetch_activities([user.ap_id | user.following], params)
+ |> ActivityPub.contain_timeline(user)
conn
|> render(ActivityView, "index.json", %{activities: activities, for: user})
@@ -123,6 +126,19 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
|> render(ActivityView, "index.json", %{activities: activities, for: user})
end
+ def dm_timeline(%{assigns: %{user: user}} = conn, params) do
+ query =
+ ActivityPub.fetch_activities_query(
+ [user.ap_id],
+ Map.merge(params, %{"type" => "Create", "user" => user, visibility: "direct"})
+ )
+
+ activities = Repo.all(query)
+
+ conn
+ |> render(ActivityView, "index.json", %{activities: activities, for: user})
+ end
+
def notifications(%{assigns: %{user: user}} = conn, params) do
notifications = Notification.for_user(user, params)
@@ -130,6 +146,19 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
|> render(NotificationView, "notification.json", %{notifications: notifications, for: user})
end
+ def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
+ Notification.set_read_up_to(user, latest_id)
+
+ notifications = Notification.for_user(user, params)
+
+ conn
+ |> render(NotificationView, "notification.json", %{notifications: notifications, for: user})
+ end
+
+ def notifications_read(%{assigns: %{user: user}} = conn, _) do
+ bad_request_reply(conn, "You need to specify latest_id")
+ end
+
def follow(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.follow(user, params) do
{:ok, user, followed, _activity} ->
@@ -261,7 +290,11 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def update_avatar(%{assigns: %{user: user}} = conn, params) do
- {:ok, object} = ActivityPub.upload(params)
+ upload_limit =
+ Application.get_env(:pleroma, :instance)
+ |> Keyword.fetch(:avatar_upload_limit)
+
+ {:ok, object} = ActivityPub.upload(params, upload_limit)
change = Changeset.change(user, %{avatar: object.data})
{:ok, user} = User.update_and_set_cache(change)
CommonAPI.update(user)
@@ -270,7 +303,11 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def update_banner(%{assigns: %{user: user}} = conn, params) do
- with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}),
+ upload_limit =
+ Application.get_env(:pleroma, :instance)
+ |> Keyword.fetch(:banner_upload_limit)
+
+ with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, upload_limit),
new_info <- Map.put(user.info, "banner", object.data),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) do
@@ -284,7 +321,11 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def update_background(%{assigns: %{user: user}} = conn, params) do
- with {:ok, object} <- ActivityPub.upload(params),
+ upload_limit =
+ Application.get_env(:pleroma, :instance)
+ |> Keyword.fetch(:background_upload_limit)
+
+ with {:ok, object} <- ActivityPub.upload(params, upload_limit),
new_info <- Map.put(user.info, "background", object.data),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, _user} <- User.update_and_set_cache(change) do
@@ -423,7 +464,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
{String.trim(name, ":"), url}
end)
- bio_html = CommonUtils.format_input(bio, mentions, tags)
+ bio_html = CommonUtils.format_input(bio, mentions, tags, "text/plain")
Map.put(params, "bio", bio_html |> Formatter.emojify(emoji))
else
params
@@ -488,6 +529,13 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
|> render(ActivityView, "index.json", %{activities: activities, for: user})
end
+ def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
+ users = User.search(query, true)
+
+ conn
+ |> render(UserView, "index.json", %{users: users, for: user})
+ end
+
defp bad_request_reply(conn, error_message) do
json = error_json(conn, error_message)
json_reply(conn, 400, json)
@@ -504,6 +552,18 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
json_reply(conn, 403, json)
end
+ def only_if_public_instance(conn = %{conn: %{assigns: %{user: _user}}}, _), do: conn
+
+ def only_if_public_instance(conn, _) do
+ if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
+ conn
+ else
+ conn
+ |> forbidden_json_reply("Invalid credentials.")
+ |> halt()
+ end
+ end
+
defp error_json(conn, error_message) do
%{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
end
diff --git a/lib/pleroma/web/twitter_api/views/activity_view.ex b/lib/pleroma/web/twitter_api/views/activity_view.ex
index b9fd062d6..83e8fb765 100644
--- a/lib/pleroma/web/twitter_api/views/activity_view.ex
+++ b/lib/pleroma/web/twitter_api/views/activity_view.ex
@@ -236,6 +236,10 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
HTML.filter_tags(content, User.html_filter_policy(opts[:for]))
|> Formatter.emojify(object["emoji"])
+ reply_parent = Activity.get_in_reply_to_activity(activity)
+
+ reply_user = reply_parent && User.get_cached_by_ap_id(reply_parent.actor)
+
%{
"id" => activity.id,
"uri" => activity.data["object"]["id"],
@@ -246,6 +250,10 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
"is_post_verb" => true,
"created_at" => created_at,
"in_reply_to_status_id" => object["inReplyToStatusId"],
+ "in_reply_to_screen_name" => reply_user && reply_user.nickname,
+ "in_reply_to_profileurl" => User.profile_url(reply_user),
+ "in_reply_to_ostatus_uri" => reply_user && reply_user.ap_id,
+ "in_reply_to_user_id" => reply_user && reply_user.id,
"statusnet_conversation_id" => conversation_id,
"attachments" => (object["attachment"] || []) |> ObjectRepresenter.enum_to_list(opts),
"attentions" => attentions,
@@ -275,11 +283,11 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
{summary, content}
end
- def render_content(%{"type" => "Article"} = object) do
+ def render_content(%{"type" => object_type} = object) when object_type in ["Article", "Page"] do
summary = object["name"] || object["summary"]
content =
- if !!summary and summary != "" do
+ if !!summary and summary != "" and is_bitstring(object["url"]) do
"<p><a href=\"#{object["url"]}\">#{summary}</a></p>#{object["content"]}"
else
object["content"]
diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex
index cfbaef4c9..a100a1127 100644
--- a/lib/pleroma/web/twitter_api/views/user_view.ex
+++ b/lib/pleroma/web/twitter_api/views/user_view.ex
@@ -37,6 +37,13 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
{String.trim(name, ":"), url}
end)
+ # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
+ # For example: [{"name": "Pronoun", "value": "she/her"}, …]
+ fields =
+ (user.info["source_data"]["attachment"] || [])
+ |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
+ |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
+
data = %{
"created_at" => user.inserted_at |> Utils.format_naive_asctime(),
"description" => HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")),
@@ -48,8 +55,12 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
"statusnet_blocking" => statusnet_blocking,
"friends_count" => user_info[:following_count],
"id" => user.id,
- "name" => user.name,
- "name_html" => HTML.strip_tags(user.name) |> Formatter.emojify(emoji),
+ "name" => user.name || user.nickname,
+ "name_html" =>
+ if(user.name,
+ do: HTML.strip_tags(user.name) |> Formatter.emojify(emoji),
+ else: user.nickname
+ ),
"profile_image_url" => image,
"profile_image_url_https" => image,
"profile_image_url_profile_size" => image,
@@ -65,7 +76,8 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
"is_local" => user.local,
"locked" => !!user.info["locked"],
"default_scope" => user.info["default_scope"] || "public",
- "no_rich_text" => user.info["no_rich_text"] || false
+ "no_rich_text" => user.info["no_rich_text"] || false,
+ "fields" => fields
}
if assigns[:token] do
diff --git a/lib/pleroma/web/web_finger/web_finger_controller.ex b/lib/pleroma/web/web_finger/web_finger_controller.ex
index 50d816256..002353166 100644
--- a/lib/pleroma/web/web_finger/web_finger_controller.ex
+++ b/lib/pleroma/web/web_finger/web_finger_controller.ex
@@ -3,6 +3,8 @@ defmodule Pleroma.Web.WebFinger.WebFingerController do
alias Pleroma.Web.WebFinger
+ plug(Pleroma.Web.FederatingPlug)
+
def host_meta(conn, _params) do
xml = WebFinger.host_meta()
diff --git a/lib/pleroma/web/websub/websub.ex b/lib/pleroma/web/websub/websub.ex
index e494811f9..396dcf045 100644
--- a/lib/pleroma/web/websub/websub.ex
+++ b/lib/pleroma/web/websub/websub.ex
@@ -252,4 +252,29 @@ defmodule Pleroma.Web.Websub do
Pleroma.Web.Federator.enqueue(:request_subscription, sub)
end)
end
+
+ def publish_one(%{xml: xml, topic: topic, callback: callback, secret: secret}) do
+ signature = sign(secret || "", xml)
+ Logger.info(fn -> "Pushing #{topic} to #{callback}" end)
+
+ with {:ok, %{status_code: 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}
+ else
+ e ->
+ Logger.debug(fn -> "Couldn't push to #{callback}, #{inspect(e)}" end)
+ {:error, e}
+ end
+ end
end
diff --git a/lib/pleroma/web/websub/websub_controller.ex b/lib/pleroma/web/websub/websub_controller.ex
index 590dd74a1..c1934ba92 100644
--- a/lib/pleroma/web/websub/websub_controller.ex
+++ b/lib/pleroma/web/websub/websub_controller.ex
@@ -5,6 +5,15 @@ defmodule Pleroma.Web.Websub.WebsubController do
alias Pleroma.Web.Websub.WebsubClientSubscription
require Logger
+ plug(
+ Pleroma.Web.FederatingPlug
+ when action in [
+ :websub_subscription_request,
+ :websub_subscription_confirmation,
+ :websub_incoming
+ ]
+ )
+
def websub_subscription_request(conn, %{"nickname" => nickname} = params) do
user = User.get_cached_by_nickname(nickname)