Merge remote-tracking branch 'origin/develop' into translate-posts

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-05-18 11:15:03 +02:00
commit d667049ca9
64 changed files with 1889 additions and 845 deletions

View file

@ -0,0 +1 @@
Mastodon API: Remove deprecated GET /api/v1/statuses/:id/card endpoint https://github.com/mastodon/mastodon/pull/11213

View file

@ -0,0 +1 @@
Add instance rules

View file

@ -0,0 +1 @@
Add new parameters to /api/v2/instance: configuration[accounts][max_pinned_statuses] and configuration[statuses][characters_reserved_per_url]

View file

@ -0,0 +1 @@
Startup detection for configured MRF modules that are missing or incorrectly defined

View file

@ -0,0 +1 @@
Refactored Rich Media to cache the content in the database. Fetching operations that could block status rendering have been eliminated.

View file

@ -0,0 +1 @@
Web Push notifications are no longer generated for muted/blocked threads and users.

View file

@ -428,7 +428,11 @@ config :pleroma, :rich_media,
Pleroma.Web.RichMedia.Parsers.OEmbed Pleroma.Web.RichMedia.Parsers.OEmbed
], ],
failure_backoff: 60_000, failure_backoff: 60_000,
ttl_setters: [Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl] ttl_setters: [
Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl,
Pleroma.Web.RichMedia.Parser.TTL.Opengraph
],
max_body: 5_000_000
config :pleroma, :media_proxy, config :pleroma, :media_proxy,
enabled: false, enabled: false,
@ -575,7 +579,8 @@ config :pleroma, Oban,
attachments_cleanup: 1, attachments_cleanup: 1,
new_users_digest: 1, new_users_digest: 1,
mute_expire: 5, mute_expire: 5,
search_indexing: 10 search_indexing: 10,
rich_media_expiration: 2
], ],
plugins: [Oban.Plugins.Pruner], plugins: [Oban.Plugins.Pruner],
crontab: [ crontab: [

View file

@ -3522,7 +3522,7 @@ config :pleroma, :config_description, [
}, },
%{ %{
key: :initial_indexing_chunk_size, key: :initial_indexing_chunk_size,
type: :int, type: :integer,
description: description:
"Amount of posts in a batch when running the initial indexing operation. Should probably not be more than 100000" <> "Amount of posts in a batch when running the initial indexing operation. Should probably not be more than 100000" <>
" since there's a limit on maximum insert size", " since there's a limit on maximum insert size",

View file

@ -61,7 +61,8 @@ config :tesla, adapter: Tesla.Mock
config :pleroma, :rich_media, config :pleroma, :rich_media,
enabled: false, enabled: false,
ignore_hosts: [], ignore_hosts: [],
ignore_tld: ["local", "localdomain", "lan"] ignore_tld: ["local", "localdomain", "lan"],
max_body: 2_000_000
config :pleroma, :instance, config :pleroma, :instance,
multi_factor_authentication: [ multi_factor_authentication: [
@ -174,6 +175,8 @@ config :pleroma, Pleroma.Uploaders.Uploader, timeout: 1_000
config :pleroma, Pleroma.Emoji.Loader, test_emoji: true config :pleroma, Pleroma.Emoji.Loader, test_emoji: true
config :pleroma, Pleroma.Web.RichMedia.Backfill, provider: Pleroma.Web.RichMedia.Backfill
if File.exists?("./config/test.secret.exs") do if File.exists?("./config/test.secret.exs") do
import_config "test.secret.exs" import_config "test.secret.exs"
else else

View file

@ -1751,3 +1751,53 @@ Note that this differs from the Mastodon API variant: Mastodon API only returns
```json ```json
{} {}
``` ```
## `GET /api/v1/pleroma/admin/rules`
### List rules
- Response: JSON, list of rules
```json
[
{
"id": "1",
"priority": 1,
"text": "There are no rules",
"hint": null
}
]
```
## `POST /api/v1/pleroma/admin/rules`
### Create a rule
- Params:
- `text`: string, required, rule content
- `hint`: string, optional, rule description
- `priority`: integer, optional, rule ordering priority
- Response: JSON, a single rule
## `PATCH /api/v1/pleroma/admin/rules/:id`
### Update a rule
- Params:
- `text`: string, optional, rule content
- `hint`: string, optional, rule description
- `priority`: integer, optional, rule ordering priority
- Response: JSON, a single rule
## `DELETE /api/v1/pleroma/admin/rules/:id`
### Delete a rule
- Response: JSON, empty object
```json
{}
```

View file

@ -28,6 +28,7 @@ defmodule Pleroma.ApplicationRequirements do
|> check_welcome_message_config!() |> check_welcome_message_config!()
|> check_rum!() |> check_rum!()
|> check_repo_pool_size!() |> check_repo_pool_size!()
|> check_mrfs()
|> handle_result() |> handle_result()
end end
@ -234,4 +235,25 @@ defmodule Pleroma.ApplicationRequirements do
true true
end end
end end
defp check_mrfs(:ok) do
mrfs = Config.get!([:mrf, :policies])
missing_mrfs =
Enum.reduce(mrfs, [], fn x, acc ->
if Code.ensure_compiled(x) do
acc
else
acc ++ [x]
end
end)
if Enum.empty?(missing_mrfs) do
:ok
else
{:error, "The following MRF modules are configured but missing: #{inspect(missing_mrfs)}"}
end
end
defp check_mrfs(result), do: result
end end

View file

@ -20,6 +20,7 @@ defmodule Pleroma.Constants do
"deleted_activity_id", "deleted_activity_id",
"pleroma_internal", "pleroma_internal",
"generator", "generator",
"rules",
"language" "language"
] ]
) )

View file

@ -65,20 +65,16 @@ defmodule Pleroma.HTML do
end end
end end
@spec extract_first_external_url_from_object(Pleroma.Object.t()) :: @spec extract_first_external_url_from_object(Pleroma.Object.t()) :: String.t() | nil
{:ok, String.t()} | {:error, :no_content}
def extract_first_external_url_from_object(%{data: %{"content" => content}}) def extract_first_external_url_from_object(%{data: %{"content" => content}})
when is_binary(content) do when is_binary(content) do
url = content
content |> Floki.parse_fragment!()
|> Floki.parse_fragment!() |> Floki.find("a:not(.mention,.hashtag,.attachment,[rel~=\"tag\"])")
|> Floki.find("a:not(.mention,.hashtag,.attachment,[rel~=\"tag\"])") |> Enum.take(1)
|> Enum.take(1) |> Floki.attribute("href")
|> Floki.attribute("href") |> Enum.at(0)
|> Enum.at(0)
{:ok, url}
end end
def extract_first_external_url_from_object(_), do: {:error, :no_content} def extract_first_external_url_from_object(_), do: nil
end end

View file

@ -361,36 +361,32 @@ defmodule Pleroma.Notification do
end end
end end
@spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []} @spec create_notifications(Activity.t()) :: {:ok, [Notification.t()] | []}
def create_notifications(activity, options \\ []) def create_notifications(activity)
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
object = Object.normalize(activity, fetch: false) object = Object.normalize(activity, fetch: false)
if object && object.data["type"] == "Answer" do if object && object.data["type"] == "Answer" do
{:ok, []} {:ok, []}
else else
do_create_notifications(activity, options) do_create_notifications(activity)
end end
end end
def create_notifications(%Activity{data: %{"type" => type}} = activity, options) def create_notifications(%Activity{data: %{"type" => type}} = activity)
when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag", "Update"] do when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag", "Update"] do
do_create_notifications(activity, options) do_create_notifications(activity)
end end
def create_notifications(_, _), do: {:ok, []} def create_notifications(_), do: {:ok, []}
defp do_create_notifications(%Activity{} = activity, options) do defp do_create_notifications(%Activity{} = activity) do
do_send = Keyword.get(options, :do_send, true) enabled_receivers = get_notified_from_activity(activity)
{enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
potential_receivers = enabled_receivers ++ disabled_receivers
notifications = notifications =
Enum.map(potential_receivers, fn user -> Enum.map(enabled_receivers, fn user ->
do_send = do_send && user in enabled_receivers create_notification(activity, user)
create_notification(activity, user, do_send: do_send)
end) end)
|> Enum.reject(&is_nil/1) |> Enum.reject(&is_nil/1)
@ -450,7 +446,6 @@ defmodule Pleroma.Notification do
# TODO move to sql, too. # TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user, opts \\ []) do def create_notification(%Activity{} = activity, %User{} = user, opts \\ []) do
do_send = Keyword.get(opts, :do_send, true)
type = Keyword.get(opts, :type, type_from_activity(activity)) type = Keyword.get(opts, :type, type_from_activity(activity))
unless skip?(activity, user, opts) do unless skip?(activity, user, opts) do
@ -465,11 +460,6 @@ defmodule Pleroma.Notification do
|> Marker.multi_set_last_read_id(user, "notifications") |> Marker.multi_set_last_read_id(user, "notifications")
|> Repo.transaction() |> Repo.transaction()
if do_send do
Streamer.stream(["user", "user:notification"], notification)
Push.send(notification)
end
notification notification
end end
end end
@ -527,10 +517,7 @@ defmodule Pleroma.Notification do
|> exclude_relationship_restricted_ap_ids(activity) |> exclude_relationship_restricted_ap_ids(activity)
|> exclude_thread_muter_ap_ids(activity) |> exclude_thread_muter_ap_ids(activity)
notification_enabled_users = Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
{notification_enabled_users, potential_receivers -- notification_enabled_users}
end end
def get_notified_from_activity(_, _local_only), do: {[], []} def get_notified_from_activity(_, _local_only), do: {[], []}
@ -643,6 +630,7 @@ defmodule Pleroma.Notification do
def skip?(%Activity{} = activity, %User{} = user, opts) do def skip?(%Activity{} = activity, %User{} = user, opts) do
[ [
:self, :self,
:internal,
:invisible, :invisible,
:block_from_strangers, :block_from_strangers,
:recently_followed, :recently_followed,
@ -662,6 +650,12 @@ defmodule Pleroma.Notification do
end end
end end
def skip?(:internal, %Activity{} = activity, _user, _opts) do
actor = activity.data["actor"]
user = User.get_cached_by_ap_id(actor)
User.internal?(user)
end
def skip?(:invisible, %Activity{} = activity, _user, _opts) do def skip?(:invisible, %Activity{} = activity, _user, _opts) do
actor = activity.data["actor"] actor = activity.data["actor"]
user = User.get_cached_by_ap_id(actor) user = User.get_cached_by_ap_id(actor)
@ -748,4 +742,12 @@ defmodule Pleroma.Notification do
) )
|> Repo.update_all(set: [seen: true]) |> Repo.update_all(set: [seen: true])
end end
@spec send(list(Notification.t())) :: :ok
def send(notifications) do
Enum.each(notifications, fn notification ->
Streamer.stream(["user", "user:notification"], notification)
Push.send(notification)
end)
end
end end

68
lib/pleroma/rule.ex Normal file
View file

@ -0,0 +1,68 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Rule do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Pleroma.Repo
alias Pleroma.Rule
schema "rules" do
field(:priority, :integer, default: 0)
field(:text, :string)
field(:hint, :string)
timestamps()
end
def changeset(%Rule{} = rule, params \\ %{}) do
rule
|> cast(params, [:priority, :text, :hint])
|> validate_required([:text])
end
def query do
Rule
|> order_by(asc: :priority)
|> order_by(asc: :id)
end
def get(ids) when is_list(ids) do
from(r in __MODULE__, where: r.id in ^ids)
|> Repo.all()
end
def get(id), do: Repo.get(__MODULE__, id)
def exists?(id) do
from(r in __MODULE__, where: r.id == ^id)
|> Repo.exists?()
end
def create(params) do
{:ok, rule} =
%Rule{}
|> changeset(params)
|> Repo.insert()
rule
end
def update(params, id) do
{:ok, rule} =
get(id)
|> changeset(params)
|> Repo.update()
rule
end
def delete(id) do
get(id)
|> Repo.delete()
end
end

View file

@ -147,9 +147,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
# Splice in the child object if we have one. # Splice in the child object if we have one.
activity = Maps.put_if_present(activity, :object, object) activity = Maps.put_if_present(activity, :object, object)
ConcurrentLimiter.limit(Pleroma.Web.RichMedia.Helpers, fn -> Pleroma.Web.RichMedia.Card.get_by_activity(activity)
Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)
end)
# Add local posts to search index # Add local posts to search index
if local, do: Pleroma.Search.add_to_index(activity) if local, do: Pleroma.Search.add_to_index(activity)
@ -177,7 +175,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
id: "pleroma:fakeid" id: "pleroma:fakeid"
} }
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) Pleroma.Web.RichMedia.Card.get_by_activity(activity)
{:ok, activity} {:ok, activity}
{:remote_limit_pass, _} -> {:remote_limit_pass, _} ->
@ -202,7 +200,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end end
def notify_and_stream(activity) do def notify_and_stream(activity) do
Notification.create_notifications(activity) {:ok, notifications} = Notification.create_notifications(activity)
Notification.send(notifications)
original_activity = original_activity =
case activity do case activity do
@ -1261,6 +1260,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_quote_url(query, _), do: query defp restrict_quote_url(query, _), do: query
defp restrict_rule(query, %{rule_id: rule_id}) do
from(
activity in query,
where: fragment("(?)->'rules' \\? (?)", activity.data, ^rule_id)
)
end
defp restrict_rule(query, _), do: query
defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query
defp exclude_poll_votes(query, _) do defp exclude_poll_votes(query, _) do
@ -1423,6 +1431,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_instance(opts) |> restrict_instance(opts)
|> restrict_announce_object_actor(opts) |> restrict_announce_object_actor(opts)
|> restrict_filtered(opts) |> restrict_filtered(opts)
|> restrict_rule(opts)
|> restrict_quote_url(opts) |> restrict_quote_url(opts)
|> maybe_restrict_deactivated_users(opts) |> maybe_restrict_deactivated_users(opts)
|> exclude_poll_votes(opts) |> exclude_poll_votes(opts)

View file

@ -21,7 +21,6 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Push
alias Pleroma.Web.Streamer alias Pleroma.Web.Streamer
alias Pleroma.Workers.PollWorker alias Pleroma.Workers.PollWorker
@ -125,7 +124,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
nil nil
end end
{:ok, notifications} = Notification.create_notifications(object, do_send: false) {:ok, notifications} = Notification.create_notifications(object)
meta = meta =
meta meta
@ -184,7 +183,11 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
liked_object = Object.get_by_ap_id(object.data["object"]) liked_object = Object.get_by_ap_id(object.data["object"])
Utils.add_like_to_object(object, liked_object) Utils.add_like_to_object(object, liked_object)
Notification.create_notifications(object) {:ok, notifications} = Notification.create_notifications(object)
meta =
meta
|> add_notifications(notifications)
{:ok, object, meta} {:ok, object, meta}
end end
@ -202,7 +205,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
def handle(%{data: %{"type" => "Create"}} = activity, meta) do def handle(%{data: %{"type" => "Create"}} = activity, meta) do
with {:ok, object, meta} <- handle_object_creation(meta[:object_data], activity, meta), with {:ok, object, meta} <- handle_object_creation(meta[:object_data], activity, meta),
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
{:ok, notifications} = Notification.create_notifications(activity, do_send: false) {:ok, notifications} = Notification.create_notifications(activity)
{:ok, _user} = ActivityPub.increase_note_count_if_public(user, object) {:ok, _user} = ActivityPub.increase_note_count_if_public(user, object)
{:ok, _user} = ActivityPub.update_last_status_at_if_public(user, object) {:ok, _user} = ActivityPub.update_last_status_at_if_public(user, object)
@ -227,9 +230,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
end end
end end
ConcurrentLimiter.limit(Pleroma.Web.RichMedia.Helpers, fn -> Pleroma.Web.RichMedia.Card.get_by_activity(activity)
Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)
end)
Pleroma.Search.add_to_index(Map.put(activity, :object, object)) Pleroma.Search.add_to_index(Map.put(activity, :object, object))
@ -258,11 +259,13 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
Utils.add_announce_to_object(object, announced_object) Utils.add_announce_to_object(object, announced_object)
if !User.internal?(user) do {:ok, notifications} = Notification.create_notifications(object)
Notification.create_notifications(object)
ap_streamer().stream_out(object) if !User.internal?(user), do: ap_streamer().stream_out(object)
end
meta =
meta
|> add_notifications(notifications)
{:ok, object, meta} {:ok, object, meta}
end end
@ -283,7 +286,11 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
reacted_object = Object.get_by_ap_id(object.data["object"]) reacted_object = Object.get_by_ap_id(object.data["object"])
Utils.add_emoji_reaction_to_object(object, reacted_object) Utils.add_emoji_reaction_to_object(object, reacted_object)
Notification.create_notifications(object) {:ok, notifications} = Notification.create_notifications(object)
meta =
meta
|> add_notifications(notifications)
{:ok, object, meta} {:ok, object, meta}
end end
@ -587,10 +594,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
defp send_notifications(meta) do defp send_notifications(meta) do
Keyword.get(meta, :notifications, []) Keyword.get(meta, :notifications, [])
|> Enum.each(fn notification -> |> Notification.send()
Streamer.stream(["user", "user:notification"], notification)
Push.send(notification)
end)
meta meta
end end

View file

@ -728,14 +728,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do
#### Flag-related helpers #### Flag-related helpers
@spec make_flag_data(map(), map()) :: map() @spec make_flag_data(map(), map()) :: map()
def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do def make_flag_data(
%{actor: actor, context: context, content: content} = params,
additional
) do
%{ %{
"type" => "Flag", "type" => "Flag",
"actor" => actor.ap_id, "actor" => actor.ap_id,
"content" => content, "content" => content,
"object" => build_flag_object(params), "object" => build_flag_object(params),
"context" => context, "context" => context,
"state" => "open" "state" => "open",
"rules" => Map.get(params, :rules, nil)
} }
|> Map.merge(additional) |> Map.merge(additional)
end end

View file

@ -0,0 +1,62 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.RuleController do
use Pleroma.Web, :controller
alias Pleroma.Repo
alias Pleroma.Rule
alias Pleroma.Web.Plugs.OAuthScopesPlug
import Pleroma.Web.ControllerHelper,
only: [
json_response: 3
]
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(
OAuthScopesPlug,
%{scopes: ["admin:write"]}
when action in [:create, :update, :delete]
)
plug(OAuthScopesPlug, %{scopes: ["admin:read"]} when action == :index)
action_fallback(AdminAPI.FallbackController)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.RuleOperation
def index(conn, _) do
rules =
Rule.query()
|> Repo.all()
render(conn, "index.json", rules: rules)
end
def create(%{body_params: params} = conn, _) do
rule =
params
|> Rule.create()
render(conn, "show.json", rule: rule)
end
def update(%{body_params: params} = conn, %{id: id}) do
rule =
params
|> Rule.update(id)
render(conn, "show.json", rule: rule)
end
def delete(conn, %{id: id}) do
with {:ok, _} <- Rule.delete(id) do
json(conn, %{})
else
_ -> json_response(conn, :bad_request, "")
end
end
end

View file

@ -6,9 +6,11 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.HTML alias Pleroma.HTML
alias Pleroma.Rule
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.Report alias Pleroma.Web.AdminAPI.Report
alias Pleroma.Web.AdminAPI.RuleView
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
@ -46,7 +48,8 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
as: :activity as: :activity
}), }),
state: report.data["state"], state: report.data["state"],
notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes}) notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes}),
rules: rules(Map.get(report.data, "rules", nil))
} }
end end
@ -71,4 +74,16 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
created_at: Utils.to_masto_date(inserted_at) created_at: Utils.to_masto_date(inserted_at)
} }
end end
defp rules(nil) do
[]
end
defp rules(rule_ids) do
rules =
rule_ids
|> Rule.get()
render(RuleView, "index.json", rules: rules)
end
end end

View file

@ -0,0 +1,22 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.RuleView do
use Pleroma.Web, :view
require Pleroma.Constants
def render("index.json", %{rules: rules} = _opts) do
render_many(rules, __MODULE__, "show.json")
end
def render("show.json", %{rule: rule} = _opts) do
%{
id: to_string(rule.id),
priority: rule.priority,
text: rule.text,
hint: rule.hint
}
end
end

View file

@ -97,6 +97,7 @@ defmodule Pleroma.Web.ApiSpec do
"Frontend management", "Frontend management",
"Instance configuration", "Instance configuration",
"Instance documents", "Instance documents",
"Instance rule managment",
"Invites", "Invites",
"MediaProxy cache", "MediaProxy cache",
"OAuth application management", "OAuth application management",

View file

@ -30,6 +30,12 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
report_state(), report_state(),
"Filter by report state" "Filter by report state"
), ),
Operation.parameter(
:rule_id,
:query,
%Schema{type: :string},
"Filter by selected rule id"
),
Operation.parameter( Operation.parameter(
:limit, :limit,
:query, :query,
@ -169,6 +175,17 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
inserted_at: %Schema{type: :string, format: :"date-time"} inserted_at: %Schema{type: :string, format: :"date-time"}
} }
} }
},
rules: %Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
id: %Schema{type: :string},
text: %Schema{type: :string},
hint: %Schema{type: :string, nullable: true}
}
}
} }
} }
} }

View file

@ -0,0 +1,115 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Admin.RuleOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.ApiError
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def index_operation do
%Operation{
tags: ["Instance rule managment"],
summary: "Retrieve list of instance rules",
operationId: "AdminAPI.RuleController.index",
security: [%{"oAuth" => ["admin:read"]}],
responses: %{
200 =>
Operation.response("Response", "application/json", %Schema{
type: :array,
items: rule()
}),
403 => Operation.response("Forbidden", "application/json", ApiError)
}
}
end
def create_operation do
%Operation{
tags: ["Instance rule managment"],
summary: "Create new rule",
operationId: "AdminAPI.RuleController.create",
security: [%{"oAuth" => ["admin:write"]}],
parameters: admin_api_params(),
requestBody: request_body("Parameters", create_request(), required: true),
responses: %{
200 => Operation.response("Response", "application/json", rule()),
400 => Operation.response("Bad Request", "application/json", ApiError),
403 => Operation.response("Forbidden", "application/json", ApiError)
}
}
end
def update_operation do
%Operation{
tags: ["Instance rule managment"],
summary: "Modify existing rule",
operationId: "AdminAPI.RuleController.update",
security: [%{"oAuth" => ["admin:write"]}],
parameters: [Operation.parameter(:id, :path, :string, "Rule ID")],
requestBody: request_body("Parameters", update_request(), required: true),
responses: %{
200 => Operation.response("Response", "application/json", rule()),
400 => Operation.response("Bad Request", "application/json", ApiError),
403 => Operation.response("Forbidden", "application/json", ApiError)
}
}
end
def delete_operation do
%Operation{
tags: ["Instance rule managment"],
summary: "Delete rule",
operationId: "AdminAPI.RuleController.delete",
parameters: [Operation.parameter(:id, :path, :string, "Rule ID")],
security: [%{"oAuth" => ["admin:write"]}],
responses: %{
200 => empty_object_response(),
404 => Operation.response("Not Found", "application/json", ApiError),
403 => Operation.response("Forbidden", "application/json", ApiError)
}
}
end
defp create_request do
%Schema{
type: :object,
required: [:text],
properties: %{
priority: %Schema{type: :integer},
text: %Schema{type: :string},
hint: %Schema{type: :string}
}
}
end
defp update_request do
%Schema{
type: :object,
properties: %{
priority: %Schema{type: :integer},
text: %Schema{type: :string},
hint: %Schema{type: :string}
}
}
end
defp rule do
%Schema{
type: :object,
properties: %{
id: %Schema{type: :string},
priority: %Schema{type: :integer},
text: %Schema{type: :string},
hint: %Schema{type: :string, nullable: true}
}
}
end
end

View file

@ -46,6 +46,17 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
} }
end end
def rules_operation do
%Operation{
tags: ["Instance misc"],
summary: "Retrieve list of instance rules",
operationId: "InstanceController.rules",
responses: %{
200 => Operation.response("Array of domains", "application/json", array_of_rules())
}
}
end
def translation_languages_operation do def translation_languages_operation do
%Operation{ %Operation{
tags: ["Instance misc"], tags: ["Instance misc"],
@ -73,6 +84,15 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
%Schema{ %Schema{
type: :object, type: :object,
properties: %{ properties: %{
accounts: %Schema{
type: :object,
properties: %{
max_featured_tags: %Schema{
type: :integer,
description: "The maximum number of featured tags allowed for each account."
}
}
},
uri: %Schema{type: :string, description: "The domain name of the instance"}, uri: %Schema{type: :string, description: "The domain name of the instance"},
title: %Schema{type: :string, description: "The title of the website"}, title: %Schema{type: :string, description: "The title of the website"},
description: %Schema{ description: %Schema{
@ -195,7 +215,8 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
"urls" => %{ "urls" => %{
"streaming_api" => "wss://lain.com" "streaming_api" => "wss://lain.com"
}, },
"version" => "2.7.2 (compatible; Pleroma 2.0.50-536-g25eec6d7-develop)" "version" => "2.7.2 (compatible; Pleroma 2.0.50-536-g25eec6d7-develop)",
"rules" => array_of_rules()
} }
} }
end end
@ -295,6 +316,19 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
type: :object, type: :object,
description: "Instance configuration", description: "Instance configuration",
properties: %{ properties: %{
accounts: %Schema{
type: :object,
properties: %{
max_featured_tags: %Schema{
type: :integer,
description: "The maximum number of featured tags allowed for each account."
},
max_pinned_statuses: %Schema{
type: :integer,
description: "The maximum number of pinned statuses for each account."
}
}
},
urls: %Schema{ urls: %Schema{
type: :object, type: :object,
properties: %{ properties: %{
@ -308,6 +342,11 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
type: :object, type: :object,
description: "A map with poll limits for local statuses", description: "A map with poll limits for local statuses",
properties: %{ properties: %{
characters_reserved_per_url: %Schema{
type: :integer,
description:
"Each URL in a status will be assumed to be exactly this many characters."
},
max_characters: %Schema{ max_characters: %Schema{
type: :integer, type: :integer,
description: "Posts character limit (CW/Subject included in the counter)" description: "Posts character limit (CW/Subject included in the counter)"
@ -367,4 +406,18 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
example: ["pleroma.site", "lain.com", "bikeshed.party"] example: ["pleroma.site", "lain.com", "bikeshed.party"]
} }
end end
defp array_of_rules do
%Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
id: %Schema{type: :string},
text: %Schema{type: :string},
hint: %Schema{type: :string}
}
}
}
end
end end

View file

@ -53,6 +53,12 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do
default: false, default: false,
description: description:
"If the account is remote, should the report be forwarded to the remote admin?" "If the account is remote, should the report be forwarded to the remote admin?"
},
rule_ids: %Schema{
type: :array,
nullable: true,
items: %Schema{type: :string},
description: "Array of rules"
} }
}, },
required: [:account_id], required: [:account_id],
@ -60,7 +66,8 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do
"account_id" => "123", "account_id" => "123",
"status_ids" => ["1337"], "status_ids" => ["1337"],
"comment" => "bad status!", "comment" => "bad status!",
"forward" => "false" "forward" => "false",
"rule_ids" => ["3"]
} }
} }
end end

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Formatter alias Pleroma.Formatter
alias Pleroma.ModerationLog alias Pleroma.ModerationLog
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Rule
alias Pleroma.ThreadMute alias Pleroma.ThreadMute
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserRelationship alias Pleroma.UserRelationship
@ -568,14 +569,16 @@ defmodule Pleroma.Web.CommonAPI do
def report(user, data) do def report(user, data) do
with {:ok, account} <- get_reported_account(data.account_id), with {:ok, account} <- get_reported_account(data.account_id),
{:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]), {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
{:ok, statuses} <- get_report_statuses(account, data) do {:ok, statuses} <- get_report_statuses(account, data),
rules <- get_report_rules(Map.get(data, :rule_ids, nil)) do
ActivityPub.flag(%{ ActivityPub.flag(%{
context: Utils.generate_context_id(), context: Utils.generate_context_id(),
actor: user, actor: user,
account: account, account: account,
statuses: statuses, statuses: statuses,
content: content_html, content: content_html,
forward: Map.get(data, :forward, false) forward: Map.get(data, :forward, false),
rules: rules
}) })
end end
end end
@ -587,6 +590,15 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
defp get_report_rules(nil) do
nil
end
defp get_report_rules(rule_ids) do
rule_ids
|> Enum.filter(&Rule.exists?/1)
end
def update_report_state(activity_ids, state) when is_list(activity_ids) do def update_report_state(activity_ids, state) when is_list(activity_ids) do
case Utils.update_report_state(activity_ids, state) do case Utils.update_report_state(activity_ids, state) do
:ok -> {:ok, activity_ids} :ok -> {:ok, activity_ids}

View file

@ -26,6 +26,11 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do
json(conn, Pleroma.Stats.get_peers()) json(conn, Pleroma.Stats.get_peers())
end end
@doc "GET /api/v1/instance/rules"
def rules(conn, _params) do
render(conn, "rules.json")
end
@doc "GET /api/v1/instance/translation_languages" @doc "GET /api/v1/instance/translation_languages"
def translation_languages(conn, _params) do def translation_languages(conn, _params) do
render(conn, "translation_languages.json") render(conn, "translation_languages.json")

View file

@ -39,7 +39,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
when action in [ when action in [
:index, :index,
:show, :show,
:card,
:context, :context,
:show_history, :show_history,
:show_source :show_source
@ -476,21 +475,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end end
end end
@doc "GET /api/v1/statuses/:id/card"
@deprecated "https://github.com/tootsuite/mastodon/pull/11213"
def card(
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: status_id}}}} = conn,
_
) do
with %Activity{} = activity <- Activity.get_by_id(status_id),
true <- Visibility.visible_for_user?(activity, user) do
data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
render(conn, "card.json", data)
else
_ -> render_error(conn, :not_found, "Record not found")
end
end
@doc "GET /api/v1/statuses/:id/favourited_by" @doc "GET /api/v1/statuses/:id/favourited_by"
def favourited_by( def favourited_by(
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,

View file

@ -76,6 +76,20 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
}) })
end end
def render("rules.json", _) do
Pleroma.Rule.query()
|> Pleroma.Repo.all()
|> render_many(__MODULE__, "rule.json", as: :rule)
end
def render("rule.json", %{rule: rule}) do
%{
id: to_string(rule.id),
text: rule.text,
hint: rule.hint || ""
}
end
def render("translation_languages.json", _) do def render("translation_languages.json", _) do
with true <- Pleroma.Language.Translation.configured?(), with true <- Pleroma.Language.Translation.configured?(),
{:ok, languages} <- Pleroma.Language.Translation.languages_matrix() do {:ok, languages} <- Pleroma.Language.Translation.languages_matrix() do
@ -87,10 +101,10 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
defp common_information(instance) do defp common_information(instance) do
%{ %{
title: Keyword.get(instance, :name),
version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
languages: Keyword.get(instance, :languages, ["en"]), languages: Keyword.get(instance, :languages, ["en"]),
rules: [] rules: render(__MODULE__, "rules.json"),
title: Keyword.get(instance, :name),
version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})"
} }
end end
@ -225,6 +239,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
defp configuration2 do defp configuration2 do
configuration() configuration()
|> put_in([:accounts, :max_pinned_statuses], Config.get([:instance, :max_pinned_statuses], 0))
|> put_in([:statuses, :characters_reserved_per_url], 0)
|> Map.merge(%{ |> Map.merge(%{
urls: %{ urls: %{
streaming: Pleroma.Web.Endpoint.websocket_url(), streaming: Pleroma.Web.Endpoint.websocket_url(),

View file

@ -21,6 +21,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
alias Pleroma.Web.PleromaAPI.EmojiReactionController alias Pleroma.Web.PleromaAPI.EmojiReactionController
alias Pleroma.Web.RichMedia.Card
import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2] import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]
@ -29,9 +30,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
# pagination is restricted to 40 activities at a time # pagination is restricted to 40 activities at a time
defp fetch_rich_media_for_activities(activities) do defp fetch_rich_media_for_activities(activities) do
Enum.each(activities, fn activity -> Enum.each(activities, fn activity ->
spawn(fn -> spawn(fn -> Card.get_by_activity(activity) end)
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
end)
end) end)
end end
@ -113,9 +112,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
# To do: check AdminAPIControllerTest on the reasons behind nil activities in the list # To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
activities = Enum.filter(opts.activities, & &1) activities = Enum.filter(opts.activities, & &1)
# Start fetching rich media before doing anything else, so that later calls to get the cards # Start prefetching rich media before doing anything else
# only block for timeout in the worst case, as opposed to
# length(activities_with_links) * timeout
fetch_rich_media_for_activities(activities) fetch_rich_media_for_activities(activities)
replied_to_activities = get_replied_to_activities(activities) replied_to_activities = get_replied_to_activities(activities)
quoted_activities = get_quoted_activities(activities) quoted_activities = get_quoted_activities(activities)
@ -364,7 +361,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
summary = object.data["summary"] || "" summary = object.data["summary"] || ""
card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)) card =
case Card.get_by_activity(activity) do
%Card{} = result -> render("card.json", result)
_ -> nil
end
url = url =
if user.local do if user.local do
@ -567,15 +568,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
} }
end end
def render("card.json", %{rich_media: rich_media, page_url: page_url}) do def render("card.json", %Card{fields: rich_media}) do
page_url_data = URI.parse(page_url) page_url_data = URI.parse(rich_media["url"])
page_url_data =
if is_binary(rich_media["url"]) do
URI.merge(page_url_data, URI.parse(rich_media["url"]))
else
page_url_data
end
page_url = page_url_data |> to_string page_url = page_url_data |> to_string

View file

@ -9,6 +9,7 @@ defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.RichMedia.Card
@cachex Pleroma.Config.get([:cachex, :provider], Cachex) @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@ -23,6 +24,12 @@ defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
} }
} }
) do ) do
card =
case Card.get_by_object(object) do
%Card{} = card_data -> StatusView.render("card.json", card_data)
_ -> nil
end
%{ %{
id: id |> to_string(), id: id |> to_string(),
content: chat_message["content"], content: chat_message["content"],
@ -34,11 +41,7 @@ defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
chat_message["attachment"] && chat_message["attachment"] &&
StatusView.render("attachment.json", attachment: chat_message["attachment"]), StatusView.render("attachment.json", attachment: chat_message["attachment"]),
unread: unread, unread: unread,
card: card: card
StatusView.render(
"card.json",
Pleroma.Web.RichMedia.Helpers.fetch_data_for_object(object)
)
} }
|> put_idempotency_key() |> put_idempotency_key()
end end

View file

@ -0,0 +1,101 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Backfill do
alias Pleroma.Web.RichMedia.Card
alias Pleroma.Web.RichMedia.Parser
alias Pleroma.Web.RichMedia.Parser.TTL
alias Pleroma.Workers.RichMediaExpirationWorker
require Logger
@backfiller Pleroma.Config.get([__MODULE__, :provider], Pleroma.Web.RichMedia.Backfill.Task)
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@max_attempts 3
@retry 5_000
def start(%{url: url} = args) when is_binary(url) do
url_hash = Card.url_to_hash(url)
args =
args
|> Map.put(:attempt, 1)
|> Map.put(:url_hash, url_hash)
@backfiller.run(args)
end
def run(%{url: url, url_hash: url_hash, attempt: attempt} = args)
when attempt <= @max_attempts do
case Parser.parse(url) do
{:ok, fields} ->
{:ok, card} = Card.create(url, fields)
maybe_schedule_expiration(url, fields)
if Map.has_key?(args, :activity_id) do
stream_update(args)
end
warm_cache(url_hash, card)
{:error, {:invalid_metadata, fields}} ->
Logger.debug("Rich media incomplete or invalid metadata for #{url}: #{inspect(fields)}")
negative_cache(url_hash)
{:error, :body_too_large} ->
Logger.error("Rich media error for #{url}: :body_too_large")
negative_cache(url_hash)
{:error, {:content_type, type}} ->
Logger.debug("Rich media error for #{url}: :content_type is #{type}")
negative_cache(url_hash)
e ->
Logger.debug("Rich media error for #{url}: #{inspect(e)}")
:timer.sleep(@retry * attempt)
run(%{args | attempt: attempt + 1})
end
end
def run(%{url: url, url_hash: url_hash}) do
Logger.debug("Rich media failure for #{url}")
negative_cache(url_hash, :timer.minutes(15))
end
defp maybe_schedule_expiration(url, fields) do
case TTL.process(fields, url) do
{:ok, ttl} when is_number(ttl) ->
timestamp = DateTime.from_unix!(ttl)
RichMediaExpirationWorker.new(%{"url" => url}, scheduled_at: timestamp)
|> Oban.insert()
_ ->
:ok
end
end
defp stream_update(%{activity_id: activity_id}) do
Pleroma.Activity.get_by_id(activity_id)
|> Pleroma.Activity.normalize()
|> Pleroma.Web.ActivityPub.ActivityPub.stream_out()
end
defp warm_cache(key, val), do: @cachex.put(:rich_media_cache, key, val)
defp negative_cache(key, ttl \\ nil), do: @cachex.put(:rich_media_cache, key, nil, ttl: ttl)
end
defmodule Pleroma.Web.RichMedia.Backfill.Task do
alias Pleroma.Web.RichMedia.Backfill
def run(args) do
Task.Supervisor.start_child(Pleroma.TaskSupervisor, Backfill, :run, [args],
name: {:global, {:rich_media, args.url_hash}}
)
end
end

View file

@ -0,0 +1,157 @@
defmodule Pleroma.Web.RichMedia.Card do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Pleroma.Activity
alias Pleroma.HTML
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.Web.RichMedia.Backfill
alias Pleroma.Web.RichMedia.Parser
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
@type t :: %__MODULE__{}
schema "rich_media_card" do
field(:url_hash, :binary)
field(:fields, :map)
timestamps()
end
@doc false
def changeset(card, attrs) do
card
|> cast(attrs, [:url_hash, :fields])
|> validate_required([:url_hash, :fields])
|> unique_constraint(:url_hash)
end
@spec create(String.t(), map()) :: {:ok, t()}
def create(url, fields) do
url_hash = url_to_hash(url)
fields = Map.put_new(fields, "url", url)
%__MODULE__{}
|> changeset(%{url_hash: url_hash, fields: fields})
|> Repo.insert(on_conflict: {:replace, [:fields]}, conflict_target: :url_hash)
end
@spec delete(String.t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | :ok
def delete(url) do
url_hash = url_to_hash(url)
@cachex.del(:rich_media_cache, url_hash)
case get_by_url(url) do
%__MODULE__{} = card -> Repo.delete(card)
nil -> :ok
end
end
@spec get_by_url(String.t() | nil) :: t() | nil | :error
def get_by_url(url) when is_binary(url) do
if @config_impl.get([:rich_media, :enabled]) do
url_hash = url_to_hash(url)
@cachex.fetch!(:rich_media_cache, url_hash, fn _ ->
result =
__MODULE__
|> where(url_hash: ^url_hash)
|> Repo.one()
case result do
%__MODULE__{} = card -> {:commit, card}
_ -> {:ignore, nil}
end
end)
else
:error
end
end
def get_by_url(nil), do: nil
@spec get_or_backfill_by_url(String.t(), map()) :: t() | nil
def get_or_backfill_by_url(url, backfill_opts \\ %{}) do
case get_by_url(url) do
%__MODULE__{} = card ->
card
nil ->
backfill_opts = Map.put(backfill_opts, :url, url)
Backfill.start(backfill_opts)
nil
:error ->
nil
end
end
@spec get_by_object(Object.t()) :: t() | nil | :error
def get_by_object(object) do
case HTML.extract_first_external_url_from_object(object) do
nil -> nil
url -> get_or_backfill_by_url(url)
end
end
@spec get_by_activity(Activity.t()) :: t() | nil | :error
# Fake/Draft activity
def get_by_activity(%Activity{id: "pleroma:fakeid"} = activity) do
with %Object{} = object <- Object.normalize(activity, fetch: false),
url when not is_nil(url) <- HTML.extract_first_external_url_from_object(object) do
case get_by_url(url) do
# Cache hit
%__MODULE__{} = card ->
card
# Cache miss, but fetch for rendering the Draft
_ ->
with {:ok, fields} <- Parser.parse(url),
{:ok, card} <- create(url, fields) do
card
else
_ -> nil
end
end
else
_ ->
nil
end
end
def get_by_activity(activity) do
with %Object{} = object <- Object.normalize(activity, fetch: false),
{_, nil} <- {:cached, get_cached_url(object, activity.id)} do
nil
else
{:cached, url} ->
get_or_backfill_by_url(url, %{activity_id: activity.id})
_ ->
:error
end
end
@spec url_to_hash(String.t()) :: String.t()
def url_to_hash(url) do
:crypto.hash(:sha256, url) |> Base.encode16(case: :lower)
end
defp get_cached_url(object, activity_id) do
key = "URL|#{activity_id}"
@cachex.fetch!(:scrubber_cache, key, fn _ ->
url = HTML.extract_first_external_url_from_object(object)
Activity.HTML.add_cache_key_for(activity_id, key)
{:commit, url}
end)
end
end

View file

@ -3,65 +3,13 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Helpers do defmodule Pleroma.Web.RichMedia.Helpers do
alias Pleroma.Activity alias Pleroma.Config
alias Pleroma.HTML
alias Pleroma.Object
alias Pleroma.Web.RichMedia.Parser
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
@options [
pool: :media,
max_body: 2_000_000,
recv_timeout: 2_000
]
def fetch_data_for_object(object) do
with true <- @config_impl.get([:rich_media, :enabled]),
{:ok, page_url} <-
HTML.extract_first_external_url_from_object(object),
{:ok, rich_media} <- Parser.parse(page_url) do
%{page_url: page_url, rich_media: rich_media}
else
_ -> %{}
end
end
def fetch_data_for_activity(%Activity{data: %{"type" => "Create"}} = activity) do
with true <- @config_impl.get([:rich_media, :enabled]),
%Object{} = object <- Object.normalize(activity, fetch: false) do
if object.data["fake"] do
fetch_data_for_object(object)
else
key = "URL|#{activity.id}"
@cachex.fetch!(:scrubber_cache, key, fn _ ->
result = fetch_data_for_object(object)
cond do
match?(%{page_url: _, rich_media: _}, result) ->
Activity.HTML.add_cache_key_for(activity.id, key)
{:commit, result}
true ->
{:ignore, %{}}
end
end)
end
else
_ -> %{}
end
end
def fetch_data_for_activity(_), do: %{}
def rich_media_get(url) do def rich_media_get(url) do
headers = [{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}] headers = [{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}]
head_check = head_check =
case Pleroma.HTTP.head(url, headers, @options) do case Pleroma.HTTP.head(url, headers, http_options()) do
# If the HEAD request didn't reach the server for whatever reason, # If the HEAD request didn't reach the server for whatever reason,
# we assume the GET that comes right after won't either # we assume the GET that comes right after won't either
{:error, _} = e -> {:error, _} = e ->
@ -76,7 +24,7 @@ defmodule Pleroma.Web.RichMedia.Helpers do
:ok :ok
end end
with :ok <- head_check, do: Pleroma.HTTP.get(url, headers, @options) with :ok <- head_check, do: Pleroma.HTTP.get(url, headers, http_options())
end end
defp check_content_type(headers) do defp check_content_type(headers) do
@ -92,12 +40,13 @@ defmodule Pleroma.Web.RichMedia.Helpers do
end end
end end
@max_body @options[:max_body]
defp check_content_length(headers) do defp check_content_length(headers) do
max_body = Keyword.get(http_options(), :max_body)
case List.keyfind(headers, "content-length", 0) do case List.keyfind(headers, "content-length", 0) do
{_, maybe_content_length} -> {_, maybe_content_length} ->
case Integer.parse(maybe_content_length) do case Integer.parse(maybe_content_length) do
{content_length, ""} when content_length <= @max_body -> :ok {content_length, ""} when content_length <= max_body -> :ok
{_, ""} -> {:error, :body_too_large} {_, ""} -> {:error, :body_too_large}
_ -> :ok _ -> :ok
end end
@ -106,4 +55,11 @@ defmodule Pleroma.Web.RichMedia.Helpers do
:ok :ok
end end
end end
defp http_options do
[
pool: :media,
max_body: Config.get([:rich_media, :max_body], 5_000_000)
]
end
end end

View file

@ -5,134 +5,28 @@
defmodule Pleroma.Web.RichMedia.Parser do defmodule Pleroma.Web.RichMedia.Parser do
require Logger require Logger
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config) @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
defp parsers do defp parsers do
Pleroma.Config.get([:rich_media, :parsers]) Pleroma.Config.get([:rich_media, :parsers])
end end
def parse(nil), do: {:error, "No URL provided"} def parse(nil), do: nil
@spec parse(String.t()) :: {:ok, map()} | {:error, any()} @spec parse(String.t()) :: {:ok, map()} | {:error, any()}
def parse(url) do def parse(url) do
with :ok <- validate_page_url(url), with :ok <- validate_page_url(url),
{:ok, data} <- get_cached_or_parse(url), {:ok, data} <- parse_url(url) do
{:ok, _} <- set_ttl_based_on_image(data, url) do data = Map.put(data, "url", url)
{:ok, data} {:ok, data}
end end
end end
defp get_cached_or_parse(url) do defp parse_url(url) do
case @cachex.fetch(:rich_media_cache, url, fn ->
case parse_url(url) do
{:ok, _} = res ->
{:commit, res}
{:error, reason} = e ->
# Unfortunately we have to log errors here, instead of doing that
# along with ttl setting at the bottom. Otherwise we can get log spam
# if more than one process was waiting for the rich media card
# while it was generated. Ideally we would set ttl here as well,
# so we don't override it number_of_waiters_on_generation
# times, but one, obviously, can't set ttl for not-yet-created entry
# and Cachex doesn't support returning ttl from the fetch callback.
log_error(url, reason)
{:commit, e}
end
end) do
{action, res} when action in [:commit, :ok] ->
case res do
{:ok, _data} = res ->
res
{:error, reason} = e ->
if action == :commit, do: set_error_ttl(url, reason)
e
end
{:error, e} ->
{:error, {:cachex_error, e}}
end
end
defp set_error_ttl(_url, :body_too_large), do: :ok
defp set_error_ttl(_url, {:content_type, _}), do: :ok
# The TTL is not set for the errors above, since they are unlikely to change
# with time
defp set_error_ttl(url, _reason) do
ttl = Pleroma.Config.get([:rich_media, :failure_backoff], 60_000)
@cachex.expire(:rich_media_cache, url, ttl)
:ok
end
defp log_error(url, {:invalid_metadata, data}) do
Logger.debug(fn -> "Incomplete or invalid metadata for #{url}: #{inspect(data)}" end)
end
defp log_error(url, reason) do
Logger.warning(fn -> "Rich media error for #{url}: #{inspect(reason)}" end)
end
@doc """
Set the rich media cache based on the expiration time of image.
Adopt behaviour `Pleroma.Web.RichMedia.Parser.TTL`
## Example
defmodule MyModule do
@behaviour Pleroma.Web.RichMedia.Parser.TTL
def ttl(data, url) do
image_url = Map.get(data, :image)
# do some parsing in the url and get the ttl of the image
# and return ttl is unix time
parse_ttl_from_url(image_url)
end
end
Define the module in the config
config :pleroma, :rich_media,
ttl_setters: [MyModule]
"""
@spec set_ttl_based_on_image(map(), String.t()) ::
{:ok, integer() | :noop} | {:error, :no_key}
def set_ttl_based_on_image(data, url) do
case get_ttl_from_image(data, url) do
ttl when is_number(ttl) ->
ttl = ttl * 1000
case @cachex.expire_at(:rich_media_cache, url, ttl) do
{:ok, true} -> {:ok, ttl}
{:ok, false} -> {:error, :no_key}
end
_ ->
{:ok, :noop}
end
end
defp get_ttl_from_image(data, url) do
[:rich_media, :ttl_setters]
|> Pleroma.Config.get()
|> Enum.reduce({:ok, nil}, fn
module, {:ok, _ttl} ->
module.ttl(data, url)
_, error ->
error
end)
end
def parse_url(url) do
with {:ok, %Tesla.Env{body: html}} <- Pleroma.Web.RichMedia.Helpers.rich_media_get(url), with {:ok, %Tesla.Env{body: html}} <- Pleroma.Web.RichMedia.Helpers.rich_media_get(url),
{:ok, html} <- Floki.parse_document(html) do {:ok, html} <- Floki.parse_document(html) do
html html
|> maybe_parse() |> maybe_parse()
|> Map.put("url", url)
|> clean_parsed_data() |> clean_parsed_data()
|> check_parsed_data() |> check_parsed_data()
end end

View file

@ -4,4 +4,17 @@
defmodule Pleroma.Web.RichMedia.Parser.TTL do defmodule Pleroma.Web.RichMedia.Parser.TTL do
@callback ttl(map(), String.t()) :: integer() | nil @callback ttl(map(), String.t()) :: integer() | nil
@spec process(map(), String.t()) :: {:ok, integer() | nil}
def process(data, url) do
[:rich_media, :ttl_setters]
|> Pleroma.Config.get()
|> Enum.reduce_while({:ok, nil}, fn
module, acc ->
case module.ttl(data, url) do
ttl when is_number(ttl) -> {:halt, {:ok, ttl}}
_ -> {:cont, acc}
end
end)
end
end end

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do
@impl true @impl true
def ttl(data, _url) do def ttl(data, _url) do
image = Map.get(data, :image) image = Map.get(data, "image")
if aws_signed_url?(image) do if aws_signed_url?(image) do
image image
@ -15,14 +15,15 @@ defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do
|> format_query_params() |> format_query_params()
|> get_expiration_timestamp() |> get_expiration_timestamp()
else else
{:error, "Not aws signed url #{inspect(image)}"} nil
end end
end end
defp aws_signed_url?(image) when is_binary(image) and image != "" do defp aws_signed_url?(image) when is_binary(image) and image != "" do
%URI{host: host, query: query} = URI.parse(image) %URI{host: host, query: query} = URI.parse(image)
String.contains?(host, "amazonaws.com") and String.contains?(query, "X-Amz-Expires") is_binary(host) and String.contains?(host, "amazonaws.com") and
String.contains?(query, "X-Amz-Expires")
end end
defp aws_signed_url?(_), do: nil defp aws_signed_url?(_), do: nil

View file

@ -0,0 +1,20 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parser.TTL.Opengraph do
@behaviour Pleroma.Web.RichMedia.Parser.TTL
@impl true
def ttl(%{"ttl" => ttl_string}, _url) when is_binary(ttl_string) do
try do
ttl = String.to_integer(ttl_string)
now = DateTime.utc_now() |> DateTime.to_unix()
now + ttl
rescue
_ -> nil
end
end
def ttl(_, _), do: nil
end

View file

@ -292,6 +292,11 @@ defmodule Pleroma.Web.Router do
post("/frontends/install", FrontendController, :install) post("/frontends/install", FrontendController, :install)
post("/backups", AdminAPIController, :create_backup) post("/backups", AdminAPIController, :create_backup)
get("/rules", RuleController, :index)
post("/rules", RuleController, :create)
patch("/rules/:id", RuleController, :update)
delete("/rules/:id", RuleController, :delete)
end end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role) # AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
@ -765,12 +770,12 @@ defmodule Pleroma.Web.Router do
get("/instance", InstanceController, :show) get("/instance", InstanceController, :show)
get("/instance/peers", InstanceController, :peers) get("/instance/peers", InstanceController, :peers)
get("/instance/rules", InstanceController, :rules)
get("/instance/translation_languages", InstanceController, :translation_languages) get("/instance/translation_languages", InstanceController, :translation_languages)
get("/statuses", StatusController, :index) get("/statuses", StatusController, :index)
get("/statuses/:id", StatusController, :show) get("/statuses/:id", StatusController, :show)
get("/statuses/:id/context", StatusController, :context) get("/statuses/:id/context", StatusController, :context)
get("/statuses/:id/card", StatusController, :card)
get("/statuses/:id/favourited_by", StatusController, :favourited_by) get("/statuses/:id/favourited_by", StatusController, :favourited_by)
get("/statuses/:id/reblogged_by", StatusController, :reblogged_by) get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
get("/statuses/:id/history", StatusController, :show_history) get("/statuses/:id/history", StatusController, :show_history)

View file

@ -0,0 +1,15 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.RichMediaExpirationWorker do
alias Pleroma.Web.RichMedia.Card
use Oban.Worker,
queue: :rich_media_expiration
@impl Oban.Worker
def perform(%Job{args: %{"url" => url} = _args}) do
Card.delete(url)
end
end

View file

@ -0,0 +1,12 @@
defmodule Pleroma.Repo.Migrations.CreateRules do
use Ecto.Migration
def change do
create_if_not_exists table(:rules) do
add(:priority, :integer, default: 0, null: false)
add(:text, :text, null: false)
timestamps()
end
end
end

View file

@ -0,0 +1,14 @@
defmodule Pleroma.Repo.Migrations.CreateRichMediaCard do
use Ecto.Migration
def change do
create table(:rich_media_card) do
add(:url_hash, :bytea)
add(:fields, :map)
timestamps()
end
create(unique_index(:rich_media_card, [:url_hash]))
end
end

View file

@ -0,0 +1,13 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Repo.Migrations.AddHintToRules do
use Ecto.Migration
def change do
alter table(:rules) do
add_if_not_exists(:hint, :text)
end
end
end

392
test/fixtures/rich_media/reddit.html vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -202,7 +202,7 @@ defmodule Pleroma.HTMLTest do
}) })
object = Object.normalize(activity, fetch: false) object = Object.normalize(activity, fetch: false)
{:ok, url} = HTML.extract_first_external_url_from_object(object) url = HTML.extract_first_external_url_from_object(object)
assert url == "https://github.com/komeiji-satori/Dress" assert url == "https://github.com/komeiji-satori/Dress"
end end
@ -217,7 +217,7 @@ defmodule Pleroma.HTMLTest do
}) })
object = Object.normalize(activity, fetch: false) object = Object.normalize(activity, fetch: false)
{:ok, url} = HTML.extract_first_external_url_from_object(object) url = HTML.extract_first_external_url_from_object(object)
assert url == "https://github.com/syuilo/misskey/blob/develop/docs/setup.en.md" assert url == "https://github.com/syuilo/misskey/blob/develop/docs/setup.en.md"
@ -233,7 +233,7 @@ defmodule Pleroma.HTMLTest do
}) })
object = Object.normalize(activity, fetch: false) object = Object.normalize(activity, fetch: false)
{:ok, url} = HTML.extract_first_external_url_from_object(object) url = HTML.extract_first_external_url_from_object(object)
assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140" assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140"
end end
@ -249,7 +249,7 @@ defmodule Pleroma.HTMLTest do
}) })
object = Object.normalize(activity, fetch: false) object = Object.normalize(activity, fetch: false)
{:ok, url} = HTML.extract_first_external_url_from_object(object) url = HTML.extract_first_external_url_from_object(object)
assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140" assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140"
end end
@ -261,7 +261,7 @@ defmodule Pleroma.HTMLTest do
object = Object.normalize(activity, fetch: false) object = Object.normalize(activity, fetch: false)
assert {:ok, nil} = HTML.extract_first_external_url_from_object(object) assert nil == HTML.extract_first_external_url_from_object(object)
end end
test "skips attachment links" do test "skips attachment links" do
@ -275,7 +275,7 @@ defmodule Pleroma.HTMLTest do
object = Object.normalize(activity, fetch: false) object = Object.normalize(activity, fetch: false)
assert {:ok, nil} = HTML.extract_first_external_url_from_object(object) assert nil == HTML.extract_first_external_url_from_object(object)
end end
end end
end end

View file

@ -6,7 +6,6 @@ defmodule Pleroma.NotificationTest do
use Pleroma.DataCase, async: false use Pleroma.DataCase, async: false
import Pleroma.Factory import Pleroma.Factory
import Mock
alias Pleroma.FollowingRelationship alias Pleroma.FollowingRelationship
alias Pleroma.Notification alias Pleroma.Notification
@ -18,8 +17,6 @@ defmodule Pleroma.NotificationTest do
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.Push
alias Pleroma.Web.Streamer
setup do setup do
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
@ -175,158 +172,7 @@ defmodule Pleroma.NotificationTest do
assert [user2.id, user3.id, user1.id] == Enum.map(notifications, & &1.user_id) assert [user2.id, user3.id, user1.id] == Enum.map(notifications, & &1.user_id)
end end
describe "CommonApi.post/2 notification-related functionality" do
test_with_mock "creates but does NOT send notification to blocker user",
Push,
[:passthrough],
[] do
user = insert(:user)
blocker = insert(:user)
{:ok, _user_relationship} = User.block(blocker, user)
{:ok, _activity} = CommonAPI.post(user, %{status: "hey @#{blocker.nickname}!"})
blocker_id = blocker.id
assert [%Notification{user_id: ^blocker_id}] = Repo.all(Notification)
refute called(Push.send(:_))
end
test_with_mock "creates but does NOT send notification to notification-muter user",
Push,
[:passthrough],
[] do
user = insert(:user)
muter = insert(:user)
{:ok, _user_relationships} = User.mute(muter, user)
{:ok, _activity} = CommonAPI.post(user, %{status: "hey @#{muter.nickname}!"})
muter_id = muter.id
assert [%Notification{user_id: ^muter_id}] = Repo.all(Notification)
refute called(Push.send(:_))
end
test_with_mock "creates but does NOT send notification to thread-muter user",
Push,
[:passthrough],
[] do
user = insert(:user)
thread_muter = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{thread_muter.nickname}!"})
{:ok, _} = CommonAPI.add_mute(thread_muter, activity)
{:ok, _same_context_activity} =
CommonAPI.post(user, %{
status: "hey-hey-hey @#{thread_muter.nickname}!",
in_reply_to_status_id: activity.id
})
[pre_mute_notification, post_mute_notification] =
Repo.all(from(n in Notification, where: n.user_id == ^thread_muter.id, order_by: n.id))
pre_mute_notification_id = pre_mute_notification.id
post_mute_notification_id = post_mute_notification.id
assert called(
Push.send(
:meck.is(fn
%Notification{id: ^pre_mute_notification_id} -> true
_ -> false
end)
)
)
refute called(
Push.send(
:meck.is(fn
%Notification{id: ^post_mute_notification_id} -> true
_ -> false
end)
)
)
end
end
describe "create_notification" do describe "create_notification" do
@tag needs_streamer: true
test "it creates a notification for user and send to the 'user' and the 'user:notification' stream" do
%{user: user, token: oauth_token} = oauth_access(["read"])
task =
Task.async(fn ->
{:ok, _topic} = Streamer.get_topic_and_add_socket("user", user, oauth_token)
assert_receive {:render_with_user, _, _, _, _}, 4_000
end)
task_user_notification =
Task.async(fn ->
{:ok, _topic} =
Streamer.get_topic_and_add_socket("user:notification", user, oauth_token)
assert_receive {:render_with_user, _, _, _, _}, 4_000
end)
activity = insert(:note_activity)
notify = Notification.create_notification(activity, user)
assert notify.user_id == user.id
Task.await(task)
Task.await(task_user_notification)
end
test "it creates a notification for user if the user blocks the activity author" do
activity = insert(:note_activity)
author = User.get_cached_by_ap_id(activity.data["actor"])
user = insert(:user)
{:ok, _user_relationship} = User.block(user, author)
assert Notification.create_notification(activity, user)
end
test "it creates a notification for the user if the user mutes the activity author" do
muter = insert(:user)
muted = insert(:user)
{:ok, _} = User.mute(muter, muted)
muter = Repo.get(User, muter.id)
{:ok, activity} = CommonAPI.post(muted, %{status: "Hi @#{muter.nickname}"})
notification = Notification.create_notification(activity, muter)
assert notification.id
assert notification.seen
end
test "notification created if user is muted without notifications" do
muter = insert(:user)
muted = insert(:user)
{:ok, _user_relationships} = User.mute(muter, muted, %{notifications: false})
{:ok, activity} = CommonAPI.post(muted, %{status: "Hi @#{muter.nickname}"})
assert Notification.create_notification(activity, muter)
end
test "it creates a notification for an activity from a muted thread" do
muter = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(muter, %{status: "hey"})
CommonAPI.add_mute(muter, activity)
{:ok, activity} =
CommonAPI.post(other_user, %{
status: "Hi @#{muter.nickname}",
in_reply_to_status_id: activity.id
})
notification = Notification.create_notification(activity, muter)
assert notification.id
assert notification.seen
end
test "it disables notifications from strangers" do test "it disables notifications from strangers" do
follower = insert(:user) follower = insert(:user)
@ -680,7 +526,7 @@ defmodule Pleroma.NotificationTest do
status: "hey @#{other_user.nickname}!" status: "hey @#{other_user.nickname}!"
}) })
{enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity) enabled_receivers = Notification.get_notified_from_activity(activity)
assert other_user in enabled_receivers assert other_user in enabled_receivers
end end
@ -712,7 +558,7 @@ defmodule Pleroma.NotificationTest do
{:ok, activity} = Transmogrifier.handle_incoming(create_activity) {:ok, activity} = Transmogrifier.handle_incoming(create_activity)
{enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity) enabled_receivers = Notification.get_notified_from_activity(activity)
assert other_user in enabled_receivers assert other_user in enabled_receivers
end end
@ -739,7 +585,7 @@ defmodule Pleroma.NotificationTest do
{:ok, activity} = Transmogrifier.handle_incoming(create_activity) {:ok, activity} = Transmogrifier.handle_incoming(create_activity)
{enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity) enabled_receivers = Notification.get_notified_from_activity(activity)
assert other_user not in enabled_receivers assert other_user not in enabled_receivers
end end
@ -756,8 +602,7 @@ defmodule Pleroma.NotificationTest do
{:ok, activity_two} = CommonAPI.favorite(third_user, activity_one.id) {:ok, activity_two} = CommonAPI.favorite(third_user, activity_one.id)
{enabled_receivers, _disabled_receivers} = enabled_receivers = Notification.get_notified_from_activity(activity_two)
Notification.get_notified_from_activity(activity_two)
assert other_user not in enabled_receivers assert other_user not in enabled_receivers
end end
@ -779,7 +624,7 @@ defmodule Pleroma.NotificationTest do
|> Map.put("to", [other_user.ap_id | like_data["to"]]) |> Map.put("to", [other_user.ap_id | like_data["to"]])
|> ActivityPub.persist(local: true) |> ActivityPub.persist(local: true)
{enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(like) enabled_receivers = Notification.get_notified_from_activity(like)
assert other_user not in enabled_receivers assert other_user not in enabled_receivers
end end
@ -796,39 +641,36 @@ defmodule Pleroma.NotificationTest do
{:ok, activity_two} = CommonAPI.repeat(activity_one.id, third_user) {:ok, activity_two} = CommonAPI.repeat(activity_one.id, third_user)
{enabled_receivers, _disabled_receivers} = enabled_receivers = Notification.get_notified_from_activity(activity_two)
Notification.get_notified_from_activity(activity_two)
assert other_user not in enabled_receivers assert other_user not in enabled_receivers
end end
test "it returns blocking recipient in disabled recipients list" do test "it does not return blocking recipient in recipients list" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, _user_relationship} = User.block(other_user, user) {:ok, _user_relationship} = User.block(other_user, user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"})
{enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity) enabled_receivers = Notification.get_notified_from_activity(activity)
assert [] == enabled_receivers assert [] == enabled_receivers
assert [other_user] == disabled_receivers
end end
test "it returns notification-muting recipient in disabled recipients list" do test "it does not return notification-muting recipient in recipients list" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, _user_relationships} = User.mute(other_user, user) {:ok, _user_relationships} = User.mute(other_user, user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"})
{enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity) enabled_receivers = Notification.get_notified_from_activity(activity)
assert [] == enabled_receivers assert [] == enabled_receivers
assert [other_user] == disabled_receivers
end end
test "it returns thread-muting recipient in disabled recipients list" do test "it does not return thread-muting recipient in recipients list" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
@ -842,14 +684,12 @@ defmodule Pleroma.NotificationTest do
in_reply_to_status_id: activity.id in_reply_to_status_id: activity.id
}) })
{enabled_receivers, disabled_receivers} = enabled_receivers = Notification.get_notified_from_activity(same_context_activity)
Notification.get_notified_from_activity(same_context_activity)
assert [other_user] == disabled_receivers
refute other_user in enabled_receivers refute other_user in enabled_receivers
end end
test "it returns non-following domain-blocking recipient in disabled recipients list" do test "it does not return non-following domain-blocking recipient in recipients list" do
blocked_domain = "blocked.domain" blocked_domain = "blocked.domain"
user = insert(:user, %{ap_id: "https://#{blocked_domain}/@actor"}) user = insert(:user, %{ap_id: "https://#{blocked_domain}/@actor"})
other_user = insert(:user) other_user = insert(:user)
@ -858,10 +698,9 @@ defmodule Pleroma.NotificationTest do
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"})
{enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity) enabled_receivers = Notification.get_notified_from_activity(activity)
assert [] == enabled_receivers assert [] == enabled_receivers
assert [other_user] == disabled_receivers
end end
test "it returns following domain-blocking recipient in enabled recipients list" do test "it returns following domain-blocking recipient in enabled recipients list" do
@ -874,10 +713,9 @@ defmodule Pleroma.NotificationTest do
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"})
{enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity) enabled_receivers = Notification.get_notified_from_activity(activity)
assert [other_user] == enabled_receivers assert [other_user] == enabled_receivers
assert [] == disabled_receivers
end end
test "it sends edited notifications to those who repeated a status" do test "it sends edited notifications to those who repeated a status" do
@ -897,11 +735,10 @@ defmodule Pleroma.NotificationTest do
status: "hey @#{other_user.nickname}! mew mew" status: "hey @#{other_user.nickname}! mew mew"
}) })
{enabled_receivers, _disabled_receivers} = enabled_receivers = Notification.get_notified_from_activity(edit_activity)
Notification.get_notified_from_activity(edit_activity)
assert repeated_user in enabled_receivers assert repeated_user in enabled_receivers
assert other_user not in enabled_receivers refute other_user in enabled_receivers
end end
end end
@ -1189,13 +1026,13 @@ defmodule Pleroma.NotificationTest do
assert Notification.for_user(user) == [] assert Notification.for_user(user) == []
end end
test "it returns notifications from a muted user when with_muted is set", %{user: user} do test "it doesn't return notifications from a muted user when with_muted is set", %{user: user} do
muted = insert(:user) muted = insert(:user)
{:ok, _user_relationships} = User.mute(user, muted) {:ok, _user_relationships} = User.mute(user, muted)
{:ok, _activity} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"}) {:ok, _activity} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"})
assert length(Notification.for_user(user, %{with_muted: true})) == 1 assert Enum.empty?(Notification.for_user(user, %{with_muted: true}))
end end
test "it doesn't return notifications from a blocked user when with_muted is set", %{ test "it doesn't return notifications from a blocked user when with_muted is set", %{

View file

@ -0,0 +1,57 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.RuleTest do
use Pleroma.DataCase, async: true
alias Pleroma.Repo
alias Pleroma.Rule
test "getting a list of rules sorted by priority" do
%{id: id1} = Rule.create(%{text: "Example rule"})
%{id: id2} = Rule.create(%{text: "Second rule", priority: 2})
%{id: id3} = Rule.create(%{text: "Third rule", priority: 1})
rules =
Rule.query()
|> Repo.all()
assert [%{id: ^id1}, %{id: ^id3}, %{id: ^id2}] = rules
end
test "creating rules" do
%{id: id} = Rule.create(%{text: "Example rule"})
assert %{text: "Example rule"} = Rule.get(id)
end
test "editing rules" do
%{id: id} = Rule.create(%{text: "Example rule"})
Rule.update(%{text: "There are no rules", priority: 2}, id)
assert %{text: "There are no rules", priority: 2} = Rule.get(id)
end
test "deleting rules" do
%{id: id} = Rule.create(%{text: "Example rule"})
Rule.delete(id)
assert [] =
Rule.query()
|> Pleroma.Repo.all()
end
test "getting rules by ids" do
%{id: id1} = Rule.create(%{text: "Example rule"})
%{id: id2} = Rule.create(%{text: "Second rule"})
%{id: _id3} = Rule.create(%{text: "Third rule"})
rules = Rule.get([id1, id2])
assert Enum.all?(rules, &(&1.id in [id1, id2]))
assert length(rules) == 2
end
end

View file

@ -827,31 +827,6 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
{:ok, announce, _} = SideEffects.handle(announce) {:ok, announce, _} = SideEffects.handle(announce)
assert Repo.get_by(Notification, user_id: poster.id, activity_id: announce.id) assert Repo.get_by(Notification, user_id: poster.id, activity_id: announce.id)
end end
test "it streams out the announce", %{announce: announce} do
with_mocks([
{
Pleroma.Web.Streamer,
[],
[
stream: fn _, _ -> nil end
]
},
{
Pleroma.Web.Push,
[],
[
send: fn _ -> nil end
]
}
]) do
{:ok, announce, _} = SideEffects.handle(announce)
assert called(Pleroma.Web.Streamer.stream(["user", "list"], announce))
assert called(Pleroma.Web.Push.send(:_))
end
end
end end
describe "removing a follower" do describe "removing a follower" do

View file

@ -11,6 +11,7 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do
alias Pleroma.ModerationLog alias Pleroma.ModerationLog
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.ReportNote alias Pleroma.ReportNote
alias Pleroma.Rule
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
setup do setup do
@ -436,6 +437,34 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do
"error" => "Invalid credentials." "error" => "Invalid credentials."
} }
end end
test "returns reports with specified role_id", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
%{id: rule_id} = Rule.create(%{text: "Example rule"})
rule_id = to_string(rule_id)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "",
rule_ids: [rule_id]
})
{:ok, _report} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: ""
})
response =
conn
|> get("/api/pleroma/admin/reports?rule_id=#{rule_id}")
|> json_response_and_validate_schema(:ok)
assert %{"reports" => [%{"id" => ^report_id}]} = response
end
end end
describe "POST /api/pleroma/admin/reports/:id/notes" do describe "POST /api/pleroma/admin/reports/:id/notes" do

View file

@ -0,0 +1,82 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.RuleControllerTest do
use Pleroma.Web.ConnCase, async: true
import Pleroma.Factory
alias Pleroma.Rule
setup do
admin = insert(:user, is_admin: true)
token = insert(:oauth_admin_token, user: admin)
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
{:ok, %{admin: admin, token: token, conn: conn}}
end
describe "GET /api/pleroma/admin/rules" do
test "sorts rules by priority", %{conn: conn} do
%{id: id1} = Rule.create(%{text: "Example rule"})
%{id: id2} = Rule.create(%{text: "Second rule", priority: 2})
%{id: id3} = Rule.create(%{text: "Third rule", priority: 1})
id1 = to_string(id1)
id2 = to_string(id2)
id3 = to_string(id3)
response =
conn
|> get("/api/pleroma/admin/rules")
|> json_response_and_validate_schema(:ok)
assert [%{"id" => ^id1}, %{"id" => ^id3}, %{"id" => ^id2}] = response
end
end
describe "POST /api/pleroma/admin/rules" do
test "creates a rule", %{conn: conn} do
%{"id" => id} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/rules", %{text: "Example rule"})
|> json_response_and_validate_schema(:ok)
assert %{text: "Example rule"} = Rule.get(id)
end
end
describe "PATCH /api/pleroma/admin/rules" do
test "edits a rule", %{conn: conn} do
%{id: id} = Rule.create(%{text: "Example rule"})
conn
|> put_req_header("content-type", "application/json")
|> patch("/api/pleroma/admin/rules/#{id}", %{text: "There are no rules", priority: 2})
|> json_response_and_validate_schema(:ok)
assert %{text: "There are no rules", priority: 2} = Rule.get(id)
end
end
describe "DELETE /api/pleroma/admin/rules" do
test "deletes a rule", %{conn: conn} do
%{id: id} = Rule.create(%{text: "Example rule"})
conn
|> put_req_header("content-type", "application/json")
|> delete("/api/pleroma/admin/rules/#{id}")
|> json_response_and_validate_schema(:ok)
assert [] =
Rule.query()
|> Pleroma.Repo.all()
end
end
end

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do
import Pleroma.Factory import Pleroma.Factory
alias Pleroma.Rule
alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.Report alias Pleroma.Web.AdminAPI.Report
alias Pleroma.Web.AdminAPI.ReportView alias Pleroma.Web.AdminAPI.ReportView
@ -38,7 +39,8 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do
statuses: [], statuses: [],
notes: [], notes: [],
state: "open", state: "open",
id: activity.id id: activity.id,
rules: []
} }
result = result =
@ -76,7 +78,8 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do
statuses: [StatusView.render("show.json", %{activity: activity})], statuses: [StatusView.render("show.json", %{activity: activity})],
state: "open", state: "open",
notes: [], notes: [],
id: report_activity.id id: report_activity.id,
rules: []
} }
result = result =
@ -168,4 +171,22 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do
assert report2.id == rendered |> Enum.at(0) |> Map.get(:id) assert report2.id == rendered |> Enum.at(0) |> Map.get(:id)
assert report1.id == rendered |> Enum.at(1) |> Map.get(:id) assert report1.id == rendered |> Enum.at(1) |> Map.get(:id)
end end
test "renders included rules" do
user = insert(:user)
other_user = insert(:user)
%{id: rule_id, text: text} = Rule.create(%{text: "Example rule"})
rule_id = to_string(rule_id)
{:ok, activity} =
CommonAPI.report(user, %{
account_id: other_user.id,
rule_ids: [rule_id]
})
assert %{rules: [%{id: ^rule_id, text: ^text}]} =
ReportView.render("show.json", Report.extract_report_info(activity))
end
end end

View file

@ -12,6 +12,7 @@ defmodule Pleroma.Web.CommonAPITest do
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.Rule
alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.UnstubbedConfigMock, as: ConfigMock
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
@ -1363,6 +1364,33 @@ defmodule Pleroma.Web.CommonAPITest do
assert first_report.data["state"] == "resolved" assert first_report.data["state"] == "resolved"
assert second_report.data["state"] == "resolved" assert second_report.data["state"] == "resolved"
end end
test "creates a report with provided rules" do
reporter = insert(:user)
target_user = insert(:user)
%{id: rule_id} = Rule.create(%{text: "There are no rules"})
reporter_ap_id = reporter.ap_id
target_ap_id = target_user.ap_id
report_data = %{
account_id: target_user.id,
rule_ids: [rule_id]
}
assert {:ok, flag_activity} = CommonAPI.report(reporter, report_data)
assert %Activity{
actor: ^reporter_ap_id,
data: %{
"type" => "Flag",
"object" => [^target_ap_id],
"state" => "open",
"rules" => [^rule_id]
}
} = flag_activity
end
end end
describe "reblog muting" do describe "reblog muting" do

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
# TODO: Should not need Cachex # TODO: Should not need Cachex
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
alias Pleroma.Rule
alias Pleroma.User alias Pleroma.User
import Pleroma.Factory import Pleroma.Factory
@ -40,7 +41,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
"banner_upload_limit" => _, "banner_upload_limit" => _,
"background_image" => from_config_background, "background_image" => from_config_background,
"shout_limit" => _, "shout_limit" => _,
"description_limit" => _ "description_limit" => _,
"rules" => _
} = result } = result
assert result["pleroma"]["metadata"]["account_activation_required"] != nil assert result["pleroma"]["metadata"]["account_activation_required"] != nil
@ -126,6 +128,31 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
|> json_response_and_validate_schema(200) |> json_response_and_validate_schema(200)
end end
test "get instance rules", %{conn: conn} do
Rule.create(%{text: "Example rule", hint: "Rule description", priority: 1})
Rule.create(%{text: "Third rule", priority: 2})
Rule.create(%{text: "Second rule", priority: 1})
conn = get(conn, "/api/v1/instance")
assert result = json_response_and_validate_schema(conn, 200)
assert [
%{
"text" => "Example rule",
"hint" => "Rule description"
},
%{
"text" => "Second rule",
"hint" => ""
},
%{
"text" => "Third rule",
"hint" => ""
}
] = result["rules"]
end
test "translation languages matrix", %{conn: conn} do test "translation languages matrix", %{conn: conn} do
clear_config([Pleroma.Language.Translation, :provider], TranslationMock) clear_config([Pleroma.Language.Translation, :provider], TranslationMock)

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.Rule
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
import Pleroma.Factory import Pleroma.Factory
@ -81,6 +82,44 @@ defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do
|> json_response_and_validate_schema(200) |> json_response_and_validate_schema(200)
end end
test "submit a report with rule_ids", %{
conn: conn,
target_user: target_user
} do
%{id: rule_id} = Rule.create(%{text: "There are no rules"})
rule_id = to_string(rule_id)
assert %{"action_taken" => false, "id" => id} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/reports", %{
"account_id" => target_user.id,
"forward" => "false",
"rule_ids" => [rule_id]
})
|> json_response_and_validate_schema(200)
assert %Activity{data: %{"rules" => [^rule_id]}} = Activity.get_report(id)
end
test "rules field is empty if provided wrong rule id", %{
conn: conn,
target_user: target_user
} do
assert %{"id" => id} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/reports", %{
"account_id" => target_user.id,
"forward" => "false",
"rule_ids" => ["-1"]
})
|> json_response_and_validate_schema(200)
assert %Activity{data: %{"rules" => []}} = Activity.get_report(id)
end
test "account_id is required", %{ test "account_id is required", %{
conn: conn, conn: conn,
activity: activity activity: activity

View file

@ -329,62 +329,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert real_status == fake_status assert real_status == fake_status
end end
test "fake statuses' preview card is not cached", %{conn: conn} do
Pleroma.StaticStubbedConfigMock
|> stub(:get, fn
[:rich_media, :enabled] -> true
path -> Pleroma.Test.StaticConfig.get(path)
end)
Tesla.Mock.mock_global(fn
env ->
apply(HttpRequestMock, :request, [env])
end)
conn1 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "https://example.com/ogp",
"preview" => true
})
conn2 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "https://example.com/twitter-card",
"preview" => true
})
assert %{"card" => %{"title" => "The Rock"}} = json_response_and_validate_schema(conn1, 200)
assert %{"card" => %{"title" => "Small Island Developing States Photo Submission"}} =
json_response_and_validate_schema(conn2, 200)
end
test "posting a status with OGP link preview", %{conn: conn} do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
Pleroma.StaticStubbedConfigMock
|> stub(:get, fn
[:rich_media, :enabled] -> true
path -> Pleroma.Test.StaticConfig.get(path)
end)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "https://example.com/ogp"
})
assert %{"id" => id, "card" => %{"title" => "The Rock"}} =
json_response_and_validate_schema(conn, 200)
assert Activity.get_by_id(id)
end
test "posting a direct status", %{conn: conn} do test "posting a direct status", %{conn: conn} do
user2 = insert(:user) user2 = insert(:user)
content = "direct cofe @#{user2.nickname}" content = "direct cofe @#{user2.nickname}"
@ -1699,93 +1643,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
end end
end end
describe "cards" do
setup do
Pleroma.StaticStubbedConfigMock
|> stub(:get, fn
[:rich_media, :enabled] -> true
path -> Pleroma.Test.StaticConfig.get(path)
end)
oauth_access(["read:statuses"])
end
test "returns rich-media card", %{conn: conn, user: user} do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
{:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp"})
card_data = %{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
"image_description" => "",
"provider_name" => "example.com",
"provider_url" => "https://example.com",
"title" => "The Rock",
"type" => "link",
"url" => "https://example.com/ogp",
"description" =>
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
"pleroma" => %{
"opengraph" => %{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
"title" => "The Rock",
"type" => "video.movie",
"url" => "https://example.com/ogp",
"description" =>
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer."
}
}
}
response =
conn
|> get("/api/v1/statuses/#{activity.id}/card")
|> json_response_and_validate_schema(200)
assert response == card_data
# works with private posts
{:ok, activity} =
CommonAPI.post(user, %{status: "https://example.com/ogp", visibility: "direct"})
response_two =
conn
|> get("/api/v1/statuses/#{activity.id}/card")
|> json_response_and_validate_schema(200)
assert response_two == card_data
end
test "replaces missing description with an empty string", %{conn: conn, user: user} do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
{:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp-missing-data"})
response =
conn
|> get("/api/v1/statuses/#{activity.id}/card")
|> json_response_and_validate_schema(:ok)
assert response == %{
"type" => "link",
"title" => "Pleroma",
"description" => "",
"image" => nil,
"image_description" => "",
"provider_name" => "example.com",
"provider_url" => "https://example.com",
"url" => "https://example.com/ogp-missing-data",
"pleroma" => %{
"opengraph" => %{
"title" => "Pleroma",
"type" => "website",
"url" => "https://example.com/ogp-missing-data"
}
}
}
end
end
test "bookmarks" do test "bookmarks" do
bookmarks_uri = "/api/v1/bookmarks" bookmarks_uri = "/api/v1/bookmarks"

View file

@ -17,6 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.RichMedia.Card
require Bitwise require Bitwise
@ -732,57 +733,72 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
describe "rich media cards" do describe "rich media cards" do
test "a rich media card without a site name renders correctly" do test "a rich media card without a site name renders correctly" do
page_url = "http://example.com" page_url = "https://example.com"
card = %{ {:ok, card} =
url: page_url, Card.create(page_url, %{image: page_url <> "/example.jpg", title: "Example website"})
image: page_url <> "/example.jpg",
title: "Example website"
}
%{provider_name: "example.com"} = assert match?(%{provider_name: "example.com"}, StatusView.render("card.json", card))
StatusView.render("card.json", %{page_url: page_url, rich_media: card})
end end
test "a rich media card without a site name or image renders correctly" do test "a rich media card without a site name or image renders correctly" do
page_url = "http://example.com" page_url = "https://example.com"
card = %{ fields = %{
url: page_url, "url" => page_url,
title: "Example website" "title" => "Example website"
} }
%{provider_name: "example.com"} = {:ok, card} = Card.create(page_url, fields)
StatusView.render("card.json", %{page_url: page_url, rich_media: card})
assert match?(%{provider_name: "example.com"}, StatusView.render("card.json", card))
end end
test "a rich media card without an image renders correctly" do test "a rich media card without an image renders correctly" do
page_url = "http://example.com" page_url = "https://example.com"
card = %{ fields = %{
url: page_url, "url" => page_url,
site_name: "Example site name", "site_name" => "Example site name",
title: "Example website" "title" => "Example website"
} }
%{provider_name: "example.com"} = {:ok, card} = Card.create(page_url, fields)
StatusView.render("card.json", %{page_url: page_url, rich_media: card})
assert match?(%{provider_name: "example.com"}, StatusView.render("card.json", card))
end
test "a rich media card without descriptions returns the fields with empty strings" do
page_url = "https://example.com"
fields = %{
"url" => page_url,
"site_name" => "Example site name",
"title" => "Example website"
}
{:ok, card} = Card.create(page_url, fields)
assert match?(
%{description: "", image_description: ""},
StatusView.render("card.json", card)
)
end end
test "a rich media card with all relevant data renders correctly" do test "a rich media card with all relevant data renders correctly" do
page_url = "http://example.com" page_url = "https://example.com"
card = %{ fields = %{
"image:alt" => "Example image description", "url" => page_url,
url: page_url, "site_name" => "Example site name",
site_name: "Example site name", "title" => "Example website",
title: "Example website", "image" => page_url <> "/example.jpg",
image: page_url <> "/example.jpg", "description" => "Example description"
description: "Example description"
} }
%{provider_name: "example.com", image_description: "Example image description"} = {:ok, card} = Card.create(page_url, fields)
StatusView.render("card.json", %{page_url: page_url, rich_media: card})
assert match?(%{provider_name: "example.com"}, StatusView.render("card.json", card))
end end
test "a rich media card has all media proxied" do test "a rich media card has all media proxied" do
@ -792,25 +808,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
ConfigMock ConfigMock
|> stub_with(Pleroma.Test.StaticConfig) |> stub_with(Pleroma.Test.StaticConfig)
page_url = "http://example.com" page_url = "https://example.com"
card = %{ fields = %{
url: page_url, "url" => page_url,
site_name: "Example site name", "site_name" => "Example site name",
title: "Example website", "title" => "Example website",
image: page_url <> "/example.jpg", "image" => page_url <> "/example.jpg",
audio: page_url <> "/example.ogg", "audio" => page_url <> "/example.ogg",
video: page_url <> "/example.mp4", "video" => page_url <> "/example.mp4",
description: "Example description" "description" => "Example description"
} }
strcard = for {k, v} <- card, into: %{}, do: {to_string(k), v} {:ok, card} = Card.create(page_url, fields)
%{ %{
provider_name: "example.com", provider_name: "example.com",
image: image, image: image,
pleroma: %{opengraph: og} pleroma: %{opengraph: og}
} = StatusView.render("card.json", %{page_url: page_url, rich_media: strcard}) } = StatusView.render("card.json", card)
assert String.match?(image, ~r/\/proxy\//) assert String.match?(image, ~r/\/proxy\//)
assert String.match?(og["image"], ~r/\/proxy\//) assert String.match?(og["image"], ~r/\/proxy\//)

View file

@ -9,7 +9,6 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceViewTest do
alias Pleroma.Chat alias Pleroma.Chat
alias Pleroma.Chat.MessageReference alias Pleroma.Chat.MessageReference
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.StaticStubbedConfigMock
alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.UnstubbedConfigMock, as: ConfigMock
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
@ -18,6 +17,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceViewTest do
import Mox import Mox
import Pleroma.Factory import Pleroma.Factory
setup do: clear_config([:rich_media, :enabled], true)
test "it displays a chat message" do test "it displays a chat message" do
user = insert(:user) user = insert(:user)
recipient = insert(:user) recipient = insert(:user)
@ -62,16 +63,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceViewTest do
assert match?([%{shortcode: "firefox"}], chat_message[:emojis]) assert match?([%{shortcode: "firefox"}], chat_message[:emojis])
assert chat_message[:idempotency_key] == "123" assert chat_message[:idempotency_key] == "123"
StaticStubbedConfigMock Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
|> stub(:get, fn
[:rich_media, :enabled] -> true
path -> Pleroma.Test.StaticConfig.get(path)
end)
Tesla.Mock.mock_global(fn
%{url: "https://example.com/ogp"} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")}
end)
{:ok, activity} = {:ok, activity} =
CommonAPI.post_chat_message(recipient, user, "gkgkgk https://example.com/ogp", CommonAPI.post_chat_message(recipient, user, "gkgkgk https://example.com/ogp",

View file

@ -0,0 +1,71 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.CardTest do
use Pleroma.DataCase, async: true
alias Pleroma.UnstubbedConfigMock, as: ConfigMock
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.RichMedia.Card
import Mox
import Pleroma.Factory
import Tesla.Mock
setup do
mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
ConfigMock
|> stub_with(Pleroma.Test.StaticConfig)
:ok
end
setup do: clear_config([:rich_media, :enabled], true)
test "crawls URL in activity" do
user = insert(:user)
url = "https://example.com/ogp"
url_hash = Card.url_to_hash(url)
{:ok, activity} =
CommonAPI.post(user, %{
status: "[test](#{url})",
content_type: "text/markdown"
})
assert %Card{url_hash: ^url_hash, fields: _} = Card.get_by_activity(activity)
end
test "recrawls URLs on status edits/updates" do
original_url = "https://google.com/"
original_url_hash = Card.url_to_hash(original_url)
updated_url = "https://yahoo.com/"
updated_url_hash = Card.url_to_hash(updated_url)
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "I like this site #{original_url}"})
# Force a backfill
Card.get_by_activity(activity)
assert match?(
%Card{url_hash: ^original_url_hash, fields: _},
Card.get_by_activity(activity)
)
{:ok, _} = CommonAPI.update(user, activity, %{status: "I like this site #{updated_url}"})
activity = Pleroma.Activity.get_by_id(activity.id)
# Force a backfill
Card.get_by_activity(activity)
assert match?(
%Card{url_hash: ^updated_url_hash, fields: _},
Card.get_by_activity(activity)
)
end
end

View file

@ -1,137 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.HelpersTest do
use Pleroma.DataCase, async: false
alias Pleroma.StaticStubbedConfigMock, as: ConfigMock
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.RichMedia.Helpers
import Mox
import Pleroma.Factory
import Tesla.Mock
setup do
mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
ConfigMock
|> stub(:get, fn
[:rich_media, :enabled] -> false
path -> Pleroma.Test.StaticConfig.get(path)
end)
|> stub(:get, fn
path, default -> Pleroma.Test.StaticConfig.get(path, default)
end)
:ok
end
test "refuses to crawl incomplete URLs" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "[test](example.com/ogp)",
content_type: "text/markdown"
})
ConfigMock
|> stub(:get, fn
[:rich_media, :enabled] -> true
path -> Pleroma.Test.StaticConfig.get(path)
end)
assert %{} == Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
end
test "refuses to crawl malformed URLs" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "[test](example.com[]/ogp)",
content_type: "text/markdown"
})
ConfigMock
|> stub(:get, fn
[:rich_media, :enabled] -> true
path -> Pleroma.Test.StaticConfig.get(path)
end)
assert %{} == Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
end
test "crawls valid, complete URLs" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "[test](https://example.com/ogp)",
content_type: "text/markdown"
})
ConfigMock
|> stub(:get, fn
[:rich_media, :enabled] -> true
path -> Pleroma.Test.StaticConfig.get(path)
end)
assert %{page_url: "https://example.com/ogp", rich_media: _} =
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
end
test "recrawls URLs on updates" do
original_url = "https://google.com/"
updated_url = "https://yahoo.com/"
Pleroma.StaticStubbedConfigMock
|> stub(:get, fn
[:rich_media, :enabled] -> true
path -> Pleroma.Test.StaticConfig.get(path)
end)
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "I like this site #{original_url}"})
assert match?(
%{page_url: ^original_url, rich_media: _},
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
)
{:ok, _} = CommonAPI.update(user, activity, %{status: "I like this site #{updated_url}"})
activity = Pleroma.Activity.get_by_id(activity.id)
assert match?(
%{page_url: ^updated_url, rich_media: _},
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
)
end
test "refuses to crawl URLs of private network from posts" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{status: "http://127.0.0.1:4000/notice/9kCP7VNyPJXFOXDrgO"})
{:ok, activity2} = CommonAPI.post(user, %{status: "https://10.111.10.1/notice/9kCP7V"})
{:ok, activity3} = CommonAPI.post(user, %{status: "https://172.16.32.40/notice/9kCP7V"})
{:ok, activity4} = CommonAPI.post(user, %{status: "https://192.168.10.40/notice/9kCP7V"})
{:ok, activity5} = CommonAPI.post(user, %{status: "https://pleroma.local/notice/9kCP7V"})
ConfigMock
|> stub(:get, fn
[:rich_media, :enabled] -> true
path -> Pleroma.Test.StaticConfig.get(path)
end)
assert %{} == Helpers.fetch_data_for_activity(activity)
assert %{} == Helpers.fetch_data_for_activity(activity2)
assert %{} == Helpers.fetch_data_for_activity(activity3)
assert %{} == Helpers.fetch_data_for_activity(activity4)
assert %{} == Helpers.fetch_data_for_activity(activity5)
end
end

View file

@ -3,8 +3,22 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrlTest do defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrlTest do
# Relies on Cachex, needs to be synchronous use Pleroma.DataCase, async: false
use Pleroma.DataCase use Oban.Testing, repo: Pleroma.Repo
import Mox
alias Pleroma.UnstubbedConfigMock, as: ConfigMock
alias Pleroma.Web.RichMedia.Card
setup do
ConfigMock
|> stub_with(Pleroma.Test.StaticConfig)
clear_config([:rich_media, :enabled], true)
:ok
end
test "s3 signed url is parsed correct for expiration time" do test "s3 signed url is parsed correct for expiration time" do
url = "https://pleroma.social/amz" url = "https://pleroma.social/amz"
@ -43,26 +57,29 @@ defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrlTest do
<meta name="twitter:site" content="Pleroma" /> <meta name="twitter:site" content="Pleroma" />
<meta name="twitter:title" content="Pleroma" /> <meta name="twitter:title" content="Pleroma" />
<meta name="twitter:description" content="Pleroma" /> <meta name="twitter:description" content="Pleroma" />
<meta name="twitter:image" content="#{Map.get(metadata, :image)}" /> <meta name="twitter:image" content="#{Map.get(metadata, "image")}" />
""" """
Tesla.Mock.mock(fn Tesla.Mock.mock(fn
%{ %{
method: :get, method: :get,
url: "https://pleroma.social/amz" url: ^url
} -> } ->
%Tesla.Env{status: 200, body: body} %Tesla.Env{status: 200, body: body}
%{method: :head} ->
%Tesla.Env{status: 200}
end) end)
Cachex.put(:rich_media_cache, url, metadata) Card.get_or_backfill_by_url(url)
Pleroma.Web.RichMedia.Parser.set_ttl_based_on_image(metadata, url) assert_enqueued(worker: Pleroma.Workers.RichMediaExpirationWorker, args: %{"url" => url})
{:ok, cache_ttl} = Cachex.ttl(:rich_media_cache, url) [%Oban.Job{scheduled_at: scheduled_at}] = all_enqueued()
# as there is delay in setting and pulling the data from cache we ignore 1 second timestamp_dt = Timex.parse!(timestamp, "{ISO:Basic:Z}")
# make it 2 seconds for flakyness
assert_in_delta(valid_till * 1000, cache_ttl, 2000) assert DateTime.diff(scheduled_at, timestamp_dt) == valid_till
end end
defp construct_s3_url(timestamp, valid_till) do defp construct_s3_url(timestamp, valid_till) do
@ -71,11 +88,11 @@ defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrlTest do
defp construct_metadata(timestamp, valid_till, url) do defp construct_metadata(timestamp, valid_till, url) do
%{ %{
image: construct_s3_url(timestamp, valid_till), "image" => construct_s3_url(timestamp, valid_till),
site: "Pleroma", "site" => "Pleroma",
title: "Pleroma", "title" => "Pleroma",
description: "Pleroma", "description" => "Pleroma",
url: url "url" => url
} }
end end
end end

View file

@ -0,0 +1,41 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parser.TTL.OpengraphTest do
use Pleroma.DataCase
use Oban.Testing, repo: Pleroma.Repo
import Mox
alias Pleroma.UnstubbedConfigMock, as: ConfigMock
alias Pleroma.Web.RichMedia.Card
setup do
ConfigMock
|> stub_with(Pleroma.Test.StaticConfig)
clear_config([:rich_media, :enabled], true)
:ok
end
test "OpenGraph TTL value is honored" do
url = "https://reddit.com/r/somepost"
Tesla.Mock.mock(fn
%{
method: :get,
url: ^url
} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/reddit.html")}
%{method: :head} ->
%Tesla.Env{status: 200}
end)
Card.get_or_backfill_by_url(url)
assert_enqueued(worker: Pleroma.Workers.RichMediaExpirationWorker, args: %{"url" => url})
end
end

View file

@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.ParserTest do defmodule Pleroma.Web.RichMedia.ParserTest do
use Pleroma.DataCase, async: false use Pleroma.DataCase
alias Pleroma.Web.RichMedia.Parser alias Pleroma.Web.RichMedia.Parser
@ -104,4 +104,27 @@ defmodule Pleroma.Web.RichMedia.ParserTest do
test "does a HEAD request to check if the body is html" do test "does a HEAD request to check if the body is html" do
assert {:error, {:content_type, _}} = Parser.parse("https://example.com/pdf-file") assert {:error, {:content_type, _}} = Parser.parse("https://example.com/pdf-file")
end end
test "refuses to crawl incomplete URLs" do
url = "example.com/ogp"
assert :error == Parser.parse(url)
end
test "refuses to crawl malformed URLs" do
url = "example.com[]/ogp"
assert :error == Parser.parse(url)
end
test "refuses to crawl URLs of private network from posts" do
[
"http://127.0.0.1:4000/notice/9kCP7VNyPJXFOXDrgO",
"https://10.111.10.1/notice/9kCP7V",
"https://172.16.32.40/notice/9kCP7V",
"https://192.168.10.40/notice/9kCP7V",
"https://pleroma.local/notice/9kCP7V"
]
|> Enum.each(fn url ->
assert :error == Parser.parse(url)
end)
end
end end