Spec improvements

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2021-09-27 09:41:36 +02:00
parent cc3106e425
commit 41f086e2c9
No known key found for this signature in database
GPG Key ID: A061B9DDE0CA0773
21 changed files with 299 additions and 90 deletions

View File

@ -25,11 +25,13 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
# Some implementations send the actor URI as the actor field, others send the entire actor object, # Some implementations send the actor URI as the actor field, others send the entire actor object,
# so figure out what the actor's URI is based on what we have. # so figure out what the actor's URI is based on what we have.
@spec get_url(map() | String.t() | list(String.t()) | any()) :: String.t() | nil
def get_url(%{"id" => id}), do: id def get_url(%{"id" => id}), do: id
def get_url(id) when is_binary(id), do: id def get_url(id) when is_binary(id), do: id
def get_url(ids) when is_list(ids), do: get_url(hd(ids)) def get_url(ids) when is_list(ids), do: get_url(hd(ids))
def get_url(_), do: nil def get_url(_), do: nil
@spec make_json_ld_header :: map()
def make_json_ld_header do def make_json_ld_header do
%{ %{
"@context" => [ "@context" => [
@ -99,6 +101,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
} }
end end
@spec make_date :: String.t()
def make_date do def make_date do
DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601() DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
end end
@ -130,6 +133,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
Applies to activities sent by group members from outside this instance to a group of this instance, Applies to activities sent by group members from outside this instance to a group of this instance,
we then need to relay (`Announce`) the object to other members on other instances. we then need to relay (`Announce`) the object to other members on other instances.
""" """
@spec maybe_relay_if_group_activity(Activity.t(), Actor.t() | nil | list(Actor.t())) :: :ok
def maybe_relay_if_group_activity(activity, attributed_to \\ nil) def maybe_relay_if_group_activity(activity, attributed_to \\ nil)
def maybe_relay_if_group_activity( def maybe_relay_if_group_activity(
@ -153,6 +157,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
:ok :ok
end end
@spec do_maybe_relay_if_group_activity(map(), list(String.t()) | String.t()) :: :ok
defp do_maybe_relay_if_group_activity(object, attributed_to) when is_list(attributed_to), defp do_maybe_relay_if_group_activity(object, attributed_to) when is_list(attributed_to),
do: do_maybe_relay_if_group_activity(object, hd(attributed_to)) do: do_maybe_relay_if_group_activity(object, hd(attributed_to))
@ -199,6 +204,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
Adds an id and a published data if they aren't there, Adds an id and a published data if they aren't there,
also adds it to an included object also adds it to an included object
""" """
@spec lazy_put_activity_defaults(map()) :: map()
def lazy_put_activity_defaults(%{"object" => _object} = map) do def lazy_put_activity_defaults(%{"object" => _object} = map) do
if is_map(map["object"]) do if is_map(map["object"]) do
object = lazy_put_object_defaults(map["object"]) object = lazy_put_object_defaults(map["object"])
@ -215,6 +221,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
Map.put_new_lazy(map, "published", &make_date/0) Map.put_new_lazy(map, "published", &make_date/0)
end end
@spec get_actor(map()) :: String.t() | nil
def get_actor(%{"actor" => actor}) when is_binary(actor) do def get_actor(%{"actor" => actor}) when is_binary(actor) do
actor actor
end end
@ -242,6 +249,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
Takes the actor or attributedTo attributes (considers only the first elem if they're an array) Takes the actor or attributedTo attributes (considers only the first elem if they're an array)
""" """
@spec origin_check?(String.t(), map()) :: boolean()
def origin_check?(id, %{"type" => "Tombstone", "id" => tombstone_id}), do: id == tombstone_id def origin_check?(id, %{"type" => "Tombstone", "id" => tombstone_id}), do: id == tombstone_id
def origin_check?(id, %{"actor" => actor, "attributedTo" => _attributed_to} = params) def origin_check?(id, %{"actor" => actor, "attributedTo" => _attributed_to} = params)
@ -283,6 +291,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
compare_uris?(uri_1, uri_2) compare_uris?(uri_1, uri_2)
end end
@spec compare_uris?(URI.t(), URI.t()) :: boolean()
defp compare_uris?(%URI{} = id_uri, %URI{} = other_uri), defp compare_uris?(%URI{} = id_uri, %URI{} = other_uri),
do: id_uri.host == other_uri.host && id_uri.port == other_uri.port do: id_uri.host == other_uri.host && id_uri.port == other_uri.port

View File

@ -8,11 +8,19 @@ defmodule Mobilizon.GraphQL.Error do
alias Mobilizon.Web.Gettext, as: GettextBackend alias Mobilizon.Web.Gettext, as: GettextBackend
import Mobilizon.Web.Gettext, only: [dgettext: 2] import Mobilizon.Web.Gettext, only: [dgettext: 2]
@type t :: %{code: atom(), message: String.t(), status_code: pos_integer(), field: atom()}
defstruct [:code, :message, :status_code, :field] defstruct [:code, :message, :status_code, :field]
@type error :: {:error, any()} | {:error, any(), any(), any()} | atom()
@doc """
Normalize an error to return `t`.
"""
# Error Tuples # Error Tuples
# ------------ # ------------
# Regular errors # Regular errors
@spec normalize(error | list(error) | String.t() | any()) :: t()
def normalize({:error, reason}) do def normalize({:error, reason}) do
handle(reason) handle(reason)
end end

View File

@ -8,18 +8,19 @@ defmodule Mobilizon.Actors.Member do
import Ecto.Changeset import Ecto.Changeset
alias Mobilizon.Actors.{Actor, MemberRole} alias Mobilizon.Actors.{Actor, MemberRole}
alias Mobilizon.Actors.Member.Metadata
alias Mobilizon.Web.Endpoint alias Mobilizon.Web.Endpoint
@type t :: %__MODULE__{ @type t :: %__MODULE__{
role: MemberRole.t(), role: MemberRole.t(),
parent: Actor.t(), parent: Actor.t(),
actor: Actor.t() actor: Actor.t(),
metadata: Metadata.t()
} }
@required_attrs [:parent_id, :actor_id, :url] @required_attrs [:parent_id, :actor_id, :url]
@optional_attrs [:role, :invited_by_id] @optional_attrs [:role, :invited_by_id]
@attrs @required_attrs ++ @optional_attrs @attrs @required_attrs ++ @optional_attrs
@metadata_attrs []
@primary_key {:id, :binary_id, autogenerate: true} @primary_key {:id, :binary_id, autogenerate: true}
schema "members" do schema "members" do
@ -27,9 +28,7 @@ defmodule Mobilizon.Actors.Member do
field(:url, :string) field(:url, :string)
field(:member_since, :utc_datetime) field(:member_since, :utc_datetime)
embeds_one :metadata, Metadata, on_replace: :delete do embeds_one(:metadata, Metadata, on_replace: :delete)
# TODO : Use this space to put notes when someone is invited / requested to join
end
belongs_to(:invited_by, Actor) belongs_to(:invited_by, Actor)
belongs_to(:parent, Actor) belongs_to(:parent, Actor)
@ -63,7 +62,7 @@ defmodule Mobilizon.Actors.Member do
def changeset(%__MODULE__{} = member, attrs) do def changeset(%__MODULE__{} = member, attrs) do
member member
|> cast(attrs, @attrs) |> cast(attrs, @attrs)
|> cast_embed(:metadata, with: &metadata_changeset/2) |> cast_embed(:metadata)
|> ensure_url() |> ensure_url()
|> update_member_since() |> update_member_since()
|> validate_required(@required_attrs) |> validate_required(@required_attrs)
@ -72,11 +71,6 @@ defmodule Mobilizon.Actors.Member do
|> unique_constraint(:url, name: :members_url_index) |> unique_constraint(:url, name: :members_url_index)
end end
defp metadata_changeset(schema, params) do
schema
|> cast(params, @metadata_attrs)
end
# If there's a blank URL that's because we're doing the first insert # If there's a blank URL that's because we're doing the first insert
@spec ensure_url(Ecto.Changeset.t()) :: Ecto.Changeset.t() @spec ensure_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp ensure_url(%Ecto.Changeset{data: %__MODULE__{url: nil}} = changeset) do defp ensure_url(%Ecto.Changeset{data: %__MODULE__{url: nil}} = changeset) do

View File

@ -0,0 +1,27 @@
defmodule Mobilizon.Actors.Member.Metadata do
@moduledoc """
Represents metadata on a membership
"""
use Ecto.Schema
import Ecto.Changeset
@type t :: %__MODULE__{}
@required_attrs []
@optional_attrs []
@attrs @required_attrs ++ @optional_attrs
embedded_schema do
# TODO : Use this space to put notes when someone is invited / requested to join
end
@doc false
@spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t()
def changeset(schema, params) do
schema
|> cast(params, @attrs)
end
end

View File

@ -5,7 +5,7 @@ defmodule Mobilizon.Medias.Media do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset, only: [cast: 3, cast_embed: 2, cast_embed: 3] import Ecto.Changeset, only: [cast: 3, cast_embed: 2]
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Discussions.Comment alias Mobilizon.Discussions.Comment
@ -20,16 +20,12 @@ defmodule Mobilizon.Medias.Media do
actor: Actor.t() actor: Actor.t()
} }
@metadata_attrs [:height, :width, :blurhash] @attrs [:actor_id]
schema "medias" do schema "medias" do
embeds_one(:file, File, on_replace: :update) embeds_one(:file, File, on_replace: :update)
embeds_one :metadata, Metadata, on_replace: :update do embeds_one(:metadata, Metadata, on_replace: :update)
field(:height, :integer)
field(:width, :integer)
field(:blurhash, :string)
end
belongs_to(:actor, Actor) belongs_to(:actor, Actor)
has_many(:event_picture, Event, foreign_key: :picture_id) has_many(:event_picture, Event, foreign_key: :picture_id)
@ -45,15 +41,8 @@ defmodule Mobilizon.Medias.Media do
@spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = media, attrs) do def changeset(%__MODULE__{} = media, attrs) do
media media
|> cast(attrs, [:actor_id]) |> cast(attrs, @attrs)
|> cast_embed(:file) |> cast_embed(:file)
|> cast_embed(:metadata, with: &metadata_changeset/2) |> cast_embed(:metadata)
end
@doc false
@spec metadata_changeset(Metadata.t(), map) :: Ecto.Changeset.t()
def metadata_changeset(metadata, attrs) do
metadata
|> cast(attrs, @metadata_attrs)
end end
end end

View File

@ -0,0 +1,38 @@
defmodule Mobilizon.Medias.Media.Metadata do
@moduledoc """
Represents a media metadata
"""
use Ecto.Schema
import Ecto.Changeset
@type t :: %__MODULE__{
width: non_neg_integer(),
height: non_neg_integer(),
blurhash: String.t()
}
@required_attrs []
@optional_attrs [
:width,
:height,
:blurhash
]
@attrs @required_attrs ++ @optional_attrs
@primary_key false
embedded_schema do
field(:height, :integer)
field(:width, :integer)
field(:blurhash, :string)
end
@doc false
@spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t()
def changeset(schema, params) do
schema
|> cast(params, @attrs)
end
end

View File

@ -10,6 +10,7 @@ defmodule Mobilizon.Resources.Resource do
import EctoEnum import EctoEnum
defenum(TypeEnum, folder: 0, link: 1, picture: 20, pad: 30, calc: 40, visio: 50) defenum(TypeEnum, folder: 0, link: 1, picture: 20, pad: 30, calc: 40, visio: 50)
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Resources.Resource.Metadata
@type t :: %__MODULE__{ @type t :: %__MODULE__{
title: String.t(), title: String.t(),
@ -17,7 +18,7 @@ defmodule Mobilizon.Resources.Resource do
url: String.t(), url: String.t(),
resource_url: String.t(), resource_url: String.t(),
type: atom(), type: atom(),
metadata: Mobilizon.Resources.Resource.Metadata.t(), metadata: Metadata.t(),
children: list(__MODULE__), children: list(__MODULE__),
parent: __MODULE__, parent: __MODULE__,
actor: Actor.t(), actor: Actor.t(),
@ -37,20 +38,7 @@ defmodule Mobilizon.Resources.Resource do
field(:local, :boolean, default: true) field(:local, :boolean, default: true)
field(:published_at, :utc_datetime) field(:published_at, :utc_datetime)
embeds_one :metadata, Metadata, on_replace: :delete do embeds_one(:metadata, Metadata, on_replace: :delete)
field(:type, :string)
field(:title, :string)
field(:description, :string)
field(:image_remote_url, :string)
field(:width, :integer)
field(:height, :integer)
field(:author_name, :string)
field(:author_url, :string)
field(:provider_name, :string)
field(:provider_url, :string)
field(:html, :string)
field(:favicon_url, :string)
end
has_many(:children, __MODULE__, foreign_key: :parent_id) has_many(:children, __MODULE__, foreign_key: :parent_id)
belongs_to(:parent, __MODULE__, type: :binary_id) belongs_to(:parent, __MODULE__, type: :binary_id)
@ -63,27 +51,13 @@ defmodule Mobilizon.Resources.Resource do
@required_attrs [:title, :url, :actor_id, :creator_id, :type, :path, :published_at] @required_attrs [:title, :url, :actor_id, :creator_id, :type, :path, :published_at]
@optional_attrs [:summary, :parent_id, :resource_url, :local] @optional_attrs [:summary, :parent_id, :resource_url, :local]
@attrs @required_attrs ++ @optional_attrs @attrs @required_attrs ++ @optional_attrs
@metadata_attrs [
:type,
:title,
:description,
:image_remote_url,
:width,
:height,
:author_name,
:author_url,
:provider_name,
:provider_url,
:html,
:favicon_url
]
@doc false @doc false
@spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t()
def changeset(resource, attrs) do def changeset(resource, attrs) do
resource resource
|> cast(attrs, @attrs) |> cast(attrs, @attrs)
|> cast_embed(:metadata, with: &metadata_changeset/2) |> cast_embed(:metadata)
|> ensure_url(:resource) |> ensure_url(:resource)
|> maybe_add_published_at() |> maybe_add_published_at()
|> validate_resource_or_folder() |> validate_resource_or_folder()
@ -91,11 +65,6 @@ defmodule Mobilizon.Resources.Resource do
|> unique_constraint(:url, name: :resource_url_index) |> unique_constraint(:url, name: :resource_url_index)
end end
defp metadata_changeset(schema, params) do
schema
|> cast(params, @metadata_attrs)
end
@spec validate_resource_or_folder(Changeset.t()) :: Changeset.t() @spec validate_resource_or_folder(Changeset.t()) :: Changeset.t()
defp validate_resource_or_folder(%Changeset{} = changeset) do defp validate_resource_or_folder(%Changeset{} = changeset) do
with {status, type} when status in [:changes, :data] <- fetch_field(changeset, :type), with {status, type} when status in [:changes, :data] <- fetch_field(changeset, :type),

View File

@ -0,0 +1,64 @@
defmodule Mobilizon.Resources.Resource.Metadata do
@moduledoc """
Represents a resource metadata
"""
use Ecto.Schema
import Ecto.Changeset
@type t :: %__MODULE__{
type: String.t(),
title: String.t(),
image_remote_url: String.t(),
width: non_neg_integer(),
height: non_neg_integer(),
author_name: String.t(),
author_url: String.t(),
provider_name: String.t(),
provider_url: String.t(),
html: String.t(),
favicon_url: String.t()
}
@required_attrs []
@optional_attrs [
:type,
:title,
:description,
:image_remote_url,
:width,
:height,
:author_name,
:author_url,
:provider_name,
:provider_url,
:html,
:favicon_url
]
@attrs @required_attrs ++ @optional_attrs
@primary_key false
embedded_schema do
field(:type, :string)
field(:title, :string)
field(:description, :string)
field(:image_remote_url, :string)
field(:width, :integer)
field(:height, :integer)
field(:author_name, :string)
field(:author_url, :string)
field(:provider_name, :string)
field(:provider_url, :string)
field(:html, :string)
field(:favicon_url, :string)
end
@doc false
@spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t()
def changeset(schema, params) do
schema
|> cast(params, @attrs)
end
end

View File

@ -6,6 +6,7 @@ defmodule Mobilizon.Users.Setting do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias Mobilizon.Users.{NotificationPendingNotificationDelay, User} alias Mobilizon.Users.{NotificationPendingNotificationDelay, User}
alias Mobilizon.Users.Setting.Location
@type t :: %__MODULE__{ @type t :: %__MODULE__{
timezone: String.t(), timezone: String.t(),
@ -40,8 +41,6 @@ defmodule Mobilizon.Users.Setting do
@attrs @required_attrs ++ @optional_attrs @attrs @required_attrs ++ @optional_attrs
@location_attrs [:name, :range, :geohash]
@primary_key {:user_id, :id, autogenerate: false} @primary_key {:user_id, :id, autogenerate: false}
schema "user_settings" do schema "user_settings" do
field(:timezone, :string) field(:timezone, :string)
@ -60,11 +59,7 @@ defmodule Mobilizon.Users.Setting do
field(:group_notifications, NotificationPendingNotificationDelay, default: :one_day) field(:group_notifications, NotificationPendingNotificationDelay, default: :one_day)
field(:last_notification_sent, :utc_datetime) field(:last_notification_sent, :utc_datetime)
embeds_one :location, Location, on_replace: :update, primary_key: false do embeds_one(:location, Location, on_replace: :update)
field(:name, :string)
field(:range, :integer)
field(:geohash, :string)
end
belongs_to(:user, User, primary_key: true, type: :id, foreign_key: :id, define_field: false) belongs_to(:user, User, primary_key: true, type: :id, foreign_key: :id, define_field: false)
@ -76,13 +71,7 @@ defmodule Mobilizon.Users.Setting do
def changeset(setting, attrs) do def changeset(setting, attrs) do
setting setting
|> cast(attrs, @attrs) |> cast(attrs, @attrs)
|> cast_embed(:location, with: &location_changeset/2) |> cast_embed(:location)
|> validate_required(@required_attrs) |> validate_required(@required_attrs)
end end
@spec location_changeset(location, map) :: Ecto.Changeset.t()
def location_changeset(schema, params) do
schema
|> cast(params, @location_attrs)
end
end end

View File

@ -0,0 +1,38 @@
defmodule Mobilizon.Users.Setting.Location do
@moduledoc """
Represents user location information
"""
use Ecto.Schema
import Ecto.Changeset
@type t :: %__MODULE__{
name: String.t(),
range: non_neg_integer(),
geohash: String.t()
}
@required_attrs []
@optional_attrs [
:name,
:range,
:geohash
]
@attrs @required_attrs ++ @optional_attrs
@primary_key false
embedded_schema do
field(:name, :string)
field(:range, :integer)
field(:geohash, :string)
end
@doc false
@spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t()
def changeset(schema, params) do
schema
|> cast(params, @attrs)
end
end

View File

@ -62,9 +62,9 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do
static_url = Mobilizon.Web.Endpoint.static_url() static_url = Mobilizon.Web.Endpoint.static_url()
websocket_url = Mobilizon.Web.Endpoint.websocket_url() websocket_url = Mobilizon.Web.Endpoint.websocket_url()
img_src = [@img_src | get_csp_config(:img_src, options)] img_src = [@img_src] ++ [get_csp_config(:img_src, options)]
media_src = [@media_src | get_csp_config(:media_src, options)] media_src = [@media_src] ++ [get_csp_config(:media_src, options)]
connect_src = [ connect_src = [
@connect_src, @connect_src,
@ -85,22 +85,22 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do
] ]
end end
script_src = [script_src | get_csp_config(:script_src, options)] script_src = [script_src] ++ [get_csp_config(:script_src, options)]
style_src = style_src =
if Config.get(:env) == :dev, do: [@style_src | "'unsafe-inline' "], else: @style_src if Config.get(:env) == :dev, do: [@style_src | "'unsafe-inline' "], else: @style_src
style_src = [style_src | get_csp_config(:style_src, options)] style_src = [style_src] ++ [get_csp_config(:style_src, options)]
font_src = [@font_src | get_csp_config(:font_src, options)] font_src = [@font_src] ++ [get_csp_config(:font_src, options)]
frame_src = if Config.get(:env) == :dev, do: "frame-src 'self' ", else: "frame-src 'none' " frame_src = if Config.get(:env) == :dev, do: "frame-src 'self' ", else: "frame-src 'none' "
frame_src = [frame_src | get_csp_config(:frame_src, options)] frame_src = [frame_src] ++ [get_csp_config(:frame_src, options)]
frame_ancestors = frame_ancestors =
if Config.get(:env) == :dev, do: "frame-ancestors 'self' ", else: "frame-ancestors 'none' " if Config.get(:env) == :dev, do: "frame-ancestors 'self' ", else: "frame-ancestors 'none' "
frame_ancestors = [frame_ancestors | get_csp_config(:frame_ancestors, options)] frame_ancestors = [frame_ancestors] ++ [get_csp_config(:frame_ancestors, options)]
insecure = if scheme == "https", do: "upgrade-insecure-requests" insecure = if scheme == "https", do: "upgrade-insecure-requests"

View File

@ -11,8 +11,10 @@ defmodule Mobilizon.Web.Plugs.SetLocalePlug do
import Plug.Conn, only: [assign: 3] import Plug.Conn, only: [assign: 3]
alias Mobilizon.Web.Gettext, as: GettextBackend alias Mobilizon.Web.Gettext, as: GettextBackend
@spec init(any()) :: nil
def init(_), do: nil def init(_), do: nil
@spec call(Plug.Conn.t(), any()) :: Plug.Conn.t()
def call(conn, _) do def call(conn, _) do
locale = locale =
[ [
@ -29,17 +31,22 @@ defmodule Mobilizon.Web.Plugs.SetLocalePlug do
assign(conn, :locale, locale) assign(conn, :locale, locale)
end end
@spec supported_locale?(String.t()) :: boolean()
defp supported_locale?(locale) do defp supported_locale?(locale) do
GettextBackend GettextBackend
|> Gettext.known_locales() |> Gettext.known_locales()
|> Enum.member?(locale) |> Enum.member?(locale)
end end
@spec default_locale :: String.t()
defp default_locale do defp default_locale do
Keyword.get(Mobilizon.Config.instance_config(), :default_language, "en") Keyword.get(Mobilizon.Config.instance_config(), :default_language, "en")
end end
@spec determine_best_locale(String.t()) :: String.t() @doc """
Determine the best available locale for a given locale ID
"""
@spec determine_best_locale(String.t()) :: String.t() | nil
def determine_best_locale(locale) when is_binary(locale) do def determine_best_locale(locale) when is_binary(locale) do
locale = String.trim(locale) locale = String.trim(locale)
locales = Gettext.known_locales(GettextBackend) locales = Gettext.known_locales(GettextBackend)
@ -58,5 +65,6 @@ defmodule Mobilizon.Web.Plugs.SetLocalePlug do
def determine_best_locale(_), do: nil def determine_best_locale(_), do: nil
# Keep only the first part of the locale # Keep only the first part of the locale
@spec split_locale(String.t()) :: String.t()
defp split_locale(locale), do: locale |> String.split("_", trim: true, parts: 2) |> hd defp split_locale(locale), do: locale |> String.split("_", trim: true, parts: 2) |> hd
end end

View File

@ -165,6 +165,11 @@ defmodule Mobilizon.Web.ReverseProxy do
|> halt() |> halt()
end end
@spec request(String.t(), String.t(), list(tuple()), Keyword.t()) ::
{:ok, 200 | 206 | 304, list(tuple()), any()}
| {:ok, 200 | 206 | 304, list(tuple())}
| {:error, {:invalid_http_response, pos_integer()}}
| {:error, any()}
defp request(method, url, headers, hackney_opts) do defp request(method, url, headers, hackney_opts) do
Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}") Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")
method = method |> String.downcase() |> String.to_existing_atom() method = method |> String.downcase() |> String.to_existing_atom()
@ -184,6 +189,8 @@ defmodule Mobilizon.Web.ReverseProxy do
end end
end end
@spec response(Plug.Conn.t(), any(), String.t(), pos_integer(), list(tuple()), Keyword.t()) ::
Plug.Conn.t()
defp response(conn, client, url, status, headers, opts) do defp response(conn, client, url, status, headers, opts) do
result = result =
conn conn
@ -209,18 +216,26 @@ defmodule Mobilizon.Web.ReverseProxy do
end end
end end
@spec chunk_reply(
Plug.Conn.t(),
any(),
Keyword.t(),
non_neg_integer(),
non_neg_integer() | :no_duration_limit
) ::
{:ok, Plug.Conn.t()} | {:error, any(), Plug.Conn.t()}
defp chunk_reply(conn, client, opts) do defp chunk_reply(conn, client, opts) do
chunk_reply(conn, client, opts, 0, 0) chunk_reply(conn, client, opts, 0, 0)
end end
defp chunk_reply(conn, client, opts, sent_so_far, duration) do defp chunk_reply(conn, client, opts, sent_so_far, duration) do
with {:ok, duration} <- with {:ok, {duration, now}} <-
check_read_duration( check_read_duration(
duration, duration,
Keyword.get(opts, :max_read_duration, @max_read_duration) Keyword.get(opts, :max_read_duration, @max_read_duration)
), ),
{:ok, data} <- @hackney.stream_body(client), {:ok, data} <- @hackney.stream_body(client),
{:ok, duration} <- increase_read_duration(duration), {:ok, duration} <- increase_read_duration({duration, now}),
sent_so_far = sent_so_far + byte_size(data), sent_so_far = sent_so_far + byte_size(data),
:ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)), :ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)),
{:ok, conn} <- chunk(conn, data) do {:ok, conn} <- chunk(conn, data) do
@ -231,6 +246,8 @@ defmodule Mobilizon.Web.ReverseProxy do
end end
end end
@spec head_response(Plug.Conn.t(), any(), pos_integer(), list(tuple()), Keyword.t()) ::
Plug.Conn.t() | no_return()
defp head_response(conn, _url, code, headers, opts) do defp head_response(conn, _url, code, headers, opts) do
conn conn
|> put_resp_headers(build_resp_headers(headers, opts)) |> put_resp_headers(build_resp_headers(headers, opts))
@ -238,6 +255,8 @@ defmodule Mobilizon.Web.ReverseProxy do
end end
# sobelow_skip ["XSS.SendResp"] # sobelow_skip ["XSS.SendResp"]
@spec error_or_redirect(Plug.Conn.t(), String.t(), pos_integer(), String.t(), Keyword.t()) ::
Plug.Conn.t()
defp error_or_redirect(conn, url, code, body, opts) do defp error_or_redirect(conn, url, code, body, opts) do
if Keyword.get(opts, :redirect_on_failure, false) do if Keyword.get(opts, :redirect_on_failure, false) do
conn conn
@ -250,12 +269,14 @@ defmodule Mobilizon.Web.ReverseProxy do
end end
end end
@spec downcase_headers(list(tuple())) :: list(tuple())
defp downcase_headers(headers) do defp downcase_headers(headers) do
Enum.map(headers, fn {k, v} -> Enum.map(headers, fn {k, v} ->
{String.downcase(k), v} {String.downcase(k), v}
end) end)
end end
@spec get_content_type(list(tuple())) :: String.t()
defp get_content_type(headers) do defp get_content_type(headers) do
{_, content_type} = {_, content_type} =
List.keyfind(headers, "content-type", 0, {"content-type", "application/octet-stream"}) List.keyfind(headers, "content-type", 0, {"content-type", "application/octet-stream"})
@ -264,12 +285,14 @@ defmodule Mobilizon.Web.ReverseProxy do
content_type content_type
end end
@spec put_resp_headers(Plug.Conn.t(), list(tuple())) :: Plug.Conn.t()
defp put_resp_headers(conn, headers) do defp put_resp_headers(conn, headers) do
Enum.reduce(headers, conn, fn {k, v}, conn -> Enum.reduce(headers, conn, fn {k, v}, conn ->
put_resp_header(conn, k, v) put_resp_header(conn, k, v)
end) end)
end end
@spec build_req_headers(list(tuple()), Keyword.t()) :: list(tuple())
defp build_req_headers(headers, opts) do defp build_req_headers(headers, opts) do
headers headers
|> downcase_headers() |> downcase_headers()
@ -290,6 +313,7 @@ defmodule Mobilizon.Web.ReverseProxy do
end).() end).()
end end
@spec build_resp_headers(list(tuple()), Keyword.t()) :: list(tuple())
defp build_resp_headers(headers, opts) do defp build_resp_headers(headers, opts) do
headers headers
|> Enum.filter(fn {k, _} -> k in @keep_resp_headers end) |> Enum.filter(fn {k, _} -> k in @keep_resp_headers end)
@ -298,6 +322,7 @@ defmodule Mobilizon.Web.ReverseProxy do
|> (fn headers -> headers ++ Keyword.get(opts, :resp_headers, []) end).() |> (fn headers -> headers ++ Keyword.get(opts, :resp_headers, []) end).()
end end
@spec build_resp_cache_headers(list(tuple()), Keyword.t()) :: list(tuple())
defp build_resp_cache_headers(headers, _opts) do defp build_resp_cache_headers(headers, _opts) do
has_cache? = Enum.any?(headers, fn {k, _} -> k in @resp_cache_headers end) has_cache? = Enum.any?(headers, fn {k, _} -> k in @resp_cache_headers end)
has_cache_control? = List.keymember?(headers, "cache-control", 0) has_cache_control? = List.keymember?(headers, "cache-control", 0)
@ -321,6 +346,7 @@ defmodule Mobilizon.Web.ReverseProxy do
end end
end end
@spec build_resp_content_disposition_header(list(tuple()), Keyword.t()) :: list(tuple())
defp build_resp_content_disposition_header(headers, opts) do defp build_resp_content_disposition_header(headers, opts) do
opt = Keyword.get(opts, :inline_content_types, @inline_content_types) opt = Keyword.get(opts, :inline_content_types, @inline_content_types)
@ -359,6 +385,8 @@ defmodule Mobilizon.Web.ReverseProxy do
end end
end end
@spec header_length_constraint(list(tuple()), non_neg_integer()) ::
:ok | {:error, :body_too_large}
defp header_length_constraint(headers, limit) when is_integer(limit) and limit > 0 do defp header_length_constraint(headers, limit) when is_integer(limit) and limit > 0 do
with {_, size} <- List.keyfind(headers, "content-length", 0), with {_, size} <- List.keyfind(headers, "content-length", 0),
{size, _} <- Integer.parse(size), {size, _} <- Integer.parse(size),
@ -375,15 +403,16 @@ defmodule Mobilizon.Web.ReverseProxy do
defp header_length_constraint(_, _), do: :ok defp header_length_constraint(_, _), do: :ok
@spec body_size_constraint(integer(), integer()) :: :ok | {:error, :body_too_large}
defp body_size_constraint(size, limit) when is_integer(limit) and limit > 0 and size >= limit do defp body_size_constraint(size, limit) when is_integer(limit) and limit > 0 and size >= limit do
{:error, :body_too_large} {:error, :body_too_large}
end end
defp body_size_constraint(_, _), do: :ok defp body_size_constraint(_, _), do: :ok
@spec check_read_duration(any(), integer()) :: @spec check_read_duration(any(), integer() | :no_duration_limit) ::
{:ok, {integer(), integer()}} {:ok, {integer(), integer()}}
| {:ok, :no_duration_limit, :no_duration_limit} | {:ok, {:no_duration_limit, :no_duration_limit}}
| {:error, :read_duration_exceeded} | {:error, :read_duration_exceeded}
defp check_read_duration(duration, max) defp check_read_duration(duration, max)
when is_integer(duration) and is_integer(max) and max > 0 do when is_integer(duration) and is_integer(max) and max > 0 do
@ -394,8 +423,12 @@ defmodule Mobilizon.Web.ReverseProxy do
end end
end end
defp check_read_duration(_, _), do: {:ok, :no_duration_limit, :no_duration_limit} defp check_read_duration(_, _), do: {:ok, {:no_duration_limit, :no_duration_limit}}
@spec increase_read_duration(
{previous_duration :: pos_integer | :no_duration_limit,
started :: pos_integer | :no_duration_limit}
) :: {:ok, pos_integer()} | {:ok, :no_duration_limit}
defp increase_read_duration({previous_duration, started}) defp increase_read_duration({previous_duration, started})
when is_integer(previous_duration) and is_integer(started) do when is_integer(previous_duration) and is_integer(started) do
duration = :erlang.system_time(:millisecond) - started duration = :erlang.system_time(:millisecond) - started
@ -403,9 +436,10 @@ defmodule Mobilizon.Web.ReverseProxy do
end end
defp increase_read_duration(_) do defp increase_read_duration(_) do
{:ok, :no_duration_limit, :no_duration_limit} {:ok, :no_duration_limit}
end end
@spec filename(String.t()) :: String.t() | nil
def filename(url_or_path) do def filename(url_or_path) do
if path = URI.parse(url_or_path).path, do: Path.basename(path) if path = URI.parse(url_or_path).path, do: Path.basename(path)
end end

View File

@ -17,6 +17,7 @@ defmodule Mobilizon.Web.ActivityPub.ActorView do
@json_ld_header Utils.make_json_ld_header() @json_ld_header Utils.make_json_ld_header()
@selected_member_roles ~w(creator administrator moderator member)a @selected_member_roles ~w(creator administrator moderator member)a
@spec render(String.t(), map()) :: map()
def render("actor.json", %{actor: actor}) do def render("actor.json", %{actor: actor}) do
actor actor
|> Convertible.model_to_as() |> Convertible.model_to_as()

View File

@ -3,6 +3,7 @@ defmodule Mobilizon.Web.ActivityPub.ObjectView do
alias Mobilizon.Federation.ActivityPub.{Activity, Utils} alias Mobilizon.Federation.ActivityPub.{Activity, Utils}
@spec render(String.t(), map()) :: map()
def render("activity.json", %{activity: %Activity{local: local, data: data} = activity}) do def render("activity.json", %{activity: %Activity{local: local, data: data} = activity}) do
%{ %{
"id" => data["id"], "id" => data["id"],

View File

@ -8,6 +8,7 @@ defmodule Mobilizon.Web.AuthView do
alias Phoenix.HTML.Tag alias Phoenix.HTML.Tag
import Mobilizon.Web.Views.Utils import Mobilizon.Web.Views.Utils
@spec render(String.t(), map()) :: String.t() | Plug.Conn.t()
def render("callback.html", %{ def render("callback.html", %{
conn: conn, conn: conn,
access_token: access_token, access_token: access_token,

View File

@ -8,6 +8,7 @@ defmodule Mobilizon.Web.ErrorHelpers do
@doc """ @doc """
Translates an error message using gettext. Translates an error message using gettext.
""" """
@spec translate_error({msg :: String.t(), opts :: map()}) :: String.t()
def translate_error({msg, opts}) do def translate_error({msg, opts}) do
# Because error messages were defined within Ecto, we must # Because error messages were defined within Ecto, we must
# call the Gettext module passing our Gettext backend. We # call the Gettext module passing our Gettext backend. We

View File

@ -6,6 +6,7 @@ defmodule Mobilizon.Web.ErrorView do
alias Mobilizon.Service.Metadata.Instance alias Mobilizon.Service.Metadata.Instance
import Mobilizon.Web.Views.Utils import Mobilizon.Web.Views.Utils
@spec render(String.t(), map()) :: map() | String.t() | Plug.Conn.t()
def render("404.html", %{conn: conn}) do def render("404.html", %{conn: conn}) do
with tags <- Instance.build_tags(), with tags <- Instance.build_tags(),
{:ok, html} <- inject_tags(tags, get_locale(conn)) do {:ok, html} <- inject_tags(tags, get_locale(conn)) do

View File

@ -8,6 +8,7 @@ defmodule Mobilizon.Web.JsonLD.ObjectView do
alias Mobilizon.Web.Endpoint alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.JsonLD.ObjectView alias Mobilizon.Web.JsonLD.ObjectView
@spec render(String.t(), map()) :: map()
def render("group.json", %{group: %Actor{} = group}) do def render("group.json", %{group: %Actor{} = group}) do
%{ %{
"@context" => "http://schema.org", "@context" => "http://schema.org",
@ -93,6 +94,7 @@ defmodule Mobilizon.Web.JsonLD.ObjectView do
} }
end end
@spec render_location(map()) :: map() | nil
defp render_location(%{physical_address: %Address{} = address}), defp render_location(%{physical_address: %Address{} = address}),
do: render_one(address, ObjectView, "place.json", as: :address) do: render_one(address, ObjectView, "place.json", as: :address)

View File

@ -19,6 +19,8 @@ defmodule Mobilizon.Web.PageView do
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
import Mobilizon.Web.Views.Utils import Mobilizon.Web.Views.Utils
@doc false
@spec render(String.t(), %{conn: Plug.Conn.t()}) :: map() | String.t() | Plug.Conn.t()
def render("actor.activity-json", %{conn: %{assigns: %{object: %Actor{} = actor}}}) do def render("actor.activity-json", %{conn: %{assigns: %{object: %Actor{} = actor}}}) do
actor actor
|> Convertible.model_to_as() |> Convertible.model_to_as()

View File

@ -11,6 +11,7 @@ defmodule Mobilizon.Factory do
alias Mobilizon.Web.{Endpoint, Upload} alias Mobilizon.Web.{Endpoint, Upload}
alias Mobilizon.Web.Router.Helpers, as: Routes alias Mobilizon.Web.Router.Helpers, as: Routes
@spec user_factory :: Mobilizon.Users.User.t()
def user_factory do def user_factory do
%Mobilizon.Users.User{ %Mobilizon.Users.User{
password_hash: "Jane Smith", password_hash: "Jane Smith",
@ -23,6 +24,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec settings_factory :: Mobilizon.Users.Setting.t()
def settings_factory do def settings_factory do
%Mobilizon.Users.Setting{ %Mobilizon.Users.Setting{
timezone: nil, timezone: nil,
@ -37,6 +39,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec actor_factory :: Mobilizon.Actors.Actor.t()
def actor_factory do def actor_factory do
preferred_username = sequence("thomas") preferred_username = sequence("thomas")
@ -62,6 +65,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec group_factory :: Mobilizon.Actors.Actor.t()
def group_factory do def group_factory do
preferred_username = sequence("myGroup") preferred_username = sequence("myGroup")
@ -82,6 +86,7 @@ defmodule Mobilizon.Factory do
) )
end end
@spec instance_actor_factory :: Mobilizon.Actors.Actor.t()
def instance_actor_factory do def instance_actor_factory do
preferred_username = "relay" preferred_username = "relay"
domain = "#{sequence("mydomain")}.com" domain = "#{sequence("mydomain")}.com"
@ -104,6 +109,7 @@ defmodule Mobilizon.Factory do
) )
end end
@spec follower_factory :: Mobilizon.Actors.Follower.t()
def follower_factory do def follower_factory do
uuid = Ecto.UUID.generate() uuid = Ecto.UUID.generate()
@ -116,6 +122,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec tag_factory :: Mobilizon.Events.Tag.t()
def tag_factory do def tag_factory do
%Mobilizon.Events.Tag{ %Mobilizon.Events.Tag{
title: sequence("MyTag"), title: sequence("MyTag"),
@ -123,6 +130,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec tag_relation_factory :: Mobilizon.Events.TagRelation.t()
def tag_relation_factory do def tag_relation_factory do
%Mobilizon.Events.TagRelation{ %Mobilizon.Events.TagRelation{
tag: build(:tag), tag: build(:tag),
@ -130,6 +138,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec address_factory :: Mobilizon.Addresses.Address.t()
def address_factory do def address_factory do
%Mobilizon.Addresses.Address{ %Mobilizon.Addresses.Address{
description: sequence("MyAddress"), description: sequence("MyAddress"),
@ -143,6 +152,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec comment_factory :: Mobilizon.Discussions.Comment.t()
def comment_factory do def comment_factory do
uuid = Ecto.UUID.generate() uuid = Ecto.UUID.generate()
@ -165,6 +175,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec event_factory :: Mobilizon.Events.Event.t()
def event_factory do def event_factory do
actor = build(:actor) actor = build(:actor)
start = Timex.shift(DateTime.utc_now(), hours: 2) start = Timex.shift(DateTime.utc_now(), hours: 2)
@ -198,6 +209,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec participant_factory :: Mobilizon.Events.Participant.t()
def participant_factory do def participant_factory do
uuid = Ecto.UUID.generate() uuid = Ecto.UUID.generate()
@ -214,6 +226,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec session_factory :: Mobilizon.Events.Session.t()
def session_factory do def session_factory do
%Mobilizon.Events.Session{ %Mobilizon.Events.Session{
title: sequence("MySession"), title: sequence("MySession"),
@ -222,6 +235,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec track_factory :: Mobilizon.Events.Track.t()
def track_factory do def track_factory do
%Mobilizon.Events.Track{ %Mobilizon.Events.Track{
name: sequence("MyTrack"), name: sequence("MyTrack"),
@ -229,6 +243,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec bot_factory :: Mobilizon.Actors.Bot.t()
def bot_factory do def bot_factory do
%Mobilizon.Actors.Bot{ %Mobilizon.Actors.Bot{
source: "https://mysource.tld/feed.ics", source: "https://mysource.tld/feed.ics",
@ -238,6 +253,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec member_factory :: Mobilizon.Actors.Member.t()
def member_factory do def member_factory do
uuid = Ecto.UUID.generate() uuid = Ecto.UUID.generate()
@ -250,6 +266,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec feed_token_factory :: Mobilizon.Events.FeedToken.t()
def feed_token_factory do def feed_token_factory do
user = build(:user) user = build(:user)
@ -260,6 +277,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec file_factory :: Mobilizon.Medias.File.t()
def file_factory do def file_factory do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
@ -285,6 +303,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec media_factory :: Mobilizon.Medias.Media.t()
def media_factory do def media_factory do
%Mobilizon.Medias.Media{ %Mobilizon.Medias.Media{
file: build(:file), file: build(:file),
@ -292,6 +311,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec report_factory :: Mobilizon.Reports.Report.t()
def report_factory do def report_factory do
%Mobilizon.Reports.Report{ %Mobilizon.Reports.Report{
content: "This is problematic", content: "This is problematic",
@ -304,6 +324,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec report_note_factory :: Mobilizon.Reports.Note.t()
def report_note_factory do def report_note_factory do
%Mobilizon.Reports.Note{ %Mobilizon.Reports.Note{
content: "My opinion", content: "My opinion",
@ -312,6 +333,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec todo_list_factory :: Mobilizon.Todos.TodoList.t()
def todo_list_factory do def todo_list_factory do
uuid = Ecto.UUID.generate() uuid = Ecto.UUID.generate()
@ -324,6 +346,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec todo_factory :: Mobilizon.Todos.Todo.t()
def todo_factory do def todo_factory do
uuid = Ecto.UUID.generate() uuid = Ecto.UUID.generate()
@ -340,6 +363,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec resource_factory :: Mobilizon.Resources.Resource.t()
def resource_factory do def resource_factory do
uuid = Ecto.UUID.generate() uuid = Ecto.UUID.generate()
title = sequence("my resource") title = sequence("my resource")
@ -358,6 +382,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec admin_setting_factory :: Mobilizon.Admin.Setting.t()
def admin_setting_factory do def admin_setting_factory do
%Mobilizon.Admin.Setting{ %Mobilizon.Admin.Setting{
group: sequence("group"), group: sequence("group"),
@ -366,6 +391,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec post_factory :: Mobilizon.Posts.Post.t()
def post_factory do def post_factory do
uuid = Ecto.UUID.generate() uuid = Ecto.UUID.generate()
@ -386,6 +412,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec tombstone_factory :: Mobilizon.Tombstone.t()
def tombstone_factory do def tombstone_factory do
uuid = Ecto.UUID.generate() uuid = Ecto.UUID.generate()
@ -395,6 +422,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec discussion_factory :: Mobilizon.Discussions.Discussion.t()
def discussion_factory do def discussion_factory do
uuid = Ecto.UUID.generate() uuid = Ecto.UUID.generate()
actor = build(:actor) actor = build(:actor)
@ -413,6 +441,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec mobilizon_activity_factory :: Mobilizon.Activities.Activity.t()
def mobilizon_activity_factory do def mobilizon_activity_factory do
group = build(:group) group = build(:group)
actor = build(:actor) actor = build(:actor)
@ -433,6 +462,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec mobilizon_activity_setting_factory :: Mobilizon.Users.ActivitySetting.t()
def mobilizon_activity_setting_factory do def mobilizon_activity_setting_factory do
%Mobilizon.Users.ActivitySetting{ %Mobilizon.Users.ActivitySetting{
key: "event_created", key: "event_created",
@ -442,6 +472,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec push_subscription_factory :: Mobilizon.Users.PushSubscription.t()
def push_subscription_factory do def push_subscription_factory do
%Mobilizon.Users.PushSubscription{ %Mobilizon.Users.PushSubscription{
digest: "", digest: "",
@ -452,6 +483,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec share_factory :: Mobilizon.Share.t()
def share_factory do def share_factory do
%Mobilizon.Share{ %Mobilizon.Share{
actor: build(:actor), actor: build(:actor),
@ -460,6 +492,7 @@ defmodule Mobilizon.Factory do
} }
end end
@spec event_metadata_factory :: Mobilizon.Events.EventMetadata.t()
def event_metadata_factory do def event_metadata_factory do
%Mobilizon.Events.EventMetadata{ %Mobilizon.Events.EventMetadata{
key: sequence("mz:custom:something"), key: sequence("mz:custom:something"),