diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aaadca42..9e6e0fdf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Ability to log slow Ecto queries by configuring `:pleroma, :telemetry, :slow_queries_logging` - Added Phoenix LiveDashboard at `/phoenix/live_dashboard` - Added `/manifest.json` for progressive web apps. +- MastoAPI: Support for `birthday` and `show_birthday` field in `/api/v1/accounts/update_credentials`. +- Configuration: Add `birthday_required` and `birthday_min_age` settings to provide a way to require users to enter their birth date. +- PleromaAPI: Add `GET /api/v1/pleroma/birthdays` API endpoint ### Fixed - Subscription(Bell) Notifications: Don't create from Pipeline Ingested replies diff --git a/config/config.exs b/config/config.exs index 1385ce5de..5e82f203c 100644 --- a/config/config.exs +++ b/config/config.exs @@ -149,8 +149,6 @@ config :pleroma, Pleroma.Web.Endpoint, ] # Configures Elixir's Logger -config :logger, truncate: 65536 - config :logger, :console, level: :debug, format: "\n$time $metadata[$level] $message\n", @@ -259,7 +257,9 @@ config :pleroma, :instance, password_reset_token_validity: 60 * 60 * 24, profile_directory: true, privileged_staff: false, - max_endorsed_users: 20 + max_endorsed_users: 20, + birthday_required: false, + birthday_min_age: 0 config :pleroma, :welcome, direct_message: [ @@ -857,13 +857,6 @@ config :pleroma, ConcurrentLimiter, [ {Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]} ] -config :pleroma, :telemetry, - slow_queries_logging: [ - enabled: false, - min_duration: 500_000, - exclude_sources: [nil, "oban_jobs"] - ] - # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/config/description.exs b/config/description.exs index 644c60a63..3f66877e4 100644 --- a/config/description.exs +++ b/config/description.exs @@ -957,6 +957,17 @@ config :pleroma, :config_description, [ type: :boolean, description: "Let moderators access sensitive data (e.g. updating user credentials, get password reset token, delete users, index and read private statuses and chats)" + }, + %{ + key: :birthday_required, + type: :boolean, + description: "Require users to enter their birthday." + }, + %{ + key: :birthday_min_age, + type: :integer, + description: + "Minimum required age for users to create account. Only used if birthday is required." } ] }, diff --git a/lib/pleroma/telemetry/logger.ex b/lib/pleroma/telemetry/logger.ex index 35e245237..10165c1b2 100644 --- a/lib/pleroma/telemetry/logger.ex +++ b/lib/pleroma/telemetry/logger.ex @@ -12,16 +12,10 @@ defmodule Pleroma.Telemetry.Logger do [:pleroma, :connection_pool, :reclaim, :stop], [:pleroma, :connection_pool, :provision_failure], [:pleroma, :connection_pool, :client, :dead], - [:pleroma, :connection_pool, :client, :add], - [:pleroma, :repo, :query] + [:pleroma, :connection_pool, :client, :add] ] def attach do - :telemetry.attach_many( - "pleroma-logger", - @events, - &Pleroma.Telemetry.Logger.handle_event/4, - [] - ) + :telemetry.attach_many("pleroma-logger", @events, &handle_event/4, []) end # Passing anonymous functions instead of strings to logger is intentional, @@ -93,64 +87,4 @@ defmodule Pleroma.Telemetry.Logger do end def handle_event([:pleroma, :connection_pool, :client, :add], _, _, _), do: :ok - - def handle_event( - [:pleroma, :repo, :query] = _name, - %{query_time: query_time} = measurements, - %{source: source} = metadata, - config - ) do - logging_config = Pleroma.Config.get([:telemetry, :slow_queries_logging], []) - - if logging_config[:enabled] && - logging_config[:min_duration] && - query_time > logging_config[:min_duration] and - (is_nil(logging_config[:exclude_sources]) or - source not in logging_config[:exclude_sources]) do - log_slow_query(measurements, metadata, config) - else - :ok - end - end - - defp log_slow_query( - %{query_time: query_time} = _measurements, - %{source: _source, query: query, params: query_params, repo: repo} = _metadata, - _config - ) do - sql_explain = - with {:ok, %{rows: explain_result_rows}} <- - repo.query("EXPLAIN " <> query, query_params, log: false) do - Enum.map_join(explain_result_rows, "\n", & &1) - end - - {:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace) - - pleroma_stacktrace = - Enum.filter(stacktrace, fn - {__MODULE__, _, _, _} -> - false - - {mod, _, _, _} -> - mod - |> to_string() - |> String.starts_with?("Elixir.Pleroma.") - end) - - Logger.warn(fn -> - """ - Slow query! - - Total time: #{round(query_time / 1_000)} ms - - #{query} - - #{inspect(query_params, limit: :infinity)} - - #{sql_explain} - - #{Exception.format_stacktrace(pleroma_stacktrace)} - """ - end) - end end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index ef5a02249..36177bda3 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -154,6 +154,8 @@ defmodule Pleroma.User do field(:pinned_objects, :map, default: %{}) field(:is_suggested, :boolean, default: false) field(:last_status_at, :naive_datetime) + field(:birthday, :date) + field(:show_birthday, :boolean, default: false) embeds_one( :notification_settings, @@ -470,7 +472,9 @@ defmodule Pleroma.User do :actor_type, :also_known_as, :accepts_chat_messages, - :pinned_objects + :pinned_objects, + :birthday, + :show_birthday ] ) |> cast(params, [:name], empty_values: []) @@ -531,9 +535,12 @@ defmodule Pleroma.User do :is_discoverable, :actor_type, :accepts_chat_messages, - :disclose_client + :disclose_client, + :birthday, + :show_birthday ] ) + |> validate_min_age() |> unique_constraint(:nickname) |> validate_format(:nickname, local_nickname_regex()) |> validate_length(:bio, max: bio_limit) @@ -738,7 +745,8 @@ defmodule Pleroma.User do :password_confirmation, :emoji, :accepts_chat_messages, - :registration_reason + :registration_reason, + :birthday ]) |> validate_required([:name, :nickname, :password, :password_confirmation]) |> validate_confirmation(:password) @@ -760,6 +768,8 @@ defmodule Pleroma.User do |> validate_length(:name, min: 1, max: name_limit) |> validate_length(:registration_reason, max: reason_limit) |> maybe_validate_required_email(opts[:external]) + |> maybe_validate_required_birthday + |> validate_min_age() |> put_password_hash |> put_ap_id() |> unique_constraint(:ap_id) @@ -776,6 +786,26 @@ defmodule Pleroma.User do end end + defp maybe_validate_required_birthday(changeset) do + if Config.get([:instance, :birthday_required]) do + validate_required(changeset, [:birthday]) + else + changeset + end + end + + defp validate_min_age(changeset) do + changeset + |> validate_change(:birthday, fn :birthday, birthday -> + valid? = + Date.utc_today() + |> Date.diff(birthday) >= + Config.get([:instance, :birthday_min_age]) + + if valid?, do: [], else: [birthday: "Invalid age"] + end) + end + defp put_ap_id(changeset) do ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)}) put_change(changeset, :ap_id, ap_id) @@ -2560,4 +2590,13 @@ defmodule Pleroma.User do _ -> {:error, user} end end + + def get_friends_birthdays_query(%User{} = user, day, month) do + User.Query.build(%{ + friends: user, + deactivated: false, + birthday_day: day, + birthday_month: month + }) + end end diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index bf78cb32d..bd11d287c 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -59,7 +59,9 @@ defmodule Pleroma.User.Query do order_by: term(), select: term(), limit: pos_integer(), - actor_types: [String.t()] + actor_types: [String.t()], + birthday_day: pos_integer(), + birthday_month: pos_integer() } | map() @@ -230,6 +232,20 @@ defmodule Pleroma.User.Query do |> where([u], not like(u.nickname, "internal.%")) end + defp compose_query({:birthday_day, day}, query) do + query + |> where([u], u.show_birthday == true) + |> where([u], not is_nil(u.birthday)) + |> where([u], fragment("date_part('day', ?)", u.birthday) == ^day) + end + + defp compose_query({:birthday_month, month}, query) do + query + |> where([u], u.show_birthday == true) + |> where([u], not is_nil(u.birthday)) + |> where([u], fragment("date_part('month', ?)", u.birthday) == ^month) + end + defp compose_query(_unsupported_param, query), do: query defp location_query(query, local) do diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 9ca44c532..e6475a2b7 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1501,6 +1501,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do nil end + birthday = + if is_binary(data["vcard:bday"]) do + case Date.from_iso8601(data["vcard:bday"]) do + {:ok, date} -> date + {:error, _} -> nil + end + else + nil + end + + show_birthday = !!birthday + user_data = %{ ap_id: data["id"], uri: get_actor_url(data["url"]), @@ -1523,7 +1535,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do inbox: data["inbox"], shared_inbox: shared_inbox, accepts_chat_messages: accepts_chat_messages, - pinned_objects: pinned_objects + pinned_objects: pinned_objects, + birthday: birthday, + show_birthday: show_birthday } # nickname can be nil because of virtual actors diff --git a/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex b/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex index 11871375e..b10b27f06 100644 --- a/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy do require Pleroma.Constants defp check_by_actor_type(user), do: user.actor_type in ["Application", "Service"] - defp check_by_nickname(user), do: Regex.match?(~r/bot@|ebooks@/i, user.nickname) + defp check_by_nickname(user), do: Regex.match?(~r/.bot@|ebooks@/i, user.nickname) defp check_if_bot(user), do: check_by_actor_type(user) or check_by_nickname(user) diff --git a/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex b/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex index 522ae2f60..255910b2f 100644 --- a/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex +++ b/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex @@ -3,7 +3,10 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do + require Pleroma.Constants + alias Pleroma.Formatter + alias Pleroma.Object alias Pleroma.User @behaviour Pleroma.Web.ActivityPub.MRF.Policy @@ -34,40 +37,89 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do do_extract(tree, []) end + defp get_replied_to_user(%{"inReplyTo" => in_reply_to}) do + case Object.normalize(in_reply_to, fetch: false) do + %Object{data: %{"actor" => actor}} -> User.get_cached_by_ap_id(actor) + _ -> nil + end + end + + defp get_replied_to_user(_object), do: nil + + # Ensure the replied-to user is sorted to the left + defp sort_replied_user([%User{id: user_id} | _] = users, %User{id: user_id}), do: users + + defp sort_replied_user(users, %User{id: user_id} = user) do + if Enum.find(users, fn u -> u.id == user_id end) do + users = Enum.reject(users, fn u -> u.id == user_id end) + [user | users] + else + users + end + end + + defp sort_replied_user(users, _), do: users + + # Drop constants and the actor's own AP ID + defp clean_recipients(recipients, object) do + Enum.reject(recipients, fn ap_id -> + ap_id in [ + object["object"]["actor"], + Pleroma.Constants.as_public(), + Pleroma.Web.ActivityPub.Utils.as_local_public() + ] + end) + end + @impl true - def filter(%{"type" => "Create", "object" => %{"type" => "Note", "tag" => tag}} = object) do + def filter( + %{ + "type" => "Create", + "object" => %{"type" => "Note", "to" => to, "inReplyTo" => in_reply_to} + } = object + ) + when is_list(to) and is_binary(in_reply_to) do # image-only posts from pleroma apparently reach this MRF without the content field content = object["object"]["content"] || "" + # Get the replied-to user for sorting + replied_to_user = get_replied_to_user(object["object"]) + mention_users = - tag - |> Enum.filter(fn tag -> tag["type"] == "Mention" end) - |> Enum.map(& &1["href"]) + to + |> clean_recipients(object) + |> Enum.map(&User.get_cached_by_ap_id/1) |> Enum.reject(&is_nil/1) - |> Enum.map(fn ap_id_or_uri -> - case User.get_or_fetch_by_ap_id(ap_id_or_uri) do - {:ok, user} -> {ap_id_or_uri, user} - _ -> {ap_id_or_uri, User.get_by_uri(ap_id_or_uri)} - end - end) - |> Enum.reject(fn {_, user} -> user == nil end) - |> Enum.into(%{}) + |> sort_replied_user(replied_to_user) explicitly_mentioned_uris = extract_mention_uris_from_content(content) added_mentions = - Enum.reduce(mention_users, "", fn {uri, user}, acc -> + Enum.reduce(mention_users, "", fn %User{ap_id: uri} = user, acc -> unless uri in explicitly_mentioned_uris do - acc <> Formatter.mention_from_user(user) + acc <> Formatter.mention_from_user(user, %{mentions_format: :compact}) <> " " else acc end end) - content = + recipients_inline = if added_mentions != "", - do: added_mentions <> " " <> content, - else: content + do: "#{added_mentions}", + else: "" + + content = + cond do + # For Markdown posts, insert the mentions inside the first
tag + recipients_inline != "" && String.starts_with?(content, "
") -> + "
" <> recipients_inline <> String.trim_leading(content, "
")
+
+ recipients_inline != "" ->
+ recipients_inline <> content
+
+ true ->
+ content
+ end
{:ok, put_in(object["object"]["content"], content)}
end
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index 344da19d3..d20d4591a 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -92,6 +92,11 @@ defmodule Pleroma.Web.ActivityPub.UserView do
%{}
end
+ birthday =
+ if user.show_birthday && user.birthday,
+ do: Date.to_iso8601(user.birthday),
+ else: nil
+
%{
"id" => user.ap_id,
"type" => user.actor_type,
@@ -116,7 +121,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
# Note: key name is indeed "discoverable" (not an error)
"discoverable" => user.is_discoverable,
"capabilities" => capabilities,
- "alsoKnownAs" => user.also_known_as
+ "alsoKnownAs" => user.also_known_as,
+ "vcard:bday" => birthday
}
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex
index 768d3c720..03efa3c38 100644
--- a/lib/pleroma/web/api_spec/operations/account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -543,6 +543,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
type: :string,
nullable: true,
description: "Invite token required when the registrations aren't public"
+ },
+ birthday: %Schema{
+ type: :string,
+ nullable: true,
+ description: "User's birthday",
+ format: :date
}
},
example: %{
@@ -720,7 +726,18 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
description:
"Discovery (listing, indexing) of this account by external services (search bots etc.) is allowed."
},
- actor_type: ActorType
+ actor_type: ActorType,
+ birthday: %Schema{
+ type: :string,
+ nullable: true,
+ description: "User's birthday",
+ format: :date
+ },
+ show_birthday: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "User's birthday will be visible"
+ }
},
example: %{
bot: false,
@@ -740,7 +757,9 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
allow_following_move: false,
also_known_as: ["https://foo.bar/users/foo"],
discoverable: false,
- actor_type: "Person"
+ actor_type: "Person",
+ show_birthday: false,
+ birthday: "2001-02-12"
}
}
end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
index ed0db173e..23201a4ba 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
@@ -4,6 +4,7 @@
defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.AccountOperation
alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
alias Pleroma.Web.ApiSpec.Schemas.ApiError
@@ -112,6 +113,34 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
}
end
+ def birthdays_operation do
+ %Operation{
+ tags: ["Retrieve account information"],
+ summary: "Birthday reminders",
+ description: "Birthday reminders about users you follow.",
+ operationId: "PleromaAPI.AccountController.birthdays",
+ parameters: [
+ Operation.parameter(
+ :day,
+ :query,
+ %Schema{type: :integer},
+ "Day of users' birthdays"
+ ),
+ Operation.parameter(
+ :month,
+ :query,
+ %Schema{type: :integer},
+ "Month of users' birthdays"
+ )
+ ],
+ security: [%{"oAuth" => ["read:accounts"]}],
+ responses: %{
+ 200 =>
+ Operation.response("Accounts", "application/json", AccountOperation.array_of_accounts())
+ }
+ }
+ end
+
defp id_param do
Operation.parameter(:id, :path, FlakeID, "Account ID",
example: "9umDrYheeY451cQnEe",
diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex
index 548e70544..029c6f6cf 100644
--- a/lib/pleroma/web/api_spec/schemas/account.ex
+++ b/lib/pleroma/web/api_spec/schemas/account.ex
@@ -47,12 +47,14 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
description: "whether the user allows automatically follow moved following accounts"
},
background_image: %Schema{type: :string, nullable: true, format: :uri},
+ birthday: %Schema{type: :string, nullable: true, format: :date},
chat_token: %Schema{type: :string},
is_confirmed: %Schema{
type: :boolean,
description:
"whether the user account is waiting on email confirmation to be activated"
},
+ show_birthday: %Schema{type: :boolean, nullable: true},
hide_favorites: %Schema{type: :boolean},
hide_followers_count: %Schema{
type: :boolean,
@@ -202,7 +204,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
},
"settings_store" => %{
"pleroma-fe" => %{}
- }
+ },
+ "birthday" => "2001-02-12"
},
"source" => %{
"fields" => [],
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index a90833bf0..8e6d49168 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -191,7 +191,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
:skip_thread_containment,
:allow_following_move,
:also_known_as,
- :accepts_chat_messages
+ :accepts_chat_messages,
+ :show_birthday
]
|> Enum.reduce(%{}, fn key, acc ->
Maps.put_if_present(acc, key, params[key], &{:ok, Params.truthy_param?(&1)})
@@ -219,6 +220,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|> Maps.put_if_present(:is_locked, params[:locked])
# Note: param name is indeed :discoverable (not an error)
|> Maps.put_if_present(:is_discoverable, params[:discoverable])
+ |> Maps.put_if_present(:birthday, params[:birthday])
# What happens here:
#
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index b964fdc54..1d78ced19 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -311,6 +311,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|> maybe_put_unread_conversation_count(user, opts[:for])
|> maybe_put_unread_notification_count(user, opts[:for])
|> maybe_put_email_address(user, opts[:for])
+ |> maybe_show_birthday(user, opts[:for])
end
defp username_from_nickname(string) when is_binary(string) do
@@ -344,6 +345,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|> Kernel.put_in([:source, :privacy], user.default_scope)
|> Kernel.put_in([:source, :pleroma, :show_role], user.show_role)
|> Kernel.put_in([:source, :pleroma, :no_rich_text], user.no_rich_text)
+ |> Kernel.put_in([:source, :pleroma, :show_birthday], user.show_birthday)
end
defp maybe_put_settings(data, _, _, _), do: data
@@ -432,6 +434,20 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp maybe_put_email_address(data, _, _), do: data
+ defp maybe_show_birthday(data, %User{id: user_id} = user, %User{id: user_id}) do
+ data
+ |> Kernel.put_in([:pleroma, :birthday], user.birthday)
+ end
+
+ defp maybe_show_birthday(data, %User{show_birthday: true} = user, _) do
+ data
+ |> Kernel.put_in([:pleroma, :birthday], user.birthday)
+ end
+
+ defp maybe_show_birthday(data, _, _) do
+ data
+ end
+
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil
end
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
index cbed5fba9..fa6c20a30 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -46,7 +46,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
federation: federation(),
fields_limits: fields_limits(),
post_formats: Config.get([:instance, :allowed_post_formats]),
- privileged_staff: Config.get([:instance, :privileged_staff])
+ privileged_staff: Config.get([:instance, :privileged_staff]),
+ birthday_required: Config.get([:instance, :birthday_required]),
+ birthday_min_age: Config.get([:instance, :birthday_min_age])
},
stats: %{mau: Pleroma.User.active_user_count()},
vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
index 66a8d1c1c..d78ebbe2e 100644
--- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
@@ -51,6 +51,11 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
when action == :endorsements
)
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:accounts"]} when action == :birthdays
+ )
+
plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend)
plug(
@@ -137,4 +142,18 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
+
+ @doc "GET /api/v1/pleroma/birthdays"
+ def birthdays(%{assigns: %{user: %User{} = user}} = conn, %{day: day, month: month} = _params) do
+ birthdays =
+ User.get_friends_birthdays_query(user, day, month)
+ |> Pleroma.Repo.all()
+
+ conn
+ |> render("index.json",
+ for: user,
+ users: birthdays,
+ as: :user
+ )
+ end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 67c1a3e5c..26706a6b8 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -448,6 +448,8 @@ defmodule Pleroma.Web.Router do
post("/accounts/:id/subscribe", AccountController, :subscribe)
post("/accounts/:id/unsubscribe", AccountController, :unsubscribe)
+
+ get("/birthdays", AccountController, :birthdays)
end
post("/accounts/confirmation_resend", AccountController, :confirmation_resend)
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index 76ca82d20..aa4dfb145 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -20,6 +20,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
|> Map.put(:name, Map.get(params, :fullname, params[:username]))
|> Map.put(:password_confirmation, params[:password])
|> Map.put(:registration_reason, params[:reason])
+ |> Map.put(:birthday, params[:birthday])
if Pleroma.Config.get([:instance, :registrations_open]) do
create_user(params, opts)
diff --git a/priv/repo/migrations/20220116183110_add_birthday_to_users.exs b/priv/repo/migrations/20220116183110_add_birthday_to_users.exs
new file mode 100644
index 000000000..0b22ecc69
--- /dev/null
+++ b/priv/repo/migrations/20220116183110_add_birthday_to_users.exs
@@ -0,0 +1,12 @@
+defmodule Pleroma.Repo.Migrations.AddBirthdayToUsers do
+ use Ecto.Migration
+
+ def change do
+ alter table(:users) do
+ add_if_not_exists(:birthday, :date)
+ add_if_not_exists(:show_birthday, :boolean, default: false, null: false)
+ end
+
+ create_if_not_exists(index(:users, [:show_birthday]))
+ end
+end
diff --git a/priv/repo/migrations/20220125104429_add_birthday_month_day_index_to_users.exs b/priv/repo/migrations/20220125104429_add_birthday_month_day_index_to_users.exs
new file mode 100644
index 000000000..8ce4c77c5
--- /dev/null
+++ b/priv/repo/migrations/20220125104429_add_birthday_month_day_index_to_users.exs
@@ -0,0 +1,11 @@
+defmodule Pleroma.Repo.Migrations.AddBirthdayMonthDayIndexToUsers do
+ use Ecto.Migration
+
+ def change do
+ create(
+ index(:users, ["date_part('month', birthday)", "date_part('day', birthday)"],
+ name: :users_birthday_month_day_index
+ )
+ )
+ end
+end
diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex
index 4694a92a5..f33a721a8 100644
--- a/priv/scrubbers/default.ex
+++ b/priv/scrubbers/default.ex
@@ -56,7 +56,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes(:u, [])
Meta.allow_tag_with_these_attributes(:ul, [])
- Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card"])
+ Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card", "recipients-inline"])
Meta.allow_tag_with_these_attributes(:span, [])
Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"])
diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld
index e7722cf72..946099a6e 100644
--- a/priv/static/schemas/litepub-0.1.jsonld
+++ b/priv/static/schemas/litepub-0.1.jsonld
@@ -35,7 +35,8 @@
"alsoKnownAs": {
"@id": "as:alsoKnownAs",
"@type": "@id"
- }
+ },
+ "vcard": "http://www.w3.org/2006/vcard/ns#"
}
]
}
diff --git a/test/fixtures/birthdays/misskey-user.json b/test/fixtures/birthdays/misskey-user.json
new file mode 100644
index 000000000..4ffee3910
--- /dev/null
+++ b/test/fixtures/birthdays/misskey-user.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","Hashtag":"as:Hashtag","quoteUrl":"as:quoteUrl","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji","featured":"toot:featured","discoverable":"toot:discoverable","schema":"http://schema.org#","PropertyValue":"schema:PropertyValue","value":"schema:value","misskey":"https://misskey.io/ns#","_misskey_content":"misskey:_misskey_content","_misskey_quote":"misskey:_misskey_quote","_misskey_reaction":"misskey:_misskey_reaction","_misskey_votes":"misskey:_misskey_votes","_misskey_talk":"misskey:_misskey_talk","isCat":"misskey:isCat","vcard":"http://www.w3.org/2006/vcard/ns#"}],"type":"Person","id":"https://misskey.io/users/8dhi2ne167","inbox":"https://misskey.io/users/8dhi2ne167/inbox","outbox":"https://misskey.io/users/8dhi2ne167/outbox","followers":"https://misskey.io/users/8dhi2ne167/followers","following":"https://misskey.io/users/8dhi2ne167/following","sharedInbox":"https://misskey.io/inbox","endpoints":{"sharedInbox":"https://misskey.io/inbox"},"url":"https://misskey.io/@mkljczk","preferredUsername":"mkljczk","name":null,"summary":null,"icon":null,"image":null,"tag":[],"manuallyApprovesFollowers":false,"discoverable":true,"publicKey":{"id":"https://misskey.io/users/8dhi2ne167#main-key","type":"Key","owner":"https://misskey.io/users/8dhi2ne167","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7CI3Ol1M0TDdLL+E8Uhd\nJ8l/RTEtxl39MKxsqSCZr9itf/EBn4dGTifK9LN3XZD2fjmX4hdwaxndp2HYVDqn\ndc6O57u8dHxFv9wTwXQrLzEonOzbrBec6WB42ZpkFHi4XEyqg8iYGu5Yy7ttXJ21\nOfWqi+eytttcTErKuu4z8MX1L1IlmpfSmH1trMyDZLFMRqVJ0416/qI0K3l3cmIf\n8cuWbJ57UxVbYxp9242der/3vrNIU24rAouYQYe1atUgFPKil3w8dCY7magy36Wg\nOXC1hdRsFcsVW54/3cSQ9fc/+1HIg16/zlS+AWb4dVDhrAUJLYIBrkMPRnu/cDuI\ndvyL+KtZUxhDBoSO0JLrd1+GZGt0WD+mfutCugJS8IGlWQmGq8WRmM2vYfZgEYkq\nCv4392VSsWvg4iluKz0eX+8l7QKHseJwGBvk89Txlz6f7QkooBXYuuyHZS1ZLZBW\nfooK+RNAquDU+cVUu1gVt1V5yt3IxF1qvMRtlElNJKN5NUJT9/K2YcVX6UoMXhDd\noSOpARqPm9E2pdjI62pAOBbCplMSoBprhoCYm0iozf9QhNyUBGWDcTsFDDgqOwy4\nYjGQ5jsnCrkhSzRkTViWD+Pgw+Ar4fxcjySGUf0x7HkNfteDPSdLMD8J2vTJXfoB\nGAQQmGMZmFgONC62FrDphlsCAwEAAQ==\n-----END PUBLIC KEY-----\n"},"isCat":true,"vcard:bday":"2001-02-12"}
\ No newline at end of file
diff --git a/test/fixtures/roadhouse-create-activity.json b/test/fixtures/roadhouse-create-activity.json
new file mode 100644
index 000000000..c082f84d7
--- /dev/null
+++ b/test/fixtures/roadhouse-create-activity.json
@@ -0,0 +1,109 @@
+{
+ "@context":[
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "zot":"https://macgirvin.com/apschema#",
+ "toot":"http://joinmastodon.org/ns#",
+ "ostatus":"http://ostatus.org#",
+ "schema":"http://schema.org#",
+ "litepub":"http://litepub.social/ns#",
+ "sm":"http://smithereen.software/ns#",
+ "conversation":"ostatus:conversation",
+ "manuallyApprovesFollowers":"as:manuallyApprovesFollowers",
+ "oauthRegistrationEndpoint":"litepub:oauthRegistrationEndpoint",
+ "sensitive":"as:sensitive",
+ "movedTo":"as:movedTo",
+ "copiedTo":"as:copiedTo",
+ "alsoKnownAs":"as:alsoKnownAs",
+ "EmojiReact":"as:EmojiReact",
+ "commentPolicy":"zot:commentPolicy",
+ "topicalCollection":"zot:topicalCollection",
+ "eventRepeat":"zot:eventRepeat",
+ "emojiReaction":"zot:emojiReaction",
+ "expires":"zot:expires",
+ "directMessage":"zot:directMessage",
+ "Category":"zot:Category",
+ "replyTo":"zot:replyTo",
+ "PropertyValue":"schema:PropertyValue",
+ "value":"schema:value",
+ "discoverable":"toot:discoverable",
+ "wall":"sm:wall",
+ "capabilities":"litepub:capabilities",
+ "acceptsJoins":"litepub:acceptsJoins"
+ }
+ ],
+ "type":"Create",
+ "id":"https://macgirvin.com/activity/ce9ce740-e1cb-4a75-aa4e-9e923555870d",
+ "published":"2022-02-02T04:41:46Z",
+ "context":"https://gleasonator.com/objects/102eb097-a18b-4cd5-abfc-f952efcb70bb",
+ "conversation":"https://gleasonator.com/objects/102eb097-a18b-4cd5-abfc-f952efcb70bb",
+ "actor":"https://macgirvin.com/channel/mike",
+ "replyTo":"https://macgirvin.com/channel/mike",
+ "url":"https://macgirvin.com/activity/ce9ce740-e1cb-4a75-aa4e-9e923555870d",
+ "object":{
+ "type":"Note",
+ "id":"https://macgirvin.com/item/ce9ce740-e1cb-4a75-aa4e-9e923555870d",
+ "published":"2022-02-02T04:41:46Z",
+ "attributedTo":"https://macgirvin.com/channel/mike",
+ "inReplyTo":"https://gleasonator.com/objects/102eb097-a18b-4cd5-abfc-f952efcb70bb",
+ "context":"https://gleasonator.com/objects/102eb097-a18b-4cd5-abfc-f952efcb70bb",
+ "conversation":"https://gleasonator.com/objects/102eb097-a18b-4cd5-abfc-f952efcb70bb",
+ "content":"The Accepts should get through now. Now to figure out why the comments are failing.",
+ "source":{
+ "content":"The Accepts should get through now. Now to figure out why the comments are failing.",
+ "mediaType":"text/x-multicode"
+ },
+ "replyTo":"https://macgirvin.com/channel/mike",
+ "url":"https://macgirvin.com/item/ce9ce740-e1cb-4a75-aa4e-9e923555870d",
+ "tag":[
+ {
+ "type":"Mention",
+ "href":"https://gleasonator.com/users/macgirvin",
+ "name":"@macgirvin@gleasonator.com"
+ },
+ {
+ "type":"Mention",
+ "href":"https://gleasonator.com/users/alex",
+ "name":"@alex@gleasonator.com"
+ }
+ ],
+ "to":[
+ "https://www.w3.org/ns/activitystreams#Public",
+ "https://gleasonator.com/users/alex",
+ "https://gleasonator.com/users/macgirvin"
+ ],
+ "cc":[
+ "https://macgirvin.com/followers/mike",
+ "https://gleasonator.com/users/macgirvin/followers"
+ ]
+ },
+ "tag":[
+ {
+ "type":"Mention",
+ "href":"https://gleasonator.com/users/macgirvin",
+ "name":"@macgirvin@gleasonator.com"
+ },
+ {
+ "type":"Mention",
+ "href":"https://gleasonator.com/users/alex",
+ "name":"@alex@gleasonator.com"
+ }
+ ],
+ "to":[
+ "https://www.w3.org/ns/activitystreams#Public",
+ "https://gleasonator.com/users/alex",
+ "https://gleasonator.com/users/macgirvin"
+ ],
+ "cc":[
+ "https://macgirvin.com/followers/mike",
+ "https://gleasonator.com/users/macgirvin/followers"
+ ],
+ "signature":{
+ "type":"RsaSignature2017",
+ "nonce":"544080164a412f0592f8257094a870a0177276f4a04cc4410974f5e8fa277a14",
+ "creator":"https://macgirvin.com/channel/mike",
+ "created":"2022-02-02T04:41:46Z",
+ "signatureValue":"BkHEdRkki/DGHctiI6BWyQdn0i9ip+7rFiqqlLgotgQzwA8wzcmyvFIRm8Z+3OqrbzkNyQLCmx4qHvkqWyNrR0eSSJPaBryYvTLpLBz5F5PSpHOI5x4kRGVaI2S1Po+pUMWgchyyQ8ylqdLVHe0FnqId7vFVo9uj4jWydU5wPGlbb5nvnRMQGGyPqzCJ69lopMgCRCUNgbOz6hYVU0Mhqgi9BDjs1crbRLoGexz87tu7FeGEmtyfz8/SBGrDy+X+U3ahykwqd7ggcptsYVu5BY7BfREOLNZE8BwapUCg+QSw9PJv7dLJytdh0kUrzpuAPNbugx7y662FALmHZlxcIC1IVwbPOwEiosDm6wXsOiIyTvnNFcvDre/B1corB2yt5wmO3Cu5jINvp4+aBTheGIKMxBvpJXywpWe6C0VBqfNZSreJtDwp9lHd6D1+L4V6hUCOqiCcqmLT/GI5TOR+EhSpJ02TOkuu+/8hyIfO6ec3uK98y8suyidckbfM60jrbvZh2s/kF9WlDsE9K+Jlyd9Xx3mI5jU+4MXo1MDA57zYpsXw0S+v++rvnOw8CkQr/opVdIRSCG+UB3VRinpgjwW1UQcdAI+fiVaAwqr45MgnIEaQdk13skJUfUl06D9IxKxhu7yNW1tR621w/HN+358qVQWWBoj+1ZhCG3Thjug="
+ }
+}
diff --git a/test/fixtures/tesla_mock/gleasonator-AG3RzWfwEKKrY63qj2.json b/test/fixtures/tesla_mock/gleasonator-AG3RzWfwEKKrY63qj2.json
new file mode 100644
index 000000000..62d7bb9ae
--- /dev/null
+++ b/test/fixtures/tesla_mock/gleasonator-AG3RzWfwEKKrY63qj2.json
@@ -0,0 +1,35 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://gleasonator.com/schemas/litepub-0.1.jsonld",
+ {
+ "@language": "und"
+ }
+ ],
+ "actor": "https://gleasonator.com/users/macgirvin",
+ "attachment": [],
+ "attributedTo": "https://gleasonator.com/users/macgirvin",
+ "cc": [
+ "https://gleasonator.com/users/macgirvin/followers"
+ ],
+ "content": "@alex Any idea why my posts are failing? I sent an Accept/Follow from macgirvin.com at 2022-02-02T04:06:01Z and it vanished into space. As do all my comments to you.
2022-02-02T04:06:01Z:LOG_INFO:d5c4aa7f6a:Queue.php:435:deliver: deliver: queue post returned 200 from https://gleasonator.com/users/macgirvin/inbox
It's OK if I'm blocked, but if that's the case, I shouldn't be able to send a follow to that address should I?",
+ "context": "https://gleasonator.com/contexts/b7f01f94-bc92-4d89-a085-0ffc211b0e42",
+ "conversation": "https://gleasonator.com/contexts/b7f01f94-bc92-4d89-a085-0ffc211b0e42",
+ "id": "https://gleasonator.com/objects/102eb097-a18b-4cd5-abfc-f952efcb70bb",
+ "published": "2022-02-02T04:14:10.965833Z",
+ "sensitive": false,
+ "source": "@alex Any idea why my posts are failing? I sent an Accept/Follow from macgirvin.com at 2022-02-02T04:06:01Z and it vanished into space. As do all my comments to you. \n\n2022-02-02T04:06:01Z:LOG_INFO:d5c4aa7f6a:Queue.php:435:deliver: deliver: queue post returned 200 from https://gleasonator.com/users/macgirvin/inbox\n\nIt's OK if I'm blocked, but if that's the case, I shouldn't be able to send a follow to that address should I?",
+ "summary": "",
+ "tag": [
+ {
+ "href": "https://gleasonator.com/users/alex",
+ "name": "@alex",
+ "type": "Mention"
+ }
+ ],
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public",
+ "https://gleasonator.com/users/alex"
+ ],
+ "type": "Note"
+}
diff --git a/test/fixtures/tesla_mock/macgirvin@gleasonator.com.json b/test/fixtures/tesla_mock/macgirvin@gleasonator.com.json
new file mode 100644
index 000000000..9d7d47d40
--- /dev/null
+++ b/test/fixtures/tesla_mock/macgirvin@gleasonator.com.json
@@ -0,0 +1,41 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://gleasonator.com/schemas/litepub-0.1.jsonld",
+ {
+ "@language": "und"
+ }
+ ],
+ "alsoKnownAs": [],
+ "attachment": [],
+ "capabilities": {
+ "acceptsChatMessages": true
+ },
+ "discoverable": false,
+ "endpoints": {
+ "oauthAuthorizationEndpoint": "https://gleasonator.com/oauth/authorize",
+ "oauthRegistrationEndpoint": "https://gleasonator.com/api/v1/apps",
+ "oauthTokenEndpoint": "https://gleasonator.com/oauth/token",
+ "sharedInbox": "https://gleasonator.com/inbox",
+ "uploadMedia": "https://gleasonator.com/api/ap/upload_media"
+ },
+ "featured": "https://gleasonator.com/users/macgirvin/collections/featured",
+ "followers": "https://gleasonator.com/users/macgirvin/followers",
+ "following": "https://gleasonator.com/users/macgirvin/following",
+ "id": "https://gleasonator.com/users/macgirvin",
+ "inbox": "https://gleasonator.com/users/macgirvin/inbox",
+ "manuallyApprovesFollowers": false,
+ "name": "macgirvin",
+ "outbox": "https://gleasonator.com/users/macgirvin/outbox",
+ "preferredUsername": "macgirvin",
+ "publicKey": {
+ "id": "https://gleasonator.com/users/macgirvin#main-key",
+ "owner": "https://gleasonator.com/users/macgirvin",
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0AUMgsQu87tLwoJjZfZu\nsj7NV0qt/THdK8aw4Ha2GZfNkwrep99AZ8gmCI+rr+N5vyETAARzG5/Qzr5bNTUx\nsc1fxFemhhi6sxzAv4qZ5AgvWQ4YPFWizSp5ZY1jpPHLOeF2IftMf8CwVI82PtD0\n7m7T6iUYA4vfvMp9LxVrzQA+CAtpsQxAejTGCt37yM9T2mEWqfmJQQHRIQ4brKBL\nI82sNbzk1cbTwCfH7vRNS/l1ND+vaUGGkDKtpRl56BLmt4picYL0avc+8oO7ebpc\n/zUoS8OOi+mpEzjv7TBrSirYEIGvIh3TKHWSPrpHpQTqj9xBQBy+AxXTWahQEO2M\ndQIDAQAB\n-----END PUBLIC KEY-----\n\n"
+ },
+ "summary": "",
+ "tag": [],
+ "type": "Person",
+ "url": "https://gleasonator.com/users/macgirvin",
+ "vcard:bday": null
+}
diff --git a/test/pleroma/reverse_proxy_test.exs b/test/pleroma/reverse_proxy_test.exs
index a4dd8e99a..49ddf251d 100644
--- a/test/pleroma/reverse_proxy_test.exs
+++ b/test/pleroma/reverse_proxy_test.exs
@@ -130,7 +130,7 @@ defmodule Pleroma.ReverseProxyTest do
assert capture_log(fn ->
ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 30)
end) =~
- "[warn] Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large"
+ "Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large"
end
end
diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs
index a5a9021f6..a9a3004a8 100644
--- a/test/pleroma/user_test.exs
+++ b/test/pleroma/user_test.exs
@@ -755,6 +755,54 @@ defmodule Pleroma.UserTest do
end
end
+ describe "user registration, with :birthday_required and :birthday_min_age" do
+ @full_user_data %{
+ bio: "A guy",
+ name: "my name",
+ nickname: "nick",
+ password: "test",
+ password_confirmation: "test",
+ email: "email@example.com"
+ }
+
+ setup do
+ clear_config([:instance, :birthday_required], true)
+ clear_config([:instance, :birthday_min_age], 18 * 365)
+ end
+
+ test "it passes when correct birth date is provided" do
+ today = Date.utc_today()
+ birthday = Date.add(today, -19 * 365)
+
+ params =
+ @full_user_data
+ |> Map.put(:birthday, birthday)
+
+ changeset = User.register_changeset(%User{}, params)
+
+ assert changeset.valid?
+ end
+
+ test "it fails when birth date is not provided" do
+ changeset = User.register_changeset(%User{}, @full_user_data)
+
+ refute changeset.valid?
+ end
+
+ test "it fails when provided invalid birth date" do
+ today = Date.utc_today()
+ birthday = Date.add(today, -17 * 365)
+
+ params =
+ @full_user_data
+ |> Map.put(:birthday, birthday)
+
+ changeset = User.register_changeset(%User{}, params)
+
+ refute changeset.valid?
+ end
+ end
+
describe "get_or_fetch/1" do
test "gets an existing user by nickname" do
user = insert(:user)
diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs
index 3d152b4d0..9789d7704 100644
--- a/test/pleroma/web/activity_pub/activity_pub_test.exs
+++ b/test/pleroma/web/activity_pub/activity_pub_test.exs
@@ -389,6 +389,26 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert %{data: %{"id" => ^object_url}} = Object.get_by_ap_id(object_url)
end
+
+ test "fetches user birthday information from misskey" do
+ user_id = "https://misskey.io/@mkljczk"
+
+ Tesla.Mock.mock(fn
+ %{
+ method: :get,
+ url: ^user_id
+ } ->
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/birthdays/misskey-user.json"),
+ headers: [{"content-type", "application/activity+json"}]
+ }
+ end)
+
+ {:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
+
+ assert user.birthday == ~D[2001-02-12]
+ end
end
test "it fetches the appropriate tag-restricted posts" do
diff --git a/test/pleroma/web/activity_pub/mrf/force_mentions_in_content_test.exs b/test/pleroma/web/activity_pub/mrf/force_mentions_in_content_test.exs
index 090bdc35e..669ec5251 100644
--- a/test/pleroma/web/activity_pub/mrf/force_mentions_in_content_test.exs
+++ b/test/pleroma/web/activity_pub/mrf/force_mentions_in_content_test.exs
@@ -3,22 +3,40 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContentTest do
- alias Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent
- import Pleroma.Factory
use Pleroma.DataCase
+ require Pleroma.Constants
+
+ alias Pleroma.Constants
+ alias Pleroma.Object
+ alias Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
test "adds mentions to post content" do
- users = %{
- "lain@lain.com" => "https://lain.com/users/lain",
- "coolboymew@shitposter.club" => "https://shitposter.club/users/coolboymew",
- "dielan@shitposter.club" => "https://shitposter.club/users/dielan",
- "hakui@tuusin.misono-ya.info" => "https://tuusin.misono-ya.info/users/hakui",
- "fence@xyzzy.link" => "https://xyzzy.link/users/fence"
- }
-
- Enum.each(users, fn {nickname, ap_id} ->
- insert(:user, ap_id: ap_id, nickname: nickname, local: false)
- end)
+ [lain, coolboymew, dielan, hakui, fence] = [
+ insert(:user, ap_id: "https://lain.com/users/lain", nickname: "lain@lain.com", local: false),
+ insert(:user,
+ ap_id: "https://shitposter.club/users/coolboymew",
+ nickname: "coolboymew@shitposter.club",
+ local: false
+ ),
+ insert(:user,
+ ap_id: "https://shitposter.club/users/dielan",
+ nickname: "dielan@shitposter.club",
+ local: false
+ ),
+ insert(:user,
+ ap_id: "https://tuusin.misono-ya.info/users/hakui",
+ nickname: "hakui@tuusin.misono-ya.info",
+ local: false
+ ),
+ insert(:user,
+ ap_id: "https://xyzzy.link/users/fence",
+ nickname: "fence@xyzzy.link",
+ local: false
+ )
+ ]
object = File.read!("test/fixtures/soapbox_no_mentions_in_content.json") |> Jason.decode!()
@@ -29,6 +47,118 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContentTest do
}
{:ok, %{"object" => %{"content" => filtered}}} = ForceMentionsInContent.filter(activity)
- Enum.each(users, fn {nickname, _} -> assert filtered =~ nickname end)
+
+ assert filtered ==
+ "
@dielan @coolboymew @fence @hakui @lain Haha yeah, you can control who you reply to.
" + end + + test "the replied-to user is sorted to the left" do + [mario, luigi, wario] = [ + insert(:user, nickname: "mario"), + insert(:user, nickname: "luigi"), + insert(:user, nickname: "wario") + ] + + {:ok, post1} = CommonAPI.post(mario, %{status: "Letsa go!"}) + + {:ok, post2} = + CommonAPI.post(luigi, %{status: "Oh yaah", in_reply_to_id: post1.id, to: [mario.ap_id]}) + + activity = %{ + "type" => "Create", + "actor" => wario.ap_id, + "object" => %{ + "type" => "Note", + "actor" => wario.ap_id, + "content" => "WHA-HA!", + "to" => [ + mario.ap_id, + luigi.ap_id, + Constants.as_public() + ], + "inReplyTo" => Object.normalize(post2).data["id"] + } + } + + {:ok, %{"object" => %{"content" => filtered}}} = ForceMentionsInContent.filter(activity) + + assert filtered == + "@luigi @mario WHA-HA!" + end + + test "don't mention self" do + mario = insert(:user, nickname: "mario") + + {:ok, post} = CommonAPI.post(mario, %{status: "Mama mia"}) + + activity = %{ + "type" => "Create", + "actor" => mario.ap_id, + "object" => %{ + "type" => "Note", + "actor" => mario.ap_id, + "content" => "I'ma tired...", + "to" => [ + mario.ap_id, + Constants.as_public() + ], + "inReplyTo" => Object.normalize(post).data["id"] + } + } + + {:ok, %{"object" => %{"content" => filtered}}} = ForceMentionsInContent.filter(activity) + assert filtered == "I'ma tired..." + end + + test "don't mention in top-level posts" do + mario = insert(:user, nickname: "mario") + luigi = insert(:user, nickname: "luigi") + + {:ok, post} = CommonAPI.post(mario, %{status: "Letsa go"}) + + activity = %{ + "type" => "Create", + "actor" => mario.ap_id, + "object" => %{ + "type" => "Note", + "actor" => mario.ap_id, + "content" => "Mama mia!", + "to" => [ + luigi.ap_id, + Constants.as_public() + ], + "quoteUrl" => Object.normalize(post).data["id"] + } + } + + {:ok, %{"object" => %{"content" => filtered}}} = ForceMentionsInContent.filter(activity) + assert filtered == "Mama mia!" + end + + test "with markdown formatting" do + mario = insert(:user, nickname: "mario") + luigi = insert(:user, nickname: "luigi") + + {:ok, post} = CommonAPI.post(luigi, %{status: "Mama mia"}) + + activity = %{ + "type" => "Create", + "actor" => mario.ap_id, + "object" => %{ + "type" => "Note", + "actor" => mario.ap_id, + "content" => "I'ma tired...
", + "to" => [ + luigi.ap_id, + Constants.as_public() + ], + "inReplyTo" => Object.normalize(post).data["id"] + } + } + + {:ok, %{"object" => %{"content" => filtered}}} = ForceMentionsInContent.filter(activity) + + assert filtered == + "@luigi I'ma tired...
" end end diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs index 720c17d8d..150b26bea 100644 --- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs @@ -32,4 +32,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) end end + + test "a Note from Roadhouse validates" do + insert(:user, ap_id: "https://macgirvin.com/channel/mike") + + %{"object" => note} = + "test/fixtures/roadhouse-create-activity.json" + |> File.read!() + |> Jason.decode!() + + %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) + end end diff --git a/test/pleroma/web/activity_pub/object_validators/create_generic_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/create_generic_validator_test.exs new file mode 100644 index 000000000..c3e6854e4 --- /dev/null +++ b/test/pleroma/web/activity_pub/object_validators/create_generic_validator_test.exs @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors