Compare commits

...

19 commits

Author SHA1 Message Date
12e7d0a25c added doc for mrf_reject_newly_created_account_notes 2024-02-17 22:25:12 +01:00
755c75d8a4 Merge pull request 'Clean up warnings (+fallback metrics)' (#685) from Oneric/akkoma:metrics into develop
Reviewed-on: AkkomaGang/akkoma#685
2024-02-17 11:41:10 +00:00
289f93f5a2 Merge pull request 'Return last_status_at as date, not datetime' (#681) from katafrakt/akkoma:fix-last-status-at into develop
Reviewed-on: AkkomaGang/akkoma#681
2024-02-17 11:37:19 +00:00
371b258c99 Merge pull request 'Fix SimplePolicy blocking account updates' (#692) from Oneric/akkoma:fix-background_removal into develop
Reviewed-on: AkkomaGang/akkoma#692
2024-02-17 10:34:16 +00:00
3b0714c4fd Fix SimplePolicy blocking account updates
This fixes an oversight in e99e2407f3
which added background_removal as a possible SimplePolicy setting.
However, it did _not_ add a default value to the base config and
as it turns out instance_list doesn’t handle unset options well.

In effect this caused federating instances with SimplePolicy enabled
but background_removal not explicitly configured to always trip up for
outgoing account updates in check_background_removal (and incoming
updates from Sharkey).
For added ""fun"" this error was able to block account updates made
e.g. via /api/v1/accounts/update_credentials.

Tests were unaffected since they explicitly override
all relevant config options.

Set a default to avoid all this
(note to self: don’t forget next time, baka!)
2024-02-17 03:10:05 +01:00
34c213f02f Merge pull request 'Federate user profile background' (#682) from Oneric/akkoma:background-federation into develop
Reviewed-on: AkkomaGang/akkoma#682
2024-02-16 21:00:10 +00:00
e99e2407f3 Add background_removal to SimplePolicy MRF 2024-02-16 16:36:45 +01:00
7622aa27ca Federate user profile background
Currently our own frontend doesn’t show backgrounds of other users, this
property is already publicly readable via REST API and likely was always
intended to be shown and federated.

Recently Sharkey added support for profile backgrounds and
immediately made them federate and be displayed to others.
We use the same AP field as Sharkey here which should make
it interoperable both ways out-of-the-box.

Ref.: 4e64397635
2024-02-16 16:35:51 +01:00
0ed815b8a1 Merge branch 'followback' into develop 2024-02-16 13:27:40 +00:00
c5dcd07e08 Merge pull request 'Fix OpenAPI spec for preferred_frontend endpoint' (#680) from katafrakt/akkoma:fix-openapi-spec-for-preferred-frontend into develop
Reviewed-on: AkkomaGang/akkoma#680
2024-02-16 12:21:00 +00:00
376f6b15ca Add ability to auto-approve followbacks
Resolves: AkkomaGang/akkoma#148
2024-02-13 15:42:37 +01:00
13e62b4e51 Fix schema and docs for status_ttl_days and instance
Fixes misspelling and omission of and example in commit
0cfd5b4e89 which added the
status_ttl_property. This was the only place this commit
referred to the property as note_ttl_days.

Partially fixes the omitted schema update of the instance metadata addition
from commit b7e8ce2350. A proper full schema
for nodeinfo is still missing.
2024-02-13 15:39:52 +01:00
29f564f700 Use fallbacks of summary metrics for prometheus 2024-02-12 02:00:09 +01:00
16197ff57a Display memory as MB in live dashboard
With kilobyte the resulting numbers got too large and were cut off
in the charts, making them useless. However, even an idle Akkoma
server’s memory usage is in the lower hundreths of megabytes, so
we don’t need this much precision to begin with for the dashboard.

Other metric users might prefer base units and can handle scaling in a
smarter way, so keep this configurable.
2024-02-12 02:00:09 +01:00
8f8e1ff214 Purge unused function scrub_css
Commit e9f1897cfd added this private
function but it never had any users resulting in warnings each startup
2024-02-12 02:00:09 +01:00
18ecae6183 Use fully qualified function capture for telementry event
Otherwise we get warnings on startup as local captures
and anonymous functions are supposedly less performant.
2024-02-12 01:59:18 +01:00
a6df71eebb Don't add summary metrics to prometheus
The exporter doesn’t support them thus we don't lose anything by this,
but it avoids a bunch of warnings each time the server starts up.
2024-02-12 01:59:18 +01:00
df21b61829
Return last_status_at as date, not datetime 2024-02-05 21:42:15 +01:00
d7d159c49f
Fix OpenAPI spec for preferred_frontend endpoint
The spec was copied from another endpoint, including the operation id,
leading to scrubbing the valid parameters from the request and simply
not working.
2024-02-03 14:27:45 +01:00
25 changed files with 354 additions and 33 deletions

View file

@ -10,11 +10,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Full compatibility with Erlang OTP26 - Full compatibility with Erlang OTP26
- handling of GET /api/v1/preferences - handling of GET /api/v1/preferences
- Akkoma API is now documented - Akkoma API is now documented
- ability to auto-approve follow requests from users you are already following
- The SimplePolicy MRF can now strip user backgrounds from selected remote hosts
## Changed ## Changed
- OTP builds are now built on erlang OTP26 - OTP builds are now built on erlang OTP26
- The base Phoenix framework is now updated to 1.7 - The base Phoenix framework is now updated to 1.7
- An `outbox` field has been added to actor profiles to comply with AP spec - An `outbox` field has been added to actor profiles to comply with AP spec
- User profile backgrounds do now federate with other Akkoma instances and Sharkey
## Fixed ## Fixed
- Documentation issue in which a non-existing nginx file was referenced - Documentation issue in which a non-existing nginx file was referenced

View file

@ -377,6 +377,7 @@
accept: [], accept: [],
avatar_removal: [], avatar_removal: [],
banner_removal: [], banner_removal: [],
background_removal: [],
reject_deletes: [], reject_deletes: [],
handle_threads: true handle_threads: true

View file

@ -124,6 +124,7 @@ To add configuration to your config file, you can copy it from the base config.
* `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)). * `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)).
* `Pleroma.Web.ActivityPub.MRF.NormalizeMarkup`: Pass inbound HTML through a scrubber to make sure it doesn't have anything unusual in it. On by default, cannot be turned off. * `Pleroma.Web.ActivityPub.MRF.NormalizeMarkup`: Pass inbound HTML through a scrubber to make sure it doesn't have anything unusual in it. On by default, cannot be turned off.
* `Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy`: Append a link to a post that quotes another post with the link to the quoted post, to ensure that software that does not understand quotes can have full context. On by default, cannot be turned off. * `Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy`: Append a link to a post that quotes another post with the link to the quoted post, to ensure that software that does not understand quotes can have full context. On by default, cannot be turned off.
* `Pleroma.Web.ActivityPub.MRF.RejectNewlyCreatedAccountNotesPolicy`: Drops posts of users which are newer than the configured time. For exmple it drops all post of users which where created one hour ago. Great to block spam accounts. (See [`:mrf_reject_newly_created_account_notes`](#:mrf_reject_newly_created_account_notes))
* `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo). * `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
* `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. * `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
* `transparency_obfuscate_domains`: Show domains with `*` in the middle, to censor them if needed. For example, `ridingho.me` will show as `rid*****.me` * `transparency_obfuscate_domains`: Show domains with `*` in the middle, to censor them if needed. For example, `ridingho.me` will show as `rid*****.me`
@ -144,6 +145,7 @@ To add configuration to your config file, you can copy it from the base config.
* `report_removal`: List of instances to reject reports from and the reason for doing so. * `report_removal`: List of instances to reject reports from and the reason for doing so.
* `avatar_removal`: List of instances to strip avatars from and the reason for doing so. * `avatar_removal`: List of instances to strip avatars from and the reason for doing so.
* `banner_removal`: List of instances to strip banners from and the reason for doing so. * `banner_removal`: List of instances to strip banners from and the reason for doing so.
* `background_removal`: List of instances to strip user backgrounds from and the reason for doing so.
* `reject_deletes`: List of instances to reject deletions from and the reason for doing so. * `reject_deletes`: List of instances to reject deletions from and the reason for doing so.
#### :mrf_subchain #### :mrf_subchain
@ -222,6 +224,18 @@ Notes:
- The hashtags in the configuration do not have a leading `#`. - The hashtags in the configuration do not have a leading `#`.
- This MRF Policy is always enabled, if you want to disable it you have to set empty lists - This MRF Policy is always enabled, if you want to disable it you have to set empty lists
#### :mrf_reject_newly_created_account_notes
This drops all posts of users which where created within the configured timeframe.
It only drops posts. Follows, reposts and so one are not effected.
* `age`: Time in seconds of which posts for newly created users are dropped.
An example:
```elixir
config :pleroma, :mrf_reject_newly_created_account_notes, age: 86400
```
### :activitypub ### :activitypub
* `unfollow_blocked`: Whether blocks result in people getting unfollowed * `unfollow_blocked`: Whether blocks result in people getting unfollowed
* `outgoing_blocks`: Whether to federate blocks to other instances * `outgoing_blocks`: Whether to federate blocks to other instances

View file

@ -35,6 +35,7 @@ Once `SimplePolicy` is enabled, you can configure various groups in the `:mrf_si
* `media_removal`: Servers in this group will have media stripped from incoming messages. * `media_removal`: Servers in this group will have media stripped from incoming messages.
* `avatar_removal`: Avatars from these servers will be stripped from incoming messages. * `avatar_removal`: Avatars from these servers will be stripped from incoming messages.
* `banner_removal`: Banner images from these servers will be stripped from incoming messages. * `banner_removal`: Banner images from these servers will be stripped from incoming messages.
* `background_removal`: User background images from these servers will be stripped from incoming messages.
* `report_removal`: Servers in this group will have their reports (flags) rejected. * `report_removal`: Servers in this group will have their reports (flags) rejected.
* `federated_timeline_removal`: Servers in this group will have their messages unlisted from the public timelines by flipping the `to` and `cc` fields. * `federated_timeline_removal`: Servers in this group will have their messages unlisted from the public timelines by flipping the `to` and `cc` fields.
* `reject_deletes`: Deletion requests will be rejected from these servers. * `reject_deletes`: Deletion requests will be rejected from these servers.

View file

@ -121,6 +121,12 @@ Has these additional fields under the `pleroma` object:
- `notification_settings`: object, can be absent. See `/api/v1/pleroma/notification_settings` for the parameters/keys returned. - `notification_settings`: object, can be absent. See `/api/v1/pleroma/notification_settings` for the parameters/keys returned.
- `favicon`: nullable URL string, Favicon image of the user's instance - `favicon`: nullable URL string, Favicon image of the user's instance
Has these additional fields under the `akkoma` object:
- `instance`: nullable object with metadata about the users instance
- `status_ttl_days`: nullable int, default time after which statuses are deleted
- `permit_followback`: boolean, whether follows from followed accounts are auto-approved
### Source ### Source
Has these additional fields under the `pleroma` object: Has these additional fields under the `pleroma` object:

View file

@ -15,8 +15,19 @@ def start_link(_) do
@impl true @impl true
def init(state) do def init(state) do
:telemetry.attach("oban-monitor-failure", [:oban, :job, :exception], &handle_event/4, nil) :telemetry.attach(
:telemetry.attach("oban-monitor-success", [:oban, :job, :stop], &handle_event/4, nil) "oban-monitor-failure",
[:oban, :job, :exception],
&Pleroma.JobQueueMonitor.handle_event/4,
nil
)
:telemetry.attach(
"oban-monitor-success",
[:oban, :job, :stop],
&Pleroma.JobQueueMonitor.handle_event/4,
nil
)
{:ok, state} {:ok, state}
end end

View file

@ -160,6 +160,7 @@ defmodule Pleroma.User do
field(:last_status_at, :naive_datetime) field(:last_status_at, :naive_datetime)
field(:language, :string) field(:language, :string)
field(:status_ttl_days, :integer, default: nil) field(:status_ttl_days, :integer, default: nil)
field(:permit_followback, :boolean, default: false)
field(:accepts_direct_messages_from, Ecto.Enum, field(:accepts_direct_messages_from, Ecto.Enum,
values: [:everybody, :people_i_follow, :nobody], values: [:everybody, :people_i_follow, :nobody],
@ -381,6 +382,10 @@ def banner_url(user, options \\ []) do
do_optional_url(user.banner, "#{Endpoint.url()}/images/banner.png", options) do_optional_url(user.banner, "#{Endpoint.url()}/images/banner.png", options)
end end
def background_url(user) do
do_optional_url(user.background, nil, no_default: true)
end
defp do_optional_url(field, default, options) do defp do_optional_url(field, default, options) do
case field do case field do
%{"url" => [%{"href" => href} | _]} when is_binary(href) -> %{"url" => [%{"href" => href} | _]} when is_binary(href) ->
@ -465,6 +470,7 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
:avatar, :avatar,
:ap_enabled, :ap_enabled,
:banner, :banner,
:background,
:is_locked, :is_locked,
:last_refreshed_at, :last_refreshed_at,
:uri, :uri,
@ -544,6 +550,7 @@ def update_changeset(struct, params \\ %{}) do
:actor_type, :actor_type,
:disclose_client, :disclose_client,
:status_ttl_days, :status_ttl_days,
:permit_followback,
:accepts_direct_messages_from :accepts_direct_messages_from
] ]
) )
@ -972,16 +979,21 @@ def needs_update?(%User{local: false} = user) do
def needs_update?(_), do: true def needs_update?(_), do: true
# "Locked" (self-locked) users demand explicit authorization of follow requests
@spec can_direct_follow_local(User.t(), User.t()) :: true | false
def can_direct_follow_local(%User{} = follower, %User{local: true} = followed) do
!followed.is_locked || (followed.permit_followback and is_friend_of(follower, followed))
end
@spec maybe_direct_follow(User.t(), User.t()) :: @spec maybe_direct_follow(User.t(), User.t()) ::
{:ok, User.t(), User.t()} | {:error, String.t()} {:ok, User.t(), User.t()} | {:error, String.t()}
# "Locked" (self-locked) users demand explicit authorization of follow requests
def maybe_direct_follow(%User{} = follower, %User{local: true, is_locked: true} = followed) do
follow(follower, followed, :follow_pending)
end
def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
follow(follower, followed) if can_direct_follow_local(follower, followed) do
follow(follower, followed)
else
follow(follower, followed, :follow_pending)
end
end end
def maybe_direct_follow(%User{} = follower, %User{} = followed) do def maybe_direct_follow(%User{} = follower, %User{} = followed) do
@ -1331,6 +1343,13 @@ def get_friends_ids(%User{} = user, page \\ nil) do
|> Repo.all() |> Repo.all()
end end
def is_friend_of(%User{} = potential_friend, %User{local: true} = user) do
user
|> get_friends_query()
|> where(id: ^potential_friend.id)
|> Repo.exists?()
end
def increase_note_count(%User{} = user) do def increase_note_count(%User{} = user) do
User User
|> where(id: ^user.id) |> where(id: ^user.id)

View file

@ -1603,6 +1603,7 @@ defp object_to_user_data(data, additional) do
uri: get_actor_url(data["url"]), uri: get_actor_url(data["url"]),
ap_enabled: true, ap_enabled: true,
banner: normalize_image(data["image"]), banner: normalize_image(data["image"]),
background: normalize_image(data["backgroundUrl"]),
fields: fields, fields: fields,
emoji: emojis, emoji: emojis,
is_locked: is_locked, is_locked: is_locked,

View file

@ -178,6 +178,23 @@ defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image
defp check_banner_removal(_actor_info, object), do: {:ok, object} defp check_banner_removal(_actor_info, object), do: {:ok, object}
defp check_background_removal(
%{host: actor_host} = _actor_info,
%{"backgroundUrl" => _bg} = object
) do
background_removal =
instance_list(:background_removal)
|> MRF.subdomains_regex()
if MRF.subdomain_match?(background_removal, actor_host) do
{:ok, Map.delete(object, "backgroundUrl")}
else
{:ok, object}
end
end
defp check_background_removal(_actor_info, object), do: {:ok, object}
defp extract_context_uri(%{"conversation" => "tag:" <> rest}) do defp extract_context_uri(%{"conversation" => "tag:" <> rest}) do
rest rest
|> String.split(",", parts: 2, trim: true) |> String.split(",", parts: 2, trim: true)
@ -283,7 +300,8 @@ def filter(%{"id" => actor, "type" => obj_type} = object)
with {:ok, _} <- check_accept(actor_info), with {:ok, _} <- check_accept(actor_info),
{:ok, _} <- check_reject(actor_info), {:ok, _} <- check_reject(actor_info),
{:ok, object} <- check_avatar_removal(actor_info, object), {:ok, object} <- check_avatar_removal(actor_info, object),
{:ok, object} <- check_banner_removal(actor_info, object) do {:ok, object} <- check_banner_removal(actor_info, object),
{:ok, object} <- check_background_removal(actor_info, object) do
{:ok, object} {:ok, object}
else else
{:reject, nil} -> {:reject, "[SimplePolicy]"} {:reject, nil} -> {:reject, "[SimplePolicy]"}
@ -447,6 +465,11 @@ def config_description do
key: :banner_removal, key: :banner_removal,
description: "List of instances to strip banners from and the reason for doing so" description: "List of instances to strip banners from and the reason for doing so"
}, },
%{
key: :background_removal,
description:
"List of instances to strip user backgrounds from and the reason for doing so"
},
%{ %{
key: :reject_deletes, key: :reject_deletes,
description: "List of instances to reject deletions from and the reason for doing so" description: "List of instances to reject deletions from and the reason for doing so"

View file

@ -109,7 +109,7 @@ def handle(
%User{} = followed <- User.get_cached_by_ap_id(followed_user), %User{} = followed <- User.get_cached_by_ap_id(followed_user),
{_, {:ok, _, _}, _, _} <- {_, {:ok, _, _}, _, _} <-
{:following, User.follow(follower, followed, :follow_pending), follower, followed} do {:following, User.follow(follower, followed, :follow_pending), follower, followed} do
if followed.local && !followed.is_locked do if followed.local && User.can_direct_follow_local(follower, followed) do
{:ok, accept_data, _} = Builder.accept(followed, object) {:ok, accept_data, _} = Builder.accept(followed, object)
{:ok, _activity, _} = Pipeline.common_pipeline(accept_data, local: true) {:ok, _activity, _} = Pipeline.common_pipeline(accept_data, local: true)
end end

View file

@ -112,6 +112,8 @@ def render("user.json", %{user: user}) do
} }
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
# Yes, the key is named ...Url eventhough it is a whole 'Image' object
|> Map.merge(maybe_insert_image("backgroundUrl", User.background_url(user)))
|> Map.merge(Utils.make_json_ld_header()) |> Map.merge(Utils.make_json_ld_header())
end end
@ -287,7 +289,12 @@ def collection(collection, iri, page, show_items \\ true, total \\ nil) do
end end
defp maybe_make_image(func, key, user) do defp maybe_make_image(func, key, user) do
if image = func.(user, no_default: true) do image = func.(user, no_default: true)
maybe_insert_image(key, image)
end
defp maybe_insert_image(key, image) do
if image do
%{ %{
key => %{ key => %{
"type" => "Image", "type" => "Image",

View file

@ -723,6 +723,12 @@ defp update_credentials_request do
description: description:
"Number of days after which statuses will be deleted. Set to -1 to disable." "Number of days after which statuses will be deleted. Set to -1 to disable."
}, },
permit_followback: %Schema{
allOf: [BooleanLike],
nullable: true,
description:
"Whether follow requests from accounts the user is already following are auto-approved (when locked)."
},
accepts_direct_messages_from: %Schema{ accepts_direct_messages_from: %Schema{
type: :string, type: :string,
enum: [ enum: [
@ -754,6 +760,7 @@ defp update_credentials_request do
discoverable: false, discoverable: false,
actor_type: "Person", actor_type: "Person",
status_ttl_days: 30, status_ttl_days: 30,
permit_followback: true,
accepts_direct_messages_from: "everybody" accepts_direct_messages_from: "everybody"
} }
} }

View file

@ -111,9 +111,9 @@ def available_frontends_operation() do
def update_preferred_frontend_operation() do def update_preferred_frontend_operation() do
%Operation{ %Operation{
tags: ["Frontends"], tags: ["Frontends"],
summary: "Frontend Settings Profiles", summary: "Update preferred frontend setting",
description: "List frontend setting profiles", description: "Store preferred frontend in cookies",
operationId: "AkkomaAPI.FrontendSettingsController.available_frontends", operationId: "AkkomaAPI.FrontendSettingsController.update_preferred_frontend",
requestBody: requestBody:
request_body( request_body(
"Frontend", "Frontend",
@ -132,9 +132,11 @@ def update_preferred_frontend_operation() do
responses: %{ responses: %{
200 => 200 =>
Operation.response("Frontends", "application/json", %Schema{ Operation.response("Frontends", "application/json", %Schema{
type: :array, type: :object,
items: %Schema{ properties: %{
type: :string frontend_name: %Schema{
type: :string
}
} }
}) })
} }

View file

@ -112,7 +112,18 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
akkoma: %Schema{ akkoma: %Schema{
type: :object, type: :object,
properties: %{ properties: %{
note_ttl_days: %Schema{type: :integer} instance: %Schema{
type: :object,
nullable: true,
properties: %{
name: %Schema{type: :string},
favicon: %Schema{type: :string, format: :uri, nullable: true},
# XXX: proper nodeinfo schema
nodeinfo: %Schema{type: :object, nullable: true}
}
},
status_ttl_days: %Schema{type: :integer, nullable: true},
permit_followback: %Schema{type: :boolean}
} }
}, },
source: %Schema{ source: %Schema{
@ -205,6 +216,18 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
"pleroma-fe" => %{} "pleroma-fe" => %{}
} }
}, },
"akkoma" => %{
"instance" => %{
"name" => "ihatebeinga.live",
"favicon" => "https://ihatebeinga.live/favicon.png",
"nodeinfo" =>
%{
# XXX: nodeinfo schema
}
},
"status_ttl_days" => nil,
"permit_followback" => true
},
"source" => %{ "source" => %{
"fields" => [], "fields" => [],
"note" => "foobar", "note" => "foobar",

View file

@ -222,6 +222,7 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p
|> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language])) |> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language]))
|> Maps.put_if_present(:status_ttl_days, params[:status_ttl_days], status_ttl_days_value) |> Maps.put_if_present(:status_ttl_days, params[:status_ttl_days], status_ttl_days_value)
|> Maps.put_if_present(:accepts_direct_messages_from, params[:accepts_direct_messages_from]) |> Maps.put_if_present(:accepts_direct_messages_from, params[:accepts_direct_messages_from])
|> Maps.put_if_present(:permit_followback, params[:permit_followback])
# What happens here: # What happens here:
# #

View file

@ -261,6 +261,9 @@ defp do_render("show.json", %{user: user} = opts) do
|> MediaProxy.url() |> MediaProxy.url()
end end
last_status_at =
if is_nil(user.last_status_at), do: nil, else: NaiveDateTime.to_date(user.last_status_at)
%{ %{
id: to_string(user.id), id: to_string(user.id),
username: username_from_nickname(user.nickname), username: username_from_nickname(user.nickname),
@ -289,10 +292,11 @@ defp do_render("show.json", %{user: user} = opts) do
actor_type: user.actor_type actor_type: user.actor_type
} }
}, },
last_status_at: user.last_status_at, last_status_at: last_status_at,
akkoma: %{ akkoma: %{
instance: render("instance.json", %{instance: instance}), instance: render("instance.json", %{instance: instance}),
status_ttl_days: user.status_ttl_days status_ttl_days: user.status_ttl_days,
permit_followback: user.permit_followback
}, },
# Pleroma extensions # Pleroma extensions
# Note: it's insecure to output :email but fully-qualified nickname may serve as safe stub # Note: it's insecure to output :email but fully-qualified nickname may serve as safe stub

View file

@ -101,7 +101,8 @@ defp distribution_metrics do
] ]
end end
defp summary_metrics do # Summary metrics are currently not (yet) supported by the prometheus exporter
defp summary_metrics(byte_unit) do
[ [
# Phoenix Metrics # Phoenix Metrics
summary("phoenix.endpoint.stop.duration", summary("phoenix.endpoint.stop.duration",
@ -118,10 +119,98 @@ defp summary_metrics do
summary("pleroma.repo.query.idle_time", unit: {:native, :millisecond}), summary("pleroma.repo.query.idle_time", unit: {:native, :millisecond}),
# VM Metrics # VM Metrics
summary("vm.memory.total", unit: {:byte, :kilobyte}), summary("vm.memory.total", unit: {:byte, byte_unit}),
summary("vm.total_run_queue_lengths.total"), summary("vm.total_run_queue_lengths.total"),
summary("vm.total_run_queue_lengths.cpu"), summary("vm.total_run_queue_lengths.cpu"),
summary("vm.total_run_queue_lengths.io"), summary("vm.total_run_queue_lengths.io")
]
end
defp sum_counter_pair(basename, opts) do
[
sum(basename <> ".psum", opts),
counter(basename <> ".pcount", opts)
]
end
# Prometheus exporter doesn't support summaries, so provide fallbacks
defp summary_fallback_metrics(byte_unit \\ :byte) do
# Summary metrics are not supported by the Prometheus exporter
# https://github.com/beam-telemetry/telemetry_metrics_prometheus_core/issues/11
# and sum metrics currently only work with integers
# https://github.com/beam-telemetry/telemetry_metrics_prometheus_core/issues/35
#
# For VM metrics this is kindof ok as they appear to always be integers
# and we can use sum + counter to get the average between polls from their change
# But for repo query times we need to use a full distribution
simple_buckets = [0, 1, 2, 4, 8, 16]
simple_buckets_quick = for t <- simple_buckets, do: t / 100.0
# Already included in distribution metrics anyway:
# phoenix.router_dispatch.stop.duration
# pleroma.repo.query.total_time
# pleroma.repo.query.queue_time
dist_metrics =
[
distribution("phoenix.endpoint.stop.duration.fdist",
event_name: [:phoenix, :endpoint, :stop],
measurement: :duration,
unit: {:native, :millisecond},
reporter_options: [
buckets: simple_buckets
]
),
distribution("pleroma.repo.query.decode_time.fdist",
event_name: [:pleroma, :repo, :query],
measurement: :decode_time,
unit: {:native, :millisecond},
reporter_options: [
buckets: simple_buckets_quick
]
),
distribution("pleroma.repo.query.query_time.fdist",
event_name: [:pleroma, :repo, :query],
measurement: :query_time,
unit: {:native, :millisecond},
reporter_options: [
buckets: simple_buckets
]
),
distribution("pleroma.repo.query.idle_time.fdist",
event_name: [:pleroma, :repo, :query],
measurement: :idle_time,
unit: {:native, :millisecond},
reporter_options: [
buckets: simple_buckets
]
)
]
vm_metrics =
sum_counter_pair("vm.memory.total",
event_name: [:vm, :memory],
measurement: :total,
unit: {:byte, byte_unit}
) ++
sum_counter_pair("vm.total_run_queue_lengths.total",
event_name: [:vm, :total_run_queue_lengths],
measurement: :total
) ++
sum_counter_pair("vm.total_run_queue_lengths.cpu",
event_name: [:vm, :total_run_queue_lengths],
measurement: :cpu
) ++
sum_counter_pair("vm.total_run_queue_lengths.io.fsum",
event_name: [:vm, :total_run_queue_lengths],
measurement: :io
)
dist_metrics ++ vm_metrics
end
defp common_metrics do
[
last_value("pleroma.local_users.total"), last_value("pleroma.local_users.total"),
last_value("pleroma.domains.total"), last_value("pleroma.domains.total"),
last_value("pleroma.local_statuses.total"), last_value("pleroma.local_statuses.total"),
@ -129,8 +218,10 @@ defp summary_metrics do
] ]
end end
def prometheus_metrics, do: summary_metrics() ++ distribution_metrics() def prometheus_metrics,
def live_dashboard_metrics, do: summary_metrics() do: common_metrics() ++ distribution_metrics() ++ summary_fallback_metrics()
def live_dashboard_metrics, do: common_metrics() ++ summary_metrics(:megabyte)
defp periodic_measurements do defp periodic_measurements do
[ [

View file

@ -0,0 +1,9 @@
defmodule Pleroma.Repo.Migrations.AddPermitFollowback do
use Ecto.Migration
def change do
alter table(:users) do
add(:permit_followback, :boolean, null: false, default: false)
end
end
end

View file

@ -128,6 +128,4 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes(:small, []) Meta.allow_tag_with_these_attributes(:small, [])
Meta.strip_everything_not_covered() Meta.strip_everything_not_covered()
defp scrub_css(value), do: value
end end

View file

@ -19,6 +19,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
accept: [], accept: [],
avatar_removal: [], avatar_removal: [],
banner_removal: [], banner_removal: [],
background_removal: [],
reject_deletes: [] reject_deletes: []
) )
@ -618,6 +619,42 @@ test "match with wildcard domain" do
end end
end end
describe "when :background_removal" do
test "is empty" do
clear_config([:mrf_simple, :background_removal], [])
remote_user = build_remote_user()
assert SimplePolicy.filter(remote_user) == {:ok, remote_user}
end
test "is not empty but it doesn't have a matching host" do
clear_config([:mrf_simple, :background_removal], [{"non.matching.remote", ""}])
remote_user = build_remote_user()
assert SimplePolicy.filter(remote_user) == {:ok, remote_user}
end
test "has a matching host" do
clear_config([:mrf_simple, :background_removal], [{"remote.instance", ""}])
remote_user = build_remote_user()
{:ok, filtered} = SimplePolicy.filter(remote_user)
refute filtered["backgroundUrl"]
end
test "match with wildcard domain" do
clear_config([:mrf_simple, :background_removal], [{"*.remote.instance", ""}])
remote_user = build_remote_user()
{:ok, filtered} = SimplePolicy.filter(remote_user)
refute filtered["backgroundUrl"]
end
end
describe "when :reject_deletes is empty" do describe "when :reject_deletes is empty" do
setup do: clear_config([:mrf_simple, :reject_deletes], []) setup do: clear_config([:mrf_simple, :reject_deletes], [])
@ -701,6 +738,10 @@ defp build_remote_user do
"url" => "http://example.com/image.jpg", "url" => "http://example.com/image.jpg",
"type" => "Image" "type" => "Image"
}, },
"backgroundUrl" => %{
"url" => "http://example.com/background.jpg",
"type" => "Image"
},
"type" => "Person" "type" => "Person"
} }
end end

View file

@ -155,7 +155,13 @@ test "it blocks but does not unfollow if the relevant setting is set", %{
user = insert(:user, local: false) user = insert(:user, local: false)
{:ok, update_data, []} = {:ok, update_data, []} =
Builder.update(user, %{"id" => user.ap_id, "type" => "Person", "name" => "new name!"}) Builder.update(user, %{
"id" => user.ap_id,
"type" => "Person",
"name" => "new name!",
"icon" => %{"type" => "Image", "url" => "https://example.org/icon.png"},
"backgroundUrl" => %{"type" => "Image", "url" => "https://example.org/bg.jxl"}
})
{:ok, update, _meta} = ActivityPub.persist(update_data, local: true) {:ok, update, _meta} = ActivityPub.persist(update_data, local: true)
@ -165,7 +171,10 @@ test "it blocks but does not unfollow if the relevant setting is set", %{
test "it updates the user", %{user: user, update: update} do test "it updates the user", %{user: user, update: update} do
{:ok, _, _} = SideEffects.handle(update) {:ok, _, _} = SideEffects.handle(update)
user = User.get_by_id(user.id) user = User.get_by_id(user.id)
assert user.name == "new name!" assert user.name == "new name!"
assert [%{"href" => "https://example.org/icon.png"}] = user.avatar["url"]
assert [%{"href" => "https://example.org/bg.jxl"}] = user.background["url"]
end end
test "it uses a given changeset to update", %{user: user, update: update} do test "it uses a given changeset to update", %{user: user, update: update} do

View file

@ -58,16 +58,19 @@ test "Does not add an avatar image if the user hasn't set one" do
result = UserView.render("user.json", %{user: user}) result = UserView.render("user.json", %{user: user})
refute result["icon"] refute result["icon"]
refute result["image"] refute result["image"]
refute result["backgroundUrl"]
user = user =
insert(:user, insert(:user,
avatar: %{"url" => [%{"href" => "https://someurl"}]}, avatar: %{"url" => [%{"href" => "https://someurl"}]},
banner: %{"url" => [%{"href" => "https://somebanner"}]} banner: %{"url" => [%{"href" => "https://somebanner"}]},
background: %{"url" => [%{"href" => "https://somebackground"}]}
) )
result = UserView.render("user.json", %{user: user}) result = UserView.render("user.json", %{user: user})
assert result["icon"]["url"] == "https://someurl" assert result["icon"]["url"] == "https://someurl"
assert result["image"]["url"] == "https://somebanner" assert result["image"]["url"] == "https://somebanner"
assert result["backgroundUrl"]["url"] == "https://somebackground"
end end
test "renders an invisible user with the invisible property set to true" do test "renders an invisible user with the invisible property set to true" do

View file

@ -119,4 +119,18 @@ test "deletes a config" do
) == nil ) == nil
end end
end end
describe "PUT /api/v1/akkoma/preferred_frontend" do
test "sets a cookie with selected frontend" do
%{conn: conn} = oauth_access(["read"])
response =
conn
|> put_req_header("content-type", "application/json")
|> put("/api/v1/akkoma/preferred_frontend", %{"frontend_name" => "pleroma-fe/stable"})
json_response_and_validate_schema(response, 200)
assert %{"preferred_frontend" => %{value: "pleroma-fe/stable"}} = response.resp_cookies
end
end
end end

View file

@ -1061,6 +1061,36 @@ test "directly follows a non-locked local user" do
assert User.following?(follower, followed) assert User.following?(follower, followed)
end end
test "directly follows back a locked, but followback-allowing local user" do
uopen = insert(:user, is_locked: false)
uselective = insert(:user, is_locked: true, permit_followback: true)
assert {:ok, uselective, uopen, %{data: %{"state" => "accept"}}} =
CommonAPI.follow(uselective, uopen)
assert User.get_follow_state(uselective, uopen) == :follow_accept
assert {:ok, uopen, uselective, %{data: %{"state" => "accept"}}} =
CommonAPI.follow(uopen, uselective)
assert User.get_follow_state(uopen, uselective) == :follow_accept
end
test "creates a pending request for locked, non-followback local user" do
uopen = insert(:user, is_locked: false)
ulocked = insert(:user, is_locked: true, permit_followback: false)
assert {:ok, ulocked, uopen, %{data: %{"state" => "accept"}}} =
CommonAPI.follow(ulocked, uopen)
assert User.get_follow_state(ulocked, uopen) == :follow_accept
assert {:ok, uopen, ulocked, %{data: %{"state" => "pending"}}} =
CommonAPI.follow(uopen, ulocked)
assert User.get_follow_state(uopen, ulocked) == :follow_pending
end
end end
describe "unfollow/2" do describe "unfollow/2" do

View file

@ -40,7 +40,8 @@ test "Represent a user account" do
emoji: %{"karjalanpiirakka" => "/file.png"}, emoji: %{"karjalanpiirakka" => "/file.png"},
raw_bio: "valid html. a\nb\nc\nd\nf '&<>\"", raw_bio: "valid html. a\nb\nc\nd\nf '&<>\"",
also_known_as: ["https://shitposter.zone/users/shp"], also_known_as: ["https://shitposter.zone/users/shp"],
status_ttl_days: 5 status_ttl_days: 5,
last_status_at: ~N[2023-12-31T15:06:17]
}) })
insert(:instance, %{host: "example.com", nodeinfo: %{version: "2.1"}}) insert(:instance, %{host: "example.com", nodeinfo: %{version: "2.1"}})
@ -65,7 +66,8 @@ test "Represent a user account" do
}, },
favicon: nil favicon: nil
}, },
status_ttl_days: 5 status_ttl_days: 5,
permit_followback: false
}, },
avatar: "http://localhost:4001/images/avi.png", avatar: "http://localhost:4001/images/avi.png",
avatar_static: "http://localhost:4001/images/avi.png", avatar_static: "http://localhost:4001/images/avi.png",
@ -91,7 +93,7 @@ test "Represent a user account" do
fields: [] fields: []
}, },
fqn: "shp@shitposter.club", fqn: "shp@shitposter.club",
last_status_at: nil, last_status_at: ~D[2023-12-31],
pleroma: %{ pleroma: %{
ap_id: user.ap_id, ap_id: user.ap_id,
also_known_as: ["https://shitposter.zone/users/shp"], also_known_as: ["https://shitposter.zone/users/shp"],
@ -248,7 +250,8 @@ test "Represent a Service(bot) account" do
favicon: "http://localhost:4001/favicon.png", favicon: "http://localhost:4001/favicon.png",
nodeinfo: %{version: "2.0"} nodeinfo: %{version: "2.0"}
}, },
status_ttl_days: nil status_ttl_days: nil,
permit_followback: false
}, },
pleroma: %{ pleroma: %{
ap_id: user.ap_id, ap_id: user.ap_id,