summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/mix/tasks/pleroma/instance.ex2
-rw-r--r--lib/mix/tasks/pleroma/sample_config.eex3
-rw-r--r--lib/mix/tasks/pleroma/uploads.ex6
-rw-r--r--lib/mix/tasks/pleroma/user.ex49
-rw-r--r--lib/pleroma/PasswordResetToken.ex6
-rw-r--r--lib/pleroma/activity.ex52
-rw-r--r--lib/pleroma/application.ex50
-rw-r--r--lib/pleroma/captcha/captcha.ex2
-rw-r--r--lib/pleroma/clippy.ex155
-rw-r--r--lib/pleroma/config/deprecation_warnings.ex30
-rw-r--r--lib/pleroma/emails/user_email.ex3
-rw-r--r--lib/pleroma/filter.ex10
-rw-r--r--lib/pleroma/flake_id.ex172
-rw-r--r--lib/pleroma/formatter.ex48
-rw-r--r--lib/pleroma/gopher/server.ex6
-rw-r--r--lib/pleroma/html.ex67
-rw-r--r--lib/pleroma/http/connection.ex3
-rw-r--r--lib/pleroma/http/http.ex3
-rw-r--r--lib/pleroma/http/request_builder.ex7
-rw-r--r--lib/pleroma/instances.ex36
-rw-r--r--lib/pleroma/instances/instance.ex113
-rw-r--r--lib/pleroma/list.ex11
-rw-r--r--lib/pleroma/mime.ex10
-rw-r--r--lib/pleroma/notification.ex74
-rw-r--r--lib/pleroma/object.ex60
-rw-r--r--lib/pleroma/plugs/http_security_plug.ex23
-rw-r--r--lib/pleroma/plugs/instance_static.ex4
-rw-r--r--lib/pleroma/plugs/oauth_plug.ex15
-rw-r--r--lib/pleroma/plugs/uploaded_media.ex2
-rw-r--r--lib/pleroma/plugs/user_fetcher_plug.ex5
-rw-r--r--lib/pleroma/reverse_proxy.ex23
-rw-r--r--lib/pleroma/stats.ex10
-rw-r--r--lib/pleroma/thread_mute.ex45
-rw-r--r--lib/pleroma/upload.ex17
-rw-r--r--lib/pleroma/upload/filter/dedupe.ex2
-rw-r--r--lib/pleroma/uploaders/mdii.ex3
-rw-r--r--lib/pleroma/uploaders/s3.ex12
-rw-r--r--lib/pleroma/uploaders/uploader.ex37
-rw-r--r--lib/pleroma/user.ex407
-rw-r--r--lib/pleroma/user/info.ex38
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex226
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub_controller.ex132
-rw-r--r--lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex57
-rw-r--r--lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex44
-rw-r--r--lib/pleroma/web/activity_pub/mrf/keyword_policy.ex81
-rw-r--r--lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex29
-rw-r--r--lib/pleroma/web/activity_pub/mrf/tag_policy.ex139
-rw-r--r--lib/pleroma/web/activity_pub/relay.ex6
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex90
-rw-r--r--lib/pleroma/web/activity_pub/utils.ex78
-rw-r--r--lib/pleroma/web/activity_pub/views/object_view.ex37
-rw-r--r--lib/pleroma/web/activity_pub/views/user_view.ex87
-rw-r--r--lib/pleroma/web/common_api/common_api.ex87
-rw-r--r--lib/pleroma/web/common_api/utils.ex71
-rw-r--r--lib/pleroma/web/endpoint.ex6
-rw-r--r--lib/pleroma/web/federator/federator.ex19
-rw-r--r--lib/pleroma/web/http_signatures/http_signatures.ex3
-rw-r--r--lib/pleroma/web/mastodon_api/mastodon_api_controller.ex333
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex9
-rw-r--r--lib/pleroma/web/mastodon_api/views/filter_view.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/views/status_view.ex93
-rw-r--r--lib/pleroma/web/mastodon_api/websocket_handler.ex3
-rw-r--r--lib/pleroma/web/media_proxy/controller.ex5
-rw-r--r--lib/pleroma/web/media_proxy/media_proxy.ex15
-rw-r--r--lib/pleroma/web/metadata.ex40
-rw-r--r--lib/pleroma/web/metadata/opengraph.ex156
-rw-r--r--lib/pleroma/web/metadata/provider.ex7
-rw-r--r--lib/pleroma/web/metadata/twitter_card.ex46
-rw-r--r--lib/pleroma/web/nodeinfo/nodeinfo_controller.ex75
-rw-r--r--lib/pleroma/web/oauth/app.ex2
-rw-r--r--lib/pleroma/web/oauth/authorization.ex11
-rw-r--r--lib/pleroma/web/oauth/fallback_controller.ex3
-rw-r--r--lib/pleroma/web/oauth/oauth_controller.ex8
-rw-r--r--lib/pleroma/web/oauth/token.ex9
-rw-r--r--lib/pleroma/web/ostatus/activity_representer.ex7
-rw-r--r--lib/pleroma/web/ostatus/feed_representer.ex5
-rw-r--r--lib/pleroma/web/ostatus/handlers/follow_handler.ex3
-rw-r--r--lib/pleroma/web/ostatus/handlers/note_handler.ex10
-rw-r--r--lib/pleroma/web/ostatus/handlers/unfollow_handler.ex3
-rw-r--r--lib/pleroma/web/ostatus/ostatus.ex19
-rw-r--r--lib/pleroma/web/ostatus/ostatus_controller.ex87
-rw-r--r--lib/pleroma/web/push/push.ex3
-rw-r--r--lib/pleroma/web/push/subscription.ex7
-rw-r--r--lib/pleroma/web/rich_media/helpers.ex21
-rw-r--r--lib/pleroma/web/rich_media/parser.ex75
-rw-r--r--lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex30
-rw-r--r--lib/pleroma/web/rich_media/parsers/oembed_parser.ex31
-rw-r--r--lib/pleroma/web/rich_media/parsers/ogp.ex11
-rw-r--r--lib/pleroma/web/rich_media/parsers/twitter_card.ex11
-rw-r--r--lib/pleroma/web/router.ex64
-rw-r--r--lib/pleroma/web/salmon/salmon.ex48
-rw-r--r--lib/pleroma/web/streamer.ex15
-rw-r--r--lib/pleroma/web/templates/layout/app.html.eex29
-rw-r--r--lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex27
-rw-r--r--lib/pleroma/web/templates/o_auth/o_auth/show.html.eex4
-rw-r--r--lib/pleroma/web/twitter_api/controllers/util_controller.ex63
-rw-r--r--lib/pleroma/web/twitter_api/representers/activity_representer.ex29
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api.ex25
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api_controller.ex83
-rw-r--r--lib/pleroma/web/twitter_api/views/activity_view.ex58
-rw-r--r--lib/pleroma/web/twitter_api/views/notification_view.ex5
-rw-r--r--lib/pleroma/web/twitter_api/views/user_view.ex26
-rw-r--r--lib/pleroma/web/uploader_controller.ex25
-rw-r--r--lib/pleroma/web/web.ex36
-rw-r--r--lib/pleroma/web/web_finger/web_finger.ex7
-rw-r--r--lib/pleroma/web/websub/websub.ex52
-rw-r--r--lib/pleroma/web/websub/websub_client_subscription.ex2
-rw-r--r--lib/pleroma/web/websub/websub_controller.ex15
108 files changed, 3782 insertions, 784 deletions
diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex
index 0a2c891c0..1ba452275 100644
--- a/lib/mix/tasks/pleroma/instance.ex
+++ b/lib/mix/tasks/pleroma/instance.ex
@@ -105,6 +105,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
)
secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
+ signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
{web_push_public_key, web_push_private_key} = :crypto.generate_key(:ecdh, :prime256v1)
result_config =
@@ -120,6 +121,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
dbpass: dbpass,
version: Pleroma.Mixfile.project() |> Keyword.get(:version),
secret: secret,
+ signing_salt: signing_salt,
web_push_public_key: Base.url_encode64(web_push_public_key, padding: false),
web_push_private_key: Base.url_encode64(web_push_private_key, padding: false)
)
diff --git a/lib/mix/tasks/pleroma/sample_config.eex b/lib/mix/tasks/pleroma/sample_config.eex
index 740b9f8d1..1c935c0d8 100644
--- a/lib/mix/tasks/pleroma/sample_config.eex
+++ b/lib/mix/tasks/pleroma/sample_config.eex
@@ -7,7 +7,8 @@ use Mix.Config
config :pleroma, Pleroma.Web.Endpoint,
url: [host: "<%= domain %>", scheme: "https", port: <%= port %>],
- secret_key_base: "<%= secret %>"
+ secret_key_base: "<%= secret %>",
+ signing_salt: "<%= signing_salt %>"
config :pleroma, :instance,
name: "<%= name %>",
diff --git a/lib/mix/tasks/pleroma/uploads.ex b/lib/mix/tasks/pleroma/uploads.ex
index f0eb13e1a..a01e61627 100644
--- a/lib/mix/tasks/pleroma/uploads.ex
+++ b/lib/mix/tasks/pleroma/uploads.ex
@@ -4,7 +4,8 @@
defmodule Mix.Tasks.Pleroma.Uploads do
use Mix.Task
- alias Pleroma.{Upload, Uploaders.Local}
+ alias Pleroma.Upload
+ alias Pleroma.Uploaders.Local
alias Mix.Tasks.Pleroma.Common
require Logger
@@ -20,7 +21,7 @@ defmodule Mix.Tasks.Pleroma.Uploads do
- `--delete` - delete local uploads after migrating them to the target uploader
- A list of avalible uploaders can be seen in config.exs
+ A list of available uploaders can be seen in config.exs
"""
def run(["migrate_local", target_uploader | args]) do
delete? = Enum.member?(args, "--delete")
@@ -96,6 +97,7 @@ defmodule Mix.Tasks.Pleroma.Uploads do
timeout: 150_000
)
|> Stream.chunk_every(@log_every)
+ # credo:disable-for-next-line Credo.Check.Warning.UnusedEnumOperation
|> Enum.reduce(0, fn done, count ->
count = count + length(done)
Mix.shell().info("Uploaded #{count}/#{total_count} files")
diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex
index 217a52fdd..037e44716 100644
--- a/lib/mix/tasks/pleroma/user.ex
+++ b/lib/mix/tasks/pleroma/user.ex
@@ -5,7 +5,8 @@
defmodule Mix.Tasks.Pleroma.User do
use Mix.Task
import Ecto.Changeset
- alias Pleroma.{Repo, User}
+ alias Pleroma.Repo
+ alias Pleroma.User
alias Mix.Tasks.Pleroma.Common
@shortdoc "Manages Pleroma users"
@@ -22,6 +23,7 @@ defmodule Mix.Tasks.Pleroma.User do
- `--password PASSWORD` - the user's password
- `--moderator`/`--no-moderator` - whether the user is a moderator
- `--admin`/`--no-admin` - whether the user is an admin
+ - `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions
## Generate an invite link.
@@ -51,6 +53,14 @@ defmodule Mix.Tasks.Pleroma.User do
- `--locked`/`--no-locked` - whether the user's account is locked
- `--moderator`/`--no-moderator` - whether the user is a moderator
- `--admin`/`--no-admin` - whether the user is an admin
+
+ ## Add tags to a user.
+
+ mix pleroma.user tag NICKNAME TAGS
+
+ ## Delete tags from a user.
+
+ mix pleroma.user untag NICKNAME TAGS
"""
def run(["new", nickname, email | rest]) do
{options, [], []} =
@@ -61,7 +71,11 @@ defmodule Mix.Tasks.Pleroma.User do
bio: :string,
password: :string,
moderator: :boolean,
- admin: :boolean
+ admin: :boolean,
+ assume_yes: :boolean
+ ],
+ aliases: [
+ y: :assume_yes
]
)
@@ -79,6 +93,7 @@ defmodule Mix.Tasks.Pleroma.User do
moderator? = Keyword.get(options, :moderator, false)
admin? = Keyword.get(options, :admin, false)
+ assume_yes? = Keyword.get(options, :assume_yes, false)
Mix.shell().info("""
A user will be created with the following information:
@@ -93,7 +108,7 @@ defmodule Mix.Tasks.Pleroma.User do
- admin: #{if(admin?, do: "true", else: "false")}
""")
- proceed? = Mix.shell().yes?("Continue?")
+ proceed? = assume_yes? or Mix.shell().yes?("Continue?")
unless not proceed? do
Common.start_pleroma()
@@ -197,7 +212,7 @@ defmodule Mix.Tasks.Pleroma.User do
user = Repo.get(User, user.id)
- if length(user.following) == 0 do
+ if Enum.empty?(user.following) do
Mix.shell().info("Successfully unsubscribed all followers from #{user.nickname}")
end
else
@@ -243,6 +258,32 @@ defmodule Mix.Tasks.Pleroma.User do
end
end
+ def run(["tag", nickname | tags]) do
+ Common.start_pleroma()
+
+ with %User{} = user <- User.get_by_nickname(nickname) do
+ user = user |> User.tag(tags)
+
+ Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}")
+ else
+ _ ->
+ Mix.shell().error("Could not change user tags for #{nickname}")
+ end
+ end
+
+ def run(["untag", nickname | tags]) do
+ Common.start_pleroma()
+
+ with %User{} = user <- User.get_by_nickname(nickname) do
+ user = user |> User.untag(tags)
+
+ Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}")
+ else
+ _ ->
+ Mix.shell().error("Could not change user tags for #{nickname}")
+ end
+ end
+
def run(["invite"]) do
Common.start_pleroma()
diff --git a/lib/pleroma/PasswordResetToken.ex b/lib/pleroma/PasswordResetToken.ex
index 1dccdadae..750ddd3c0 100644
--- a/lib/pleroma/PasswordResetToken.ex
+++ b/lib/pleroma/PasswordResetToken.ex
@@ -7,10 +7,12 @@ defmodule Pleroma.PasswordResetToken do
import Ecto.Changeset
- alias Pleroma.{User, PasswordResetToken, Repo}
+ alias Pleroma.User
+ alias Pleroma.Repo
+ alias Pleroma.PasswordResetToken
schema "password_reset_tokens" do
- belongs_to(:user, User)
+ belongs_to(:user, User, type: Pleroma.FlakeId)
field(:token, :string)
field(:used, :boolean, default: false)
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index 353f9f6cd..cdfe7ea9e 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -4,10 +4,15 @@
defmodule Pleroma.Activity do
use Ecto.Schema
- alias Pleroma.{Repo, Activity, Notification}
+
+ alias Pleroma.Repo
+ alias Pleroma.Activity
+ alias Pleroma.Notification
+
import Ecto.Query
@type t :: %__MODULE__{}
+ @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
# https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
@mastodon_notification_types %{
@@ -36,10 +41,11 @@ defmodule Pleroma.Activity do
)
end
- # TODO:
- # Go through these and fix them everywhere.
- # Wrong name, only returns create activities
- def all_by_object_ap_id_q(ap_id) do
+ def get_by_id(id) do
+ Repo.get(Activity, id)
+ end
+
+ def by_object_ap_id(ap_id) do
from(
activity in Activity,
where:
@@ -48,57 +54,55 @@ defmodule Pleroma.Activity do
activity.data,
activity.data,
^to_string(ap_id)
- ),
- where: fragment("(?)->>'type' = 'Create'", activity.data)
+ )
)
end
- # Wrong name, returns all.
- def all_non_create_by_object_ap_id_q(ap_id) do
+ def create_by_object_ap_id(ap_ids) when is_list(ap_ids) do
from(
activity in Activity,
where:
fragment(
- "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
+ "coalesce((?)->'object'->>'id', (?)->>'object') = ANY(?)",
activity.data,
activity.data,
- ^to_string(ap_id)
- )
+ ^ap_ids
+ ),
+ where: fragment("(?)->>'type' = 'Create'", activity.data)
)
end
- # Wrong name plz fix thx
- def all_by_object_ap_id(ap_id) do
- Repo.all(all_by_object_ap_id_q(ap_id))
- end
-
- def create_activity_by_object_id_query(ap_ids) do
+ def create_by_object_ap_id(ap_id) do
from(
activity in Activity,
where:
fragment(
- "coalesce((?)->'object'->>'id', (?)->>'object') = ANY(?)",
+ "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
activity.data,
- ^ap_ids
+ ^to_string(ap_id)
),
where: fragment("(?)->>'type' = 'Create'", activity.data)
)
end
- def get_create_activity_by_object_ap_id(ap_id) when is_binary(ap_id) do
- create_activity_by_object_id_query([ap_id])
+ def get_all_create_by_object_ap_id(ap_id) do
+ Repo.all(create_by_object_ap_id(ap_id))
+ end
+
+ def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
+ create_by_object_ap_id(ap_id)
|> Repo.one()
end
- def get_create_activity_by_object_ap_id(_), do: nil
+ def get_create_by_object_ap_id(_), do: nil
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)
+ get_create_by_object_ap_id(ap_id)
end
def get_in_reply_to_activity(_), do: nil
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index cb3e6b69b..d67e2cdc8 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -6,11 +6,13 @@ defmodule Pleroma.Application do
use Application
import Supervisor.Spec
- @name "Pleroma"
+ @name Mix.Project.config()[:name]
@version Mix.Project.config()[:version]
+ @repository Mix.Project.config()[:source_url]
def name, do: @name
def version, do: @version
def named_version(), do: @name <> " " <> @version
+ def repository, do: @repository
def user_agent() do
info = "#{Pleroma.Web.base_url()} <#{Pleroma.Config.get([:instance, :email], "")}>"
@@ -22,6 +24,8 @@ defmodule Pleroma.Application do
def start(_type, _args) do
import Cachex.Spec
+ Pleroma.Config.DeprecationWarnings.warn()
+
# Define workers and child supervisors to be supervised
children =
[
@@ -66,6 +70,17 @@ defmodule Pleroma.Application do
worker(
Cachex,
[
+ :rich_media_cache,
+ [
+ default_ttl: :timer.minutes(120),
+ limit: 5000
+ ]
+ ],
+ id: :cachex_rich_media
+ ),
+ worker(
+ Cachex,
+ [
:scrubber_cache,
[
limit: 2500
@@ -88,11 +103,15 @@ defmodule Pleroma.Application do
],
id: :cachex_idem
),
- worker(Pleroma.Web.Federator.RetryQueue, []),
- worker(Pleroma.Web.Federator, []),
- worker(Pleroma.Stats, []),
- worker(Pleroma.Web.Push, [])
+ worker(Pleroma.FlakeId, [])
] ++
+ hackney_pool_children() ++
+ [
+ worker(Pleroma.Web.Federator.RetryQueue, []),
+ worker(Pleroma.Web.Federator, []),
+ worker(Pleroma.Stats, []),
+ worker(Pleroma.Web.Push, [])
+ ] ++
streamer_child() ++
chat_child() ++
[
@@ -107,6 +126,20 @@ defmodule Pleroma.Application do
Supervisor.start_link(children, opts)
end
+ def enabled_hackney_pools() do
+ [:media] ++
+ if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do
+ [:federation]
+ else
+ []
+ end ++
+ if Pleroma.Config.get([Pleroma.Uploader, :proxy_remote]) do
+ [:upload]
+ else
+ []
+ end
+ end
+
if Mix.env() == :test do
defp streamer_child(), do: []
defp chat_child(), do: []
@@ -123,4 +156,11 @@ defmodule Pleroma.Application do
end
end
end
+
+ defp hackney_pool_children() do
+ for pool <- enabled_hackney_pools() do
+ options = Pleroma.Config.get([:hackney_pools, pool])
+ :hackney_pool.child_spec(pool, options)
+ end
+ end
end
diff --git a/lib/pleroma/captcha/captcha.ex b/lib/pleroma/captcha/captcha.ex
index 0207bcbea..aa41acd1a 100644
--- a/lib/pleroma/captcha/captcha.ex
+++ b/lib/pleroma/captcha/captcha.ex
@@ -3,9 +3,9 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha do
+ alias Calendar.DateTime
alias Plug.Crypto.KeyGenerator
alias Plug.Crypto.MessageEncryptor
- alias Calendar.DateTime
use GenServer
diff --git a/lib/pleroma/clippy.ex b/lib/pleroma/clippy.ex
new file mode 100644
index 000000000..4e9bdbe19
--- /dev/null
+++ b/lib/pleroma/clippy.ex
@@ -0,0 +1,155 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Clippy do
+ @moduledoc false
+ # No software is complete until they have a Clippy implementation.
+ # A ballmer peak _may_ be required to change this module.
+
+ def tip() do
+ tips()
+ |> Enum.random()
+ |> puts()
+ end
+
+ def tips() do
+ host = Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
+
+ [
+ "“πλήρωμα” is “pleroma” in greek",
+ "For an extended Pleroma Clippy Experience, use the “Redmond” themes in Pleroma FE settings",
+ "Staff accounts and MRF policies of Pleroma instances are disclosed on the NodeInfo endpoints for easy transparency!\n
+- https://catgirl.science/misc/nodeinfo.lua?#{host}
+- https://fediverse.network/#{host}/federation",
+ "Pleroma can federate to the Dark Web!\n
+- Tor: https://git.pleroma.social/pleroma/pleroma/wikis/Easy%20Onion%20Federation%20(Tor)
+- i2p: https://git.pleroma.social/pleroma/pleroma/wikis/I2p%20federation",
+ "Lists of Pleroma instances:\n\n- http://distsn.org/pleroma-instances.html\n- https://fediverse.network/pleroma\n- https://the-federation.info/pleroma",
+ "Pleroma uses the LitePub protocol - https://litepub.social",
+ "To receive more federated posts, subscribe to relays!\n
+- How-to: https://git.pleroma.social/pleroma/pleroma/wikis/Admin%20tasks#relay-managment
+- Relays: https://fediverse.network/activityrelay"
+ ]
+ end
+
+ @spec puts(String.t() | [[IO.ANSI.ansicode() | String.t(), ...], ...]) :: nil
+ def puts(text_or_lines) do
+ import IO.ANSI
+
+ lines =
+ if is_binary(text_or_lines) do
+ String.split(text_or_lines, ~r/\n/)
+ else
+ text_or_lines
+ end
+
+ longest_line_size =
+ lines
+ |> Enum.map(&charlist_count_text/1)
+ |> Enum.sort(&>=/2)
+ |> List.first()
+
+ pad_text = longest_line_size
+
+ pad =
+ for(_ <- 1..pad_text, do: "_")
+ |> Enum.join("")
+
+ pad_spaces =
+ for(_ <- 1..pad_text, do: " ")
+ |> Enum.join("")
+
+ spaces = " "
+
+ pre_lines = [
+ " / \\#{spaces} _#{pad}___",
+ " | |#{spaces} / #{pad_spaces} \\"
+ ]
+
+ for l <- pre_lines do
+ IO.puts(l)
+ end
+
+ clippy_lines = [
+ " #{bright()}@ @#{reset()}#{spaces} ",
+ " || ||#{spaces}",
+ " || || <--",
+ " |\\_/| ",
+ " \\___/ "
+ ]
+
+ noclippy_line = " "
+
+ env = %{
+ max_size: pad_text,
+ pad: pad,
+ pad_spaces: pad_spaces,
+ spaces: spaces,
+ pre_lines: pre_lines,
+ noclippy_line: noclippy_line
+ }
+
+ # surrond one/five line clippy with blank lines around to not fuck up the layout
+ #
+ # yes this fix sucks but it's good enough, have you ever seen a release of windows wihtout some butched
+ # features anyway?
+ lines =
+ if length(lines) == 1 or length(lines) == 5 do
+ [""] ++ lines ++ [""]
+ else
+ lines
+ end
+
+ clippy_line(lines, clippy_lines, env)
+ rescue
+ e ->
+ IO.puts("(Clippy crashed, sorry: #{inspect(e)})")
+ IO.puts(text_or_lines)
+ end
+
+ defp clippy_line([line | lines], [prefix | clippy_lines], env) do
+ IO.puts([prefix <> "| ", rpad_line(line, env.max_size)])
+ clippy_line(lines, clippy_lines, env)
+ end
+
+ # more text lines but clippy's complete
+ defp clippy_line([line | lines], [], env) do
+ IO.puts([env.noclippy_line, "| ", rpad_line(line, env.max_size)])
+
+ if lines == [] do
+ IO.puts(env.noclippy_line <> "\\_#{env.pad}___/")
+ end
+
+ clippy_line(lines, [], env)
+ end
+
+ # no more text lines but clippy's not complete
+ defp clippy_line([], [clippy | clippy_lines], env) do
+ if env.pad do
+ IO.puts(clippy <> "\\_#{env.pad}___/")
+ clippy_line([], clippy_lines, %{env | pad: nil})
+ else
+ IO.puts(clippy)
+ clippy_line([], clippy_lines, env)
+ end
+ end
+
+ defp clippy_line(_, _, _) do
+ end
+
+ defp rpad_line(line, max) do
+ pad = max - (charlist_count_text(line) - 2)
+ pads = Enum.join(for(_ <- 1..pad, do: " "))
+ [IO.ANSI.format(line), pads <> " |"]
+ end
+
+ defp charlist_count_text(line) do
+ if is_list(line) do
+ text = Enum.join(Enum.filter(line, &is_binary/1))
+ String.length(text)
+ else
+ String.length(line)
+ end
+ end
+end
diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex
new file mode 100644
index 000000000..7451fd0a7
--- /dev/null
+++ b/lib/pleroma/config/deprecation_warnings.ex
@@ -0,0 +1,30 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Config.DeprecationWarnings do
+ require Logger
+
+ def check_frontend_config_mechanism() do
+ if Pleroma.Config.get(:fe) do
+ Logger.warn("""
+ !!!DEPRECATION WARNING!!!
+ You are using the old configuration mechanism for the frontend. Please check config.md.
+ """)
+ end
+ end
+
+ def check_hellthread_threshold do
+ if Pleroma.Config.get([:mrf_hellthread, :threshold]) do
+ Logger.warn("""
+ !!!DEPRECATION WARNING!!!
+ You are using the old configuration mechanism for the hellthread filter. Please check config.md.
+ """)
+ end
+ end
+
+ def warn do
+ check_frontend_config_mechanism()
+ check_hellthread_threshold()
+ end
+end
diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex
index c42c53c99..a3a09e96c 100644
--- a/lib/pleroma/emails/user_email.ex
+++ b/lib/pleroma/emails/user_email.ex
@@ -7,7 +7,8 @@ defmodule Pleroma.UserEmail do
import Swoosh.Email
- alias Pleroma.Web.{Endpoint, Router}
+ alias Pleroma.Web.Endpoint
+ alias Pleroma.Web.Router
defp instance_config, do: Pleroma.Config.get(:instance)
diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex
index df5374a5c..bdc34698c 100644
--- a/lib/pleroma/filter.ex
+++ b/lib/pleroma/filter.ex
@@ -4,11 +4,15 @@
defmodule Pleroma.Filter do
use Ecto.Schema
- import Ecto.{Changeset, Query}
- alias Pleroma.{User, Repo}
+
+ import Ecto.Changeset
+ import Ecto.Query
+
+ alias Pleroma.User
+ alias Pleroma.Repo
schema "filters" do
- belongs_to(:user, User)
+ belongs_to(:user, User, type: Pleroma.FlakeId)
field(:filter_id, :integer)
field(:hide, :boolean, default: false)
field(:whole_word, :boolean, default: true)
diff --git a/lib/pleroma/flake_id.ex b/lib/pleroma/flake_id.ex
new file mode 100644
index 000000000..9f098ce33
--- /dev/null
+++ b/lib/pleroma/flake_id.ex
@@ -0,0 +1,172 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.FlakeId do
+ @moduledoc """
+ Flake is a decentralized, k-ordered id generation service.
+
+ Adapted from:
+
+ * [flaky](https://github.com/nirvana/flaky), released under the terms of the Truly Free License,
+ * [Flake](https://github.com/boundary/flake), Copyright 2012, Boundary, Apache License, Version 2.0
+ """
+
+ @type t :: binary
+
+ @behaviour Ecto.Type
+ use GenServer
+ require Logger
+ alias __MODULE__
+ import Kernel, except: [to_string: 1]
+
+ defstruct node: nil, time: 0, sq: 0
+
+ @doc "Converts a binary Flake to a String"
+ def to_string(<<0::integer-size(64), id::integer-size(64)>>) do
+ Kernel.to_string(id)
+ end
+
+ def to_string(<<_::integer-size(64), _::integer-size(48), _::integer-size(16)>> = flake) do
+ encode_base62(flake)
+ end
+
+ def to_string(s), do: s
+
+ def from_string(int) when is_integer(int) do
+ from_string(Kernel.to_string(int))
+ end
+
+ for i <- [-1, 0] do
+ def from_string(unquote(i)), do: <<0::integer-size(128)>>
+ def from_string(unquote(Kernel.to_string(i))), do: <<0::integer-size(128)>>
+ end
+
+ def from_string(<<_::integer-size(128)>> = flake), do: flake
+
+ def from_string(string) when is_binary(string) and byte_size(string) < 18 do
+ case Integer.parse(string) do
+ {id, _} -> <<0::integer-size(64), id::integer-size(64)>>
+ _ -> nil
+ end
+ end
+
+ def from_string(string) do
+ string |> decode_base62 |> from_integer
+ end
+
+ def to_integer(<<integer::integer-size(128)>>), do: integer
+
+ def from_integer(integer) do
+ <<_time::integer-size(64), _node::integer-size(48), _seq::integer-size(16)>> =
+ <<integer::integer-size(128)>>
+ end
+
+ @doc "Generates a Flake"
+ @spec get :: binary
+ def get, do: to_string(:gen_server.call(:flake, :get))
+
+ # -- Ecto.Type API
+ @impl Ecto.Type
+ def type, do: :uuid
+
+ @impl Ecto.Type
+ def cast(value) do
+ {:ok, FlakeId.to_string(value)}
+ end
+
+ @impl Ecto.Type
+ def load(value) do
+ {:ok, FlakeId.to_string(value)}
+ end
+
+ @impl Ecto.Type
+ def dump(value) do
+ {:ok, FlakeId.from_string(value)}
+ end
+
+ def autogenerate(), do: get()
+
+ # -- GenServer API
+ def start_link do
+ :gen_server.start_link({:local, :flake}, __MODULE__, [], [])
+ end
+
+ @impl GenServer
+ def init([]) do
+ {:ok, %FlakeId{node: worker_id(), time: time()}}
+ end
+
+ @impl GenServer
+ def handle_call(:get, _from, state) do
+ {flake, new_state} = get(time(), state)
+ {:reply, flake, new_state}
+ end
+
+ # Matches when the calling time is the same as the state time. Incr. sq
+ defp get(time, %FlakeId{time: time, node: node, sq: seq}) do
+ new_state = %FlakeId{time: time, node: node, sq: seq + 1}
+ {gen_flake(new_state), new_state}
+ end
+
+ # Matches when the times are different, reset sq
+ defp get(newtime, %FlakeId{time: time, node: node}) when newtime > time do
+ new_state = %FlakeId{time: newtime, node: node, sq: 0}
+ {gen_flake(new_state), new_state}
+ end
+
+ # Error when clock is running backwards
+ defp get(newtime, %FlakeId{time: time}) when newtime < time do
+ {:error, :clock_running_backwards}
+ end
+
+ defp gen_flake(%FlakeId{time: time, node: node, sq: seq}) do
+ <<time::integer-size(64), node::integer-size(48), seq::integer-size(16)>>
+ end
+
+ defp nthchar_base62(n) when n <= 9, do: ?0 + n
+ defp nthchar_base62(n) when n <= 35, do: ?A + n - 10
+ defp nthchar_base62(n), do: ?a + n - 36
+
+ defp encode_base62(<<integer::integer-size(128)>>) do
+ integer
+ |> encode_base62([])
+ |> List.to_string()
+ end
+
+ defp encode_base62(int, acc) when int < 0, do: encode_base62(-int, acc)
+ defp encode_base62(int, []) when int == 0, do: '0'
+ defp encode_base62(int, acc) when int == 0, do: acc
+
+ defp encode_base62(int, acc) do
+ r = rem(int, 62)
+ id = div(int, 62)
+ acc = [nthchar_base62(r) | acc]
+ encode_base62(id, acc)
+ end
+
+ defp decode_base62(s) do
+ decode_base62(String.to_charlist(s), 0)
+ end
+
+ defp decode_base62([c | cs], acc) when c >= ?0 and c <= ?9,
+ do: decode_base62(cs, 62 * acc + (c - ?0))
+
+ defp decode_base62([c | cs], acc) when c >= ?A and c <= ?Z,
+ do: decode_base62(cs, 62 * acc + (c - ?A + 10))
+
+ defp decode_base62([c | cs], acc) when c >= ?a and c <= ?z,
+ do: decode_base62(cs, 62 * acc + (c - ?a + 36))
+
+ defp decode_base62([], acc), do: acc
+
+ defp time do
+ {mega_seconds, seconds, micro_seconds} = :erlang.timestamp()
+ 1_000_000_000 * mega_seconds + seconds * 1000 + :erlang.trunc(micro_seconds / 1000)
+ end
+
+ defp worker_id() do
+ <<worker::integer-size(48)>> = :crypto.strong_rand_bytes(6)
+ worker
+ end
+end
diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex
index d80ae6576..f31aafa0d 100644
--- a/lib/pleroma/formatter.ex
+++ b/lib/pleroma/formatter.ex
@@ -3,10 +3,10 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Formatter do
+ alias Pleroma.Emoji
+ alias Pleroma.HTML
alias Pleroma.User
alias Pleroma.Web.MediaProxy
- alias Pleroma.HTML
- alias Pleroma.Emoji
@tag_regex ~r/((?<=[^&])|\A)(\#)(\w+)/u
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
@@ -43,7 +43,7 @@ defmodule Pleroma.Formatter do
def emojify(text, nil), do: text
- def emojify(text, emoji) do
+ def emojify(text, emoji, strip \\ false) do
Enum.reduce(emoji, text, fn {emoji, file}, text ->
emoji = HTML.strip_tags(emoji)
file = HTML.strip_tags(file)
@@ -51,14 +51,24 @@ defmodule Pleroma.Formatter do
String.replace(
text,
":#{emoji}:",
- "<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{
- MediaProxy.url(file)
- }' />"
+ if not strip do
+ "<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{
+ MediaProxy.url(file)
+ }' />"
+ else
+ ""
+ end
)
|> HTML.filter_tags()
end)
end
+ def demojify(text) do
+ emojify(text, Emoji.get_all(), true)
+ end
+
+ def demojify(text, nil), do: text
+
def get_emoji(text) when is_binary(text) do
Enum.filter(Emoji.get_all(), fn {emoji, _} -> String.contains?(text, ":#{emoji}:") end)
end
@@ -120,7 +130,7 @@ defmodule Pleroma.Formatter do
end
@doc "Adds the links to mentioned users"
- def add_user_links({subs, text}, mentions) do
+ def add_user_links({subs, text}, mentions, options \\ []) do
mentions =
mentions
|> Enum.sort_by(fn {name, _} -> -String.length(name) end)
@@ -142,10 +152,16 @@ defmodule Pleroma.Formatter do
ap_id
end
- short_match = String.split(match, "@") |> tl() |> hd()
+ nickname =
+ if options[:format] == :full do
+ User.full_nickname(match)
+ else
+ User.local_nickname(match)
+ end
{uuid,
- "<span><a data-user='#{id}' class='mention' href='#{ap_id}'>@<span>#{short_match}</span></a></span>"}
+ "<span class='h-card'><a data-user='#{id}' class='u-url mention' href='#{ap_id}'>" <>
+ "@<span>#{nickname}</span></a></span>"}
end)
{subs, uuid_text}
@@ -168,7 +184,7 @@ defmodule Pleroma.Formatter do
subs ++
Enum.map(tags, fn {tag_text, tag, uuid} ->
url =
- "<a data-tag='#{tag}' href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{
+ "<a class='hashtag' data-tag='#{tag}' href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{
tag_text
}</a>"
@@ -183,4 +199,16 @@ defmodule Pleroma.Formatter do
String.replace(result_text, uuid, replacement)
end)
end
+
+ def truncate(text, max_length \\ 200, omission \\ "...") do
+ # Remove trailing whitespace
+ text = Regex.replace(~r/([^ \t\r\n])([ \t]+$)/u, text, "\\g{1}")
+
+ if String.length(text) < max_length do
+ text
+ else
+ length_with_omission = max_length - String.length(omission)
+ String.slice(text, 0, length_with_omission) <> omission
+ end
+ end
end
diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex
index 336142e9b..32cb817d2 100644
--- a/lib/pleroma/gopher/server.ex
+++ b/lib/pleroma/gopher/server.ex
@@ -37,17 +37,17 @@ end
defmodule Pleroma.Gopher.Server.ProtocolHandler do
alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.User
alias Pleroma.Activity
- alias Pleroma.Repo
alias Pleroma.HTML
+ alias Pleroma.User
+ alias Pleroma.Repo
def start_link(ref, socket, transport, opts) do
pid = spawn_link(__MODULE__, :init, [ref, socket, transport, opts])
{:ok, pid}
end
- def init(ref, socket, transport, _Opts = []) do
+ def init(ref, socket, transport, [] = _Opts) do
:ok = :ranch.accept_ack(ref)
loop(socket, transport)
end
diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex
index 71db516e6..4dc6998b1 100644
--- a/lib/pleroma/html.ex
+++ b/lib/pleroma/html.ex
@@ -28,13 +28,18 @@ defmodule Pleroma.HTML do
def filter_tags(html), do: filter_tags(html, nil)
def strip_tags(html), do: Scrubber.scrub(html, Scrubber.StripTags)
- def get_cached_scrubbed_html_for_object(content, scrubbers, object) do
- key = "#{generate_scrubber_signature(scrubbers)}|#{object.id}"
+ def get_cached_scrubbed_html_for_object(content, scrubbers, object, module) do
+ key = "#{module}#{generate_scrubber_signature(scrubbers)}|#{object.id}"
Cachex.fetch!(:scrubber_cache, key, fn _key -> ensure_scrubbed_html(content, scrubbers) end)
end
- def get_cached_stripped_html_for_object(content, object) do
- get_cached_scrubbed_html_for_object(content, HtmlSanitizeEx.Scrubber.StripTags, object)
+ def get_cached_stripped_html_for_object(content, object, module) do
+ get_cached_scrubbed_html_for_object(
+ content,
+ HtmlSanitizeEx.Scrubber.StripTags,
+ object,
+ module
+ )
end
def ensure_scrubbed_html(
@@ -50,15 +55,23 @@ defmodule Pleroma.HTML do
defp generate_scrubber_signature(scrubbers) do
Enum.reduce(scrubbers, "", fn scrubber, signature ->
- # If a scrubber does not have a version(e.g HtmlSanitizeEx.Scrubber.StripTags) it is assumed it is always 0)
- version =
- if Kernel.function_exported?(scrubber, :version, 0) do
- scrubber.version
- else
- 0
- end
-
- "#{signature}#{to_string(scrubber)}#{version}"
+ "#{signature}#{to_string(scrubber)}"
+ end)
+ end
+
+ def extract_first_external_url(_, nil), do: {:error, "No content"}
+
+ def extract_first_external_url(object, content) do
+ key = "URL|#{object.id}"
+
+ Cachex.fetch!(:scrubber_cache, key, fn _key ->
+ result =
+ content
+ |> Floki.filter_out("a.mention")
+ |> Floki.attribute("a", "href")
+ |> Enum.at(0)
+
+ {:commit, {:ok, result}}
end)
end
end
@@ -70,29 +83,24 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
"""
@markup Application.get_env(:pleroma, :markup)
- @uri_schemes Application.get_env(:pleroma, :uri_schemes, [])
- @valid_schemes Keyword.get(@uri_schemes, :valid_schemes, [])
+ @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
require HtmlSanitizeEx.Scrubber.Meta
alias HtmlSanitizeEx.Scrubber.Meta
- def version do
- 0
- end
-
Meta.remove_cdata_sections_before_scrub()
Meta.strip_comments()
# links
Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
- Meta.allow_tag_with_these_attributes("a", ["name", "title"])
+ Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"])
# paragraphs and linebreaks
Meta.allow_tag_with_these_attributes("br", [])
Meta.allow_tag_with_these_attributes("p", [])
# microformats
- Meta.allow_tag_with_these_attributes("span", [])
+ Meta.allow_tag_with_these_attributes("span", ["class"])
# allow inline images for custom emoji
@allow_inline_images Keyword.get(@markup, :allow_inline_images)
@@ -117,20 +125,17 @@ defmodule Pleroma.HTML.Scrubber.Default do
require HtmlSanitizeEx.Scrubber.Meta
alias HtmlSanitizeEx.Scrubber.Meta
-
- def version do
- 0
- end
+ # credo:disable-for-previous-line
+ # No idea how to fix this one…
@markup Application.get_env(:pleroma, :markup)
- @uri_schemes Application.get_env(:pleroma, :uri_schemes, [])
- @valid_schemes Keyword.get(@uri_schemes, :valid_schemes, [])
+ @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
Meta.remove_cdata_sections_before_scrub()
Meta.strip_comments()
Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
- Meta.allow_tag_with_these_attributes("a", ["name", "title"])
+ Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"])
Meta.allow_tag_with_these_attributes("abbr", ["title"])
@@ -145,7 +150,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes("ol", [])
Meta.allow_tag_with_these_attributes("p", [])
Meta.allow_tag_with_these_attributes("pre", [])
- Meta.allow_tag_with_these_attributes("span", [])
+ Meta.allow_tag_with_these_attributes("span", ["class"])
Meta.allow_tag_with_these_attributes("strong", [])
Meta.allow_tag_with_these_attributes("u", [])
Meta.allow_tag_with_these_attributes("ul", [])
@@ -199,10 +204,6 @@ defmodule Pleroma.HTML.Transform.MediaProxy do
alias Pleroma.Web.MediaProxy
- def version do
- 0
- end
-
def before_scrub(html), do: html
def scrub_attribute("img", {"src", "http" <> target}) do
diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex
index 699d80cd7..b798eaa5a 100644
--- a/lib/pleroma/http/connection.ex
+++ b/lib/pleroma/http/connection.ex
@@ -10,7 +10,8 @@ defmodule Pleroma.HTTP.Connection do
@hackney_options [
timeout: 10000,
recv_timeout: 20000,
- follow_redirect: true
+ follow_redirect: true,
+ pool: :federation
]
@adapter Application.get_env(:tesla, :adapter)
diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex
index b8103cef6..75c58e6c9 100644
--- a/lib/pleroma/http/http.ex
+++ b/lib/pleroma/http/http.ex
@@ -31,12 +31,15 @@ defmodule Pleroma.HTTP do
process_request_options(options)
|> process_sni_options(url)
+ params = Keyword.get(options, :params, [])
+
%{}
|> Builder.method(method)
|> Builder.headers(headers)
|> Builder.opts(options)
|> Builder.url(url)
|> Builder.add_param(:body, :body, body)
+ |> Builder.add_param(:query, :query, params)
|> Enum.into([])
|> (&Tesla.request(Connection.new(), &1)).()
end
diff --git a/lib/pleroma/http/request_builder.ex b/lib/pleroma/http/request_builder.ex
index bffc7c6fe..5f2cff2c0 100644
--- a/lib/pleroma/http/request_builder.ex
+++ b/lib/pleroma/http/request_builder.ex
@@ -100,6 +100,8 @@ defmodule Pleroma.HTTP.RequestBuilder do
Map
"""
@spec add_param(map(), atom, atom, any()) :: map()
+ def add_param(request, :query, :query, values), do: Map.put(request, :query, values)
+
def add_param(request, :body, :body, value), do: Map.put(request, :body, value)
def add_param(request, :body, key, value) do
@@ -107,7 +109,10 @@ defmodule Pleroma.HTTP.RequestBuilder do
|> Map.put_new_lazy(:body, &Tesla.Multipart.new/0)
|> Map.update!(
:body,
- &Tesla.Multipart.add_field(&1, key, Poison.encode!(value),
+ &Tesla.Multipart.add_field(
+ &1,
+ key,
+ Jason.encode!(value),
headers: [{:"Content-Type", "application/json"}]
)
)
diff --git a/lib/pleroma/instances.ex b/lib/pleroma/instances.ex
new file mode 100644
index 000000000..5e107f4c9
--- /dev/null
+++ b/lib/pleroma/instances.ex
@@ -0,0 +1,36 @@
+defmodule Pleroma.Instances do
+ @moduledoc "Instances context."
+
+ @adapter Pleroma.Instances.Instance
+
+ defdelegate filter_reachable(urls_or_hosts), to: @adapter
+ defdelegate reachable?(url_or_host), to: @adapter
+ defdelegate set_reachable(url_or_host), to: @adapter
+ defdelegate set_unreachable(url_or_host, unreachable_since \\ nil), to: @adapter
+
+ def set_consistently_unreachable(url_or_host),
+ do: set_unreachable(url_or_host, reachability_datetime_threshold())
+
+ def reachability_datetime_threshold do
+ federation_reachability_timeout_days =
+ Pleroma.Config.get(:instance)[:federation_reachability_timeout_days] || 0
+
+ if federation_reachability_timeout_days > 0 do
+ NaiveDateTime.add(
+ NaiveDateTime.utc_now(),
+ -federation_reachability_timeout_days * 24 * 3600,
+ :second
+ )
+ else
+ ~N[0000-01-01 00:00:00]
+ end
+ end
+
+ def host(url_or_host) when is_binary(url_or_host) do
+ if url_or_host =~ ~r/^http/i do
+ URI.parse(url_or_host).host
+ else
+ url_or_host
+ end
+ end
+end
diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex
new file mode 100644
index 000000000..48bc939dd
--- /dev/null
+++ b/lib/pleroma/instances/instance.ex
@@ -0,0 +1,113 @@
+defmodule Pleroma.Instances.Instance do
+ @moduledoc "Instance."
+
+ alias Pleroma.Instances
+ alias Pleroma.Repo
+ alias Pleroma.Instances.Instance
+
+ use Ecto.Schema
+
+ import Ecto.Query
+ import Ecto.Changeset
+
+ schema "instances" do
+ field(:host, :string)
+ field(:unreachable_since, :naive_datetime)
+
+ timestamps()
+ end
+
+ defdelegate host(url_or_host), to: Instances
+
+ def changeset(struct, params \\ %{}) do
+ struct
+ |> cast(params, [:host, :unreachable_since])
+ |> validate_required([:host])
+ |> unique_constraint(:host)
+ end
+
+ def filter_reachable([]), do: %{}
+
+ def filter_reachable(urls_or_hosts) when is_list(urls_or_hosts) do
+ hosts =
+ urls_or_hosts
+ |> Enum.map(&(&1 && host(&1)))
+ |> Enum.filter(&(to_string(&1) != ""))
+
+ unreachable_since_by_host =
+ Repo.all(
+ from(i in Instance,
+ where: i.host in ^hosts,
+ select: {i.host, i.unreachable_since}
+ )
+ )
+ |> Map.new(& &1)
+
+ reachability_datetime_threshold = Instances.reachability_datetime_threshold()
+
+ for entry <- Enum.filter(urls_or_hosts, &is_binary/1) do
+ host = host(entry)
+ unreachable_since = unreachable_since_by_host[host]
+
+ if !unreachable_since ||
+ NaiveDateTime.compare(unreachable_since, reachability_datetime_threshold) == :gt do
+ {entry, unreachable_since}
+ end
+ end
+ |> Enum.filter(& &1)
+ |> Map.new(& &1)
+ end
+
+ def reachable?(url_or_host) when is_binary(url_or_host) do
+ !Repo.one(
+ from(i in Instance,
+ where:
+ i.host == ^host(url_or_host) and
+ i.unreachable_since <= ^Instances.reachability_datetime_threshold(),
+ select: true
+ )
+ )
+ end
+
+ def reachable?(_), do: true
+
+ def set_reachable(url_or_host) when is_binary(url_or_host) do
+ with host <- host(url_or_host),
+ %Instance{} = existing_record <- Repo.get_by(Instance, %{host: host}) do
+ {:ok, _instance} =
+ existing_record
+ |> changeset(%{unreachable_since: nil})
+ |> Repo.update()
+ end
+ end
+
+ def set_reachable(_), do: {:error, nil}
+
+ def set_unreachable(url_or_host, unreachable_since \\ nil)
+
+ def set_unreachable(url_or_host, unreachable_since) when is_binary(url_or_host) do
+ unreachable_since = unreachable_since || DateTime.utc_now()
+ host = host(url_or_host)
+ existing_record = Repo.get_by(Instance, %{host: host})
+
+ changes = %{unreachable_since: unreachable_since}
+
+ cond do
+ is_nil(existing_record) ->
+ %Instance{}
+ |> changeset(Map.put(changes, :host, host))
+ |> Repo.insert()
+
+ existing_record.unreachable_since &&
+ NaiveDateTime.compare(existing_record.unreachable_since, unreachable_since) != :gt ->
+ {:ok, existing_record}
+
+ true ->
+ existing_record
+ |> changeset(changes)
+ |> Repo.update()
+ end
+ end
+
+ def set_unreachable(_, _), do: {:error, nil}
+end
diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex
index a75dc006e..55c4cf6df 100644
--- a/lib/pleroma/list.ex
+++ b/lib/pleroma/list.ex
@@ -4,11 +4,16 @@
defmodule Pleroma.List do
use Ecto.Schema
- import Ecto.{Changeset, Query}
- alias Pleroma.{User, Repo, Activity}
+
+ import Ecto.Query
+ import Ecto.Changeset
+
+ alias Pleroma.Activity
+ alias Pleroma.Repo
+ alias Pleroma.User
schema "lists" do
- belongs_to(:user, Pleroma.User)
+ belongs_to(:user, User, type: Pleroma.FlakeId)
field(:title, :string)
field(:following, {:array, :string}, default: [])
diff --git a/lib/pleroma/mime.ex b/lib/pleroma/mime.ex
index 84fb536e0..36771533f 100644
--- a/lib/pleroma/mime.ex
+++ b/lib/pleroma/mime.ex
@@ -102,10 +102,18 @@ defmodule Pleroma.MIME do
"audio/ogg"
end
- defp check_mime_type(<<0x52, 0x49, 0x46, 0x46, _::binary>>) do
+ defp check_mime_type(<<"RIFF", _::binary-size(4), "WAVE", _::binary>>) do
"audio/wav"
end
+ defp check_mime_type(<<"RIFF", _::binary-size(4), "WEBP", _::binary>>) do
+ "image/webp"
+ end
+
+ defp check_mime_type(<<"RIFF", _::binary-size(4), "AVI.", _::binary>>) do
+ "video/avi"
+ end
+
defp check_mime_type(_) do
@default
end
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index 51d59870c..c88512567 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -4,13 +4,20 @@
defmodule Pleroma.Notification do
use Ecto.Schema
- alias Pleroma.{User, Activity, Notification, Repo, Object}
+
+ alias Pleroma.User
+ alias Pleroma.Activity
+ alias Pleroma.Notification
+ alias Pleroma.Repo
+ alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.CommonAPI
+
import Ecto.Query
schema "notifications" do
field(:seen, :boolean, default: false)
- belongs_to(:user, Pleroma.User)
- belongs_to(:activity, Pleroma.Activity)
+ belongs_to(:user, User, type: Pleroma.FlakeId)
+ belongs_to(:activity, Activity, type: Pleroma.FlakeId)
timestamps()
end
@@ -34,7 +41,8 @@ defmodule Pleroma.Notification do
n in Notification,
where: n.user_id == ^user.id,
order_by: [desc: n.id],
- preload: [:activity],
+ join: activity in assoc(n, :activity),
+ preload: [activity: activity],
limit: 20
)
@@ -65,7 +73,8 @@ defmodule Pleroma.Notification do
from(
n in Notification,
where: n.id == ^id,
- preload: [:activity]
+ join: activity in assoc(n, :activity),
+ preload: [activity: activity]
)
notification = Repo.one(query)
@@ -96,7 +105,7 @@ defmodule Pleroma.Notification do
end
end
- def create_notifications(%Activity{id: _, data: %{"to" => _, "type" => type}} = activity)
+ def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)
when type in ["Create", "Like", "Announce", "Follow"] do
users = get_notified_from_activity(activity)
@@ -109,7 +118,12 @@ defmodule Pleroma.Notification do
# TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user) do
unless User.blocks?(user, %{ap_id: activity.data["actor"]}) or
- user.ap_id == activity.data["actor"] do
+ CommonAPI.thread_muted?(user, activity) or user.ap_id == activity.data["actor"] or
+ (activity.data["type"] == "Follow" and
+ Enum.any?(Notification.for_user(user), fn notif ->
+ notif.activity.data["type"] == "Follow" and
+ notif.activity.data["actor"] == activity.data["actor"]
+ end)) do
notification = %Notification{user_id: user.id, activity: activity}
{:ok, notification} = Repo.insert(notification)
Pleroma.Web.Streamer.stream("user", notification)
@@ -127,54 +141,12 @@ defmodule Pleroma.Notification do
when type in ["Create", "Like", "Announce", "Follow"] do
recipients =
[]
- |> maybe_notify_to_recipients(activity)
- |> maybe_notify_mentioned_recipients(activity)
+ |> Utils.maybe_notify_to_recipients(activity)
+ |> Utils.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 ff5eb9b27..5f1fc801b 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -4,8 +4,15 @@
defmodule Pleroma.Object do
use Ecto.Schema
- alias Pleroma.{Repo, Object, User, Activity, ObjectTombstone}
- import Ecto.{Query, Changeset}
+
+ alias Pleroma.Repo
+ alias Pleroma.Object
+ alias Pleroma.User
+ alias Pleroma.Activity
+ alias Pleroma.ObjectTombstone
+
+ import Ecto.Query
+ import Ecto.Changeset
schema "objects" do
field(:data, :map)
@@ -31,8 +38,8 @@ defmodule Pleroma.Object do
Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
end
- def normalize(obj) when is_map(obj), do: Object.get_by_ap_id(obj["id"])
- def normalize(ap_id) when is_binary(ap_id), do: Object.get_by_ap_id(ap_id)
+ def normalize(%{"id" => ap_id}), do: normalize(ap_id)
+ def normalize(ap_id) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id)
def normalize(_), do: nil
# Owned objects can only be mutated by their owner
@@ -42,24 +49,18 @@ defmodule Pleroma.Object do
# Legacy objects can be mutated by anybody
def authorize_mutation(%Object{}, %User{}), do: true
- if Mix.env() == :test do
- def get_cached_by_ap_id(ap_id) do
- get_by_ap_id(ap_id)
- end
- else
- def get_cached_by_ap_id(ap_id) do
- key = "object:#{ap_id}"
-
- Cachex.fetch!(:object_cache, key, fn _ ->
- object = get_by_ap_id(ap_id)
-
- if object do
- {:commit, object}
- else
- {:ignore, object}
- end
- end)
- end
+ def get_cached_by_ap_id(ap_id) do
+ key = "object:#{ap_id}"
+
+ Cachex.fetch!(:object_cache, key, fn _ ->
+ object = get_by_ap_id(ap_id)
+
+ if object do
+ {:commit, object}
+ else
+ {:ignore, object}
+ end
+ end)
end
def context_mapping(context) do
@@ -85,9 +86,22 @@ defmodule Pleroma.Object do
def delete(%Object{data: %{"id" => id}} = object) do
with {:ok, _obj} = swap_object_with_tombstone(object),
- Repo.delete_all(Activity.all_non_create_by_object_ap_id_q(id)),
+ Repo.delete_all(Activity.by_object_ap_id(id)),
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
{:ok, object}
end
end
+
+ def set_cache(%Object{data: %{"id" => ap_id}} = object) do
+ Cachex.put(:object_cache, "object:#{ap_id}", object)
+ {:ok, object}
+ end
+
+ def update_and_set_cache(changeset) do
+ with {:ok, object} <- Repo.update(changeset) do
+ set_cache(object)
+ else
+ e -> e
+ end
+ end
end
diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex
index 2a266c407..057553e24 100644
--- a/lib/pleroma/plugs/http_security_plug.ex
+++ b/lib/pleroma/plugs/http_security_plug.ex
@@ -33,7 +33,22 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
end
defp csp_string do
- protocol = Config.get([Pleroma.Web.Endpoint, :protocol])
+ scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme]
+ websocket_url = String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws")
+
+ connect_src =
+ if Mix.env() == :dev do
+ "connect-src 'self' http://localhost:3035/ " <> websocket_url
+ else
+ "connect-src 'self' " <> websocket_url
+ end
+
+ script_src =
+ if Mix.env() == :dev do
+ "script-src 'self' 'unsafe-eval'"
+ else
+ "script-src 'self'"
+ end
[
"default-src 'none'",
@@ -43,10 +58,10 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
"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
+ connect_src,
+ script_src,
+ if scheme == "https" do
"upgrade-insecure-requests"
end
]
diff --git a/lib/pleroma/plugs/instance_static.ex b/lib/pleroma/plugs/instance_static.ex
index af2f6f331..41125921a 100644
--- a/lib/pleroma/plugs/instance_static.ex
+++ b/lib/pleroma/plugs/instance_static.ex
@@ -21,7 +21,7 @@ defmodule Pleroma.Plugs.InstanceStatic do
end
end
- @only ~w(index.html static emoji packs sounds images instance favicon.png)
+ @only ~w(index.html static emoji packs sounds images instance favicon.png sw.js sw-pleroma.js)
def init(opts) do
opts
@@ -33,7 +33,7 @@ defmodule Pleroma.Plugs.InstanceStatic do
for only <- @only do
at = Plug.Router.Utils.split("/")
- def call(conn = %{request_path: "/" <> unquote(only) <> _}, opts) do
+ def call(%{request_path: "/" <> unquote(only) <> _} = conn, opts) do
call_static(
conn,
opts,
diff --git a/lib/pleroma/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex
index 437aa95b3..22f0406f4 100644
--- a/lib/pleroma/plugs/oauth_plug.ex
+++ b/lib/pleroma/plugs/oauth_plug.ex
@@ -6,11 +6,9 @@ defmodule Pleroma.Plugs.OAuthPlug do
import Plug.Conn
import Ecto.Query
- alias Pleroma.{
- User,
- Repo,
- Web.OAuth.Token
- }
+ alias Pleroma.User
+ alias Pleroma.Repo
+ alias Pleroma.Web.OAuth.Token
@realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i")
@@ -33,7 +31,12 @@ defmodule Pleroma.Plugs.OAuthPlug do
#
@spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil
defp fetch_user_and_token(token) do
- query = from(q in Token, where: q.token == ^token, preload: [:user])
+ query =
+ from(t in Token,
+ where: t.token == ^token,
+ join: user in assoc(t, :user),
+ preload: [user: user]
+ )
with %Token{user: %{info: %{deactivated: false} = _} = user} = token_record <- Repo.one(query) do
{:ok, user, token_record}
diff --git a/lib/pleroma/plugs/uploaded_media.ex b/lib/pleroma/plugs/uploaded_media.ex
index be53ac00c..13aa8641a 100644
--- a/lib/pleroma/plugs/uploaded_media.ex
+++ b/lib/pleroma/plugs/uploaded_media.ex
@@ -23,7 +23,7 @@ defmodule Pleroma.Plugs.UploadedMedia do
%{static_plug_opts: static_plug_opts}
end
- def call(conn = %{request_path: <<"/", @path, "/", file::binary>>}, opts) do
+ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
config = Pleroma.Config.get([Pleroma.Upload])
with uploader <- Keyword.fetch!(config, :uploader),
diff --git a/lib/pleroma/plugs/user_fetcher_plug.ex b/lib/pleroma/plugs/user_fetcher_plug.ex
index f874e2f95..7ed4602bb 100644
--- a/lib/pleroma/plugs/user_fetcher_plug.ex
+++ b/lib/pleroma/plugs/user_fetcher_plug.ex
@@ -3,9 +3,10 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.UserFetcherPlug do
- import Plug.Conn
- alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.Repo
+
+ import Plug.Conn
def init(options) do
options
diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex
index a3846c3bb..a25b5ea4e 100644
--- a/lib/pleroma/reverse_proxy.ex
+++ b/lib/pleroma/reverse_proxy.ex
@@ -275,11 +275,24 @@ defmodule Pleroma.ReverseProxy do
defp build_resp_cache_headers(headers, _opts) do
has_cache? = Enum.any?(headers, fn {k, _} -> k in @resp_cache_headers end)
-
- if has_cache? do
- headers
- else
- List.keystore(headers, "cache-control", 0, {"cache-control", @default_cache_control_header})
+ has_cache_control? = List.keymember?(headers, "cache-control", 0)
+
+ cond do
+ has_cache? && has_cache_control? ->
+ headers
+
+ has_cache? ->
+ # There's caching header present but no cache-control -- we need to explicitely override it to public
+ # as Plug defaults to "max-age=0, private, must-revalidate"
+ List.keystore(headers, "cache-control", 0, {"cache-control", "public"})
+
+ true ->
+ List.keystore(
+ headers,
+ "cache-control",
+ 0,
+ {"cache-control", @default_cache_control_header}
+ )
end
end
diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex
index 8a030ecd0..fe0ce9051 100644
--- a/lib/pleroma/stats.ex
+++ b/lib/pleroma/stats.ex
@@ -4,7 +4,8 @@
defmodule Pleroma.Stats do
import Ecto.Query
- alias Pleroma.{User, Repo}
+ alias Pleroma.User
+ alias Pleroma.Repo
def start_link do
agent = Agent.start_link(fn -> {[], %{}} end, name: __MODULE__)
@@ -23,7 +24,7 @@ defmodule Pleroma.Stats do
def schedule_update do
spawn(fn ->
# 1 hour
- Process.sleep(1000 * 60 * 60 * 1)
+ Process.sleep(1000 * 60 * 60)
schedule_update()
end)
@@ -34,10 +35,11 @@ defmodule Pleroma.Stats do
peers =
from(
u in Pleroma.User,
- select: fragment("distinct ?->'host'", u.info),
+ select: fragment("distinct split_part(?, '@', 2)", u.nickname),
where: u.local != ^true
)
|> Repo.all()
+ |> Enum.filter(& &1)
domain_count = Enum.count(peers)
@@ -45,7 +47,7 @@ defmodule Pleroma.Stats do
from(u in User.local_user_query(), select: fragment("sum((?->>'note_count')::int)", u.info))
status_count = Repo.one(status_query)
- user_count = Repo.aggregate(User.local_user_query(), :count, :id)
+ user_count = Repo.aggregate(User.active_local_user_query(), :count, :id)
Agent.update(__MODULE__, fn _ ->
{peers, %{domain_count: domain_count, status_count: status_count, user_count: user_count}}
diff --git a/lib/pleroma/thread_mute.ex b/lib/pleroma/thread_mute.ex
new file mode 100644
index 000000000..0b577113d
--- /dev/null
+++ b/lib/pleroma/thread_mute.ex
@@ -0,0 +1,45 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ThreadMute do
+ use Ecto.Schema
+ alias Pleroma.{Repo, User, ThreadMute}
+ require Ecto.Query
+
+ schema "thread_mutes" do
+ belongs_to(:user, User, type: Pleroma.FlakeId)
+ field(:context, :string)
+ end
+
+ def changeset(mute, params \\ %{}) do
+ mute
+ |> Ecto.Changeset.cast(params, [:user_id, :context])
+ |> Ecto.Changeset.foreign_key_constraint(:user_id)
+ |> Ecto.Changeset.unique_constraint(:user_id, name: :unique_index)
+ end
+
+ def query(user_id, context) do
+ user_id = Pleroma.FlakeId.from_string(user_id)
+
+ ThreadMute
+ |> Ecto.Query.where(user_id: ^user_id)
+ |> Ecto.Query.where(context: ^context)
+ end
+
+ def add_mute(user_id, context) do
+ %ThreadMute{}
+ |> changeset(%{user_id: user_id, context: context})
+ |> Repo.insert()
+ end
+
+ def remove_mute(user_id, context) do
+ query(user_id, context)
+ |> Repo.delete_all()
+ end
+
+ def check_muted(user_id, context) do
+ query(user_id, context)
+ |> Repo.all()
+ end
+end
diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex
index 0b1bdeec4..91a5db8c5 100644
--- a/lib/pleroma/upload.ex
+++ b/lib/pleroma/upload.ex
@@ -34,8 +34,9 @@ defmodule Pleroma.Upload do
require Logger
@type source ::
- Plug.Upload.t() | data_uri_string ::
- String.t() | {:from_local, name :: String.t(), id :: String.t(), path :: String.t()}
+ Plug.Upload.t()
+ | (data_uri_string :: String.t())
+ | {:from_local, name :: String.t(), id :: String.t(), path :: String.t()}
@type option ::
{:type, :avatar | :banner | :background}
@@ -123,10 +124,10 @@ defmodule Pleroma.Upload do
:pleroma, Pleroma.Upload, [filters: [Pleroma.Upload.Filter.Mogrify]]
- :pleroma, Pleroma.Upload.Filter.Mogrify, args: "strip"
+ :pleroma, Pleroma.Upload.Filter.Mogrify, args: ["strip", "auto-orient"]
""")
- Pleroma.Config.put([Pleroma.Upload.Filter.Mogrify], args: "strip")
+ Pleroma.Config.put([Pleroma.Upload.Filter.Mogrify], args: ["strip", "auto-orient"])
Map.put(opts, :filters, opts.filters ++ [Pleroma.Upload.Filter.Mogrify])
else
opts
@@ -179,7 +180,7 @@ defmodule Pleroma.Upload do
end
# For Mix.Tasks.MigrateLocalUploads
- defp prepare_upload(upload = %__MODULE__{tempfile: path}, _opts) do
+ defp prepare_upload(%__MODULE__{tempfile: path} = upload, _opts) do
with {:ok, content_type} <- Pleroma.MIME.file_mime_type(path) do
{:ok, %__MODULE__{upload | content_type: content_type}}
end
@@ -215,6 +216,12 @@ defmodule Pleroma.Upload do
end
defp url_from_spec(base_url, {:file, path}) do
+ path =
+ path
+ |> URI.encode()
+ |> String.replace("?", "%3F")
+ |> String.replace(":", "%3A")
+
[base_url, "media", path]
|> Path.join()
end
diff --git a/lib/pleroma/upload/filter/dedupe.ex b/lib/pleroma/upload/filter/dedupe.ex
index 8fcce320f..e4c225833 100644
--- a/lib/pleroma/upload/filter/dedupe.ex
+++ b/lib/pleroma/upload/filter/dedupe.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.Upload.Filter.Dedupe do
@behaviour Pleroma.Upload.Filter
alias Pleroma.Upload
- def filter(upload = %Upload{name: name}) do
+ def filter(%Upload{name: name} = upload) do
extension = String.split(name, ".") |> List.last()
shasum = :crypto.hash(:sha256, File.read!(upload.tempfile)) |> Base.encode16(case: :lower)
filename = shasum <> "." <> extension
diff --git a/lib/pleroma/uploaders/mdii.ex b/lib/pleroma/uploaders/mdii.ex
index 530b34362..190ed9f3a 100644
--- a/lib/pleroma/uploaders/mdii.ex
+++ b/lib/pleroma/uploaders/mdii.ex
@@ -24,7 +24,8 @@ defmodule Pleroma.Uploaders.MDII do
extension = String.split(upload.name, ".") |> List.last()
query = "#{cgi}?#{extension}"
- with {:ok, %{status: 200, body: body}} <- @httpoison.post(query, file_data) do
+ with {:ok, %{status: 200, body: body}} <-
+ @httpoison.post(query, file_data, [], adapter: [pool: :default]) do
remote_file_name = String.split(body) |> List.first()
public_url = "#{files}/#{remote_file_name}.#{extension}"
{:ok, {:url, public_url}}
diff --git a/lib/pleroma/uploaders/s3.ex b/lib/pleroma/uploaders/s3.ex
index 108cf06b5..0038ba01f 100644
--- a/lib/pleroma/uploaders/s3.ex
+++ b/lib/pleroma/uploaders/s3.ex
@@ -9,17 +9,25 @@ defmodule Pleroma.Uploaders.S3 do
# The file name is re-encoded with S3's constraints here to comply with previous links with less strict filenames
def get_file(file) do
config = Pleroma.Config.get([__MODULE__])
+ bucket = Keyword.fetch!(config, :bucket)
+
+ bucket_with_namespace =
+ if namespace = Keyword.get(config, :bucket_namespace) do
+ namespace <> ":" <> bucket
+ else
+ bucket
+ end
{:ok,
{:url,
Path.join([
Keyword.fetch!(config, :public_endpoint),
- Keyword.fetch!(config, :bucket),
+ bucket_with_namespace,
strict_encode(URI.decode(file))
])}}
end
- def put_file(upload = %Pleroma.Upload{}) do
+ def put_file(%Pleroma.Upload{} = upload) do
config = Pleroma.Config.get([__MODULE__])
bucket = Keyword.get(config, :bucket)
diff --git a/lib/pleroma/uploaders/uploader.ex b/lib/pleroma/uploaders/uploader.ex
index 0959d7a3e..ce83cbbbc 100644
--- a/lib/pleroma/uploaders/uploader.ex
+++ b/lib/pleroma/uploaders/uploader.ex
@@ -27,18 +27,47 @@ defmodule Pleroma.Uploaders.Uploader do
This allows to correctly proxy or redirect requests to the backend, while allowing to migrate backends without breaking any URL.
* `{url, url :: String.t}` to bypass `get_file/2` and use the `url` directly in the activity.
* `{:error, String.t}` error information if the file failed to be saved to the backend.
+ * `:wait_callback` will wait for an http post request at `/api/pleroma/upload_callback/:upload_path` and call the uploader's `http_callback/3` method.
"""
+ @type file_spec :: {:file | :url, String.t()}
@callback put_file(Pleroma.Upload.t()) ::
- :ok | {:ok, {:file | :url, String.t()}} | {:error, String.t()}
+ :ok | {:ok, file_spec()} | {:error, String.t()} | :wait_callback
+
+ @callback http_callback(Plug.Conn.t(), Map.t()) ::
+ {:ok, Plug.Conn.t()}
+ | {:ok, Plug.Conn.t(), file_spec()}
+ | {:error, Plug.Conn.t(), String.t()}
+ @optional_callbacks http_callback: 2
+
+ @spec put_file(module(), Pleroma.Upload.t()) :: {:ok, file_spec()} | {:error, String.t()}
- @spec put_file(module(), Pleroma.Upload.t()) ::
- {:ok, {:file | :url, String.t()}} | {:error, String.t()}
def put_file(uploader, upload) do
case uploader.put_file(upload) do
:ok -> {:ok, {:file, upload.path}}
- other -> other
+ :wait_callback -> handle_callback(uploader, upload)
+ {:ok, _} = ok -> ok
+ {:error, _} = error -> error
+ end
+ end
+
+ defp handle_callback(uploader, upload) do
+ :global.register_name({__MODULE__, upload.path}, self())
+
+ receive do
+ {__MODULE__, pid, conn, params} ->
+ case uploader.http_callback(conn, params) do
+ {:ok, conn, ok} ->
+ send(pid, {__MODULE__, conn})
+ {:ok, ok}
+
+ {:error, conn, error} ->
+ send(pid, {__MODULE__, conn})
+ {:error, error}
+ end
+ after
+ 30_000 -> {:error, "Uploader callback timeout"}
end
end
end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 1edded415..3232cb842 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -5,18 +5,30 @@
defmodule Pleroma.User do
use Ecto.Schema
- import Ecto.{Changeset, Query}
- alias Pleroma.{Repo, User, Object, Web, Activity, Notification}
+ import Ecto.Changeset
+ import Ecto.Query
+
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Object
+ alias Pleroma.Web
+ alias Pleroma.Activity
+ alias Pleroma.Notification
alias Comeonin.Pbkdf2
alias Pleroma.Formatter
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
- alias Pleroma.Web.{OStatus, Websub, OAuth}
- alias Pleroma.Web.ActivityPub.{Utils, ActivityPub}
+ alias Pleroma.Web.OStatus
+ alias Pleroma.Web.Websub
+ alias Pleroma.Web.OAuth
+ alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.ActivityPub.ActivityPub
require Logger
@type t :: %__MODULE__{}
+ @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
+
@email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
@strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
@@ -35,8 +47,9 @@ defmodule Pleroma.User do
field(:avatar, :map)
field(:local, :boolean, default: true)
field(:follower_address, :string)
- field(:search_distance, :float, virtual: true)
+ field(:search_rank, :float, virtual: true)
field(:tags, {:array, :string}, default: [])
+ field(:bookmarks, {:array, :string}, default: [])
field(:last_refreshed_at, :naive_datetime)
has_many(:notifications, Notification)
embeds_one(:info, Pleroma.User.Info)
@@ -44,20 +57,28 @@ defmodule Pleroma.User do
timestamps()
end
- def auth_active?(%User{} = user) do
- (user.info && !user.info.confirmation_pending) ||
- !Pleroma.Config.get([:instance, :account_activation_required])
- end
+ def auth_active?(%User{local: false}), do: true
+
+ def auth_active?(%User{info: %User.Info{confirmation_pending: false}}), do: true
+
+ def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
+ do: !Pleroma.Config.get([:instance, :account_activation_required])
- def remote_or_auth_active?(%User{} = user), do: !user.local || auth_active?(user)
+ def auth_active?(_), do: false
- def visible_for?(%User{} = user, for_user \\ nil) do
- User.remote_or_auth_active?(user) || (for_user && for_user.id == user.id) ||
- User.superuser?(for_user)
+ def visible_for?(user, for_user \\ nil)
+
+ def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
+
+ def visible_for?(%User{} = user, for_user) do
+ auth_active?(user) || superuser?(for_user)
end
- def superuser?(nil), do: false
- def superuser?(%User{} = user), do: user.info && User.Info.superuser?(user.info)
+ def visible_for?(_, _), do: false
+
+ def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
+ def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
+ def superuser?(_), do: false
def avatar_url(user) do
case user.avatar do
@@ -85,12 +106,6 @@ defmodule Pleroma.User do
"#{ap_id(user)}/followers"
end
- def follow_changeset(struct, params \\ %{}) do
- struct
- |> cast(params, [:following])
- |> validate_required([:following])
- end
-
def user_info(%User{} = user) do
oneself = if user.local, do: 1, else: 0
@@ -229,10 +244,24 @@ defmodule Pleroma.User do
end
end
+ defp autofollow_users(user) do
+ candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
+
+ autofollowed_users =
+ from(u in User,
+ where: u.local == true,
+ where: u.nickname in ^candidates
+ )
+ |> Repo.all()
+
+ follow_all(user, autofollowed_users)
+ end
+
@doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset),
- {:ok, _} = try_send_confirmation_email(user) do
+ {:ok, user} <- autofollow_users(user),
+ {:ok, _} <- try_send_confirmation_email(user) do
{:ok, user}
end
end
@@ -282,6 +311,38 @@ defmodule Pleroma.User do
end
end
+ @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
+ @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
+ def follow_all(follower, followeds) do
+ followed_addresses =
+ followeds
+ |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
+ |> Enum.map(fn %{follower_address: fa} -> fa end)
+
+ q =
+ from(u in User,
+ where: u.id == ^follower.id,
+ update: [
+ set: [
+ following:
+ fragment(
+ "array(select distinct unnest (array_cat(?, ?)))",
+ u.following,
+ ^followed_addresses
+ )
+ ]
+ ]
+ )
+
+ {1, [follower]} = Repo.update_all(q, [], returning: true)
+
+ Enum.each(followeds, fn followed ->
+ update_follower_count(followed)
+ end)
+
+ set_cache(follower)
+ end
+
def 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)
@@ -300,18 +361,17 @@ defmodule Pleroma.User do
Websub.subscribe(follower, followed)
end
- following =
- [ap_followers | follower.following]
- |> Enum.uniq()
+ q =
+ from(u in User,
+ where: u.id == ^follower.id,
+ update: [push: [following: ^ap_followers]]
+ )
- follower =
- follower
- |> follow_changeset(%{following: following})
- |> update_and_set_cache
+ {1, [follower]} = Repo.update_all(q, [], returning: true)
{:ok, _} = update_follower_count(followed)
- follower
+ set_cache(follower)
end
end
@@ -319,17 +379,18 @@ defmodule Pleroma.User do
ap_followers = followed.follower_address
if following?(follower, followed) and follower.ap_id != followed.ap_id do
- following =
- follower.following
- |> List.delete(ap_followers)
+ q =
+ from(u in User,
+ where: u.id == ^follower.id,
+ update: [pull: [following: ^ap_followers]]
+ )
- {:ok, follower} =
- follower
- |> follow_changeset(%{following: following})
- |> update_and_set_cache
+ {1, [follower]} = Repo.update_all(q, [], returning: true)
{:ok, followed} = update_follower_count(followed)
+ set_cache(follower)
+
{:ok, follower, Utils.fetch_latest_follow(follower, followed)}
else
{:error, "Not subscribed!"}
@@ -363,16 +424,33 @@ defmodule Pleroma.User do
user.info.locked || false
end
+ def get_by_id(id) do
+ Repo.get_by(User, id: id)
+ end
+
def get_by_ap_id(ap_id) do
Repo.get_by(User, ap_id: ap_id)
end
+ # This is mostly an SPC migration fix. This guesses the user nickname (by taking the last part of the ap_id and the domain) and tries to get that user
+ def get_by_guessed_nickname(ap_id) do
+ domain = URI.parse(ap_id).host
+ name = List.last(String.split(ap_id, "/"))
+ nickname = "#{name}@#{domain}"
+
+ get_by_nickname(nickname)
+ end
+
+ def set_cache(user) do
+ Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
+ Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
+ Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
+ {:ok, user}
+ end
+
def update_and_set_cache(changeset) do
with {:ok, user} <- Repo.update(changeset) do
- Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
- Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
- Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
- {:ok, user}
+ set_cache(user)
else
e -> e
end
@@ -389,16 +467,37 @@ defmodule Pleroma.User do
Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
end
+ def get_cached_by_id(id) do
+ key = "id:#{id}"
+
+ ap_id =
+ Cachex.fetch!(:user_cache, key, fn _ ->
+ user = get_by_id(id)
+
+ if user do
+ Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
+ {:commit, user.ap_id}
+ else
+ {:ignore, ""}
+ end
+ end)
+
+ get_cached_by_ap_id(ap_id)
+ end
+
def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}"
Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
end
+ def get_cached_by_nickname_or_id(nickname_or_id) do
+ get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
+ end
+
def get_by_nickname(nickname) do
Repo.get_by(User, nickname: nickname) ||
if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
- [local_nickname, _] = String.split(nickname, "@")
- Repo.get_by(User, nickname: local_nickname)
+ Repo.get_by(User, nickname: local_nickname(nickname))
end
end
@@ -437,7 +536,7 @@ defmodule Pleroma.User do
end
end
- def get_followers_query(%User{id: id, follower_address: follower_address}) do
+ def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
from(
u in User,
where: fragment("? <@ ?", ^[follower_address], u.following),
@@ -445,13 +544,29 @@ defmodule Pleroma.User do
)
end
- def get_followers(user) do
- q = get_followers_query(user)
+ def get_followers_query(user, page) do
+ from(
+ u in get_followers_query(user, nil),
+ limit: 20,
+ offset: ^((page - 1) * 20)
+ )
+ end
+
+ def get_followers_query(user), do: get_followers_query(user, nil)
+
+ def get_followers(user, page \\ nil) do
+ q = get_followers_query(user, page)
{:ok, Repo.all(q)}
end
- def get_friends_query(%User{id: id, following: following}) do
+ def get_followers_ids(user, page \\ nil) do
+ q = get_followers_query(user, page)
+
+ Repo.all(from(u in q, select: u.id))
+ end
+
+ def get_friends_query(%User{id: id, following: following}, nil) do
from(
u in User,
where: u.follower_address in ^following,
@@ -459,12 +574,28 @@ defmodule Pleroma.User do
)
end
- def get_friends(user) do
- q = get_friends_query(user)
+ def get_friends_query(user, page) do
+ from(
+ u in get_friends_query(user, nil),
+ limit: 20,
+ offset: ^((page - 1) * 20)
+ )
+ end
+
+ def get_friends_query(user), do: get_friends_query(user, nil)
+
+ def get_friends(user, page \\ nil) do
+ q = get_friends_query(user, page)
{:ok, Repo.all(q)}
end
+ def get_friends_ids(user, page \\ nil) do
+ q = get_friends_query(user, page)
+
+ Repo.all(from(u in q, select: u.id))
+ end
+
def get_follow_requests_query(%User{} = user) do
from(
a in Activity,
@@ -596,37 +727,120 @@ defmodule Pleroma.User do
Repo.all(query)
end
- def search(query, resolve \\ false) do
- # strip the beginning @ off if there is a query
+ def search(query, resolve \\ false, for_user \\ nil) do
+ # Strip the beginning @ off if there is a query
query = String.trim_leading(query, "@")
- if resolve do
- User.get_or_fetch_by_nickname(query)
- end
+ if resolve, do: User.get_or_fetch_by_nickname(query)
- inner =
- from(
- u in User,
- select_merge: %{
- search_distance:
- fragment(
- "? <-> (? || ?)",
- ^query,
- u.nickname,
- u.name
- )
- },
- where: not is_nil(u.nickname)
- )
+ fts_results = do_search(fts_search_subquery(query), for_user)
+
+ {:ok, trigram_results} =
+ Repo.transaction(fn ->
+ Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
+ do_search(trigram_search_subquery(query), for_user)
+ end)
+
+ Enum.uniq_by(fts_results ++ trigram_results, & &1.id)
+ end
+ defp do_search(subquery, for_user, options \\ []) do
q =
from(
- s in subquery(inner),
- order_by: s.search_distance,
- limit: 20
+ s in subquery(subquery),
+ order_by: [desc: s.search_rank],
+ limit: ^(options[:limit] || 20)
)
- Repo.all(q)
+ results =
+ q
+ |> Repo.all()
+ |> Enum.filter(&(&1.search_rank > 0))
+
+ boost_search_results(results, for_user)
+ end
+
+ defp fts_search_subquery(query) do
+ processed_query =
+ query
+ |> String.replace(~r/\W+/, " ")
+ |> String.trim()
+ |> String.split()
+ |> Enum.map(&(&1 <> ":*"))
+ |> Enum.join(" | ")
+
+ from(
+ u in User,
+ select_merge: %{
+ search_rank:
+ fragment(
+ """
+ ts_rank_cd(
+ setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
+ setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
+ to_tsquery('simple', ?),
+ 32
+ )
+ """,
+ u.nickname,
+ u.name,
+ ^processed_query
+ )
+ },
+ where:
+ fragment(
+ """
+ (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
+ setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
+ """,
+ u.nickname,
+ u.name,
+ ^processed_query
+ )
+ )
+ end
+
+ defp trigram_search_subquery(query) do
+ from(
+ u in User,
+ select_merge: %{
+ search_rank:
+ fragment(
+ "similarity(?, trim(? || ' ' || coalesce(?, '')))",
+ ^query,
+ u.nickname,
+ u.name
+ )
+ },
+ where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^query)
+ )
+ end
+
+ defp boost_search_results(results, nil), do: results
+
+ defp boost_search_results(results, for_user) do
+ friends_ids = get_friends_ids(for_user)
+ followers_ids = get_followers_ids(for_user)
+
+ Enum.map(
+ results,
+ fn u ->
+ search_rank_coef =
+ cond do
+ u.id in friends_ids ->
+ 1.2
+
+ u.id in followers_ids ->
+ 1.1
+
+ true ->
+ 1
+ end
+
+ Map.put(u, :search_rank, u.search_rank * search_rank_coef)
+ end
+ )
+ |> Enum.sort_by(&(-&1.search_rank))
end
def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
@@ -726,7 +940,7 @@ defmodule Pleroma.User do
update_and_set_cache(cng)
end
- def local_user_query() do
+ def local_user_query do
from(
u in User,
where: u.local == true,
@@ -734,7 +948,14 @@ defmodule Pleroma.User do
)
end
- def moderator_user_query() do
+ def active_local_user_query do
+ from(
+ u in local_user_query(),
+ where: fragment("not (?->'deactivated' @> 'true')", u.info)
+ )
+ end
+
+ def moderator_user_query do
from(
u in User,
where: u.local == true,
@@ -920,7 +1141,7 @@ defmodule Pleroma.User do
end)
bio
- |> CommonUtils.format_input(mentions, tags, "text/plain")
+ |> CommonUtils.format_input(mentions, tags, "text/plain", user_links: [format: :full])
|> Formatter.emojify(emoji)
end
@@ -957,6 +1178,22 @@ defmodule Pleroma.User do
updated_user
end
+ def bookmark(%User{} = user, status_id) do
+ bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
+ update_bookmarks(user, bookmarks)
+ end
+
+ def unbookmark(%User{} = user, status_id) do
+ bookmarks = Enum.uniq(user.bookmarks -- [status_id])
+ update_bookmarks(user, bookmarks)
+ end
+
+ def update_bookmarks(%User{} = user, bookmarks) do
+ user
+ |> change(%{bookmarks: bookmarks})
+ |> update_and_set_cache
+ end
+
defp normalize_tags(tags) do
[tags]
|> List.flatten()
@@ -970,4 +1207,24 @@ defmodule Pleroma.User do
@strict_local_nickname_regex
end
end
+
+ def local_nickname(nickname_or_mention) do
+ nickname_or_mention
+ |> full_nickname()
+ |> String.split("@")
+ |> hd()
+ end
+
+ def full_nickname(nickname_or_mention),
+ do: String.trim_leading(nickname_or_mention, "@")
+
+ def error_user(ap_id) do
+ %User{
+ name: ap_id,
+ ap_id: ap_id,
+ info: %User.Info{},
+ nickname: "erroruser@example.com",
+ inserted_at: NaiveDateTime.utc_now()
+ }
+ end
end
diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex
index 2f419a5a2..9d8779fab 100644
--- a/lib/pleroma/user/info.ex
+++ b/lib/pleroma/user/info.ex
@@ -23,6 +23,7 @@ defmodule Pleroma.User.Info do
field(:ap_enabled, :boolean, default: false)
field(:is_moderator, :boolean, default: false)
field(:is_admin, :boolean, default: false)
+ field(:show_role, :boolean, default: true)
field(:keys, :string, default: nil)
field(:settings, :map, default: nil)
field(:magic_key, :string, default: nil)
@@ -30,7 +31,9 @@ defmodule Pleroma.User.Info do
field(:topic, :string, default: nil)
field(:hub, :string, default: nil)
field(:salmon, :string, default: nil)
- field(:hide_network, :boolean, default: false)
+ field(:hide_followers, :boolean, default: false)
+ field(:hide_follows, :boolean, default: false)
+ field(:pinned_activities, {:array, :string}, default: [])
# Found in the wild
# ap_id -> Where is this used?
@@ -41,8 +44,6 @@ defmodule Pleroma.User.Info do
# subject _> Where is this used?
end
- def superuser?(info), do: info.is_admin || info.is_moderator
-
def set_activation_status(info, deactivated) do
params = %{deactivated: deactivated}
@@ -144,8 +145,10 @@ defmodule Pleroma.User.Info do
:no_rich_text,
:default_scope,
:banner,
- :hide_network,
- :background
+ :hide_follows,
+ :hide_followers,
+ :background,
+ :show_role
])
end
@@ -195,7 +198,30 @@ defmodule Pleroma.User.Info do
info
|> cast(params, [
:is_moderator,
- :is_admin
+ :is_admin,
+ :show_role
])
end
+
+ def add_pinnned_activity(info, %Pleroma.Activity{id: id}) do
+ if id not in info.pinned_activities do
+ max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
+ params = %{pinned_activities: info.pinned_activities ++ [id]}
+
+ info
+ |> cast(params, [:pinned_activities])
+ |> validate_length(:pinned_activities,
+ max: max_pinned_statuses,
+ message: "You have already pinned the maximum number of statuses"
+ )
+ else
+ change(info)
+ end
+ end
+
+ def remove_pinnned_activity(info, %Pleroma.Activity{id: id}) do
+ params = %{pinned_activities: List.delete(info.pinned_activities, id)}
+
+ cast(info, params, [:pinned_activities])
+ end
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 4d754de13..c46d8233e 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -3,13 +3,22 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPub do
- alias Pleroma.{Activity, Repo, Object, Upload, User, Notification}
- alias Pleroma.Web.ActivityPub.{Transmogrifier, MRF}
+ alias Pleroma.Activity
+ alias Pleroma.Repo
+ alias Pleroma.Object
+ alias Pleroma.Upload
+ alias Pleroma.User
+ alias Pleroma.Notification
+ alias Pleroma.Instances
+ alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.WebFinger
alias Pleroma.Web.Federator
alias Pleroma.Web.OStatus
+
import Ecto.Query
import Pleroma.Web.ActivityPub.Utils
+
require Logger
@httpoison Application.get_env(:pleroma, :httpoison)
@@ -19,20 +28,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp get_recipients(%{"type" => "Announce"} = data) do
to = data["to"] || []
cc = data["cc"] || []
- recipients = to ++ cc
actor = User.get_cached_by_ap_id(data["actor"])
- recipients
- |> Enum.filter(fn recipient ->
- case User.get_cached_by_ap_id(recipient) do
- nil ->
- true
+ recipients =
+ (to ++ cc)
+ |> Enum.filter(fn recipient ->
+ case User.get_cached_by_ap_id(recipient) do
+ nil ->
+ true
+
+ user ->
+ User.following?(user, actor)
+ end
+ end)
- user ->
- User.following?(user, actor)
- end
- end)
+ {recipients, to, cc}
+ end
+ defp get_recipients(%{"type" => "Create"} = data) do
+ to = data["to"] || []
+ cc = data["cc"] || []
+ actor = data["actor"] || []
+ recipients = (to ++ cc ++ [actor]) |> Enum.uniq()
{recipients, to, cc}
end
@@ -56,7 +73,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
- defp check_remote_limit(%{"object" => %{"content" => content}}) do
+ defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do
limit = Pleroma.Config.get([:instance, :remote_limit])
String.length(content) <= limit
end
@@ -80,6 +97,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
recipients: recipients
})
+ Task.start(fn ->
+ Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
+ end)
+
Notification.create_notifications(activity)
stream_out(activity)
{:ok, activity}
@@ -92,7 +113,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def stream_out(activity) do
public = "https://www.w3.org/ns/activitystreams#Public"
- if activity.data["type"] in ["Create", "Announce"] do
+ if activity.data["type"] in ["Create", "Announce", "Delete"] do
Pleroma.Web.Streamer.stream("user", activity)
Pleroma.Web.Streamer.stream("list", activity)
@@ -103,16 +124,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
Pleroma.Web.Streamer.stream("public:local", activity)
end
- activity.data["object"]
- |> Map.get("tag", [])
- |> Enum.filter(fn tag -> is_bitstring(tag) end)
- |> Enum.map(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end)
+ if activity.data["type"] in ["Create"] do
+ activity.data["object"]
+ |> Map.get("tag", [])
+ |> Enum.filter(fn tag -> is_bitstring(tag) end)
+ |> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end)
- if activity.data["object"]["attachment"] != [] do
- Pleroma.Web.Streamer.stream("public:media", activity)
+ if activity.data["object"]["attachment"] != [] do
+ Pleroma.Web.Streamer.stream("public:media", activity)
- if activity.local do
- Pleroma.Web.Streamer.stream("public:local:media", activity)
+ if activity.local do
+ Pleroma.Web.Streamer.stream("public:local:media", activity)
+ end
end
end
else
@@ -138,8 +161,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
additional
),
{:ok, activity} <- insert(create_data, local),
- :ok <- maybe_federate(activity),
- {:ok, _actor} <- User.increase_note_count(actor) do
+ # Changing note count prior to enqueuing federation task in order to avoid race conditions on updating user.info
+ {:ok, _actor} <- User.increase_note_count(actor),
+ :ok <- maybe_federate(activity) do
{:ok, activity}
end
end
@@ -224,10 +248,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
%User{ap_id: _} = user,
%Object{data: %{"id" => _}} = object,
activity_id \\ nil,
- local \\ true
+ local \\ true,
+ public \\ true
) do
with true <- is_public?(object),
- announce_data <- make_announce_data(user, object, activity_id),
+ announce_data <- make_announce_data(user, object, activity_id, public),
{:ok, activity} <- insert(announce_data, local),
{:ok, object} <- add_announce_to_object(activity, object),
:ok <- maybe_federate(activity) do
@@ -285,8 +310,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
with {:ok, _} <- Object.delete(object),
{:ok, activity} <- insert(data, local),
- :ok <- maybe_federate(activity),
- {:ok, _actor} <- User.decrease_note_count(user) do
+ # Changing note count prior to enqueuing federation task in order to avoid race conditions on updating user.info
+ {:ok, _actor} <- User.decrease_note_count(user),
+ :ok <- maybe_federate(activity) do
{:ok, activity}
end
end
@@ -364,21 +390,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
@valid_visibilities ~w[direct unlisted public private]
- defp restrict_visibility(query, %{visibility: "direct"}) do
- public = "https://www.w3.org/ns/activitystreams#Public"
+ defp restrict_visibility(query, %{visibility: visibility})
+ when visibility in @valid_visibilities do
+ query =
+ from(
+ a in query,
+ where:
+ fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility)
+ )
- from(
- activity in query,
- join: sender in User,
- on: sender.ap_id == activity.actor,
- # Are non-direct statuses with no to/cc possible?
- where:
- fragment(
- "not (? && ?)",
- [^public, sender.follower_address],
- activity.recipients
- )
- )
+ Ecto.Adapters.SQL.to_sql(:all, Repo, query)
+
+ query
end
defp restrict_visibility(_query, %{visibility: visibility})
@@ -394,6 +417,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Map.put("type", ["Create", "Announce"])
|> Map.put("actor_id", user.ap_id)
|> Map.put("whole_db", true)
+ |> Map.put("pinned_activity_ids", user.info.pinned_activities)
recipients =
if reading_user do
@@ -407,13 +431,42 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Enum.reverse()
end
+ defp restrict_since(query, %{"since_id" => ""}), do: query
+
defp restrict_since(query, %{"since_id" => since_id}) do
from(activity in query, where: activity.id > ^since_id)
end
defp restrict_since(query, _), do: query
- defp restrict_tag(query, %{"tag" => tag}) do
+ defp restrict_tag_reject(query, %{"tag_reject" => tag_reject})
+ when is_list(tag_reject) and tag_reject != [] do
+ from(
+ activity in query,
+ where: fragment("(not (? #> '{\"object\",\"tag\"}') \\?| ?)", activity.data, ^tag_reject)
+ )
+ end
+
+ defp restrict_tag_reject(query, _), do: query
+
+ defp restrict_tag_all(query, %{"tag_all" => tag_all})
+ when is_list(tag_all) and tag_all != [] do
+ from(
+ activity in query,
+ where: fragment("(? #> '{\"object\",\"tag\"}') \\?& ?", activity.data, ^tag_all)
+ )
+ end
+
+ defp restrict_tag_all(query, _), do: query
+
+ defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do
+ from(
+ activity in query,
+ where: fragment("(? #> '{\"object\",\"tag\"}') \\?| ?", activity.data, ^tag)
+ )
+ end
+
+ defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do
from(
activity in query,
where: fragment("? <@ (? #> '{\"object\",\"tag\"}')", ^tag, activity.data)
@@ -462,6 +515,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_local(query, _), do: query
+ defp restrict_max(query, %{"max_id" => ""}), do: query
+
defp restrict_max(query, %{"max_id" => max_id}) do
from(activity in query, where: activity.id < ^max_id)
end
@@ -475,7 +530,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_actor(query, _), do: query
defp restrict_type(query, %{"type" => type}) when is_binary(type) do
- restrict_type(query, %{"type" => [type]})
+ from(activity in query, where: fragment("?->>'type' = ?", activity.data, ^type))
end
defp restrict_type(query, %{"type" => type}) do
@@ -517,15 +572,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_reblogs(query, _), do: query
- # Only search through last 100_000 activities by default
- defp restrict_recent(query, %{"whole_db" => true}), do: query
-
- defp restrict_recent(query, _) do
- since = (Repo.aggregate(Activity, :max, :id) || 0) - 100_000
-
- from(activity in query, where: activity.id > ^since)
- end
-
defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
blocks = info.blocks || []
domain_blocks = info.domain_blocks || []
@@ -552,6 +598,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
)
end
+ defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids}) do
+ from(activity in query, where: activity.id in ^ids)
+ end
+
+ defp restrict_pinned(query, _), do: query
+
def fetch_activities_query(recipients, opts \\ %{}) do
base_query =
from(
@@ -563,6 +615,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
base_query
|> restrict_recipients(recipients, opts["user"])
|> restrict_tag(opts)
+ |> restrict_tag_reject(opts)
+ |> restrict_tag_all(opts)
|> restrict_since(opts)
|> restrict_local(opts)
|> restrict_limit(opts)
@@ -570,12 +624,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_actor(opts)
|> restrict_type(opts)
|> restrict_favorited_by(opts)
- |> restrict_recent(opts)
|> restrict_blocked(opts)
|> restrict_media(opts)
|> restrict_visibility(opts)
|> restrict_replies(opts)
|> restrict_reblogs(opts)
+ |> restrict_pinned(opts)
end
def fetch_activities(recipients, opts \\ %{}) do
@@ -689,7 +743,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
def publish(actor, activity) do
- followers =
+ remote_followers =
if actor.follower_address in activity.recipients do
{:ok, followers} = User.get_followers(actor)
followers |> Enum.filter(&(!&1.local))
@@ -699,29 +753,31 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
public = is_public?(activity)
- remote_inboxes =
- (Pleroma.Web.Salmon.remote_users(activity) ++ followers)
+ reachable_inboxes_metadata =
+ (Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers)
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
|> Enum.map(fn %{info: %{source_data: data}} ->
(is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
end)
|> Enum.uniq()
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
+ |> Instances.filter_reachable()
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
json = Jason.encode!(data)
- Enum.each(remote_inboxes, fn inbox ->
+ Enum.each(reachable_inboxes_metadata, fn {inbox, unreachable_since} ->
Federator.enqueue(:publish_single_ap, %{
inbox: inbox,
json: json,
actor: actor,
- id: activity.data["id"]
+ id: activity.data["id"],
+ unreachable_since: unreachable_since
})
end)
end
- def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do
+ def publish_one(%{inbox: inbox, json: json, actor: actor, id: id} = params) do
Logger.info("Federating #{id} to #{inbox}")
host = URI.parse(inbox).host
@@ -734,15 +790,26 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
digest: digest
})
- @httpoison.post(
- inbox,
- json,
- [
- {"Content-Type", "application/activity+json"},
- {"signature", signature},
- {"digest", digest}
- ]
- )
+ with {:ok, %{status: code}} when code in 200..299 <-
+ result =
+ @httpoison.post(
+ inbox,
+ json,
+ [
+ {"Content-Type", "application/activity+json"},
+ {"signature", signature},
+ {"digest", digest}
+ ]
+ ) do
+ if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
+ do: Instances.set_reachable(inbox)
+
+ result
+ else
+ {_post_result, response} ->
+ unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
+ {:error, response}
+ end
end
# TODO:
@@ -801,9 +868,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
- def is_public?(activity) do
- "https://www.w3.org/ns/activitystreams#Public" in (activity.data["to"] ++
- (activity.data["cc"] || []))
+ def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false
+ def is_public?(%Object{data: data}), do: is_public?(data)
+ def is_public?(%Activity{data: data}), do: is_public?(data)
+ def is_public?(%{"directMessage" => true}), do: false
+
+ def is_public?(data) do
+ "https://www.w3.org/ns/activitystreams#Public" in (data["to"] ++ (data["cc"] || []))
+ end
+
+ def is_private?(activity) do
+ !is_public?(activity) && Enum.any?(activity.data["to"], &String.contains?(&1, "/followers"))
+ end
+
+ def is_direct?(%Activity{data: %{"directMessage" => true}}), do: true
+ def is_direct?(%Object{data: %{"directMessage" => true}}), do: true
+
+ def is_direct?(activity) do
+ !is_public?(activity) && !is_private?(activity)
end
def visible_for_user?(activity, nil) do
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index fc7972eaf..69879476e 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -4,12 +4,16 @@
defmodule Pleroma.Web.ActivityPub.ActivityPubController do
use Pleroma.Web, :controller
- alias Pleroma.{Activity, User, Object}
- alias Pleroma.Web.ActivityPub.{ObjectView, UserView}
+
+ alias Pleroma.Activity
+ alias Pleroma.User
+ alias Pleroma.Object
+ alias Pleroma.Web.ActivityPub.ObjectView
+ alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
- alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Federator
require Logger
@@ -17,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
action_fallback(:errors)
plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay])
+ plug(:set_requester_reachable when action in [:inbox])
plug(:relay_active? when action in [:relay])
def relay_active?(conn, _) do
@@ -54,6 +59,49 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
end
+ def object_likes(conn, %{"uuid" => uuid, "page" => page}) do
+ with ap_id <- o_status_url(conn, :object, uuid),
+ %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
+ {_, true} <- {:public?, ActivityPub.is_public?(object)},
+ likes <- Utils.get_object_likes(object) do
+ {page, _} = Integer.parse(page)
+
+ conn
+ |> put_resp_header("content-type", "application/activity+json")
+ |> json(ObjectView.render("likes.json", ap_id, likes, page))
+ else
+ {:public?, false} ->
+ {:error, :not_found}
+ end
+ end
+
+ def object_likes(conn, %{"uuid" => uuid}) do
+ with ap_id <- o_status_url(conn, :object, uuid),
+ %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
+ {_, true} <- {:public?, ActivityPub.is_public?(object)},
+ likes <- Utils.get_object_likes(object) do
+ conn
+ |> put_resp_header("content-type", "application/activity+json")
+ |> json(ObjectView.render("likes.json", ap_id, likes))
+ else
+ {:public?, false} ->
+ {:error, :not_found}
+ end
+ end
+
+ def activity(conn, %{"uuid" => uuid}) do
+ with ap_id <- o_status_url(conn, :activity, uuid),
+ %Activity{} = activity <- Activity.normalize(ap_id),
+ {_, true} <- {:public?, ActivityPub.is_public?(activity)} do
+ conn
+ |> put_resp_header("content-type", "application/activity+json")
+ |> json(ObjectView.render("object.json", %{object: activity}))
+ else
+ {:public?, false} ->
+ {:error, :not_found}
+ end
+ end
+
def following(conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
@@ -153,6 +201,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
end
+ def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
+ conn
+ |> put_resp_header("content-type", "application/activity+json")
+ |> json(UserView.render("user.json", %{user: user}))
+ end
+
+ def whoami(_conn, _params), do: {:error, :not_found}
+
def read_inbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = params) do
if nickname == user.nickname do
conn
@@ -165,9 +221,48 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
end
+ def handle_user_activity(user, %{"type" => "Create"} = params) do
+ object =
+ params["object"]
+ |> Map.merge(Map.take(params, ["to", "cc"]))
+ |> Map.put("attributedTo", user.ap_id())
+ |> Transmogrifier.fix_object()
+
+ ActivityPub.create(%{
+ to: params["to"],
+ actor: user,
+ context: object["context"],
+ object: object,
+ additional: Map.take(params, ["cc"])
+ })
+ end
+
+ def handle_user_activity(user, %{"type" => "Delete"} = params) do
+ with %Object{} = object <- Object.normalize(params["object"]),
+ true <- user.info.is_moderator || user.ap_id == object.data["actor"],
+ {:ok, delete} <- ActivityPub.delete(object) do
+ {:ok, delete}
+ else
+ _ -> {:error, "Can't delete object"}
+ end
+ end
+
+ def handle_user_activity(user, %{"type" => "Like"} = params) do
+ with %Object{} = object <- Object.normalize(params["object"]),
+ {:ok, activity, _object} <- ActivityPub.like(user, object) do
+ {:ok, activity}
+ else
+ _ -> {:error, "Can't like object"}
+ end
+ end
+
+ def handle_user_activity(_, _) do
+ {:error, "Unhandled activity type"}
+ end
+
def update_outbox(
%{assigns: %{user: user}} = conn,
- %{"nickname" => nickname, "type" => "Create"} = params
+ %{"nickname" => nickname} = params
) do
if nickname == user.nickname do
actor = user.ap_id()
@@ -178,24 +273,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> Map.put("actor", actor)
|> Transmogrifier.fix_addressing()
- object =
- params["object"]
- |> Map.merge(Map.take(params, ["to", "cc"]))
- |> Map.put("attributedTo", actor)
- |> Transmogrifier.fix_object()
-
- with {:ok, %Activity{} = activity} <-
- ActivityPub.create(%{
- to: params["to"],
- actor: user,
- context: object["context"],
- object: object,
- additional: Map.take(params, ["cc"])
- }) do
+ with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
conn
|> put_status(:created)
|> put_resp_header("location", activity.data["id"])
|> json(activity.data)
+ else
+ {:error, message} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(message)
end
else
conn
@@ -215,4 +302,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> put_status(500)
|> json("error")
end
+
+ defp set_requester_reachable(%Plug.Conn{} = conn, _) do
+ with actor <- conn.params["actor"],
+ true <- is_binary(actor) do
+ Pleroma.Instances.set_reachable(actor)
+ end
+
+ conn
+ end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
new file mode 100644
index 000000000..7c6ad582a
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
@@ -0,0 +1,57 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
+ alias Pleroma.User
+
+ @behaviour Pleroma.Web.ActivityPub.MRF
+
+ # XXX: this should become User.normalize_by_ap_id() or similar, really.
+ defp normalize_by_ap_id(%{"id" => id}), do: User.get_cached_by_ap_id(id)
+ defp normalize_by_ap_id(uri) when is_binary(uri), do: User.get_cached_by_ap_id(uri)
+ defp normalize_by_ap_id(_), do: nil
+
+ defp score_nickname("followbot@" <> _), do: 1.0
+ defp score_nickname("federationbot@" <> _), do: 1.0
+ defp score_nickname("federation_bot@" <> _), do: 1.0
+ defp score_nickname(_), do: 0.0
+
+ defp score_displayname("federation bot"), do: 1.0
+ defp score_displayname("federationbot"), do: 1.0
+ defp score_displayname("fedibot"), do: 1.0
+ defp score_displayname(_), do: 0.0
+
+ defp determine_if_followbot(%User{nickname: nickname, name: displayname}) do
+ nick_score =
+ nickname
+ |> String.downcase()
+ |> score_nickname()
+
+ name_score =
+ displayname
+ |> String.downcase()
+ |> score_displayname()
+
+ nick_score + name_score
+ end
+
+ defp determine_if_followbot(_), do: 0.0
+
+ @impl true
+ def filter(%{"type" => "Follow", "actor" => actor_id} = message) do
+ %User{} = actor = normalize_by_ap_id(actor_id)
+
+ score = determine_if_followbot(actor)
+
+ # TODO: scan biography data for keywords and score it somehow.
+ if score < 0.8 do
+ {:ok, message}
+ else
+ {:reject, nil}
+ end
+ end
+
+ @impl true
+ def filter(message), do: {:ok, message}
+end
diff --git a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
index a3f516ae7..4c6e612b2 100644
--- a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
@@ -3,20 +3,46 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
+ alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF
+ defp delist_message(message) do
+ follower_collection = User.get_cached_by_ap_id(message["actor"]).follower_address
+
+ message
+ |> Map.put("to", [follower_collection])
+ |> Map.put("cc", ["https://www.w3.org/ns/activitystreams#Public"])
+ end
+
@impl true
- def filter(%{"type" => "Create"} = object) do
- threshold = Pleroma.Config.get([:mrf_hellthread, :threshold])
- recipients = (object["to"] || []) ++ (object["cc"] || [])
-
- if length(recipients) > threshold do
- {:reject, nil}
- else
- {:ok, object}
+ def filter(%{"type" => "Create"} = message) do
+ delist_threshold = Pleroma.Config.get([:mrf_hellthread, :delist_threshold])
+
+ reject_threshold =
+ Pleroma.Config.get(
+ [:mrf_hellthread, :reject_threshold],
+ Pleroma.Config.get([:mrf_hellthread, :threshold])
+ )
+
+ recipients = (message["to"] || []) ++ (message["cc"] || [])
+
+ cond do
+ length(recipients) > reject_threshold and reject_threshold > 0 ->
+ {:reject, nil}
+
+ length(recipients) > delist_threshold and delist_threshold > 0 ->
+ if Enum.member?(message["to"], "https://www.w3.org/ns/activitystreams#Public") or
+ Enum.member?(message["cc"], "https://www.w3.org/ns/activitystreams#Public") do
+ {:ok, delist_message(message)}
+ else
+ {:ok, message}
+ end
+
+ true ->
+ {:ok, message}
end
end
@impl true
- def filter(object), do: {:ok, object}
+ def filter(message), do: {:ok, message}
end
diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
new file mode 100644
index 000000000..5fdc03414
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
@@ -0,0 +1,81 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
+ @behaviour Pleroma.Web.ActivityPub.MRF
+ defp string_matches?(string, pattern) when is_binary(pattern) do
+ String.contains?(string, pattern)
+ end
+
+ defp string_matches?(string, pattern) do
+ String.match?(string, pattern)
+ end
+
+ defp check_reject(%{"object" => %{"content" => content, "summary" => summary}} = message) do
+ if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
+ string_matches?(content, pattern) or string_matches?(summary, pattern)
+ end) do
+ {:reject, nil}
+ else
+ {:ok, message}
+ end
+ end
+
+ defp check_ftl_removal(
+ %{"to" => to, "object" => %{"content" => content, "summary" => summary}} = message
+ ) do
+ if "https://www.w3.org/ns/activitystreams#Public" in to and
+ Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern ->
+ string_matches?(content, pattern) or string_matches?(summary, pattern)
+ end) do
+ to = List.delete(to, "https://www.w3.org/ns/activitystreams#Public")
+ cc = ["https://www.w3.org/ns/activitystreams#Public" | message["cc"] || []]
+
+ message =
+ message
+ |> Map.put("to", to)
+ |> Map.put("cc", cc)
+
+ {:ok, message}
+ else
+ {:ok, message}
+ end
+ end
+
+ defp check_replace(%{"object" => %{"content" => content, "summary" => summary}} = message) do
+ {content, summary} =
+ Enum.reduce(Pleroma.Config.get([:mrf_keyword, :replace]), {content, summary}, fn {pattern,
+ replacement},
+ {content_acc,
+ summary_acc} ->
+ {String.replace(content_acc, pattern, replacement),
+ String.replace(summary_acc, pattern, replacement)}
+ end)
+
+ {:ok,
+ message
+ |> put_in(["object", "content"], content)
+ |> put_in(["object", "summary"], summary)}
+ end
+
+ @impl true
+ def filter(%{"object" => %{"content" => nil}} = message) do
+ {:ok, message}
+ end
+
+ @impl true
+ def filter(%{"type" => "Create", "object" => %{"content" => _content}} = message) do
+ with {:ok, message} <- check_reject(message),
+ {:ok, message} <- check_ftl_removal(message),
+ {:ok, message} <- check_replace(message) do
+ {:ok, message}
+ else
+ _e ->
+ {:reject, nil}
+ end
+ end
+
+ @impl true
+ def filter(message), do: {:ok, message}
+end
diff --git a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex
new file mode 100644
index 000000000..081456046
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex
@@ -0,0 +1,29 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do
+ @behaviour Pleroma.Web.ActivityPub.MRF
+
+ @impl true
+ def filter(
+ %{
+ "type" => "Create",
+ "object" => %{"content" => content, "attachment" => _attachment} = child_object
+ } = object
+ )
+ when content in [".", "<p>.</p>"] do
+ child_object =
+ child_object
+ |> Map.put("content", "")
+
+ object =
+ object
+ |> Map.put("object", child_object)
+
+ {:ok, object}
+ end
+
+ @impl true
+ def filter(object), do: {:ok, object}
+end
diff --git a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex
new file mode 100644
index 000000000..b242e44e6
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex
@@ -0,0 +1,139 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
+ alias Pleroma.User
+ @behaviour Pleroma.Web.ActivityPub.MRF
+
+ defp get_tags(%User{tags: tags}) when is_list(tags), do: tags
+ defp get_tags(_), do: []
+
+ defp process_tag(
+ "mrf_tag:media-force-nsfw",
+ %{"type" => "Create", "object" => %{"attachment" => child_attachment} = object} = message
+ )
+ when length(child_attachment) > 0 do
+ tags = (object["tag"] || []) ++ ["nsfw"]
+
+ object =
+ object
+ |> Map.put("tags", tags)
+ |> Map.put("sensitive", true)
+
+ message = Map.put(message, "object", object)
+
+ {:ok, message}
+ end
+
+ defp process_tag(
+ "mrf_tag:media-strip",
+ %{"type" => "Create", "object" => %{"attachment" => child_attachment} = object} = message
+ )
+ when length(child_attachment) > 0 do
+ object = Map.delete(object, "attachment")
+ message = Map.put(message, "object", object)
+
+ {:ok, message}
+ end
+
+ defp process_tag(
+ "mrf_tag:force-unlisted",
+ %{"type" => "Create", "to" => to, "cc" => cc, "actor" => actor} = message
+ ) do
+ user = User.get_cached_by_ap_id(actor)
+
+ if Enum.member?(to, "https://www.w3.org/ns/activitystreams#Public") do
+ to =
+ List.delete(to, "https://www.w3.org/ns/activitystreams#Public") ++ [user.follower_address]
+
+ cc =
+ List.delete(cc, user.follower_address) ++ ["https://www.w3.org/ns/activitystreams#Public"]
+
+ object =
+ message["object"]
+ |> Map.put("to", to)
+ |> Map.put("cc", cc)
+
+ message =
+ message
+ |> Map.put("to", to)
+ |> Map.put("cc", cc)
+ |> Map.put("object", object)
+
+ {:ok, message}
+ else
+ {:ok, message}
+ end
+ end
+
+ defp process_tag(
+ "mrf_tag:sandbox",
+ %{"type" => "Create", "to" => to, "cc" => cc, "actor" => actor} = message
+ ) do
+ user = User.get_cached_by_ap_id(actor)
+
+ if Enum.member?(to, "https://www.w3.org/ns/activitystreams#Public") or
+ Enum.member?(cc, "https://www.w3.org/ns/activitystreams#Public") do
+ to =
+ List.delete(to, "https://www.w3.org/ns/activitystreams#Public") ++ [user.follower_address]
+
+ cc = List.delete(cc, "https://www.w3.org/ns/activitystreams#Public")
+
+ object =
+ message["object"]
+ |> Map.put("to", to)
+ |> Map.put("cc", cc)
+
+ message =
+ message
+ |> Map.put("to", to)
+ |> Map.put("cc", cc)
+ |> Map.put("object", object)
+
+ {:ok, message}
+ else
+ {:ok, message}
+ end
+ end
+
+ defp process_tag(
+ "mrf_tag:disable-remote-subscription",
+ %{"type" => "Follow", "actor" => actor} = message
+ ) do
+ user = User.get_cached_by_ap_id(actor)
+
+ if user.local == true do
+ {:ok, message}
+ else
+ {:reject, nil}
+ end
+ end
+
+ defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow"}), do: {:reject, nil}
+
+ defp process_tag(_, message), do: {:ok, message}
+
+ def filter_message(actor, message) do
+ User.get_cached_by_ap_id(actor)
+ |> get_tags()
+ |> Enum.reduce({:ok, message}, fn
+ tag, {:ok, message} ->
+ process_tag(tag, message)
+
+ _, error ->
+ error
+ end)
+ end
+
+ @impl true
+ def filter(%{"object" => target_actor, "type" => "Follow"} = message),
+ do: filter_message(target_actor, message)
+
+ @impl true
+ def filter(%{"actor" => actor, "type" => "Create"} = message),
+ do: filter_message(actor, message)
+
+ @impl true
+ def filter(message), do: {:ok, message}
+end
diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex
index abddbc790..c496063ea 100644
--- a/lib/pleroma/web/activity_pub/relay.ex
+++ b/lib/pleroma/web/activity_pub/relay.ex
@@ -3,7 +3,9 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Relay do
- alias Pleroma.{User, Object, Activity}
+ alias Pleroma.User
+ alias Pleroma.Object
+ alias Pleroma.Activity
alias Pleroma.Web.ActivityPub.ActivityPub
require Logger
@@ -40,7 +42,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do
def publish(%Activity{data: %{"type" => "Create"}} = activity) do
with %User{} = user <- get_actor(),
%Object{} = object <- Object.normalize(activity.data["object"]["id"]) do
- ActivityPub.announce(user, object)
+ ActivityPub.announce(user, object, nil, true, false)
else
e -> Logger.error("error: #{inspect(e)}")
end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 87b7fc07f..98a2af819 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -6,9 +6,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
@moduledoc """
A module to handle coding from internal to wire ActivityPub and back.
"""
+ alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.Object
- alias Pleroma.Activity
alias Pleroma.Repo
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
@@ -93,12 +93,47 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
- def fix_addressing(map) do
- map
+ def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mentions) do
+ explicit_to =
+ to
+ |> Enum.filter(fn x -> x in explicit_mentions end)
+
+ explicit_cc =
+ to
+ |> Enum.filter(fn x -> x not in explicit_mentions end)
+
+ final_cc =
+ (cc ++ explicit_cc)
+ |> Enum.uniq()
+
+ object
+ |> Map.put("to", explicit_to)
+ |> Map.put("cc", final_cc)
+ end
+
+ def fix_explicit_addressing(object, _explicit_mentions), do: object
+
+ # if directMessage flag is set to true, leave the addressing alone
+ def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
+
+ def fix_explicit_addressing(object) do
+ explicit_mentions =
+ object
+ |> Utils.determine_explicit_mentions()
+
+ explicit_mentions = explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public"]
+
+ object
+ |> fix_explicit_addressing(explicit_mentions)
+ end
+
+ def fix_addressing(object) do
+ object
|> fix_addressing_list("to")
|> fix_addressing_list("cc")
|> fix_addressing_list("bto")
|> fix_addressing_list("bcc")
+ |> fix_explicit_addressing
end
def fix_actor(%{"attributedTo" => actor} = object) do
@@ -106,11 +141,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("actor", get_actor(%{"actor" => actor}))
end
- def fix_likes(%{"likes" => likes} = object)
- when is_bitstring(likes) do
- # Check for standardisation
- # This is what Peertube does
- # curl -H 'Accept: application/activity+json' $likes | jq .totalItems
+ # Check for standardisation
+ # This is what Peertube does
+ # curl -H 'Accept: application/activity+json' $likes | jq .totalItems
+ # Prismo returns only an integer (count) as "likes"
+ def fix_likes(%{"likes" => likes} = object) when not is_map(likes) do
object
|> Map.put("likes", [])
|> Map.put("like_count", 0)
@@ -141,7 +176,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
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
+ Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
object
|> Map.put("inReplyTo", replied_object.data["id"])
|> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
@@ -278,6 +313,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("tag", combined)
end
+ def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
+
def fix_tag(object), do: object
# content map usually only has one language so this will do for now.
@@ -334,7 +371,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
Map.put(data, "actor", actor)
|> fix_addressing
- with nil <- Activity.get_create_activity_by_object_ap_id(object["id"]),
+ with nil <- Activity.get_create_by_object_ap_id(object["id"]),
%User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do
object = fix_object(data["object"])
@@ -348,6 +385,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
additional:
Map.take(data, [
"cc",
+ "directMessage",
"id"
])
}
@@ -417,9 +455,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{: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(%{
+ ActivityPub.reject(%{
to: follow_activity.data["to"],
- type: "Accept",
+ type: "Reject",
actor: followed.ap_id,
object: follow_activity.data["id"],
local: false
@@ -451,7 +489,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
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
+ public <- ActivityPub.is_public?(data),
+ {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
{:ok, activity}
else
_e -> :error
@@ -629,6 +668,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> add_mention_tags
|> add_emoji_tags
|> add_attributed_to
+ |> add_likes
|> prepare_attachments
|> set_conversation
|> set_reply_to_uri
@@ -641,7 +681,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
# internal -> Mastodon
# """
- def prepare_outgoing(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
+ def prepare_outgoing(%{"type" => "Create", "object" => object} = data) do
object =
object
|> prepare_object
@@ -788,6 +828,22 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("attributedTo", attributedTo)
end
+ def add_likes(%{"id" => id, "like_count" => likes} = object) do
+ likes = %{
+ "id" => "#{id}/likes",
+ "first" => "#{id}/likes?page=1",
+ "type" => "OrderedCollection",
+ "totalItems" => likes
+ }
+
+ object
+ |> Map.put("likes", likes)
+ end
+
+ def add_likes(object) do
+ object
+ end
+
def prepare_attachments(object) do
attachments =
(object["attachment"] || [])
@@ -803,7 +859,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
defp strip_internal_fields(object) do
object
|> Map.drop([
- "likes",
"like_count",
"announcements",
"announcement_count",
@@ -847,15 +902,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
maybe_retire_websub(user.ap_id)
- # Only do this for recent activties, don't go through the whole db.
- # Only look at the last 1000 activities.
- since = (Repo.aggregate(Activity, :max, :id) || 0) - 1_000
-
q =
from(
a in Activity,
where: ^old_follower_address in a.recipients,
- where: a.id > ^since,
update: [
set: [
recipients:
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index b313996db..964e11c9d 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -3,11 +3,19 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Utils do
- alias Pleroma.{Repo, Web, Object, Activity, User, Notification}
+ alias Pleroma.Repo
+ alias Pleroma.Web
+ alias Pleroma.Object
+ alias Pleroma.Activity
+ alias Pleroma.User
+ alias Pleroma.Notification
alias Pleroma.Web.Router.Helpers
alias Pleroma.Web.Endpoint
- alias Ecto.{Changeset, UUID}
+ alias Ecto.Changeset
+ alias Ecto.UUID
+
import Ecto.Query
+
require Logger
@supported_object_types ["Article", "Note", "Video", "Page"]
@@ -25,6 +33,20 @@ defmodule Pleroma.Web.ActivityPub.Utils do
Map.put(params, "actor", get_ap_id(params["actor"]))
end
+ def determine_explicit_mentions(%{"tag" => tag} = _object) when is_list(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
+
+ def determine_explicit_mentions(%{"tag" => tag} = object) when is_map(tag) do
+ Map.put(object, "tag", [tag])
+ |> determine_explicit_mentions()
+ end
+
+ def determine_explicit_mentions(_), do: []
+
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
@@ -198,7 +220,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
# Update activities that already had this. Could be done in a seperate process.
# Alternatively, just don't do this and fetch the current object each time. Most
# could probably be taken from cache.
- relevant_activities = Activity.all_by_object_ap_id(id)
+ relevant_activities = Activity.get_all_create_by_object_ap_id(id)
Enum.map(relevant_activities, fn activity ->
new_activity_data = activity.data |> Map.put("object", object.data)
@@ -231,6 +253,27 @@ defmodule Pleroma.Web.ActivityPub.Utils do
Repo.one(query)
end
+ @doc """
+ Returns like activities targeting an object
+ """
+ def get_object_likes(%{data: %{"id" => id}}) do
+ query =
+ from(
+ activity in Activity,
+ # this is to use the index
+ where:
+ fragment(
+ "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
+ activity.data,
+ activity.data,
+ ^id
+ ),
+ where: fragment("(?)->>'type' = 'Like'", activity.data)
+ )
+
+ Repo.all(query)
+ end
+
def make_like_data(%User{ap_id: ap_id} = actor, %{data: %{"id" => id}} = object, activity_id) do
data = %{
"type" => "Like",
@@ -250,7 +293,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> Map.put("#{property}_count", length(element))
|> Map.put("#{property}s", element),
changeset <- Changeset.change(object, data: new_data),
- {:ok, object} <- Repo.update(changeset),
+ {:ok, object} <- Object.update_and_set_cache(changeset),
_ <- update_object_in_activities(object) do
{:ok, object}
end
@@ -281,6 +324,25 @@ defmodule Pleroma.Web.ActivityPub.Utils do
@doc """
Updates a follow activity's state (for locked accounts).
"""
+ def update_follow_state(
+ %Activity{data: %{"actor" => actor, "object" => object, "state" => "pending"}} = activity,
+ state
+ ) do
+ try do
+ Ecto.Adapters.SQL.query!(
+ Repo,
+ "UPDATE activities SET data = jsonb_set(data, '{state}', $1) WHERE data->>'type' = 'Follow' AND data->>'actor' = $2 AND data->>'object' = $3 AND data->>'state' = 'pending'",
+ [state, actor, object]
+ )
+
+ activity = Repo.get(Activity, activity.id)
+ {:ok, activity}
+ rescue
+ e ->
+ {:error, e}
+ end
+ end
+
def update_follow_state(%Activity{} = activity, state) do
with new_data <-
activity.data
@@ -365,9 +427,10 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"""
# for relayed messages, we only want to send to subscribers
def make_announce_data(
- %User{ap_id: ap_id, nickname: nil} = user,
+ %User{ap_id: ap_id} = user,
%Object{data: %{"id" => id}} = object,
- activity_id
+ activity_id,
+ false
) do
data = %{
"type" => "Announce",
@@ -384,7 +447,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
def make_announce_data(
%User{ap_id: ap_id} = user,
%Object{data: %{"id" => id}} = object,
- activity_id
+ activity_id,
+ true
) do
data = %{
"type" => "Announce",
diff --git a/lib/pleroma/web/activity_pub/views/object_view.ex b/lib/pleroma/web/activity_pub/views/object_view.ex
index b5c9bf8d0..84fa94e32 100644
--- a/lib/pleroma/web/activity_pub/views/object_view.ex
+++ b/lib/pleroma/web/activity_pub/views/object_view.ex
@@ -4,7 +4,8 @@
defmodule Pleroma.Web.ActivityPub.ObjectView do
use Pleroma.Web, :view
- alias Pleroma.{Object, Activity}
+ alias Pleroma.Activity
+ alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Transmogrifier
def render("object.json", %{object: %Object{} = object}) do
@@ -35,4 +36,38 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
Map.merge(base, additional)
end
+
+ def render("likes.json", ap_id, likes, page) do
+ collection(likes, "#{ap_id}/likes", page)
+ |> Map.merge(Pleroma.Web.ActivityPub.Utils.make_json_ld_header())
+ end
+
+ def render("likes.json", ap_id, likes) do
+ %{
+ "id" => "#{ap_id}/likes",
+ "type" => "OrderedCollection",
+ "totalItems" => length(likes),
+ "first" => collection(likes, "#{ap_id}/likes", 1)
+ }
+ |> Map.merge(Pleroma.Web.ActivityPub.Utils.make_json_ld_header())
+ end
+
+ def collection(collection, iri, page) do
+ offset = (page - 1) * 10
+ items = Enum.slice(collection, offset, 10)
+ items = Enum.map(items, fn object -> Transmogrifier.prepare_object(object.data) end)
+ total = length(collection)
+
+ map = %{
+ "id" => "#{iri}?page=#{page}",
+ "type" => "OrderedCollectionPage",
+ "partOf" => iri,
+ "totalItems" => total,
+ "orderedItems" => items
+ }
+
+ if offset < total do
+ Map.put(map, "next", "#{iri}?page=#{page + 1}")
+ end
+ 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 fe8248107..c8e154989 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -4,15 +4,34 @@
defmodule Pleroma.Web.ActivityPub.UserView do
use Pleroma.Web, :view
- alias Pleroma.Web.Salmon
+
alias Pleroma.Web.WebFinger
+ alias Pleroma.Web.Salmon
alias Pleroma.User
alias Pleroma.Repo
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.Router.Helpers
+ alias Pleroma.Web.Endpoint
+
import Ecto.Query
+ def render("endpoints.json", %{user: %User{nickname: nil, local: true} = _user}) do
+ %{"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox)}
+ end
+
+ def render("endpoints.json", %{user: %User{local: true} = _user}) do
+ %{
+ "oauthAuthorizationEndpoint" => Helpers.o_auth_url(Endpoint, :authorize),
+ "oauthRegistrationEndpoint" => Helpers.mastodon_api_url(Endpoint, :create_app),
+ "oauthTokenEndpoint" => Helpers.o_auth_url(Endpoint, :token_exchange),
+ "sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox)
+ }
+ end
+
+ def render("endpoints.json", _), do: %{}
+
# the instance itself is not a Person, but instead an Application
def render("user.json", %{user: %{nickname: nil} = user}) do
{:ok, user} = WebFinger.ensure_keys_present(user)
@@ -20,6 +39,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
+ endpoints = render("endpoints.json", %{user: user})
+
%{
"id" => user.ap_id,
"type" => "Application",
@@ -35,9 +56,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"owner" => user.ap_id,
"publicKeyPem" => public_key
},
- "endpoints" => %{
- "sharedInbox" => "#{Pleroma.Web.Endpoint.url()}/inbox"
- }
+ "endpoints" => endpoints
}
|> Map.merge(Utils.make_json_ld_header())
end
@@ -48,6 +67,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
+ endpoints = render("endpoints.json", %{user: user})
+
%{
"id" => user.ap_id,
"type" => "Person",
@@ -65,9 +86,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"owner" => user.ap_id,
"publicKeyPem" => public_key
},
- "endpoints" => %{
- "sharedInbox" => "#{Pleroma.Web.Endpoint.url()}/inbox"
- },
+ "endpoints" => endpoints,
"icon" => %{
"type" => "Image",
"url" => User.avatar_url(user)
@@ -86,7 +105,14 @@ defmodule Pleroma.Web.ActivityPub.UserView do
query = from(user in query, select: [:ap_id])
following = Repo.all(query)
- collection(following, "#{user.ap_id}/following", page, !user.info.hide_network)
+ total =
+ if !user.info.hide_follows do
+ length(following)
+ else
+ 0
+ end
+
+ collection(following, "#{user.ap_id}/following", page, !user.info.hide_follows, total)
|> Map.merge(Utils.make_json_ld_header())
end
@@ -95,11 +121,18 @@ defmodule Pleroma.Web.ActivityPub.UserView do
query = from(user in query, select: [:ap_id])
following = Repo.all(query)
+ total =
+ if !user.info.hide_follows do
+ length(following)
+ else
+ 0
+ end
+
%{
"id" => "#{user.ap_id}/following",
"type" => "OrderedCollection",
- "totalItems" => length(following),
- "first" => collection(following, "#{user.ap_id}/following", 1, !user.info.hide_network)
+ "totalItems" => total,
+ "first" => collection(following, "#{user.ap_id}/following", 1, !user.info.hide_follows)
}
|> Map.merge(Utils.make_json_ld_header())
end
@@ -109,7 +142,14 @@ defmodule Pleroma.Web.ActivityPub.UserView do
query = from(user in query, select: [:ap_id])
followers = Repo.all(query)
- collection(followers, "#{user.ap_id}/followers", page, !user.info.hide_network)
+ total =
+ if !user.info.hide_followers do
+ length(followers)
+ else
+ 0
+ end
+
+ collection(followers, "#{user.ap_id}/followers", page, !user.info.hide_followers, total)
|> Map.merge(Utils.make_json_ld_header())
end
@@ -118,19 +158,24 @@ defmodule Pleroma.Web.ActivityPub.UserView do
query = from(user in query, select: [:ap_id])
followers = Repo.all(query)
+ total =
+ if !user.info.hide_followers do
+ length(followers)
+ else
+ 0
+ end
+
%{
"id" => "#{user.ap_id}/followers",
"type" => "OrderedCollection",
- "totalItems" => length(followers),
- "first" => collection(followers, "#{user.ap_id}/followers", 1, !user.info.hide_network)
+ "totalItems" => total,
+ "first" =>
+ collection(followers, "#{user.ap_id}/followers", 1, !user.info.hide_followers, total)
}
|> Map.merge(Utils.make_json_ld_header())
end
def render("outbox.json", %{user: user, max_id: max_qid}) do
- # XXX: technically note_count is wrong for this, but it's better than nothing
- info = User.user_info(user)
-
params = %{
"limit" => "10"
}
@@ -158,16 +203,14 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"id" => "#{iri}?max_id=#{max_id}",
"type" => "OrderedCollectionPage",
"partOf" => iri,
- "totalItems" => info.note_count,
"orderedItems" => collection,
- "next" => "#{iri}?max_id=#{min_id - 1}"
+ "next" => "#{iri}?max_id=#{min_id}"
}
if max_qid == nil do
%{
"id" => iri,
"type" => "OrderedCollection",
- "totalItems" => info.note_count,
"first" => page
}
|> Map.merge(Utils.make_json_ld_header())
@@ -205,16 +248,14 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"id" => "#{iri}?max_id=#{max_id}",
"type" => "OrderedCollectionPage",
"partOf" => iri,
- "totalItems" => -1,
"orderedItems" => collection,
- "next" => "#{iri}?max_id=#{min_id - 1}"
+ "next" => "#{iri}?max_id=#{min_id}"
}
if max_qid == nil do
%{
"id" => iri,
"type" => "OrderedCollection",
- "totalItems" => -1,
"first" => page
}
|> Map.merge(Utils.make_json_ld_header())
@@ -239,6 +280,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
if offset < total do
Map.put(map, "next", "#{iri}?page=#{page + 1}")
+ else
+ map
end
end
end
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index ef79b9c5d..90b208e54 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -3,7 +3,11 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI do
- alias Pleroma.{User, Repo, Activity, Object}
+ alias Pleroma.User
+ alias Pleroma.Repo
+ alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.ThreadMute
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Formatter
@@ -14,6 +18,7 @@ defmodule Pleroma.Web.CommonAPI do
with %Activity{data: %{"object" => %{"id" => object_id}}} <- Repo.get(Activity, activity_id),
%Object{} = object <- Object.normalize(object_id),
true <- user.info.is_moderator || user.ap_id == object.data["actor"],
+ {:ok, _} <- unpin(activity_id, user),
{:ok, delete} <- ActivityPub.delete(object) do
{:ok, delete}
end
@@ -102,7 +107,14 @@ defmodule Pleroma.Web.CommonAPI do
attachments,
tags,
get_content_type(data["content_type"]),
- Enum.member?([true, "true"], data["no_attachment_links"])
+ Enum.member?(
+ [true, "true"],
+ Map.get(
+ data,
+ "no_attachment_links",
+ Pleroma.Config.get([:instance, :no_attachment_links], false)
+ )
+ )
),
context <- make_context(inReplyTo),
cw <- data["spoiler_text"],
@@ -124,7 +136,7 @@ defmodule Pleroma.Web.CommonAPI do
Map.put(
object,
"emoji",
- Formatter.get_emoji(status)
+ (Formatter.get_emoji(status) ++ Formatter.get_emoji(data["spoiler_text"]))
|> Enum.reduce(%{}, fn {name, file}, acc ->
Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
end)
@@ -135,7 +147,7 @@ defmodule Pleroma.Web.CommonAPI do
actor: user,
context: context,
object: object,
- additional: %{"cc" => cc}
+ additional: %{"cc" => cc, "directMessage" => visibility == "direct"}
})
res
@@ -164,4 +176,71 @@ defmodule Pleroma.Web.CommonAPI do
object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
})
end
+
+ def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
+ with %Activity{
+ actor: ^user_ap_id,
+ data: %{
+ "type" => "Create",
+ "object" => %{
+ "to" => object_to,
+ "type" => "Note"
+ }
+ }
+ } = activity <- get_by_id_or_ap_id(id_or_ap_id),
+ true <- Enum.member?(object_to, "https://www.w3.org/ns/activitystreams#Public"),
+ %{valid?: true} = info_changeset <-
+ Pleroma.User.Info.add_pinnned_activity(user.info, activity),
+ changeset <-
+ Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
+ {:ok, _user} <- User.update_and_set_cache(changeset) do
+ {:ok, activity}
+ else
+ %{errors: [pinned_activities: {err, _}]} ->
+ {:error, err}
+
+ _ ->
+ {:error, "Could not pin"}
+ end
+ end
+
+ def unpin(id_or_ap_id, user) do
+ with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
+ %{valid?: true} = info_changeset <-
+ Pleroma.User.Info.remove_pinnned_activity(user.info, activity),
+ changeset <-
+ Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
+ {:ok, _user} <- User.update_and_set_cache(changeset) do
+ {:ok, activity}
+ else
+ %{errors: [pinned_activities: {err, _}]} ->
+ {:error, err}
+
+ _ ->
+ {:error, "Could not unpin"}
+ end
+ end
+
+ def add_mute(user, activity) do
+ with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
+ {:ok, activity}
+ else
+ {:error, _} -> {:error, "conversation is already muted"}
+ end
+ end
+
+ def remove_mute(user, activity) do
+ ThreadMute.remove_mute(user.id, activity.data["context"])
+ {:ok, activity}
+ end
+
+ def thread_muted?(%{id: nil} = _user, _activity), do: false
+
+ def thread_muted?(user, activity) do
+ with [] <- ThreadMute.check_muted(user.id, activity.data["context"]) do
+ false
+ else
+ _ -> true
+ end
+ end
end
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 59df48ed6..abdeee947 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -5,22 +5,25 @@
defmodule Pleroma.Web.CommonAPI.Utils do
alias Calendar.Strftime
alias Comeonin.Pbkdf2
- alias Pleroma.{Activity, Formatter, Object, Repo}
+ alias Pleroma.Activity
+ alias Pleroma.Formatter
+ alias Pleroma.Object
+ alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web
- alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Endpoint
alias Pleroma.Web.MediaProxy
+ alias Pleroma.Web.ActivityPub.Utils
# This is a hack for twidere.
def get_by_id_or_ap_id(id) do
- activity = Repo.get(Activity, id) || Activity.get_create_activity_by_object_ap_id(id)
+ activity = Repo.get(Activity, id) || Activity.get_create_by_object_ap_id(id)
activity &&
if activity.data["type"] == "Create" do
activity
else
- Activity.get_create_activity_by_object_ap_id(activity.data["object"])
+ Activity.get_create_by_object_ap_id(activity.data["object"])
end
end
@@ -111,7 +114,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def make_context(%Activity{data: %{"context" => context}}), do: context
def make_context(_), do: Utils.generate_context_id()
- def maybe_add_attachments(text, _attachments, _no_links = true), do: text
+ def maybe_add_attachments(text, _attachments, true = _no_links), do: text
def maybe_add_attachments(text, attachments, _no_links) do
add_attachments(text, attachments)
@@ -132,16 +135,18 @@ defmodule Pleroma.Web.CommonAPI.Utils do
Enum.join([text | attachment_text], "<br>")
end
+ def format_input(text, mentions, tags, format, options \\ [])
+
@doc """
Formatting text to plain text.
"""
- def format_input(text, mentions, tags, "text/plain") do
+ def format_input(text, mentions, tags, "text/plain", options) do
text
|> Formatter.html_escape("text/plain")
|> String.replace(~r/\r?\n/, "<br>")
|> (&{[], &1}).()
|> Formatter.add_links()
- |> Formatter.add_user_links(mentions)
+ |> Formatter.add_user_links(mentions, options[:user_links] || [])
|> Formatter.add_hashtag_links(tags)
|> Formatter.finalize()
end
@@ -149,26 +154,24 @@ defmodule Pleroma.Web.CommonAPI.Utils do
@doc """
Formatting text to html.
"""
- def format_input(text, mentions, _tags, "text/html") do
+ def format_input(text, mentions, _tags, "text/html", options) do
text
|> Formatter.html_escape("text/html")
- |> String.replace(~r/\r?\n/, "<br>")
|> (&{[], &1}).()
- |> Formatter.add_user_links(mentions)
+ |> Formatter.add_user_links(mentions, options[:user_links] || [])
|> Formatter.finalize()
end
@doc """
Formatting text to markdown.
"""
- def format_input(text, mentions, tags, "text/markdown") do
+ def format_input(text, mentions, tags, "text/markdown", options) do
text
|> Formatter.mentions_escape(mentions)
|> Earmark.as_html!()
|> Formatter.html_escape("text/html")
- |> String.replace(~r/\r?\n/, "")
|> (&{[], &1}).()
- |> Formatter.add_user_links(mentions)
+ |> Formatter.add_user_links(mentions, options[:user_links] || [])
|> Formatter.add_hashtag_links(tags)
|> Formatter.finalize()
end
@@ -277,4 +280,46 @@ defmodule Pleroma.Web.CommonAPI.Utils do
}
end)
end
+
+ def maybe_notify_to_recipients(
+ recipients,
+ %Activity{data: %{"to" => to, "type" => _type}} = _activity
+ ) do
+ recipients ++ to
+ end
+
+ def 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
+
+ def maybe_notify_mentioned_recipients(recipients, _), do: recipients
+
+ def 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
+
+ def maybe_extract_mentions(_), do: []
end
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index 0b4ce9cc4..3eed047ca 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -25,7 +25,7 @@ defmodule Pleroma.Web.Endpoint do
at: "/",
from: :pleroma,
only:
- ~w(index.html static finmoji emoji packs sounds images instance sw.js favicon.png schemas doc)
+ ~w(index.html static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc)
)
# Code reloading can be explicitly enabled under the
@@ -82,4 +82,8 @@ defmodule Pleroma.Web.Endpoint do
port = System.get_env("PORT") || raise "expected the PORT environment variable to be set"
{:ok, Keyword.put(config, :http, [:inet6, port: port])}
end
+
+ def websocket_url do
+ String.replace_leading(url(), "http", "ws")
+ end
end
diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex
index f3a0e18b8..468959a65 100644
--- a/lib/pleroma/web/federator/federator.ex
+++ b/lib/pleroma/web/federator/federator.ex
@@ -4,15 +4,19 @@
defmodule Pleroma.Web.Federator do
use GenServer
- alias Pleroma.User
+
alias Pleroma.Activity
- alias Pleroma.Web.{WebFinger, Websub}
- alias Pleroma.Web.Federator.RetryQueue
+ alias Pleroma.User
+ alias Pleroma.Web.WebFinger
+ alias Pleroma.Web.Websub
+ alias Pleroma.Web.Salmon
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.Federator.RetryQueue
alias Pleroma.Web.OStatus
+
require Logger
@websub Application.get_env(:pleroma, :websub)
@@ -25,7 +29,7 @@ defmodule Pleroma.Web.Federator do
def start_link do
spawn(fn ->
# 1 minute
- Process.sleep(1000 * 60 * 1)
+ Process.sleep(1000 * 60)
enqueue(:refresh_subscriptions, nil)
end)
@@ -124,6 +128,10 @@ defmodule Pleroma.Web.Federator do
end
end
+ def handle(:publish_single_salmon, params) do
+ Salmon.send_to_user(params)
+ end
+
def handle(:publish_single_ap, params) do
case ActivityPub.publish_one(params) do
{:ok, _} ->
@@ -192,8 +200,7 @@ defmodule Pleroma.Web.Federator do
{:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
end
- def handle_cast(m, state) do
- IO.inspect("Unknown: #{inspect(m)}, #{inspect(state)}")
+ def handle_cast(_, state) do
{:noreply, state}
end
diff --git a/lib/pleroma/web/http_signatures/http_signatures.ex b/lib/pleroma/web/http_signatures/http_signatures.ex
index e81f9e27a..8e2e2a44b 100644
--- a/lib/pleroma/web/http_signatures/http_signatures.ex
+++ b/lib/pleroma/web/http_signatures/http_signatures.ex
@@ -5,8 +5,9 @@
# https://tools.ietf.org/html/draft-cavage-http-signatures-08
defmodule Pleroma.Web.HTTPSignatures do
alias Pleroma.User
- alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Utils
+
require Logger
def split_signature(sig) do
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index 95d0f849c..dcaeccac6 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -4,34 +4,44 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller
- alias Pleroma.{Repo, Object, Activity, User, Notification, Stats}
+ alias Pleroma.Activity
+ alias Pleroma.Config
+ alias Pleroma.Filter
+ alias Pleroma.Notification
+ alias Pleroma.Object
+ alias Pleroma.Repo
+ alias Pleroma.Stats
+ alias Pleroma.User
alias Pleroma.Web
-
- alias Pleroma.Web.MastodonAPI.{
- StatusView,
- AccountView,
- MastodonView,
- ListView,
- FilterView,
- PushSubscriptionView
- }
-
- alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.OAuth.{Authorization, Token, App}
alias Pleroma.Web.MediaProxy
+ alias Pleroma.Web.Push
+ alias Push.Subscription
+
+ alias Pleroma.Web.MastodonAPI.AccountView
+ alias Pleroma.Web.MastodonAPI.FilterView
+ alias Pleroma.Web.MastodonAPI.ListView
+ alias Pleroma.Web.MastodonAPI.MastodonView
+ alias Pleroma.Web.MastodonAPI.PushSubscriptionView
+ alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.OAuth.App
+ alias Pleroma.Web.OAuth.Authorization
+ alias Pleroma.Web.OAuth.Token
import Ecto.Query
require Logger
@httpoison Application.get_env(:pleroma, :httpoison)
+ @local_mastodon_name "Mastodon-Local"
action_fallback(:errors)
def create_app(conn, params) do
- with cs <- App.register_changeset(%App{}, params) |> IO.inspect(),
- {:ok, app} <- Repo.insert(cs) |> IO.inspect() do
+ with cs <- App.register_changeset(%App{}, params),
+ false <- cs.changes[:client_name] == @local_mastodon_name,
+ {:ok, app} <- Repo.insert(cs) do
res = %{
id: app.id |> to_string,
name: app.client_name,
@@ -129,7 +139,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
@mastodon_api_level "2.5.0"
def masto_instance(conn, _params) do
- instance = Pleroma.Config.get(:instance)
+ instance = Config.get(:instance)
response = %{
uri: Web.base_url(),
@@ -138,7 +148,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
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")
+ streaming_api: Pleroma.Web.Endpoint.websocket_url()
},
stats: Stats.get_stats(),
thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
@@ -225,7 +235,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> Map.put("user", user)
activities =
- ActivityPub.fetch_activities([user.ap_id | user.following], params)
+ [user.ap_id | user.following]
+ |> ActivityPub.fetch_activities(params)
|> ActivityPub.contain_timeline(user)
|> Enum.reverse()
@@ -238,14 +249,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def public_timeline(%{assigns: %{user: user}} = conn, params) do
local_only = params["local"] in [true, "True", "true", "1"]
- params =
+ activities =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
-
- activities =
- ActivityPub.fetch_public_activities(params)
+ |> ActivityPub.fetch_public_activities()
|> Enum.reverse()
conn
@@ -256,13 +265,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
with %User{} = user <- Repo.get(User, params["id"]) do
- # Since Pleroma has no "pinned" posts feature, we'll just set an empty list here
- activities =
- if params["pinned"] == "true" do
- []
- else
- ActivityPub.fetch_user_activities(user, reading_user, params)
- end
+ activities = ActivityPub.fetch_user_activities(user, reading_user, params)
conn
|> add_link_headers(:user_statuses, activities, params["id"])
@@ -320,6 +323,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
as: :activity
)
|> Enum.reverse(),
+ # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
descendants:
StatusView.render(
"index.json",
@@ -328,6 +332,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
as: :activity
)
|> Enum.reverse()
+ # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
}
json(conn, result)
@@ -347,7 +352,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
params =
params
|> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
- |> Map.put("no_attachment_links", true)
idempotency_key =
case get_req_header(conn, "idempotency-key") do
@@ -384,7 +388,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
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
+ %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
@@ -393,7 +397,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
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
+ %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
@@ -402,7 +406,75 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
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
+ %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
+ conn
+ |> put_view(StatusView)
+ |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+ end
+ end
+
+ def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+ with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
+ conn
+ |> put_view(StatusView)
+ |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+ else
+ {:error, reason} ->
+ conn
+ |> put_resp_content_type("application/json")
+ |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
+ end
+ end
+
+ def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+ with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
+ conn
+ |> put_view(StatusView)
+ |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+ end
+ end
+
+ def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with %Activity{} = activity <- Repo.get(Activity, id),
+ %User{} = user <- User.get_by_nickname(user.nickname),
+ true <- ActivityPub.visible_for_user?(activity, user),
+ {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
+ conn
+ |> put_view(StatusView)
+ |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+ end
+ end
+
+ def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with %Activity{} = activity <- Repo.get(Activity, id),
+ %User{} = user <- User.get_by_nickname(user.nickname),
+ true <- ActivityPub.visible_for_user?(activity, user),
+ {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
+ conn
+ |> put_view(StatusView)
+ |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+ end
+ end
+
+ def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ activity = Activity.get_by_id(id)
+
+ with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
+ conn
+ |> put_view(StatusView)
+ |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+ else
+ {:error, reason} ->
+ conn
+ |> put_resp_content_type("application/json")
+ |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
+ end
+ end
+
+ def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ activity = Activity.get_by_id(id)
+
+ with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
@@ -413,9 +485,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
notifications = Notification.for_user(user, params)
result =
- Enum.map(notifications, fn x ->
- render_notification(user, x)
- end)
+ notifications
+ |> Enum.map(fn x -> render_notification(user, x) end)
|> Enum.filter(& &1)
conn
@@ -485,7 +556,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
with {:ok, object} <-
- ActivityPub.upload(file,
+ ActivityPub.upload(
+ file,
actor: User.ap_id(user),
description: Map.get(data, "description")
) do
@@ -526,15 +598,32 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
local_only = params["local"] in [true, "True", "true", "1"]
- params =
+ tags =
+ [params["tag"], params["any"]]
+ |> List.flatten()
+ |> Enum.uniq()
+ |> Enum.filter(& &1)
+ |> Enum.map(&String.downcase(&1))
+
+ tag_all =
+ params["all"] ||
+ []
+ |> Enum.map(&String.downcase(&1))
+
+ tag_reject =
+ params["none"] ||
+ []
+ |> Enum.map(&String.downcase(&1))
+
+ activities =
params
|> 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)
+ |> Map.put("tag", tags)
+ |> Map.put("tag_all", tag_all)
+ |> Map.put("tag_reject", tag_reject)
+ |> ActivityPub.fetch_public_activities()
|> Enum.reverse()
conn
@@ -549,7 +638,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
followers =
cond do
for_user && user.id == for_user.id -> followers
- user.info.hide_network -> []
+ user.info.hide_followers -> []
true -> followers
end
@@ -565,7 +654,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
followers =
cond do
for_user && user.id == for_user.id -> followers
- user.info.hide_network -> []
+ user.info.hide_follows -> []
true -> followers
end
@@ -634,7 +723,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
{:ok, _activity} <- ActivityPub.follow(follower, followed),
{:ok, follower, followed} <-
User.wait_and_refresh(
- Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
+ Config.get([:activitypub, :follow_handshake_timeout]),
follower,
followed
) do
@@ -725,11 +814,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
json(conn, %{})
end
- def status_search(query) do
+ def status_search(user, query) do
fetched =
if Regex.match?(~r/https?:/, query) do
- with {:ok, object} <- ActivityPub.fetch_object_from_id(query) do
- [Activity.get_create_activity_by_object_ap_id(object.data["id"])]
+ with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
+ %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
+ true <- ActivityPub.visible_for_user?(activity, user) do
+ [activity]
else
_e -> []
end
@@ -754,14 +845,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
- accounts = User.search(query, params["resolve"] == "true")
+ accounts = User.search(query, params["resolve"] == "true", user)
- statuses = status_search(query)
+ statuses = status_search(user, query)
tags_path = Web.base_url() <> "/tag/"
tags =
- String.split(query)
+ query
+ |> String.split()
|> Enum.uniq()
|> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
|> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
@@ -778,12 +870,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
- accounts = User.search(query, params["resolve"] == "true")
+ accounts = User.search(query, params["resolve"] == "true", user)
- statuses = status_search(query)
+ statuses = status_search(user, query)
tags =
- String.split(query)
+ query
+ |> String.split()
|> Enum.uniq()
|> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
|> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
@@ -799,22 +892,34 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
- accounts = User.search(query, params["resolve"] == "true")
+ accounts = User.search(query, params["resolve"] == "true", user)
res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
json(conn, res)
end
- def favourites(%{assigns: %{user: user}} = conn, _) do
- params =
- %{}
+ def favourites(%{assigns: %{user: user}} = conn, params) do
+ activities =
+ params
|> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", user)
+ |> ActivityPub.fetch_public_activities()
+ |> Enum.reverse()
+
+ conn
+ |> add_link_headers(:favourites, activities)
+ |> put_view(StatusView)
+ |> render("index.json", %{activities: activities, for: user, as: :activity})
+ end
+
+ def bookmarks(%{assigns: %{user: user}} = conn, _) do
+ user = Repo.get(User, user.id)
activities =
- ActivityPub.fetch_public_activities(params)
+ user.bookmarks
+ |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
|> Enum.reverse()
conn
@@ -833,7 +938,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
res = ListView.render("list.json", list: list)
json(conn, res)
else
- _e -> json(conn, "error")
+ _e ->
+ conn
+ |> put_status(404)
+ |> json(%{error: "Record not found"})
end
end
@@ -913,12 +1021,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
# we must filter the following list for the user to avoid leaking statuses the user
# does not actually have permission to see (for more info, peruse security issue #270).
- following_to =
+ activities =
following
|> Enum.filter(fn x -> x in user.following end)
-
- activities =
- ActivityPub.fetch_activities_bounded(following_to, following, params)
+ |> ActivityPub.fetch_activities_bounded(following, params)
|> Enum.reverse()
conn
@@ -940,7 +1046,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
if user && token do
mastodon_emoji = mastodonized_emoji()
- limit = Pleroma.Config.get([:instance, :limit])
+ limit = Config.get([:instance, :limit])
accounts =
Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
@@ -964,8 +1070,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
max_toot_chars: limit
},
rights: %{
- delete_others_notice: !!user.info.is_moderator,
- admin: !!user.info.is_admin
+ delete_others_notice: present?(user.info.is_moderator),
+ admin: present?(user.info.is_admin)
},
compose: %{
me: "#{user.id}",
@@ -1064,7 +1170,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def login(conn, _) do
with {:ok, app} <- get_or_make_app() do
path =
- o_auth_path(conn, :authorize,
+ o_auth_path(
+ conn,
+ :authorize,
response_type: "code",
client_id: app.client_id,
redirect_uri: ".",
@@ -1077,16 +1185,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
defp get_or_make_app() do
- with %App{} = app <- Repo.get_by(App, client_name: "Mastodon-Local") do
+ find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
+
+ with %App{} = app <- Repo.get_by(App, find_attrs) do
{:ok, app}
else
_e ->
- cs =
- App.register_changeset(%App{}, %{
- client_name: "Mastodon-Local",
- redirect_uris: ".",
- scopes: "read,write,follow"
- })
+ cs = App.register_changeset(%App{}, Map.put(find_attrs, :scopes, "read,write,follow"))
Repo.insert(cs)
end
@@ -1120,7 +1225,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
actor = User.get_cached_by_ap_id(activity.data["actor"])
- parent_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
+ parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
mastodon_type = Activity.mastodon_notification_type(activity)
response = %{
@@ -1158,7 +1263,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def get_filters(%{assigns: %{user: user}} = conn, _) do
- filters = Pleroma.Filter.get_filters(user)
+ filters = Filter.get_filters(user)
res = FilterView.render("filters.json", filters: filters)
json(conn, res)
end
@@ -1167,7 +1272,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
%{assigns: %{user: user}} = conn,
%{"phrase" => phrase, "context" => context} = params
) do
- query = %Pleroma.Filter{
+ query = %Filter{
user_id: user.id,
phrase: phrase,
context: context,
@@ -1176,13 +1281,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
# expires_at
}
- {:ok, response} = Pleroma.Filter.create(query)
+ {:ok, response} = Filter.create(query)
res = FilterView.render("filter.json", filter: response)
json(conn, res)
end
def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
- filter = Pleroma.Filter.get(filter_id, user)
+ filter = Filter.get(filter_id, user)
res = FilterView.render("filter.json", filter: filter)
json(conn, res)
end
@@ -1191,7 +1296,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
%{assigns: %{user: user}} = conn,
%{"phrase" => phrase, "context" => context, "id" => filter_id} = params
) do
- query = %Pleroma.Filter{
+ query = %Filter{
user_id: user.id,
filter_id: filter_id,
phrase: phrase,
@@ -1201,32 +1306,32 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
# expires_at
}
- {:ok, response} = Pleroma.Filter.update(query)
+ {:ok, response} = Filter.update(query)
res = FilterView.render("filter.json", filter: response)
json(conn, res)
end
def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
- query = %Pleroma.Filter{
+ query = %Filter{
user_id: user.id,
filter_id: filter_id
}
- {:ok, _} = Pleroma.Filter.delete(query)
+ {:ok, _} = Filter.delete(query)
json(conn, %{})
end
def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
- true = Pleroma.Web.Push.enabled()
- Pleroma.Web.Push.Subscription.delete_if_exists(user, token)
- {:ok, subscription} = Pleroma.Web.Push.Subscription.create(user, token, params)
+ true = Push.enabled()
+ Subscription.delete_if_exists(user, token)
+ {:ok, subscription} = Subscription.create(user, token, params)
view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
json(conn, view)
end
def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
- true = Pleroma.Web.Push.enabled()
- subscription = Pleroma.Web.Push.Subscription.get(user, token)
+ true = Push.enabled()
+ subscription = Subscription.get(user, token)
view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
json(conn, view)
end
@@ -1235,15 +1340,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
%{assigns: %{user: user, token: token}} = conn,
params
) do
- true = Pleroma.Web.Push.enabled()
- {:ok, subscription} = Pleroma.Web.Push.Subscription.update(user, token, params)
+ true = Push.enabled()
+ {:ok, subscription} = Subscription.update(user, token, params)
view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
json(conn, view)
end
def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
- true = Pleroma.Web.Push.enabled()
- {:ok, _response} = Pleroma.Web.Push.Subscription.delete(user, token)
+ true = Push.enabled()
+ {:ok, _response} = Subscription.delete(user, token)
json(conn, %{})
end
@@ -1254,17 +1359,21 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def suggestions(%{assigns: %{user: user}} = conn, _) do
- suggestions = Pleroma.Config.get(:suggestions)
+ suggestions = 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 = Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
+ host = Config.get([Pleroma.Web.Endpoint, :url, :host])
user = user.nickname
- url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user)
+
+ url =
+ api
+ |> String.replace("{{host}}", host)
+ |> String.replace("{{user}}", user)
with {:ok, %{status: 200, body: body}} <-
@httpoison.get(
@@ -1272,12 +1381,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
[],
adapter: [
timeout: timeout,
- recv_timeout: timeout
+ recv_timeout: timeout,
+ pool: :default
]
),
{:ok, data} <- Jason.decode(body) do
- data2 =
- Enum.slice(data, 0, limit)
+ data =
+ data
+ |> Enum.slice(0, limit)
|> Enum.map(fn x ->
Map.put(
x,
@@ -1296,7 +1407,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end)
conn
- |> json(data2)
+ |> json(data)
else
e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
end
@@ -1305,6 +1416,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
+ def status_card(conn, %{"id" => status_id}) do
+ with %Activity{} = activity <- Repo.get(Activity, status_id),
+ true <- ActivityPub.is_public?(activity) do
+ data =
+ StatusView.render(
+ "card.json",
+ Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
+ )
+
+ json(conn, data)
+ else
+ _e ->
+ %{}
+ end
+ end
+
def try_render(conn, target, params)
when is_binary(target) do
res = render(conn, target, params)
@@ -1323,4 +1450,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> put_status(501)
|> json(%{error: "Can't display this activity"})
end
+
+ defp present?(nil), do: false
+ defp present?(false), do: false
+ defp present?(_), do: true
end
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index bfd6b8b22..9df9f14b2 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -4,11 +4,12 @@
defmodule Pleroma.Web.MastodonAPI.AccountView do
use Pleroma.Web, :view
+
+ alias Pleroma.HTML
alias Pleroma.User
- alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MediaProxy
- alias Pleroma.HTML
def render("accounts.json", %{users: users} = opts) do
users
@@ -112,7 +113,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
# Pleroma extension
pleroma: %{
confirmation_pending: user_info.confirmation_pending,
- tags: user.tags
+ tags: user.tags,
+ is_moderator: user.info.is_moderator,
+ is_admin: user.info.is_admin
}
}
end
diff --git a/lib/pleroma/web/mastodon_api/views/filter_view.ex b/lib/pleroma/web/mastodon_api/views/filter_view.ex
index 1052a449d..a685bc7b6 100644
--- a/lib/pleroma/web/mastodon_api/views/filter_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/filter_view.ex
@@ -4,8 +4,8 @@
defmodule Pleroma.Web.MastodonAPI.FilterView do
use Pleroma.Web, :view
- alias Pleroma.Web.MastodonAPI.FilterView
alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.MastodonAPI.FilterView
def render("filters.json", %{filters: filters} = opts) do
render_many(filters, FilterView, "filter.json", opts)
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 477ab3b5f..69f5f992c 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -9,10 +9,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.HTML
alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
- alias Pleroma.Web.MediaProxy
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.MediaProxy
# TODO: Add cached version.
defp get_replied_to_activities(activities) do
@@ -25,33 +26,45 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
nil
end)
|> Enum.filter(& &1)
- |> Activity.create_activity_by_object_id_query()
+ |> Activity.create_by_object_ap_id()
|> Repo.all()
|> Enum.reduce(%{}, fn activity, acc ->
Map.put(acc, activity.data["object"]["id"], activity)
end)
end
+ defp get_user(ap_id) do
+ cond do
+ user = User.get_cached_by_ap_id(ap_id) ->
+ user
+
+ user = User.get_by_guessed_nickname(ap_id) ->
+ user
+
+ true ->
+ User.error_user(ap_id)
+ end
+ end
+
def render("index.json", opts) do
replied_to_activities = get_replied_to_activities(opts.activities)
opts.activities
- |> render_many(
+ |> safe_render_many(
StatusView,
"status.json",
Map.put(opts, :replied_to_activities, replied_to_activities)
)
- |> Enum.filter(fn x -> not is_nil(x) end)
end
def render(
"status.json",
%{activity: %{data: %{"type" => "Announce", "object" => object}} = activity} = opts
) do
- user = User.get_cached_by_ap_id(activity.data["actor"])
+ user = get_user(activity.data["actor"])
created_at = Utils.to_masto_date(activity.data["published"])
- reblogged = Activity.get_create_activity_by_object_ap_id(object)
+ reblogged = Activity.get_create_by_object_ap_id(object)
reblogged = render("status.json", Map.put(opts, :activity, reblogged))
mentions =
@@ -75,7 +88,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
favourites_count: 0,
reblogged: false,
favourited: false,
+ bookmarked: false,
muted: false,
+ pinned: pinned?(activity, user),
sensitive: false,
spoiler_text: "",
visibility: "public",
@@ -92,7 +107,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
def render("status.json", %{activity: %{data: %{"object" => object}} = activity} = opts) do
- user = User.get_cached_by_ap_id(activity.data["actor"])
+ user = get_user(activity.data["actor"])
like_count = object["like_count"] || 0
announcement_count = object["announcement_count"] || 0
@@ -108,6 +123,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || [])
favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || [])
+ bookmarked = opts[:for] && object["id"] in opts[:for].bookmarks
attachment_data = object["attachment"] || []
attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
@@ -115,12 +131,18 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
created_at = Utils.to_masto_date(object["published"])
reply_to = get_reply_to(activity, opts)
- reply_to_user = reply_to && User.get_cached_by_ap_id(reply_to.data["actor"])
+ reply_to_user = reply_to && get_user(reply_to.data["actor"])
content =
object
|> render_content()
- |> HTML.get_cached_scrubbed_html_for_object(User.html_filter_policy(opts[:for]), activity)
+ |> HTML.get_cached_scrubbed_html_for_object(
+ User.html_filter_policy(opts[:for]),
+ activity,
+ __MODULE__
+ )
+
+ card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
%{
id: to_string(activity.id),
@@ -130,6 +152,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
in_reply_to_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
reblog: nil,
+ card: card,
content: content,
created_at: created_at,
reblogs_count: announcement_count,
@@ -137,7 +160,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
favourites_count: like_count,
reblogged: present?(repeated),
favourited: present?(favorited),
- muted: false,
+ bookmarked: present?(bookmarked),
+ muted: CommonAPI.thread_muted?(user, activity),
+ pinned: pinned?(activity, user),
sensitive: sensitive,
spoiler_text: object["summary"] || "",
visibility: get_visibility(object),
@@ -157,6 +182,46 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
nil
end
+ def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
+ page_url_data = URI.parse(page_url)
+
+ page_url_data =
+ if rich_media[:url] != nil do
+ URI.merge(page_url_data, URI.parse(rich_media[:url]))
+ else
+ page_url_data
+ end
+
+ page_url = page_url_data |> to_string
+
+ image_url =
+ if rich_media[:image] != nil do
+ URI.merge(page_url_data, URI.parse(rich_media[:image]))
+ |> to_string
+ else
+ nil
+ end
+
+ site_name = rich_media[:site_name] || page_url_data.host
+
+ %{
+ type: "link",
+ provider_name: site_name,
+ provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
+ url: page_url,
+ image: image_url |> MediaProxy.url(),
+ title: rich_media[:title],
+ description: rich_media[:description],
+ pleroma: %{
+ opengraph: rich_media
+ }
+ }
+ end
+
+ def render("card.json", _) do
+ nil
+ end
+
def render("attachment.json", %{attachment: attachment}) do
[attachment_url | _] = attachment["url"]
media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
@@ -190,7 +255,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
def get_reply_to(%{data: %{"object" => object}}, _) do
if object["inReplyTo"] && object["inReplyTo"] != "" do
- Activity.get_create_activity_by_object_ap_id(object["inReplyTo"])
+ Activity.get_create_by_object_ap_id(object["inReplyTo"])
else
nil
end
@@ -212,6 +277,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
Enum.any?(to, &String.contains?(&1, "/followers")) ->
"private"
+ length(cc) > 0 ->
+ "private"
+
true ->
"direct"
end
@@ -291,4 +359,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
defp present?(nil), do: false
defp present?(false), do: false
defp present?(_), do: true
+
+ defp pinned?(%Activity{id: id}, %User{info: %{pinned_activities: pinned_activities}}),
+ do: id in pinned_activities
end
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index c0254c8e6..ea75070c4 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -6,7 +6,8 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
require Logger
alias Pleroma.Web.OAuth.Token
- alias Pleroma.{User, Repo}
+ alias Pleroma.Repo
+ alias Pleroma.User
@behaviour :cowboy_websocket_handler
diff --git a/lib/pleroma/web/media_proxy/controller.ex b/lib/pleroma/web/media_proxy/controller.ex
index de79cad73..c0552d89f 100644
--- a/lib/pleroma/web/media_proxy/controller.ex
+++ b/lib/pleroma/web/media_proxy/controller.ex
@@ -4,11 +4,12 @@
defmodule Pleroma.Web.MediaProxy.MediaProxyController do
use Pleroma.Web, :controller
- alias Pleroma.{Web.MediaProxy, ReverseProxy}
+ alias Pleroma.ReverseProxy
+ alias Pleroma.Web.MediaProxy
@default_proxy_opts [max_body_length: 25 * 1_048_576, http: [follow_redirect: true]]
- def remote(conn, params = %{"sig" => sig64, "url" => url64}) do
+ def remote(conn, %{"sig" => sig64, "url" => url64} = params) do
with config <- Pleroma.Config.get([:media_proxy], []),
true <- Keyword.get(config, :enabled, false),
{:ok, url} <- MediaProxy.decode_url(sig64, url64),
diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex
index e1eb1472d..39a725a69 100644
--- a/lib/pleroma/web/media_proxy/media_proxy.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.MediaProxy do
def url(""), do: nil
- def url(url = "/" <> _), do: url
+ def url("/" <> _ = url), do: url
def url(url) do
config = Application.get_env(:pleroma, :media_proxy, [])
@@ -19,11 +19,16 @@ defmodule Pleroma.Web.MediaProxy do
else
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
+ # Must preserve `%2F` for compatibility with S3 (https://git.pleroma.social/pleroma/pleroma/issues/580)
+ replacement = get_replacement(url, ":2F:")
+
# The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice.
base64 =
url
+ |> String.replace("%2F", replacement)
|> URI.decode()
|> URI.encode()
+ |> String.replace(replacement, "%2F")
|> Base.url_encode64(@base64_opts)
sig = :crypto.hmac(:sha, secret, base64)
@@ -60,4 +65,12 @@ defmodule Pleroma.Web.MediaProxy do
|> Enum.filter(fn value -> value end)
|> Path.join()
end
+
+ defp get_replacement(url, replacement) do
+ if String.contains?(url, replacement) do
+ get_replacement(url, replacement <> replacement)
+ else
+ replacement
+ end
+ end
end
diff --git a/lib/pleroma/web/metadata.ex b/lib/pleroma/web/metadata.ex
new file mode 100644
index 000000000..8761260f2
--- /dev/null
+++ b/lib/pleroma/web/metadata.ex
@@ -0,0 +1,40 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Metadata do
+ alias Phoenix.HTML
+
+ def build_tags(params) do
+ Enum.reduce(Pleroma.Config.get([__MODULE__, :providers], []), "", fn parser, acc ->
+ rendered_html =
+ params
+ |> parser.build_tags()
+ |> Enum.map(&to_tag/1)
+ |> Enum.map(&HTML.safe_to_string/1)
+ |> Enum.join()
+
+ acc <> rendered_html
+ end)
+ end
+
+ def to_tag(data) do
+ with {name, attrs, _content = []} <- data do
+ HTML.Tag.tag(name, attrs)
+ else
+ {name, attrs, content} ->
+ HTML.Tag.content_tag(name, content, attrs)
+
+ _ ->
+ raise ArgumentError, message: "make_tag invalid args"
+ end
+ end
+
+ def activity_nsfw?(%{data: %{"sensitive" => sensitive}}) do
+ Pleroma.Config.get([__MODULE__, :unfurl_nsfw], false) == false and sensitive
+ end
+
+ def activity_nsfw?(_) do
+ false
+ end
+end
diff --git a/lib/pleroma/web/metadata/opengraph.ex b/lib/pleroma/web/metadata/opengraph.ex
new file mode 100644
index 000000000..190377767
--- /dev/null
+++ b/lib/pleroma/web/metadata/opengraph.ex
@@ -0,0 +1,156 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
+ alias Pleroma.HTML
+ alias Pleroma.Formatter
+ alias Pleroma.User
+ alias Pleroma.Web.Metadata
+ alias Pleroma.Web.MediaProxy
+ alias Pleroma.Web.Metadata.Providers.Provider
+
+ @behaviour Provider
+
+ @impl Provider
+ def build_tags(%{
+ object: object,
+ url: url,
+ user: user
+ }) do
+ attachments = build_attachments(object)
+ scrubbed_content = scrub_html_and_truncate(object)
+ # Zero width space
+ content =
+ if scrubbed_content != "" and scrubbed_content != "\u200B" do
+ ": “" <> scrubbed_content <> "”"
+ else
+ ""
+ end
+
+ # Most previews only show og:title which is inconvenient. Instagram
+ # hacks this by putting the description in the title and making the
+ # description longer prefixed by how many likes and shares the post
+ # has. Here we use the descriptive nickname in the title, and expand
+ # the full account & nickname in the description. We also use the cute^Wevil
+ # smart quotes around the status text like Instagram, too.
+ [
+ {:meta,
+ [
+ property: "og:title",
+ content: "#{user.name}" <> content
+ ], []},
+ {:meta, [property: "og:url", content: url], []},
+ {:meta,
+ [
+ property: "og:description",
+ content: "#{user_name_string(user)}" <> content
+ ], []},
+ {:meta, [property: "og:type", content: "website"], []}
+ ] ++
+ if attachments == [] or Metadata.activity_nsfw?(object) do
+ [
+ {:meta, [property: "og:image", content: attachment_url(User.avatar_url(user))], []},
+ {:meta, [property: "og:image:width", content: 150], []},
+ {:meta, [property: "og:image:height", content: 150], []}
+ ]
+ else
+ attachments
+ end
+ end
+
+ @impl Provider
+ def build_tags(%{user: user}) do
+ with truncated_bio = scrub_html_and_truncate(user.bio || "") do
+ [
+ {:meta,
+ [
+ property: "og:title",
+ content: user_name_string(user)
+ ], []},
+ {:meta, [property: "og:url", content: User.profile_url(user)], []},
+ {:meta, [property: "og:description", content: truncated_bio], []},
+ {:meta, [property: "og:type", content: "website"], []},
+ {:meta, [property: "og:image", content: attachment_url(User.avatar_url(user))], []},
+ {:meta, [property: "og:image:width", content: 150], []},
+ {:meta, [property: "og:image:height", content: 150], []}
+ ]
+ end
+ end
+
+ defp build_attachments(%{data: %{"attachment" => attachments}}) do
+ Enum.reduce(attachments, [], fn attachment, acc ->
+ rendered_tags =
+ Enum.reduce(attachment["url"], [], fn url, acc ->
+ media_type =
+ Enum.find(["image", "audio", "video"], fn media_type ->
+ String.starts_with?(url["mediaType"], media_type)
+ end)
+
+ # TODO: Add additional properties to objects when we have the data available.
+ # Also, Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image
+ # object when a Video or GIF is attached it will display that in the Whatsapp Rich Preview.
+ case media_type do
+ "audio" ->
+ [
+ {:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], []}
+ | acc
+ ]
+
+ "image" ->
+ [
+ {:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])],
+ []},
+ {:meta, [property: "og:image:width", content: 150], []},
+ {:meta, [property: "og:image:height", content: 150], []}
+ | acc
+ ]
+
+ "video" ->
+ [
+ {:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], []}
+ | acc
+ ]
+
+ _ ->
+ acc
+ end
+ end)
+
+ acc ++ rendered_tags
+ end)
+ end
+
+ defp scrub_html_and_truncate(%{data: %{"content" => content}} = object) do
+ content
+ # html content comes from DB already encoded, decode first and scrub after
+ |> HtmlEntities.decode()
+ |> String.replace(~r/<br\s?\/?>/, " ")
+ |> HTML.get_cached_stripped_html_for_object(object, __MODULE__)
+ |> Formatter.demojify()
+ |> Formatter.truncate()
+ end
+
+ defp scrub_html_and_truncate(content) when is_binary(content) do
+ content
+ # html content comes from DB already encoded, decode first and scrub after
+ |> HtmlEntities.decode()
+ |> String.replace(~r/<br\s?\/?>/, " ")
+ |> HTML.strip_tags()
+ |> Formatter.demojify()
+ |> Formatter.truncate()
+ end
+
+ defp attachment_url(url) do
+ MediaProxy.url(url)
+ end
+
+ defp user_name_string(user) do
+ "#{user.name} " <>
+ if user.local do
+ "(@#{user.nickname}@#{Pleroma.Web.Endpoint.host()})"
+ else
+ "(@#{user.nickname})"
+ end
+ end
+end
diff --git a/lib/pleroma/web/metadata/provider.ex b/lib/pleroma/web/metadata/provider.ex
new file mode 100644
index 000000000..197fb2a77
--- /dev/null
+++ b/lib/pleroma/web/metadata/provider.ex
@@ -0,0 +1,7 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Metadata.Providers.Provider do
+ @callback build_tags(map()) :: list()
+end
diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/twitter_card.ex
new file mode 100644
index 000000000..32b979357
--- /dev/null
+++ b/lib/pleroma/web/metadata/twitter_card.ex
@@ -0,0 +1,46 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Metadata.Providers.TwitterCard do
+ alias Pleroma.Web.Metadata.Providers.Provider
+ alias Pleroma.Web.Metadata
+
+ @behaviour Provider
+
+ @impl Provider
+ def build_tags(%{object: object}) do
+ if Metadata.activity_nsfw?(object) or object.data["attachment"] == [] do
+ build_tags(nil)
+ else
+ case find_first_acceptable_media_type(object) do
+ "image" ->
+ [{:meta, [property: "twitter:card", content: "summary_large_image"], []}]
+
+ "audio" ->
+ [{:meta, [property: "twitter:card", content: "player"], []}]
+
+ "video" ->
+ [{:meta, [property: "twitter:card", content: "player"], []}]
+
+ _ ->
+ build_tags(nil)
+ end
+ end
+ end
+
+ @impl Provider
+ def build_tags(_) do
+ [{:meta, [property: "twitter:card", content: "summary"], []}]
+ end
+
+ def find_first_acceptable_media_type(%{data: %{"attachment" => attachment}}) do
+ Enum.find_value(attachment, fn attachment ->
+ Enum.find_value(attachment["url"], fn url ->
+ Enum.find(["image", "audio", "video"], fn media_type ->
+ String.starts_with?(url["mediaType"], media_type)
+ end)
+ end)
+ end)
+ end
+end
diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
index 11b97164d..f4867d05b 100644
--- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
+++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
@@ -5,10 +5,11 @@
defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
use Pleroma.Web, :controller
+ alias Pleroma.Config
+ alias Pleroma.Repo
alias Pleroma.Stats
+ alias Pleroma.User
alias Pleroma.Web
- alias Pleroma.{User, Repo}
- alias Pleroma.Config
alias Pleroma.Web.ActivityPub.MRF
plug(Pleroma.Web.FederatingPlug)
@@ -19,6 +20,10 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
%{
rel: "http://nodeinfo.diaspora.software/ns/schema/2.0",
href: Web.base_url() <> "/nodeinfo/2.0.json"
+ },
+ %{
+ rel: "http://nodeinfo.diaspora.software/ns/schema/2.1",
+ href: Web.base_url() <> "/nodeinfo/2.1.json"
}
]
}
@@ -26,8 +31,9 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
json(conn, response)
end
- # Schema definition: https://github.com/jhass/nodeinfo/blob/master/schemas/2.0/schema.json
- def nodeinfo(conn, %{"version" => "2.0"}) do
+ # returns a nodeinfo 2.0 map, since 2.1 just adds a repository field
+ # under software.
+ def raw_nodeinfo do
instance = Application.get_env(:pleroma, :instance)
media_proxy = Application.get_env(:pleroma, :media_proxy)
suggestions = Application.get_env(:pleroma, :suggestions)
@@ -39,6 +45,33 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
Application.get_env(:pleroma, :mrf_simple)
|> Enum.into(%{})
+ # This horror is needed to convert regex sigils to strings
+ mrf_keyword =
+ Application.get_env(:pleroma, :mrf_keyword, [])
+ |> Enum.map(fn {key, value} ->
+ {key,
+ Enum.map(value, fn
+ {pattern, replacement} ->
+ %{
+ "pattern" =>
+ if not is_binary(pattern) do
+ inspect(pattern)
+ else
+ pattern
+ end,
+ "replacement" => replacement
+ }
+
+ pattern ->
+ if not is_binary(pattern) do
+ inspect(pattern)
+ else
+ pattern
+ end
+ end)}
+ end)
+ |> Enum.into(%{})
+
mrf_policies =
MRF.get_policies()
|> Enum.map(fn policy -> to_string(policy) |> String.split(".") |> List.last() end)
@@ -61,13 +94,12 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
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
+ if Keyword.get(instance, :mrf_transparency) do
%{
mrf_policies: mrf_policies,
mrf_simple: mrf_simple,
+ mrf_keyword: mrf_keyword,
mrf_user_allowlist: mrf_user_allowlist,
quarantined_instances: quarantined
}
@@ -98,10 +130,10 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
]
|> Enum.filter(& &1)
- response = %{
+ %{
version: "2.0",
software: %{
- name: Pleroma.Application.name(),
+ name: Pleroma.Application.name() |> String.downcase(),
version: Pleroma.Application.version()
},
protocols: ["ostatus", "activitypub"],
@@ -142,12 +174,37 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
restrictedNicknames: Pleroma.Config.get([Pleroma.User, :restricted_nicknames])
}
}
+ end
+ # Schema definition: https://github.com/jhass/nodeinfo/blob/master/schemas/2.0/schema.json
+ # and https://github.com/jhass/nodeinfo/blob/master/schemas/2.1/schema.json
+ def nodeinfo(conn, %{"version" => "2.0"}) do
conn
|> put_resp_header(
"content-type",
"application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8"
)
+ |> json(raw_nodeinfo())
+ end
+
+ def nodeinfo(conn, %{"version" => "2.1"}) do
+ raw_response = raw_nodeinfo()
+
+ updated_software =
+ raw_response
+ |> Map.get(:software)
+ |> Map.put(:repository, Pleroma.Application.repository())
+
+ response =
+ raw_response
+ |> Map.put(:software, updated_software)
+ |> Map.put(:version, "2.1")
+
+ conn
+ |> put_resp_header(
+ "content-type",
+ "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.1#; charset=utf-8"
+ )
|> json(response)
end
diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/oauth/app.ex
index 967ac04b5..3e8acde31 100644
--- a/lib/pleroma/web/oauth/app.ex
+++ b/lib/pleroma/web/oauth/app.ex
@@ -4,7 +4,7 @@
defmodule Pleroma.Web.OAuth.App do
use Ecto.Schema
- import Ecto.{Changeset}
+ import Ecto.Changeset
schema "apps" do
field(:client_name, :string)
diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex
index cc4b74bc5..75c9ab9aa 100644
--- a/lib/pleroma/web/oauth/authorization.ex
+++ b/lib/pleroma/web/oauth/authorization.ex
@@ -5,16 +5,19 @@
defmodule Pleroma.Web.OAuth.Authorization do
use Ecto.Schema
- alias Pleroma.{User, Repo}
- alias Pleroma.Web.OAuth.{Authorization, App}
+ alias Pleroma.User
+ alias Pleroma.Repo
+ alias Pleroma.Web.OAuth.Authorization
+ alias Pleroma.Web.OAuth.App
- import Ecto.{Changeset, Query}
+ import Ecto.Changeset
+ import Ecto.Query
schema "oauth_authorizations" do
field(:token, :string)
field(:valid_until, :naive_datetime)
field(:used, :boolean, default: false)
- belongs_to(:user, Pleroma.User)
+ belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId)
belongs_to(:app, App)
timestamps()
diff --git a/lib/pleroma/web/oauth/fallback_controller.ex b/lib/pleroma/web/oauth/fallback_controller.ex
index 1eeda3d24..f0fe3b578 100644
--- a/lib/pleroma/web/oauth/fallback_controller.ex
+++ b/lib/pleroma/web/oauth/fallback_controller.ex
@@ -9,7 +9,8 @@ defmodule Pleroma.Web.OAuth.FallbackController do
# No user/password
def call(conn, _) do
conn
+ |> put_status(:unauthorized)
|> put_flash(:error, "Invalid Username/Password")
- |> OAuthController.authorize(conn.params)
+ |> OAuthController.authorize(conn.params["authorization"])
end
end
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index 4d4e85836..e4d0601f8 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -5,8 +5,11 @@
defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller
- alias Pleroma.Web.OAuth.{Authorization, Token, App}
- alias Pleroma.{Repo, User}
+ alias Pleroma.Web.OAuth.Authorization
+ alias Pleroma.Web.OAuth.Token
+ alias Pleroma.Web.OAuth.App
+ alias Pleroma.Repo
+ alias Pleroma.User
alias Comeonin.Pbkdf2
plug(:fetch_session)
@@ -37,6 +40,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
true <- Pbkdf2.checkpw(password, user.password_hash),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
%App{} = app <- Repo.get_by(App, client_id: client_id),
+ true <- redirect_uri in String.split(app.redirect_uris),
{:ok, auth} <- Authorization.create_authorization(app, user) do
# Special case: Local MastodonFE.
redirect_uri =
diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex
index f0ebc63f6..b0bbeeb69 100644
--- a/lib/pleroma/web/oauth/token.ex
+++ b/lib/pleroma/web/oauth/token.ex
@@ -7,14 +7,17 @@ defmodule Pleroma.Web.OAuth.Token do
import Ecto.Query
- alias Pleroma.{User, Repo}
- alias Pleroma.Web.OAuth.{Token, App, Authorization}
+ alias Pleroma.User
+ alias Pleroma.Repo
+ alias Pleroma.Web.OAuth.Token
+ alias Pleroma.Web.OAuth.App
+ alias Pleroma.Web.OAuth.Authorization
schema "oauth_tokens" do
field(:token, :string)
field(:refresh_token, :string)
field(:valid_until, :naive_datetime)
- belongs_to(:user, Pleroma.User)
+ belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId)
belongs_to(:app, App)
timestamps()
diff --git a/lib/pleroma/web/ostatus/activity_representer.ex b/lib/pleroma/web/ostatus/activity_representer.ex
index 94b1a7ad1..9e1f24bc4 100644
--- a/lib/pleroma/web/ostatus/activity_representer.ex
+++ b/lib/pleroma/web/ostatus/activity_representer.ex
@@ -3,8 +3,11 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.ActivityRepresenter do
- alias Pleroma.{Activity, User, Object}
+ alias Pleroma.Activity
+ alias Pleroma.User
+ alias Pleroma.Object
alias Pleroma.Web.OStatus.UserRepresenter
+
require Logger
defp get_href(id) do
@@ -183,7 +186,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
_in_reply_to = get_in_reply_to(activity.data)
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
- retweeted_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
+ retweeted_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
retweeted_user = User.get_cached_by_ap_id(retweeted_activity.data["actor"])
retweeted_xml = to_simple_form(retweeted_activity, retweeted_user, true)
diff --git a/lib/pleroma/web/ostatus/feed_representer.ex b/lib/pleroma/web/ostatus/feed_representer.ex
index 934d4042f..025d4731c 100644
--- a/lib/pleroma/web/ostatus/feed_representer.ex
+++ b/lib/pleroma/web/ostatus/feed_representer.ex
@@ -3,10 +3,11 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.FeedRepresenter do
- alias Pleroma.Web.OStatus
- alias Pleroma.Web.OStatus.{UserRepresenter, ActivityRepresenter}
alias Pleroma.User
+ alias Pleroma.Web.OStatus
alias Pleroma.Web.MediaProxy
+ alias Pleroma.Web.OStatus.ActivityRepresenter
+ alias Pleroma.Web.OStatus.UserRepresenter
def to_simple_form(user, activities, _users) do
most_recent_update =
diff --git a/lib/pleroma/web/ostatus/handlers/follow_handler.ex b/lib/pleroma/web/ostatus/handlers/follow_handler.ex
index becdf2fbf..91ad4bc40 100644
--- a/lib/pleroma/web/ostatus/handlers/follow_handler.ex
+++ b/lib/pleroma/web/ostatus/handlers/follow_handler.ex
@@ -3,7 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.FollowHandler do
- alias Pleroma.Web.{XML, OStatus}
+ alias Pleroma.Web.XML
+ alias Pleroma.Web.OStatus
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.User
diff --git a/lib/pleroma/web/ostatus/handlers/note_handler.ex b/lib/pleroma/web/ostatus/handlers/note_handler.ex
index 5aeed46f0..c2e585cac 100644
--- a/lib/pleroma/web/ostatus/handlers/note_handler.ex
+++ b/lib/pleroma/web/ostatus/handlers/note_handler.ex
@@ -4,8 +4,10 @@
defmodule Pleroma.Web.OStatus.NoteHandler do
require Logger
- alias Pleroma.Web.{XML, OStatus}
- alias Pleroma.{Object, Activity}
+ alias Pleroma.Web.OStatus
+ alias Pleroma.Web.XML
+ alias Pleroma.Activity
+ alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
@@ -86,7 +88,7 @@ defmodule Pleroma.Web.OStatus.NoteHandler do
end
def fetch_replied_to_activity(entry, inReplyTo) do
- with %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(inReplyTo) do
+ with %Activity{} = activity <- Activity.get_create_by_object_ap_id(inReplyTo) do
activity
else
_e ->
@@ -103,7 +105,7 @@ defmodule Pleroma.Web.OStatus.NoteHandler do
# TODO: Clean this up a bit.
def handle_note(entry, doc \\ nil) do
with id <- XML.string_from_xpath("//id", entry),
- activity when is_nil(activity) <- Activity.get_create_activity_by_object_ap_id(id),
+ activity when is_nil(activity) <- Activity.get_create_by_object_ap_id(id),
[author] <- :xmerl_xpath.string('//author[1]', doc),
{:ok, actor} <- OStatus.find_make_or_update_user(author),
content_html <- OStatus.get_content(entry),
diff --git a/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex b/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex
index 1c64f3c3d..c9085894d 100644
--- a/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex
+++ b/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex
@@ -3,7 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.UnfollowHandler do
- alias Pleroma.Web.{XML, OStatus}
+ alias Pleroma.Web.XML
+ alias Pleroma.Web.OStatus
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.User
diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex
index bb28cd786..b4f5761ac 100644
--- a/lib/pleroma/web/ostatus/ostatus.ex
+++ b/lib/pleroma/web/ostatus/ostatus.ex
@@ -9,11 +9,19 @@ defmodule Pleroma.Web.OStatus do
import Pleroma.Web.XML
require Logger
- alias Pleroma.{Repo, User, Web, Object, Activity}
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Web
+ alias Pleroma.Object
+ alias Pleroma.Activity
alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.{WebFinger, Websub}
- alias Pleroma.Web.OStatus.{FollowHandler, UnfollowHandler, NoteHandler, DeleteHandler}
alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Web.WebFinger
+ alias Pleroma.Web.Websub
+ alias Pleroma.Web.OStatus.FollowHandler
+ alias Pleroma.Web.OStatus.UnfollowHandler
+ alias Pleroma.Web.OStatus.NoteHandler
+ alias Pleroma.Web.OStatus.DeleteHandler
def is_representable?(%Activity{data: data}) do
object = Object.normalize(data["object"])
@@ -48,6 +56,9 @@ defmodule Pleroma.Web.OStatus do
def handle_incoming(xml_string) do
with doc when doc != :error <- parse_document(xml_string) do
+ with {:ok, actor_user} <- find_make_or_update_user(doc),
+ do: Pleroma.Instances.set_reachable(actor_user.ap_id)
+
entries = :xmerl_xpath.string('//entry', doc)
activities =
@@ -148,7 +159,7 @@ defmodule Pleroma.Web.OStatus do
Logger.debug("Trying to get entry from db")
with id when not is_nil(id) <- string_from_xpath("//activity:object[1]/id", entry),
- %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
+ %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
{:ok, activity}
else
_ ->
diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex
index 9b600737f..db4c8f4da 100644
--- a/lib/pleroma/web/ostatus/ostatus_controller.ex
+++ b/lib/pleroma/web/ostatus/ostatus_controller.ex
@@ -5,22 +5,30 @@
defmodule Pleroma.Web.OStatus.OStatusController do
use Pleroma.Web, :controller
- alias Pleroma.{User, Activity, Object}
- alias Pleroma.Web.OStatus.{FeedRepresenter, ActivityRepresenter}
- alias Pleroma.Repo
- alias Pleroma.Web.{OStatus, Federator}
- alias Pleroma.Web.XML
- alias Pleroma.Web.ActivityPub.ObjectView
- alias Pleroma.Web.ActivityPub.ActivityPubController
+ alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.ActivityPubController
+ alias Pleroma.Web.ActivityPub.ObjectView
+ alias Pleroma.Web.OStatus.ActivityRepresenter
+ alias Pleroma.Web.OStatus.FeedRepresenter
+ alias Pleroma.Web.Federator
+ alias Pleroma.Web.OStatus
+ alias Pleroma.Web.XML
plug(Pleroma.Web.FederatingPlug when action in [:salmon_incoming])
+
action_fallback(:errors)
def feed_redirect(conn, %{"nickname" => nickname}) do
case get_format(conn) do
"html" ->
- Fallback.RedirectController.redirector(conn, nil)
+ with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
+ Fallback.RedirectController.redirector_with_meta(conn, %{user: user})
+ else
+ nil -> {:error, :not_found}
+ end
"activity+json" ->
ActivityPubController.call(conn, :user)
@@ -90,8 +98,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
ActivityPubController.call(conn, :object)
else
with id <- o_status_url(conn, :object, uuid),
- {_, %Activity{} = activity} <-
- {:activity, Activity.get_create_activity_by_object_ap_id(id)},
+ {_, %Activity{} = activity} <- {:activity, Activity.get_create_by_object_ap_id(id)},
{_, true} <- {:public?, ActivityPub.is_public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case get_format(conn) do
@@ -112,45 +119,65 @@ defmodule Pleroma.Web.OStatus.OStatusController do
end
def activity(conn, %{"uuid" => uuid}) do
- with id <- o_status_url(conn, :activity, uuid),
- {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
- {_, true} <- {:public?, ActivityPub.is_public?(activity)},
- %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
- case format = get_format(conn) do
- "html" -> redirect(conn, to: "/notice/#{activity.id}")
- _ -> represent_activity(conn, format, activity, user)
- end
+ if get_format(conn) == "activity+json" do
+ ActivityPubController.call(conn, :activity)
else
- {:public?, false} ->
- {:error, :not_found}
+ with id <- o_status_url(conn, :activity, uuid),
+ {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
+ {_, true} <- {:public?, ActivityPub.is_public?(activity)},
+ %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
+ case format = get_format(conn) do
+ "html" -> redirect(conn, to: "/notice/#{activity.id}")
+ _ -> represent_activity(conn, format, activity, user)
+ end
+ else
+ {:public?, false} ->
+ {:error, :not_found}
- {:activity, nil} ->
- {:error, :not_found}
+ {:activity, nil} ->
+ {:error, :not_found}
- e ->
- e
+ e ->
+ e
+ end
end
end
def notice(conn, %{"id" => id}) do
- with {_, %Activity{} = activity} <- {:activity, Repo.get(Activity, id)},
+ with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id(id)},
{_, true} <- {:public?, ActivityPub.is_public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case format = get_format(conn) do
"html" ->
- conn
- |> put_resp_content_type("text/html")
- |> send_file(200, Pleroma.Plugs.InstanceStatic.file_path("index.html"))
+ if activity.data["type"] == "Create" do
+ %Object{} = object = Object.normalize(activity.data["object"])
+
+ Fallback.RedirectController.redirector_with_meta(conn, %{
+ object: object,
+ url:
+ Pleroma.Web.Router.Helpers.o_status_url(
+ Pleroma.Web.Endpoint,
+ :notice,
+ activity.id
+ ),
+ user: user
+ })
+ else
+ Fallback.RedirectController.redirector(conn, nil)
+ end
_ ->
represent_activity(conn, format, activity, user)
end
else
{:public?, false} ->
- {:error, :not_found}
+ conn
+ |> put_status(404)
+ |> Fallback.RedirectController.redirector(nil, 404)
{:activity, nil} ->
- {:error, :not_found}
+ conn
+ |> Fallback.RedirectController.redirector(nil, 404)
e ->
e
diff --git a/lib/pleroma/web/push/push.ex b/lib/pleroma/web/push/push.ex
index ffd2aac91..ddd4fe037 100644
--- a/lib/pleroma/web/push/push.ex
+++ b/lib/pleroma/web/push/push.ex
@@ -5,7 +5,8 @@
defmodule Pleroma.Web.Push do
use GenServer
- alias Pleroma.{Repo, User}
+ alias Pleroma.Repo
+ alias Pleroma.User
alias Pleroma.Web.Push.Subscription
require Logger
diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex
index 82b30950c..242e30910 100644
--- a/lib/pleroma/web/push/subscription.ex
+++ b/lib/pleroma/web/push/subscription.ex
@@ -4,13 +4,16 @@
defmodule Pleroma.Web.Push.Subscription do
use Ecto.Schema
+
import Ecto.Changeset
- alias Pleroma.{Repo, User}
+
+ alias Pleroma.Repo
+ alias Pleroma.User
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Push.Subscription
schema "push_subscriptions" do
- belongs_to(:user, User)
+ belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:token, Token)
field(:endpoint, :string)
field(:key_p256dh, :string)
diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex
new file mode 100644
index 000000000..abb1cf7f2
--- /dev/null
+++ b/lib/pleroma/web/rich_media/helpers.ex
@@ -0,0 +1,21 @@
+# Pleroma: A lightweight social networking server
+# Copyright _ 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.RichMedia.Helpers do
+ alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.HTML
+ alias Pleroma.Web.RichMedia.Parser
+
+ def fetch_data_for_activity(%Activity{} = activity) do
+ with true <- Pleroma.Config.get([:rich_media, :enabled]),
+ %Object{} = object <- Object.normalize(activity.data["object"]),
+ {:ok, page_url} <- HTML.extract_first_external_url(object, object.data["content"]),
+ {:ok, rich_media} <- Parser.parse(page_url) do
+ %{page_url: page_url, rich_media: rich_media}
+ else
+ _ -> %{}
+ end
+ end
+end
diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex
new file mode 100644
index 000000000..4341141df
--- /dev/null
+++ b/lib/pleroma/web/rich_media/parser.ex
@@ -0,0 +1,75 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.RichMedia.Parser do
+ @parsers [
+ Pleroma.Web.RichMedia.Parsers.OGP,
+ Pleroma.Web.RichMedia.Parsers.TwitterCard,
+ Pleroma.Web.RichMedia.Parsers.OEmbed
+ ]
+
+ @hackney_options [
+ pool: :media,
+ timeout: 2_000,
+ recv_timeout: 2_000,
+ max_body: 2_000_000
+ ]
+
+ def parse(nil), do: {:error, "No URL provided"}
+
+ if Mix.env() == :test do
+ def parse(url), do: parse_url(url)
+ else
+ def parse(url) do
+ try do
+ Cachex.fetch!(:rich_media_cache, url, fn _ ->
+ {:commit, parse_url(url)}
+ end)
+ rescue
+ e ->
+ {:error, "Cachex error: #{inspect(e)}"}
+ end
+ end
+ end
+
+ defp parse_url(url) do
+ try do
+ {:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: @hackney_options)
+
+ html |> maybe_parse() |> clean_parsed_data() |> check_parsed_data()
+ rescue
+ e ->
+ {:error, "Parsing error: #{inspect(e)}"}
+ end
+ end
+
+ defp maybe_parse(html) do
+ Enum.reduce_while(@parsers, %{}, fn parser, acc ->
+ case parser.parse(html, acc) do
+ {:ok, data} -> {:halt, data}
+ {:error, _msg} -> {:cont, acc}
+ end
+ end)
+ end
+
+ defp check_parsed_data(%{title: title} = data) when is_binary(title) and byte_size(title) > 0 do
+ {:ok, data}
+ end
+
+ defp check_parsed_data(data) do
+ {:error, "Found metadata was invalid or incomplete: #{inspect(data)}"}
+ end
+
+ defp clean_parsed_data(data) do
+ data
+ |> Enum.reject(fn {key, val} ->
+ with {:ok, _} <- Jason.encode(%{key => val}) do
+ false
+ else
+ _ -> true
+ end
+ end)
+ |> Map.new()
+ end
+end
diff --git a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex
new file mode 100644
index 000000000..4a7c5eae0
--- /dev/null
+++ b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex
@@ -0,0 +1,30 @@
+defmodule Pleroma.Web.RichMedia.Parsers.MetaTagsParser do
+ def parse(html, data, prefix, error_message, key_name, value_name \\ "content") do
+ with elements = [_ | _] <- get_elements(html, key_name, prefix),
+ meta_data =
+ Enum.reduce(elements, data, fn el, acc ->
+ attributes = normalize_attributes(el, prefix, key_name, value_name)
+
+ Map.merge(acc, attributes)
+ end) do
+ {:ok, meta_data}
+ else
+ _e -> {:error, error_message}
+ end
+ end
+
+ defp get_elements(html, key_name, prefix) do
+ html |> Floki.find("meta[#{key_name}^='#{prefix}:']")
+ end
+
+ defp normalize_attributes(html_node, prefix, key_name, value_name) do
+ {_tag, attributes, _children} = html_node
+
+ data =
+ Enum.into(attributes, %{}, fn {name, value} ->
+ {name, String.trim_leading(value, "#{prefix}:")}
+ end)
+
+ %{String.to_atom(data[key_name]) => data[value_name]}
+ end
+end
diff --git a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex
new file mode 100644
index 000000000..2530b8c9d
--- /dev/null
+++ b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex
@@ -0,0 +1,31 @@
+defmodule Pleroma.Web.RichMedia.Parsers.OEmbed do
+ def parse(html, _data) do
+ with elements = [_ | _] <- get_discovery_data(html),
+ {:ok, oembed_url} <- get_oembed_url(elements),
+ {:ok, oembed_data} <- get_oembed_data(oembed_url) do
+ {:ok, oembed_data}
+ else
+ _e -> {:error, "No OEmbed data found"}
+ end
+ end
+
+ defp get_discovery_data(html) do
+ html |> Floki.find("link[type='application/json+oembed']")
+ end
+
+ defp get_oembed_url(nodes) do
+ {"link", attributes, _children} = nodes |> hd()
+
+ {:ok, Enum.into(attributes, %{})["href"]}
+ end
+
+ defp get_oembed_data(url) do
+ {:ok, %Tesla.Env{body: json}} = Pleroma.HTTP.get(url, [], adapter: [pool: :media])
+
+ {:ok, data} = Jason.decode(json)
+
+ data = data |> Map.new(fn {k, v} -> {String.to_atom(k), v} end)
+
+ {:ok, data}
+ end
+end
diff --git a/lib/pleroma/web/rich_media/parsers/ogp.ex b/lib/pleroma/web/rich_media/parsers/ogp.ex
new file mode 100644
index 000000000..0e1a0e719
--- /dev/null
+++ b/lib/pleroma/web/rich_media/parsers/ogp.ex
@@ -0,0 +1,11 @@
+defmodule Pleroma.Web.RichMedia.Parsers.OGP do
+ def parse(html, data) do
+ Pleroma.Web.RichMedia.Parsers.MetaTagsParser.parse(
+ html,
+ data,
+ "og",
+ "No OGP metadata found",
+ "property"
+ )
+ end
+end
diff --git a/lib/pleroma/web/rich_media/parsers/twitter_card.ex b/lib/pleroma/web/rich_media/parsers/twitter_card.ex
new file mode 100644
index 000000000..a317c3e78
--- /dev/null
+++ b/lib/pleroma/web/rich_media/parsers/twitter_card.ex
@@ -0,0 +1,11 @@
+defmodule Pleroma.Web.RichMedia.Parsers.TwitterCard do
+ def parse(html, data) do
+ Pleroma.Web.RichMedia.Parsers.MetaTagsParser.parse(
+ html,
+ data,
+ "twitter",
+ "No twitter card metadata found",
+ "name"
+ )
+ end
+end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 1f929ee21..d66a1c2a1 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -107,6 +107,11 @@ defmodule Pleroma.Web.Router do
get("/captcha", UtilController, :captcha)
end
+ scope "/api/pleroma", Pleroma.Web do
+ pipe_through(:pleroma_api)
+ post("/uploader_callback/:upload_path", UploaderController, :callback)
+ end
+
scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:admin_api)
delete("/user", AdminAPIController, :user_delete)
@@ -180,6 +185,7 @@ defmodule Pleroma.Web.Router do
get("/timelines/direct", MastodonAPIController, :dm_timeline)
get("/favourites", MastodonAPIController, :favourites)
+ get("/bookmarks", MastodonAPIController, :bookmarks)
post("/statuses", MastodonAPIController, :post_status)
delete("/statuses/:id", MastodonAPIController, :delete_status)
@@ -188,6 +194,12 @@ defmodule Pleroma.Web.Router do
post("/statuses/:id/unreblog", MastodonAPIController, :unreblog_status)
post("/statuses/:id/favourite", MastodonAPIController, :fav_status)
post("/statuses/:id/unfavourite", MastodonAPIController, :unfav_status)
+ post("/statuses/:id/pin", MastodonAPIController, :pin_status)
+ post("/statuses/:id/unpin", MastodonAPIController, :unpin_status)
+ post("/statuses/:id/bookmark", MastodonAPIController, :bookmark_status)
+ post("/statuses/:id/unbookmark", MastodonAPIController, :unbookmark_status)
+ post("/statuses/:id/mute", MastodonAPIController, :mute_conversation)
+ post("/statuses/:id/unmute", MastodonAPIController, :unmute_conversation)
post("/notifications/clear", MastodonAPIController, :clear_notifications)
post("/notifications/dismiss", MastodonAPIController, :dismiss_notification)
@@ -245,7 +257,7 @@ defmodule Pleroma.Web.Router do
get("/statuses/:id", MastodonAPIController, :get_status)
get("/statuses/:id/context", MastodonAPIController, :get_context)
- get("/statuses/:id/card", MastodonAPIController, :empty_object)
+ get("/statuses/:id/card", MastodonAPIController, :status_card)
get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by)
get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by)
@@ -271,6 +283,7 @@ defmodule Pleroma.Web.Router do
post("/help/test", TwitterAPI.UtilController, :help_test)
get("/statusnet/config", TwitterAPI.UtilController, :config)
get("/statusnet/version", TwitterAPI.UtilController, :version)
+ get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations)
end
scope "/api", Pleroma.Web do
@@ -347,6 +360,9 @@ defmodule Pleroma.Web.Router do
post("/statuses/unretweet/:id", TwitterAPI.Controller, :unretweet)
post("/statuses/destroy/:id", TwitterAPI.Controller, :delete_post)
+ post("/statuses/pin/:id", TwitterAPI.Controller, :pin)
+ post("/statuses/unpin/:id", TwitterAPI.Controller, :unpin)
+
get("/pleroma/friend_requests", TwitterAPI.Controller, :friend_requests)
post("/pleroma/friendships/approve", TwitterAPI.Controller, :approve_friend_request)
post("/pleroma/friendships/deny", TwitterAPI.Controller, :deny_friend_request)
@@ -380,7 +396,11 @@ defmodule Pleroma.Web.Router do
end
pipeline :ostatus do
- plug(:accepts, ["xml", "atom", "html", "activity+json"])
+ plug(:accepts, ["html", "xml", "atom", "activity+json"])
+ end
+
+ pipeline :oembed do
+ plug(:accepts, ["json", "xml"])
end
scope "/", Pleroma.Web do
@@ -398,6 +418,12 @@ defmodule Pleroma.Web.Router do
post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
end
+ scope "/", Pleroma.Web do
+ pipe_through(:oembed)
+
+ get("/oembed", OEmbed.OEmbedController, :url)
+ end
+
pipeline :activitypub do
plug(:accepts, ["activity+json"])
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
@@ -410,6 +436,7 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/followers", ActivityPubController, :followers)
get("/users/:nickname/following", ActivityPubController, :following)
get("/users/:nickname/outbox", ActivityPubController, :outbox)
+ get("/objects/:uuid/likes", ActivityPubController, :object_likes)
end
pipeline :activitypub_client do
@@ -429,6 +456,7 @@ defmodule Pleroma.Web.Router do
scope "/", Pleroma.Web.ActivityPub do
pipe_through([:activitypub_client])
+ get("/api/ap/whoami", ActivityPubController, :whoami)
get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
end
@@ -440,8 +468,8 @@ defmodule Pleroma.Web.Router do
scope "/", Pleroma.Web.ActivityPub do
pipe_through(:activitypub)
- post("/users/:nickname/inbox", ActivityPubController, :inbox)
post("/inbox", ActivityPubController, :inbox)
+ post("/users/:nickname/inbox", ActivityPubController, :inbox)
end
scope "/.well-known", Pleroma.Web do
@@ -484,6 +512,7 @@ defmodule Pleroma.Web.Router do
scope "/", Fallback do
get("/registration/:token", RedirectController, :registration_page)
+ get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)
get("/*path", RedirectController, :redirector)
options("/*path", RedirectController, :empty)
@@ -492,11 +521,36 @@ end
defmodule Fallback.RedirectController do
use Pleroma.Web, :controller
+ alias Pleroma.Web.Metadata
+ alias Pleroma.User
- def redirector(conn, _params) do
+ def redirector(conn, _params, code \\ 200) do
conn
|> put_resp_content_type("text/html")
- |> send_file(200, Pleroma.Plugs.InstanceStatic.file_path("index.html"))
+ |> send_file(code, index_file_path())
+ end
+
+ def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do
+ with %User{} = user <- User.get_cached_by_nickname_or_id(maybe_nickname_or_id) do
+ redirector_with_meta(conn, %{user: user})
+ else
+ nil ->
+ redirector(conn, params)
+ end
+ end
+
+ def redirector_with_meta(conn, params) do
+ {:ok, index_content} = File.read(index_file_path())
+ tags = Metadata.build_tags(params)
+ response = String.replace(index_content, "<!--server-generated-meta-->", tags)
+
+ conn
+ |> put_resp_content_type("text/html")
+ |> send_resp(200, response)
+ end
+
+ def index_file_path do
+ Pleroma.Plugs.InstanceStatic.file_path("index.html")
end
def registration_page(conn, params) do
diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex
index e41657da1..a5a9e16c6 100644
--- a/lib/pleroma/web/salmon/salmon.ex
+++ b/lib/pleroma/web/salmon/salmon.ex
@@ -6,9 +6,12 @@ defmodule Pleroma.Web.Salmon do
@httpoison Application.get_env(:pleroma, :httpoison)
use Bitwise
+
+ alias Pleroma.Instances
+ alias Pleroma.User
alias Pleroma.Web.XML
alias Pleroma.Web.OStatus.ActivityRepresenter
- alias Pleroma.User
+
require Logger
def decode(salmon) do
@@ -161,25 +164,31 @@ defmodule Pleroma.Web.Salmon do
|> Enum.filter(fn user -> user && !user.local end)
end
- # push an activity to remote accounts
- #
- defp send_to_user(%{info: %{salmon: salmon}}, feed, poster),
- do: send_to_user(salmon, feed, poster)
+ @doc "Pushes an activity to remote account."
+ def send_to_user(%{recipient: %{info: %{salmon: salmon}}} = params),
+ do: send_to_user(Map.put(params, :recipient, salmon))
- defp send_to_user(url, feed, poster) when is_binary(url) do
- with {:ok, %{status: code}} <-
+ def send_to_user(%{recipient: url, feed: feed, poster: poster} = params) when is_binary(url) do
+ with {:ok, %{status: code}} when code in 200..299 <-
poster.(
url,
feed,
[{"Content-Type", "application/magic-envelope+xml"}]
) do
+ if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
+ do: Instances.set_reachable(url)
+
Logger.debug(fn -> "Pushed to #{url}, code #{code}" end)
+ :ok
else
- e -> Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end)
+ e ->
+ unless params[:unreachable_since], do: Instances.set_reachable(url)
+ Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end)
+ :error
end
end
- defp send_to_user(_, _, _), do: nil
+ def send_to_user(_), do: :noop
@supported_activities [
"Create",
@@ -209,12 +218,23 @@ defmodule Pleroma.Web.Salmon do
{:ok, private, _} = keys_from_pem(keys)
{:ok, feed} = encode(private, feed)
- remote_users(activity)
+ remote_users = remote_users(activity)
+
+ salmon_urls = Enum.map(remote_users, & &1.info.salmon)
+ reachable_urls_metadata = Instances.filter_reachable(salmon_urls)
+ reachable_urls = Map.keys(reachable_urls_metadata)
+
+ remote_users
+ |> Enum.filter(&(&1.info.salmon in reachable_urls))
|> Enum.each(fn remote_user ->
- Task.start(fn ->
- Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end)
- send_to_user(remote_user, feed, poster)
- end)
+ Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end)
+
+ Pleroma.Web.Federator.enqueue(:publish_single_salmon, %{
+ recipient: remote_user,
+ feed: feed,
+ poster: poster,
+ unreachable_since: reachable_urls_metadata[remote_user.info.salmon]
+ })
end)
end
end
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index 3136b1b9d..4de7608e4 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -5,7 +5,11 @@
defmodule Pleroma.Web.Streamer do
use GenServer
require Logger
- alias Pleroma.{User, Notification, Activity, Object, Repo}
+ alias Pleroma.User
+ alias Pleroma.Notification
+ alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.Repo
alias Pleroma.Web.ActivityPub.ActivityPub
@keepalive_interval :timer.seconds(30)
@@ -205,6 +209,15 @@ defmodule Pleroma.Web.Streamer do
end)
end
+ def push_to_socket(topics, topic, %Activity{id: id, data: %{"type" => "Delete"}}) do
+ Enum.each(topics[topic] || [], fn socket ->
+ send(
+ socket.transport_pid,
+ {:text, %{event: "delete", payload: to_string(id)} |> Jason.encode!()}
+ )
+ end)
+ end
+
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.
diff --git a/lib/pleroma/web/templates/layout/app.html.eex b/lib/pleroma/web/templates/layout/app.html.eex
index 2e96c1509..520e4b3d5 100644
--- a/lib/pleroma/web/templates/layout/app.html.eex
+++ b/lib/pleroma/web/templates/layout/app.html.eex
@@ -1,7 +1,8 @@
<!DOCTYPE html>
<html>
<head>
- <meta charset=utf-8 />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" />
<title>
<%= Application.get_env(:pleroma, :instance)[:name] %>
</title>
@@ -66,6 +67,32 @@
font-weight: 500;
font-size: 16px;
}
+
+ .alert-danger {
+ box-sizing: border-box;
+ width: 100%;
+ color: #D8000C;
+ background-color: #FFD2D2;
+ border-radius: 4px;
+ border: none;
+ padding: 10px;
+ margin-top: 20px;
+ font-weight: 500;
+ font-size: 16px;
+ }
+
+ .alert-info {
+ box-sizing: border-box;
+ width: 100%;
+ color: #00529B;
+ background-color: #BDE5F8;
+ border-radius: 4px;
+ border: none;
+ padding: 10px;
+ margin-top: 20px;
+ font-weight: 500;
+ font-size: 16px;
+ }
</style>
</head>
<body>
diff --git a/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex b/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex
index 0862412ea..9a725e420 100644
--- a/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex
+++ b/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex
@@ -1,23 +1,28 @@
<!DOCTYPE html>
<html lang='en'>
<head>
+<meta charset='utf-8'>
+<meta content='width=device-width, initial-scale=1' name='viewport'>
<title>
<%= Application.get_env(:pleroma, :instance)[:name] %>
</title>
-<meta charset='utf-8'>
-<meta content='width=device-width, initial-scale=1' name='viewport'>
<link rel="icon" type="image/png" href="/favicon.png"/>
-<link rel="stylesheet" media="all" href="/packs/common.css" />
-<link rel="stylesheet" media="all" href="/packs/default.css" />
+<script crossorigin='anonymous' src="/packs/locales.js"></script>
+<script crossorigin='anonymous' src="/packs/locales/glitch/en.js"></script>
-<script src="/packs/common.js"></script>
-<script src="/packs/locale_en.js"></script>
-<link as='script' crossorigin='anonymous' href='/packs/features/getting_started.js' rel='preload'>
-<link as='script' crossorigin='anonymous' href='/packs/features/compose.js' rel='preload'>
-<link as='script' crossorigin='anonymous' href='/packs/features/home_timeline.js' rel='preload'>
-<link as='script' crossorigin='anonymous' href='/packs/features/notifications.js' rel='preload'>
+<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/getting_started.js'>
+<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/compose.js'>
+<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/home_timeline.js'>
+<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/notifications.js'>
<script id='initial-state' type='application/json'><%= raw @initial_state %></script>
-<script src="/packs/application.js"></script>
+
+<script src="/packs/core/common.js"></script>
+<link rel="stylesheet" media="all" href="/packs/core/common.css" />
+
+<script src="/packs/flavours/glitch/common.js"></script>
+<link rel="stylesheet" media="all" href="/packs/flavours/glitch/common.css" />
+
+<script src="/packs/flavours/glitch/home.js"></script>
</head>
<body class='app-body no-reduce-motion system-font'>
<div class='app-holder' data-props='{&quot;locale&quot;:&quot;en&quot;}' id='mastodon'>
diff --git a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex
index de2241ec9..32c458f0c 100644
--- a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex
+++ b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex
@@ -1,5 +1,9 @@
+<%= if get_flash(@conn, :info) do %>
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
+<% end %>
+<%= if get_flash(@conn, :error) do %>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
+<% end %>
<h2>OAuth Authorization</h2>
<%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %>
<%= label f, :name, "Name or email" %>
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index a79072f3d..e2fdedb25 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -4,14 +4,19 @@
defmodule Pleroma.Web.TwitterAPI.UtilController do
use Pleroma.Web, :controller
+
require Logger
+
+ alias Comeonin.Pbkdf2
+ alias Pleroma.Emoji
+ alias Pleroma.PasswordResetToken
+ alias Pleroma.User
+ alias Pleroma.Repo
alias Pleroma.Web
+ alias Pleroma.Web.CommonAPI
alias Pleroma.Web.OStatus
alias Pleroma.Web.WebFinger
- alias Pleroma.Web.CommonAPI
- alias Comeonin.Pbkdf2
alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.{Repo, PasswordResetToken, User, Emoji}
def show_password_reset(conn, %{"token" => token}) do
with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
@@ -183,25 +188,31 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
invitesEnabled: if(Keyword.get(instance, :invites_enabled, false), do: "1", else: "0")
}
- 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),
- 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),
- scopeCopy: Keyword.get(instance_fe, :scope_copy),
- subjectLineBehavior: Keyword.get(instance_fe, :subject_line_behavior),
- alwaysShowSubjectInput: Keyword.get(instance_fe, :always_show_subject_input)
- }
+ pleroma_fe =
+ if instance_fe do
+ %{
+ 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),
+ scopeCopy: Keyword.get(instance_fe, :scope_copy),
+ subjectLineBehavior: Keyword.get(instance_fe, :subject_line_behavior),
+ alwaysShowSubjectInput: Keyword.get(instance_fe, :always_show_subject_input)
+ }
+ else
+ Pleroma.Config.get([:frontend_configurations, :pleroma_fe])
+ end
managed_config = Keyword.get(instance, :managed_config)
@@ -216,6 +227,14 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
+ def frontend_configurations(conn, _params) do
+ config =
+ Pleroma.Config.get(:frontend_configurations, %{})
+ |> Enum.into(%{})
+
+ json(conn, config)
+ end
+
def version(conn, _params) do
version = Pleroma.Application.named_version()
diff --git a/lib/pleroma/web/twitter_api/representers/activity_representer.ex b/lib/pleroma/web/twitter_api/representers/activity_representer.ex
index 2a221cc66..192ab7334 100644
--- a/lib/pleroma/web/twitter_api/representers/activity_representer.ex
+++ b/lib/pleroma/web/twitter_api/representers/activity_representer.ex
@@ -2,16 +2,21 @@
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
+# FIXME: Remove this module?
# THIS MODULE IS DEPRECATED! DON'T USE IT!
# USE THE Pleroma.Web.TwitterAPI.Views.ActivityView MODULE!
defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
use Pleroma.Web.TwitterAPI.Representers.BaseRepresenter
alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter
- alias Pleroma.{Activity, User}
- alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView}
- alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Activity
alias Pleroma.Formatter
alias Pleroma.HTML
+ alias Pleroma.User
+ alias Pleroma.Web.TwitterAPI.ActivityView
+ alias Pleroma.Web.TwitterAPI.TwitterAPI
+ alias Pleroma.Web.TwitterAPI.UserView
+ alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.MastodonAPI.StatusView
defp user_by_ap_id(user_list, ap_id) do
Enum.find(user_list, fn %{ap_id: user_id} -> ap_id == user_id end)
@@ -153,11 +158,14 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
announcement_count = object["announcement_count"] || 0
favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || [])
repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || [])
+ pinned = activity.id in user.info.pinned_activities
mentions = opts[:mentioned] || []
attentions =
- activity.recipients
+ []
+ |> Utils.maybe_notify_to_recipients(activity)
+ |> Utils.maybe_notify_mentioned_recipients(activity)
|> Enum.map(fn ap_id -> Enum.find(mentions, fn user -> ap_id == user.ap_id end) end)
|> Enum.filter(& &1)
|> Enum.map(fn user -> UserView.render("show.json", %{user: user, for: opts[:for]}) end)
@@ -181,6 +189,14 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
reply_user = reply_parent && User.get_cached_by_ap_id(reply_parent.actor)
+ summary = HTML.strip_tags(object["summary"])
+
+ card =
+ StatusView.render(
+ "card.json",
+ Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
+ )
+
%{
"id" => activity.id,
"uri" => activity.data["object"]["id"],
@@ -202,12 +218,15 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
"repeat_num" => announcement_count,
"favorited" => to_boolean(favorited),
"repeated" => to_boolean(repeated),
+ "pinned" => pinned,
"external_url" => object["external_url"] || object["id"],
"tags" => tags,
"activity_type" => "post",
"possibly_sensitive" => possibly_sensitive,
"visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object),
- "summary" => object["summary"]
+ "summary" => summary,
+ "summary_html" => summary |> Formatter.emojify(object["emoji"]),
+ "card" => card
}
end
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index ecf81d492..db521a3ad 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -3,8 +3,13 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
- alias Pleroma.{UserInviteToken, User, Activity, Repo, Object}
- alias Pleroma.{UserEmail, Mailer}
+ alias Pleroma.UserInviteToken
+ alias Pleroma.User
+ alias Pleroma.Activity
+ alias Pleroma.Repo
+ alias Pleroma.Object
+ alias Pleroma.UserEmail
+ alias Pleroma.Mailer
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.TwitterAPI.UserView
alias Pleroma.Web.CommonAPI
@@ -70,28 +75,36 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
def repeat(%User{} = user, ap_id_or_id) do
with {:ok, _announce, %{data: %{"id" => id}}} <- CommonAPI.repeat(ap_id_or_id, user),
- %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
+ %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
{:ok, activity}
end
end
def unrepeat(%User{} = user, 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
+ %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
{:ok, activity}
end
end
+ def pin(%User{} = user, ap_id_or_id) do
+ CommonAPI.pin(ap_id_or_id, user)
+ end
+
+ def unpin(%User{} = user, ap_id_or_id) do
+ CommonAPI.unpin(ap_id_or_id, user)
+ end
+
def fav(%User{} = user, 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
+ %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
{:ok, activity}
end
end
def unfav(%User{} = user, ap_id_or_id) do
with {:ok, _unfav, _fav, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
- %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
+ %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
{:ok, activity}
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 1e04b8c4b..c2f0dc2a9 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -7,12 +7,19 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
- alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView, NotificationView}
- alias Pleroma.Web.CommonAPI
- alias Pleroma.{Repo, Activity, Object, User, Notification}
+ alias Ecto.Changeset
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
- alias Ecto.Changeset
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.TwitterAPI.ActivityView
+ alias Pleroma.Web.TwitterAPI.NotificationView
+ alias Pleroma.Web.TwitterAPI.TwitterAPI
+ alias Pleroma.Web.TwitterAPI.UserView
+ alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.Notification
+ alias Pleroma.Repo
+ alias Pleroma.User
require Logger
@@ -24,7 +31,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
conn
|> put_view(UserView)
- |> render("show.json", %{user: user, token: token})
+ |> render("show.json", %{user: user, token: token, for: user})
end
def status_update(%{assigns: %{user: user}} = conn, %{"status" => _} = status_data) do
@@ -265,8 +272,6 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- id = String.to_integer(id)
-
with context when is_binary(context) <- TwitterAPI.conversation_id_to_context(id),
activities <-
ActivityPub.fetch_activities_for_context(context, %{
@@ -330,48 +335,74 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def get_by_id_or_ap_id(id) do
- activity = Repo.get(Activity, id) || Activity.get_create_activity_by_object_ap_id(id)
+ activity = Repo.get(Activity, id) || Activity.get_create_by_object_ap_id(id)
if activity.data["type"] == "Create" do
activity
else
- Activity.get_create_activity_by_object_ap_id(activity.data["object"])
+ Activity.get_create_by_object_ap_id(activity.data["object"])
end
end
def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
- {:ok, activity} <- TwitterAPI.fav(user, id) do
+ with {:ok, activity} <- TwitterAPI.fav(user, id) do
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
+ else
+ _ -> json_reply(conn, 400, Jason.encode!(%{}))
end
end
def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
- {:ok, activity} <- TwitterAPI.unfav(user, id) do
+ with {:ok, activity} <- TwitterAPI.unfav(user, id) do
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
+ else
+ _ -> json_reply(conn, 400, Jason.encode!(%{}))
end
end
def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
- {:ok, activity} <- TwitterAPI.repeat(user, id) do
+ with {:ok, activity} <- TwitterAPI.repeat(user, id) do
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
+ else
+ _ -> json_reply(conn, 400, Jason.encode!(%{}))
end
end
def unretweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
- {:ok, activity} <- TwitterAPI.unrepeat(user, id) do
+ with {:ok, activity} <- TwitterAPI.unrepeat(user, id) do
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
+ else
+ _ -> json_reply(conn, 400, Jason.encode!(%{}))
+ end
+ end
+
+ def pin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with {:ok, activity} <- TwitterAPI.pin(user, id) do
+ conn
+ |> put_view(ActivityView)
+ |> render("activity.json", %{activity: activity, for: user})
+ else
+ {:error, message} -> bad_request_reply(conn, message)
+ err -> err
+ end
+ end
+
+ def unpin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with {:ok, activity} <- TwitterAPI.unpin(user, id) do
+ conn
+ |> put_view(ActivityView)
+ |> render("activity.json", %{activity: activity, for: user})
+ else
+ {:error, message} -> bad_request_reply(conn, message)
+ err -> err
end
end
@@ -472,12 +503,14 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def followers(%{assigns: %{user: for_user}} = conn, params) do
+ {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
+
with {:ok, user} <- TwitterAPI.get_user(for_user, params),
- {:ok, followers} <- User.get_followers(user) do
+ {:ok, followers} <- User.get_followers(user, page) do
followers =
cond do
for_user && user.id == for_user.id -> followers
- user.info.hide_network -> []
+ user.info.hide_followers -> []
true -> followers
end
@@ -490,12 +523,14 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def friends(%{assigns: %{user: for_user}} = conn, params) do
+ {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
+
with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
- {:ok, friends} <- User.get_friends(user) do
+ {:ok, friends} <- User.get_friends(user, page) do
friends =
cond do
for_user && user.id == for_user.id -> friends
- user.info.hide_network -> []
+ user.info.hide_follows -> []
true -> friends
end
@@ -528,7 +563,6 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
def approve_friend_request(conn, %{"user_id" => uid} = _params) do
with followed <- conn.assigns[:user],
- uid when is_number(uid) <- String.to_integer(uid),
%User{} = follower <- Repo.get(User, uid),
{:ok, follower} <- User.maybe_follow(follower, followed),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
@@ -550,7 +584,6 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
def deny_friend_request(conn, %{"user_id" => uid} = _params) do
with followed <- conn.assigns[:user],
- uid when is_number(uid) <- String.to_integer(uid),
%User{} = follower <- Repo.get(User, uid),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
@@ -592,7 +625,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
defp build_info_cng(user, params) do
info_params =
- ["no_rich_text", "locked", "hide_network"]
+ ["no_rich_text", "locked", "hide_followers", "hide_follows", "show_role"]
|> Enum.reduce(%{}, fn key, res ->
if value = params[key] do
Map.put(res, key, value == "true")
@@ -647,7 +680,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
- users = User.search(query, true)
+ users = User.search(query, true, user)
conn
|> put_view(UserView)
diff --git a/lib/pleroma/web/twitter_api/views/activity_view.ex b/lib/pleroma/web/twitter_api/views/activity_view.ex
index 84f35ebf9..661022afa 100644
--- a/lib/pleroma/web/twitter_api/views/activity_view.ex
+++ b/lib/pleroma/web/twitter_api/views/activity_view.ex
@@ -4,18 +4,18 @@
defmodule Pleroma.Web.TwitterAPI.ActivityView do
use Pleroma.Web, :view
- alias Pleroma.Web.CommonAPI.Utils
- alias Pleroma.User
- alias Pleroma.Web.TwitterAPI.UserView
- alias Pleroma.Web.TwitterAPI.ActivityView
- alias Pleroma.Web.TwitterAPI.TwitterAPI
- alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter
alias Pleroma.Activity
+ alias Pleroma.Formatter
alias Pleroma.HTML
alias Pleroma.Object
- alias Pleroma.User
alias Pleroma.Repo
- alias Pleroma.Formatter
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.TwitterAPI.ActivityView
+ alias Pleroma.Web.TwitterAPI.TwitterAPI
+ alias Pleroma.Web.TwitterAPI.UserView
+ alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter
import Ecto.Query
require Logger
@@ -94,8 +94,14 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
ap_id == "https://www.w3.org/ns/activitystreams#Public" ->
nil
+ user = User.get_cached_by_ap_id(ap_id) ->
+ user
+
+ user = User.get_by_guessed_nickname(ap_id) ->
+ user
+
true ->
- User.get_cached_by_ap_id(ap_id)
+ User.error_user(ap_id)
end
end
@@ -108,7 +114,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
|> Map.put(:context_ids, context_ids)
|> Map.put(:users, users)
- render_many(
+ safe_render_many(
opts.activities,
ActivityView,
"activity.json",
@@ -162,7 +168,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
def render("activity.json", %{activity: %{data: %{"type" => "Announce"}} = activity} = opts) do
user = get_user(activity.data["actor"], opts)
created_at = activity.data["published"] |> Utils.date_to_asctime()
- announced_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
+ announced_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
text = "#{user.nickname} retweeted a status."
@@ -186,7 +192,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
def render("activity.json", %{activity: %{data: %{"type" => "Like"}} = activity} = opts) do
user = get_user(activity.data["actor"], opts)
- liked_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
+ liked_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
liked_activity_id = if liked_activity, do: liked_activity.id, else: nil
created_at =
@@ -227,9 +233,12 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
announcement_count = object["announcement_count"] || 0
favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || [])
repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || [])
+ pinned = activity.id in user.info.pinned_activities
attentions =
- activity.recipients
+ []
+ |> Utils.maybe_notify_to_recipients(activity)
+ |> Utils.maybe_notify_mentioned_recipients(activity)
|> Enum.map(fn ap_id -> get_user(ap_id, opts) end)
|> Enum.filter(& &1)
|> Enum.map(fn user -> UserView.render("show.json", %{user: user, for: opts[:for]}) end)
@@ -245,20 +254,32 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
html =
content
- |> HTML.get_cached_scrubbed_html_for_object(User.html_filter_policy(opts[:for]), activity)
+ |> HTML.get_cached_scrubbed_html_for_object(
+ User.html_filter_policy(opts[:for]),
+ activity,
+ __MODULE__
+ )
|> Formatter.emojify(object["emoji"])
text =
if content do
content
|> String.replace(~r/<br\s?\/?>/, "\n")
- |> HTML.get_cached_stripped_html_for_object(activity)
+ |> HTML.get_cached_stripped_html_for_object(activity, __MODULE__)
end
reply_parent = Activity.get_in_reply_to_activity(activity)
reply_user = reply_parent && User.get_cached_by_ap_id(reply_parent.actor)
+ summary = HTML.strip_tags(summary)
+
+ card =
+ StatusView.render(
+ "card.json",
+ Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
+ )
+
%{
"id" => activity.id,
"uri" => activity.data["object"]["id"],
@@ -280,12 +301,15 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
"repeat_num" => announcement_count,
"favorited" => !!favorited,
"repeated" => !!repeated,
+ "pinned" => pinned,
"external_url" => object["external_url"] || object["id"],
"tags" => tags,
"activity_type" => "post",
"possibly_sensitive" => possibly_sensitive,
- "visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object),
- "summary" => summary
+ "visibility" => StatusView.get_visibility(object),
+ "summary" => summary,
+ "summary_html" => summary |> Formatter.emojify(object["emoji"]),
+ "card" => card
}
end
diff --git a/lib/pleroma/web/twitter_api/views/notification_view.ex b/lib/pleroma/web/twitter_api/views/notification_view.ex
index d6a1c0a4d..e7c7a7496 100644
--- a/lib/pleroma/web/twitter_api/views/notification_view.ex
+++ b/lib/pleroma/web/twitter_api/views/notification_view.ex
@@ -4,10 +4,11 @@
defmodule Pleroma.Web.TwitterAPI.NotificationView do
use Pleroma.Web, :view
- alias Pleroma.{Notification, User}
+ alias Pleroma.Notification
+ alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
- alias Pleroma.Web.TwitterAPI.UserView
alias Pleroma.Web.TwitterAPI.ActivityView
+ alias Pleroma.Web.TwitterAPI.UserView
defp get_user(ap_id, opts) do
cond do
diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex
index a8cf83613..a09450df7 100644
--- a/lib/pleroma/web/twitter_api/views/user_view.ex
+++ b/lib/pleroma/web/twitter_api/views/user_view.ex
@@ -4,11 +4,11 @@
defmodule Pleroma.Web.TwitterAPI.UserView do
use Pleroma.Web, :view
- alias Pleroma.User
alias Pleroma.Formatter
+ alias Pleroma.HTML
+ alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MediaProxy
- alias Pleroma.HTML
def render("show.json", %{user: user = %User{}} = assigns) do
render_one(user, Pleroma.Web.TwitterAPI.UserView, "user.json", assigns)
@@ -108,6 +108,8 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
"locked" => user.info.locked,
"default_scope" => user.info.default_scope,
"no_rich_text" => user.info.no_rich_text,
+ "hide_followers" => user.info.hide_followers,
+ "hide_follows" => user.info.hide_follows,
"fields" => fields,
# Pleroma extension
@@ -117,6 +119,12 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
}
}
+ data =
+ if(user.info.is_admin || user.info.is_moderator,
+ do: maybe_with_role(data, user, for_user),
+ else: data
+ )
+
if assigns[:token] do
Map.put(data, "token", token_string(assigns[:token]))
else
@@ -124,6 +132,20 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
end
end
+ defp maybe_with_role(data, %User{id: id} = user, %User{id: id}) do
+ Map.merge(data, %{"role" => role(user), "show_role" => user.info.show_role})
+ end
+
+ defp maybe_with_role(data, %User{info: %{show_role: true}} = user, _user) do
+ Map.merge(data, %{"role" => role(user)})
+ end
+
+ defp maybe_with_role(data, _, _), do: data
+
+ defp role(%User{info: %{:is_admin => true}}), do: "admin"
+ defp role(%User{info: %{:is_moderator => true}}), do: "moderator"
+ defp role(_), do: "member"
+
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil
diff --git a/lib/pleroma/web/uploader_controller.ex b/lib/pleroma/web/uploader_controller.ex
new file mode 100644
index 000000000..5d8a77346
--- /dev/null
+++ b/lib/pleroma/web/uploader_controller.ex
@@ -0,0 +1,25 @@
+defmodule Pleroma.Web.UploaderController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Uploaders.Uploader
+
+ def callback(conn, %{"upload_path" => upload_path} = params) do
+ process_callback(conn, :global.whereis_name({Uploader, upload_path}), params)
+ end
+
+ def callbacks(conn, _) do
+ send_resp(conn, 400, "bad request")
+ end
+
+ defp process_callback(conn, pid, params) when is_pid(pid) do
+ send(pid, {Uploader, self(), conn, params})
+
+ receive do
+ {Uploader, conn} -> conn
+ end
+ end
+
+ defp process_callback(conn, _, _) do
+ send_resp(conn, 400, "bad request")
+ end
+end
diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex
index 74b13f929..853aa2a87 100644
--- a/lib/pleroma/web/web.ex
+++ b/lib/pleroma/web/web.ex
@@ -24,7 +24,8 @@ defmodule Pleroma.Web do
quote do
use Phoenix.Controller, namespace: Pleroma.Web
import Plug.Conn
- import Pleroma.Web.{Gettext, Router.Helpers}
+ import Pleroma.Web.Gettext
+ import Pleroma.Web.Router.Helpers
end
end
@@ -37,13 +38,43 @@ defmodule Pleroma.Web do
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]
- import Pleroma.Web.{ErrorHelpers, Gettext, Router.Helpers}
+ import Pleroma.Web.ErrorHelpers
+ import Pleroma.Web.Gettext
+ import Pleroma.Web.Router.Helpers
+
+ require Logger
+
+ @doc "Same as `render/3` but wrapped in a rescue block"
+ def safe_render(view, template, assigns \\ %{}) do
+ Phoenix.View.render(view, template, assigns)
+ rescue
+ error ->
+ Logger.error(
+ "#{__MODULE__} failed to render #{inspect({view, template})}: #{inspect(error)}"
+ )
+
+ Logger.error(inspect(__STACKTRACE__))
+ nil
+ end
+
+ @doc """
+ Same as `render_many/4` but wrapped in rescue block.
+ """
+ def safe_render_many(collection, view, template, assigns \\ %{}) do
+ Enum.map(collection, fn resource ->
+ as = Map.get(assigns, :as) || view.__resource__
+ assigns = Map.put(assigns, as, resource)
+ safe_render(view, template, assigns)
+ end)
+ |> Enum.filter(& &1)
+ end
end
end
def router do
quote do
use Phoenix.Router
+ # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
import Plug.Conn
import Phoenix.Controller
end
@@ -51,6 +82,7 @@ defmodule Pleroma.Web do
def channel do
quote do
+ # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
use Phoenix.Channel
import Pleroma.Web.Gettext
end
diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex
index 0a6338312..5ea5ae48e 100644
--- a/lib/pleroma/web/web_finger/web_finger.ex
+++ b/lib/pleroma/web/web_finger/web_finger.ex
@@ -5,9 +5,12 @@
defmodule Pleroma.Web.WebFinger do
@httpoison Application.get_env(:pleroma, :httpoison)
- alias Pleroma.{User, XmlBuilder}
+ alias Pleroma.User
+ alias Pleroma.XmlBuilder
alias Pleroma.Web
- alias Pleroma.Web.{XML, Salmon, OStatus}
+ alias Pleroma.Web.XML
+ alias Pleroma.Web.Salmon
+ alias Pleroma.Web.OStatus
require Jason
require Logger
diff --git a/lib/pleroma/web/websub/websub.ex b/lib/pleroma/web/websub/websub.ex
index 3a287edd9..a08d7993d 100644
--- a/lib/pleroma/web/websub/websub.ex
+++ b/lib/pleroma/web/websub/websub.ex
@@ -4,10 +4,14 @@
defmodule Pleroma.Web.Websub do
alias Ecto.Changeset
+ alias Pleroma.Instances
alias Pleroma.Repo
- alias Pleroma.Web.Websub.{WebsubServerSubscription, WebsubClientSubscription}
+ alias Pleroma.Web.Websub.WebsubServerSubscription
+ alias Pleroma.Web.Websub.WebsubClientSubscription
alias Pleroma.Web.OStatus.FeedRepresenter
- alias Pleroma.Web.{XML, Endpoint, OStatus}
+ alias Pleroma.Web.XML
+ alias Pleroma.Web.Endpoint
+ alias Pleroma.Web.OStatus
alias Pleroma.Web.Router.Helpers
require Logger
@@ -53,28 +57,34 @@ defmodule Pleroma.Web.Websub do
]
def publish(topic, user, %{data: %{"type" => type}} = activity)
when type in @supported_activities do
- # TODO: Only send to still valid subscriptions.
+ response =
+ user
+ |> FeedRepresenter.to_simple_form([activity], [user])
+ |> :xmerl.export_simple(:xmerl_xml)
+ |> to_string
+
query =
from(
sub in WebsubServerSubscription,
where: sub.topic == ^topic and sub.state == "active",
- where: fragment("? > NOW()", sub.valid_until)
+ where: fragment("? > (NOW() at time zone 'UTC')", sub.valid_until)
)
subscriptions = Repo.all(query)
- Enum.each(subscriptions, fn sub ->
- response =
- user
- |> FeedRepresenter.to_simple_form([activity], [user])
- |> :xmerl.export_simple(:xmerl_xml)
- |> to_string
+ callbacks = Enum.map(subscriptions, & &1.callback)
+ reachable_callbacks_metadata = Instances.filter_reachable(callbacks)
+ reachable_callbacks = Map.keys(reachable_callbacks_metadata)
+ subscriptions
+ |> Enum.filter(&(&1.callback in reachable_callbacks))
+ |> Enum.each(fn sub ->
data = %{
xml: response,
topic: topic,
callback: sub.callback,
- secret: sub.secret
+ secret: sub.secret,
+ unreachable_since: reachable_callbacks_metadata[sub.callback]
}
Pleroma.Web.Federator.enqueue(:publish_single_websub, data)
@@ -121,6 +131,12 @@ defmodule Pleroma.Web.Websub do
end
end
+ def incoming_subscription_request(user, params) do
+ Logger.info("Unhandled WebSub request for #{user.nickname}: #{inspect(params)}")
+
+ {:error, "Invalid WebSub request"}
+ end
+
defp get_subscription(topic, callback) do
Repo.get_by(WebsubServerSubscription, topic: topic, callback: callback) ||
%WebsubServerSubscription{}
@@ -257,11 +273,11 @@ defmodule Pleroma.Web.Websub do
end)
end
- def publish_one(%{xml: xml, topic: topic, callback: callback, secret: secret}) do
+ def publish_one(%{xml: xml, topic: topic, callback: callback, secret: secret} = params) do
signature = sign(secret || "", xml)
Logger.info(fn -> "Pushing #{topic} to #{callback}" end)
- with {:ok, %{status: code}} <-
+ with {:ok, %{status: code}} when code in 200..299 <-
@httpoison.post(
callback,
xml,
@@ -270,12 +286,16 @@ defmodule Pleroma.Web.Websub do
{"X-Hub-Signature", "sha1=#{signature}"}
]
) do
+ if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
+ do: Instances.set_reachable(callback)
+
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}
+ {_post_result, response} ->
+ unless params[:unreachable_since], do: Instances.set_reachable(callback)
+ Logger.debug(fn -> "Couldn't push to #{callback}, #{inspect(response)}" end)
+ {:error, response}
end
end
end
diff --git a/lib/pleroma/web/websub/websub_client_subscription.ex b/lib/pleroma/web/websub/websub_client_subscription.ex
index 105b0069f..969ee0684 100644
--- a/lib/pleroma/web/websub/websub_client_subscription.ex
+++ b/lib/pleroma/web/websub/websub_client_subscription.ex
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.Websub.WebsubClientSubscription do
field(:state, :string)
field(:subscribers, {:array, :string}, default: [])
field(:hub, :string)
- belongs_to(:user, User)
+ belongs_to(:user, User, type: Pleroma.FlakeId)
timestamps()
end
diff --git a/lib/pleroma/web/websub/websub_controller.ex b/lib/pleroma/web/websub/websub_controller.ex
index 27304d988..1ad18a8a4 100644
--- a/lib/pleroma/web/websub/websub_controller.ex
+++ b/lib/pleroma/web/websub/websub_controller.ex
@@ -4,9 +4,13 @@
defmodule Pleroma.Web.Websub.WebsubController do
use Pleroma.Web, :controller
- alias Pleroma.{Repo, User}
- alias Pleroma.Web.{Websub, Federator}
+
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Web.Websub
+ alias Pleroma.Web.Federator
alias Pleroma.Web.Websub.WebsubClientSubscription
+
require Logger
plug(
@@ -67,6 +71,13 @@ defmodule Pleroma.Web.Websub.WebsubController do
end
end
+ def websub_subscription_confirmation(conn, params) do
+ Logger.info("Invalid WebSub confirmation request: #{inspect(params)}")
+
+ conn
+ |> send_resp(500, "Invalid parameters")
+ end
+
def websub_incoming(conn, %{"id" => id}) do
with "sha1=" <> signature <- hd(get_req_header(conn, "x-hub-signature")),
signature <- String.downcase(signature),