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:
marcin mikołajczak 2022-01-18 14:57:48 +01:00
parent 84dcb55b0f
commit b108b05650
16 changed files with 168 additions and 14 deletions

View File

@ -259,7 +259,9 @@ 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,
birth_date_required: false,
birth_date_min_age: 0
config :pleroma, :welcome, config :pleroma, :welcome,
direct_message: [ direct_message: [

View File

@ -957,6 +957,16 @@ 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: :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."
} }
] ]
}, },

View File

@ -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 @@ defmodule Pleroma.User 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 @@ defmodule Pleroma.User 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 @@ defmodule Pleroma.User 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 @@ 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_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 @@ defmodule Pleroma.User 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)

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()],
birth_day: pos_integer(),
birth_month: pos_integer()
} }
| map() | map()
@ -230,6 +232,18 @@ defmodule Pleroma.User.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

View File

@ -1523,7 +1523,8 @@ 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,
birth_date: data["vcard:bday"]
} }
# nickname can be nil because of virtual actors # nickname can be nil because of virtual actors

View File

@ -92,6 +92,11 @@ defmodule Pleroma.Web.ActivityPub.UserView 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 @@ 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" => 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))

View File

@ -543,7 +543,13 @@ 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"
} },
birth_date: %Schema{
type: :string,
nullable: true,
description: "User's birth date",
format: :date
},
}, },
example: %{ example: %{
"username" => "cofe", "username" => "cofe",
@ -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,
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 @@ 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",
hide_birth_date: true,
birth_date: "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},
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" => [],

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,
: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 @@ 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(:birth_date, params[:birth_date])
# What happens here: # What happens here:
# #

View File

@ -249,6 +249,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountView 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 @@ defmodule Pleroma.Web.MastodonAPI.AccountView 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])

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

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("/birthday_reminders", AccountController, :birthdays)
end end
post("/accounts/confirmation_resend", AccountController, :confirmation_resend) post("/accounts/confirmation_resend", AccountController, :confirmation_resend)

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(: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)

View File

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

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#"
} }
] ]
} }