Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma into emr_develop

This commit is contained in:
a1batross 2022-02-14 18:25:15 +01:00
commit 927419d2e7
51 changed files with 1186 additions and 150 deletions

View File

@ -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` - Ability to log slow Ecto queries by configuring `:pleroma, :telemetry, :slow_queries_logging`
- Added Phoenix LiveDashboard at `/phoenix/live_dashboard` - Added Phoenix LiveDashboard at `/phoenix/live_dashboard`
- Added `/manifest.json` for progressive web apps. - 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 ### Fixed
- Subscription(Bell) Notifications: Don't create from Pipeline Ingested replies - Subscription(Bell) Notifications: Don't create from Pipeline Ingested replies

View File

@ -149,8 +149,6 @@ config :pleroma, Pleroma.Web.Endpoint,
] ]
# Configures Elixir's Logger # Configures Elixir's Logger
config :logger, truncate: 65536
config :logger, :console, config :logger, :console,
level: :debug, level: :debug,
format: "\n$time $metadata[$level] $message\n", format: "\n$time $metadata[$level] $message\n",
@ -259,7 +257,10 @@ config :pleroma, :instance,
password_reset_token_validity: 60 * 60 * 24, password_reset_token_validity: 60 * 60 * 24,
profile_directory: true, profile_directory: true,
privileged_staff: false, privileged_staff: false,
max_endorsed_users: 20 max_endorsed_users: 20,
birthday_required: false,
birthday_min_age: 0,
max_media_attachments: 1_000
config :pleroma, :welcome, config :pleroma, :welcome,
direct_message: [ direct_message: [
@ -857,13 +858,6 @@ config :pleroma, ConcurrentLimiter, [
{Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]} {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 # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs" import_config "#{Mix.env()}.exs"

View File

@ -552,6 +552,14 @@ config :pleroma, :config_description, [
100_000 100_000
] ]
}, },
%{
key: :max_media_attachments,
type: :integer,
description: "Maximum number of post media attachments",
suggestions: [
1_000_000
]
},
%{ %{
key: :upload_limit, key: :upload_limit,
type: :integer, type: :integer,
@ -957,6 +965,17 @@ config :pleroma, :config_description, [
type: :boolean, type: :boolean,
description: description:
"Let moderators access sensitive data (e.g. updating user credentials, get password reset token, delete users, index and read private statuses and chats)" "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."
} }
] ]
}, },

View File

@ -116,3 +116,9 @@ Feel free to contact us to be added to this list!
- Contact: [@r@freesoftwareextremist.com](https://freesoftwareextremist.com/users/r) - Contact: [@r@freesoftwareextremist.com](https://freesoftwareextremist.com/users/r)
- Features: Does not requires JavaScript - Features: Does not requires JavaScript
- Features: MastoAPI - Features: MastoAPI
### Glitch-lily
- Source Code: <https://lily.kazv.moe/infra/glitch-lily>
- Contact: [@tusooa@kazv.moe](https://kazv.moe/users/tusooa)
- Features: MastoAPI
- Based on [glitch-soc](https://github.com/glitch-soc/mastodon) frontend

View File

@ -12,16 +12,10 @@ defmodule Pleroma.Telemetry.Logger do
[:pleroma, :connection_pool, :reclaim, :stop], [:pleroma, :connection_pool, :reclaim, :stop],
[:pleroma, :connection_pool, :provision_failure], [:pleroma, :connection_pool, :provision_failure],
[:pleroma, :connection_pool, :client, :dead], [:pleroma, :connection_pool, :client, :dead],
[:pleroma, :connection_pool, :client, :add], [:pleroma, :connection_pool, :client, :add]
[:pleroma, :repo, :query]
] ]
def attach do def attach do
:telemetry.attach_many( :telemetry.attach_many("pleroma-logger", @events, &handle_event/4, [])
"pleroma-logger",
@events,
&Pleroma.Telemetry.Logger.handle_event/4,
[]
)
end end
# Passing anonymous functions instead of strings to logger is intentional, # Passing anonymous functions instead of strings to logger is intentional,
@ -93,64 +87,4 @@ defmodule Pleroma.Telemetry.Logger do
end end
def handle_event([:pleroma, :connection_pool, :client, :add], _, _, _), do: :ok 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 end

View File

@ -154,6 +154,8 @@ defmodule Pleroma.User do
field(:pinned_objects, :map, default: %{}) field(:pinned_objects, :map, default: %{})
field(:is_suggested, :boolean, default: false) field(:is_suggested, :boolean, default: false)
field(:last_status_at, :naive_datetime) field(:last_status_at, :naive_datetime)
field(:birthday, :date)
field(:show_birthday, :boolean, default: false)
embeds_one( embeds_one(
:notification_settings, :notification_settings,
@ -470,7 +472,9 @@ defmodule Pleroma.User do
:actor_type, :actor_type,
:also_known_as, :also_known_as,
:accepts_chat_messages, :accepts_chat_messages,
:pinned_objects :pinned_objects,
:birthday,
:show_birthday
] ]
) )
|> cast(params, [:name], empty_values: []) |> cast(params, [:name], empty_values: [])
@ -531,9 +535,12 @@ defmodule Pleroma.User do
:is_discoverable, :is_discoverable,
:actor_type, :actor_type,
:accepts_chat_messages, :accepts_chat_messages,
:disclose_client :disclose_client,
:birthday,
:show_birthday
] ]
) )
|> validate_min_age()
|> unique_constraint(:nickname) |> unique_constraint(:nickname)
|> validate_format(:nickname, local_nickname_regex()) |> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit) |> validate_length(:bio, max: bio_limit)
@ -738,7 +745,8 @@ defmodule Pleroma.User do
:password_confirmation, :password_confirmation,
:emoji, :emoji,
:accepts_chat_messages, :accepts_chat_messages,
:registration_reason :registration_reason,
:birthday
]) ])
|> validate_required([:name, :nickname, :password, :password_confirmation]) |> validate_required([:name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password) |> validate_confirmation(:password)
@ -760,6 +768,8 @@ defmodule Pleroma.User do
|> validate_length(:name, min: 1, max: name_limit) |> validate_length(:name, min: 1, max: name_limit)
|> validate_length(:registration_reason, max: reason_limit) |> validate_length(:registration_reason, max: reason_limit)
|> maybe_validate_required_email(opts[:external]) |> maybe_validate_required_email(opts[:external])
|> maybe_validate_required_birthday
|> validate_min_age()
|> put_password_hash |> put_password_hash
|> put_ap_id() |> put_ap_id()
|> unique_constraint(:ap_id) |> unique_constraint(:ap_id)
@ -776,6 +786,26 @@ defmodule Pleroma.User do
end end
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 defp put_ap_id(changeset) do
ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)}) ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
put_change(changeset, :ap_id, ap_id) put_change(changeset, :ap_id, ap_id)
@ -2279,6 +2309,7 @@ defmodule Pleroma.User do
def get_ap_ids_by_nicknames(nicknames) do def get_ap_ids_by_nicknames(nicknames) do
from(u in User, from(u in User,
where: u.nickname in ^nicknames, where: u.nickname in ^nicknames,
order_by: fragment("array_position(?, ?)", ^nicknames, u.nickname),
select: u.ap_id select: u.ap_id
) )
|> Repo.all() |> Repo.all()
@ -2559,4 +2590,13 @@ defmodule Pleroma.User do
_ -> {:error, user} _ -> {:error, user}
end end
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 end

View File

@ -59,7 +59,9 @@ defmodule Pleroma.User.Query do
order_by: term(), order_by: term(),
select: term(), select: term(),
limit: pos_integer(), limit: pos_integer(),
actor_types: [String.t()] actor_types: [String.t()],
birthday_day: pos_integer(),
birthday_month: pos_integer()
} }
| map() | map()
@ -230,6 +232,20 @@ defmodule Pleroma.User.Query do
|> where([u], not like(u.nickname, "internal.%")) |> where([u], not like(u.nickname, "internal.%"))
end 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 compose_query(_unsupported_param, query), do: query
defp location_query(query, local) do defp location_query(query, local) do

View File

@ -1501,6 +1501,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
nil nil
end 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 = %{ user_data = %{
ap_id: data["id"], ap_id: data["id"],
uri: get_actor_url(data["url"]), uri: get_actor_url(data["url"]),
@ -1523,7 +1535,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
inbox: data["inbox"], inbox: data["inbox"],
shared_inbox: shared_inbox, shared_inbox: shared_inbox,
accepts_chat_messages: accepts_chat_messages, 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 # nickname can be nil because of virtual actors
@ -1664,7 +1678,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
"orderedItems" => objects "orderedItems" => objects
}) })
when type in ["OrderedCollection", "Collection"] do when type in ["OrderedCollection", "Collection"] do
Map.new(objects, fn %{"id" => object_ap_id} -> {object_ap_id, NaiveDateTime.utc_now()} end) Map.new(objects, fn
%{"id" => object_ap_id} -> {object_ap_id, NaiveDateTime.utc_now()}
object_ap_id when is_binary(object_ap_id) -> {object_ap_id, NaiveDateTime.utc_now()}
end)
end end
def fetch_and_prepare_featured_from_ap_id(nil) do def fetch_and_prepare_featured_from_ap_id(nil) do

View File

@ -10,7 +10,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy do
require Pleroma.Constants require Pleroma.Constants
defp check_by_actor_type(user), do: user.actor_type in ["Application", "Service"] 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) defp check_if_bot(user), do: check_by_actor_type(user) or check_by_nickname(user)

View File

@ -3,7 +3,10 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do
require Pleroma.Constants
alias Pleroma.Formatter alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF.Policy @behaviour Pleroma.Web.ActivityPub.MRF.Policy
@ -34,40 +37,89 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do
do_extract(tree, []) do_extract(tree, [])
end 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 @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 # image-only posts from pleroma apparently reach this MRF without the content field
content = object["object"]["content"] || "" content = object["object"]["content"] || ""
# Get the replied-to user for sorting
replied_to_user = get_replied_to_user(object["object"])
mention_users = mention_users =
tag to
|> Enum.filter(fn tag -> tag["type"] == "Mention" end) |> clean_recipients(object)
|> Enum.map(& &1["href"]) |> Enum.map(&User.get_cached_by_ap_id/1)
|> Enum.reject(&is_nil/1) |> Enum.reject(&is_nil/1)
|> Enum.map(fn ap_id_or_uri -> |> sort_replied_user(replied_to_user)
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(%{})
explicitly_mentioned_uris = extract_mention_uris_from_content(content) explicitly_mentioned_uris = extract_mention_uris_from_content(content)
added_mentions = 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 unless uri in explicitly_mentioned_uris do
acc <> Formatter.mention_from_user(user) acc <> Formatter.mention_from_user(user, %{mentions_format: :compact}) <> " "
else else
acc acc
end end
end) end)
content = recipients_inline =
if added_mentions != "", if added_mentions != "",
do: added_mentions <> " " <> content, do: "<span class=\"recipients-inline\">#{added_mentions}</span>",
else: content else: ""
content =
cond do
# For Markdown posts, insert the mentions inside the first <p> tag
recipients_inline != "" && String.starts_with?(content, "<p>") ->
"<p>" <> recipients_inline <> String.trim_leading(content, "<p>")
recipients_inline != "" ->
recipients_inline <> content
true ->
content
end
{:ok, put_in(object["object"]["content"], content)} {:ok, put_in(object["object"]["content"], content)}
end end

View File

@ -92,6 +92,11 @@ defmodule Pleroma.Web.ActivityPub.UserView do
%{} %{}
end end
birthday =
if user.show_birthday && user.birthday,
do: Date.to_iso8601(user.birthday),
else: nil
%{ %{
"id" => user.ap_id, "id" => user.ap_id,
"type" => user.actor_type, "type" => user.actor_type,
@ -116,7 +121,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
# Note: key name is indeed "discoverable" (not an error) # Note: key name is indeed "discoverable" (not an error)
"discoverable" => user.is_discoverable, "discoverable" => user.is_discoverable,
"capabilities" => capabilities, "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.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))

View File

@ -543,6 +543,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
type: :string, type: :string,
nullable: true, nullable: true,
description: "Invite token required when the registrations aren't public" description: "Invite token required when the registrations aren't public"
},
birthday: %Schema{
type: :string,
nullable: true,
description: "User's birthday",
format: :date
} }
}, },
example: %{ example: %{
@ -720,7 +726,18 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
description: description:
"Discovery (listing, indexing) of this account by external services (search bots etc.) is allowed." "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: %{ example: %{
bot: false, bot: false,
@ -740,7 +757,9 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
allow_following_move: false, allow_following_move: false,
also_known_as: ["https://foo.bar/users/foo"], also_known_as: ["https://foo.bar/users/foo"],
discoverable: false, discoverable: false,
actor_type: "Person" actor_type: "Person",
show_birthday: false,
birthday: "2001-02-12"
} }
} }
end end

View File

@ -4,6 +4,7 @@
defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
alias OpenApiSpex.Operation alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.AccountOperation alias Pleroma.Web.ApiSpec.AccountOperation
alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
alias Pleroma.Web.ApiSpec.Schemas.ApiError alias Pleroma.Web.ApiSpec.Schemas.ApiError
@ -112,6 +113,34 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
} }
end 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 defp id_param do
Operation.parameter(:id, :path, FlakeID, "Account ID", Operation.parameter(:id, :path, FlakeID, "Account ID",
example: "9umDrYheeY451cQnEe", example: "9umDrYheeY451cQnEe",

View File

@ -47,12 +47,14 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
description: "whether the user allows automatically follow moved following accounts" description: "whether the user allows automatically follow moved following accounts"
}, },
background_image: %Schema{type: :string, nullable: true, format: :uri}, background_image: %Schema{type: :string, nullable: true, format: :uri},
birthday: %Schema{type: :string, nullable: true, format: :date},
chat_token: %Schema{type: :string}, chat_token: %Schema{type: :string},
is_confirmed: %Schema{ is_confirmed: %Schema{
type: :boolean, type: :boolean,
description: description:
"whether the user account is waiting on email confirmation to be activated" "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_favorites: %Schema{type: :boolean},
hide_followers_count: %Schema{ hide_followers_count: %Schema{
type: :boolean, type: :boolean,
@ -202,7 +204,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
}, },
"settings_store" => %{ "settings_store" => %{
"pleroma-fe" => %{} "pleroma-fe" => %{}
} },
"birthday" => "2001-02-12"
}, },
"source" => %{ "source" => %{
"fields" => [], "fields" => [],

View File

@ -112,7 +112,12 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
defp attachments(%{params: params} = draft) do defp attachments(%{params: params} = draft) do
attachments = Utils.attachments_from_ids(params) attachments = Utils.attachments_from_ids(params)
%__MODULE__{draft | attachments: attachments} draft = %__MODULE__{draft | attachments: attachments}
case Utils.validate_attachments_count(attachments) do
:ok -> draft
{:error, message} -> add_error(draft, message)
end
end end
defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft

View File

@ -492,4 +492,19 @@ defmodule Pleroma.Web.CommonAPI.Utils do
{:error, dgettext("errors", "The status is over the character limit")} {:error, dgettext("errors", "The status is over the character limit")}
end end
end end
def validate_attachments_count([] = _attachments) do
:ok
end
def validate_attachments_count(attachments) do
limit = Config.get([:instance, :max_media_attachments])
count = length(attachments)
if count <= limit do
:ok
else
{:error, dgettext("errors", "Too many attachments")}
end
end
end end

View File

@ -191,7 +191,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
:skip_thread_containment, :skip_thread_containment,
:allow_following_move, :allow_following_move,
:also_known_as, :also_known_as,
:accepts_chat_messages :accepts_chat_messages,
:show_birthday
] ]
|> Enum.reduce(%{}, fn key, acc -> |> Enum.reduce(%{}, fn key, acc ->
Maps.put_if_present(acc, key, params[key], &{:ok, Params.truthy_param?(&1)}) 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]) |> Maps.put_if_present(:is_locked, params[:locked])
# Note: param name is indeed :discoverable (not an error) # Note: param name is indeed :discoverable (not an error)
|> Maps.put_if_present(:is_discoverable, params[:discoverable]) |> Maps.put_if_present(:is_discoverable, params[:discoverable])
|> Maps.put_if_present(:birthday, params[:birthday])
# What happens here: # What happens here:
# #

View File

@ -311,6 +311,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|> maybe_put_unread_conversation_count(user, opts[:for]) |> maybe_put_unread_conversation_count(user, opts[:for])
|> maybe_put_unread_notification_count(user, opts[:for]) |> maybe_put_unread_notification_count(user, opts[:for])
|> maybe_put_email_address(user, opts[:for]) |> maybe_put_email_address(user, opts[:for])
|> maybe_show_birthday(user, opts[:for])
end end
defp username_from_nickname(string) when is_binary(string) do 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, :privacy], user.default_scope)
|> Kernel.put_in([:source, :pleroma, :show_role], user.show_role) |> 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, :no_rich_text], user.no_rich_text)
|> Kernel.put_in([:source, :pleroma, :show_birthday], user.show_birthday)
end end
defp maybe_put_settings(data, _, _, _), do: data 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_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(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil defp image_url(_), do: nil
end end

View File

@ -31,6 +31,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
approval_required: Keyword.get(instance, :account_approval_required), approval_required: Keyword.get(instance, :account_approval_required),
# Extra (not present in Mastodon): # Extra (not present in Mastodon):
max_toot_chars: Keyword.get(instance, :limit), max_toot_chars: Keyword.get(instance, :limit),
max_media_attachments: Keyword.get(instance, :max_media_attachments),
poll_limits: Keyword.get(instance, :poll_limits), poll_limits: Keyword.get(instance, :poll_limits),
upload_limit: Keyword.get(instance, :upload_limit), upload_limit: Keyword.get(instance, :upload_limit),
avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit), avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit),
@ -46,7 +47,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
federation: federation(), federation: federation(),
fields_limits: fields_limits(), fields_limits: fields_limits(),
post_formats: Config.get([:instance, :allowed_post_formats]), 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()}, stats: %{mau: Pleroma.User.active_user_count()},
vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)

View File

@ -51,6 +51,11 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
when action == :endorsements when action == :endorsements
) )
plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"]} when action == :birthdays
)
plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend) plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend)
plug( plug(
@ -137,4 +142,18 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
{:error, message} -> json_response(conn, :forbidden, %{error: message}) {:error, message} -> json_response(conn, :forbidden, %{error: message})
end end
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 end

View File

@ -448,6 +448,8 @@ defmodule Pleroma.Web.Router do
post("/accounts/:id/subscribe", AccountController, :subscribe) post("/accounts/:id/subscribe", AccountController, :subscribe)
post("/accounts/:id/unsubscribe", AccountController, :unsubscribe) post("/accounts/:id/unsubscribe", AccountController, :unsubscribe)
get("/birthdays", AccountController, :birthdays)
end end
post("/accounts/confirmation_resend", AccountController, :confirmation_resend) post("/accounts/confirmation_resend", AccountController, :confirmation_resend)

View File

@ -10,7 +10,7 @@
<%= form_for @conn, Routes.mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %> <%= form_for @conn, Routes.mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
<div class="input"> <div class="input">
<%= label f, :code, "Authentication code" %> <%= label f, :code, "Authentication code" %>
<%= text_input f, :code, [autocomplete: false, autocorrect: "off", autocapitalize: "off", autofocus: true, pattern: "[0-9]*", spellcheck: false] %> <%= text_input f, :code, [autocomplete: "one-time-code", autocorrect: "off", autocapitalize: "off", autofocus: true, pattern: "[0-9]*", spellcheck: false] %>
<%= hidden_input f, :mfa_token, value: @mfa_token %> <%= hidden_input f, :mfa_token, value: @mfa_token %>
<%= hidden_input f, :state, value: @state %> <%= hidden_input f, :state, value: @state %>
<%= hidden_input f, :redirect_uri, value: @redirect_uri %> <%= hidden_input f, :redirect_uri, value: @redirect_uri %>

View File

@ -12,11 +12,11 @@
<div class="input"> <div class="input">
<%= label f, :nickname, "Nickname" %> <%= label f, :nickname, "Nickname" %>
<%= text_input f, :nickname, value: @nickname %> <%= text_input f, :nickname, value: @nickname, autocomplete: "username" %>
</div> </div>
<div class="input"> <div class="input">
<%= label f, :email, "Email" %> <%= label f, :email, "Email" %>
<%= text_input f, :email, value: @email %> <%= text_input f, :email, value: @email, autocomplete: "email" %>
</div> </div>
<%= submit "Proceed as new user", name: "op", value: "register" %> <%= submit "Proceed as new user", name: "op", value: "register" %>
@ -25,11 +25,11 @@
<div class="input"> <div class="input">
<%= label f, :name, "Name or email" %> <%= label f, :name, "Name or email" %>
<%= text_input f, :name %> <%= text_input f, :name, autocomplete: "username" %>
</div> </div>
<div class="input"> <div class="input">
<%= label f, :password, "Password" %> <%= label f, :password, "Password" %>
<%= password_input f, :password %> <%= password_input f, :password, autocomplete: "password" %>
</div> </div>
<%= submit "Proceed as existing user", name: "op", value: "connect" %> <%= submit "Proceed as existing user", name: "op", value: "connect" %>

View File

@ -35,7 +35,7 @@
<p>Choose carefully! You won't be able to change this later. You will be able to change your display name, though.</p> <p>Choose carefully! You won't be able to change this later. You will be able to change your display name, though.</p>
<div class="input"> <div class="input">
<%= label f, :nickname, "Pleroma Handle" %> <%= label f, :nickname, "Pleroma Handle" %>
<%= text_input f, :nickname, placeholder: "lain" %> <%= text_input f, :nickname, placeholder: "lain", autocomplete: "username" %>
</div> </div>
<%= hidden_input f, :name, value: @params["name"] %> <%= hidden_input f, :name, value: @params["name"] %>
<%= hidden_input f, :password, value: @params["password"] %> <%= hidden_input f, :password, value: @params["password"] %>

View File

@ -5,9 +5,9 @@
<p><%= @followee.nickname %></p> <p><%= @followee.nickname %></p>
<img height="128" width="128" src="<%= avatar_url(@followee) %>"> <img height="128" width="128" src="<%= avatar_url(@followee) %>">
<%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "authorization"], fn f -> %> <%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "authorization"], fn f -> %>
<%= text_input f, :name, placeholder: "Username", required: true %> <%= text_input f, :name, placeholder: "Username", required: true, autocomplete: "username" %>
<br> <br>
<%= password_input f, :password, placeholder: "Password", required: true %> <%= password_input f, :password, placeholder: "Password", required: true, autocomplete: "password" %>
<br> <br>
<%= hidden_input f, :id, value: @followee.id %> <%= hidden_input f, :id, value: @followee.id %>
<%= submit "Authorize" %> <%= submit "Authorize" %>

View File

@ -20,6 +20,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
|> Map.put(:name, Map.get(params, :fullname, params[:username])) |> Map.put(:name, Map.get(params, :fullname, params[:username]))
|> Map.put(:password_confirmation, params[:password]) |> Map.put(:password_confirmation, params[:password])
|> Map.put(:registration_reason, params[:reason]) |> Map.put(:registration_reason, params[:reason])
|> Map.put(:birthday, params[:birthday])
if Pleroma.Config.get([:instance, :registrations_open]) do if Pleroma.Config.get([:instance, :registrations_open]) do
create_user(params, opts) create_user(params, opts)

View File

@ -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

View File

@ -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

View File

@ -56,7 +56,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes(:u, []) Meta.allow_tag_with_these_attributes(:u, [])
Meta.allow_tag_with_these_attributes(:ul, []) 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_these_attributes(:span, [])
Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"]) Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"])

View File

@ -35,7 +35,8 @@
"alsoKnownAs": { "alsoKnownAs": {
"@id": "as:alsoKnownAs", "@id": "as:alsoKnownAs",
"@type": "@id" "@type": "@id"
} },
"vcard": "http://www.w3.org/2006/vcard/ns#"
} }
] ]
} }

View File

@ -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"}

View File

@ -0,0 +1,14 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://{{domain}}/schemas/litepub-0.1.jsonld",
{
"@language": "und"
}
],
"id": "https://{{domain}}/users/{{nickname}}/collections/featured",
"orderedItems": [
"https://{{domain}}/objects/{{object_id}}"
],
"type": "OrderedCollection"
}

View File

@ -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="
}
}

View File

@ -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": "<span class=\"h-card\"><a class=\"u-url mention\" data-user=\"9v5bmRalQvjOy0ECcC\" href=\"https://gleasonator.com/users/alex\" rel=\"ugc\">@<span>alex</span></a></span> Any idea why my posts are failing? I sent an Accept/Follow from <a href=\"http://macgirvin.com\" rel=\"ugc\">macgirvin.com</a> at 2022-02-02T04:06:01Z and it vanished into space. As do all my comments to you. <br><br>2022-02-02T04:06:01Z:LOG_INFO:d5c4aa7f6a:Queue.php:435:deliver: deliver: queue post returned 200 from <a href=\"https://gleasonator.com/users/macgirvin/inbox\" rel=\"ugc\">https://gleasonator.com/users/macgirvin/inbox</a><br><br>It&#39;s OK if I&#39;m blocked, but if that&#39;s the case, I shouldn&#39;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"
}

View File

@ -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
}

View File

@ -130,7 +130,7 @@ defmodule Pleroma.ReverseProxyTest do
assert capture_log(fn -> assert capture_log(fn ->
ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 30) ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 30)
end) =~ 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
end end

View File

@ -755,6 +755,54 @@ defmodule Pleroma.UserTest do
end end
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 describe "get_or_fetch/1" do
test "gets an existing user by nickname" do test "gets an existing user by nickname" do
user = insert(:user) user = insert(:user)
@ -2084,6 +2132,17 @@ defmodule Pleroma.UserTest do
assert user.ap_id in ap_ids assert user.ap_id in ap_ids
assert user_two.ap_id in ap_ids assert user_two.ap_id in ap_ids
end end
test "it returns a list of AP ids in the same order" do
user = insert(:user)
user_two = insert(:user)
user_three = insert(:user)
ap_ids =
User.get_ap_ids_by_nicknames([user.nickname, user_three.nickname, user_two.nickname])
assert [user.ap_id, user_three.ap_id, user_two.ap_id] == ap_ids
end
end end
describe "sync followers count" do describe "sync followers count" do

View File

@ -312,6 +312,103 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert %{data: %{"id" => ^object_url}} = Object.get_by_ap_id(object_url) assert %{data: %{"id" => ^object_url}} = Object.get_by_ap_id(object_url)
end end
test "fetches user featured collection without embedded object" do
ap_id = "https://example.com/users/lain"
featured_url = "https://example.com/users/lain/collections/featured"
user_data =
"test/fixtures/users_mock/user.json"
|> File.read!()
|> String.replace("{{nickname}}", "lain")
|> Jason.decode!()
|> Map.put("featured", featured_url)
|> Jason.encode!()
object_id = Ecto.UUID.generate()
featured_data =
"test/fixtures/mastodon/collections/external_featured.json"
|> File.read!()
|> String.replace("{{domain}}", "example.com")
|> String.replace("{{nickname}}", "lain")
|> String.replace("{{object_id}}", object_id)
object_url = "https://example.com/objects/#{object_id}"
object_data =
"test/fixtures/statuses/note.json"
|> File.read!()
|> String.replace("{{object_id}}", object_id)
|> String.replace("{{nickname}}", "lain")
Tesla.Mock.mock(fn
%{
method: :get,
url: ^ap_id
} ->
%Tesla.Env{
status: 200,
body: user_data,
headers: [{"content-type", "application/activity+json"}]
}
%{
method: :get,
url: ^featured_url
} ->
%Tesla.Env{
status: 200,
body: featured_data,
headers: [{"content-type", "application/activity+json"}]
}
end)
Tesla.Mock.mock_global(fn
%{
method: :get,
url: ^object_url
} ->
%Tesla.Env{
status: 200,
body: object_data,
headers: [{"content-type", "application/activity+json"}]
}
end)
{:ok, user} = ActivityPub.make_user_from_ap_id(ap_id)
Process.sleep(50)
assert user.featured_address == featured_url
assert Map.has_key?(user.pinned_objects, object_url)
in_db = Pleroma.User.get_by_ap_id(ap_id)
assert in_db.featured_address == featured_url
assert Map.has_key?(user.pinned_objects, object_url)
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 end
test "it fetches the appropriate tag-restricted posts" do test "it fetches the appropriate tag-restricted posts" do

View File

@ -3,22 +3,40 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContentTest do defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContentTest do
alias Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent
import Pleroma.Factory
use Pleroma.DataCase 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 test "adds mentions to post content" do
users = %{ [lain, coolboymew, dielan, hakui, fence] = [
"lain@lain.com" => "https://lain.com/users/lain", insert(:user, ap_id: "https://lain.com/users/lain", nickname: "lain@lain.com", local: false),
"coolboymew@shitposter.club" => "https://shitposter.club/users/coolboymew", insert(:user,
"dielan@shitposter.club" => "https://shitposter.club/users/dielan", ap_id: "https://shitposter.club/users/coolboymew",
"hakui@tuusin.misono-ya.info" => "https://tuusin.misono-ya.info/users/hakui", nickname: "coolboymew@shitposter.club",
"fence@xyzzy.link" => "https://xyzzy.link/users/fence" local: false
} ),
insert(:user,
Enum.each(users, fn {nickname, ap_id} -> ap_id: "https://shitposter.club/users/dielan",
insert(:user, ap_id: ap_id, nickname: nickname, local: false) nickname: "dielan@shitposter.club",
end) 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!() 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) {:ok, %{"object" => %{"content" => filtered}}} = ForceMentionsInContent.filter(activity)
Enum.each(users, fn {nickname, _} -> assert filtered =~ nickname end)
assert filtered ==
"<p><span class=\"recipients-inline\"><span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{dielan.id}\" href=\"https://shitposter.club/users/dielan\" rel=\"ugc\">@<span>dielan</span></a></span> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{coolboymew.id}\" href=\"https://shitposter.club/users/coolboymew\" rel=\"ugc\">@<span>coolboymew</span></a></span> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{fence.id}\" href=\"https://xyzzy.link/users/fence\" rel=\"ugc\">@<span>fence</span></a></span> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{hakui.id}\" href=\"https://tuusin.misono-ya.info/users/hakui\" rel=\"ugc\">@<span>hakui</span></a></span> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{lain.id}\" href=\"https://lain.com/users/lain\" rel=\"ugc\">@<span>lain</span></a></span> </span>Haha yeah, you can control who you reply to.</p>"
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 ==
"<span class=\"recipients-inline\"><span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{luigi.id}\" href=\"#{luigi.ap_id}\" rel=\"ugc\">@<span>luigi</span></a></span> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{mario.id}\" href=\"#{mario.ap_id}\" rel=\"ugc\">@<span>mario</span></a></span> </span>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" => "<p>I'ma tired...</p>",
"to" => [
luigi.ap_id,
Constants.as_public()
],
"inReplyTo" => Object.normalize(post).data["id"]
}
}
{:ok, %{"object" => %{"content" => filtered}}} = ForceMentionsInContent.filter(activity)
assert filtered ==
"<p><span class=\"recipients-inline\"><span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{luigi.id}\" href=\"#{luigi.ap_id}\" rel=\"ugc\">@<span>luigi</span></a></span> </span>I'ma tired...</p>"
end end
end end

View File

@ -32,4 +32,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end end
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 end

View File

@ -0,0 +1,59 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidatorTest do
use Pleroma.DataCase, async: true
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator
alias Pleroma.Web.ActivityPub.Utils
import Pleroma.Factory
test "a Create/Note from Roadhouse validates" do
insert(:user, ap_id: "https://macgirvin.com/channel/mike")
note_activity =
"test/fixtures/roadhouse-create-activity.json"
|> File.read!()
|> Jason.decode!()
# Build metadata
{:ok, object_data} = ObjectValidator.cast_and_apply(note_activity["object"])
meta = [object_data: ObjectValidator.stringify_keys(object_data)]
%{valid?: true} = CreateGenericValidator.cast_and_validate(note_activity, meta)
end
test "a Create/Note with mismatched context is invalid" do
user = insert(:user)
note = %{
"id" => Utils.generate_object_id(),
"type" => "Note",
"actor" => user.ap_id,
"to" => [user.follower_address],
"cc" => [],
"content" => "Hello world",
"context" => Utils.generate_context_id()
}
note_activity = %{
"id" => Utils.generate_activity_id(),
"type" => "Create",
"actor" => note["actor"],
"to" => note["to"],
"cc" => note["cc"],
"object" => note,
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"context" => Utils.generate_context_id()
}
# Build metadata
{:ok, object_data} = ObjectValidator.cast_and_apply(note_activity["object"])
meta = [object_data: ObjectValidator.stringify_keys(object_data)]
%{valid?: false} = CreateGenericValidator.cast_and_validate(note_activity, meta)
end
end

View File

@ -107,6 +107,17 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert activity.data["target"] == new_user.ap_id assert activity.data["target"] == new_user.ap_id
assert activity.data["type"] == "Move" assert activity.data["type"] == "Move"
end end
test "a reply with mismatched context is rejected" do
insert(:user, ap_id: "https://macgirvin.com/channel/mike")
note_activity =
"test/fixtures/roadhouse-create-activity.json"
|> File.read!()
|> Jason.decode!()
assert {:error, _} = Transmogrifier.handle_incoming(note_activity)
end
end end
describe "prepare outgoing" do describe "prepare outgoing" do

View File

@ -355,6 +355,7 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do
} = note } = note
end end
@tag :erratic
test "it returns reports with notes", %{conn: conn, admin: admin} do test "it returns reports with notes", %{conn: conn, admin: admin} do
conn = get(conn, "/api/pleroma/admin/reports") conn = get(conn, "/api/pleroma/admin/reports")

View File

@ -309,7 +309,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
assert capture_log(fn -> assert capture_log(fn ->
assert Utils.date_to_asctime(date) == expected assert Utils.date_to_asctime(date) == expected
end) =~ "[warn] Date #{date} in wrong format, must be ISO 8601" end) =~ "Date #{date} in wrong format, must be ISO 8601"
end end
test "when date is a Unix timestamp" do test "when date is a Unix timestamp" do
@ -319,7 +319,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
assert capture_log(fn -> assert capture_log(fn ->
assert Utils.date_to_asctime(date) == expected assert Utils.date_to_asctime(date) == expected
end) =~ "[warn] Date #{date} in wrong format, must be ISO 8601" end) =~ "Date #{date} in wrong format, must be ISO 8601"
end end
test "when date is nil" do test "when date is nil" do
@ -327,13 +327,13 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
assert capture_log(fn -> assert capture_log(fn ->
assert Utils.date_to_asctime(nil) == expected assert Utils.date_to_asctime(nil) == expected
end) =~ "[warn] Date in wrong format, must be ISO 8601" end) =~ "Date in wrong format, must be ISO 8601"
end end
test "when date is a random string" do test "when date is a random string" do
assert capture_log(fn -> assert capture_log(fn ->
assert Utils.date_to_asctime("foo") == "" assert Utils.date_to_asctime("foo") == ""
end) =~ "[warn] Date foo in wrong format, must be ISO 8601" end) =~ "Date foo in wrong format, must be ISO 8601"
end end
end end

View File

@ -683,6 +683,32 @@ defmodule Pleroma.Web.CommonAPITest do
assert {:ok, _activity} = CommonAPI.post(user, %{status: "12345"}) assert {:ok, _activity} = CommonAPI.post(user, %{status: "12345"})
end end
test "it validates media attachment limits are correctly enforced" do
clear_config([:instance, :max_media_attachments], 4)
user = insert(:user)
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
assert {:error, "Too many attachments"} =
CommonAPI.post(user, %{
status: "",
media_ids: List.duplicate(upload.id, 5)
})
assert {:ok, _activity} =
CommonAPI.post(user, %{
status: "",
media_ids: [upload.id]
})
end
test "it can handle activities that expire" do test "it can handle activities that expire" do
user = insert(:user) user = insert(:user)

View File

@ -5,7 +5,9 @@
defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.InternalFetchActor alias Pleroma.Web.ActivityPub.InternalFetchActor
@ -404,15 +406,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
assert id_two == to_string(activity.id) assert id_two == to_string(activity.id)
end end
test "unimplemented pinned statuses feature", %{conn: conn} do
note = insert(:note_activity)
user = User.get_cached_by_ap_id(note.data["actor"])
conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?pinned=true")
assert json_response_and_validate_schema(conn, 200) == []
end
test "gets an users media, excludes reblogs", %{conn: conn} do test "gets an users media, excludes reblogs", %{conn: conn} do
note = insert(:note_activity) note = insert(:note_activity)
user = User.get_cached_by_ap_id(note.data["actor"]) user = User.get_cached_by_ap_id(note.data["actor"])
@ -1038,6 +1031,35 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
end end
end end
test "view pinned private statuses" do
user = insert(:user)
reader = insert(:user)
# Create a private status and pin it
{:ok, %{id: activity_id} = activity} =
CommonAPI.post(user, %{status: "psst", visibility: "private"})
%{data: %{"id" => object_ap_id}} = Object.normalize(activity)
{:ok, _} = User.add_pinned_object_id(user, object_ap_id)
%{conn: conn} = oauth_access(["read:statuses"], user: reader)
# A non-follower can't see the pinned status
assert [] ==
conn
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|> json_response_and_validate_schema(200)
# Follow the user, then the pinned status can be seen
CommonAPI.follow(reader, user)
ObanHelpers.perform_all()
assert [%{"id" => ^activity_id, "pinned" => true}] =
conn
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|> json_response_and_validate_schema(200)
end
test "blocking / unblocking a user" do test "blocking / unblocking a user" do
%{conn: conn} = oauth_access(["follow"]) %{conn: conn} = oauth_access(["follow"])
other_user = insert(:user) other_user = insert(:user)
@ -1586,6 +1608,60 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
end end
end end
describe "create account with required birth date" do
setup %{conn: conn} do
clear_config([:instance, :birthday_required], true)
clear_config([:instance, :birthday_min_age], 18 * 365)
app_token = insert(:oauth_token, user: nil)
conn =
conn
|> put_req_header("authorization", "Bearer " <> app_token.token)
|> put_req_header("content-type", "multipart/form-data")
[conn: conn]
end
test "creates an account if provided valid birth date", %{conn: conn} do
birthday =
Date.utc_today()
|> Date.add(-19 * 365)
|> Date.to_string()
params = %{
username: "mkljczk",
email: "mkljczk@example.org",
password: "dupa.8",
agreement: true,
birthday: birthday
}
res =
conn
|> post("/api/v1/accounts", params)
assert json_response_and_validate_schema(res, 200)
end
test "returns an error if missing birth date", %{conn: conn} do
params = %{
username: "mkljczk",
email: "mkljczk@example.org",
password: "dupa.8",
agreement: true
}
res =
conn
|> post("/api/v1/accounts", params)
assert json_response_and_validate_schema(res, 400) == %{
"error" => "{\"birthday\":[\"can't be blank\"]}"
}
end
end
describe "GET /api/v1/accounts/:id/lists - account_lists" do describe "GET /api/v1/accounts/:id/lists - account_lists" do
test "returns lists to which the account belongs" do test "returns lists to which the account belongs" do
%{user: user, conn: conn} = oauth_access(["read:lists"]) %{user: user, conn: conn} = oauth_access(["read:lists"])

View File

@ -64,12 +64,13 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
assert response["irreversible"] == false assert response["irreversible"] == false
expires_at = expected_expiration =
NaiveDateTime.utc_now() NaiveDateTime.utc_now()
|> NaiveDateTime.add(in_seconds) |> NaiveDateTime.add(in_seconds)
|> Pleroma.Web.CommonAPI.Utils.to_masto_date()
assert response["expires_at"] == expires_at {:ok, actual_expiration} = NaiveDateTime.from_iso8601(response["expires_at"])
assert abs(NaiveDateTime.diff(expected_expiration, actual_expiration)) <= 5
filter = Filter.get(response["id"], user) filter = Filter.get(response["id"], user)
@ -176,6 +177,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
assert response["whole_word"] == true assert response["whole_word"] == true
end end
@tag :erratic
test "with adding expires_at", %{conn: conn, user: user} do test "with adding expires_at", %{conn: conn, user: user} do
filter = insert(:filter, user: user) filter = insert(:filter, user: user)
in_seconds = 600 in_seconds = 600

View File

@ -370,6 +370,26 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do
] ]
end end
test "updates birth date", %{conn: conn} do
res =
patch(conn, "/api/v1/accounts/update_credentials", %{
"birthday" => "2001-02-12"
})
assert user_data = json_response_and_validate_schema(res, 200)
assert user_data["pleroma"]["birthday"] == "2001-02-12"
end
test "updates the user's show_birthday status", %{conn: conn} do
res =
patch(conn, "/api/v1/accounts/update_credentials", %{
"show_birthday" => true
})
assert user_data = json_response_and_validate_schema(res, 200)
assert user_data["source"]["pleroma"]["show_birthday"] == true
end
test "emojis in fields labels", %{conn: conn} do test "emojis in fields labels", %{conn: conn} do
fields = [ fields = [
%{"name" => ":firefox:", "value" => "is best 2hu"}, %{"name" => ":firefox:", "value" => "is best 2hu"},

View File

@ -494,6 +494,40 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
end end
end end
describe "hiding birthday" do
test "doesn't show birthday if hidden" do
user =
insert(:user, %{
birthday: "2001-02-12",
show_birthday: false
})
other_user = insert(:user)
user = User.get_cached_by_ap_id(user.ap_id)
assert AccountView.render(
"show.json",
%{user: user, for: other_user}
)[:birthday] == nil
end
test "shows hidden birthday to the account owner" do
user =
insert(:user, %{
birthday: "2001-02-12",
show_birthday: false
})
user = User.get_cached_by_ap_id(user.ap_id)
assert AccountView.render(
"show.json",
%{user: user, for: user}
)[:birthday] == nil
end
end
describe "follow requests counter" do describe "follow requests counter" do
test "shows zero when no follow requests are pending" do test "shows zero when no follow requests are pending" do
user = insert(:user) user = insert(:user)

View File

@ -304,4 +304,59 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end end
end end
describe "birthday reminders" do
test "returns a list of friends having birthday on specified day" do
%{user: user, conn: conn} = oauth_access(["read:accounts"])
%{id: id1} =
user1 =
insert(:user, %{
birthday: "2001-02-12",
show_birthday: true
})
user2 =
insert(:user, %{
birthday: "2001-02-14",
show_birthday: true
})
user3 = insert(:user)
CommonAPI.follow(user, user1)
CommonAPI.follow(user, user2)
CommonAPI.follow(user, user3)
[%{"id" => ^id1}] =
conn
|> get("/api/v1/pleroma/birthdays?day=12&month=2")
|> json_response_and_validate_schema(:ok)
end
test "the list doesn't list friends with hidden birth date" do
%{user: user, conn: conn} = oauth_access(["read:accounts"])
user1 =
insert(:user, %{
birthday: "2001-02-12",
show_birthday: false
})
%{id: id2} =
user2 =
insert(:user, %{
birthday: "2001-02-12",
show_birthday: true
})
CommonAPI.follow(user, user1)
CommonAPI.follow(user, user2)
[%{"id" => ^id2}] =
conn
|> get("/api/v1/pleroma/birthdays?day=12&month=2")
|> json_response_and_validate_schema(:ok)
end
end
end end

View File

@ -1311,6 +1311,36 @@ defmodule HttpRequestMock do
}} }}
end end
def get("https://gleasonator.com/objects/102eb097-a18b-4cd5-abfc-f952efcb70bb", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/gleasonator-AG3RzWfwEKKrY63qj2.json"),
headers: activitypub_object_headers()
}}
end
def get("https://gleasonator.com/users/macgirvin", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/macgirvin@gleasonator.com.json"),
headers: activitypub_object_headers()
}}
end
def get("https://gleasonator.com/users/macgirvin/collections/featured", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!("test/fixtures/users_mock/masto_featured.json")
|> String.replace("{{domain}}", "gleasonator.com")
|> String.replace("{{nickname}}", "macgirvin"),
headers: activitypub_object_headers()
}}
end
def get(url, query, body, headers) do def get(url, query, body, headers) do
{:error, {:error,
"Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"} "Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"}