Birth dates, birthday reminders API, allow instance admins to require minimum age
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
84dcb55b0f
commit
b108b05650
16 changed files with 168 additions and 14 deletions
|
@ -259,7 +259,9 @@
|
||||||
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,
|
||||||
|
birth_date_required: false,
|
||||||
|
birth_date_min_age: 0
|
||||||
|
|
||||||
config :pleroma, :welcome,
|
config :pleroma, :welcome,
|
||||||
direct_message: [
|
direct_message: [
|
||||||
|
|
|
@ -957,6 +957,16 @@
|
||||||
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: :birth_date_required,
|
||||||
|
type: :boolean,
|
||||||
|
description: "Require users to provide birth day."
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :birth_date_min_age,
|
||||||
|
type: :integer,
|
||||||
|
description: "Min age for users to create account. Only makes sense if birth date is required."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -154,6 +154,9 @@ 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(:birth_date, :date)
|
||||||
|
field(:hide_birth_date, :boolean, default: false)
|
||||||
|
|
||||||
|
|
||||||
embeds_one(
|
embeds_one(
|
||||||
:notification_settings,
|
:notification_settings,
|
||||||
|
@ -470,7 +473,8 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
|
||||||
:actor_type,
|
:actor_type,
|
||||||
:also_known_as,
|
:also_known_as,
|
||||||
:accepts_chat_messages,
|
:accepts_chat_messages,
|
||||||
:pinned_objects
|
:pinned_objects,
|
||||||
|
:birth_date
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|> cast(params, [:name], empty_values: [])
|
|> cast(params, [:name], empty_values: [])
|
||||||
|
@ -531,9 +535,11 @@ def update_changeset(struct, params \\ %{}) do
|
||||||
:is_discoverable,
|
:is_discoverable,
|
||||||
:actor_type,
|
:actor_type,
|
||||||
:accepts_chat_messages,
|
:accepts_chat_messages,
|
||||||
:disclose_client
|
:disclose_client,
|
||||||
|
:birth_date
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|> 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 +744,8 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
|
||||||
:password_confirmation,
|
:password_confirmation,
|
||||||
:emoji,
|
:emoji,
|
||||||
:accepts_chat_messages,
|
:accepts_chat_messages,
|
||||||
:registration_reason
|
:registration_reason,
|
||||||
|
:birth_date
|
||||||
])
|
])
|
||||||
|> validate_required([:name, :nickname, :password, :password_confirmation])
|
|> validate_required([:name, :nickname, :password, :password_confirmation])
|
||||||
|> validate_confirmation(:password)
|
|> validate_confirmation(:password)
|
||||||
|
@ -760,6 +767,8 @@ def register_changeset(struct, params \\ %{}, opts \\ []) 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_birth_date
|
||||||
|
|> 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 +785,26 @@ def maybe_validate_required_email(changeset, _) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp maybe_validate_required_birth_date(changeset) do
|
||||||
|
if Config.get([:instance, :birth_date_required]) do
|
||||||
|
validate_required(changeset, [:birth_date])
|
||||||
|
else
|
||||||
|
changeset
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_min_age(changeset) do
|
||||||
|
changeset
|
||||||
|
|> validate_change(:birth_date, fn :birth_date, birth_date ->
|
||||||
|
valid? =
|
||||||
|
Date.utc_today()
|
||||||
|
|> Date.diff(birth_date) >=
|
||||||
|
Config.get([:instance, :birth_date_min_age])
|
||||||
|
|
||||||
|
if valid?, do: [], else: [birth_date: "Invalid birth date"]
|
||||||
|
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)
|
||||||
|
|
|
@ -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()],
|
||||||
|
birth_day: pos_integer(),
|
||||||
|
birth_month: pos_integer()
|
||||||
}
|
}
|
||||||
| map()
|
| map()
|
||||||
|
|
||||||
|
@ -230,6 +232,18 @@ defp compose_query({:internal, false}, query) do
|
||||||
|> where([u], not like(u.nickname, "internal.%"))
|
|> where([u], not like(u.nickname, "internal.%"))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp compose_query({:birth_day, day}, query) do
|
||||||
|
query
|
||||||
|
|> where([u], not is_nil(u.birth_date))
|
||||||
|
|> where([u], fragment("date_part('day', ?)", u.birth_date) == ^day)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp compose_query({:birth_month, month}, query) do
|
||||||
|
query
|
||||||
|
|> where([u], not is_nil(u.birth_date))
|
||||||
|
|> where([u], fragment("date_part('month', ?)", u.birth_date) == ^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
|
||||||
|
|
|
@ -1523,7 +1523,8 @@ defp object_to_user_data(data) 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,
|
||||||
|
birth_date: data["vcard:bday"]
|
||||||
}
|
}
|
||||||
|
|
||||||
# nickname can be nil because of virtual actors
|
# nickname can be nil because of virtual actors
|
||||||
|
|
|
@ -92,6 +92,11 @@ def render("user.json", %{user: user}) do
|
||||||
%{}
|
%{}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
birth_date =
|
||||||
|
if !user.hide_birth_date,
|
||||||
|
do: user.birth_date,
|
||||||
|
else: nil
|
||||||
|
|
||||||
%{
|
%{
|
||||||
"id" => user.ap_id,
|
"id" => user.ap_id,
|
||||||
"type" => user.actor_type,
|
"type" => user.actor_type,
|
||||||
|
@ -116,7 +121,8 @@ def render("user.json", %{user: user}) 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" => birth_date
|
||||||
}
|
}
|
||||||
|> 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))
|
||||||
|
|
|
@ -543,7 +543,13 @@ defp create_request 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"
|
||||||
}
|
},
|
||||||
|
birth_date: %Schema{
|
||||||
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
|
description: "User's birth date",
|
||||||
|
format: :date
|
||||||
|
},
|
||||||
},
|
},
|
||||||
example: %{
|
example: %{
|
||||||
"username" => "cofe",
|
"username" => "cofe",
|
||||||
|
@ -720,7 +726,18 @@ defp update_credentials_request 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,
|
||||||
|
birth_date: %Schema{
|
||||||
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
|
description: "User's birth date",
|
||||||
|
format: :date
|
||||||
|
},
|
||||||
|
hide_birth_date: %Schema{
|
||||||
|
allOf: [BooleanLike],
|
||||||
|
nullable: true,
|
||||||
|
description: "User's birth date will be hidden"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
example: %{
|
example: %{
|
||||||
bot: false,
|
bot: false,
|
||||||
|
@ -740,7 +757,9 @@ defp update_credentials_request 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",
|
||||||
|
hide_birth_date: true,
|
||||||
|
birth_date: "2001-02-12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
@ -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 @@ def unsubscribe_operation 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",
|
||||||
|
|
|
@ -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},
|
||||||
|
birth_date: %Schema{type: :string, 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"
|
||||||
},
|
},
|
||||||
|
hide_birth_date: %Schema{type: :boolean},
|
||||||
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" => %{}
|
||||||
}
|
},
|
||||||
|
"birth_date" => "2001-02-12"
|
||||||
},
|
},
|
||||||
"source" => %{
|
"source" => %{
|
||||||
"fields" => [],
|
"fields" => [],
|
||||||
|
|
|
@ -191,7 +191,8 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p
|
||||||
:skip_thread_containment,
|
:skip_thread_containment,
|
||||||
:allow_following_move,
|
:allow_following_move,
|
||||||
:also_known_as,
|
:also_known_as,
|
||||||
:accepts_chat_messages
|
:accepts_chat_messages,
|
||||||
|
:hide_birth_date
|
||||||
]
|
]
|
||||||
|> 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 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p
|
||||||
|> 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(:birth_date, params[:birth_date])
|
||||||
|
|
||||||
# What happens here:
|
# What happens here:
|
||||||
#
|
#
|
||||||
|
|
|
@ -249,6 +249,11 @@ defp do_render("show.json", %{user: user} = opts) do
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
birth_date =
|
||||||
|
if !user.hide_birth_date or opts[:for] == user,
|
||||||
|
do: user.birth_date,
|
||||||
|
else: nil
|
||||||
|
|
||||||
%{
|
%{
|
||||||
id: to_string(user.id),
|
id: to_string(user.id),
|
||||||
username: username_from_nickname(user.nickname),
|
username: username_from_nickname(user.nickname),
|
||||||
|
@ -297,7 +302,8 @@ defp do_render("show.json", %{user: user} = opts) do
|
||||||
skip_thread_containment: user.skip_thread_containment,
|
skip_thread_containment: user.skip_thread_containment,
|
||||||
background_image: image_url(user.background) |> MediaProxy.url(),
|
background_image: image_url(user.background) |> MediaProxy.url(),
|
||||||
accepts_chat_messages: user.accepts_chat_messages,
|
accepts_chat_messages: user.accepts_chat_messages,
|
||||||
favicon: favicon
|
favicon: favicon,
|
||||||
|
birth_date: birth_date
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|> maybe_put_role(user, opts[:for])
|
|> maybe_put_role(user, opts[:for])
|
||||||
|
|
|
@ -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 @@ def unsubscribe(%{assigns: %{user: user, account: subscription_target}} = conn,
|
||||||
{: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/birthday_reminders"
|
||||||
|
def birthdays(%{assigns: %{user: %User{} = user}} = conn, %{day: day, month: month} = _params) do
|
||||||
|
birthdays =
|
||||||
|
User.Query.build(%{friends: user, deactivated: false, birth_day: day, birth_month: month})
|
||||||
|
|> Pleroma.Repo.all()
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> render("index.json",
|
||||||
|
for: user,
|
||||||
|
users: birthdays,
|
||||||
|
as: :user
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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("/birthday_reminders", AccountController, :birthdays)
|
||||||
end
|
end
|
||||||
|
|
||||||
post("/accounts/confirmation_resend", AccountController, :confirmation_resend)
|
post("/accounts/confirmation_resend", AccountController, :confirmation_resend)
|
||||||
|
|
|
@ -20,6 +20,7 @@ def register_user(params, opts \\ []) 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(:birth_date, params[:birth_date])
|
||||||
|
|
||||||
if Pleroma.Config.get([:instance, :registrations_open]) do
|
if Pleroma.Config.get([:instance, :registrations_open]) do
|
||||||
create_user(params, opts)
|
create_user(params, opts)
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
defmodule Pleroma.Repo.Migrations.AddBirthDateToUsers do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:users) do
|
||||||
|
add_if_not_exists(:birth_date, :date)
|
||||||
|
add_if_not_exists(:hide_birth_date, :boolean, default: false, null: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -35,7 +35,8 @@
|
||||||
"alsoKnownAs": {
|
"alsoKnownAs": {
|
||||||
"@id": "as:alsoKnownAs",
|
"@id": "as:alsoKnownAs",
|
||||||
"@type": "@id"
|
"@type": "@id"
|
||||||
}
|
},
|
||||||
|
"vcard": "http://www.w3.org/2006/vcard/ns#"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue