forked from AkkomaGang/akkoma
Compare commits
19 commits
874ee73a87
...
12e7d0a25c
Author | SHA1 | Date | |
---|---|---|---|
12e7d0a25c | |||
755c75d8a4 | |||
289f93f5a2 | |||
371b258c99 | |||
3b0714c4fd | |||
34c213f02f | |||
e99e2407f3 | |||
7622aa27ca | |||
0ed815b8a1 | |||
c5dcd07e08 | |||
376f6b15ca | |||
13e62b4e51 | |||
29f564f700 | |||
16197ff57a | |||
8f8e1ff214 | |||
18ecae6183 | |||
a6df71eebb | |||
df21b61829 | |||
d7d159c49f |
25 changed files with 354 additions and 33 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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 user’s 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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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:
|
||||||
#
|
#
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
[
|
[
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue