From 4a0c1ea42e9f45a5b45fbfaf812a8bfa34ca9330 Mon Sep 17 00:00:00 2001 From: miffy Date: Sat, 7 Sep 2019 02:32:23 +0200 Subject: [PATCH 01/29] Add separate module for pagination of queries --- lib/mobilizon/page.ex | 48 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 lib/mobilizon/page.ex diff --git a/lib/mobilizon/page.ex b/lib/mobilizon/page.ex new file mode 100644 index 000000000..ce2d440b5 --- /dev/null +++ b/lib/mobilizon/page.ex @@ -0,0 +1,48 @@ +defmodule Mobilizon.Page do + @moduledoc """ + Module for pagination of queries. + """ + + import Ecto.Query, warn: false + + alias Mobilizon.Repo + + defstruct [ + :total, + :elements + ] + + @type t :: %__MODULE__{ + total: integer, + elements: struct + } + + @doc """ + Returns a Page struct for a query. + """ + @spec build_page(Ecto.Query.t(), integer | nil, integer | nil) :: t + def build_page(query, page, limit) do + [total, elements] = + [ + fn -> Repo.aggregate(query, :count, :id) end, + fn -> Repo.all(paginate(query, page, limit)) end + ] + |> Enum.map(&Task.async/1) + |> Enum.map(&Task.await/1) + + %__MODULE__{total: total, elements: elements} + end + + @doc """ + Add limit and offset to the query. + """ + @spec paginate(Ecto.Query.t(), integer | nil, integer | nil) :: Ecto.Query.t() + def paginate(query, page \\ 1, size \\ 10) + + def paginate(query, page, _size) when is_nil(page), do: paginate(query) + def paginate(query, page, size) when is_nil(size), do: paginate(query, page) + + def paginate(query, page, size) do + from(query, limit: ^size, offset: ^((page - 1) * size)) + end +end From c2b4fb6cff2d6ed545534f718454d2fa0a92370f Mon Sep 17 00:00:00 2001 From: miffy Date: Sat, 7 Sep 2019 02:36:37 +0200 Subject: [PATCH 02/29] Refactoring of Media context --- lib/mobilizon/media.ex | 125 ---------------------------- lib/mobilizon/media/file.ex | 26 ++++-- lib/mobilizon/media/media.ex | 90 ++++++++++++++++++++ lib/mobilizon/media/picture.ex | 15 +++- test/mobilizon/media/media_test.exs | 5 -- 5 files changed, 122 insertions(+), 139 deletions(-) delete mode 100644 lib/mobilizon/media.ex create mode 100644 lib/mobilizon/media/media.ex diff --git a/lib/mobilizon/media.ex b/lib/mobilizon/media.ex deleted file mode 100644 index 850b5274e..000000000 --- a/lib/mobilizon/media.ex +++ /dev/null @@ -1,125 +0,0 @@ -defmodule Mobilizon.Media do - @moduledoc """ - The Media context. - """ - - import Ecto.Query, warn: false - alias Mobilizon.Repo - - alias Mobilizon.Media.Picture - alias Mobilizon.Media.File - alias Ecto.Multi - - @doc false - def data() do - Dataloader.Ecto.new(Mobilizon.Repo, query: &query/2) - end - - @doc false - def query(queryable, _params) do - queryable - end - - @doc """ - Gets a single picture. - - Raises `Ecto.NoResultsError` if the Picture does not exist. - - ## Examples - - iex> get_picture!(123) - %Picture{} - - iex> get_picture!(456) - ** (Ecto.NoResultsError) - - """ - def get_picture!(id), do: Repo.get!(Picture, id) - - def get_picture(id), do: Repo.get(Picture, id) - - @doc """ - Get a picture by it's URL - """ - @spec get_picture_by_url(String.t()) :: Picture.t() | nil - def get_picture_by_url(url) do - from( - p in Picture, - where: fragment("? @> ?", p.file, ~s|{"url": "#{url}"}|) - ) - |> Repo.one() - end - - @doc """ - Creates a picture. - - ## Examples - - iex> create_picture(%{field: value}) - {:ok, %Picture{}} - - iex> create_picture(%{field: bad_value}) - {:error, %Ecto.Changeset{}} - - """ - def create_picture(attrs \\ %{}) do - %Picture{} - |> Picture.changeset(attrs) - |> Repo.insert() - end - - @doc """ - Updates a picture. - - ## Examples - - iex> update_picture(picture, %{field: new_value}) - {:ok, %Picture{}} - - iex> update_picture(picture, %{field: bad_value}) - {:error, %Ecto.Changeset{}} - - """ - def update_picture(%Picture{} = picture, attrs) do - picture - |> Picture.changeset(attrs) - |> Repo.update() - end - - @doc """ - Deletes a Picture. - - ## Examples - - iex> delete_picture(picture) - {:ok, %Picture{}} - - iex> delete_picture(picture) - {:error, %Ecto.Changeset{}} - - """ - def delete_picture(%Picture{} = picture) do - case Multi.new() - |> Multi.delete(:picture, picture) - |> Multi.run(:remove, fn _repo, %{picture: %Picture{file: %File{url: url}}} = _picture -> - MobilizonWeb.Upload.remove(url) - end) - |> Repo.transaction() do - {:ok, %{picture: %Picture{} = picture}} -> {:ok, picture} - {:error, :remove, error, _} -> {:error, error} - end - end - - @doc """ - Returns an `%Ecto.Changeset{}` for tracking picture changes. - - ## Examples - - iex> change_picture(picture) - %Ecto.Changeset{source: %Picture{}} - - """ - def change_picture(%Picture{} = picture) do - Picture.changeset(picture, %{}) - end -end diff --git a/lib/mobilizon/media/file.ex b/lib/mobilizon/media/file.ex index 2574a012b..611cd367d 100644 --- a/lib/mobilizon/media/file.ex +++ b/lib/mobilizon/media/file.ex @@ -1,9 +1,22 @@ defmodule Mobilizon.Media.File do @moduledoc """ - Represents a file entity + Represents a file entity. """ + use Ecto.Schema - import Ecto.Changeset + + import Ecto.Changeset, only: [cast: 3, validate_required: 2] + + @type t :: %__MODULE__{ + name: String.t(), + url: String.t(), + content_type: String.t(), + size: integer + } + + @required_attrs [:name, :url] + @optional_attrs [:content_type, :size] + @attrs @required_attrs ++ @optional_attrs embedded_schema do field(:name, :string) @@ -15,9 +28,10 @@ defmodule Mobilizon.Media.File do end @doc false - def changeset(picture, attrs) do - picture - |> cast(attrs, [:name, :url, :content_type, :size]) - |> validate_required([:name, :url]) + @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() + def changeset(file, attrs) do + file + |> cast(attrs, @attrs) + |> validate_required(@required_attrs) end end diff --git a/lib/mobilizon/media/media.ex b/lib/mobilizon/media/media.ex new file mode 100644 index 000000000..70762091b --- /dev/null +++ b/lib/mobilizon/media/media.ex @@ -0,0 +1,90 @@ +defmodule Mobilizon.Media do + @moduledoc """ + The Media context. + """ + + import Ecto.Query + + alias Ecto.Multi + + alias Mobilizon.Media.{File, Picture} + alias Mobilizon.Repo + + @doc false + @spec data :: Dataloader.Ecto.t() + def data, do: Dataloader.Ecto.new(Mobilizon.Repo, query: &query/2) + + @doc false + @spec query(Ecto.Query.t(), map) :: Ecto.Query.t() + def query(queryable, _params), do: queryable + + @doc """ + Gets a single picture. + """ + @spec get_picture(integer | String.t()) :: Picture.t() | nil + def get_picture(id), do: Repo.get(Picture, id) + + @doc """ + Gets a single picture. + Raises `Ecto.NoResultsError` if the picture does not exist. + """ + @spec get_picture!(integer | String.t()) :: Picture.t() + def get_picture!(id), do: Repo.get!(Picture, id) + + @doc """ + Get a picture by it's URL. + """ + @spec get_picture_by_url(String.t()) :: Picture.t() | nil + def get_picture_by_url(url) do + url + |> picture_by_url_query() + |> Repo.one() + end + + @doc """ + Creates a picture. + """ + @spec create_picture(map) :: {:ok, Picture.t()} | {:error, Ecto.Changeset.t()} + def create_picture(attrs \\ %{}) do + %Picture{} + |> Picture.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a picture. + """ + @spec update_picture(Picture.t(), map) :: {:ok, Picture.t()} | {:error, Ecto.Changeset.t()} + def update_picture(%Picture{} = picture, attrs) do + picture + |> Picture.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a picture. + """ + @spec delete_picture(Picture.t()) :: {:ok, Picture.t()} | {:error, Ecto.Changeset.t()} + def delete_picture(%Picture{} = picture) do + transaction = + Multi.new() + |> Multi.delete(:picture, picture) + |> Multi.run(:remove, fn _repo, %{picture: %Picture{file: %File{url: url}}} -> + MobilizonWeb.Upload.remove(url) + end) + |> Repo.transaction() + + case transaction do + {:ok, %{picture: %Picture{} = picture}} -> {:ok, picture} + {:error, :remove, error, _} -> {:error, error} + end + end + + @spec picture_by_url_query(String.t()) :: Ecto.Query.t() + defp picture_by_url_query(url) do + from( + p in Picture, + where: fragment("? @> ?", p.file, ~s|{"url": "#{url}"}|) + ) + end +end diff --git a/lib/mobilizon/media/picture.ex b/lib/mobilizon/media/picture.ex index 62b811894..00f55e75d 100644 --- a/lib/mobilizon/media/picture.ex +++ b/lib/mobilizon/media/picture.ex @@ -1,11 +1,19 @@ defmodule Mobilizon.Media.Picture do @moduledoc """ - Represents a picture entity + Represents a picture entity. """ + use Ecto.Schema - import Ecto.Changeset - alias Mobilizon.Media.File + + import Ecto.Changeset, only: [cast: 3, cast_embed: 2] + alias Mobilizon.Actors.Actor + alias Mobilizon.Media.File + + @type t :: %__MODULE__{ + file: File.t(), + actor: Actor.t() + } schema "pictures" do embeds_one(:file, File, on_replace: :update) @@ -15,6 +23,7 @@ defmodule Mobilizon.Media.Picture do end @doc false + @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() def changeset(picture, attrs) do picture |> cast(attrs, [:actor_id]) diff --git a/test/mobilizon/media/media_test.exs b/test/mobilizon/media/media_test.exs index 6ba645d6a..50b3b96ee 100644 --- a/test/mobilizon/media/media_test.exs +++ b/test/mobilizon/media/media_test.exs @@ -60,10 +60,5 @@ defmodule Mobilizon.MediaTest do "/" <> path ) end - - test "change_picture/1 returns a picture changeset" do - picture = insert(:picture) - assert %Ecto.Changeset{} = Media.change_picture(picture) - end end end From d0c99745585514ee5b8ec4d49b46f27699e7004a Mon Sep 17 00:00:00 2001 From: miffy Date: Sat, 7 Sep 2019 02:38:13 +0200 Subject: [PATCH 03/29] Refactoring of Reports context --- lib/mobilizon/reports.ex | 236 ------------------ lib/mobilizon/reports/note.ex | 22 +- lib/mobilizon/reports/report.ex | 48 ++-- lib/mobilizon/reports/reports.ex | 162 ++++++++++++ lib/mobilizon_web/api/reports.ex | 6 +- .../20190712125833_create_reports.exs | 8 +- 6 files changed, 214 insertions(+), 268 deletions(-) delete mode 100644 lib/mobilizon/reports.ex create mode 100644 lib/mobilizon/reports/reports.ex diff --git a/lib/mobilizon/reports.ex b/lib/mobilizon/reports.ex deleted file mode 100644 index 1bfaac7ac..000000000 --- a/lib/mobilizon/reports.ex +++ /dev/null @@ -1,236 +0,0 @@ -defmodule Mobilizon.Reports do - @moduledoc """ - The Reports context. - """ - - import Ecto.Query, warn: false - alias Mobilizon.Repo - import Mobilizon.Ecto - - alias Mobilizon.Reports.Report - alias Mobilizon.Reports.Note - - @doc false - def data() do - Dataloader.Ecto.new(Mobilizon.Repo, query: &query/2) - end - - @doc false - def query(queryable, _params) do - queryable - end - - @doc """ - Returns the list of reports. - - ## Examples - - iex> list_reports() - [%Report{}, ...] - - """ - @spec list_reports(integer(), integer(), atom(), atom()) :: list(Report.t()) - def list_reports(page \\ nil, limit \\ nil, sort \\ :updated_at, direction \\ :asc) do - from( - r in Report, - preload: [:reported, :reporter, :manager, :event, :comments, :notes] - ) - |> paginate(page, limit) - |> sort(sort, direction) - |> Repo.all() - end - - @doc """ - Gets a single report. - - Raises `Ecto.NoResultsError` if the Report does not exist. - - ## Examples - - iex> get_report!(123) - %Report{} - - iex> get_report!(456) - ** (Ecto.NoResultsError) - - """ - def get_report!(id) do - with %Report{} = report <- Repo.get!(Report, id) do - Repo.preload(report, [:reported, :reporter, :manager, :event, :comments, :notes]) - end - end - - @doc """ - Gets a single report. - - Returns `nil` if the Report does not exist. - - ## Examples - - iex> get_report(123) - %Report{} - - iex> get_report(456) - nil - - """ - def get_report(id) do - with %Report{} = report <- Repo.get(Report, id) do - Repo.preload(report, [:reported, :reporter, :manager, :event, :comments, :notes]) - end - end - - @doc """ - Get a report by it's URL - """ - @spec get_report_by_url(String.t()) :: Report.t() | nil - def get_report_by_url(url) do - from( - r in Report, - where: r.uri == ^url - ) - |> Repo.one() - end - - @doc """ - Creates a report. - - ## Examples - - iex> create_report(%{field: value}) - {:ok, %Report{}} - - iex> create_report(%{field: bad_value}) - {:error, %Ecto.Changeset{}} - - """ - def create_report(attrs \\ %{}) do - with {:ok, %Report{} = report} <- - %Report{} - |> Report.creation_changeset(attrs) - |> Repo.insert() do - {:ok, Repo.preload(report, [:event, :reported, :reporter, :comments])} - end - end - - @doc """ - Updates a report. - - ## Examples - - iex> update_report(report, %{field: new_value}) - {:ok, %Report{}} - - iex> update_report(report, %{field: bad_value}) - {:error, %Ecto.Changeset{}} - - """ - def update_report(%Report{} = report, attrs) do - report - |> Report.changeset(attrs) - |> Repo.update() - end - - @doc """ - Deletes a Report. - - ## Examples - - iex> delete_report(report) - {:ok, %Report{}} - - iex> delete_report(report) - {:error, %Ecto.Changeset{}} - - """ - def delete_report(%Report{} = report) do - Repo.delete(report) - end - - @doc """ - Returns an `%Ecto.Changeset{}` for tracking report changes. - - ## Examples - - iex> change_report(report) - %Ecto.Changeset{source: %Report{}} - - """ - def change_report(%Report{} = report) do - Report.changeset(report, %{}) - end - - @doc """ - Returns the list of notes for a report. - - ## Examples - - iex> list_notes_for_report(%Report{id: 1}) - [%Note{}, ...] - - """ - @spec list_notes_for_report(Report.t()) :: list(Report.t()) - def list_notes_for_report(%Report{id: report_id}) do - from( - n in Note, - where: n.report_id == ^report_id, - preload: [:report, :moderator] - ) - |> Repo.all() - end - - @doc """ - Gets a single note. - - Raises `Ecto.NoResultsError` if the Note does not exist. - - ## Examples - - iex> get_note!(123) - %Note{} - - iex> get_note!(456) - ** (Ecto.NoResultsError) - - """ - def get_note!(id), do: Repo.get!(Note, id) - - def get_note(id), do: Repo.get(Note, id) - - @doc """ - Creates a note report. - - ## Examples - - iex> create_report_note(%{field: value}) - {:ok, %Note{}} - - iex> create_report_note(%{field: bad_value}) - {:error, %Ecto.Changeset{}} - - """ - def create_report_note(attrs \\ %{}) do - with {:ok, %Note{} = note} <- - %Note{} - |> Note.changeset(attrs) - |> Repo.insert() do - {:ok, Repo.preload(note, [:report, :moderator])} - end - end - - @doc """ - Deletes a note report. - - ## Examples - - iex> delete_report_note(note) - {:ok, %Note{}} - - iex> delete_report_note(note) - {:error, %Ecto.Changeset{}} - - """ - def delete_report_note(%Note{} = note) do - Repo.delete(note) - end -end diff --git a/lib/mobilizon/reports/note.ex b/lib/mobilizon/reports/note.ex index a7d8ad30e..c28d18df4 100644 --- a/lib/mobilizon/reports/note.ex +++ b/lib/mobilizon/reports/note.ex @@ -1,27 +1,39 @@ defmodule Mobilizon.Reports.Note do @moduledoc """ - Report Note entity + Represents a note entity. """ + use Ecto.Schema - import Ecto.Changeset + + import Ecto.Changeset, only: [cast: 3, validate_required: 2] + alias Mobilizon.Actors.Actor alias Mobilizon.Reports.Report - @attrs [:content, :moderator_id, :report_id] + @required_attrs [:content, :moderator_id, :report_id] + @attrs @required_attrs + + @type t :: %__MODULE__{ + content: String.t(), + report: Report.t(), + moderator: Actor.t() + } @derive {Jason.Encoder, only: [:content]} schema "report_notes" do field(:content, :string) - belongs_to(:moderator, Actor) + belongs_to(:report, Report) + belongs_to(:moderator, Actor) timestamps() end @doc false + @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() def changeset(note, attrs) do note |> cast(attrs, @attrs) - |> validate_required(@attrs) + |> validate_required(@required_attrs) end end diff --git a/lib/mobilizon/reports/report.ex b/lib/mobilizon/reports/report.ex index 11d55e824..768a5ed44 100644 --- a/lib/mobilizon/reports/report.ex +++ b/lib/mobilizon/reports/report.ex @@ -1,43 +1,48 @@ -import EctoEnum - -defenum(Mobilizon.Reports.ReportStateEnum, :report_state, [ - :open, - :closed, - :resolved -]) - defmodule Mobilizon.Reports.Report do @moduledoc """ - Report entity + Represents a report entity. """ + use Ecto.Schema + import Ecto.Changeset - alias Mobilizon.Events.Comment - alias Mobilizon.Events.Event + alias Mobilizon.Actors.Actor - alias Mobilizon.Reports.Note + alias Mobilizon.Events.{Comment, Event} + alias Mobilizon.Reports.{Note, ReportStatus} + + @type t :: %__MODULE__{ + content: String.t(), + status: ReportStatus.t(), + uri: String.t(), + reported: Actor.t(), + reporter: Actor.t(), + manager: Actor.t(), + event: Event.t(), + comments: [Comment.t()], + notes: [Note.t()] + } + + @required_attrs [:content, :uri, :reported_id, :reporter_id] + @optional_attrs [:status, :manager_id, :event_id] + @attrs @required_attrs ++ @optional_attrs @derive {Jason.Encoder, only: [:status, :uri]} schema "reports" do field(:content, :string) - field(:status, Mobilizon.Reports.ReportStateEnum, default: :open) + field(:status, ReportStatus, default: :open) field(:uri, :string) # The reported actor belongs_to(:reported, Actor) - # The actor who reported belongs_to(:reporter, Actor) - # The actor who last acted on this report belongs_to(:manager, Actor) - # The eventual Event inside the report belongs_to(:event, Event) - # The eventual Comments inside the report many_to_many(:comments, Comment, join_through: "reports_comments", on_replace: :delete) - # The notes associated to the report has_many(:notes, Note, foreign_key: :report_id) @@ -45,12 +50,15 @@ defmodule Mobilizon.Reports.Report do end @doc false + @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() def changeset(report, attrs) do report - |> cast(attrs, [:content, :status, :uri, :reported_id, :reporter_id, :manager_id, :event_id]) - |> validate_required([:content, :uri, :reported_id, :reporter_id]) + |> cast(attrs, @attrs) + |> validate_required(@required_attrs) end + @doc false + @spec creation_changeset(Report.t(), map) :: Ecto.Changeset.t() def creation_changeset(report, attrs) do report |> changeset(attrs) diff --git a/lib/mobilizon/reports/reports.ex b/lib/mobilizon/reports/reports.ex new file mode 100644 index 000000000..173dc5fa7 --- /dev/null +++ b/lib/mobilizon/reports/reports.ex @@ -0,0 +1,162 @@ +defmodule Mobilizon.Reports do + @moduledoc """ + The Reports context. + """ + + import Ecto.Query + import EctoEnum + + import Mobilizon.Ecto + + alias Mobilizon.{Page, Repo} + alias Mobilizon.Reports.{Note, Report} + + defenum(ReportStatus, :report_status, [:open, :closed, :resolved]) + + @doc false + @spec data :: Dataloader.Ecto.t() + def data, do: Dataloader.Ecto.new(Mobilizon.Repo, query: &query/2) + + @doc false + @spec query(Ecto.Query.t(), map) :: Ecto.Query.t() + def query(queryable, _params), do: queryable + + @doc """ + Returns the list of reports. + """ + @spec list_reports(integer | nil, integer | nil, atom, atom) :: [Report.t()] + def list_reports(page \\ nil, limit \\ nil, sort \\ :updated_at, direction \\ :asc) do + list_reports_query() + |> Page.paginate(page, limit) + |> sort(sort, direction) + |> Repo.all() + end + + @doc """ + Gets a single report. + """ + @spec get_report(integer | String.t()) :: Report.t() | nil + def get_report(id) do + Report + |> Repo.get(id) + |> Repo.preload([:reported, :reporter, :manager, :event, :comments, :notes]) + end + + @doc """ + Gets a single report. + Raises `Ecto.NoResultsError` if the report does not exist. + """ + @spec get_report!(integer | String.t()) :: Report.t() + def get_report!(id) do + Report + |> Repo.get!(id) + |> Repo.preload([:reported, :reporter, :manager, :event, :comments, :notes]) + end + + @doc """ + Get a report by its URL + """ + @spec get_report_by_url(String.t()) :: Report.t() | nil + def get_report_by_url(url) do + url + |> report_by_url_query() + |> Repo.one() + end + + @doc """ + Creates a report. + """ + @spec create_report(map) :: {:ok, Report.t()} | {:error, Ecto.Changeset.t()} + def create_report(attrs \\ %{}) do + with {:ok, %Report{} = report} <- + %Report{} + |> Report.changeset(attrs) + |> Repo.insert() do + {:ok, Repo.preload(report, [:event, :reported, :reporter, :comments])} + end + end + + @doc """ + Updates a report. + """ + @spec update_report(Report.t(), map) :: {:ok, Report.t()} | {:error, Ecto.Changeset.t()} + def update_report(%Report{} = report, attrs) do + report + |> Report.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a report. + """ + @spec delete_report(Report.t()) :: {:ok, Report.t()} | {:error, Ecto.Changeset.t()} + def delete_report(%Report{} = report) do + Repo.delete(report) + end + + @doc """ + Returns the list of notes for a report. + """ + @spec list_notes_for_report(Report.t()) :: [Note.t()] + def list_notes_for_report(%Report{id: report_id}) do + report_id + |> list_notes_for_report_query() + |> Repo.all() + end + + @doc """ + Gets a single note. + """ + @spec get_note(integer | String.t()) :: Note.t() | nil + def get_note(id), do: Repo.get(Note, id) + + @doc """ + Gets a single note. + Raises `Ecto.NoResultsError` if the Note does not exist. + """ + @spec get_note!(integer | String.t()) :: Note.t() + def get_note!(id), do: Repo.get!(Note, id) + + @doc """ + Creates a note. + """ + @spec create_note(map) :: {:ok, Note.t()} | {:error, Ecto.Changeset.t()} + def create_note(attrs \\ %{}) do + with {:ok, %Note{} = note} <- + %Note{} + |> Note.changeset(attrs) + |> Repo.insert() do + {:ok, Repo.preload(note, [:report, :moderator])} + end + end + + @doc """ + Deletes a note. + """ + @spec delete_note(Note.t()) :: {:ok, Note.t()} | {:error, Ecto.Changeset.t()} + def delete_note(%Note{} = note) do + Repo.delete(note) + end + + @spec list_reports_query :: Ecto.Query.t() + defp list_reports_query do + from( + r in Report, + preload: [:reported, :reporter, :manager, :event, :comments, :notes] + ) + end + + @spec report_by_url_query(String.t()) :: Ecto.Query.t() + defp report_by_url_query(url) do + from(r in Report, where: r.uri == ^url) + end + + @spec list_notes_for_report_query(integer | String.t()) :: Ecto.Query.t() + defp list_notes_for_report_query(report_id) do + from( + n in Note, + where: n.report_id == ^report_id, + preload: [:report, :moderator] + ) + end +end diff --git a/lib/mobilizon_web/api/reports.ex b/lib/mobilizon_web/api/reports.ex index 3010c6ecb..815471acd 100644 --- a/lib/mobilizon_web/api/reports.ex +++ b/lib/mobilizon_web/api/reports.ex @@ -61,7 +61,7 @@ defmodule MobilizonWeb.API.Reports do """ def update_report_status(%Actor{} = actor, %Report{} = report, state) do with {:valid_state, true} <- - {:valid_state, Mobilizon.Reports.ReportStateEnum.valid_value?(state)}, + {:valid_state, Mobilizon.Reports.ReportStatus.valid_value?(state)}, {:ok, report} <- ReportsAction.update_report(report, %{"status" => state}), {:ok, _} <- log_action(actor, "update", report) do {:ok, report} @@ -89,7 +89,7 @@ defmodule MobilizonWeb.API.Reports do with %User{role: role} <- Users.get_user!(user_id), {:role, true} <- {:role, role in [:administrator, :moderator]}, {:ok, %Note{} = note} <- - Mobilizon.Reports.create_report_note(%{ + Mobilizon.Reports.create_note(%{ "report_id" => report_id, "moderator_id" => moderator_id, "content" => content @@ -114,7 +114,7 @@ defmodule MobilizonWeb.API.Reports do %User{role: role} <- Users.get_user!(user_id), {:role, true} <- {:role, role in [:administrator, :moderator]}, {:ok, %Note{} = note} <- - Mobilizon.Reports.delete_report_note(note), + Mobilizon.Reports.delete_note(note), {:ok, _} <- log_action(moderator, "delete", note) do {:ok, note} else diff --git a/priv/repo/migrations/20190712125833_create_reports.exs b/priv/repo/migrations/20190712125833_create_reports.exs index ce466b3e9..334b7ac08 100644 --- a/priv/repo/migrations/20190712125833_create_reports.exs +++ b/priv/repo/migrations/20190712125833_create_reports.exs @@ -1,13 +1,13 @@ defmodule Mobilizon.Repo.Migrations.CreateReports do use Ecto.Migration - alias Mobilizon.Reports.ReportStateEnum + alias Mobilizon.Reports.ReportStatus def up do - ReportStateEnum.create_type() + ReportStatus.create_type() create table(:reports) do add(:content, :string) - add(:status, ReportStateEnum.type(), default: "open", null: false) + add(:status, ReportStatus.type(), default: "open", null: false) add(:uri, :string, null: false) add(:reported_id, references(:actors, on_delete: :delete_all), null: false) @@ -28,6 +28,6 @@ defmodule Mobilizon.Repo.Migrations.CreateReports do drop(table(:reports_comments)) drop(table(:reports)) - ReportStateEnum.drop_type() + ReportStatus.drop_type() end end From f316f0a940da291af62a775ca3f6f2d97eced9bb Mon Sep 17 00:00:00 2001 From: miffy Date: Sat, 7 Sep 2019 19:54:11 +0200 Subject: [PATCH 04/29] Refactoring of Users context --- lib/mobilizon/crypto.ex | 15 + lib/mobilizon/users/user.ex | 170 ++++---- lib/mobilizon/users/users.ex | 412 +++++++++--------- lib/mobilizon_web/api/groups.ex | 15 +- lib/mobilizon_web/resolvers/event.ex | 122 ++---- lib/mobilizon_web/resolvers/feed_token.ex | 7 +- lib/mobilizon_web/resolvers/group.ex | 82 +--- lib/mobilizon_web/resolvers/person.ex | 36 +- lib/mobilizon_web/resolvers/picture.ex | 37 +- lib/mobilizon_web/resolvers/report.ex | 45 +- lib/mobilizon_web/resolvers/user.ex | 10 +- mix.exs | 2 +- .../20190307125009_move_user_role_to_enum.exs | 8 +- .../resolvers/report_resolver_test.exs | 3 +- 14 files changed, 421 insertions(+), 543 deletions(-) create mode 100644 lib/mobilizon/crypto.ex diff --git a/lib/mobilizon/crypto.ex b/lib/mobilizon/crypto.ex new file mode 100644 index 000000000..9068c8787 --- /dev/null +++ b/lib/mobilizon/crypto.ex @@ -0,0 +1,15 @@ +defmodule Mobilizon.Crypto do + @moduledoc """ + Utility module which contains cryptography related functions. + """ + + @doc """ + Returns random byte sequence of the length encoded to Base64. + """ + @spec random_string(integer) :: String.t() + def random_string(length) do + length + |> :crypto.strong_rand_bytes() + |> Base.url_encode64() + end +end diff --git a/lib/mobilizon/users/user.ex b/lib/mobilizon/users/user.ex index 449f4b1cb..eb8895108 100644 --- a/lib/mobilizon/users/user.ex +++ b/lib/mobilizon/users/user.ex @@ -1,63 +1,80 @@ -import EctoEnum - -defenum(Mobilizon.Users.UserRoleEnum, :user_role_type, [ - :administrator, - :moderator, - :user -]) - defmodule Mobilizon.Users.User do @moduledoc """ - Represents a local user + Represents a local user. """ + use Ecto.Schema + import Ecto.Changeset + alias Mobilizon.Actors.Actor - alias Mobilizon.Users.User - alias Mobilizon.Service.EmailChecker + alias Mobilizon.Crypto alias Mobilizon.Events.FeedToken + alias Mobilizon.Service.EmailChecker + alias Mobilizon.Users.{User, UserRole} + + @type t :: %__MODULE__{ + email: String.t(), + password_hash: String.t(), + password: String.t(), + role: UserRole.t(), + confirmed_at: DateTime.t(), + confirmation_sent_at: DateTime.t(), + confirmation_token: String.t(), + reset_password_sent_at: DateTime.t(), + reset_password_token: String.t(), + default_actor: Actor.t(), + actors: [Actor.t()], + feed_tokens: [FeedToken.t()] + } + + @required_attrs [:email] + @optional_attrs [ + :role, + :password, + :password_hash, + :confirmed_at, + :confirmation_sent_at, + :confirmation_token, + :reset_password_sent_at, + :reset_password_token + ] + @attrs @required_attrs ++ @optional_attrs + + @registration_required_attrs [:email, :password] + + @password_reset_required_attrs [:password, :reset_password_token, :reset_password_sent_at] + + @confirmation_token_length 30 schema "users" do field(:email, :string) field(:password_hash, :string) field(:password, :string, virtual: true) - field(:role, Mobilizon.Users.UserRoleEnum, default: :user) - has_many(:actors, Actor) - belongs_to(:default_actor, Actor) + field(:role, UserRole, default: :user) field(:confirmed_at, :utc_datetime) field(:confirmation_sent_at, :utc_datetime) field(:confirmation_token, :string) field(:reset_password_sent_at, :utc_datetime) field(:reset_password_token, :string) + + belongs_to(:default_actor, Actor) + has_many(:actors, Actor) has_many(:feed_tokens, FeedToken, foreign_key: :user_id) timestamps() end @doc false + @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() def changeset(%User{} = user, attrs) do changeset = user - |> cast(attrs, [ - :email, - :role, - :password, - :password_hash, - :confirmed_at, - :confirmation_sent_at, - :confirmation_token, - :reset_password_sent_at, - :reset_password_token - ]) - |> validate_required([:email]) + |> cast(attrs, @attrs) + |> validate_required(@required_attrs) |> unique_constraint(:email, message: "This email is already used.") |> validate_email() - |> validate_length( - :password, - min: 6, - max: 100, - message: "The chosen password is too short." - ) + |> validate_length(:password, min: 6, max: 100, message: "The chosen password is too short.") if Map.has_key?(attrs, :default_actor) do put_assoc(changeset, :default_actor, attrs.default_actor) @@ -66,11 +83,13 @@ defmodule Mobilizon.Users.User do end end - def registration_changeset(struct, params) do - struct - |> changeset(params) + @doc false + @spec registration_changeset(User.t(), map) :: Ecto.Changeset.t() + def registration_changeset(%User{} = user, attrs) do + user + |> changeset(attrs) |> cast_assoc(:default_actor) - |> validate_required([:email, :password]) + |> validate_required(@registration_required_attrs) |> hash_password() |> save_confirmation_token() |> unique_constraint( @@ -79,16 +98,18 @@ defmodule Mobilizon.Users.User do ) end + @doc false + @spec send_password_reset_changeset(User.t(), map) :: Ecto.Changeset.t() def send_password_reset_changeset(%User{} = user, attrs) do - user - |> cast(attrs, [:reset_password_token, :reset_password_sent_at]) + cast(user, attrs, [:reset_password_token, :reset_password_sent_at]) end + @doc false + @spec password_reset_changeset(User.t(), map) :: Ecto.Changeset.t() def password_reset_changeset(%User{} = user, attrs) do user - |> cast(attrs, [:password, :reset_password_token, :reset_password_sent_at]) - |> validate_length( - :password, + |> cast(attrs, @password_reset_required_attrs) + |> validate_length(:password, min: 6, max: 100, message: "registration.error.password_too_short" @@ -96,28 +117,45 @@ defmodule Mobilizon.Users.User do |> hash_password() end + @doc """ + Checks whether an user is confirmed. + """ + @spec is_confirmed(User.t()) :: boolean + def is_confirmed(%User{confirmed_at: nil}), do: false + def is_confirmed(%User{}), do: true + + @doc """ + Returns whether an user owns an actor. + """ + @spec owns_actor(User.t(), integer | String.t()) :: {:is_owned, Actor.t() | nil} + def owns_actor(%User{actors: actors}, actor_id) do + user_actor = Enum.find(actors, fn actor -> "#{actor.id}" == "#{actor_id}" end) + + {:is_owned, user_actor} + end + + @spec save_confirmation_token(Ecto.Changeset.t()) :: Ecto.Changeset.t() defp save_confirmation_token(changeset) do case changeset do %Ecto.Changeset{valid?: true, changes: %{email: _email}} -> - changeset = put_change(changeset, :confirmation_token, random_string(30)) + now = DateTime.utc_now() - put_change( - changeset, - :confirmation_sent_at, - DateTime.utc_now() |> DateTime.truncate(:second) - ) + changeset + |> put_change(:confirmation_token, Crypto.random_string(@confirmation_token_length)) + |> put_change(:confirmation_sent_at, DateTime.truncate(now, :second)) _ -> changeset end end + @spec validate_email(Ecto.Changeset.t()) :: Ecto.Changeset.t() defp validate_email(changeset) do case changeset do %Ecto.Changeset{valid?: true, changes: %{email: email}} -> case EmailChecker.valid?(email) do false -> add_error(changeset, :email, "Email doesn't fit required format") - _ -> changeset + true -> changeset end _ -> @@ -125,46 +163,14 @@ defmodule Mobilizon.Users.User do end end - defp random_string(length) do - length - |> :crypto.strong_rand_bytes() - |> Base.url_encode64() - end - - # Hash password when it's changed + @spec hash_password(Ecto.Changeset.t()) :: Ecto.Changeset.t() defp hash_password(changeset) do case changeset do %Ecto.Changeset{valid?: true, changes: %{password: password}} -> - put_change( - changeset, - :password_hash, - Argon2.hash_pwd_salt(password) - ) + put_change(changeset, :password_hash, Argon2.hash_pwd_salt(password)) _ -> changeset end end - - def is_confirmed(%User{confirmed_at: nil} = _user), do: {:error, :unconfirmed} - def is_confirmed(%User{} = user), do: {:ok, user} - - @doc """ - Returns whether an user owns an actor - """ - @spec owns_actor(struct(), String.t()) :: {:is_owned, false} | {:is_owned, true, Actor.t()} - def owns_actor(%User{} = user, actor_id) when is_binary(actor_id) do - case Integer.parse(actor_id) do - {actor_id, ""} -> owns_actor(user, actor_id) - _ -> {:is_owned, false} - end - end - - @spec owns_actor(struct(), integer()) :: {:is_owned, false} | {:is_owned, true, Actor.t()} - def owns_actor(%User{actors: actors}, actor_id) do - case Enum.find(actors, fn a -> a.id == actor_id end) do - nil -> {:is_owned, false} - actor -> {:is_owned, true, actor} - end - end end diff --git a/lib/mobilizon/users/users.ex b/lib/mobilizon/users/users.ex index 92697d931..af515c2cf 100644 --- a/lib/mobilizon/users/users.ex +++ b/lib/mobilizon/users/users.ex @@ -3,69 +3,60 @@ defmodule Mobilizon.Users do The Users context. """ - import Ecto.Query, warn: false + import Ecto.Query + import EctoEnum - alias Mobilizon.Repo import Mobilizon.Ecto alias Mobilizon.Actors.Actor + alias Mobilizon.Events + alias Mobilizon.{Page, Repo} alias Mobilizon.Users.User - @doc false - def data() do - Dataloader.Ecto.new(Repo, query: &query/2) - end + @type tokens :: %{ + required(:access_token) => String.t(), + required(:refresh_token) => String.t() + } + + defenum(UserRole, :user_role, [:administrator, :moderator, :user]) @doc false - def query(queryable, _params) do - queryable - end + @spec data :: Dataloader.Ecto.t() + def data, do: Dataloader.Ecto.new(Mobilizon.Repo, query: &query/2) + + @doc false + @spec query(Ecto.Query.t(), map) :: Ecto.Query.t() + def query(queryable, _params), do: queryable @doc """ - Register user + Registers an user. """ - @spec register(map()) :: {:ok, User.t()} | {:error, String.t()} + @spec register(map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} def register(%{email: _email, password: _password} = args) do with {:ok, %User{} = user} <- %User{} |> User.registration_changeset(args) - |> Mobilizon.Repo.insert() do - Mobilizon.Events.create_feed_token(%{"user_id" => user.id}) + |> Repo.insert() do + Events.create_feed_token(%{"user_id" => user.id}) + {:ok, user} end end @doc """ - Gets an user by it's email - - ## Examples - - iex> get_user_by_email("test@test.tld", true) - {:ok, %Mobilizon.Users.User{}} - - iex> get_user_by_email("test@notfound.tld", false) - {:error, :user_not_found} + Gets a single user. + Raises `Ecto.NoResultsError` if the user does not exist. """ + @spec get_user!(integer | String.t()) :: User.t() + def get_user!(id), do: Repo.get!(User, id) + + @doc """ + Gets an user by its email. + """ + @spec get_user_by_email(String.t(), boolean | nil) :: + {:ok, User.t()} | {:error, :user_not_found} def get_user_by_email(email, activated \\ nil) do - query = - case activated do - nil -> - from(u in User, where: u.email == ^email, preload: :default_actor) - - true -> - from( - u in User, - where: u.email == ^email and not is_nil(u.confirmed_at), - preload: :default_actor - ) - - false -> - from( - u in User, - where: u.email == ^email and is_nil(u.confirmed_at), - preload: :default_actor - ) - end + query = user_by_email_query(email, activated) case Repo.one(query) do nil -> {:error, :user_not_found} @@ -74,45 +65,29 @@ defmodule Mobilizon.Users do end @doc """ - Get an user by it's activation token + Get an user by its activation token. """ - @spec get_user_by_activation_token(String.t()) :: Actor.t() + @spec get_user_by_activation_token(String.t()) :: Actor.t() | nil def get_user_by_activation_token(token) do - Repo.one( - from( - u in User, - where: u.confirmation_token == ^token, - preload: [:default_actor] - ) - ) + token + |> user_by_activation_token_query() + |> Repo.one() end @doc """ - Get an user by it's reset password token + Get an user by its reset password token. """ @spec get_user_by_reset_password_token(String.t()) :: Actor.t() def get_user_by_reset_password_token(token) do - Repo.one( - from( - u in User, - where: u.reset_password_token == ^token, - preload: [:default_actor] - ) - ) + token + |> user_by_reset_password_token_query() + |> Repo.one() end @doc """ - Updates a user. - - ## Examples - - iex> update_user(User{}, %{password: "coucou"}) - {:ok, %Mobilizon.Users.User{}} - - iex> update_user(User{}, %{password: nil}) - {:error, %Ecto.Changeset{}} - + Updates an user. """ + @spec update_user(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} def update_user(%User{} = user, attrs) do with {:ok, %User{} = user} <- user @@ -123,65 +98,28 @@ defmodule Mobilizon.Users do end @doc """ - Deletes a User. - - ## Examples - - iex> delete_user(%User{email: "test@test.tld"}) - {:ok, %Mobilizon.Users.User{}} - - iex> delete_user(%User{}) - {:error, %Ecto.Changeset{}} - + Deletes an user. """ + @spec delete_user(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} def delete_user(%User{} = user) do Repo.delete(user) end - # @doc """ - # Returns an `%Ecto.Changeset{}` for tracking user changes. - - # ## Examples - - # iex> change_user(%Mobilizon.Users.User{}) - # %Ecto.Changeset{data: %Mobilizon.Users.User{}} - - # """ - # def change_user(%User{} = user) do - # User.changeset(user, %{}) - # end - @doc """ - Gets a single user. - - Raises `Ecto.NoResultsError` if the User does not exist. - - ## Examples - - iex> get_user!(123) - %Mobilizon.Users.User{} - - iex> get_user!(456) - ** (Ecto.NoResultsError) - + Get an user with its actors + Raises `Ecto.NoResultsError` if the user does not exist. """ - def get_user!(id), do: Repo.get!(User, id) - - @doc """ - Get an user with it's actors - - Raises `Ecto.NoResultsError` if the User does not exist. - """ - @spec get_user_with_actors!(integer()) :: User.t() + @spec get_user_with_actors!(integer | String.t()) :: User.t() def get_user_with_actors!(id) do - user = Repo.get!(User, id) - Repo.preload(user, [:actors, :default_actor]) + id + |> get_user!() + |> Repo.preload([:actors, :default_actor]) end @doc """ - Get user with it's actors by ID + Get user with its actors. """ - @spec get_user_with_actors(integer()) :: User.t() + @spec get_user_with_actors(integer()) :: {:ok, User.t()} | {:error, String.t()} def get_user_with_actors(id) do case Repo.get(User, id) do nil -> @@ -198,21 +136,19 @@ defmodule Mobilizon.Users do end @doc """ - Returns the associated actor for an user, either the default set one or the first found + Gets the associated actor for an user, either the default set one or the first + found. """ - @spec get_actor_for_user(Mobilizon.Users.User.t()) :: Mobilizon.Actors.Actor.t() - def get_actor_for_user(%Mobilizon.Users.User{} = user) do - case Repo.one( - from( - a in Actor, - join: u in User, - on: u.default_actor_id == a.id, - where: u.id == ^user.id - ) - ) do + @spec get_actor_for_user(User.t()) :: Actor.t() | nil + def get_actor_for_user(%User{} = user) do + actor = + user + |> actor_for_user_query() + |> Repo.one() + + case actor do nil -> - case user - |> get_actors_for_user() do + case get_actors_for_user(user) do [] -> nil actors -> hd(actors) end @@ -222,94 +158,48 @@ defmodule Mobilizon.Users do end end - def get_actors_for_user(%User{id: user_id}) do - Repo.all(from(a in Actor, where: a.user_id == ^user_id)) + @doc """ + Gets actors for an user. + """ + @spec get_actors_for_user(User.t()) :: [Actor.t()] + def get_actors_for_user(%User{} = user) do + user + |> actors_for_user_query() + |> Repo.all() end @doc """ - Authenticate user + Updates user's default actor. + Raises `Ecto.NoResultsError` if the user does not exist. """ - def authenticate(%{user: user, password: password}) do - # Does password match the one stored in the database? - with true <- Argon2.verify_pass(password, user.password_hash), - # Yes, create and return the token - {:ok, tokens} <- generate_tokens(user) do - {:ok, tokens} - else - _ -> - # No, return an error - {:error, :unauthorized} - end - end - - @doc """ - Generate access token and refresh token - """ - def generate_tokens(user) do - with {:ok, access_token} <- generate_access_token(user), - {:ok, refresh_token} <- generate_refresh_token(user) do - {:ok, %{access_token: access_token, refresh_token: refresh_token}} - end - end - - defp generate_access_token(user) do - with {:ok, access_token, _claims} <- - MobilizonWeb.Guardian.encode_and_sign(user, %{}, token_type: "access") do - {:ok, access_token} - end - end - - def generate_refresh_token(user) do - with {:ok, refresh_token, _claims} <- - MobilizonWeb.Guardian.encode_and_sign(user, %{}, token_type: "refresh") do - {:ok, refresh_token} - end - end - + @spec update_user_default_actor(integer | String.t(), integer | String.t()) :: User.t() def update_user_default_actor(user_id, actor_id) do with _ <- - from( - u in User, - where: u.id == ^user_id, - update: [ - set: [ - default_actor_id: ^actor_id - ] - ] - ) + user_id + |> update_user_default_actor_query(actor_id) |> Repo.update_all([]) do - Repo.get!(User, user_id) + user_id + |> get_user!() |> Repo.preload([:default_actor]) end end @doc """ Returns the list of users. - - ## Examples - - iex> list_users() - [%Mobilizon.Users.User{}] - """ + @spec list_users(integer | nil, integer | nil, atom | nil, atom | nil) :: [User.t()] def list_users(page \\ nil, limit \\ nil, sort \\ nil, direction \\ nil) do - Repo.all( - User - |> paginate(page, limit) - |> sort(sort, direction) - ) + User + |> Page.paginate(page, limit) + |> sort(sort, direction) + |> Repo.all() end @doc """ Returns the list of administrators. - - ## Examples - - iex> list_admins() - [%Mobilizon.Users.User{role: :administrator}] - """ - def list_admins() do + @spec list_admins :: [User.t()] + def list_admins do User |> where([u], u.role == ^:administrator) |> Repo.all() @@ -317,25 +207,129 @@ defmodule Mobilizon.Users do @doc """ Returns the list of moderators. - - ## Examples - - iex> list_moderators() - [%Mobilizon.Users.User{role: :moderator}, %Mobilizon.Users.User{role: :administrator}] - """ - def list_moderators() do + @spec list_moderators :: [User.t()] + def list_moderators do User |> where([u], u.role in ^[:administrator, :moderator]) |> Repo.all() end - def count_users() do - Repo.one( - from( - u in User, - select: count(u.id) - ) + @doc """ + Counts users. + """ + @spec count_users :: integer + def count_users do + Repo.one(from(u in User, select: count(u.id))) + end + + @doc """ + Authenticate an user. + """ + @spec authenticate(User.t()) :: {:ok, tokens} | {:error, :unauthorized} + def authenticate(%{user: %User{password_hash: password_hash} = user, password: password}) do + # Does password match the one stored in the database? + if Argon2.verify_pass(password, password_hash) do + {:ok, _tokens} = generate_tokens(user) + else + {:error, :unauthorized} + end + end + + @doc """ + Generates access token and refresh token for an user. + """ + @spec generate_tokens(User.t()) :: {:ok, tokens} + def generate_tokens(user) do + with {:ok, access_token} <- generate_access_token(user), + {:ok, refresh_token} <- generate_refresh_token(user) do + {:ok, %{access_token: access_token, refresh_token: refresh_token}} + end + end + + @doc """ + Generates access token for an user. + """ + @spec generate_access_token(User.t()) :: {:ok, String.t()} + def generate_access_token(user) do + with {:ok, access_token, _claims} <- + MobilizonWeb.Guardian.encode_and_sign(user, %{}, token_type: "access") do + {:ok, access_token} + end + end + + @doc """ + Generates refresh token for an user. + """ + @spec generate_refresh_token(User.t()) :: {:ok, String.t()} + def generate_refresh_token(user) do + with {:ok, refresh_token, _claims} <- + MobilizonWeb.Guardian.encode_and_sign(user, %{}, token_type: "refresh") do + {:ok, refresh_token} + end + end + + @spec user_by_email_query(String.t(), boolean | nil) :: Ecto.Query.t() + defp user_by_email_query(email, nil) do + from(u in User, where: u.email == ^email, preload: :default_actor) + end + + defp user_by_email_query(email, true) do + from( + u in User, + where: u.email == ^email and not is_nil(u.confirmed_at), + preload: :default_actor + ) + end + + defp user_by_email_query(email, false) do + from( + u in User, + where: u.email == ^email and is_nil(u.confirmed_at), + preload: :default_actor + ) + end + + @spec user_by_activation_token_query(String.t()) :: Ecto.Query.t() + defp user_by_activation_token_query(token) do + from( + u in User, + where: u.confirmation_token == ^token, + preload: [:default_actor] + ) + end + + @spec user_by_reset_password_token_query(String.t()) :: Ecto.Query.t() + defp user_by_reset_password_token_query(token) do + from( + u in User, + where: u.reset_password_token == ^token, + preload: [:default_actor] + ) + end + + @spec actor_for_user_query(User.t()) :: Ecto.Query.t() + defp actor_for_user_query(%User{id: user_id}) do + from( + a in Actor, + join: u in User, + on: u.default_actor_id == a.id, + where: u.id == ^user_id + ) + end + + @spec actors_for_user_query(User.t()) :: Ecto.Query.t() + defp actors_for_user_query(%User{id: user_id}) do + from(a in Actor, where: a.user_id == ^user_id) + end + + @spec update_user_default_actor_query(integer | String.t(), integer | String.t()) :: + Ecto.Query.t() + defp update_user_default_actor_query(user_id, actor_id) do + from( + u in User, + where: u.id == ^user_id, + update: [set: [default_actor_id: ^actor_id]] ) end end diff --git a/lib/mobilizon_web/api/groups.ex b/lib/mobilizon_web/api/groups.ex index 7ff870670..aaac7a6b1 100644 --- a/lib/mobilizon_web/api/groups.ex +++ b/lib/mobilizon_web/api/groups.ex @@ -3,6 +3,7 @@ defmodule MobilizonWeb.API.Groups do API for Events """ alias Mobilizon.Actors + alias Mobilizon.Actors.Actor alias Mobilizon.Users.User alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils @@ -22,21 +23,13 @@ defmodule MobilizonWeb.API.Groups do banner: _banner } = args ) do - with {:is_owned, true, actor} <- User.owns_actor(user, creator_actor_id), + with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, creator_actor_id), title <- String.trim(title), {:existing_group, nil} <- {:existing_group, Actors.get_group_by_title(title)}, visibility <- Map.get(args, :visibility, :public), {content_html, tags, to, cc} <- Utils.prepare_content(actor, summary, visibility, [], nil), - group <- - ActivityPubUtils.make_group_data( - actor.url, - to, - title, - content_html, - tags, - cc - ) do + group <- ActivityPubUtils.make_group_data(actor.url, to, title, content_html, tags, cc) do ActivityPub.create(%{ to: ["https://www.w3.org/ns/activitystreams#Public"], actor: actor, @@ -47,7 +40,7 @@ defmodule MobilizonWeb.API.Groups do {:existing_group, _} -> {:error, "A group with this name already exists"} - {:is_owned, _} -> + {:is_owned, nil} -> {:error, "Actor id is not owned by authenticated user"} end end diff --git a/lib/mobilizon_web/resolvers/event.ex b/lib/mobilizon_web/resolvers/event.ex index 8eadf44a6..fee135bf5 100644 --- a/lib/mobilizon_web/resolvers/event.ex +++ b/lib/mobilizon_web/resolvers/event.ex @@ -3,6 +3,7 @@ defmodule MobilizonWeb.Resolvers.Event do Handles the event-related GraphQL calls """ alias Mobilizon.Activity + alias Mobilizon.Actors.Actor alias Mobilizon.Addresses alias Mobilizon.Addresses.Address alias Mobilizon.Events @@ -64,11 +65,8 @@ defmodule MobilizonWeb.Resolvers.Event do # We find similar events with the same tags # uniq_by : It's possible event_from_same_actor is inside events_from_tags events = - (events ++ - Events.find_similar_events_by_common_tags( - tags, - @number_of_related_events - )) + events + |> Enum.concat(Events.find_similar_events_by_common_tags(tags, @number_of_related_events)) |> uniq_events() # TODO: We should use tag_relations to find more appropriate events @@ -76,8 +74,10 @@ defmodule MobilizonWeb.Resolvers.Event do # We've considered all recommended events, so we fetch the latest events events = if @number_of_related_events - length(events) > 0 do - (events ++ - Events.list_events(1, @number_of_related_events, :begins_on, :asc, true, true)) + events + |> Enum.concat( + Events.list_events(1, @number_of_related_events, :begins_on, :asc, true, true) + ) |> uniq_events() else events @@ -101,26 +101,23 @@ defmodule MobilizonWeb.Resolvers.Event do def actor_join_event( _parent, %{actor_id: actor_id, event_id: event_id}, - %{ - context: %{ - current_user: user - } - } + %{context: %{current_user: user}} ) do - with {:is_owned, true, actor} <- User.owns_actor(user, actor_id), + with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id), {:has_event, {:ok, %Event{} = event}} <- {:has_event, Mobilizon.Events.get_event_full(event_id)}, {:error, :participant_not_found} <- Mobilizon.Events.get_participant(event_id, actor_id), {:ok, _activity, participant} <- MobilizonWeb.API.Participations.join(event, actor), participant <- - Map.put(participant, :event, event) + participant + |> Map.put(:event, event) |> Map.put(:actor, Person.proxify_pictures(actor)) do {:ok, participant} else {:has_event, _} -> {:error, "Event with this ID #{inspect(event_id)} doesn't exist"} - {:is_owned, false} -> + {:is_owned, nil} -> {:error, "Actor id is not owned by authenticated user"} {:error, :event_not_found} -> @@ -141,32 +138,18 @@ defmodule MobilizonWeb.Resolvers.Event do def actor_leave_event( _parent, %{actor_id: actor_id, event_id: event_id}, - %{ - context: %{ - current_user: user - } - } + %{context: %{current_user: user}} ) do - with {:is_owned, true, actor} <- User.owns_actor(user, actor_id), + with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id), {:has_event, {:ok, %Event{} = event}} <- {:has_event, Mobilizon.Events.get_event_full(event_id)}, {:ok, _activity, _participant} <- MobilizonWeb.API.Participations.leave(event, actor) do - { - :ok, - %{ - event: %{ - id: event_id - }, - actor: %{ - id: actor_id - } - } - } + {:ok, %{event: %{id: event_id}, actor: %{id: actor_id}}} else {:has_event, _} -> {:error, "Event with this ID #{inspect(event_id)} doesn't exist"} - {:is_owned, false} -> + {:is_owned, nil} -> {:error, "Actor id is not owned by authenticated user"} {:only_organizer, true} -> @@ -187,31 +170,19 @@ defmodule MobilizonWeb.Resolvers.Event do def create_event( _parent, %{organizer_actor_id: organizer_actor_id} = args, - %{ - context: %{ - current_user: user - } - } = _resolution + %{context: %{current_user: user}} = _resolution ) do # See https://github.com/absinthe-graphql/absinthe/issues/490 with args <- Map.put(args, :options, args[:options] || %{}), - {:is_owned, true, organizer_actor} <- User.owns_actor(user, organizer_actor_id), + {:is_owned, %Actor{} = organizer_actor} <- User.owns_actor(user, organizer_actor_id), {:ok, args} <- save_attached_picture(args), {:ok, args} <- save_physical_address(args), args_with_organizer <- Map.put(args, :organizer_actor, organizer_actor), - { - :ok, - %Activity{ - data: %{ - "object" => %{"type" => "Event"} = _object - } - }, - %Event{} = event - } <- + {:ok, %Activity{data: %{"object" => %{"type" => "Event"} = _object}}, %Event{} = event} <- MobilizonWeb.API.Events.create_event(args_with_organizer) do {:ok, event} else - {:is_owned, false} -> + {:is_owned, nil} -> {:error, "Organizer actor id is not owned by the user"} end end @@ -226,35 +197,24 @@ defmodule MobilizonWeb.Resolvers.Event do def update_event( _parent, %{event_id: event_id} = args, - %{ - context: %{ - current_user: user - } - } = _resolution + %{context: %{current_user: user}} = _resolution ) do # See https://github.com/absinthe-graphql/absinthe/issues/490 with args <- Map.put(args, :options, args[:options] || %{}), {:ok, %Event{} = event} <- Mobilizon.Events.get_event_full(event_id), - {:is_owned, true, organizer_actor} <- User.owns_actor(user, event.organizer_actor_id), + {:is_owned, %Actor{} = organizer_actor} <- + User.owns_actor(user, event.organizer_actor_id), {:ok, args} <- save_attached_picture(args), {:ok, args} <- save_physical_address(args), args <- Map.put(args, :organizer_actor, organizer_actor), - { - :ok, - %Activity{ - data: %{ - "object" => %{"type" => "Event"} = _object - } - }, - %Event{} = event - } <- + {:ok, %Activity{data: %{"object" => %{"type" => "Event"} = _object}}, %Event{} = event} <- MobilizonWeb.API.Events.update_event(args, event) do {:ok, event} else {:error, :event_not_found} -> {:error, "Event not found"} - {:is_owned, _} -> + {:is_owned, nil} -> {:error, "User doesn't own actor"} end end @@ -268,24 +228,14 @@ defmodule MobilizonWeb.Resolvers.Event do # However, we need to pass it's actor ID @spec save_attached_picture(map()) :: {:ok, map()} defp save_attached_picture( - %{ - picture: %{ - picture: %{file: %Plug.Upload{} = _picture} = all_pic - } - } = args + %{picture: %{picture: %{file: %Plug.Upload{} = _picture} = all_pic}} = args ) do {:ok, Map.put(args, :picture, Map.put(all_pic, :actor_id, args.organizer_actor_id))} end # Otherwise if we use a previously uploaded picture we need to fetch it from database @spec save_attached_picture(map()) :: {:ok, map()} - defp save_attached_picture( - %{ - picture: %{ - picture_id: picture_id - } - } = args - ) do + defp save_attached_picture(%{picture: %{picture_id: picture_id}} = args) do with %Picture{} = picture <- Mobilizon.Media.get_picture(picture_id) do {:ok, Map.put(args, :picture, picture)} end @@ -295,13 +245,7 @@ defmodule MobilizonWeb.Resolvers.Event do defp save_attached_picture(args), do: {:ok, args} @spec save_physical_address(map()) :: {:ok, map()} - defp save_physical_address( - %{ - physical_address: %{ - url: physical_address_url - } - } = args - ) + defp save_physical_address(%{physical_address: %{url: physical_address_url}} = args) when not is_nil(physical_address_url) do with %Address{} = address <- Addresses.get_address_by_url(physical_address_url), args <- Map.put(args, :physical_address, address.url) do @@ -326,14 +270,10 @@ defmodule MobilizonWeb.Resolvers.Event do def delete_event( _parent, %{event_id: event_id, actor_id: actor_id}, - %{ - context: %{ - current_user: user - } - } + %{context: %{current_user: user}} ) do with {:ok, %Event{} = event} <- Mobilizon.Events.get_event(event_id), - {:is_owned, true, _} <- User.owns_actor(user, actor_id), + {:is_owned, %Actor{}} <- User.owns_actor(user, actor_id), {:event_can_be_managed, true} <- Event.can_event_be_managed_by(event, actor_id), event <- Mobilizon.Events.delete_event!(event) do {:ok, %{id: event.id}} @@ -341,7 +281,7 @@ defmodule MobilizonWeb.Resolvers.Event do {:error, :event_not_found} -> {:error, "Event not found"} - {:is_owned, false} -> + {:is_owned, nil} -> {:error, "Actor id is not owned by authenticated user"} {:event_can_be_managed, false} -> diff --git a/lib/mobilizon_web/resolvers/feed_token.ex b/lib/mobilizon_web/resolvers/feed_token.ex index 02ad34bc7..3d8242a84 100644 --- a/lib/mobilizon_web/resolvers/feed_token.ex +++ b/lib/mobilizon_web/resolvers/feed_token.ex @@ -2,10 +2,11 @@ defmodule MobilizonWeb.Resolvers.FeedToken do @moduledoc """ Handles the feed tokens-related GraphQL calls """ - require Logger + alias Mobilizon.Actors.Actor alias Mobilizon.Users.User alias Mobilizon.Events alias Mobilizon.Events.FeedToken + require Logger @doc """ Create an feed token for an user and a defined actor @@ -14,11 +15,11 @@ defmodule MobilizonWeb.Resolvers.FeedToken do def create_feed_token(_parent, %{actor_id: actor_id}, %{ context: %{current_user: %User{id: id} = user} }) do - with {:is_owned, true, _actor} <- User.owns_actor(user, actor_id), + with {:is_owned, %Actor{}} <- User.owns_actor(user, actor_id), {:ok, feed_token} <- Events.create_feed_token(%{"user_id" => id, "actor_id" => actor_id}) do {:ok, feed_token} else - {:is_owned, false} -> + {:is_owned, nil} -> {:error, "Actor id is not owned by authenticated user"} end end diff --git a/lib/mobilizon_web/resolvers/group.ex b/lib/mobilizon_web/resolvers/group.ex index 84239788a..a8e378a2f 100644 --- a/lib/mobilizon_web/resolvers/group.ex +++ b/lib/mobilizon_web/resolvers/group.ex @@ -40,19 +40,11 @@ defmodule MobilizonWeb.Resolvers.Group do def create_group( _parent, args, - %{ - context: %{ - current_user: user - } - } + %{context: %{current_user: user}} ) do with { :ok, - %Activity{ - data: %{ - "object" => %{"type" => "Group"} = _object - } - }, + %Activity{data: %{"object" => %{"type" => "Group"} = _object}}, %Actor{} = group } <- MobilizonWeb.API.Groups.create_group( @@ -66,10 +58,7 @@ defmodule MobilizonWeb.Resolvers.Group do banner: Map.get(args, "banner") } ) do - { - :ok, - group - } + {:ok, group} end end @@ -83,14 +72,10 @@ defmodule MobilizonWeb.Resolvers.Group do def delete_group( _parent, %{group_id: group_id, actor_id: actor_id}, - %{ - context: %{ - current_user: user - } - } + %{context: %{current_user: user}} ) do with {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), - {:is_owned, true, _} <- User.owns_actor(user, actor_id), + {:is_owned, %Actor{}} <- User.owns_actor(user, actor_id), {:ok, %Member{} = member} <- Member.get_member(actor_id, group.id), {:is_admin, true} <- Member.is_administrator(member), group <- Actors.delete_group!(group) do @@ -99,7 +84,7 @@ defmodule MobilizonWeb.Resolvers.Group do {:error, :group_not_found} -> {:error, "Group not found"} - {:is_owned, false} -> + {:is_owned, nil} -> {:error, "Actor id is not owned by authenticated user"} {:error, :member_not_found} -> @@ -120,37 +105,24 @@ defmodule MobilizonWeb.Resolvers.Group do def join_group( _parent, %{group_id: group_id, actor_id: actor_id}, - %{ - context: %{ - current_user: user - } - } + %{context: %{current_user: user}} ) do - with {:is_owned, true, actor} <- User.owns_actor(user, actor_id), + with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id), {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), {:error, :member_not_found} <- Member.get_member(actor.id, group.id), {:is_able_to_join, true} <- {:is_able_to_join, Member.can_be_joined(group)}, role <- Mobilizon.Actors.get_default_member_role(group), - {:ok, _} <- - Actors.create_member(%{ - parent_id: group.id, - actor_id: actor.id, - role: role - }) do + {:ok, _} <- Actors.create_member(%{parent_id: group.id, actor_id: actor.id, role: role}) do { :ok, %{ - parent: - group - |> Person.proxify_pictures(), - actor: - actor - |> Person.proxify_pictures(), + parent: Person.proxify_pictures(group), + actor: Person.proxify_pictures(actor), role: role } } else - {:is_owned, false} -> + {:is_owned, nil} -> {:error, "Actor id is not owned by authenticated user"} {:error, :group_not_found} -> @@ -174,31 +146,17 @@ defmodule MobilizonWeb.Resolvers.Group do def leave_group( _parent, %{group_id: group_id, actor_id: actor_id}, - %{ - context: %{ - current_user: user - } - } + %{context: %{current_user: user}} ) do - with {:is_owned, true, actor} <- User.owns_actor(user, actor_id), + with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id), {:ok, %Member{} = member} <- Member.get_member(actor.id, group_id), {:only_administrator, false} <- {:only_administrator, check_that_member_is_not_last_administrator(group_id, actor_id)}, {:ok, _} <- Mobilizon.Actors.delete_member(member) do - { - :ok, - %{ - parent: %{ - id: group_id - }, - actor: %{ - id: actor_id - } - } - } + {:ok, %{parent: %{id: group_id}, actor: %{id: actor_id}}} else - {:is_owned, false} -> + {:is_owned, nil} -> {:error, "Actor id is not owned by authenticated user"} {:error, :member_not_found} -> @@ -219,13 +177,7 @@ defmodule MobilizonWeb.Resolvers.Group do @spec check_that_member_is_not_last_administrator(integer(), integer()) :: boolean() defp check_that_member_is_not_last_administrator(group_id, actor_id) do case Member.list_administrator_members_for_group(group_id) do - [ - %Member{ - actor: %Actor{ - id: member_actor_id - } - } - ] -> + [%Member{actor: %Actor{id: member_actor_id}}] -> actor_id == member_actor_id _ -> diff --git a/lib/mobilizon_web/resolvers/person.ex b/lib/mobilizon_web/resolvers/person.ex index 88092c65c..347b32131 100644 --- a/lib/mobilizon_web/resolvers/person.ex +++ b/lib/mobilizon_web/resolvers/person.ex @@ -50,9 +50,7 @@ defmodule MobilizonWeb.Resolvers.Person do def create_person( _parent, %{preferred_username: _preferred_username} = args, - %{ - context: %{current_user: user} - } = _resolution + %{context: %{current_user: user}} = _resolution ) do args = Map.put(args, :user_id, user.id) @@ -75,17 +73,13 @@ defmodule MobilizonWeb.Resolvers.Person do def update_person( _parent, %{preferred_username: preferred_username} = args, - %{ - context: %{ - current_user: user - } - } = _resolution + %{context: %{current_user: user}} = _resolution ) do args = Map.put(args, :user_id, user.id) with {:find_actor, %Actor{} = actor} <- {:find_actor, Actors.get_actor_by_name(preferred_username)}, - {:is_owned, true, _} <- User.owns_actor(user, actor.id), + {:is_owned, %Actor{}} <- User.owns_actor(user, actor.id), args <- save_attached_pictures(args), {:ok, actor} <- Actors.update_actor(actor, args) do {:ok, actor} @@ -93,7 +87,7 @@ defmodule MobilizonWeb.Resolvers.Person do {:find_actor, nil} -> {:error, "Actor not found"} - {:is_owned, false} -> + {:is_owned, nil} -> {:error, "Actor is not owned by authenticated user"} end end @@ -108,15 +102,11 @@ defmodule MobilizonWeb.Resolvers.Person do def delete_person( _parent, %{preferred_username: preferred_username} = _args, - %{ - context: %{ - current_user: user - } - } = _resolution + %{context: %{current_user: user}} = _resolution ) do with {:find_actor, %Actor{} = actor} <- {:find_actor, Actors.get_actor_by_name(preferred_username)}, - {:is_owned, true, _} <- User.owns_actor(user, actor.id), + {:is_owned, %Actor{}} <- User.owns_actor(user, actor.id), {:last_identity, false} <- {:last_identity, last_identity?(user)}, {:last_admin, false} <- {:last_admin, last_admin_of_a_group?(actor.id)}, {:ok, actor} <- Actors.delete_actor(actor) do @@ -131,7 +121,7 @@ defmodule MobilizonWeb.Resolvers.Person do {:last_admin, true} -> {:error, "Cannot remove the last administrator of a group"} - {:is_owned, false} -> + {:is_owned, nil} -> {:error, "Actor is not owned by authenticated user"} end end @@ -184,14 +174,12 @@ defmodule MobilizonWeb.Resolvers.Person do @doc """ Returns the list of events this person is going to """ - def person_going_to_events(%Actor{id: actor_id}, _args, %{ - context: %{current_user: user} - }) do - with {:is_owned, true, actor} <- User.owns_actor(user, actor_id), + def person_going_to_events(%Actor{id: actor_id}, _args, %{context: %{current_user: user}}) do + with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id), events <- Events.list_event_participations_for_actor(actor) do {:ok, events} else - {:is_owned, false} -> + {:is_owned, nil} -> {:error, "Actor id is not owned by authenticated user"} end end @@ -199,9 +187,7 @@ defmodule MobilizonWeb.Resolvers.Person do @doc """ Returns the list of events this person is going to """ - def person_going_to_events(_parent, %{}, %{ - context: %{current_user: user} - }) do + def person_going_to_events(_parent, %{}, %{context: %{current_user: user}}) do with %Actor{} = actor <- Users.get_actor_for_user(user), events <- Events.list_event_participations_for_actor(actor) do {:ok, events} diff --git a/lib/mobilizon_web/resolvers/picture.ex b/lib/mobilizon_web/resolvers/picture.ex index 494ab6916..9043c2558 100644 --- a/lib/mobilizon_web/resolvers/picture.ex +++ b/lib/mobilizon_web/resolvers/picture.ex @@ -2,6 +2,7 @@ defmodule MobilizonWeb.Resolvers.Picture do @moduledoc """ Handles the picture-related GraphQL calls """ + alias Mobilizon.Actors.Actor alias Mobilizon.Media alias Mobilizon.Media.Picture alias Mobilizon.Users.User @@ -10,9 +11,7 @@ defmodule MobilizonWeb.Resolvers.Picture do Get picture for an event's pic """ def picture(%{picture_id: picture_id} = _parent, _args, _resolution) do - with {:ok, picture} <- do_fetch_picture(picture_id) do - {:ok, picture} - end + with {:ok, picture} <- do_fetch_picture(picture_id), do: {:ok, picture} end @doc """ @@ -20,15 +19,9 @@ defmodule MobilizonWeb.Resolvers.Picture do See MobilizonWeb.Resolvers.Event.create_event/3 """ - def picture(%{picture: picture} = _parent, _args, _resolution) do - {:ok, picture} - end - + def picture(%{picture: picture} = _parent, _args, _resolution), do: {:ok, picture} def picture(_parent, %{id: picture_id}, _resolution), do: do_fetch_picture(picture_id) - - def picture(_parent, _args, _resolution) do - {:ok, nil} - end + def picture(_parent, _args, _resolution), do: {:ok, nil} @spec do_fetch_picture(nil) :: {:error, nil} defp do_fetch_picture(nil), do: {:error, nil} @@ -36,7 +29,7 @@ defmodule MobilizonWeb.Resolvers.Picture do @spec do_fetch_picture(String.t()) :: {:ok, Picture.t()} | {:error, :not_found} defp do_fetch_picture(picture_id) do case Media.get_picture(picture_id) do - %Picture{id: id, file: file} = _pic -> + %Picture{id: id, file: file} -> {:ok, %{ name: file.name, @@ -46,18 +39,18 @@ defmodule MobilizonWeb.Resolvers.Picture do size: file.size }} - _err -> + _error -> {:error, "Picture with ID #{picture_id} was not found"} end end @spec upload_picture(map(), map(), map()) :: {:ok, Picture.t()} | {:error, any()} - def upload_picture(_parent, %{file: %Plug.Upload{} = file, actor_id: actor_id} = args, %{ - context: %{ - current_user: user - } - }) do - with {:is_owned, true, _actor} <- User.owns_actor(user, actor_id), + def upload_picture( + _parent, + %{file: %Plug.Upload{} = file, actor_id: actor_id} = args, + %{context: %{current_user: user}} + ) do + with {:is_owned, %Actor{}} <- User.owns_actor(user, actor_id), {:ok, %{"url" => [%{"href" => url, "mediaType" => content_type}], "size" => size}} <- MobilizonWeb.Upload.store(file), args <- @@ -76,11 +69,11 @@ defmodule MobilizonWeb.Resolvers.Picture do size: picture.file.size }} else - {:is_owned, false} -> + {:is_owned, nil} -> {:error, "Actor id is not owned by authenticated user"} - err -> - {:error, err} + error -> + {:error, error} end end diff --git a/lib/mobilizon_web/resolvers/report.ex b/lib/mobilizon_web/resolvers/report.ex index dd3655050..85796e158 100644 --- a/lib/mobilizon_web/resolvers/report.ex +++ b/lib/mobilizon_web/resolvers/report.ex @@ -10,9 +10,11 @@ defmodule MobilizonWeb.Resolvers.Report do alias MobilizonWeb.API.Reports, as: ReportsAPI import Mobilizon.Users.Guards - def list_reports(_parent, %{page: page, limit: limit}, %{ - context: %{current_user: %User{role: role}} - }) + def list_reports( + _parent, + %{page: page, limit: limit}, + %{context: %{current_user: %User{role: role}}} + ) when is_moderator(role) do {:ok, Mobilizon.Reports.list_reports(page, limit)} end @@ -21,9 +23,7 @@ defmodule MobilizonWeb.Resolvers.Report do {:error, "You need to be logged-in and a moderator to list reports"} end - def get_report(_parent, %{id: id}, %{ - context: %{current_user: %User{role: role}} - }) + def get_report(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}}) when is_moderator(role) do {:ok, Mobilizon.Reports.get_report(id)} end @@ -40,14 +40,14 @@ defmodule MobilizonWeb.Resolvers.Report do %{reporter_actor_id: reporter_actor_id} = args, %{context: %{current_user: user}} = _resolution ) do - with {:is_owned, true, _} <- User.owns_actor(user, reporter_actor_id), + with {:is_owned, %Actor{}} <- User.owns_actor(user, reporter_actor_id), {:ok, _, %Report{} = report} <- ReportsAPI.report(args) do {:ok, report} else - {:is_owned, false} -> + {:is_owned, nil} -> {:error, "Reporter actor id is not owned by authenticated user"} - _err -> + _error -> {:error, "Error while saving report"} end end @@ -62,22 +62,19 @@ defmodule MobilizonWeb.Resolvers.Report do def update_report( _parent, %{report_id: report_id, moderator_id: moderator_id, status: status}, - %{ - context: %{current_user: %User{role: role} = user} - } + %{context: %{current_user: %User{role: role} = user}} ) when is_moderator(role) do - with {:is_owned, true, _} <- User.owns_actor(user, moderator_id), - %Actor{} = actor <- Actors.get_actor!(moderator_id), + with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, moderator_id), %Report{} = report <- Mobilizon.Reports.get_report(report_id), {:ok, %Report{} = report} <- MobilizonWeb.API.Reports.update_report_status(actor, report, status) do {:ok, report} else - {:is_owned, false} -> + {:is_owned, nil} -> {:error, "Actor id is not owned by authenticated user"} - _err -> + _error -> {:error, "Error while updating report"} end end @@ -89,12 +86,10 @@ defmodule MobilizonWeb.Resolvers.Report do def create_report_note( _parent, %{report_id: report_id, moderator_id: moderator_id, content: content}, - %{ - context: %{current_user: %User{role: role} = user} - } + %{context: %{current_user: %User{role: role} = user}} ) when is_moderator(role) do - with {:is_owned, true, _} <- User.owns_actor(user, moderator_id), + with {:is_owned, %Actor{}} <- User.owns_actor(user, moderator_id), %Report{} = report <- Reports.get_report(report_id), %Actor{} = moderator <- Actors.get_local_actor_with_everything(moderator_id), {:ok, %Note{} = note} <- @@ -103,11 +98,13 @@ defmodule MobilizonWeb.Resolvers.Report do end end - def delete_report_note(_parent, %{note_id: note_id, moderator_id: moderator_id}, %{ - context: %{current_user: %User{role: role} = user} - }) + def delete_report_note( + _parent, + %{note_id: note_id, moderator_id: moderator_id}, + %{context: %{current_user: %User{role: role} = user}} + ) when is_moderator(role) do - with {:is_owned, true, _} <- User.owns_actor(user, moderator_id), + with {:is_owned, %Actor{}} <- User.owns_actor(user, moderator_id), %Note{} = note <- Reports.get_note(note_id), %Actor{} = moderator <- Actors.get_local_actor_with_everything(moderator_id), {:ok, %Note{} = note} <- diff --git a/lib/mobilizon_web/resolvers/user.ex b/lib/mobilizon_web/resolvers/user.ex index 500036fa3..d25b971f2 100644 --- a/lib/mobilizon_web/resolvers/user.ex +++ b/lib/mobilizon_web/resolvers/user.ex @@ -118,8 +118,8 @@ defmodule MobilizonWeb.Resolvers.User do {:registrations_open, false} -> {:error, "Registrations are not enabled"} - err -> - err + error -> + error end end @@ -139,9 +139,9 @@ defmodule MobilizonWeb.Resolvers.User do user: Map.put(user, :default_actor, actor) }} else - err -> + error -> Logger.info("Unable to validate user with token #{token}") - Logger.debug(inspect(err)) + Logger.debug(inspect(error)) {:error, "Unable to validate user"} end end @@ -213,7 +213,7 @@ defmodule MobilizonWeb.Resolvers.User do {:user_actor, _} -> {:error, :actor_not_from_user} - _err -> + _error -> {:error, :unable_to_change_default_actor} end end diff --git a/mix.exs b/mix.exs index 83d043e07..f54af6661 100644 --- a/mix.exs +++ b/mix.exs @@ -200,7 +200,7 @@ defmodule Mobilizon.Mixfile do Mobilizon.Events.TagRelation, Mobilizon.Users, Mobilizon.Users.User, - Mobilizon.Users.UserRoleEnum, + Mobilizon.Users.UserRole, Mobilizon.Users.Guards, Mobilizon.Activity, Mobilizon.Ecto, diff --git a/priv/repo/migrations/20190307125009_move_user_role_to_enum.exs b/priv/repo/migrations/20190307125009_move_user_role_to_enum.exs index 1508391ce..66f8ce5be 100644 --- a/priv/repo/migrations/20190307125009_move_user_role_to_enum.exs +++ b/priv/repo/migrations/20190307125009_move_user_role_to_enum.exs @@ -1,13 +1,13 @@ defmodule Mobilizon.Repo.Migrations.MoveUserRoleToEnum do use Ecto.Migration - alias Mobilizon.Users.UserRoleEnum + alias Mobilizon.Users.UserRole def up do - UserRoleEnum.create_type() + UserRole.create_type() alter table(:users) do - add(:role_tmp, UserRoleEnum.type(), default: "user") + add(:role_tmp, UserRole.type(), default: "user") end execute("UPDATE users set role_tmp = 'user' where role = 0") @@ -34,7 +34,7 @@ defmodule Mobilizon.Repo.Migrations.MoveUserRoleToEnum do remove(:role) end - UserRoleEnum.drop_type() + UserRole.drop_type() rename(table(:users), :role_tmp, to: :role) end diff --git a/test/mobilizon_web/resolvers/report_resolver_test.exs b/test/mobilizon_web/resolvers/report_resolver_test.exs index 915ccbbe0..cf369a4dd 100644 --- a/test/mobilizon_web/resolvers/report_resolver_test.exs +++ b/test/mobilizon_web/resolvers/report_resolver_test.exs @@ -195,7 +195,8 @@ defmodule MobilizonWeb.Resolvers.ReportResolverTest do assert json_response(res, 200)["errors"] == nil assert json_response(res, 200)["data"]["reports"] - |> Enum.map(fn report -> Map.get(report, "id") end) == + |> Enum.map(fn report -> Map.get(report, "id") end) + |> Enum.sort() == Enum.map([report_1_id, report_2_id, report_3_id], &to_string/1) query = """ From 96f51d3a272520d3f2accca93ba9f9ca00e03fd1 Mon Sep 17 00:00:00 2001 From: miffy Date: Sat, 7 Sep 2019 23:58:53 +0200 Subject: [PATCH 05/29] Refactoring of Email context --- lib/mobilizon/email/admin.ex | 48 +++++++++++------------ lib/mobilizon/email/email.ex | 17 ++++++++ lib/mobilizon/email/user.ex | 75 +++++++++++++++++++----------------- lib/mobilizon/mailer.ex | 2 +- 4 files changed, 81 insertions(+), 61 deletions(-) create mode 100644 lib/mobilizon/email/email.ex diff --git a/lib/mobilizon/email/admin.ex b/lib/mobilizon/email/admin.ex index 2157257a6..5a2f78cf7 100644 --- a/lib/mobilizon/email/admin.ex +++ b/lib/mobilizon/email/admin.ex @@ -1,38 +1,36 @@ defmodule Mobilizon.Email.Admin do @moduledoc """ - Handles emails sent to admins + Handles emails sent to admins. """ + + use Bamboo.Phoenix, view: Mobilizon.EmailView + + import Bamboo.{Email, Phoenix} + + import MobilizonWeb.Gettext + + alias Mobilizon.{Config, Email} + alias Mobilizon.Reports.Report alias Mobilizon.Users.User - import Bamboo.Email - import Bamboo.Phoenix - use Bamboo.Phoenix, view: Mobilizon.EmailView - import MobilizonWeb.Gettext - alias Mobilizon.Reports.Report - - def report(%User{email: email} = _user, %Report{} = report, locale \\ "en") do + @spec report(User.t(), Report.t(), String.t()) :: Bamboo.Email.t() + def report(%User{email: email}, %Report{} = report, locale \\ "en") do Gettext.put_locale(locale) - instance_url = get_config(:hostname) - base_email() + instance_url = Config.instance_url() + + subject = + gettext( + "Mobilizon: New report on instance %{instance}", + instance: instance_url + ) + + Email.base_email() |> to(email) - |> subject(gettext("Mobilizon: New report on instance %{instance}", instance: instance_url)) - |> put_header("Reply-To", get_config(:email_reply_to)) + |> subject(subject) + |> put_header("Reply-To", Config.instance_email_reply_to()) |> assign(:report, report) |> assign(:instance, instance_url) |> render(:report) end - - defp base_email do - # Here you can set a default from, default headers, etc. - new_email() - |> from(get_config(:email_from)) - |> put_html_layout({Mobilizon.EmailView, "email.html"}) - |> put_text_layout({Mobilizon.EmailView, "email.text"}) - end - - @spec get_config(atom()) :: any() - defp get_config(key) do - Mobilizon.CommonConfig.instance_config() |> Keyword.get(key) - end end diff --git a/lib/mobilizon/email/email.ex b/lib/mobilizon/email/email.ex new file mode 100644 index 000000000..0a849583c --- /dev/null +++ b/lib/mobilizon/email/email.ex @@ -0,0 +1,17 @@ +defmodule Mobilizon.Email do + @moduledoc """ + The Email context. + """ + + use Bamboo.Phoenix, view: Mobilizon.EmailView + + alias Mobilizon.Config + + @spec base_email :: Bamboo.Email.t() + def base_email do + new_email() + |> from(Config.instance_email_from()) + |> put_html_layout({Mobilizon.EmailView, "email.html"}) + |> put_text_layout({Mobilizon.EmailView, "email.text"}) + end +end diff --git a/lib/mobilizon/email/user.ex b/lib/mobilizon/email/user.ex index 53bdb4a1d..a05778254 100644 --- a/lib/mobilizon/email/user.ex +++ b/lib/mobilizon/email/user.ex @@ -1,57 +1,62 @@ defmodule Mobilizon.Email.User do @moduledoc """ - Handles emails sent to users + Handles emails sent to users. """ - alias Mobilizon.Users.User - import Bamboo.Email - import Bamboo.Phoenix use Bamboo.Phoenix, view: Mobilizon.EmailView + + import Bamboo.{Email, Phoenix} + import MobilizonWeb.Gettext - def confirmation_email(%User{} = user, locale \\ "en") do - Gettext.put_locale(locale) - instance_url = get_config(:instance) + alias Mobilizon.{Config, Email} + alias Mobilizon.Users.User - base_email() - |> to(user.email) - |> subject( - gettext("Mobilizon: Confirmation instructions for %{instance}", instance: instance_url) - ) - |> put_header("Reply-To", get_config(:email_reply_to)) - |> assign(:token, user.confirmation_token) + @spec confirmation_email(User.t(), String.t()) :: Bamboo.Email.t() + def confirmation_email( + %User{email: email, confirmation_token: confirmation_token}, + locale \\ "en" + ) do + Gettext.put_locale(locale) + + instance_url = Config.instance_url() + + subject = + gettext( + "Mobilizon: Confirmation instructions for %{instance}", + instance: instance_url + ) + + Email.base_email() + |> to(email) + |> subject(subject) + |> put_header("Reply-To", Config.instance_email_reply_to()) + |> assign(:token, confirmation_token) |> assign(:instance, instance_url) |> render(:registration_confirmation) end - def reset_password_email(%User{} = user, locale \\ "en") do + @spec reset_password_email(User.t(), String.t()) :: Bamboo.Email.t() + def reset_password_email( + %User{email: email, reset_password_token: reset_password_token}, + locale \\ "en" + ) do Gettext.put_locale(locale) - instance_url = get_config(:hostname) - base_email() - |> to(user.email) - |> subject( + instance_url = Config.instance_url() + + subject = gettext( "Mobilizon: Reset your password on %{instance} instructions", instance: instance_url ) - ) - |> put_header("Reply-To", get_config(:email_reply_to)) - |> assign(:token, user.reset_password_token) + + Email.base_email() + |> to(email) + |> subject(subject) + |> put_header("Reply-To", Config.instance_email_reply_to()) + |> assign(:token, reset_password_token) |> assign(:instance, instance_url) |> render(:password_reset) end - - defp base_email do - # Here you can set a default from, default headers, etc. - new_email() - |> from(get_config(:email_from)) - |> put_html_layout({Mobilizon.EmailView, "email.html"}) - |> put_text_layout({Mobilizon.EmailView, "email.text"}) - end - - @spec get_config(atom()) :: any() - defp get_config(key) do - Mobilizon.CommonConfig.instance_config() |> Keyword.get(key) - end end diff --git a/lib/mobilizon/mailer.ex b/lib/mobilizon/mailer.ex index 0884ceb77..177399ebb 100644 --- a/lib/mobilizon/mailer.ex +++ b/lib/mobilizon/mailer.ex @@ -1,6 +1,6 @@ defmodule Mobilizon.Mailer do @moduledoc """ - Mailer + Mobilizon Mailer. """ use Bamboo.Mailer, otp_app: :mobilizon end From a32ab590abe293ea8a82f8f96ac724c29e23f3c1 Mon Sep 17 00:00:00 2001 From: miffy Date: Sun, 8 Sep 2019 00:05:54 +0200 Subject: [PATCH 06/29] Refactoring of config wrapper --- lib/mobilizon/actors/actor.ex | 8 +- lib/mobilizon/application.ex | 6 +- lib/mobilizon/common-config.ex | 71 ----------------- lib/mobilizon/config.ex | 77 +++++++++++++++++++ lib/mobilizon_web/api/utils.ex | 4 +- .../controllers/activity_pub_controller.ex | 8 +- .../controllers/media_proxy_controller.ex | 5 +- .../controllers/node_info_controller.ex | 14 ++-- lib/mobilizon_web/media_proxy.ex | 5 +- lib/mobilizon_web/plugs/uploaded_media.ex | 10 ++- lib/mobilizon_web/resolvers/config.ex | 12 +-- lib/mobilizon_web/resolvers/user.ex | 10 ++- lib/mobilizon_web/upload.ex | 18 +++-- .../upload/filter/anonymize_filename.ex | 5 +- lib/mobilizon_web/upload/filter/mogrify.ex | 5 +- lib/mobilizon_web/uploaders/local.ex | 5 +- lib/service/activity_pub/activity_pub.ex | 3 +- test/mobilizon/actors/actors_test.exs | 17 ++-- test/mobilizon/media/media_test.exs | 7 +- .../activity_pub_controller_test.exs | 6 +- .../controllers/nodeinfo_controller_test.exs | 12 +-- test/mobilizon_web/media_proxy_test.exs | 32 ++++---- .../resolvers/user_resolver_test.exs | 4 +- test/mobilizon_web/upload_test.exs | 9 ++- test/support/data_case.ex | 14 ++-- 25 files changed, 207 insertions(+), 160 deletions(-) delete mode 100644 lib/mobilizon/common-config.ex create mode 100644 lib/mobilizon/config.ex diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex index 2cf983479..6fe728e2a 100644 --- a/lib/mobilizon/actors/actor.ex +++ b/lib/mobilizon/actors/actor.ex @@ -30,10 +30,11 @@ defmodule Mobilizon.Actors.Actor do import Ecto.Changeset alias Mobilizon.Actors - alias Mobilizon.Users.User alias Mobilizon.Actors.{Actor, Follower, Member} + alias Mobilizon.Config alias Mobilizon.Events.{Event, FeedToken} alias Mobilizon.Media.File + alias Mobilizon.Users.User alias Mobilizon.Reports.{Report, Note} @@ -207,9 +208,8 @@ defmodule Mobilizon.Actors.Actor do pem = [entry] |> :public_key.pem_encode() |> String.trim_trailing() vars = %{ - "name" => Mobilizon.CommonConfig.get([:instance, :name], "Mobilizon"), - "summary" => - Mobilizon.CommonConfig.get( + "name" => Config.get([:instance, :name], "Mobilizon"), + "summary" => Config.get( [:instance, :description], "An internal service actor for this Mobilizon instance" ), diff --git a/lib/mobilizon/application.ex b/lib/mobilizon/application.ex index 22bf699d8..3bbb4b0f3 100644 --- a/lib/mobilizon/application.ex +++ b/lib/mobilizon/application.ex @@ -2,8 +2,12 @@ defmodule Mobilizon.Application do @moduledoc """ The Mobilizon application """ + use Application + import Cachex.Spec + + alias Mobilizon.Config alias Mobilizon.Service.Export.{Feed, ICalendar} @name Mix.Project.config()[:name] @@ -90,7 +94,7 @@ defmodule Mobilizon.Application do def user_agent do info = - "#{MobilizonWeb.Endpoint.url()} <#{Mobilizon.CommonConfig.get([:instance, :email], "")}>" + "#{MobilizonWeb.Endpoint.url()} <#{Config.get([:instance, :email], "")}>" named_version() <> "; " <> info end diff --git a/lib/mobilizon/common-config.ex b/lib/mobilizon/common-config.ex deleted file mode 100644 index 091857777..000000000 --- a/lib/mobilizon/common-config.ex +++ /dev/null @@ -1,71 +0,0 @@ -defmodule Mobilizon.CommonConfig do - @moduledoc """ - Instance configuration wrapper - """ - - def registrations_open?() do - instance_config() - |> get_in([:registrations_open]) - |> to_bool - end - - def instance_name() do - instance_config() - |> get_in([:name]) - end - - def instance_description() do - instance_config() - |> get_in([:description]) - end - - def instance_hostname() do - instance_config() - |> get_in([:hostname]) - end - - def instance_config(), do: Application.get_env(:mobilizon, :instance) - - defp to_bool(v), do: v == true or v == "true" or v == "True" - - def get(key), do: get(key, nil) - - def get([key], default), do: get(key, default) - - def get([parent_key | keys], default) do - case :mobilizon - |> Application.get_env(parent_key) - |> get_in(keys) do - nil -> default - any -> any - end - end - - def get(key, default) do - Application.get_env(:mobilizon, key, default) - end - - def get!(key) do - value = get(key, nil) - - if value == nil do - raise("Missing configuration value: #{inspect(key)}") - else - value - end - end - - def put([key], value), do: put(key, value) - - def put([parent_key | keys], value) do - parent = - Application.get_env(:mobilizon, parent_key) - |> put_in(keys, value) - - Application.put_env(:mobilizon, parent_key, parent) - end - - def put(key, value) do - Application.put_env(:mobilizon, key, value) - end -end diff --git a/lib/mobilizon/config.ex b/lib/mobilizon/config.ex new file mode 100644 index 000000000..1d3581baa --- /dev/null +++ b/lib/mobilizon/config.ex @@ -0,0 +1,77 @@ +defmodule Mobilizon.Config do + @moduledoc """ + Configuration wrapper. + """ + + @spec instance_config :: keyword + def instance_config, do: Application.get_env(:mobilizon, :instance) + + @spec instance_url :: String.t() + def instance_url, do: instance_config()[:instance] + + @spec instance_name :: String.t() + def instance_name, do: instance_config()[:name] + + @spec instance_description :: String.t() + def instance_description, do: instance_config()[:description] + + @spec instance_version :: String.t() + def instance_version, do: instance_config()[:version] + + @spec instance_hostname :: String.t() + def instance_hostname, do: instance_config()[:hostname] + + @spec instance_registrations_open? :: boolean + def instance_registrations_open?, do: to_boolean(instance_config()[:registrations_open]) + + @spec instance_repository :: String.t() + def instance_repository, do: instance_config()[:repository] + + @spec instance_email_from :: String.t() + def instance_email_from, do: instance_config()[:email_from] + + @spec instance_email_reply_to :: String.t() + def instance_email_reply_to, do: instance_config()[:email_reply_to] + + @spec get(module | atom) :: any + def get(key), do: get(key, nil) + + @spec get([module | atom]) :: any + def get([key], default), do: get(key, default) + + def get([parent_key | keys], default) do + case get_in(Application.get_env(:mobilizon, parent_key), keys) do + nil -> default + any -> any + end + end + + @spec get(module | atom, any) :: any + def get(key, default), do: Application.get_env(:mobilizon, key, default) + + @spec get!(module | atom) :: any + def get!(key) do + value = get(key, nil) + + if value == nil do + raise("Missing configuration value: #{inspect(key)}") + else + value + end + end + + @spec put([module | atom], any) :: any + def put([key], value), do: put(key, value) + + def put([parent_key | keys], value) do + parent = put_in(Application.get_env(:mobilizon, parent_key), keys, value) + + Application.put_env(:mobilizon, parent_key, parent) + end + + @spec put(module | atom, any) :: any + def put(key, value), do: Application.put_env(:mobilizon, key, value) + + @spec to_boolean(boolean | String.t()) :: boolean + defp to_boolean(boolean), do: "true" == String.downcase("#{boolean}") +end diff --git a/lib/mobilizon_web/api/utils.ex b/lib/mobilizon_web/api/utils.ex index 6c983e447..c99710093 100644 --- a/lib/mobilizon_web/api/utils.ex +++ b/lib/mobilizon_web/api/utils.ex @@ -2,7 +2,9 @@ defmodule MobilizonWeb.API.Utils do @moduledoc """ Utils for API """ + alias Mobilizon.Actors.Actor + alias Mobilizon.Config alias Mobilizon.Service.Formatter @doc """ @@ -125,7 +127,7 @@ defmodule MobilizonWeb.API.Utils do def make_report_content_html(nil), do: {:ok, {nil, [], []}} def make_report_content_html(comment) do - max_size = Mobilizon.CommonConfig.get([:instance, :max_report_comment_size], 1000) + max_size = Config.get([:instance, :max_report_comment_size], 1000) if String.length(comment) <= max_size do {:ok, Formatter.html_escape(comment, "text/plain")} diff --git a/lib/mobilizon_web/controllers/activity_pub_controller.ex b/lib/mobilizon_web/controllers/activity_pub_controller.ex index 63e7d3fb9..1bdbbf49a 100644 --- a/lib/mobilizon_web/controllers/activity_pub_controller.ex +++ b/lib/mobilizon_web/controllers/activity_pub_controller.ex @@ -5,11 +5,13 @@ defmodule MobilizonWeb.ActivityPubController do use MobilizonWeb, :controller - alias Mobilizon.{Actors, Actors.Actor} - alias MobilizonWeb.ActivityPub.ActorView + + alias Mobilizon.{Actors, Actors.Actor, Config} alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.Federator + alias MobilizonWeb.ActivityPub.ActorView + require Logger action_fallback(:errors) @@ -17,7 +19,7 @@ defmodule MobilizonWeb.ActivityPubController do plug(:relay_active? when action in [:relay]) def relay_active?(conn, _) do - if Mobilizon.CommonConfig.get([:instance, :allow_relay]) do + if Config.get([:instance, :allow_relay]) do conn else conn diff --git a/lib/mobilizon_web/controllers/media_proxy_controller.ex b/lib/mobilizon_web/controllers/media_proxy_controller.ex index 55f3fc20a..630d8b25f 100644 --- a/lib/mobilizon_web/controllers/media_proxy_controller.ex +++ b/lib/mobilizon_web/controllers/media_proxy_controller.ex @@ -5,13 +5,16 @@ defmodule MobilizonWeb.MediaProxyController do use MobilizonWeb, :controller + + alias Mobilizon.Config + alias MobilizonWeb.ReverseProxy alias MobilizonWeb.MediaProxy @default_proxy_opts [max_body_length: 25 * 1_048_576, http: [follow_redirect: true]] def remote(conn, %{"sig" => sig64, "url" => url64} = params) do - with config <- Mobilizon.CommonConfig.get([:media_proxy], []), + with config <- Config.get([:media_proxy], []), true <- Keyword.get(config, :enabled, false), {:ok, url} <- MediaProxy.decode_url(sig64, url64), :ok <- filename_matches(Map.has_key?(params, "filename"), conn.request_path, url) do diff --git a/lib/mobilizon_web/controllers/node_info_controller.ex b/lib/mobilizon_web/controllers/node_info_controller.ex index 31bf6bc18..a2c2f45d3 100644 --- a/lib/mobilizon_web/controllers/node_info_controller.ex +++ b/lib/mobilizon_web/controllers/node_info_controller.ex @@ -6,10 +6,8 @@ defmodule MobilizonWeb.NodeInfoController do use MobilizonWeb, :controller - alias Mobilizon.{Events, Users} - alias Mobilizon.CommonConfig + alias Mobilizon.{Config, Events, Users} - @instance Application.get_env(:mobilizon, :instance) @node_info_supported_versions ["2.0", "2.1"] @node_info_schema_uri "http://nodeinfo.diaspora.software/ns/schema/" @@ -35,14 +33,14 @@ defmodule MobilizonWeb.NodeInfoController do version: version, software: %{ name: "mobilizon", - version: Keyword.get(@instance, :version) + version: Config.instance_version() }, protocols: ["activitypub"], services: %{ inbound: [], outbound: ["atom1.0"] }, - openRegistrations: CommonConfig.registrations_open?(), + openRegistrations: Config.instance_registrations_open?(), usage: %{ users: %{ total: Users.count_users() @@ -51,14 +49,14 @@ defmodule MobilizonWeb.NodeInfoController do localComments: Events.count_local_comments() }, metadata: %{ - nodeName: CommonConfig.instance_name(), - nodeDescription: CommonConfig.instance_description() + nodeName: Config.instance_name(), + nodeDescription: Config.instance_description() } } response = if version == "2.1" do - put_in(response, [:software, :repository], Keyword.get(@instance, :repository)) + put_in(response, [:software, :repository], Config.instance_repository()) else response end diff --git a/lib/mobilizon_web/media_proxy.ex b/lib/mobilizon_web/media_proxy.ex index 229e50e24..0f03544e2 100644 --- a/lib/mobilizon_web/media_proxy.ex +++ b/lib/mobilizon_web/media_proxy.ex @@ -7,6 +7,9 @@ defmodule MobilizonWeb.MediaProxy do @moduledoc """ Handles proxifying media files """ + + alias Mobilizon.Config + @base64_opts [padding: false] def url(nil), do: nil @@ -66,7 +69,7 @@ defmodule MobilizonWeb.MediaProxy do def build_url(sig_base64, url_base64, filename \\ nil) do [ - Mobilizon.CommonConfig.get([:media_proxy, :base_url], MobilizonWeb.Endpoint.url()), + Config.get([:media_proxy, :base_url], MobilizonWeb.Endpoint.url()), "proxy", sig_base64, url_base64, diff --git a/lib/mobilizon_web/plugs/uploaded_media.ex b/lib/mobilizon_web/plugs/uploaded_media.ex index f755e0e87..13164a8d5 100644 --- a/lib/mobilizon_web/plugs/uploaded_media.ex +++ b/lib/mobilizon_web/plugs/uploaded_media.ex @@ -8,10 +8,14 @@ defmodule MobilizonWeb.Plugs.UploadedMedia do Serves uploaded media files """ + @behaviour Plug + import Plug.Conn + + alias Mobilizon.Config + require Logger - @behaviour Plug # no slashes @path "media" @@ -38,7 +42,7 @@ defmodule MobilizonWeb.Plugs.UploadedMedia do conn end - config = Mobilizon.CommonConfig.get([MobilizonWeb.Upload]) + config = Config.get([MobilizonWeb.Upload]) with uploader <- Keyword.fetch!(config, :uploader), proxy_remote = Keyword.get(config, :proxy_remote, false), @@ -75,7 +79,7 @@ defmodule MobilizonWeb.Plugs.UploadedMedia do conn |> MobilizonWeb.ReverseProxy.call( url, - Mobilizon.CommonConfig.get([Mobilizon.Upload, :proxy_opts], []) + Config.get([Mobilizon.Upload, :proxy_opts], []) ) end diff --git a/lib/mobilizon_web/resolvers/config.ex b/lib/mobilizon_web/resolvers/config.ex index 3cb632da9..dc762c1e2 100644 --- a/lib/mobilizon_web/resolvers/config.ex +++ b/lib/mobilizon_web/resolvers/config.ex @@ -1,19 +1,19 @@ defmodule MobilizonWeb.Resolvers.Config do @moduledoc """ - Handles the config-related GraphQL calls + Handles the config-related GraphQL calls. """ - import Mobilizon.CommonConfig + alias Mobilizon.Config @doc """ - Get config + Gets config. """ def get_config(_parent, _params, _context) do {:ok, %{ - name: instance_name(), - registrations_open: registrations_open?(), - description: instance_description() + name: Config.instance_name(), + registrations_open: Config.instance_registrations_open?(), + description: Config.instance_description() }} end end diff --git a/lib/mobilizon_web/resolvers/user.ex b/lib/mobilizon_web/resolvers/user.ex index d25b971f2..5d954f35c 100644 --- a/lib/mobilizon_web/resolvers/user.ex +++ b/lib/mobilizon_web/resolvers/user.ex @@ -2,12 +2,14 @@ defmodule MobilizonWeb.Resolvers.User do @moduledoc """ Handles the user-related GraphQL calls """ + + alias Mobilizon.{Actors, Config, Users} alias Mobilizon.Actors.Actor - alias Mobilizon.CommonConfig - alias Mobilizon.Users.User - alias Mobilizon.{Actors, Users} alias Mobilizon.Service.Users.{ResetPassword, Activation} + alias Mobilizon.Users.User + import Mobilizon.Users.Guards + require Logger @doc """ @@ -110,7 +112,7 @@ defmodule MobilizonWeb.Resolvers.User do """ @spec create_user(any(), map(), any()) :: tuple() def create_user(_parent, args, _resolution) do - with {:registrations_open, true} <- {:registrations_open, CommonConfig.registrations_open?()}, + with {:registrations_open, true} <- {:registrations_open, Config.instance_registrations_open?()}, {:ok, %User{} = user} <- Users.register(args) do Activation.send_confirmation_email(user) {:ok, user} diff --git a/lib/mobilizon_web/upload.ex b/lib/mobilizon_web/upload.ex index 486d820f1..b245f74b6 100644 --- a/lib/mobilizon_web/upload.ex +++ b/lib/mobilizon_web/upload.ex @@ -31,7 +31,11 @@ defmodule MobilizonWeb.Upload do * `MobilizonWeb.Upload.Filter` """ + alias Ecto.UUID + + alias Mobilizon.Config + require Logger @type source :: @@ -110,26 +114,26 @@ defmodule MobilizonWeb.Upload do {size_limit, activity_type} = case Keyword.get(opts, :type) do :banner -> - {Mobilizon.CommonConfig.get!([:instance, :banner_upload_limit]), "Image"} + {Config.get!([:instance, :banner_upload_limit]), "Image"} :avatar -> - {Mobilizon.CommonConfig.get!([:instance, :avatar_upload_limit]), "Image"} + {Config.get!([:instance, :avatar_upload_limit]), "Image"} _ -> - {Mobilizon.CommonConfig.get!([:instance, :upload_limit]), nil} + {Config.get!([:instance, :upload_limit]), nil} end %{ activity_type: Keyword.get(opts, :activity_type, activity_type), size_limit: Keyword.get(opts, :size_limit, size_limit), - uploader: Keyword.get(opts, :uploader, Mobilizon.CommonConfig.get([__MODULE__, :uploader])), - filters: Keyword.get(opts, :filters, Mobilizon.CommonConfig.get([__MODULE__, :filters])), + uploader: Keyword.get(opts, :uploader, Config.get([__MODULE__, :uploader])), + filters: Keyword.get(opts, :filters, Config.get([__MODULE__, :filters])), description: Keyword.get(opts, :description), base_url: Keyword.get( opts, :base_url, - Mobilizon.CommonConfig.get([__MODULE__, :base_url], MobilizonWeb.Endpoint.url()) + Config.get([__MODULE__, :base_url], MobilizonWeb.Endpoint.url()) ) } end @@ -173,7 +177,7 @@ defmodule MobilizonWeb.Upload do defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do path = URI.encode(path, &char_unescaped?/1) <> - if Mobilizon.CommonConfig.get([__MODULE__, :link_name], false) do + if Config.get([__MODULE__, :link_name], false) do "?name=#{URI.encode(name, &char_unescaped?/1)}" else "" diff --git a/lib/mobilizon_web/upload/filter/anonymize_filename.ex b/lib/mobilizon_web/upload/filter/anonymize_filename.ex index 57c5cbb76..290b9df5a 100644 --- a/lib/mobilizon_web/upload/filter/anonymize_filename.ex +++ b/lib/mobilizon_web/upload/filter/anonymize_filename.ex @@ -9,11 +9,14 @@ defmodule MobilizonWeb.Upload.Filter.AnonymizeFilename do Should be used after `MobilizonWeb.Upload.Filter.Dedupe`. """ + @behaviour MobilizonWeb.Upload.Filter + alias Mobilizon.Config + def filter(upload) do extension = List.last(String.split(upload.name, ".")) - name = Mobilizon.CommonConfig.get([__MODULE__, :text], random(extension)) + name = Config.get([__MODULE__, :text], random(extension)) {:ok, %MobilizonWeb.Upload{upload | name: name}} end diff --git a/lib/mobilizon_web/upload/filter/mogrify.ex b/lib/mobilizon_web/upload/filter/mogrify.ex index f0d1f42c2..e7ae715cd 100644 --- a/lib/mobilizon_web/upload/filter/mogrify.ex +++ b/lib/mobilizon_web/upload/filter/mogrify.ex @@ -7,13 +7,16 @@ defmodule MobilizonWeb.Upload.Filter.Mogrify do @moduledoc """ Handle mogrify transformations """ + @behaviour MobilizonWeb.Upload.Filter + alias Mobilizon.Config + @type conversion :: action :: String.t() | {action :: String.t(), opts :: String.t()} @type conversions :: conversion() | [conversion()] def filter(%MobilizonWeb.Upload{tempfile: file, content_type: "image" <> _}) do - filters = Mobilizon.CommonConfig.get!([__MODULE__, :args]) + filters = Config.get!([__MODULE__, :args]) file |> Mogrify.open() diff --git a/lib/mobilizon_web/uploaders/local.ex b/lib/mobilizon_web/uploaders/local.ex index 82d87ae56..4e5f0ea99 100644 --- a/lib/mobilizon_web/uploaders/local.ex +++ b/lib/mobilizon_web/uploaders/local.ex @@ -7,8 +7,11 @@ defmodule MobilizonWeb.Uploaders.Local do @moduledoc """ Local uploader for files """ + @behaviour MobilizonWeb.Uploaders.Uploader + alias Mobilizon.Config + def get_file(_) do {:ok, {:static_dir, upload_path()}} end @@ -59,6 +62,6 @@ defmodule MobilizonWeb.Uploaders.Local do end def upload_path do - Mobilizon.CommonConfig.get!([__MODULE__, :uploads]) + Config.get!([__MODULE__, :uploads]) end end diff --git a/lib/service/activity_pub/activity_pub.ex b/lib/service/activity_pub/activity_pub.ex index 6cad45aab..c0d98a0ae 100644 --- a/lib/service/activity_pub/activity_pub.ex +++ b/lib/service/activity_pub/activity_pub.ex @@ -10,6 +10,7 @@ defmodule Mobilizon.Service.ActivityPub do Every ActivityPub method """ + alias Mobilizon.Config alias Mobilizon.Events alias Mobilizon.Events.{Event, Comment, Participant} alias Mobilizon.Service.ActivityPub.Transmogrifier @@ -521,7 +522,7 @@ defmodule Mobilizon.Service.ActivityPub do public = is_public?(activity) - if public && Mobilizon.CommonConfig.get([:instance, :allow_relay]) do + if public && Config.get([:instance, :allow_relay]) do Logger.info(fn -> "Relaying #{activity.data["id"]} out" end) Mobilizon.Service.ActivityPub.Relay.publish(activity) end diff --git a/test/mobilizon/actors/actors_test.exs b/test/mobilizon/actors/actors_test.exs index 6b2ae6917..2c589a0da 100644 --- a/test/mobilizon/actors/actors_test.exs +++ b/test/mobilizon/actors/actors_test.exs @@ -1,9 +1,8 @@ defmodule Mobilizon.ActorsTest do use Mobilizon.DataCase - alias Mobilizon.Actors + alias Mobilizon.{Actors, Config, Users} alias Mobilizon.Actors.{Actor, Member, Follower, Bot} - alias Mobilizon.Users alias Mobilizon.Media.File, as: FileModel import Mobilizon.Factory use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney @@ -281,12 +280,12 @@ defmodule Mobilizon.ActorsTest do %URI{path: "/media/" <> banner_path} = URI.parse(banner_url) assert File.exists?( - Mobilizon.CommonConfig.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> + Config.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> "/" <> avatar_path ) assert File.exists?( - Mobilizon.CommonConfig.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> + Config.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> "/" <> banner_path ) @@ -312,12 +311,12 @@ defmodule Mobilizon.ActorsTest do refute actor.suspended refute File.exists?( - Mobilizon.CommonConfig.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> + Config.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> "/" <> avatar_path ) assert File.exists?( - Mobilizon.CommonConfig.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> + Config.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> "/" <> banner_path ) end @@ -335,12 +334,12 @@ defmodule Mobilizon.ActorsTest do %URI{path: "/media/" <> banner_path} = URI.parse(banner_url) assert File.exists?( - Mobilizon.CommonConfig.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> + Config.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> "/" <> avatar_path ) assert File.exists?( - Mobilizon.CommonConfig.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> + Config.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> "/" <> banner_path ) @@ -348,7 +347,7 @@ defmodule Mobilizon.ActorsTest do assert_raise Ecto.NoResultsError, fn -> Actors.get_actor!(actor_id) end refute File.exists?( - Mobilizon.CommonConfig.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> + Config.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> "/" <> banner_path ) end diff --git a/test/mobilizon/media/media_test.exs b/test/mobilizon/media/media_test.exs index 50b3b96ee..2fb5e3f21 100644 --- a/test/mobilizon/media/media_test.exs +++ b/test/mobilizon/media/media_test.exs @@ -1,9 +1,10 @@ defmodule Mobilizon.MediaTest do use Mobilizon.DataCase - alias Mobilizon.Media import Mobilizon.Factory + alias Mobilizon.{Config, Media} + describe "media" do setup [:ensure_local_uploader] alias Mobilizon.Media.Picture @@ -48,7 +49,7 @@ defmodule Mobilizon.MediaTest do %URI{path: "/media/" <> path} = URI.parse(picture.file.url) assert File.exists?( - Mobilizon.CommonConfig.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> + Config.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> "/" <> path ) @@ -56,7 +57,7 @@ defmodule Mobilizon.MediaTest do assert_raise Ecto.NoResultsError, fn -> Media.get_picture!(picture.id) end refute File.exists?( - Mobilizon.CommonConfig.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> + Config.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> "/" <> path ) end diff --git a/test/mobilizon_web/controllers/activity_pub_controller_test.exs b/test/mobilizon_web/controllers/activity_pub_controller_test.exs index df97d7763..54cb958da 100644 --- a/test/mobilizon_web/controllers/activity_pub_controller_test.exs +++ b/test/mobilizon_web/controllers/activity_pub_controller_test.exs @@ -8,7 +8,7 @@ defmodule MobilizonWeb.ActivityPubControllerTest do import Mobilizon.Factory alias MobilizonWeb.ActivityPub.ActorView alias MobilizonWeb.PageView - alias Mobilizon.Actors + alias Mobilizon.{Actors, Config} alias Mobilizon.Actors.Actor alias Mobilizon.Service.ActivityPub use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney @@ -290,14 +290,14 @@ defmodule MobilizonWeb.ActivityPubControllerTest do end test "with the relay disabled, it returns 404", %{conn: conn} do - Mobilizon.CommonConfig.put([:instance, :allow_relay], false) + Config.put([:instance, :allow_relay], false) conn |> get(activity_pub_path(conn, :relay)) |> json_response(404) |> assert - Mobilizon.CommonConfig.put([:instance, :allow_relay], true) + Config.put([:instance, :allow_relay], true) end end diff --git a/test/mobilizon_web/controllers/nodeinfo_controller_test.exs b/test/mobilizon_web/controllers/nodeinfo_controller_test.exs index 44592c05c..9cd94f63f 100644 --- a/test/mobilizon_web/controllers/nodeinfo_controller_test.exs +++ b/test/mobilizon_web/controllers/nodeinfo_controller_test.exs @@ -1,7 +1,7 @@ defmodule MobilizonWeb.NodeInfoControllerTest do use MobilizonWeb.ConnCase - @instance Application.get_env(:mobilizon, :instance) + alias Mobilizon.Config test "Get node info schemas", %{conn: conn} do conn = get(conn, node_info_path(conn, :schemas)) @@ -37,16 +37,16 @@ defmodule MobilizonWeb.NodeInfoControllerTest do assert resp == %{ "metadata" => %{ - "nodeName" => Mobilizon.CommonConfig.instance_name(), - "nodeDescription" => Mobilizon.CommonConfig.instance_description() + "nodeName" => Config.instance_name(), + "nodeDescription" => Config.instance_description() }, - "openRegistrations" => Keyword.get(@instance, :registrations_open), + "openRegistrations" => Config.instance_registrations_open?(), "protocols" => ["activitypub"], "services" => %{"inbound" => [], "outbound" => ["atom1.0"]}, "software" => %{ "name" => "mobilizon", - "version" => Keyword.get(@instance, :version), - "repository" => Keyword.get(@instance, :repository) + "version" => Config.instance_version(), + "repository" => Config.instance_repository() }, "usage" => %{"localComments" => 0, "localPosts" => 0, "users" => %{"total" => 0}}, "version" => "2.1" diff --git a/test/mobilizon_web/media_proxy_test.exs b/test/mobilizon_web/media_proxy_test.exs index 912806c59..46ba74420 100644 --- a/test/mobilizon_web/media_proxy_test.exs +++ b/test/mobilizon_web/media_proxy_test.exs @@ -5,18 +5,22 @@ defmodule MobilizonWeb.MediaProxyTest do use ExUnit.Case + import MobilizonWeb.MediaProxy + + alias Mobilizon.Config + alias MobilizonWeb.MediaProxyController setup do - enabled = Mobilizon.CommonConfig.get([:media_proxy, :enabled]) - on_exit(fn -> Mobilizon.CommonConfig.put([:media_proxy, :enabled], enabled) end) + enabled = Config.get([:media_proxy, :enabled]) + on_exit(fn -> Config.put([:media_proxy, :enabled], enabled) end) :ok end describe "when enabled" do setup do - Mobilizon.CommonConfig.put([:media_proxy, :enabled], true) + Config.put([:media_proxy, :enabled], true) :ok end @@ -43,7 +47,7 @@ defmodule MobilizonWeb.MediaProxyTest do assert String.starts_with?( encoded, - Mobilizon.CommonConfig.get([:media_proxy, :base_url], MobilizonWeb.Endpoint.url()) + Config.get([:media_proxy, :base_url], MobilizonWeb.Endpoint.url()) ) assert String.ends_with?(encoded, "/logo.png") @@ -80,15 +84,15 @@ defmodule MobilizonWeb.MediaProxyTest do end test "validates signature" do - secret_key_base = Mobilizon.CommonConfig.get([MobilizonWeb.Endpoint, :secret_key_base]) + secret_key_base = Config.get([MobilizonWeb.Endpoint, :secret_key_base]) on_exit(fn -> - Mobilizon.CommonConfig.put([MobilizonWeb.Endpoint, :secret_key_base], secret_key_base) + Config.put([MobilizonWeb.Endpoint, :secret_key_base], secret_key_base) end) encoded = url("https://pleroma.social") - Mobilizon.CommonConfig.put( + Config.put( [MobilizonWeb.Endpoint, :secret_key_base], "00000000000000000000000000000000000000000000000" ) @@ -126,20 +130,20 @@ defmodule MobilizonWeb.MediaProxyTest do end test "uses the configured base_url" do - base_url = Mobilizon.CommonConfig.get([:media_proxy, :base_url]) + base_url = Config.get([:media_proxy, :base_url]) if base_url do on_exit(fn -> - Mobilizon.CommonConfig.put([:media_proxy, :base_url], base_url) + Config.put([:media_proxy, :base_url], base_url) end) end - Mobilizon.CommonConfig.put([:media_proxy, :base_url], "https://cache.pleroma.social") + Config.put([:media_proxy, :base_url], "https://cache.pleroma.social") url = "https://pleroma.soykaf.com/static/logo.png" encoded = url(url) - assert String.starts_with?(encoded, Mobilizon.CommonConfig.get([:media_proxy, :base_url])) + assert String.starts_with?(encoded, Config.get([:media_proxy, :base_url])) end # https://git.pleroma.social/pleroma/pleroma/issues/580 @@ -154,13 +158,13 @@ defmodule MobilizonWeb.MediaProxyTest do describe "when disabled" do setup do - enabled = Mobilizon.CommonConfig.get([:media_proxy, :enabled]) + enabled = Config.get([:media_proxy, :enabled]) if enabled do - Mobilizon.CommonConfig.put([:media_proxy, :enabled], false) + Config.put([:media_proxy, :enabled], false) on_exit(fn -> - Mobilizon.CommonConfig.put([:media_proxy, :enabled], enabled) + Config.put([:media_proxy, :enabled], enabled) :ok end) end diff --git a/test/mobilizon_web/resolvers/user_resolver_test.exs b/test/mobilizon_web/resolvers/user_resolver_test.exs index b996d7ea9..52e37c6e9 100644 --- a/test/mobilizon_web/resolvers/user_resolver_test.exs +++ b/test/mobilizon_web/resolvers/user_resolver_test.exs @@ -1,6 +1,6 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do use MobilizonWeb.ConnCase - alias Mobilizon.{Actors, Users, CommonConfig} + alias Mobilizon.{Actors, Config, Users} alias Mobilizon.Actors.Actor alias Mobilizon.Users.User alias Mobilizon.Users @@ -401,7 +401,7 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do end test "test create_user/3 doesn't create a user when registration is disabled", context do - with_mock CommonConfig, registrations_open?: fn -> false end do + with_mock Config, instance_registrations_open?: fn -> false end do mutation = """ mutation { createUser( diff --git a/test/mobilizon_web/upload_test.exs b/test/mobilizon_web/upload_test.exs index 4434d0937..de41d172f 100644 --- a/test/mobilizon_web/upload_test.exs +++ b/test/mobilizon_web/upload_test.exs @@ -4,9 +4,12 @@ # Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/test/upload_test.ex defmodule Mobilizon.UploadTest do - alias MobilizonWeb.Upload use Mobilizon.DataCase + alias Mobilizon.Config + + alias MobilizonWeb.Upload + describe "Storing a file with the Local uploader" do setup [:ensure_local_uploader] @@ -185,7 +188,7 @@ defmodule Mobilizon.UploadTest do test "delete a not existing file" do file = - Mobilizon.CommonConfig.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> + Config.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> "/not_existing/definitely.jpg" refute File.exists?(file) @@ -215,6 +218,6 @@ defmodule Mobilizon.UploadTest do assert String.starts_with?(url, MobilizonWeb.Endpoint.url() <> "/media/") %URI{path: "/media/" <> path} = URI.parse(url) - {Mobilizon.CommonConfig.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> "/" <> path, url} + {Config.get!([MobilizonWeb.Uploaders.Local, :uploads]) <> "/" <> path, url} end end diff --git a/test/support/data_case.ex b/test/support/data_case.ex index bb22f12bf..b4c47f9f5 100644 --- a/test/support/data_case.ex +++ b/test/support/data_case.ex @@ -14,6 +14,8 @@ defmodule Mobilizon.DataCase do use ExUnit.CaseTemplate + alias Mobilizon.Config + using do quote do alias Mobilizon.Repo @@ -52,16 +54,16 @@ defmodule Mobilizon.DataCase do end def ensure_local_uploader(_context) do - uploader = Mobilizon.CommonConfig.get([MobilizonWeb.Upload, :uploader]) - filters = Mobilizon.CommonConfig.get([MobilizonWeb.Upload, :filters]) + uploader = Config.get([MobilizonWeb.Upload, :uploader]) + filters = Config.get([MobilizonWeb.Upload, :filters]) unless uploader == MobilizonWeb.Uploaders.Local || filters != [] do - Mobilizon.CommonConfig.put([MobilizonWeb.Upload, :uploader], MobilizonWeb.Uploaders.Local) - Mobilizon.CommonConfig.put([MobilizonWeb.Upload, :filters], []) + Config.put([MobilizonWeb.Upload, :uploader], MobilizonWeb.Uploaders.Local) + Config.put([MobilizonWeb.Upload, :filters], []) on_exit(fn -> - Mobilizon.CommonConfig.put([MobilizonWeb.Upload, :uploader], uploader) - Mobilizon.CommonConfig.put([MobilizonWeb.Upload, :filters], filters) + Config.put([MobilizonWeb.Upload, :uploader], uploader) + Config.put([MobilizonWeb.Upload, :filters], filters) end) end From 86a0630a7df4120ad908ce6cb290cc9f581eb5cd Mon Sep 17 00:00:00 2001 From: miffy Date: Sun, 8 Sep 2019 00:06:22 +0200 Subject: [PATCH 07/29] Remove unused config --- config/dogma.exs | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 config/dogma.exs diff --git a/config/dogma.exs b/config/dogma.exs deleted file mode 100644 index 8c2d5141f..000000000 --- a/config/dogma.exs +++ /dev/null @@ -1,16 +0,0 @@ -use Mix.Config -alias Dogma.Rule - -config :dogma, - # Select a set of rules as a base - rule_set: Dogma.RuleSet.All, - - # Pick paths not to lint - exclude: [ - ~r(\Alib/vendor/) - ], - - # Override an existing rule configuration - override: [ - %Rule.LineLength{enabled: false} - ] From fa037fd683a71388960e55fb9db70ee3c17a507e Mon Sep 17 00:00:00 2001 From: miffy Date: Sun, 8 Sep 2019 01:49:56 +0200 Subject: [PATCH 08/29] Separating of Storage context --- config/config.exs | 4 +-- config/dev.exs | 6 ++-- config/prod.exs | 6 ++-- config/test.exs | 9 +++-- lib/mobilizon.ex | 9 +++-- lib/mobilizon/actors/actor.ex | 15 ++++---- lib/mobilizon/actors/actors.ex | 15 ++++---- lib/mobilizon/actors/member.ex | 11 +++--- lib/mobilizon/addresses/addresses.ex | 5 +-- lib/mobilizon/admin.ex | 7 ++-- lib/mobilizon/application.ex | 2 +- lib/mobilizon/ecto.ex | 44 ----------------------- lib/mobilizon/{ => email}/mailer.ex | 2 +- lib/mobilizon/events/events.ex | 31 +++++++++-------- lib/mobilizon/events/tag.ex | 46 +++++++------------------ lib/mobilizon/events/tag/title_slug.ex | 33 ++++++++++++++++++ lib/mobilizon/media/media.ex | 4 +-- lib/mobilizon/postgrex_types.ex | 5 --- lib/mobilizon/reports/reports.ex | 6 ++-- lib/mobilizon/storage/ecto.ex | 15 ++++++++ lib/mobilizon/{ => storage}/page.ex | 8 ++--- lib/mobilizon/storage/postgrex_types.ex | 5 +++ lib/mobilizon/{ => storage}/repo.ex | 8 ++--- lib/mobilizon/users/users.ex | 6 ++-- lib/service/activity_pub/utils.ex | 4 +-- lib/service/users/activation.ex | 5 +-- lib/service/users/reset_password.ex | 10 +++--- mix.exs | 6 ++-- priv/repo/seeds.exs | 2 +- test/support/channel_case.ex | 4 +-- test/support/conn_case.ex | 4 +-- test/support/data_case.ex | 6 ++-- test/support/factory.ex | 2 +- test/test_helper.exs | 2 +- 34 files changed, 167 insertions(+), 180 deletions(-) delete mode 100644 lib/mobilizon/ecto.ex rename lib/mobilizon/{ => email}/mailer.ex (70%) create mode 100644 lib/mobilizon/events/tag/title_slug.ex delete mode 100644 lib/mobilizon/postgrex_types.ex create mode 100644 lib/mobilizon/storage/ecto.ex rename lib/mobilizon/{ => storage}/page.ex (84%) create mode 100644 lib/mobilizon/storage/postgrex_types.ex rename lib/mobilizon/{ => storage}/repo.ex (61%) diff --git a/config/config.exs b/config/config.exs index 28978814f..9d19f5b02 100644 --- a/config/config.exs +++ b/config/config.exs @@ -7,7 +7,7 @@ use Mix.Config # General application configuration config :mobilizon, - ecto_repos: [Mobilizon.Repo] + ecto_repos: [Mobilizon.Storage.Repo] config :mobilizon, :instance, name: System.get_env("MOBILIZON_INSTANCE_NAME") || "Localhost", @@ -78,7 +78,7 @@ config :mobilizon, MobilizonWeb.Guardian, secret_key: "ty0WM7YBE3ojvxoUQxo8AERrNpfbXnIJ82ovkPdqbUFw31T5LcK8wGjaOiReVQjo" config :guardian, Guardian.DB, - repo: Mobilizon.Repo, + repo: Mobilizon.Storage.Repo, # default schema_name: "guardian_tokens", # store all token types if not set diff --git a/config/dev.exs b/config/dev.exs index 21c109e0b..c7345d110 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -61,11 +61,11 @@ config :phoenix, :stacktrace_depth, 20 # Initialize plugs at runtime for faster development compilation config :phoenix, :plug_init_mode, :runtime -config :mobilizon, Mobilizon.Mailer, adapter: Bamboo.LocalAdapter +config :mobilizon, Mobilizon.Email.Mailer, adapter: Bamboo.LocalAdapter # Configure your database -config :mobilizon, Mobilizon.Repo, - types: Mobilizon.PostgresTypes, +config :mobilizon, Mobilizon.Storage.Repo, + types: Mobilizon.Storage.PostgresTypes, username: System.get_env("MOBILIZON_DATABASE_USERNAME") || "mobilizon", password: System.get_env("MOBILIZON_DATABASE_PASSWORD") || "mobilizon", database: System.get_env("MOBILIZON_DATABASE_DBNAME") || "mobilizon_dev", diff --git a/config/prod.exs b/config/prod.exs index 79835c89b..d580935ca 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -12,8 +12,8 @@ config :mobilizon, MobilizonWeb.Endpoint, cache_static_manifest: "priv/static/manifest.json" # Configure your database -config :mobilizon, Mobilizon.Repo, - types: Mobilizon.PostgresTypes, +config :mobilizon, Mobilizon.Storage.Repo, + types: Mobilizon.Storage.PostgresTypes, username: System.get_env("MOBILIZON_DATABASE_USERNAME") || "mobilizon", password: System.get_env("MOBILIZON_DATABASE_PASSWORD") || "mobilizon", database: System.get_env("MOBILIZON_DATABASE_DBNAME") || "mobilizon_prod", @@ -21,7 +21,7 @@ config :mobilizon, Mobilizon.Repo, port: System.get_env("MOBILIZON_DATABASE_PORT") || "5432", pool_size: 15 -config :mobilizon, Mobilizon.Mailer, +config :mobilizon, Mobilizon.Email.Mailer, adapter: Bamboo.SMTPAdapter, server: "localhost", hostname: "localhost", diff --git a/config/test.exs b/config/test.exs index 21432393b..8d174e40b 100644 --- a/config/test.exs +++ b/config/test.exs @@ -22,16 +22,15 @@ config :logger, level: :info # Configure your database -config :mobilizon, Mobilizon.Repo, - types: Mobilizon.PostgresTypes, +config :mobilizon, Mobilizon.Storage.Repo, + types: Mobilizon.Storage.PostgresTypes, username: System.get_env("MOBILIZON_DATABASE_USERNAME") || "mobilizon", password: System.get_env("MOBILIZON_DATABASE_PASSWORD") || "mobilizon", database: System.get_env("MOBILIZON_DATABASE_DBNAME") || "mobilizon_test", hostname: System.get_env("MOBILIZON_DATABASE_HOST") || "localhost", - pool: Ecto.Adapters.SQL.Sandbox, - types: Mobilizon.PostgresTypes + pool: Ecto.Adapters.SQL.Sandbox -config :mobilizon, Mobilizon.Mailer, adapter: Bamboo.TestAdapter +config :mobilizon, Mobilizon.Email.Mailer, adapter: Bamboo.TestAdapter config :mobilizon, MobilizonWeb.Upload, filters: [], link_name: false diff --git a/lib/mobilizon.ex b/lib/mobilizon.ex index 1953b2895..36e0d6c61 100644 --- a/lib/mobilizon.ex +++ b/lib/mobilizon.ex @@ -1,9 +1,12 @@ defmodule Mobilizon do @moduledoc """ - Mobilizon is a decentralized and federated Meetup-like using [ActivityPub](http://activitypub.rocks/). + Mobilizon is a decentralized and federated Meetup-like using + [ActivityPub](http://activitypub.rocks/). - It consists of an API server build with [Elixir](http://elixir-lang.github.io/) and the [Phoenix Framework](https://hexdocs.pm/phoenix). + It consists of an API server build with [Elixir](http://elixir-lang.github.io/) + and the [Phoenix Framework](https://hexdocs.pm/phoenix). - Mobilizon relies on `Guardian` for auth and `Geo`/Postgis for geographical informations. + Mobilizon relies on `Guardian` for auth and `Geo`/Postgis for geographical + information. """ end diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex index 6fe728e2a..e83d9a308 100644 --- a/lib/mobilizon/actors/actor.ex +++ b/lib/mobilizon/actors/actor.ex @@ -26,25 +26,24 @@ defmodule Mobilizon.Actors.Actor do @moduledoc """ Represents an actor (local and remote actors) """ + use Ecto.Schema + import Ecto.Changeset + import Ecto.Query alias Mobilizon.Actors alias Mobilizon.Actors.{Actor, Follower, Member} alias Mobilizon.Config alias Mobilizon.Events.{Event, FeedToken} alias Mobilizon.Media.File - alias Mobilizon.Users.User - alias Mobilizon.Reports.{Report, Note} + alias Mobilizon.Storage.{Page, Repo} + alias Mobilizon.Users.User alias MobilizonWeb.Router.Helpers, as: Routes alias MobilizonWeb.Endpoint - import Ecto.Query - import Mobilizon.Ecto - alias Mobilizon.Repo - require Logger # @type t :: %Actor{description: String.t, id: integer(), inserted_at: DateTime.t, updated_at: DateTime.t, display_name: String.t, domain: String.t, keys: String.t, suspended: boolean(), url: String.t, username: String.t, organized_events: list(), groups: list(), group_request: list(), user: User.t, field: ActorTypeEnum.t} @@ -383,7 +382,7 @@ defmodule Mobilizon.Actors.Actor do ) total = Task.async(fn -> Repo.aggregate(query, :count, :id) end) - elements = Task.async(fn -> Repo.all(paginate(query, page, limit)) end) + elements = Task.async(fn -> Repo.all(Page.paginate(query, page, limit)) end) %{total: Task.await(total), elements: Task.await(elements)} end @@ -428,7 +427,7 @@ defmodule Mobilizon.Actors.Actor do ) total = Task.async(fn -> Repo.aggregate(query, :count, :id) end) - elements = Task.async(fn -> Repo.all(paginate(query, page, limit)) end) + elements = Task.async(fn -> Repo.all(Page.paginate(query, page, limit)) end) %{total: Task.await(total), elements: Task.await(elements)} end diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index 7ab1a9b0b..3a3617b9d 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -3,16 +3,15 @@ defmodule Mobilizon.Actors do The Actors context. """ - import Ecto.Query, warn: false - import Mobilizon.Ecto + import Ecto.Query - alias Mobilizon.Repo + alias Ecto.Multi alias Mobilizon.Actors.{Actor, Bot, Member, Follower} alias Mobilizon.Media.File - alias Ecto.Multi - alias Mobilizon.Service.ActivityPub + alias Mobilizon.Storage.{Page, Repo} + require Logger @doc false @@ -203,7 +202,7 @@ defmodule Mobilizon.Actors do where: a.type == ^:Group, where: a.visibility in [^:public, ^:unlisted] ) - |> paginate(page, limit) + |> Page.paginate(page, limit) ) end @@ -586,7 +585,7 @@ defmodule Mobilizon.Actors do ^username ) ) - |> paginate(page, limit) + |> Page.paginate(page, limit) total = Task.async(fn -> Repo.aggregate(query, :count, :id) end) elements = Task.async(fn -> Repo.all(query) end) @@ -652,7 +651,7 @@ defmodule Mobilizon.Actors do }) try do - Mobilizon.Repo.insert!(actor) + Repo.insert!(actor) rescue e in Ecto.InvalidChangesetError -> {:error, e.changeset} diff --git a/lib/mobilizon/actors/member.ex b/lib/mobilizon/actors/member.ex index c3b4767be..02d70f1bf 100644 --- a/lib/mobilizon/actors/member.ex +++ b/lib/mobilizon/actors/member.ex @@ -12,15 +12,14 @@ defmodule Mobilizon.Actors.Member do @moduledoc """ Represents the membership of an actor to a group """ + use Ecto.Schema import Ecto.Changeset - import Ecto.Query, warn: false - import Mobilizon.Ecto + import Ecto.Query - alias Mobilizon.Actors.Member - alias Mobilizon.Actors.Actor - alias Mobilizon.Repo + alias Mobilizon.Actors.{Actor, Member} + alias Mobilizon.Storage.{Page, Repo} schema "members" do field(:role, Mobilizon.Actors.MemberRoleEnum, default: :member) @@ -64,7 +63,7 @@ defmodule Mobilizon.Actors.Member do where: m.parent_id == ^id and (m.role == ^:creator or m.role == ^:administrator), preload: [:actor] ) - |> paginate(page, limit) + |> Page.paginate(page, limit) ) end diff --git a/lib/mobilizon/addresses/addresses.ex b/lib/mobilizon/addresses/addresses.ex index cc6c407c5..19f6325ad 100644 --- a/lib/mobilizon/addresses/addresses.ex +++ b/lib/mobilizon/addresses/addresses.ex @@ -4,10 +4,11 @@ defmodule Mobilizon.Addresses do """ import Ecto.Query, warn: false - alias Mobilizon.Repo - require Logger alias Mobilizon.Addresses.Address + alias Mobilizon.Storage.Repo + + require Logger @geom_types [:point] diff --git a/lib/mobilizon/admin.ex b/lib/mobilizon/admin.ex index fba5ee050..47eb3097b 100644 --- a/lib/mobilizon/admin.ex +++ b/lib/mobilizon/admin.ex @@ -3,11 +3,10 @@ defmodule Mobilizon.Admin do The Admin context. """ - import Ecto.Query, warn: false - alias Mobilizon.Repo - import Mobilizon.Ecto + import Ecto.Query alias Mobilizon.Admin.ActionLog + alias Mobilizon.Storage.{Page, Repo} @doc """ Returns the list of action_logs. @@ -24,7 +23,7 @@ defmodule Mobilizon.Admin do r in ActionLog, preload: [:actor] ) - |> paginate(page, limit) + |> Page.paginate(page, limit) |> Repo.all() end diff --git a/lib/mobilizon/application.ex b/lib/mobilizon/application.ex index 3bbb4b0f3..5291896a8 100644 --- a/lib/mobilizon/application.ex +++ b/lib/mobilizon/application.ex @@ -21,7 +21,7 @@ defmodule Mobilizon.Application do # Define workers and child supervisors to be supervised children = [ # Start the Ecto repository - supervisor(Mobilizon.Repo, []), + supervisor(Mobilizon.Storage.Repo, []), # Start the endpoint when the application starts supervisor(MobilizonWeb.Endpoint, []), # Start your own worker by calling: Mobilizon.Worker.start_link(arg1, arg2, arg3) diff --git a/lib/mobilizon/ecto.ex b/lib/mobilizon/ecto.ex deleted file mode 100644 index 66fb2ec72..000000000 --- a/lib/mobilizon/ecto.ex +++ /dev/null @@ -1,44 +0,0 @@ -defmodule Mobilizon.Ecto do - @moduledoc """ - Mobilizon Ecto utils - """ - - import Ecto.Query, warn: false - - @doc """ - Add limit and offset to the query - """ - def paginate(query, page \\ 1, size \\ 10) - def paginate(query, page, _size) when is_nil(page), do: paginate(query) - def paginate(query, page, size) when is_nil(size), do: paginate(query, page) - - def paginate(query, page, size) do - from(query, - limit: ^size, - offset: ^((page - 1) * size) - ) - end - - @doc """ - Add sort to the query - """ - def sort(query, sort, direction) do - from( - query, - order_by: [{^direction, ^sort}] - ) - end - - def increment_slug(slug) do - case List.pop_at(String.split(slug, "-"), -1) do - {nil, _} -> - slug - - {suffix, slug_parts} -> - case Integer.parse(suffix) do - {id, _} -> Enum.join(slug_parts, "-") <> "-" <> Integer.to_string(id + 1) - :error -> slug <> "-1" - end - end - end -end diff --git a/lib/mobilizon/mailer.ex b/lib/mobilizon/email/mailer.ex similarity index 70% rename from lib/mobilizon/mailer.ex rename to lib/mobilizon/email/mailer.ex index 177399ebb..027647609 100644 --- a/lib/mobilizon/mailer.ex +++ b/lib/mobilizon/email/mailer.ex @@ -1,4 +1,4 @@ -defmodule Mobilizon.Mailer do +defmodule Mobilizon.Email.Mailer do @moduledoc """ Mobilizon Mailer. """ diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index cc6d71702..a6256b294 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -3,17 +3,18 @@ defmodule Mobilizon.Events do The Events context. """ - import Ecto.Query, warn: false - import Mobilizon.Ecto + import Ecto.Query + + import Mobilizon.Storage.Ecto - alias Mobilizon.Repo - alias Mobilizon.Events.{Event, Comment, Participant} alias Mobilizon.Actors.Actor - alias Mobilizon.Users.User alias Mobilizon.Addresses.Address + alias Mobilizon.Events.{Event, Comment, Participant} + alias Mobilizon.Storage.{Page, Repo} + alias Mobilizon.Users.User def data() do - Dataloader.Ecto.new(Mobilizon.Repo, query: &query/2) + Dataloader.Ecto.new(Repo, query: &query/2) end def query(queryable, _params) do @@ -36,7 +37,7 @@ defmodule Mobilizon.Events do :picture ] ) - |> paginate(page, limit) + |> Page.paginate(page, limit) events = Repo.all(query) @@ -301,7 +302,7 @@ defmodule Mobilizon.Events do e in Event, preload: [:organizer_actor, :participants] ) - |> paginate(page, limit) + |> Page.paginate(page, limit) |> sort(sort, direction) |> restrict_future_events(future) |> allow_unlisted(unlisted) @@ -348,7 +349,7 @@ defmodule Mobilizon.Events do ), preload: [:organizer_actor] ) - |> paginate(page, limit) + |> Page.paginate(page, limit) total = Task.async(fn -> Repo.aggregate(query, :count, :id) end) elements = Task.async(fn -> Repo.all(query) end) @@ -493,7 +494,7 @@ defmodule Mobilizon.Events do def list_tags(page \\ nil, limit \\ nil) do Repo.all( Tag - |> paginate(page, limit) + |> Page.paginate(page, limit) ) end @@ -514,7 +515,7 @@ defmodule Mobilizon.Events do on: t.id == e.tag_id, where: e.event_id == ^id ) - |> paginate(page, limit) + |> Page.paginate(page, limit) ) end @@ -744,7 +745,7 @@ defmodule Mobilizon.Events do where: e.uuid == ^uuid, preload: [:actor] ) - |> paginate(page, limit) + |> Page.paginate(page, limit) end @doc """ @@ -769,7 +770,7 @@ defmodule Mobilizon.Events do where: a.id == ^id and p.role != ^:not_approved, preload: [:picture, :tags] ) - |> paginate(page, limit) + |> Page.paginate(page, limit) ) end @@ -789,7 +790,7 @@ defmodule Mobilizon.Events do where: p.event_id == ^id and p.role == ^:creator, preload: [:actor] ) - |> paginate(page, limit) + |> Page.paginate(page, limit) ) end @@ -1159,7 +1160,7 @@ defmodule Mobilizon.Events do :event ] ) - |> paginate(page, limit) + |> Page.paginate(page, limit) comments = Repo.all(query) diff --git a/lib/mobilizon/events/tag.ex b/lib/mobilizon/events/tag.ex index fafcfe157..5d38dec3e 100644 --- a/lib/mobilizon/events/tag.ex +++ b/lib/mobilizon/events/tag.ex @@ -1,36 +1,3 @@ -defmodule Mobilizon.Events.Tag.TitleSlug do - @moduledoc """ - Generates slugs for tags - """ - alias Mobilizon.Events.Tag - import Ecto.Query - alias Mobilizon.Repo - use EctoAutoslugField.Slug, from: :title, to: :slug - - def build_slug(sources, changeset) do - slug = super(sources, changeset) - build_unique_slug(slug, changeset) - end - - defp build_unique_slug(slug, changeset) do - query = - from( - t in Tag, - where: t.slug == ^slug - ) - - case Repo.one(query) do - nil -> - slug - - _tag -> - slug - |> Mobilizon.Ecto.increment_slug() - |> build_unique_slug(changeset) - end - end -end - defmodule Mobilizon.Events.Tag do @moduledoc """ Represents a tag for events @@ -57,4 +24,17 @@ defmodule Mobilizon.Events.Tag do |> validate_required([:title, :slug]) |> TitleSlug.unique_constraint() end + + def increment_slug(slug) do + case List.pop_at(String.split(slug, "-"), -1) do + {nil, _} -> + slug + + {suffix, slug_parts} -> + case Integer.parse(suffix) do + {id, _} -> Enum.join(slug_parts, "-") <> "-" <> Integer.to_string(id + 1) + :error -> slug <> "-1" + end + end + end end diff --git a/lib/mobilizon/events/tag/title_slug.ex b/lib/mobilizon/events/tag/title_slug.ex new file mode 100644 index 000000000..9c64841d0 --- /dev/null +++ b/lib/mobilizon/events/tag/title_slug.ex @@ -0,0 +1,33 @@ +defmodule Mobilizon.Events.Tag.TitleSlug do + @moduledoc """ + Generates slugs for tags + """ + + alias Mobilizon.Events.Tag + import Ecto.Query + alias Mobilizon.Storage.Repo + use EctoAutoslugField.Slug, from: :title, to: :slug + + def build_slug(sources, changeset) do + slug = super(sources, changeset) + build_unique_slug(slug, changeset) + end + + defp build_unique_slug(slug, changeset) do + query = + from( + t in Tag, + where: t.slug == ^slug + ) + + case Repo.one(query) do + nil -> + slug + + _tag -> + slug + |> Tag.increment_slug() + |> build_unique_slug(changeset) + end + end +end diff --git a/lib/mobilizon/media/media.ex b/lib/mobilizon/media/media.ex index 70762091b..73533934d 100644 --- a/lib/mobilizon/media/media.ex +++ b/lib/mobilizon/media/media.ex @@ -8,11 +8,11 @@ defmodule Mobilizon.Media do alias Ecto.Multi alias Mobilizon.Media.{File, Picture} - alias Mobilizon.Repo + alias Mobilizon.Storage.Repo @doc false @spec data :: Dataloader.Ecto.t() - def data, do: Dataloader.Ecto.new(Mobilizon.Repo, query: &query/2) + def data, do: Dataloader.Ecto.new(Repo, query: &query/2) @doc false @spec query(Ecto.Query.t(), map) :: Ecto.Query.t() diff --git a/lib/mobilizon/postgrex_types.ex b/lib/mobilizon/postgrex_types.ex deleted file mode 100644 index d06805578..000000000 --- a/lib/mobilizon/postgrex_types.ex +++ /dev/null @@ -1,5 +0,0 @@ -Postgrex.Types.define( - Mobilizon.PostgresTypes, - [Geo.PostGIS.Extension] ++ Ecto.Adapters.Postgres.extensions(), - json: Jason -) diff --git a/lib/mobilizon/reports/reports.ex b/lib/mobilizon/reports/reports.ex index 173dc5fa7..055e2ee91 100644 --- a/lib/mobilizon/reports/reports.ex +++ b/lib/mobilizon/reports/reports.ex @@ -6,16 +6,16 @@ defmodule Mobilizon.Reports do import Ecto.Query import EctoEnum - import Mobilizon.Ecto + import Mobilizon.Storage.Ecto - alias Mobilizon.{Page, Repo} alias Mobilizon.Reports.{Note, Report} + alias Mobilizon.Storage.{Page, Repo} defenum(ReportStatus, :report_status, [:open, :closed, :resolved]) @doc false @spec data :: Dataloader.Ecto.t() - def data, do: Dataloader.Ecto.new(Mobilizon.Repo, query: &query/2) + def data, do: Dataloader.Ecto.new(Repo, query: &query/2) @doc false @spec query(Ecto.Query.t(), map) :: Ecto.Query.t() diff --git a/lib/mobilizon/storage/ecto.ex b/lib/mobilizon/storage/ecto.ex new file mode 100644 index 000000000..3a1dc5411 --- /dev/null +++ b/lib/mobilizon/storage/ecto.ex @@ -0,0 +1,15 @@ +defmodule Mobilizon.Storage.Ecto do + @moduledoc """ + Mobilizon Ecto utils + """ + + import Ecto.Query, warn: false + + @doc """ + Adds sort to the query. + """ + @spec sort(Ecto.Query.t(), atom, atom) :: Ecto.Query.t() + def sort(query, sort, direction) do + from(query, order_by: [{^direction, ^sort}]) + end +end diff --git a/lib/mobilizon/page.ex b/lib/mobilizon/storage/page.ex similarity index 84% rename from lib/mobilizon/page.ex rename to lib/mobilizon/storage/page.ex index ce2d440b5..af20d4a7b 100644 --- a/lib/mobilizon/page.ex +++ b/lib/mobilizon/storage/page.ex @@ -1,11 +1,11 @@ -defmodule Mobilizon.Page do +defmodule Mobilizon.Storage.Page do @moduledoc """ Module for pagination of queries. """ - import Ecto.Query, warn: false + import Ecto.Query - alias Mobilizon.Repo + alias Mobilizon.Storage.Repo defstruct [ :total, @@ -36,7 +36,7 @@ defmodule Mobilizon.Page do @doc """ Add limit and offset to the query. """ - @spec paginate(Ecto.Query.t(), integer | nil, integer | nil) :: Ecto.Query.t() + @spec paginate(Ecto.Query.t() | struct, integer | nil, integer | nil) :: Ecto.Query.t() def paginate(query, page \\ 1, size \\ 10) def paginate(query, page, _size) when is_nil(page), do: paginate(query) diff --git a/lib/mobilizon/storage/postgrex_types.ex b/lib/mobilizon/storage/postgrex_types.ex new file mode 100644 index 000000000..a260d062d --- /dev/null +++ b/lib/mobilizon/storage/postgrex_types.ex @@ -0,0 +1,5 @@ +Postgrex.Types.define( + Mobilizon.Storage.PostgresTypes, + [Geo.PostGIS.Extension | Ecto.Adapters.Postgres.extensions()], + json: Jason +) diff --git a/lib/mobilizon/repo.ex b/lib/mobilizon/storage/repo.ex similarity index 61% rename from lib/mobilizon/repo.ex rename to lib/mobilizon/storage/repo.ex index d25e5b21b..8cdde1a45 100644 --- a/lib/mobilizon/repo.ex +++ b/lib/mobilizon/storage/repo.ex @@ -1,14 +1,14 @@ -defmodule Mobilizon.Repo do +defmodule Mobilizon.Storage.Repo do @moduledoc """ - Mobilizon Repo + Mobilizon Repo. """ + use Ecto.Repo, otp_app: :mobilizon, adapter: Ecto.Adapters.Postgres @doc """ - Dynamically loads the repository url from the - DATABASE_URL environment variable. + Dynamically loads the repository url from the DATABASE_URL environment variable. """ def init(_, opts) do {:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))} diff --git a/lib/mobilizon/users/users.ex b/lib/mobilizon/users/users.ex index af515c2cf..c03fe3588 100644 --- a/lib/mobilizon/users/users.ex +++ b/lib/mobilizon/users/users.ex @@ -6,11 +6,11 @@ defmodule Mobilizon.Users do import Ecto.Query import EctoEnum - import Mobilizon.Ecto + import Mobilizon.Storage.Ecto alias Mobilizon.Actors.Actor alias Mobilizon.Events - alias Mobilizon.{Page, Repo} + alias Mobilizon.Storage.{Page, Repo} alias Mobilizon.Users.User @type tokens :: %{ @@ -22,7 +22,7 @@ defmodule Mobilizon.Users do @doc false @spec data :: Dataloader.Ecto.t() - def data, do: Dataloader.Ecto.new(Mobilizon.Repo, query: &query/2) + def data, do: Dataloader.Ecto.new(Repo, query: &query/2) @doc false @spec query(Ecto.Query.t(), map) :: Ecto.Query.t() diff --git a/lib/service/activity_pub/utils.ex b/lib/service/activity_pub/utils.ex index d68c62287..db8757bb6 100644 --- a/lib/service/activity_pub/utils.ex +++ b/lib/service/activity_pub/utils.ex @@ -10,7 +10,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do Various utils """ - alias Mobilizon.Repo + alias Mobilizon.Storage.Repo alias Mobilizon.Addresses alias Mobilizon.Addresses.Address alias Mobilizon.Actors @@ -165,7 +165,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do Enum.each(Users.list_moderators(), fn moderator -> moderator |> Mobilizon.Email.Admin.report(moderator, report) - |> Mobilizon.Mailer.deliver_later() + |> Mobilizon.Email.Mailer.deliver_later() end) {:ok, report} diff --git a/lib/service/users/activation.ex b/lib/service/users/activation.ex index c89d2ec95..a111c5698 100644 --- a/lib/service/users/activation.ex +++ b/lib/service/users/activation.ex @@ -1,9 +1,10 @@ defmodule Mobilizon.Service.Users.Activation do @moduledoc false - alias Mobilizon.{Mailer, Users} - alias Mobilizon.Users.User + alias Mobilizon.Email.Mailer alias Mobilizon.Email.User, as: UserEmail + alias Mobilizon.Users + alias Mobilizon.Users.User alias Mobilizon.Service.Users.Tools require Logger diff --git a/lib/service/users/reset_password.ex b/lib/service/users/reset_password.ex index e34565166..dd6341877 100644 --- a/lib/service/users/reset_password.ex +++ b/lib/service/users/reset_password.ex @@ -1,12 +1,14 @@ defmodule Mobilizon.Service.Users.ResetPassword do @moduledoc false - require Logger - - alias Mobilizon.Users.User - alias Mobilizon.{Mailer, Repo, Users} + alias Mobilizon.Email.Mailer alias Mobilizon.Email.User, as: UserEmail alias Mobilizon.Service.Users.Tools + alias Mobilizon.Storage.Repo + alias Mobilizon.Users + alias Mobilizon.Users.User + + require Logger @doc """ Check that the provided token is correct and update provided password diff --git a/mix.exs b/mix.exs index f54af6661..40eb97754 100644 --- a/mix.exs +++ b/mix.exs @@ -203,8 +203,8 @@ defmodule Mobilizon.Mixfile do Mobilizon.Users.UserRole, Mobilizon.Users.Guards, Mobilizon.Activity, - Mobilizon.Ecto, - Mobilizon.Repo + Mobilizon.Storage.Ecto, + Mobilizon.Storage.Repo ], APIs: [ MobilizonWeb.API.Comments, @@ -299,7 +299,7 @@ defmodule Mobilizon.Mixfile do Tools: [ Mobilizon.Application, Mobilizon.Factory, - Mobilizon.Mailer, + Mobilizon.Email.Mailer, Mobilizon.EmailView, Mobilizon.Email.User ] diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index f3367609b..9ef0b716a 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -5,7 +5,7 @@ # Inside the script, you can read and write to any of your # repositories directly: # -# Mobilizon.Repo.insert!(%Mobilizon.SomeSchema{}) +# Mobilizon.Storage.Repo.insert!(%Mobilizon.SomeSchema{}) # # We recommend using the bang functions (`insert!`, `update!` # and so on) as they will fail if something goes wrong. diff --git a/test/support/channel_case.ex b/test/support/channel_case.ex index cc77f22f4..c9b207b83 100644 --- a/test/support/channel_case.ex +++ b/test/support/channel_case.ex @@ -26,10 +26,10 @@ defmodule MobilizonWeb.ChannelCase do end setup tags do - :ok = Ecto.Adapters.SQL.Sandbox.checkout(Mobilizon.Repo) + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Mobilizon.Storage.Repo) unless tags[:async] do - Ecto.Adapters.SQL.Sandbox.mode(Mobilizon.Repo, {:shared, self()}) + Ecto.Adapters.SQL.Sandbox.mode(Mobilizon.Storage.Repo, {:shared, self()}) end :ok diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 170d6e9e6..eae09e0c7 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -35,10 +35,10 @@ defmodule MobilizonWeb.ConnCase do end setup tags do - :ok = Ecto.Adapters.SQL.Sandbox.checkout(Mobilizon.Repo) + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Mobilizon.Storage.Repo) unless tags[:async] do - Ecto.Adapters.SQL.Sandbox.mode(Mobilizon.Repo, {:shared, self()}) + Ecto.Adapters.SQL.Sandbox.mode(Mobilizon.Storage.Repo, {:shared, self()}) end {:ok, conn: Phoenix.ConnTest.build_conn()} diff --git a/test/support/data_case.ex b/test/support/data_case.ex index b4c47f9f5..76f4d26dc 100644 --- a/test/support/data_case.ex +++ b/test/support/data_case.ex @@ -18,7 +18,7 @@ defmodule Mobilizon.DataCase do using do quote do - alias Mobilizon.Repo + alias Mobilizon.Storage.Repo import Ecto import Ecto.Changeset @@ -28,10 +28,10 @@ defmodule Mobilizon.DataCase do end setup tags do - :ok = Ecto.Adapters.SQL.Sandbox.checkout(Mobilizon.Repo) + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Mobilizon.Storage.Repo) unless tags[:async] do - Ecto.Adapters.SQL.Sandbox.mode(Mobilizon.Repo, {:shared, self()}) + Ecto.Adapters.SQL.Sandbox.mode(Mobilizon.Storage.Repo, {:shared, self()}) end :ok diff --git a/test/support/factory.ex b/test/support/factory.ex index e91cd42a5..97974657d 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -3,7 +3,7 @@ defmodule Mobilizon.Factory do Factory for fixtures with ExMachina """ # with Ecto - use ExMachina.Ecto, repo: Mobilizon.Repo + use ExMachina.Ecto, repo: Mobilizon.Storage.Repo alias Mobilizon.Actors.Actor alias MobilizonWeb.Router.Helpers, as: Routes alias MobilizonWeb.Endpoint diff --git a/test/test_helper.exs b/test/test_helper.exs index 43b41df65..ee5409205 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -3,4 +3,4 @@ ExUnit.configure(formatters: [ExUnit.CLIFormatter, ExUnitNotifier]) ExUnit.start() -Ecto.Adapters.SQL.Sandbox.mode(Mobilizon.Repo, :manual) +Ecto.Adapters.SQL.Sandbox.mode(Mobilizon.Storage.Repo, :manual) From 2e2dcc8208f16f343c5dc9de385cd7ab44f5f1db Mon Sep 17 00:00:00 2001 From: miffy Date: Sun, 8 Sep 2019 01:51:18 +0200 Subject: [PATCH 09/29] Add script for cleaning for tests --- mix.exs | 18 +++++++++++++++--- script/clean_after_tests | 3 +++ 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100755 script/clean_after_tests diff --git a/mix.exs b/mix.exs index 40eb97754..311f0b954 100644 --- a/mix.exs +++ b/mix.exs @@ -120,9 +120,21 @@ defmodule Mobilizon.Mixfile do # See the documentation for `Mix` for more info on aliases. defp aliases do [ - "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], - "ecto.reset": ["ecto.drop", "ecto.setup"], - test: ["ecto.create --quiet", "ecto.migrate", "test"], + "ecto.setup": [ + "ecto.create", + "ecto.migrate", + "run priv/repo/seeds.exs" + ], + "ecto.reset": [ + "ecto.drop", + "ecto.setup" + ], + test: [ + "ecto.create --quiet", + "ecto.migrate", + "test", + "cmd ./script/clean_after_tests" + ], "phx.deps_migrate_serve": [ "deps.get", "ecto.create --quiet", diff --git a/script/clean_after_tests b/script/clean_after_tests new file mode 100755 index 000000000..7344216e4 --- /dev/null +++ b/script/clean_after_tests @@ -0,0 +1,3 @@ +#!/bin/bash + +rm -rf test/uploads From 2a9605c66ad8f4865035383c4b72a37b0dd3934e Mon Sep 17 00:00:00 2001 From: miffy Date: Sun, 8 Sep 2019 02:06:28 +0200 Subject: [PATCH 10/29] Refactoring of Admin context --- lib/mobilizon/admin/action_log.ex | 23 +++++++++++++++++++---- lib/mobilizon/{ => admin}/admin.ex | 30 +++++++++--------------------- 2 files changed, 28 insertions(+), 25 deletions(-) rename lib/mobilizon/{ => admin}/admin.ex (52%) diff --git a/lib/mobilizon/admin/action_log.ex b/lib/mobilizon/admin/action_log.ex index 2b3f80f7e..68d1f189b 100644 --- a/lib/mobilizon/admin/action_log.ex +++ b/lib/mobilizon/admin/action_log.ex @@ -1,27 +1,42 @@ defmodule Mobilizon.Admin.ActionLog do @moduledoc """ - ActionLog entity schema + Represents an action log entity. """ + use Ecto.Schema + import Ecto.Changeset + alias Mobilizon.Actors.Actor - @required_attrs [:action, :target_type, :target_id, :changes, :actor_id] + @type t :: %__MODULE__{ + action: String.t(), + target_type: String.t(), + target_id: integer, + changes: map, + actor: Actor.t() + } + + @required_attrs [:action, :target_type, :target_id, :actor_id] + @optional_attrs [:changes] + @attrs @required_attrs ++ @optional_attrs schema "admin_action_logs" do field(:action, :string) field(:target_type, :string) field(:target_id, :integer) field(:changes, :map) + belongs_to(:actor, Actor) timestamps() end @doc false + @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() def changeset(action_log, attrs) do action_log - |> cast(attrs, @required_attrs) - |> validate_required(@required_attrs -- [:changes]) + |> cast(attrs, @attrs) + |> validate_required(@required_attrs) end end diff --git a/lib/mobilizon/admin.ex b/lib/mobilizon/admin/admin.ex similarity index 52% rename from lib/mobilizon/admin.ex rename to lib/mobilizon/admin/admin.ex index 47eb3097b..aa348cfa8 100644 --- a/lib/mobilizon/admin.ex +++ b/lib/mobilizon/admin/admin.ex @@ -9,39 +9,27 @@ defmodule Mobilizon.Admin do alias Mobilizon.Storage.{Page, Repo} @doc """ - Returns the list of action_logs. - - ## Examples - - iex> list_action_logs() - [%ActionLog{}, ...] - + Returns the list of action logs. """ - @spec list_action_logs(integer(), integer()) :: list(ActionLog.t()) + @spec list_action_logs(integer | nil, integer | nil) :: [ActionLog.t()] def list_action_logs(page \\ nil, limit \\ nil) do - from( - r in ActionLog, - preload: [:actor] - ) + list_action_logs_query() |> Page.paginate(page, limit) |> Repo.all() end @doc """ Creates a action_log. - - ## Examples - - iex> create_action_log(%{field: value}) - {:ok, %ActionLog{}} - - iex> create_action_log(%{field: bad_value}) - {:error, %Ecto.Changeset{}} - """ + @spec create_action_log(map) :: {:ok, ActionLog.t()} | {:error, Ecto.Changeset.t()} def create_action_log(attrs \\ %{}) do %ActionLog{} |> ActionLog.changeset(attrs) |> Repo.insert() end + + @spec list_action_logs_query :: Ecto.Query.t() + defp list_action_logs_query do + from(r in ActionLog, preload: [:actor]) + end end From 3a4a006c4454b1caf29e0f5faea1d8310e7ebf5d Mon Sep 17 00:00:00 2001 From: miffy Date: Sun, 8 Sep 2019 03:05:30 +0200 Subject: [PATCH 11/29] Refactoring of Adresses context --- lib/mobilizon/addresses/address.ex | 43 +++- lib/mobilizon/addresses/addresses.ex | 257 +++++++------------- test/mobilizon/addresses/addresses_test.exs | 18 -- 3 files changed, 119 insertions(+), 199 deletions(-) diff --git a/lib/mobilizon/addresses/address.ex b/lib/mobilizon/addresses/address.ex index 97f635668..7fd4b3af7 100644 --- a/lib/mobilizon/addresses/address.ex +++ b/lib/mobilizon/addresses/address.ex @@ -1,12 +1,31 @@ defmodule Mobilizon.Addresses.Address do - @moduledoc "An address for an event or a group" + @moduledoc """ + Represents an address for an event or a group. + """ use Ecto.Schema + import Ecto.Changeset + alias Mobilizon.Addresses.Address alias Mobilizon.Events.Event - # alias Mobilizon.Actors.Actor - @attrs [ + + @type t :: %__MODULE__{ + country: String.t(), + locality: String.t(), + region: String.t(), + description: String.t(), + floor: String.t(), + geom: Geo.PostGIS.Geometry.t(), + postal_code: String.t(), + street: String.t(), + url: String.t(), + origin_id: String.t(), + events: [Event.t()] + } + + @required_attrs [:url] + @optional_attrs [ :description, :floor, :geom, @@ -15,12 +34,9 @@ defmodule Mobilizon.Addresses.Address do :region, :postal_code, :street, - :url, :origin_id ] - @required [ - :url - ] + @attrs @required_attrs ++ @optional_attrs schema "addresses" do field(:country, :string) @@ -33,22 +49,29 @@ defmodule Mobilizon.Addresses.Address do field(:street, :string) field(:url, :string) field(:origin_id, :string) - has_many(:event, Event, foreign_key: :physical_address_id) + + has_many(:events, Event, foreign_key: :physical_address_id) timestamps() end @doc false + @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() def changeset(%Address{} = address, attrs) do address |> cast(attrs, @attrs) |> set_url() - |> validate_required(@required) + |> validate_required(@required_attrs) end + @spec set_url(Ecto.Changeset.t()) :: Ecto.Changeset.t() defp set_url(%Ecto.Changeset{changes: changes} = changeset) do url = - Map.get(changes, :url, MobilizonWeb.Endpoint.url() <> "/address/#{Ecto.UUID.generate()}") + Map.get( + changes, + :url, + "#{MobilizonWeb.Endpoint.url()}/address/#{Ecto.UUID.generate()}" + ) put_change(changeset, :url, url) end diff --git a/lib/mobilizon/addresses/addresses.ex b/lib/mobilizon/addresses/addresses.ex index 19f6325ad..d1dc3b99c 100644 --- a/lib/mobilizon/addresses/addresses.ex +++ b/lib/mobilizon/addresses/addresses.ex @@ -3,83 +3,50 @@ defmodule Mobilizon.Addresses do The Addresses context. """ - import Ecto.Query, warn: false + import Ecto.Query alias Mobilizon.Addresses.Address alias Mobilizon.Storage.Repo require Logger - @geom_types [:point] + @doc false + @spec data :: Dataloader.Ecto.t() + def data, do: Dataloader.Ecto.new(Repo, query: &query/2) @doc false - def data() do - Dataloader.Ecto.new(Repo, query: &query/2) - end - - @doc false - def query(queryable, _params) do - queryable - end + @spec query(Ecto.Query.t(), map) :: Ecto.Query.t() + def query(queryable, _params), do: queryable @doc """ Returns the list of addresses. - - ## Examples - - iex> list_addresses() - [%Address{}, ...] - """ - def list_addresses do - Repo.all(Address) - end + @spec list_addresses :: [Address.t()] + def list_addresses, do: Repo.all(Address) @doc """ Gets a single address. - - Raises `Ecto.NoResultsError` if the Address does not exist. - - ## Examples - - iex> get_address!(123) - %Address{} - - iex> get_address!(456) - ** (Ecto.NoResultsError) - """ - def get_address!(id), do: Repo.get!(Address, id) - + @spec get_address(integer | String.t()) :: Address.t() | nil def get_address(id), do: Repo.get(Address, id) @doc """ - Gets a single address by it's url - - ## Examples - - iex> get_address_by_url("https://mobilizon.social/addresses/4572") - %Address{} - - iex> get_address_by_url("https://mobilizon.social/addresses/099") - nil + Gets a single address. + Raises `Ecto.NoResultsError` if the address does not exist. """ - def get_address_by_url(url) do - Repo.get_by(Address, url: url) - end + @spec get_address!(integer | String.t()) :: Address.t() + def get_address!(id), do: Repo.get!(Address, id) @doc """ - Creates a address. - - ## Examples - - iex> create_address(%{field: value}) - {:ok, %Address{}} - - iex> create_address(%{field: bad_value}) - {:error, %Ecto.Changeset{}} - + Gets a single address by its url. """ + @spec get_address_by_url(String.t()) :: Address.t() | nil + def get_address_by_url(url), do: Repo.get_by(Address, url: url) + + @doc """ + Creates an address. + """ + @spec create_address(map) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()} def create_address(attrs \\ %{}) do %Address{} |> Address.changeset(attrs) @@ -90,17 +57,9 @@ defmodule Mobilizon.Addresses do end @doc """ - Updates a address. - - ## Examples - - iex> update_address(address, %{field: new_value}) - {:ok, %Address{}} - - iex> update_address(address, %{field: bad_value}) - {:error, %Ecto.Changeset{}} - + Updates an address. """ + @spec update_address(Address.t(), map) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()} def update_address(%Address{} = address, attrs) do address |> Address.changeset(attrs) @@ -108,131 +67,87 @@ defmodule Mobilizon.Addresses do end @doc """ - Deletes a Address. - - ## Examples - - iex> delete_address(address) - {:ok, %Address{}} - - iex> delete_address(address) - {:error, %Ecto.Changeset{}} - + Deletes an address. """ - def delete_address(%Address{} = address) do - Repo.delete(address) - end + @spec delete_address(Address.t()) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()} + def delete_address(%Address{} = address), do: Repo.delete(address) @doc """ - Returns an `%Ecto.Changeset{}` for tracking address changes. - - ## Examples - - iex> change_address(address) - %Ecto.Changeset{source: %Address{}} + Searches addresses. + We only look at the description for now, and eventually order by object distance. """ - def change_address(%Address{} = address) do - Address.changeset(address, %{}) - end + @spec search_addresses(String.t(), keyword) :: [Address.t()] + def search_addresses(search, options \\ []) do + query = + search + |> search_addresses_query(Keyword.get(options, :limit, 5)) + |> order_by_coords(Keyword.get(options, :coords)) + |> filter_by_contry(Keyword.get(options, :country)) - @doc """ - Processes raw geo data informations and return a `Geo` geometry which can be one of `Geo.Point`. - """ - # TODO: Unused, remove me - def process_geom(%{"type" => type_input, "data" => data}) do - type = - if !is_atom(type_input) && type_input != nil do - try do - String.to_existing_atom(type_input) - rescue - e in ArgumentError -> - Logger.error("#{type_input} is not an existing atom : #{inspect(e)}") - :invalid_type - end - else - type_input - end - - if Enum.member?(@geom_types, type) do - case type do - :point -> - process_point(data["latitude"], data["longitude"]) - end - else - {:error, :invalid_type} + case Keyword.get(options, :single, false) do + true -> Repo.one(query) + false -> Repo.all(query) end end - @doc false - def process_geom(nil) do - {:error, nil} - end - - @spec process_point(number(), number()) :: tuple() - defp process_point(latitude, longitude) when is_number(latitude) and is_number(longitude) do - {:ok, %Geo.Point{coordinates: {latitude, longitude}, srid: 4326}} - end - - defp process_point(_, _) do - {:error, "Latitude and longitude must be numbers"} - end - @doc """ - Search addresses in our database + Reverse geocode from coordinates. - We only look at the description for now, and eventually order by object distance + We only take addresses 50km around and sort them by distance. """ - @spec search_addresses(String.t(), list()) :: list(Address.t()) - def search_addresses(search, options \\ []) do - limit = Keyword.get(options, :limit, 5) - - query = from(a in Address, where: ilike(a.description, ^"%#{search}%"), limit: ^limit) - - query = - if coords = Keyword.get(options, :coords, false), - do: - from(a in query, - order_by: [fragment("? <-> ?", a.geom, ^"POINT(#{coords.lon} #{coords.lat})'")] - ), - else: query - - query = - if country = Keyword.get(options, :country, nil), - do: from(a in query, where: ilike(a.country, ^"%#{country}%")), - else: query - - if Keyword.get(options, :single, false) == true, do: Repo.one(query), else: Repo.all(query) - end - - @doc """ - Reverse geocode from coordinates in our database - - We only take addresses 50km around and sort them by distance - """ - @spec reverse_geocode(number(), number(), list()) :: list(Address.t()) + @spec reverse_geocode(number, number, keyword) :: [Address.t()] def reverse_geocode(lon, lat, options) do limit = Keyword.get(options, :limit, 5) radius = Keyword.get(options, :radius, 50_000) - country = Keyword.get(options, :country, nil) + country = Keyword.get(options, :country) srid = Keyword.get(options, :srid, 4326) - import Geo.PostGIS - with {:ok, point} <- Geo.WKT.decode("SRID=#{srid};POINT(#{lon} #{lat})") do - query = - from(a in Address, - order_by: [fragment("? <-> ?", a.geom, ^point)], - limit: ^limit, - where: st_dwithin_in_meters(^point, a.geom, ^radius) - ) - - query = - if country, - do: from(a in query, where: ilike(a.country, ^"%#{country}%")), - else: query - - Repo.all(query) + point + |> addresses_around_query(radius, limit) + |> filter_by_contry(country) + |> Repo.all() end end + + @spec search_addresses_query(String.t(), integer) :: Ecto.Query.t() + defp search_addresses_query(search, limit) do + from( + a in Address, + where: ilike(a.description, ^"%#{search}%"), + limit: ^limit + ) + end + + @spec order_by_coords(Ecto.Query.t(), map | nil) :: Ecto.Query.t() + defp order_by_coords(query, nil), do: query + + defp order_by_coords(query, coords) do + from( + a in query, + order_by: [fragment("? <-> ?", a.geom, ^"POINT(#{coords.lon} #{coords.lat})'")] + ) + end + + @spec filter_by_contry(Ecto.Query.t(), String.t() | nil) :: Ecto.Query.t() + defp filter_by_contry(query, nil), do: query + + defp filter_by_contry(query, country) do + from( + a in query, + where: ilike(a.country, ^"%#{country}%") + ) + end + + @spec addresses_around_query(Geo.geometry(), integer, integer) :: Ecto.Query.t() + defp addresses_around_query(point, radius, limit) do + import Geo.PostGIS + + from(a in Address, + where: st_dwithin_in_meters(^point, a.geom, ^radius), + order_by: [fragment("? <-> ?", a.geom, ^point)], + limit: ^limit + ) + end end diff --git a/test/mobilizon/addresses/addresses_test.exs b/test/mobilizon/addresses/addresses_test.exs index 8f9e267fe..8be79b2f1 100644 --- a/test/mobilizon/addresses/addresses_test.exs +++ b/test/mobilizon/addresses/addresses_test.exs @@ -76,23 +76,5 @@ defmodule Mobilizon.AddressesTest do assert {:ok, %Address{}} = Addresses.delete_address(address) assert_raise Ecto.NoResultsError, fn -> Addresses.get_address!(address.id) end end - - test "change_address/1 returns a address changeset" do - address = insert(:address) - assert %Ecto.Changeset{} = Addresses.change_address(address) - end - - test "process_geom/2 with valid data returns a Point element" do - attrs = %{"type" => "point", "data" => %{"latitude" => 10, "longitude" => -10}} - assert {:ok, %Geo.Point{}} = Addresses.process_geom(attrs) - end - - test "process_geom/2 with invalid data returns nil" do - attrs = %{"type" => :point, "data" => %{"latitude" => nil, "longitude" => nil}} - assert {:error, "Latitude and longitude must be numbers"} = Addresses.process_geom(attrs) - - attrs = %{"type" => :not_valid, "data" => %{"latitude" => nil, "longitude" => nil}} - assert {:error, :invalid_type} == Addresses.process_geom(attrs) - end end end From 4418275223a0ecc902271eeb8f4e69976cf36986 Mon Sep 17 00:00:00 2001 From: miffigriffy Date: Mon, 9 Sep 2019 00:52:49 +0200 Subject: [PATCH 12/29] Refactoring of Actors context --- lib/mobilizon/actors/actor.ex | 672 ++++------- lib/mobilizon/actors/actors.ex | 1055 +++++++++-------- lib/mobilizon/actors/bot.ex | 22 +- lib/mobilizon/actors/follower.ex | 53 +- lib/mobilizon/actors/member.ex | 99 +- lib/mobilizon/addresses/addresses.ex | 12 +- lib/mobilizon/admin/admin.ex | 20 +- lib/mobilizon/crypto.ex | 13 + lib/mobilizon/reports/reports.ex | 48 +- lib/mobilizon/users/users.ex | 8 +- lib/mobilizon_web/api/follows.ex | 2 +- lib/mobilizon_web/api/search.ex | 76 +- .../controllers/activity_pub_controller.ex | 8 +- lib/mobilizon_web/resolvers/group.ex | 10 +- lib/mobilizon_web/resolvers/person.ex | 9 +- lib/mobilizon_web/resolvers/report.ex | 4 +- .../views/activity_pub/actor_view.ex | 21 +- lib/service/activity_pub/activity_pub.ex | 31 +- .../activity_pub/converters/comment.ex | 3 +- lib/service/activity_pub/relay.ex | 8 +- lib/service/activity_pub/transmogrifier.ex | 18 +- lib/service/export/feed.ex | 2 +- lib/service/export/icalendar.ex | 2 +- lib/service/http_signatures/signature.ex | 55 +- mix.exs | 6 +- ...80517100700_move_from_account_to_actor.exs | 4 +- ...0190301141830_move_member_role_to_enum.exs | 8 +- .../20190301143831_actor_group_openness.exs | 6 +- ...20190425075451_add_visibility_to_actor.exs | 8 +- test/mobilizon/actors/actors_test.exs | 135 +-- .../activity_pub/activity_pub_test.exs | 3 +- .../activity_pub/transmogrifier_test.exs | 22 +- test/mobilizon_web/api/search_test.exs | 14 +- .../activity_pub_controller_test.exs | 14 +- test/support/factory.ex | 13 +- test/tasks/relay_test.exs | 6 +- 36 files changed, 1145 insertions(+), 1345 deletions(-) diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex index e83d9a308..7820af547 100644 --- a/lib/mobilizon/actors/actor.ex +++ b/lib/mobilizon/actors/actor.ex @@ -1,44 +1,17 @@ -import EctoEnum - -defenum(Mobilizon.Actors.ActorTypeEnum, :actor_type, [ - :Person, - :Application, - :Group, - :Organization, - :Service -]) - -defenum(Mobilizon.Actors.ActorOpennessEnum, :actor_openness, [ - :invite_only, - :moderated, - :open -]) - -defenum(Mobilizon.Actors.ActorVisibilityEnum, :actor_visibility_type, [ - :public, - :unlisted, - # Probably unused - :restricted, - :private -]) - defmodule Mobilizon.Actors.Actor do @moduledoc """ - Represents an actor (local and remote actors) + Represents an actor (local and remote). """ use Ecto.Schema import Ecto.Changeset - import Ecto.Query - alias Mobilizon.Actors - alias Mobilizon.Actors.{Actor, Follower, Member} - alias Mobilizon.Config + alias Mobilizon.{Actors, Config, Crypto} + alias Mobilizon.Actors.{Actor, ActorOpenness, ActorType, ActorVisibility, Follower, Member} alias Mobilizon.Events.{Event, FeedToken} alias Mobilizon.Media.File alias Mobilizon.Reports.{Report, Note} - alias Mobilizon.Storage.{Page, Repo} alias Mobilizon.Users.User alias MobilizonWeb.Router.Helpers, as: Routes @@ -46,7 +19,97 @@ defmodule Mobilizon.Actors.Actor do require Logger - # @type t :: %Actor{description: String.t, id: integer(), inserted_at: DateTime.t, updated_at: DateTime.t, display_name: String.t, domain: String.t, keys: String.t, suspended: boolean(), url: String.t, username: String.t, organized_events: list(), groups: list(), group_request: list(), user: User.t, field: ActorTypeEnum.t} + @type t :: %__MODULE__{ + url: String.t(), + outbox_url: String.t(), + inbox_url: String.t(), + following_url: String.t(), + followers_url: String.t(), + shared_inbox_url: String.t(), + type: ActorType.t(), + name: String.t(), + domain: String.t(), + summary: String.t(), + preferred_username: String.t(), + keys: String.t(), + manually_approves_followers: boolean, + openness: ActorOpenness.t(), + visibility: ActorVisibility.t(), + suspended: boolean, + avatar: File.t(), + banner: File.t(), + user: User.t(), + followers: [Follower.t()], + followings: [Follower.t()], + organized_events: [Event.t()], + feed_tokens: [FeedToken.t()], + created_reports: [Report.t()], + subject_reports: [Report.t()], + report_notes: [Note.t()], + memberships: [Actor.t()] + } + + @required_attrs [:preferred_username, :keys, :suspended, :url] + @optional_attrs [ + :outbox_url, + :inbox_url, + :shared_inbox_url, + :following_url, + :followers_url, + :type, + :name, + :domain, + :summary, + :manually_approves_followers, + :user_id + ] + @attrs @required_attrs ++ @optional_attrs + + @update_required_attrs @required_attrs + @update_optional_attrs [:name, :summary, :manually_approves_followers, :user_id] + @update_attrs @update_required_attrs ++ @update_optional_attrs + + @registration_required_attrs [:preferred_username, :keys, :suspended, :url, :type] + @registration_optional_attrs [:domain, :name, :summary, :user_id] + @registration_attrs @registration_required_attrs ++ @registration_optional_attrs + + @remote_actor_creation_required_attrs [ + :url, + :inbox_url, + :type, + :domain, + :preferred_username, + :keys + ] + @remote_actor_creation_optional_attrs [ + :outbox_url, + :shared_inbox_url, + :following_url, + :followers_url, + :name, + :summary, + :manually_approves_followers + ] + @remote_actor_creation_attrs @remote_actor_creation_required_attrs ++ + @remote_actor_creation_optional_attrs + + @relay_creation_attrs [ + :type, + :name, + :summary, + :url, + :keys, + :preferred_username, + :domain, + :inbox_url, + :followers_url, + :following_url, + :shared_inbox_url + ] + + @group_creation_required_attrs [:url, :outbox_url, :inbox_url, :type, :preferred_username] + @group_creation_optional_attrs [:shared_inbox_url, :name, :domain, :summary] + @group_creation_attrs @group_creation_required_attrs ++ @group_creation_optional_attrs schema "actors" do field(:url, :string) @@ -55,187 +118,156 @@ defmodule Mobilizon.Actors.Actor do field(:following_url, :string) field(:followers_url, :string) field(:shared_inbox_url, :string) - field(:type, Mobilizon.Actors.ActorTypeEnum, default: :Person) + field(:type, ActorType, default: :Person) field(:name, :string) field(:domain, :string, default: nil) field(:summary, :string) field(:preferred_username, :string) field(:keys, :string) field(:manually_approves_followers, :boolean, default: false) - field(:openness, Mobilizon.Actors.ActorOpennessEnum, default: :moderated) - field(:visibility, Mobilizon.Actors.ActorVisibilityEnum, default: :private) + field(:openness, ActorOpenness, default: :moderated) + field(:visibility, ActorVisibility, default: :private) field(:suspended, :boolean, default: false) - # field(:openness, Mobilizon.Actors.ActorOpennessEnum, default: :moderated) + + embeds_one(:avatar, File, on_replace: :update) + embeds_one(:banner, File, on_replace: :update) + belongs_to(:user, User) has_many(:followers, Follower, foreign_key: :target_actor_id) has_many(:followings, Follower, foreign_key: :actor_id) has_many(:organized_events, Event, foreign_key: :organizer_actor_id) - many_to_many(:memberships, Actor, join_through: Member) - belongs_to(:user, User) has_many(:feed_tokens, FeedToken, foreign_key: :actor_id) - embeds_one(:avatar, File, on_replace: :update) - embeds_one(:banner, File, on_replace: :update) has_many(:created_reports, Report, foreign_key: :reporter_id) has_many(:subject_reports, Report, foreign_key: :reported_id) has_many(:report_notes, Note, foreign_key: :moderator_id) + many_to_many(:memberships, Actor, join_through: Member) timestamps() end + @doc """ + Checks whether actor visibility is public. + """ + @spec is_public_visibility(Actor.t()) :: boolean + def is_public_visibility(%Actor{visibility: visibility}) do + visibility in [:public, :unlisted] + end + + @doc """ + Returns the display name if available, or the preferred username + (with the eventual @domain suffix if it's a distant actor). + """ + @spec display_name(Actor.t()) :: String.t() + def display_name(%Actor{name: name} = actor) when name in [nil, ""] do + preferred_username_and_domain(actor) + end + + def display_name(%Actor{name: name}), do: name + + @doc """ + Returns display name and username. + """ + @spec display_name_and_username(Actor.t()) :: String.t() + def display_name_and_username(%Actor{name: name} = actor) when name in [nil, ""] do + preferred_username_and_domain(actor) + end + + def display_name_and_username(%Actor{name: name} = actor) do + "#{name} (#{preferred_username_and_domain(actor)})" + end + + @doc """ + Returns the preferred username with the eventual @domain suffix if it's + a distant actor. + """ + @spec preferred_username_and_domain(Actor.t()) :: String.t() + def preferred_username_and_domain(%Actor{preferred_username: preferred_username, domain: nil}) do + preferred_username + end + + def preferred_username_and_domain(%Actor{preferred_username: preferred_username, domain: domain}) do + "#{preferred_username}@#{domain}" + end + @doc false + @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() def changeset(%Actor{} = actor, attrs) do actor - |> Ecto.Changeset.cast(attrs, [ - :url, - :outbox_url, - :inbox_url, - :shared_inbox_url, - :following_url, - :followers_url, - :type, - :name, - :domain, - :summary, - :preferred_username, - :keys, - :manually_approves_followers, - :suspended, - :user_id - ]) + |> cast(attrs, @attrs) |> build_urls() |> cast_embed(:avatar) |> cast_embed(:banner) |> unique_username_validator() - |> validate_required([:preferred_username, :keys, :suspended, :url]) - |> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index) + |> validate_required(@required_attrs) + |> unique_constraint(:preferred_username, + name: :actors_preferred_username_domain_type_index + ) |> unique_constraint(:url, name: :actors_url_index) end @doc false + @spec update_changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() def update_changeset(%Actor{} = actor, attrs) do actor - |> Ecto.Changeset.cast(attrs, [ - :name, - :summary, - :keys, - :manually_approves_followers, - :suspended, - :user_id - ]) + |> cast(attrs, @update_attrs) |> cast_embed(:avatar) |> cast_embed(:banner) - |> validate_required([:preferred_username, :keys, :suspended, :url]) - |> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index) + |> validate_required(@update_required_attrs) + |> unique_constraint(:preferred_username, + name: :actors_preferred_username_domain_type_index + ) |> unique_constraint(:url, name: :actors_url_index) end @doc """ - Changeset for person registration + Changeset for person registration. """ - @spec registration_changeset(struct(), map()) :: Ecto.Changeset.t() + @spec registration_changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() def registration_changeset(%Actor{} = actor, attrs) do actor - |> Ecto.Changeset.cast(attrs, [ - :preferred_username, - :domain, - :name, - :summary, - :keys, - :suspended, - :url, - :type, - :user_id - ]) + |> cast(attrs, @registration_attrs) |> build_urls() |> cast_embed(:avatar) |> cast_embed(:banner) - # Needed because following constraint can't work for domain null values (local) |> unique_username_validator() - |> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index) + |> unique_constraint(:preferred_username, + name: :actors_preferred_username_domain_type_index + ) |> unique_constraint(:url, name: :actors_url_index) - |> validate_required([:preferred_username, :keys, :suspended, :url, :type]) + |> validate_required(@registration_required_attrs) end - # TODO : Use me ! - # @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ @doc """ - Changeset for remote actor creation + Changeset for remote actor creation. """ - @spec remote_actor_creation(map()) :: Ecto.Changeset.t() - def remote_actor_creation(params) do - changes = + @spec remote_actor_creation_changeset(map) :: Ecto.Changeset.t() + def remote_actor_creation_changeset(attrs) do + changeset = %Actor{} - |> Ecto.Changeset.cast(params, [ - :url, - :outbox_url, - :inbox_url, - :shared_inbox_url, - :following_url, - :followers_url, - :type, - :name, - :domain, - :summary, - :preferred_username, - :keys, - :manually_approves_followers - ]) - |> validate_required([ - :url, - :inbox_url, - :type, - :domain, - :preferred_username, - :keys - ]) + |> cast(attrs, @remote_actor_creation_attrs) + |> validate_required(@remote_actor_creation_required_attrs) |> cast_embed(:avatar) |> cast_embed(:banner) - # Needed because following constraint can't work for domain null values (local) |> unique_username_validator() - |> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index) + |> unique_constraint(:preferred_username, + name: :actors_preferred_username_domain_type_index + ) |> unique_constraint(:url, name: :actors_url_index) |> validate_length(:summary, max: 5000) |> validate_length(:preferred_username, max: 100) - Logger.debug("Remote actor creation") - Logger.debug(inspect(changes)) - changes + Logger.debug("Remote actor creation: #{inspect(changeset)}") + + changeset end - def relay_creation(%{url: url, preferred_username: preferred_username} = _params) do - key = :public_key.generate_key({:rsa, 2048, 65_537}) - entry = :public_key.pem_entry_encode(:RSAPrivateKey, key) - pem = [entry] |> :public_key.pem_encode() |> String.trim_trailing() + @doc """ + Changeset for relay creation. + """ + @spec relay_creation_changeset(map) :: Ecto.Changeset.t() + def relay_creation_changeset(attrs) do + relay_creation_attrs = build_relay_creation_attrs(attrs) - vars = %{ - "name" => Config.get([:instance, :name], "Mobilizon"), - "summary" => Config.get( - [:instance, :description], - "An internal service actor for this Mobilizon instance" - ), - "url" => url, - "keys" => pem, - "preferred_username" => preferred_username, - "domain" => nil, - "inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox", - "followers_url" => "#{url}/followers", - "following_url" => "#{url}/following", - "shared_inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox", - "type" => :Application - } - - cast(%Actor{}, vars, [ - :type, - :name, - :summary, - :url, - :keys, - :preferred_username, - :domain, - :inbox_url, - :followers_url, - :following_url, - :shared_inbox_url - ]) + cast(%Actor{}, relay_creation_attrs, @relay_creation_attrs) end @doc """ @@ -244,68 +276,48 @@ defmodule Mobilizon.Actors.Actor do @spec group_creation(struct(), map()) :: Ecto.Changeset.t() def group_creation(%Actor{} = actor, params) do actor - |> Ecto.Changeset.cast(params, [ - :url, - :outbox_url, - :inbox_url, - :shared_inbox_url, - :type, - :name, - :domain, - :summary, - :preferred_username - ]) + |> cast(params, @group_creation_attrs) |> cast_embed(:avatar) |> cast_embed(:banner) |> build_urls(:Group) |> put_change(:domain, nil) - |> put_change(:keys, Actors.create_keys()) + |> put_change(:keys, Crypto.generate_rsa_2048_private_key()) |> put_change(:type, :Group) |> unique_username_validator() - |> validate_required([:url, :outbox_url, :inbox_url, :type, :preferred_username]) - |> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index) + |> validate_required(@group_creation_required_attrs) + |> unique_constraint(:preferred_username, + name: :actors_preferred_username_domain_type_index + ) |> unique_constraint(:url, name: :actors_url_index) |> validate_length(:summary, max: 5000) |> validate_length(:preferred_username, max: 100) end + # Needed because following constraint can't work for domain null values (local) + @spec unique_username_validator(Ecto.Changeset.t()) :: Ecto.Changeset.t() defp unique_username_validator( %Ecto.Changeset{changes: %{preferred_username: username} = changes} = changeset ) do with nil <- Map.get(changes, :domain, nil), - %Actor{preferred_username: _username} <- Actors.get_local_actor_by_name(username) do - changeset |> add_error(:preferred_username, "Username is already taken") + %Actor{preferred_username: _} <- Actors.get_local_actor_by_name(username) do + add_error(changeset, :preferred_username, "Username is already taken") else _ -> changeset end end # When we don't even have any preferred_username, don't even try validating preferred_username - defp unique_username_validator(changeset) do - changeset - end + defp unique_username_validator(changeset), do: changeset - @spec build_urls(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t() + @spec build_urls(Ecto.Changeset.t(), ActorType.t()) :: Ecto.Changeset.t() defp build_urls(changeset, type \\ :Person) defp build_urls(%Ecto.Changeset{changes: %{preferred_username: username}} = changeset, _type) do changeset - |> put_change( - :outbox_url, - build_url(username, :outbox) - ) - |> put_change( - :followers_url, - build_url(username, :followers) - ) - |> put_change( - :following_url, - build_url(username, :following) - ) - |> put_change( - :inbox_url, - build_url(username, :inbox) - ) + |> put_change(:outbox_url, build_url(username, :outbox)) + |> put_change(:followers_url, build_url(username, :followers)) + |> put_change(:following_url, build_url(username, :following)) + |> put_change(:inbox_url, build_url(username, :inbox)) |> put_change(:shared_inbox_url, "#{MobilizonWeb.Endpoint.url()}/inbox") |> put_change(:url, build_url(username, :page)) end @@ -313,19 +325,19 @@ defmodule Mobilizon.Actors.Actor do defp build_urls(%Ecto.Changeset{} = changeset, _type), do: changeset @doc """ - Build an AP URL for an actor + Builds an AP URL for an actor. """ - @spec build_url(String.t(), atom()) :: String.t() + @spec build_url(String.t(), atom, keyword) :: String.t() def build_url(preferred_username, endpoint, args \\ []) + def build_url(username, :inbox, _args), do: "#{build_url(username, :page)}/inbox" + def build_url(preferred_username, :page, args) do Endpoint |> Routes.page_url(:actor, preferred_username, args) |> URI.decode() end - def build_url(username, :inbox, _args), do: "#{build_url(username, :page)}/inbox" - def build_url(preferred_username, endpoint, args) when endpoint in [:outbox, :following, :followers] do Endpoint @@ -333,267 +345,35 @@ defmodule Mobilizon.Actors.Actor do |> URI.decode() end - @doc """ - Get a public key for a given ActivityPub actor ID (url) - """ - @spec get_public_key_for_url(String.t()) :: {:ok, String.t()} | {:error, atom()} - def get_public_key_for_url(url) do - with {:ok, %Actor{keys: keys}} <- Actors.get_or_fetch_by_url(url), - {:ok, public_key} <- prepare_public_key(keys) do - {:ok, public_key} - else - {:error, :pem_decode_error} -> - Logger.error("Error while decoding PEM") - {:error, :pem_decode_error} - - _ -> - Logger.error("Unable to fetch actor, so no keys for you") - {:error, :actor_fetch_error} - end - end - - @doc """ - Convert internal PEM encoded keys to public key format - """ - @spec prepare_public_key(String.t()) :: {:ok, tuple()} | {:error, :pem_decode_error} - def prepare_public_key(public_key_code) do - case :public_key.pem_decode(public_key_code) do - [public_key_entry] -> - {:ok, :public_key.pem_entry_decode(public_key_entry)} - - _err -> - {:error, :pem_decode_error} - end - end - - @doc """ - Get followers from an actor - - If actor A and C both follow actor B, actor B's followers are A and C - """ - @spec get_followers(struct(), number(), number()) :: map() - def get_followers(%Actor{id: actor_id} = _actor, page \\ nil, limit \\ nil) do - query = - from( - a in Actor, - join: f in Follower, - on: a.id == f.actor_id, - where: f.target_actor_id == ^actor_id - ) - - total = Task.async(fn -> Repo.aggregate(query, :count, :id) end) - elements = Task.async(fn -> Repo.all(Page.paginate(query, page, limit)) end) - - %{total: Task.await(total), elements: Task.await(elements)} - end - - defp get_full_followers_query(%Actor{id: actor_id} = _actor) do - from( - a in Actor, - join: f in Follower, - on: a.id == f.actor_id, - where: f.target_actor_id == ^actor_id - ) - end - - @spec get_full_followers(struct()) :: list() - def get_full_followers(%Actor{} = actor) do - actor - |> get_full_followers_query() - |> Repo.all() - end - - @spec get_full_external_followers(struct()) :: list() - def get_full_external_followers(%Actor{} = actor) do - actor - |> get_full_followers_query() - |> where([a], not is_nil(a.domain)) - |> Repo.all() - end - - @doc """ - Get followings from an actor - - If actor A follows actor B and C, actor A's followings are B and B - """ - @spec get_followings(struct(), number(), number()) :: list() - def get_followings(%Actor{id: actor_id} = _actor, page \\ nil, limit \\ nil) do - query = - from( - a in Actor, - join: f in Follower, - on: a.id == f.target_actor_id, - where: f.actor_id == ^actor_id - ) - - total = Task.async(fn -> Repo.aggregate(query, :count, :id) end) - elements = Task.async(fn -> Repo.all(Page.paginate(query, page, limit)) end) - - %{total: Task.await(total), elements: Task.await(elements)} - end - - @spec get_full_followings(struct()) :: list() - def get_full_followings(%Actor{id: actor_id} = _actor) do - Repo.all( - from( - a in Actor, - join: f in Follower, - on: a.id == f.target_actor_id, - where: f.actor_id == ^actor_id - ) - ) - end - - @doc """ - Returns the groups an actor is member of - """ - @spec get_groups_member_of(struct()) :: list() - def get_groups_member_of(%Actor{id: actor_id}) do - Repo.all( - from( - a in Actor, - join: m in Member, - on: a.id == m.parent_id, - where: m.actor_id == ^actor_id - ) - ) - end - - @doc """ - Returns the members for a group actor - """ - @spec get_members_for_group(struct()) :: list() - def get_members_for_group(%Actor{id: actor_id}) do - Repo.all( - from( - a in Actor, - join: m in Member, - on: a.id == m.actor_id, - where: m.parent_id == ^actor_id - ) - ) - end - - @doc """ - Make an actor follow another - """ - @spec follow(struct(), struct(), boolean()) :: Follower.t() | {:error, String.t()} - def follow(%Actor{} = followed, %Actor{} = follower, url \\ nil, approved \\ true) do - with {:suspended, false} <- {:suspended, followed.suspended}, - # Check if followed has blocked follower - {:already_following, false} <- {:already_following, following?(follower, followed)} do - do_follow(follower, followed, approved, url) - else - {:already_following, %Follower{}} -> - {:error, :already_following, - "Could not follow actor: you are already following #{followed.preferred_username}"} - - {:suspended, _} -> - {:error, :suspended, - "Could not follow actor: #{followed.preferred_username} has been suspended"} - end - end - - @doc """ - Unfollow an actor (remove a `Mobilizon.Actors.Follower`) - """ - @spec unfollow(struct(), struct()) :: {:ok, Follower.t()} | {:error, Ecto.Changeset.t()} - def unfollow(%Actor{} = followed, %Actor{} = follower) do - case {:already_following, following?(follower, followed)} do - {:already_following, %Follower{} = follow} -> - Actors.delete_follower(follow) - - {:already_following, false} -> - {:error, "Could not unfollow actor: you are not following #{followed.preferred_username}"} - end - end - - @spec do_follow(struct(), struct(), boolean(), String.t()) :: - {:ok, Follower.t()} | {:error, Ecto.Changeset.t()} - defp do_follow(%Actor{} = follower, %Actor{} = followed, approved, url) do - Logger.info( - "Making #{follower.preferred_username} follow #{followed.preferred_username} (approved: #{ - approved - })" - ) - - Actors.create_follower(%{ - "actor_id" => follower.id, - "target_actor_id" => followed.id, - "approved" => approved, - "url" => url - }) - end - - @doc """ - Returns whether an actor is following another - """ - @spec following?(struct(), struct()) :: Follower.t() | false - def following?( - %Actor{} = follower_actor, - %Actor{} = followed_actor - ) do - case Actors.get_follower(followed_actor, follower_actor) do - nil -> false - %Follower{} = follow -> follow - end - end - - @spec public_visibility?(struct()) :: boolean() - def public_visibility?(%Actor{visibility: visibility}), do: visibility in [:public, :unlisted] - - @doc """ - Return the preferred_username with the eventual @domain suffix if it's a distant actor - """ - @spec actor_acct_from_actor(struct()) :: String.t() - def actor_acct_from_actor(%Actor{preferred_username: preferred_username, domain: domain}) do - if is_nil(domain) do - preferred_username - else - "#{preferred_username}@#{domain}" - end - end - - @doc """ - Returns the display name if available, or the preferred_username (with the eventual @domain suffix if it's a distant actor). - """ - @spec display_name(struct()) :: String.t() - def display_name(%Actor{name: name} = actor) do - case name do - nil -> actor_acct_from_actor(actor) - "" -> actor_acct_from_actor(actor) - name -> name - end - end - - @doc """ - Return display name and username - - ## Examples - iex> display_name_and_username(%Actor{name: "Thomas C", preferred_username: "tcit", domain: nil}) - "Thomas (tcit)" - - iex> display_name_and_username(%Actor{name: "Thomas C", preferred_username: "tcit", domain: "framapiaf.org"}) - "Thomas (tcit@framapiaf.org)" - - iex> display_name_and_username(%Actor{name: nil, preferred_username: "tcit", domain: "framapiaf.org"}) - "tcit@framapiaf.org" - - """ - @spec display_name_and_username(struct()) :: String.t() - def display_name_and_username(%Actor{name: nil} = actor), do: actor_acct_from_actor(actor) - def display_name_and_username(%Actor{name: ""} = actor), do: actor_acct_from_actor(actor) - - def display_name_and_username(%Actor{name: name} = actor), - do: name <> " (" <> actor_acct_from_actor(actor) <> ")" - @doc """ Clear multiple caches for an actor """ + # TODO: move to MobilizonWeb @spec clear_cache(struct()) :: {:ok, true} def clear_cache(%Actor{preferred_username: preferred_username, domain: nil}) do Cachex.del(:activity_pub, "actor_" <> preferred_username) Cachex.del(:feed, "actor_" <> preferred_username) Cachex.del(:ics, "actor_" <> preferred_username) end + + @spec build_relay_creation_attrs(map) :: map + defp build_relay_creation_attrs(%{url: url, preferred_username: preferred_username}) do + %{ + "name" => Config.get([:instance, :name], "Mobilizon"), + "summary" => + Config.get( + [:instance, :description], + "An internal service actor for this Mobilizon instance" + ), + "url" => url, + "keys" => Crypto.generate_rsa_2048_private_key(), + "preferred_username" => preferred_username, + "domain" => nil, + "inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox", + "followers_url" => "#{url}/followers", + "following_url" => "#{url}/following", + "shared_inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox", + "type" => :Application + } + end end diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index 3a3617b9d..eb4da0fbb 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -4,105 +4,206 @@ defmodule Mobilizon.Actors do """ import Ecto.Query + import EctoEnum alias Ecto.Multi - alias Mobilizon.Actors.{Actor, Bot, Member, Follower} + alias Mobilizon.Actors.{Actor, Bot, Follower, Member} + alias Mobilizon.Crypto alias Mobilizon.Media.File - alias Mobilizon.Service.ActivityPub alias Mobilizon.Storage.{Page, Repo} require Logger - @doc false - def data() do - Dataloader.Ecto.new(Repo, query: &query/2) - end + defenum(ActorType, :actor_type, [ + :Person, + :Application, + :Group, + :Organization, + :Service + ]) + + defenum(ActorOpenness, :actor_openness, [ + :invite_only, + :moderated, + :open + ]) + + defenum(ActorVisibility, :actor_visibility, [ + :public, + :unlisted, + # Probably unused + :restricted, + :private + ]) + + defenum(MemberRole, :member_role, [ + :not_approved, + :member, + :moderator, + :administrator, + :creator + ]) @doc false - def query(queryable, _params) do - queryable - end + @spec data :: Dataloader.Ecto.t() + def data, do: Dataloader.Ecto.new(Repo, query: &query/2) - @doc """ - Returns the list of actors. - - ## Examples - - iex> Mobilizon.Actors.list_actors() - [%Mobilizon.Actors.Actor{}] - - """ - @spec list_actors() :: list() - def list_actors do - Repo.all(Actor) - end + @doc false + @spec query(Ecto.Query.t(), map) :: Ecto.Query.t() + def query(queryable, _params), do: queryable @doc """ Gets a single actor. + """ + @spec get_actor(integer | String.t()) :: Actor.t() | nil + def get_actor(id), do: Repo.get(Actor, id) + @doc """ + Gets a single actor. Raises `Ecto.NoResultsError` if the Actor does not exist. - - ## Examples - - iex> get_actor!(123) - %Mobilizon.Actors.Actor{} - - iex> get_actor!(456) - ** (Ecto.NoResultsError) - """ - @spec get_actor!(integer()) :: Actor.t() - def get_actor!(id) do - Repo.get!(Actor, id) - end - - def get_actor(id) do - Repo.get(Actor, id) - end - - # Get actor by ID and preload organized events, followers and followings - @spec get_actor_with_everything(integer()) :: Ecto.Query.t() - defp do_get_actor_with_everything(id) do - from(a in Actor, - where: a.id == ^id, - preload: [:organized_events, :followers, :followings] - ) - end + @spec get_actor!(integer | String.t()) :: Actor.t() + def get_actor!(id), do: Repo.get!(Actor, id) @doc """ - Returns an actor with every relation + Gets an actor with preloaded relations. """ - @spec get_actor_with_everything(integer()) :: Mobilizon.Actors.Actor.t() - def get_actor_with_everything(id) do + @spec get_actor_with_preload(integer | String.t()) :: Actor.t() | nil + def get_actor_with_preload(id) do id - |> do_get_actor_with_everything + |> actor_with_preload_query() |> Repo.one() end @doc """ - Returns an actor with every relation + Gets a local actor with preloaded relations. """ - @spec get_local_actor_with_everything(integer()) :: Mobilizon.Actors.Actor.t() - def get_local_actor_with_everything(id) do + @spec get_local_actor_with_preload(integer | String.t()) :: Actor.t() | nil + def get_local_actor_with_preload(id) do id - |> do_get_actor_with_everything - |> where([a], is_nil(a.domain)) + |> actor_with_preload_query() + |> filter_local() |> Repo.one() end @doc """ - Creates a actor. - - ## Examples - - iex> create_actor(%{preferred_username: "test"}) - {:ok, %Mobilizon.Actors.Actor{preferred_username: "test"}} - - iex> create_actor(%{preferred_username: nil}) - {:error, %Ecto.Changeset{}} - + Gets an actor by its URL (ActivityPub ID). The `:preload` option allows to + preload the followers relation. """ + @spec get_actor_by_url(String.t(), boolean) :: + {:ok, Actor.t()} | {:error, :actor_not_found} + def get_actor_by_url(url, preload \\ false) do + case Repo.get_by(Actor, url: url) do + nil -> + {:error, :actor_not_found} + + actor -> + {:ok, preload_followers(actor, preload)} + end + end + + @doc """ + Gets an actor by its URL (ActivityPub ID). The `:preload` option allows to + preload the followers relation. + Raises `Ecto.NoResultsError` if the actor does not exist. + """ + @spec get_actor_by_url!(String.t(), boolean) :: Actor.t() + def get_actor_by_url!(url, preload \\ false) do + Actor + |> Repo.get_by!(url: url) + |> preload_followers(preload) + end + + @doc """ + Gets an actor by name. + """ + @spec get_actor_by_name(String.t(), atom | nil) :: Actor.t() | nil + def get_actor_by_name(name, type \\ nil) do + from(a in Actor) + |> filter_by_type(type) + |> filter_by_name(String.split(name, "@")) + |> Repo.one() + end + + @doc """ + Gets a local actor by its preferred username. + """ + @spec get_local_actor_by_name(String.t()) :: Actor.t() | nil + def get_local_actor_by_name(name) do + from(a in Actor) + |> filter_by_name([name]) + |> Repo.one() + end + + @doc """ + Gets a local actor by its preferred username and preloaded relations + (organized events, followers and followings). + """ + @spec get_local_actor_by_name_with_preload(String.t()) :: Actor.t() | nil + def get_local_actor_by_name_with_preload(name) do + name + |> get_local_actor_by_name() + |> Repo.preload([:organized_events, :followers, :followings]) + end + + @doc """ + Gets an actor by name and preloads the organized events. + """ + @spec get_actor_by_name_with_preload(String.t(), atom() | nil) :: Actor.t() | nil + def get_actor_by_name_with_preload(name, type \\ nil) do + name + |> get_actor_by_name(type) + |> Repo.preload(:organized_events) + end + + @doc """ + Gets a cached local actor by username. + #TODO: move to MobilizonWeb layer + """ + @spec get_cached_local_actor_by_name(String.t()) :: + {:commit, Actor.t()} | {:ignore, any()} + def get_cached_local_actor_by_name(name) do + Cachex.fetch(:activity_pub, "actor_" <> name, fn "actor_" <> name -> + case get_local_actor_by_name(name) do + nil -> {:ignore, nil} + %Actor{} = actor -> {:commit, actor} + end + end) + end + + @doc """ + Gets local actors by their username. + """ + @spec get_local_actor_by_username(String.t()) :: [Actor.t()] + def get_local_actor_by_username(username) do + username + |> actor_by_username_query() + |> filter_local() + |> Repo.all() + |> Repo.preload(:organized_events) + end + + @doc """ + Builds a page struct for actors by their name or displayed name. + """ + @spec build_actors_by_username_or_name_page( + String.t(), + [ActorType.t()], + integer | nil, + integer | nil + ) :: Page.t() + def build_actors_by_username_or_name_page(username, types, page \\ nil, limit \\ nil) do + username + |> actor_by_username_or_name_query() + |> filter_by_types(types) + |> Page.build_page(page, limit) + end + + @doc """ + Creates an actor. + """ + @spec create_actor(map) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()} def create_actor(attrs \\ %{}) do %Actor{} |> Actor.changeset(attrs) @@ -110,17 +211,9 @@ defmodule Mobilizon.Actors do end @doc """ - Updates a actor. - - ## Examples - - iex> update_actor(%Actor{preferred_username: "toto"}, %{preferred_username: "tata"}) - {:ok, %Mobilizon.Actors.Actor{preferred_username: "tata"}} - - iex> update_actor(%Actor{preferred_username: "toto"}, %{preferred_username: nil}) - {:error, %Ecto.Changeset{}} - + Updates an actor. """ + @spec update_actor(Actor.t(), map) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()} def update_actor(%Actor{} = actor, attrs) do actor |> Actor.update_changeset(attrs) @@ -128,43 +221,46 @@ defmodule Mobilizon.Actors do |> Repo.update() end - defp delete_files_if_media_changed(%Ecto.Changeset{changes: changes} = changeset) do - Enum.each([:avatar, :banner], fn key -> - if Map.has_key?(changes, key) do - with %Ecto.Changeset{changes: %{url: new_url}} <- changes[key], - %{url: old_url} = _old_key <- Map.from_struct(changeset.data) |> Map.get(key), - false <- new_url == old_url do - MobilizonWeb.Upload.remove(old_url) - end - end - end) + @doc """ + Upserts an actor. + Conflicts on actor's URL/AP ID, replaces keys, avatar and banner, name and summary. + """ + @spec upsert_actor(map, boolean) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()} + def upsert_actor(%{keys: keys, name: name, summary: summary} = data, preload \\ false) do + insert = + data + |> Actor.remote_actor_creation_changeset() + |> Repo.insert( + on_conflict: [set: [keys: keys, name: name, summary: summary]], + conflict_target: [:url] + ) - changeset + case insert do + {:ok, actor} -> + actor = if preload, do: Repo.preload(actor, [:followers]), else: actor + + {:ok, actor} + + error -> + Logger.debug(inspect(error)) + + {:error, error} + end end @doc """ - Deletes a Actor. - - ## Examples - - iex> delete_actor(%Actor{}) - {:ok, %Mobilizon.Actors.Actor{}} - - iex> delete_actor(nil) - {:error, %Ecto.Changeset{}} - + Deletes an actor. """ @spec delete_actor(Actor.t()) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()} def delete_actor(%Actor{domain: nil} = actor) do - case Multi.new() - |> Multi.delete(:actor, actor) - |> Multi.run(:remove_banner, fn _repo, %{actor: %Actor{}} = _picture -> - remove_banner(actor) - end) - |> Multi.run(:remove_avatar, fn _repo, %{actor: %Actor{}} = _picture -> - remove_avatar(actor) - end) - |> Repo.transaction() do + transaction = + Multi.new() + |> Multi.delete(:actor, actor) + |> Multi.run(:remove_banner, fn _, %{actor: %Actor{}} -> remove_banner(actor) end) + |> Multi.run(:remove_avatar, fn _, %{actor: %Actor{}} -> remove_avatar(actor) end) + |> Repo.transaction() + + case transaction do {:ok, %{actor: %Actor{} = actor}} -> {:ok, actor} @@ -173,78 +269,153 @@ defmodule Mobilizon.Actors do end end - def delete_actor(%Actor{} = actor) do - Repo.delete(actor) + def delete_actor(%Actor{} = actor), do: Repo.delete(actor) + + @doc """ + Returns the list of actors. + """ + @spec list_actors :: [Actor.t()] + def list_actors, do: Repo.all(Actor) + + @doc """ + Gets a group by its title. + """ + @spec get_group_by_title(String.t()) :: Actor.t() | nil + def get_group_by_title(title) do + group_query() + |> filter_by_name(String.split(title, "@")) + |> Repo.one() end @doc """ - Returns an `%Ecto.Changeset{}` for tracking actor changes. - - ## Examples - - iex> change_actor(%Actor{}) - %Ecto.Changeset{data: %Mobilizon.Actors.Actor{}} - + Gets a local group by its title. """ - @spec change_actor(Actor.t()) :: Ecto.Changeset.t() - def change_actor(%Actor{} = actor) do - Actor.changeset(actor, %{}) + @spec get_local_group_by_title(String.t()) :: Actor.t() | nil + def get_local_group_by_title(title) do + group_query() + |> filter_by_name([title]) + |> Repo.one() end + @spec actor_with_preload_query(integer | String.t()) :: Ecto.Query.t() + defp actor_with_preload_query(id) do + from( + a in Actor, + where: a.id == ^id, + preload: [:organized_events, :followers, :followings] + ) + end + + @spec actor_by_username_query(String.t()) :: Ecto.Query.t() + defp actor_by_username_query(username) do + from( + a in Actor, + where: + fragment( + "f_unaccent(?) <% f_unaccent(?) or f_unaccent(coalesce(?, '')) <% f_unaccent(?)", + a.preferred_username, + ^username, + a.name, + ^username + ), + order_by: + fragment( + "word_similarity(?, ?) + word_similarity(coalesce(?, ''), ?) desc", + a.preferred_username, + ^username, + a.name, + ^username + ) + ) + end + + @spec actor_by_username_or_name_query(String.t()) :: Ecto.Query.t() + defp actor_by_username_or_name_query(username) do + from( + a in Actor, + where: + fragment( + "f_unaccent(?) %> f_unaccent(?) or f_unaccent(coalesce(?, '')) %> f_unaccent(?)", + a.preferred_username, + ^username, + a.name, + ^username + ), + order_by: + fragment( + "word_similarity(?, ?) + word_similarity(coalesce(?, ''), ?) desc", + a.preferred_username, + ^username, + a.name, + ^username + ) + ) + end + + @spec group_query :: Ecto.Query.t() + defp group_query do + from(a in Actor, where: a.type == "Group") + end + + @spec filter_local(Ecto.Query.t()) :: Ecto.Query.t() + defp filter_local(query) do + from(a in query, where: is_nil(a.domain)) + end + + @spec filter_by_type(Ecto.Query.t(), ActorType.t()) :: Ecto.Query.t() + defp filter_by_type(query, type) when type in [:Person, :Group] do + from(a in query, where: a.type == ^type) + end + + defp filter_by_type(query, _type), do: query + + @spec filter_by_types(Ecto.Query.t(), [ActorType.t()]) :: Ecto.Query.t() + defp filter_by_types(query, types) do + from(a in query, where: a.type in ^types) + end + + @spec filter_by_name(Ecto.Query.t(), [String.t()]) :: Ecto.Query.t() + defp filter_by_name(query, [name]) do + from(a in query, where: a.preferred_username == ^name and is_nil(a.domain)) + end + + defp filter_by_name(query, [name, domain]) do + from(a in query, where: a.preferred_username == ^name and a.domain == ^domain) + end + + @spec preload_followers(Actor.t(), boolean) :: Actor.t() + defp preload_followers(actor, true), do: Repo.preload(actor, [:followers]) + defp preload_followers(actor, false), do: actor + + ##### TODO: continue refactoring from here ##### + @doc """ - List the groups + Returns the groups an actor is member of """ - @spec list_groups(number(), number()) :: list(Actor.t()) - def list_groups(page \\ nil, limit \\ nil) do + @spec get_groups_member_of(struct()) :: list() + def get_groups_member_of(%Actor{id: actor_id}) do Repo.all( from( a in Actor, - where: a.type == ^:Group, - where: a.visibility in [^:public, ^:unlisted] + join: m in Member, + on: a.id == m.parent_id, + where: m.actor_id == ^actor_id ) - |> Page.paginate(page, limit) ) end @doc """ - Get the default member role depending on the actor openness + Returns the members for a group actor """ - @spec get_default_member_role(Actor.t()) :: atom() - def get_default_member_role(%Actor{openness: :open}), do: :member - def get_default_member_role(%Actor{}), do: :not_approved - - @doc """ - Get a group by it's title - """ - @spec get_group_by_title(String.t()) :: Actor.t() | nil - def get_group_by_title(title) do - case String.split(title, "@") do - [title] -> - get_local_group_by_title(title) - - [title, domain] -> - Repo.one( - from(a in Actor, - where: a.preferred_username == ^title and a.type == "Group" and a.domain == ^domain - ) - ) - end - end - - @doc """ - Get a local group by it's title - """ - @spec get_local_group_by_title(String.t()) :: Actor.t() | nil - def get_local_group_by_title(title) do - title - |> do_get_local_group_by_title - |> Repo.one() - end - - @spec do_get_local_group_by_title(String.t()) :: Ecto.Query.t() - defp do_get_local_group_by_title(title) do - from(a in Actor, - where: a.preferred_username == ^title and a.type == "Group" and is_nil(a.domain) + @spec get_members_for_group(struct()) :: list() + def get_members_for_group(%Actor{id: actor_id}) do + Repo.all( + from( + a in Actor, + join: m in Member, + on: a.id == m.actor_id, + where: m.parent_id == ^actor_id + ) ) end @@ -274,323 +445,18 @@ defmodule Mobilizon.Actors do end @doc """ - Upsert an actor. - - Conflicts on actor's URL/AP ID. Replaces keys, avatar and banner, name and summary. + List the groups """ - @spec insert_or_update_actor(map(), boolean()) :: {:ok, Actor.t()} - def insert_or_update_actor(data, preload \\ false) do - cs = - data - |> Actor.remote_actor_creation() - - case Repo.insert( - cs, - on_conflict: [ - set: [ - keys: data.keys, - name: data.name, - summary: data.summary - ] - ], - conflict_target: [:url] - ) do - {:ok, actor} -> - actor = if preload, do: Repo.preload(actor, [:followers]), else: actor - {:ok, actor} - - err -> - Logger.debug(inspect(err)) - {:error, err} - end - end - - # def increase_event_count(%Actor{} = actor) do - # event_count = (actor.info["event_count"] || 0) + 1 - # new_info = Map.put(actor.info, "note_count", note_count) - # - # cs = info_changeset(actor, %{info: new_info}) - # - # update_and_set_cache(cs) - # end - - @doc """ - Get an actor by it's URL (ActivityPub ID). The `:preload` option allows preloading the Followers relation. - - Raises `Ecto.NoResultsError` if the Actor does not exist. - - ## Examples - iex> get_actor_by_url("https://mastodon.server.tld/users/user") - {:ok, %Mobilizon.Actors.Actor{preferred_username: "user"}} - - iex> get_actor_by_url("https://mastodon.server.tld/users/user", true) - {:ok, %Mobilizon.Actors.Actor{preferred_username: "user", followers: []}} - - iex> get_actor_by_url("non existent") - {:error, :actor_not_found} - - """ - @spec get_actor_by_url(String.t(), boolean()) :: {:ok, Actor.t()} | {:error, :actor_not_found} - def get_actor_by_url(url, preload \\ false) do - case Repo.get_by(Actor, url: url) do - nil -> - {:error, :actor_not_found} - - actor -> - actor = if preload, do: Repo.preload(actor, [:followers]), else: actor - {:ok, actor} - end - end - - @doc """ - Get an actor by it's URL (ActivityPub ID). The `:preload` option allows preloading the Followers relation. - - Raises `Ecto.NoResultsError` if the Actor does not exist. - - ## Examples - iex> get_actor_by_url!("https://mastodon.server.tld/users/user") - %Mobilizon.Actors.Actor{} - - iex> get_actor_by_url!("https://mastodon.server.tld/users/user", true) - {:ok, %Mobilizon.Actors.Actor{preferred_username: "user", followers: []}} - - iex> get_actor_by_url!("non existent") - ** (Ecto.NoResultsError) - - """ - @spec get_actor_by_url!(String.t(), boolean()) :: struct() - def get_actor_by_url!(url, preload \\ false) do - actor = Repo.get_by!(Actor, url: url) - if preload, do: Repo.preload(actor, [:followers]), else: actor - end - - @doc """ - Get an actor by name - - ## Examples - iex> get_actor_by_name("tcit") - %Mobilizon.Actors.Actor{preferred_username: "tcit", domain: nil} - - iex> get_actor_by_name("tcit@social.tcit.fr") - %Mobilizon.Actors.Actor{preferred_username: "tcit", domain: "social.tcit.fr"} - - iex> get_actor_by_name("tcit", :Group) - nil - - """ - @spec get_actor_by_name(String.t(), atom() | nil) :: Actor.t() - def get_actor_by_name(name, type \\ nil) do - # Base query - query = from(a in Actor) - - # If we have Person / Group information - query = - if type in [:Person, :Group] do - from(a in query, where: a.type == ^type) - else - query - end - - # If the name is a remote actor - query = - case String.split(name, "@") do - [name] -> do_get_actor_by_name(query, name) - [name, domain] -> do_get_actor_by_name(query, name, domain) - end - - Repo.one(query) - end - - # Get actor by username and domain is nil - @spec do_get_actor_by_name(Ecto.Queryable.t(), String.t()) :: Ecto.Queryable.t() - defp do_get_actor_by_name(query, name) do - from(a in query, where: a.preferred_username == ^name and is_nil(a.domain)) - end - - # Get actor by username and domain - @spec do_get_actor_by_name(Ecto.Queryable.t(), String.t(), String.t()) :: Ecto.Queryable.t() - defp do_get_actor_by_name(query, name, domain) do - from(a in query, where: a.preferred_username == ^name and a.domain == ^domain) - end - - @doc """ - Return a local actor by it's preferred username - """ - @spec get_local_actor_by_name(String.t()) :: Actor.t() | nil - def get_local_actor_by_name(name) do - Repo.one( - from(a in Actor, - where: a.preferred_username == ^name and is_nil(a.domain) - ) - ) - end - - @doc """ - Return a local actor by it's preferred username and preload associations - - Preloads organized_events, followers and followings - """ - @spec get_local_actor_by_name_with_everything(String.t()) :: Actor.t() | nil - def get_local_actor_by_name_with_everything(name) do - actor = Repo.one(from(a in Actor, where: a.preferred_username == ^name and is_nil(a.domain))) - Repo.preload(actor, [:organized_events, :followers, :followings]) - end - - @doc """ - Returns actor by name and preloads the organized events - - ## Examples - iex> get_actor_by_name_with_everything("tcit") - %Mobilizon.Actors.Actor{preferred_username: "tcit", domain: nil, organized_events: []} - - iex> get_actor_by_name_with_everything("tcit@social.tcit.fr") - %Mobilizon.Actors.Actor{preferred_username: "tcit", domain: "social.tcit.fr", organized_events: []} - - iex> get_actor_by_name_with_everything("tcit", :Group) - nil - - """ - @spec get_actor_by_name_with_everything(String.t(), atom() | nil) :: Actor.t() - def get_actor_by_name_with_everything(name, type \\ nil) do - name - |> get_actor_by_name(type) - |> Repo.preload(:organized_events) - end - - @doc """ - Returns a cached local actor by username - """ - @spec get_cached_local_actor_by_name(String.t()) :: - {:ok, Actor.t()} | {:commit, Actor.t()} | {:ignore, any()} - def get_cached_local_actor_by_name(name) do - Cachex.fetch(:activity_pub, "actor_" <> name, fn "actor_" <> name -> - case get_local_actor_by_name(name) do - nil -> {:ignore, nil} - %Actor{} = actor -> {:commit, actor} - end - end) - end - - @doc """ - Getting an actor from url, eventually creating it - """ - # TODO: Move this to Mobilizon.Service.ActivityPub - @spec get_or_fetch_by_url(String.t(), bool()) :: {:ok, Actor.t()} | {:error, String.t()} - def get_or_fetch_by_url(url, preload \\ false) do - case get_actor_by_url(url, preload) do - {:ok, %Actor{} = actor} -> - {:ok, actor} - - _ -> - case ActivityPub.make_actor_from_url(url, preload) do - {:ok, %Actor{} = actor} -> - {:ok, actor} - - _ -> - Logger.warn("Could not fetch by AP id") - {:error, "Could not fetch by AP id"} - end - end - end - - @doc """ - Getting an actor from url, eventually creating it - - Returns an error if fetch failed - """ - # TODO: Move this to Mobilizon.Service.ActivityPub - @spec get_or_fetch_by_url!(String.t(), bool()) :: Actor.t() - def get_or_fetch_by_url!(url, preload \\ false) do - case get_actor_by_url(url, preload) do - {:ok, actor} -> - actor - - _ -> - case ActivityPub.make_actor_from_url(url, preload) do - {:ok, actor} -> - actor - - _ -> - raise "Could not fetch by AP id" - end - end - end - - @doc """ - Find local users by their username - """ - # TODO: This doesn't seem to be used anyway - @spec find_local_by_username(String.t()) :: list(Actor.t()) - def find_local_by_username(username) do - actors = - Repo.all( - from( - a in Actor, - where: - fragment( - "f_unaccent(?) <% f_unaccent(?) or - f_unaccent(coalesce(?, '')) <% f_unaccent(?)", - a.preferred_username, - ^username, - a.name, - ^username - ), - where: is_nil(a.domain), - order_by: - fragment( - "word_similarity(?, ?) + word_similarity(coalesce(?, ''), ?) desc", - a.preferred_username, - ^username, - a.name, - ^username - ) - ) - ) - - Repo.preload(actors, :organized_events) - end - - @doc """ - Find actors by their name or displayed name - """ - @spec find_and_count_actors_by_username_or_name( - String.t(), - [ActorTypeEnum.t()], - integer() | nil, - integer() | nil - ) :: - %{total: integer(), elements: list(Actor.t())} - def find_and_count_actors_by_username_or_name(username, _types, page \\ nil, limit \\ nil) - - def find_and_count_actors_by_username_or_name(username, types, page, limit) do - query = + @spec list_groups(number(), number()) :: list(Actor.t()) + def list_groups(page \\ nil, limit \\ nil) do + Repo.all( from( a in Actor, - where: - fragment( - "f_unaccent(?) %> f_unaccent(?) or - f_unaccent(coalesce(?, '')) %> f_unaccent(?)", - a.preferred_username, - ^username, - a.name, - ^username - ), - where: a.type in ^types, - order_by: - fragment( - "word_similarity(?, ?) + word_similarity(coalesce(?, ''), ?) desc", - a.preferred_username, - ^username, - a.name, - ^username - ) + where: a.type == ^:Group, + where: a.visibility in [^:public, ^:unlisted] ) |> Page.paginate(page, limit) - - total = Task.async(fn -> Repo.aggregate(query, :count, :id) end) - elements = Task.async(fn -> Repo.all(query) end) - - %{total: Task.await(total), elements: Task.await(elements)} + ) end @doc """ @@ -603,25 +469,12 @@ defmodule Mobilizon.Actors do end end - @doc """ - Create a new RSA key - """ - @spec create_keys() :: String.t() - def create_keys() do - key = :public_key.generate_key({:rsa, 2048, 65_537}) - entry = :public_key.pem_entry_encode(:RSAPrivateKey, key) - [entry] |> :public_key.pem_encode() |> String.trim_trailing() - end - @doc """ Create a new person actor """ @spec new_person(map()) :: {:ok, Actor.t()} | any def new_person(args) do - key = :public_key.generate_key({:rsa, 2048, 65_537}) - entry = :public_key.pem_entry_encode(:RSAPrivateKey, key) - pem = [entry] |> :public_key.pem_encode() |> String.trim_trailing() - args = Map.put(args, :keys, pem) + args = Map.put(args, :keys, Crypto.generate_rsa_2048_private_key()) with {:ok, %Actor{} = person} <- %Actor{} @@ -637,15 +490,11 @@ defmodule Mobilizon.Actors do """ @spec register_bot_account(map()) :: Actor.t() def register_bot_account(%{name: name, summary: summary}) do - key = :public_key.generate_key({:rsa, 2048, 65_537}) - entry = :public_key.pem_entry_encode(:RSAPrivateKey, key) - pem = [entry] |> :public_key.pem_encode() |> String.trim_trailing() - actor = Mobilizon.Actors.Actor.registration_changeset(%Mobilizon.Actors.Actor{}, %{ preferred_username: name, domain: nil, - keys: pem, + keys: Crypto.generate_rsa_2048_private_key(), summary: summary, type: :Service }) @@ -665,12 +514,20 @@ defmodule Mobilizon.Actors do _ -> %{url: url, preferred_username: preferred_username} - |> Actor.relay_creation() + |> Actor.relay_creation_changeset() |> Repo.insert() end end - alias Mobilizon.Actors.Member + @doc """ + Gets a single member of an actor (for example a group) + """ + def get_member(actor_id, parent_id) do + case Repo.get_by(Member, actor_id: actor_id, parent_id: parent_id) do + nil -> {:error, :member_not_found} + member -> {:ok, member} + end + end @doc """ Gets a single member. @@ -744,16 +601,41 @@ defmodule Mobilizon.Actors do end @doc """ - Returns an `%Ecto.Changeset{}` for tracking member changes. - - ## Examples - - iex> change_member(%Member{}) - %Ecto.Changeset{data: %Mobilizon.Actors.Member{}} - + Returns the list of administrator members for a group. """ - def change_member(%Member{} = member) do - Member.changeset(member, %{}) + def list_administrator_members_for_group(id, page \\ nil, limit \\ nil) do + Repo.all( + from( + m in Member, + where: m.parent_id == ^id and (m.role == ^:creator or m.role == ^:administrator), + preload: [:actor] + ) + |> Page.paginate(page, limit) + ) + end + + @doc """ + Get all group ids where the actor_id is the last administrator + """ + def list_group_id_where_last_administrator(actor_id) do + in_query = + from( + m in Member, + where: m.actor_id == ^actor_id and (m.role == ^:creator or m.role == ^:administrator), + select: m.parent_id + ) + + Repo.all( + from( + m in Member, + where: m.role == ^:creator or m.role == ^:administrator, + join: m2 in subquery(in_query), + on: m.parent_id == m2.parent_id, + group_by: m.parent_id, + select: m.parent_id, + having: count(m.actor_id) == 1 + ) + ) end @doc """ @@ -888,8 +770,6 @@ defmodule Mobilizon.Actors do Bot.changeset(bot, %{}) end - alias Mobilizon.Actors.Follower - @doc """ Gets a single follower. @@ -932,6 +812,84 @@ defmodule Mobilizon.Actors do ) end + @doc """ + Get followers from an actor + + If actor A and C both follow actor B, actor B's followers are A and C + """ + @spec get_followers(struct(), number(), number()) :: map() + def get_followers(%Actor{id: actor_id} = _actor, page \\ nil, limit \\ nil) do + query = + from( + a in Actor, + join: f in Follower, + on: a.id == f.actor_id, + where: f.target_actor_id == ^actor_id + ) + + total = Task.async(fn -> Repo.aggregate(query, :count, :id) end) + elements = Task.async(fn -> Repo.all(Page.paginate(query, page, limit)) end) + + %{total: Task.await(total), elements: Task.await(elements)} + end + + @spec get_full_followers(struct()) :: list() + def get_full_followers(%Actor{} = actor) do + actor + |> get_full_followers_query() + |> Repo.all() + end + + @spec get_full_external_followers(struct()) :: list() + def get_full_external_followers(%Actor{} = actor) do + actor + |> get_full_followers_query() + |> where([a], not is_nil(a.domain)) + |> Repo.all() + end + + @doc """ + Get followings from an actor + + If actor A follows actor B and C, actor A's followings are B and B + """ + @spec get_followings(struct(), number(), number()) :: list() + def get_followings(%Actor{id: actor_id} = _actor, page \\ nil, limit \\ nil) do + query = + from( + a in Actor, + join: f in Follower, + on: a.id == f.target_actor_id, + where: f.actor_id == ^actor_id + ) + + total = Task.async(fn -> Repo.aggregate(query, :count, :id) end) + elements = Task.async(fn -> Repo.all(Page.paginate(query, page, limit)) end) + + %{total: Task.await(total), elements: Task.await(elements)} + end + + @spec get_full_followings(struct()) :: list() + def get_full_followings(%Actor{id: actor_id} = _actor) do + Repo.all( + from( + a in Actor, + join: f in Follower, + on: a.id == f.target_actor_id, + where: f.actor_id == ^actor_id + ) + ) + end + + defp get_full_followers_query(%Actor{id: actor_id} = _actor) do + from( + a in Actor, + join: f in Follower, + on: a.id == f.actor_id, + where: f.target_actor_id == ^actor_id + ) + end + @doc """ Creates a follower. @@ -1006,16 +964,68 @@ defmodule Mobilizon.Actors do end @doc """ - Returns an `%Ecto.Changeset{}` for tracking follower changes. - - ## Examples - - iex> change_follower(Follower{}) - %Ecto.Changeset{data: %Mobilizon.Actors.Follower{}} - + Make an actor follow another """ - def change_follower(%Follower{} = follower) do - Follower.changeset(follower, %{}) + @spec follow(struct(), struct(), boolean()) :: Follower.t() | {:error, String.t()} + def follow(%Actor{} = followed, %Actor{} = follower, url \\ nil, approved \\ true) do + with {:suspended, false} <- {:suspended, followed.suspended}, + # Check if followed has blocked follower + {:already_following, false} <- {:already_following, following?(follower, followed)} do + do_follow(follower, followed, approved, url) + else + {:already_following, %Follower{}} -> + {:error, :already_following, + "Could not follow actor: you are already following #{followed.preferred_username}"} + + {:suspended, _} -> + {:error, :suspended, + "Could not follow actor: #{followed.preferred_username} has been suspended"} + end + end + + @doc """ + Unfollow an actor (remove a `Mobilizon.Actors.Follower`) + """ + @spec unfollow(struct(), struct()) :: {:ok, Follower.t()} | {:error, Ecto.Changeset.t()} + def unfollow(%Actor{} = followed, %Actor{} = follower) do + case {:already_following, following?(follower, followed)} do + {:already_following, %Follower{} = follow} -> + delete_follower(follow) + + {:already_following, false} -> + {:error, "Could not unfollow actor: you are not following #{followed.preferred_username}"} + end + end + + @spec do_follow(struct(), struct(), boolean(), String.t()) :: + {:ok, Follower.t()} | {:error, Ecto.Changeset.t()} + defp do_follow(%Actor{} = follower, %Actor{} = followed, approved, url) do + Logger.info( + "Making #{follower.preferred_username} follow #{followed.preferred_username} (approved: #{ + approved + })" + ) + + create_follower(%{ + "actor_id" => follower.id, + "target_actor_id" => followed.id, + "approved" => approved, + "url" => url + }) + end + + @doc """ + Returns whether an actor is following another + """ + @spec following?(struct(), struct()) :: Follower.t() | false + def following?( + %Actor{} = follower_actor, + %Actor{} = followed_actor + ) do + case get_follower(followed_actor, follower_actor) do + nil -> false + %Follower{} = follow -> follow + end end defp remove_banner(%Actor{banner: nil} = actor), do: {:ok, actor} @@ -1041,4 +1051,19 @@ defmodule Mobilizon.Actors do {:ok, actor} end end + + @spec delete_files_if_media_changed(Ecto.Changeset.t()) :: Ecto.Changeset.t() + defp delete_files_if_media_changed(%Ecto.Changeset{changes: changes, data: data} = changeset) do + Enum.each([:avatar, :banner], fn key -> + if Map.has_key?(changes, key) do + with %Ecto.Changeset{changes: %{url: new_url}} <- changes[key], + %{url: old_url} <- data |> Map.from_struct() |> Map.get(key), + false <- new_url == old_url do + MobilizonWeb.Upload.remove(old_url) + end + end + end) + + changeset + end end diff --git a/lib/mobilizon/actors/bot.ex b/lib/mobilizon/actors/bot.ex index cde23e939..46d3c4b87 100644 --- a/lib/mobilizon/actors/bot.ex +++ b/lib/mobilizon/actors/bot.ex @@ -1,15 +1,30 @@ defmodule Mobilizon.Actors.Bot do @moduledoc """ - Represents a local bot + Represents a local bot. """ + use Ecto.Schema + import Ecto.Changeset + alias Mobilizon.Actors.Actor alias Mobilizon.Users.User + @type t :: %__MODULE__{ + source: String.t(), + type: String.t(), + actor: Actor.t(), + user: User.t() + } + + @required_attrs [:source] + @optional_attrs [:type, :actor_id, :user_id] + @attrs @required_attrs ++ @optional_attrs + schema "bots" do field(:source, :string) field(:type, :string, default: :ics) + belongs_to(:actor, Actor) belongs_to(:user, User) @@ -17,9 +32,10 @@ defmodule Mobilizon.Actors.Bot do end @doc false + @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() def changeset(bot, attrs) do bot - |> cast(attrs, [:source, :type, :actor_id, :user_id]) - |> validate_required([:source]) + |> cast(attrs, @attrs) + |> validate_required(@required_attrs) end end diff --git a/lib/mobilizon/actors/follower.ex b/lib/mobilizon/actors/follower.ex index 426706737..b8497e966 100644 --- a/lib/mobilizon/actors/follower.ex +++ b/lib/mobilizon/actors/follower.ex @@ -1,52 +1,63 @@ defmodule Mobilizon.Actors.Follower do @moduledoc """ - Represents the following of an actor to another actor + Represents the following of an actor to another actor. """ + use Ecto.Schema + import Ecto.Changeset - alias Mobilizon.Actors.Follower + alias Mobilizon.Actors.Actor - @primary_key {:id, :binary_id, autogenerate: true} + @type t :: %__MODULE__{ + approved: boolean, + url: String.t(), + target_actor: Actor.t(), + actor: Actor.t() + } + @required_attrs [:url, :approved, :target_actor_id, :actor_id] + @attrs @required_attrs + + @primary_key {:id, :binary_id, autogenerate: true} schema "followers" do field(:approved, :boolean, default: false) field(:url, :string) + belongs_to(:target_actor, Actor) belongs_to(:actor, Actor) end @doc false - def changeset(%Follower{} = member, attrs) do - member - |> cast(attrs, [:url, :approved, :target_actor_id, :actor_id]) - |> generate_url() - |> validate_required([:url, :approved, :target_actor_id, :actor_id]) - |> unique_constraint(:target_actor_id, name: :followers_actor_target_actor_unique_index) + @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() + def changeset(follower, attrs) do + follower + |> cast(attrs, @attrs) + |> ensure_url() + |> validate_required(@required_attrs) + |> unique_constraint(:target_actor_id, + name: :followers_actor_target_actor_unique_index + ) end # If there's a blank URL that's because we're doing the first insert - defp generate_url(%Ecto.Changeset{data: %Follower{url: nil}} = changeset) do + @spec ensure_url(Ecto.Changeset.t()) :: Ecto.Changeset.t() + defp ensure_url(%Ecto.Changeset{data: %__MODULE__{url: nil}} = changeset) do case fetch_change(changeset, :url) do {:ok, _url} -> changeset - :error -> do_generate_url(changeset) + :error -> generate_url(changeset) end end # Most time just go with the given URL - defp generate_url(%Ecto.Changeset{} = changeset), do: changeset + defp ensure_url(%Ecto.Changeset{} = changeset), do: changeset - defp do_generate_url(%Ecto.Changeset{} = changeset) do + @spec generate_url(Ecto.Changeset.t()) :: Ecto.Changeset.t() + defp generate_url(%Ecto.Changeset{} = changeset) do uuid = Ecto.UUID.generate() changeset - |> put_change( - :url, - "#{MobilizonWeb.Endpoint.url()}/follow/#{uuid}" - ) - |> put_change( - :id, - uuid - ) + |> put_change(:id, uuid) + |> put_change(:url, "#{MobilizonWeb.Endpoint.url()}/follow/#{uuid}") end end diff --git a/lib/mobilizon/actors/member.ex b/lib/mobilizon/actors/member.ex index 02d70f1bf..98951e6b3 100644 --- a/lib/mobilizon/actors/member.ex +++ b/lib/mobilizon/actors/member.ex @@ -1,100 +1,59 @@ -import EctoEnum - -defenum(Mobilizon.Actors.MemberRoleEnum, :member_role_type, [ - :not_approved, - :member, - :moderator, - :administrator, - :creator -]) - defmodule Mobilizon.Actors.Member do @moduledoc """ - Represents the membership of an actor to a group + Represents the membership of an actor to a group. """ use Ecto.Schema import Ecto.Changeset - import Ecto.Query - alias Mobilizon.Actors.{Actor, Member} - alias Mobilizon.Storage.{Page, Repo} + alias Mobilizon.Actors.{Actor, Member, MemberRole} + + @type t :: %__MODULE__{ + role: MemberRole.t(), + parent: Actor.t(), + actor: Actor.t() + } + + @required_attrs [:parent_id, :actor_id] + @optional_attrs [:role] + @attrs @required_attrs ++ @optional_attrs schema "members" do - field(:role, Mobilizon.Actors.MemberRoleEnum, default: :member) + field(:role, MemberRole, default: :member) + belongs_to(:parent, Actor) belongs_to(:actor, Actor) timestamps() end - @doc false - def changeset(%Member{} = member, attrs) do - member - |> cast(attrs, [:role, :parent_id, :actor_id]) - |> validate_required([:parent_id, :actor_id]) - |> unique_constraint(:parent_id, name: :members_actor_parent_unique_index) - end - @doc """ - Gets a single member of an actor (for example a group) + Gets the default member role depending on the actor openness. """ - def get_member(actor_id, parent_id) do - case Repo.get_by(Member, actor_id: actor_id, parent_id: parent_id) do - nil -> {:error, :member_not_found} - member -> {:ok, member} - end - end + @spec get_default_member_role(Actor.t()) :: atom + def get_default_member_role(%Actor{openness: :open}), do: :member + def get_default_member_role(%Actor{}), do: :not_approved @doc """ - Gets a single member of an actor (for example a group) + Checks whether the actor can be joined to the group. """ def can_be_joined(%Actor{type: :Group, openness: :invite_only}), do: false def can_be_joined(%Actor{type: :Group}), do: true @doc """ - Returns the list of administrator members for a group. - """ - def list_administrator_members_for_group(id, page \\ nil, limit \\ nil) do - Repo.all( - from( - m in Member, - where: m.parent_id == ^id and (m.role == ^:creator or m.role == ^:administrator), - preload: [:actor] - ) - |> Page.paginate(page, limit) - ) - end - - @doc """ - Get all group ids where the actor_id is the last administrator - """ - def list_group_id_where_last_administrator(actor_id) do - in_query = - from( - m in Member, - where: m.actor_id == ^actor_id and (m.role == ^:creator or m.role == ^:administrator), - select: m.parent_id - ) - - Repo.all( - from( - m in Member, - where: m.role == ^:creator or m.role == ^:administrator, - join: m2 in subquery(in_query), - on: m.parent_id == m2.parent_id, - group_by: m.parent_id, - select: m.parent_id, - having: count(m.actor_id) == 1 - ) - ) - end - - @doc """ - Returns true if the member is an administrator (admin or creator) of the group + Checks whether the member is an administrator (admin or creator) of the group. """ def is_administrator(%Member{role: :administrator}), do: {:is_admin, true} def is_administrator(%Member{role: :creator}), do: {:is_admin, true} def is_administrator(%Member{}), do: {:is_admin, false} + + @doc false + @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() + def changeset(member, attrs) do + member + |> cast(attrs, @attrs) + |> validate_required(@required_attrs) + |> unique_constraint(:parent_id, name: :members_actor_parent_unique_index) + end end diff --git a/lib/mobilizon/addresses/addresses.ex b/lib/mobilizon/addresses/addresses.ex index d1dc3b99c..7c8ac7b42 100644 --- a/lib/mobilizon/addresses/addresses.ex +++ b/lib/mobilizon/addresses/addresses.ex @@ -18,12 +18,6 @@ defmodule Mobilizon.Addresses do @spec query(Ecto.Query.t(), map) :: Ecto.Query.t() def query(queryable, _params), do: queryable - @doc """ - Returns the list of addresses. - """ - @spec list_addresses :: [Address.t()] - def list_addresses, do: Repo.all(Address) - @doc """ Gets a single address. """ @@ -72,6 +66,12 @@ defmodule Mobilizon.Addresses do @spec delete_address(Address.t()) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()} def delete_address(%Address{} = address), do: Repo.delete(address) + @doc """ + Returns the list of addresses. + """ + @spec list_addresses :: [Address.t()] + def list_addresses, do: Repo.all(Address) + @doc """ Searches addresses. diff --git a/lib/mobilizon/admin/admin.ex b/lib/mobilizon/admin/admin.ex index aa348cfa8..f660ab080 100644 --- a/lib/mobilizon/admin/admin.ex +++ b/lib/mobilizon/admin/admin.ex @@ -8,16 +8,6 @@ defmodule Mobilizon.Admin do alias Mobilizon.Admin.ActionLog alias Mobilizon.Storage.{Page, Repo} - @doc """ - Returns the list of action logs. - """ - @spec list_action_logs(integer | nil, integer | nil) :: [ActionLog.t()] - def list_action_logs(page \\ nil, limit \\ nil) do - list_action_logs_query() - |> Page.paginate(page, limit) - |> Repo.all() - end - @doc """ Creates a action_log. """ @@ -28,6 +18,16 @@ defmodule Mobilizon.Admin do |> Repo.insert() end + @doc """ + Returns the list of action logs. + """ + @spec list_action_logs(integer | nil, integer | nil) :: [ActionLog.t()] + def list_action_logs(page \\ nil, limit \\ nil) do + list_action_logs_query() + |> Page.paginate(page, limit) + |> Repo.all() + end + @spec list_action_logs_query :: Ecto.Query.t() defp list_action_logs_query do from(r in ActionLog, preload: [:actor]) diff --git a/lib/mobilizon/crypto.ex b/lib/mobilizon/crypto.ex index 9068c8787..05ced94db 100644 --- a/lib/mobilizon/crypto.ex +++ b/lib/mobilizon/crypto.ex @@ -12,4 +12,17 @@ defmodule Mobilizon.Crypto do |> :crypto.strong_rand_bytes() |> Base.url_encode64() end + + @doc """ + Generate RSA 2048-bit private key. + """ + @spec generate_rsa_2048_private_key :: String.t() + def generate_rsa_2048_private_key do + key = :public_key.generate_key({:rsa, 2048, 65_537}) + entry = :public_key.pem_entry_encode(:RSAPrivateKey, key) + + [entry] + |> :public_key.pem_encode() + |> String.trim_trailing() + end end diff --git a/lib/mobilizon/reports/reports.ex b/lib/mobilizon/reports/reports.ex index 055e2ee91..95c96f73f 100644 --- a/lib/mobilizon/reports/reports.ex +++ b/lib/mobilizon/reports/reports.ex @@ -21,17 +21,6 @@ defmodule Mobilizon.Reports do @spec query(Ecto.Query.t(), map) :: Ecto.Query.t() def query(queryable, _params), do: queryable - @doc """ - Returns the list of reports. - """ - @spec list_reports(integer | nil, integer | nil, atom, atom) :: [Report.t()] - def list_reports(page \\ nil, limit \\ nil, sort \\ :updated_at, direction \\ :asc) do - list_reports_query() - |> Page.paginate(page, limit) - |> sort(sort, direction) - |> Repo.all() - end - @doc """ Gets a single report. """ @@ -90,17 +79,16 @@ defmodule Mobilizon.Reports do Deletes a report. """ @spec delete_report(Report.t()) :: {:ok, Report.t()} | {:error, Ecto.Changeset.t()} - def delete_report(%Report{} = report) do - Repo.delete(report) - end + def delete_report(%Report{} = report), do: Repo.delete(report) @doc """ - Returns the list of notes for a report. + Returns the list of reports. """ - @spec list_notes_for_report(Report.t()) :: [Note.t()] - def list_notes_for_report(%Report{id: report_id}) do - report_id - |> list_notes_for_report_query() + @spec list_reports(integer | nil, integer | nil, atom, atom) :: [Report.t()] + def list_reports(page \\ nil, limit \\ nil, sort \\ :updated_at, direction \\ :asc) do + list_reports_query() + |> Page.paginate(page, limit) + |> sort(sort, direction) |> Repo.all() end @@ -134,8 +122,21 @@ defmodule Mobilizon.Reports do Deletes a note. """ @spec delete_note(Note.t()) :: {:ok, Note.t()} | {:error, Ecto.Changeset.t()} - def delete_note(%Note{} = note) do - Repo.delete(note) + def delete_note(%Note{} = note), do: Repo.delete(note) + + @doc """ + Returns the list of notes for a report. + """ + @spec list_notes_for_report(Report.t()) :: [Note.t()] + def list_notes_for_report(%Report{id: report_id}) do + report_id + |> list_notes_for_report_query() + |> Repo.all() + end + + @spec report_by_url_query(String.t()) :: Ecto.Query.t() + defp report_by_url_query(url) do + from(r in Report, where: r.uri == ^url) end @spec list_reports_query :: Ecto.Query.t() @@ -146,11 +147,6 @@ defmodule Mobilizon.Reports do ) end - @spec report_by_url_query(String.t()) :: Ecto.Query.t() - defp report_by_url_query(url) do - from(r in Report, where: r.uri == ^url) - end - @spec list_notes_for_report_query(integer | String.t()) :: Ecto.Query.t() defp list_notes_for_report_query(report_id) do from( diff --git a/lib/mobilizon/users/users.ex b/lib/mobilizon/users/users.ex index c03fe3588..4fe5e8068 100644 --- a/lib/mobilizon/users/users.ex +++ b/lib/mobilizon/users/users.ex @@ -101,9 +101,7 @@ defmodule Mobilizon.Users do Deletes an user. """ @spec delete_user(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} - def delete_user(%User{} = user) do - Repo.delete(user) - end + def delete_user(%User{} = user), do: Repo.delete(user) @doc """ Get an user with its actors @@ -219,9 +217,7 @@ defmodule Mobilizon.Users do Counts users. """ @spec count_users :: integer - def count_users do - Repo.one(from(u in User, select: count(u.id))) - end + def count_users, do: Repo.one(from(u in User, select: count(u.id))) @doc """ Authenticate an user. diff --git a/lib/mobilizon_web/api/follows.ex b/lib/mobilizon_web/api/follows.ex index 626c710b9..a96b5c609 100644 --- a/lib/mobilizon_web/api/follows.ex +++ b/lib/mobilizon_web/api/follows.ex @@ -32,7 +32,7 @@ defmodule MobilizonWeb.API.Follows do def accept(%Actor{} = follower, %Actor{} = followed) do with %Follower{approved: false, id: follow_id, url: follow_url} = follow <- - Actor.following?(follower, followed), + Actors.following?(follower, followed), activity_follow_url <- "#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follow_id}", data <- ActivityPub.Utils.make_follow_data(followed, follower, follow_url), diff --git a/lib/mobilizon_web/api/search.ex b/lib/mobilizon_web/api/search.ex index 0e1bfabe5..67e9a2726 100644 --- a/lib/mobilizon_web/api/search.ex +++ b/lib/mobilizon_web/api/search.ex @@ -1,20 +1,21 @@ defmodule MobilizonWeb.API.Search do @moduledoc """ - API for Search + API for search. """ - alias Mobilizon.Service.ActivityPub + alias Mobilizon.Actors - alias Mobilizon.Actors.Actor + alias Mobilizon.Actors.ActorType alias Mobilizon.Events - alias Mobilizon.Events.{Event, Comment} + alias Mobilizon.Service.ActivityPub + alias Mobilizon.Storage.Page require Logger @doc """ - Search actors + Searches actors. """ - @spec search_actors(String.t(), integer(), integer(), String.t()) :: - {:ok, %{total: integer(), elements: list(Actor.t())}} | {:error, any()} + @spec search_actors(String.t(), integer | nil, integer | nil, ActorType.t()) :: + {:ok, Page.t()} | {:error, String.t()} def search_actors(search, page \\ 1, limit \\ 10, result_type) do search = String.trim(search) @@ -22,31 +23,33 @@ defmodule MobilizonWeb.API.Search do search == "" -> {:error, "Search can't be empty"} - # Some URLs could be domain.tld/@username, so keep this condition above handle_search? function - url_search?(search) -> - # If this is not an actor, skip + # Some URLs could be domain.tld/@username, so keep this condition above + # the `is_handle` function + is_url(search) -> + # skip, if it's not an actor case process_from_url(search) do - %{:total => total, :elements => [%Actor{}] = elements} -> - {:ok, %{total: total, elements: elements}} + %Page{total: _total, elements: _elements} = page -> + {:ok, page} _ -> {:ok, %{total: 0, elements: []}} end - handle_search?(search) -> + is_handle(search) -> {:ok, process_from_username(search)} true -> - {:ok, - Actors.find_and_count_actors_by_username_or_name(search, [result_type], page, limit)} + page = Actors.build_actors_by_username_or_name_page(search, [result_type], page, limit) + + {:ok, page} end end @doc """ Search events """ - @spec search_events(String.t(), integer(), integer()) :: - {:ok, %{total: integer(), elements: list(Event.t())}} | {:error, any()} + @spec search_events(String.t(), integer | nil, integer | nil) :: + {:ok, Page.t()} | {:error, String.t()} def search_events(search, page \\ 1, limit \\ 10) do search = String.trim(search) @@ -54,11 +57,11 @@ defmodule MobilizonWeb.API.Search do search == "" -> {:error, "Search can't be empty"} - url_search?(search) -> - # If this is not an event, skip + is_url(search) -> + # skip, if it's w not an actor case process_from_url(search) do - {total = total, [%Event{} = elements]} -> - {:ok, %{total: total, elements: elements}} + %Page{total: _total, elements: _elements} = page -> + {:ok, page} _ -> {:ok, %{total: 0, elements: []}} @@ -70,43 +73,36 @@ defmodule MobilizonWeb.API.Search do end # If the search string is an username - @spec process_from_username(String.t()) :: %{total: integer(), elements: [Actor.t()]} + @spec process_from_username(String.t()) :: Page.t() defp process_from_username(search) do case ActivityPub.find_or_make_actor_from_nickname(search) do {:ok, actor} -> - %{total: 1, elements: [actor]} + %Page{total: 1, elements: [actor]} {:error, _err} -> Logger.debug(fn -> "Unable to find or make actor '#{search}'" end) - %{total: 0, elements: []} + + %Page{total: 0, elements: []} end end # If the search string is an URL - @spec process_from_url(String.t()) :: %{ - total: integer(), - elements: [Actor.t() | Event.t() | Comment.t()] - } + @spec process_from_url(String.t()) :: Page.t() defp process_from_url(search) do case ActivityPub.fetch_object_from_url(search) do {:ok, object} -> - %{total: 1, elements: [object]} + %Page{total: 1, elements: [object]} {:error, _err} -> Logger.debug(fn -> "Unable to find or make object from URL '#{search}'" end) - %{total: 0, elements: []} + + %Page{total: 0, elements: []} end end - # Is the search an URL search? - @spec url_search?(String.t()) :: boolean - defp url_search?(search) do - String.starts_with?(search, "https://") or String.starts_with?(search, "http://") - end + @spec is_url(String.t()) :: boolean + defp is_url(search), do: String.starts_with?(search, ["http://", "https://"]) - # Is the search an handle search? - @spec handle_search?(String.t()) :: boolean - defp handle_search?(search) do - String.match?(search, ~r/@/) - end + @spec is_handle(String.t()) :: boolean + defp is_handle(search), do: String.match?(search, ~r/@/) end diff --git a/lib/mobilizon_web/controllers/activity_pub_controller.ex b/lib/mobilizon_web/controllers/activity_pub_controller.ex index 1bdbbf49a..6fa703f59 100644 --- a/lib/mobilizon_web/controllers/activity_pub_controller.ex +++ b/lib/mobilizon_web/controllers/activity_pub_controller.ex @@ -31,7 +31,7 @@ defmodule MobilizonWeb.ActivityPubController do def following(conn, %{"name" => name, "page" => page}) do with {page, ""} <- Integer.parse(page), - %Actor{} = actor <- Actors.get_local_actor_by_name_with_everything(name) do + %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do conn |> put_resp_header("content-type", "application/activity+json") |> json(ActorView.render("following.json", %{actor: actor, page: page})) @@ -39,7 +39,7 @@ defmodule MobilizonWeb.ActivityPubController do end def following(conn, %{"name" => name}) do - with %Actor{} = actor <- Actors.get_local_actor_by_name_with_everything(name) do + with %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do conn |> put_resp_header("content-type", "application/activity+json") |> json(ActorView.render("following.json", %{actor: actor})) @@ -48,7 +48,7 @@ defmodule MobilizonWeb.ActivityPubController do def followers(conn, %{"name" => name, "page" => page}) do with {page, ""} <- Integer.parse(page), - %Actor{} = actor <- Actors.get_local_actor_by_name_with_everything(name) do + %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do conn |> put_resp_header("content-type", "application/activity+json") |> json(ActorView.render("followers.json", %{actor: actor, page: page})) @@ -56,7 +56,7 @@ defmodule MobilizonWeb.ActivityPubController do end def followers(conn, %{"name" => name}) do - with %Actor{} = actor <- Actors.get_local_actor_by_name_with_everything(name) do + with %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do conn |> put_resp_header("content-type", "application/activity+json") |> json(ActorView.render("followers.json", %{actor: actor})) diff --git a/lib/mobilizon_web/resolvers/group.ex b/lib/mobilizon_web/resolvers/group.ex index a8e378a2f..5aa35e861 100644 --- a/lib/mobilizon_web/resolvers/group.ex +++ b/lib/mobilizon_web/resolvers/group.ex @@ -76,7 +76,7 @@ defmodule MobilizonWeb.Resolvers.Group do ) do with {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), {:is_owned, %Actor{}} <- User.owns_actor(user, actor_id), - {:ok, %Member{} = member} <- Member.get_member(actor_id, group.id), + {:ok, %Member{} = member} <- Actors.get_member(actor_id, group.id), {:is_admin, true} <- Member.is_administrator(member), group <- Actors.delete_group!(group) do {:ok, %{id: group.id}} @@ -109,9 +109,9 @@ defmodule MobilizonWeb.Resolvers.Group do ) do with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id), {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), - {:error, :member_not_found} <- Member.get_member(actor.id, group.id), + {:error, :member_not_found} <- Actors.get_member(actor.id, group.id), {:is_able_to_join, true} <- {:is_able_to_join, Member.can_be_joined(group)}, - role <- Mobilizon.Actors.get_default_member_role(group), + role <- Member.get_default_member_role(group), {:ok, _} <- Actors.create_member(%{parent_id: group.id, actor_id: actor.id, role: role}) do { :ok, @@ -149,7 +149,7 @@ defmodule MobilizonWeb.Resolvers.Group do %{context: %{current_user: user}} ) do with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id), - {:ok, %Member{} = member} <- Member.get_member(actor.id, group_id), + {:ok, %Member{} = member} <- Actors.get_member(actor.id, group_id), {:only_administrator, false} <- {:only_administrator, check_that_member_is_not_last_administrator(group_id, actor_id)}, {:ok, _} <- @@ -176,7 +176,7 @@ defmodule MobilizonWeb.Resolvers.Group do # and that it's the actor requesting leaving the group we return true @spec check_that_member_is_not_last_administrator(integer(), integer()) :: boolean() defp check_that_member_is_not_last_administrator(group_id, actor_id) do - case Member.list_administrator_members_for_group(group_id) do + case Actors.list_administrator_members_for_group(group_id) do [%Member{actor: %Actor{id: member_actor_id}}] -> actor_id == member_actor_id diff --git a/lib/mobilizon_web/resolvers/person.ex b/lib/mobilizon_web/resolvers/person.ex index 347b32131..8f751b7e9 100644 --- a/lib/mobilizon_web/resolvers/person.ex +++ b/lib/mobilizon_web/resolvers/person.ex @@ -2,12 +2,13 @@ defmodule MobilizonWeb.Resolvers.Person do @moduledoc """ Handles the person-related GraphQL calls """ + alias Mobilizon.Actors - alias Mobilizon.Actors.{Actor, Member} - alias Mobilizon.Users.User - alias Mobilizon.Users + alias Mobilizon.Actors.Actor alias Mobilizon.Events alias Mobilizon.Service.ActivityPub + alias Mobilizon.Users + alias Mobilizon.Users.User @doc """ Find a person @@ -206,7 +207,7 @@ defmodule MobilizonWeb.Resolvers.Person do # We check that the actor is not the last administrator/creator of a group @spec last_admin_of_a_group?(integer()) :: boolean() defp last_admin_of_a_group?(actor_id) do - length(Member.list_group_id_where_last_administrator(actor_id)) > 0 + length(Actors.list_group_id_where_last_administrator(actor_id)) > 0 end @spec proxify_avatar(Actor.t()) :: Actor.t() diff --git a/lib/mobilizon_web/resolvers/report.ex b/lib/mobilizon_web/resolvers/report.ex index 85796e158..472b60c6c 100644 --- a/lib/mobilizon_web/resolvers/report.ex +++ b/lib/mobilizon_web/resolvers/report.ex @@ -91,7 +91,7 @@ defmodule MobilizonWeb.Resolvers.Report do when is_moderator(role) do with {:is_owned, %Actor{}} <- User.owns_actor(user, moderator_id), %Report{} = report <- Reports.get_report(report_id), - %Actor{} = moderator <- Actors.get_local_actor_with_everything(moderator_id), + %Actor{} = moderator <- Actors.get_local_actor_with_preload(moderator_id), {:ok, %Note{} = note} <- MobilizonWeb.API.Reports.create_report_note(report, moderator, content) do {:ok, note} @@ -106,7 +106,7 @@ defmodule MobilizonWeb.Resolvers.Report do when is_moderator(role) do with {:is_owned, %Actor{}} <- User.owns_actor(user, moderator_id), %Note{} = note <- Reports.get_note(note_id), - %Actor{} = moderator <- Actors.get_local_actor_with_everything(moderator_id), + %Actor{} = moderator <- Actors.get_local_actor_with_preload(moderator_id), {:ok, %Note{} = note} <- MobilizonWeb.API.Reports.delete_report_note(note, moderator) do {:ok, %{id: note.id}} diff --git a/lib/mobilizon_web/views/activity_pub/actor_view.ex b/lib/mobilizon_web/views/activity_pub/actor_view.ex index 2d57cc278..00dbd0416 100644 --- a/lib/mobilizon_web/views/activity_pub/actor_view.ex +++ b/lib/mobilizon_web/views/activity_pub/actor_view.ex @@ -1,6 +1,7 @@ defmodule MobilizonWeb.ActivityPub.ActorView do use MobilizonWeb, :view + alias Mobilizon.Actors alias Mobilizon.Actors.Actor alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub.Utils @@ -47,8 +48,8 @@ defmodule MobilizonWeb.ActivityPub.ActorView do def render("following.json", %{actor: actor, page: page}) do %{total: total, elements: following} = - if Actor.public_visibility?(actor), - do: Actor.get_followings(actor, page), + if Actor.is_public_visibility(actor), + do: Actors.get_followings(actor, page), else: @private_visibility_empty_collection following @@ -58,8 +59,8 @@ defmodule MobilizonWeb.ActivityPub.ActorView do def render("following.json", %{actor: actor}) do %{total: total, elements: following} = - if Actor.public_visibility?(actor), - do: Actor.get_followings(actor), + if Actor.is_public_visibility(actor), + do: Actors.get_followings(actor), else: @private_visibility_empty_collection %{ @@ -73,8 +74,8 @@ defmodule MobilizonWeb.ActivityPub.ActorView do def render("followers.json", %{actor: actor, page: page}) do %{total: total, elements: followers} = - if Actor.public_visibility?(actor), - do: Actor.get_followers(actor, page), + if Actor.is_public_visibility(actor), + do: Actors.get_followers(actor, page), else: @private_visibility_empty_collection followers @@ -84,8 +85,8 @@ defmodule MobilizonWeb.ActivityPub.ActorView do def render("followers.json", %{actor: actor}) do %{total: total, elements: followers} = - if Actor.public_visibility?(actor), - do: Actor.get_followers(actor), + if Actor.is_public_visibility(actor), + do: Actors.get_followers(actor), else: @private_visibility_empty_collection %{ @@ -99,7 +100,7 @@ defmodule MobilizonWeb.ActivityPub.ActorView do def render("outbox.json", %{actor: actor, page: page}) do %{total: total, elements: followers} = - if Actor.public_visibility?(actor), + if Actor.is_public_visibility(actor), do: ActivityPub.fetch_public_activities_for_actor(actor, page), else: @private_visibility_empty_collection @@ -110,7 +111,7 @@ defmodule MobilizonWeb.ActivityPub.ActorView do def render("outbox.json", %{actor: actor}) do %{total: total, elements: followers} = - if Actor.public_visibility?(actor), + if Actor.is_public_visibility(actor), do: ActivityPub.fetch_public_activities_for_actor(actor), else: @private_visibility_empty_collection diff --git a/lib/service/activity_pub/activity_pub.ex b/lib/service/activity_pub/activity_pub.ex index c0d98a0ae..cccdae211 100644 --- a/lib/service/activity_pub/activity_pub.ex +++ b/lib/service/activity_pub/activity_pub.ex @@ -113,6 +113,29 @@ defmodule Mobilizon.Service.ActivityPub do end end + + @doc """ + Getting an actor from url, eventually creating it + """ + @spec get_or_fetch_by_url(String.t(), boolean) :: {:ok, Actor.t()} | {:error, String.t()} + def get_or_fetch_by_url(url, preload \\ false) do + case Actors.get_actor_by_url(url, preload) do + {:ok, %Actor{} = actor} -> + {:ok, actor} + + _ -> + case make_actor_from_url(url, preload) do + {:ok, %Actor{} = actor} -> + {:ok, actor} + + _ -> + Logger.warn("Could not fetch by AP id") + + {:error, "Could not fetch by AP id"} + end + end + end + @doc """ Create an activity of type "Create" """ @@ -279,7 +302,7 @@ defmodule Mobilizon.Service.ActivityPub do """ def follow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do with {:ok, %Follower{url: follow_url}} <- - Actor.follow(followed, follower, activity_id, false), + Actors.follow(followed, follower, activity_id, false), activity_follow_id <- activity_id || follow_url, data <- make_follow_data(followed, follower, activity_follow_id), @@ -298,7 +321,7 @@ defmodule Mobilizon.Service.ActivityPub do """ @spec unfollow(Actor.t(), Actor.t(), String.t(), boolean()) :: {:ok, map()} | any() def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do - with {:ok, %Follower{id: follow_id}} <- Actor.unfollow(followed, follower), + with {:ok, %Follower{id: follow_id}} <- Actors.unfollow(followed, follower), # We recreate the follow activity data <- make_follow_data( @@ -466,7 +489,7 @@ defmodule Mobilizon.Service.ActivityPub do def make_actor_from_url(url, preload \\ false) do case fetch_and_prepare_actor_from_url(url) do {:ok, data} -> - Actors.insert_or_update_actor(data, preload) + Actors.upsert_actor(data, preload) # Request returned 410 {:error, :actor_deleted} -> @@ -529,7 +552,7 @@ defmodule Mobilizon.Service.ActivityPub do followers = if actor.followers_url in activity.recipients do - Actor.get_full_external_followers(actor) + Actors.get_full_external_followers(actor) else [] end diff --git a/lib/service/activity_pub/converters/comment.ex b/lib/service/activity_pub/converters/comment.ex index d412ad457..920d699e8 100644 --- a/lib/service/activity_pub/converters/comment.ex +++ b/lib/service/activity_pub/converters/comment.ex @@ -4,7 +4,6 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Comment do This module allows to convert events from ActivityStream format to our own internal one, and back """ - alias Mobilizon.Actors alias Mobilizon.Actors.Actor alias Mobilizon.Events.Comment, as: CommentModel alias Mobilizon.Events.Event @@ -20,7 +19,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Comment do @impl Converter @spec as_to_model_data(map()) :: map() def as_to_model_data(object) do - {:ok, %Actor{id: actor_id}} = Actors.get_or_fetch_by_url(object["actor"]) + {:ok, %Actor{id: actor_id}} = ActivityPub.get_or_fetch_by_url(object["actor"]) Logger.debug("Inserting full comment") Logger.debug(inspect(object)) diff --git a/lib/service/activity_pub/relay.ex b/lib/service/activity_pub/relay.ex index 933644c68..96e2bedb6 100644 --- a/lib/service/activity_pub/relay.ex +++ b/lib/service/activity_pub/relay.ex @@ -24,7 +24,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do def follow(target_instance) do with %Actor{} = local_actor <- get_actor(), - {:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance), + {:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_by_url(target_instance), {:ok, activity} <- Follows.follow(local_actor, target_actor) do Logger.info("Relay: followed instance #{target_instance}; id=#{activity.data["id"]}") {:ok, activity} @@ -37,7 +37,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do def unfollow(target_instance) do with %Actor{} = local_actor <- get_actor(), - {:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance), + {:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_by_url(target_instance), {:ok, activity} <- Follows.unfollow(local_actor, target_actor) do Logger.info("Relay: unfollowed instance #{target_instance}: id=#{activity.data["id"]}") {:ok, activity} @@ -50,7 +50,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do def accept(target_instance) do with %Actor{} = local_actor <- get_actor(), - {:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance), + {:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_by_url(target_instance), {:ok, activity} <- Follows.accept(target_actor, local_actor) do {:ok, activity} end @@ -58,7 +58,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do # def reject(target_instance) do # with %Actor{} = local_actor <- get_actor(), - # {:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance), + # {:ok, %Actor{} = target_actor} <- Activity.get_or_fetch_by_url(target_instance), # {:ok, activity} <- Follows.reject(target_actor, local_actor) do # {:ok, activity} # end diff --git a/lib/service/activity_pub/transmogrifier.ex b/lib/service/activity_pub/transmogrifier.ex index 2d5de4370..a94548aa5 100644 --- a/lib/service/activity_pub/transmogrifier.ex +++ b/lib/service/activity_pub/transmogrifier.ex @@ -139,7 +139,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do Logger.info("Handle incoming to create notes") - with {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(data["actor"]) do + with {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(data["actor"]) do Logger.debug("found actor") Logger.debug(inspect(actor)) @@ -163,7 +163,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Event"} = object} = data) do Logger.info("Handle incoming to create event") - with {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(data["actor"]) do + with {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(data["actor"]) do Logger.debug("found actor") Logger.debug(inspect(actor)) @@ -187,8 +187,8 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do def handle_incoming( %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = _data ) do - with {:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed, true), - {:ok, %Actor{} = follower} <- Actors.get_or_fetch_by_url(follower), + with {:ok, %Actor{} = followed} <- ActivityPub.get_or_fetch_by_url(followed, true), + {:ok, %Actor{} = follower} <- ActivityPub.get_or_fetch_by_url(follower), {:ok, activity, object} <- ActivityPub.follow(follower, followed, id, false) do {:ok, activity, object} else @@ -207,7 +207,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do } = data ) do with actor_url <- get_actor(data), - {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor_url), + {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(actor_url), {:object_not_found, {:ok, activity, object}} <- {:object_not_found, do_handle_incoming_accept_following(accepted_object, actor) || @@ -236,7 +236,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do %{"type" => "Reject", "object" => rejected_object, "actor" => _actor, "id" => id} = data ) do with actor_url <- get_actor(data), - {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor_url), + {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(actor_url), {:object_not_found, {:ok, activity, object}} <- {:object_not_found, do_handle_incoming_reject_following(rejected_object, actor) || @@ -279,7 +279,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data ) do with actor <- get_actor(data), - {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor), + {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(actor), {:ok, object} <- fetch_obj_helper_as_activity_streams(object_id), public <- Visibility.is_public?(data), {:ok, activity, object} <- ActivityPub.announce(actor, object, id, false, public) do @@ -347,7 +347,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do } = data ) do with actor <- get_actor(data), - {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor), + {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(actor), {:ok, object} <- fetch_obj_helper_as_activity_streams(object_id), {:ok, activity, object} <- ActivityPub.unannounce(actor, object, id, cancelled_activity_id, false) do @@ -451,7 +451,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do # } = data # ) do # with actor <- get_actor(data), - # %Actor{} = actor <- Actors.get_or_fetch_by_url(actor), + # %Actor{} = actor <- ActivityPub.get_or_fetch_by_url(actor), # {:ok, object} <- fetch_obj_helper(object_id) || fetch_obj_helper(object_id), # {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do # {:ok, activity} diff --git a/lib/service/export/feed.ex b/lib/service/export/feed.ex index e58f05e3e..39fe709d2 100644 --- a/lib/service/export/feed.ex +++ b/lib/service/export/feed.ex @@ -44,7 +44,7 @@ defmodule Mobilizon.Service.Export.Feed do @spec fetch_actor_event_feed(String.t()) :: String.t() defp fetch_actor_event_feed(name) do with %Actor{} = actor <- Actors.get_local_actor_by_name(name), - {:visibility, true} <- {:visibility, Actor.public_visibility?(actor)}, + {:visibility, true} <- {:visibility, Actor.is_public_visibility(actor)}, {:ok, events, _count} <- Events.get_public_events_for_actor(actor) do {:ok, build_actor_feed(actor, events)} else diff --git a/lib/service/export/icalendar.ex b/lib/service/export/icalendar.ex index 866685b33..6a67a3072 100644 --- a/lib/service/export/icalendar.ex +++ b/lib/service/export/icalendar.ex @@ -44,7 +44,7 @@ defmodule Mobilizon.Service.Export.ICalendar do """ @spec export_public_actor(Actor.t()) :: String.t() def export_public_actor(%Actor{} = actor) do - with true <- Actor.public_visibility?(actor), + with true <- Actor.is_public_visibility(actor), {:ok, events, _} <- Events.get_public_events_for_actor(actor) do {:ok, %ICalendar{events: events |> Enum.map(&do_export_event/1)} |> ICalendar.to_ics()} end diff --git a/lib/service/http_signatures/signature.ex b/lib/service/http_signatures/signature.ex index 4fc6dcfb3..0012ea02c 100644 --- a/lib/service/http_signatures/signature.ex +++ b/lib/service/http_signatures/signature.ex @@ -7,20 +7,23 @@ defmodule Mobilizon.Service.HTTPSignatures.Signature do @moduledoc """ Adapter for the `HTTPSignatures` lib that handles signing and providing public keys to verify HTTPSignatures """ + @behaviour HTTPSignatures.Adapter alias Mobilizon.Actors.Actor alias Mobilizon.Service.ActivityPub + require Logger def key_id_to_actor_url(key_id) do - uri = - URI.parse(key_id) + %{path: path} = uri = + key_id + |> URI.parse() |> Map.put(:fragment, nil) uri = - if not is_nil(uri.path) and String.ends_with?(uri.path, "/publickey") do - Map.put(uri, :path, String.replace(uri.path, "/publickey", "")) + if not is_nil(path) do + Map.put(uri, :path, String.trim_trailing(path, "/publickey")) else uri end @@ -28,11 +31,47 @@ defmodule Mobilizon.Service.HTTPSignatures.Signature do URI.to_string(uri) end + @doc """ + Convert internal PEM encoded keys to public key format. + """ + @spec prepare_public_key(String.t()) :: {:ok, tuple} | {:error, :pem_decode_error} + def prepare_public_key(public_key_code) do + case :public_key.pem_decode(public_key_code) do + [public_key_entry] -> + {:ok, :public_key.pem_entry_decode(public_key_entry)} + + _ -> + {:error, :pem_decode_error} + end + end + + @doc """ + Gets a public key for a given ActivityPub actor ID (url). + """ + @spec get_public_key_for_url(String.t()) :: + {:ok, String.t()} | {:error, :actor_fetch_error | :pem_decode_error} + def get_public_key_for_url(url) do + with {:ok, %Actor{keys: keys}} <- ActivityPub.get_or_fetch_by_url(url), + {:ok, public_key} <- prepare_public_key(keys) do + {:ok, public_key} + else + {:error, :pem_decode_error} -> + Logger.error("Error while decoding PEM") + + {:error, :pem_decode_error} + + _ -> + Logger.error("Unable to fetch actor, so no keys for you") + + {:error, :actor_fetch_error} + end + end + def fetch_public_key(conn) do with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), actor_id <- key_id_to_actor_url(kid), :ok <- Logger.debug("Fetching public key for #{actor_id}"), - {:ok, public_key} <- Actor.get_public_key_for_url(actor_id) do + {:ok, public_key} <- get_public_key_for_url(actor_id) do {:ok, public_key} else e -> @@ -45,7 +84,7 @@ defmodule Mobilizon.Service.HTTPSignatures.Signature do actor_id <- key_id_to_actor_url(kid), :ok <- Logger.debug("Refetching public key for #{actor_id}"), {:ok, _actor} <- ActivityPub.make_actor_from_url(actor_id), - {:ok, public_key} <- Actor.get_public_key_for_url(actor_id) do + {:ok, public_key} <- get_public_key_for_url(actor_id) do {:ok, public_key} else e -> @@ -53,12 +92,12 @@ defmodule Mobilizon.Service.HTTPSignatures.Signature do end end - def sign(%Actor{} = actor, headers) do + def sign(%Actor{keys: keys} = actor, headers) do Logger.debug("Signing on behalf of #{actor.url}") Logger.debug("headers") Logger.debug(inspect(headers)) - with {:ok, key} <- actor.keys |> Actor.prepare_public_key() do + with {:ok, key} <- prepare_public_key(keys) do HTTPSignatures.sign(key, actor.url <> "#main-key", headers) end end diff --git a/mix.exs b/mix.exs index 311f0b954..1435bc9d9 100644 --- a/mix.exs +++ b/mix.exs @@ -184,9 +184,9 @@ defmodule Mobilizon.Mixfile do Models: [ Mobilizon.Actors, Mobilizon.Actors.Actor, - Mobilizon.Actors.ActorOpennessEnum, - Mobilizon.Actors.ActorTypeEnum, - Mobilizon.Actors.MemberRoleEnum, + Mobilizon.Actors.ActorOpenness, + Mobilizon.Actors.ActorType, + Mobilizon.Actors.MemberRole, Mobilizon.Actors.Bot, Mobilizon.Actors.Follower, Mobilizon.Actors.Member, diff --git a/priv/repo/migrations/20180517100700_move_from_account_to_actor.exs b/priv/repo/migrations/20180517100700_move_from_account_to_actor.exs index 3ba343e97..8c6b678c7 100644 --- a/priv/repo/migrations/20180517100700_move_from_account_to_actor.exs +++ b/priv/repo/migrations/20180517100700_move_from_account_to_actor.exs @@ -18,7 +18,7 @@ defmodule Mobilizon.Repo.Migrations.MoveFromAccountToActor do drop(table("groups")) rename(table("accounts"), to: table("actors")) - Mobilizon.Actors.ActorTypeEnum.create_type() + Mobilizon.Actors.ActorType.create_type() rename(table("actors"), :username, to: :name) rename(table("actors"), :description, to: :summary) rename(table("actors"), :display_name, to: :preferred_username) @@ -86,7 +86,7 @@ defmodule Mobilizon.Repo.Migrations.MoveFromAccountToActor do modify(:display_name, :string, null: true) end - Mobilizon.Actors.ActorTypeEnum.drop_type() + Mobilizon.Actors.ActorType.drop_type() rename(table("events"), :organizer_actor_id, to: :organizer_account_id) diff --git a/priv/repo/migrations/20190301141830_move_member_role_to_enum.exs b/priv/repo/migrations/20190301141830_move_member_role_to_enum.exs index b0e8dfea1..30a1ca745 100644 --- a/priv/repo/migrations/20190301141830_move_member_role_to_enum.exs +++ b/priv/repo/migrations/20190301141830_move_member_role_to_enum.exs @@ -1,12 +1,12 @@ defmodule Mobilizon.Repo.Migrations.MoveMemberRoleToEnum do use Ecto.Migration - alias Mobilizon.Actors.MemberRoleEnum + alias Mobilizon.Actors.MemberRole def up do - MemberRoleEnum.create_type() + MemberRole.create_type() alter table(:members) do - add(:role_tmp, MemberRoleEnum.type(), default: "member") + add(:role_tmp, MemberRole.type(), default: "member") end execute("UPDATE members set role_tmp = 'member' where role = 0") @@ -39,7 +39,7 @@ defmodule Mobilizon.Repo.Migrations.MoveMemberRoleToEnum do remove(:role) end - MemberRoleEnum.drop_type() + MemberRole.drop_type() rename(table(:members), :role_tmp, to: :role) end diff --git a/priv/repo/migrations/20190301143831_actor_group_openness.exs b/priv/repo/migrations/20190301143831_actor_group_openness.exs index 34c551734..75542a26b 100644 --- a/priv/repo/migrations/20190301143831_actor_group_openness.exs +++ b/priv/repo/migrations/20190301143831_actor_group_openness.exs @@ -1,12 +1,12 @@ defmodule Mobilizon.Repo.Migrations.ActorGroupOpenness do use Ecto.Migration - alias Mobilizon.Actors.ActorOpennessEnum + alias Mobilizon.Actors.ActorOpenness def up do - ActorOpennessEnum.create_type() + ActorOpenness.create_type() alter table(:actors) do - add(:openness, ActorOpennessEnum.type(), default: "moderated") + add(:openness, ActorOpenness.type(), default: "moderated") end end diff --git a/priv/repo/migrations/20190425075451_add_visibility_to_actor.exs b/priv/repo/migrations/20190425075451_add_visibility_to_actor.exs index d100b8191..381d9b8e8 100644 --- a/priv/repo/migrations/20190425075451_add_visibility_to_actor.exs +++ b/priv/repo/migrations/20190425075451_add_visibility_to_actor.exs @@ -1,13 +1,13 @@ defmodule Mobilizon.Repo.Migrations.AddVisibilityToActor do use Ecto.Migration - alias Mobilizon.Actors.ActorVisibilityEnum + alias Mobilizon.Actors.ActorVisibility def up do - ActorVisibilityEnum.create_type() + ActorVisibility.create_type() alter table(:actors) do - add(:visibility, ActorVisibilityEnum.type(), default: "private") + add(:visibility, ActorVisibility.type(), default: "private") end end @@ -16,6 +16,6 @@ defmodule Mobilizon.Repo.Migrations.AddVisibilityToActor do remove(:visibility) end - ActorVisibilityEnum.drop_type() + ActorVisibility.drop_type() end end diff --git a/test/mobilizon/actors/actors_test.exs b/test/mobilizon/actors/actors_test.exs index 2c589a0da..7147cda41 100644 --- a/test/mobilizon/actors/actors_test.exs +++ b/test/mobilizon/actors/actors_test.exs @@ -1,11 +1,15 @@ defmodule Mobilizon.ActorsTest do + use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney + use Mobilizon.DataCase + import Mobilizon.Factory + alias Mobilizon.{Actors, Config, Users} alias Mobilizon.Actors.{Actor, Member, Follower, Bot} alias Mobilizon.Media.File, as: FileModel - import Mobilizon.Factory - use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney + alias Mobilizon.Service.ActivityPub + alias Mobilizon.Storage.Page describe "actors" do @valid_attrs %{ @@ -40,8 +44,6 @@ defmodule Mobilizon.ActorsTest do } @remote_account_url "https://social.tcit.fr/users/tcit" - @remote_account_username "tcit" - @remote_account_domain "social.tcit.fr" setup do user = insert(:user) @@ -70,14 +72,14 @@ defmodule Mobilizon.ActorsTest do assert actor_id == Users.get_actor_for_user(user).id end - test "get_actor_with_everything/1 returns the actor with it's organized events", %{ + test "get_actor_with_preload/1 returns the actor with it's organized events", %{ actor: actor } do - assert Actors.get_actor_with_everything(actor.id).organized_events == [] + assert Actors.get_actor_with_preload(actor.id).organized_events == [] event = insert(:event, organizer_actor: actor) event_found_id = - Actors.get_actor_with_everything(actor.id).organized_events |> hd |> Map.get(:id) + Actors.get_actor_with_preload(actor.id).organized_events |> hd |> Map.get(:id) assert event_found_id == event.id end @@ -97,7 +99,7 @@ defmodule Mobilizon.ActorsTest do preferred_username: preferred_username, domain: domain, avatar: %FileModel{name: picture_name} = _picture - } = _actor} = Actors.get_or_fetch_by_url(@remote_account_url) + } = _actor} = ActivityPub.get_or_fetch_by_url(@remote_account_url) assert picture_name == "avatar" @@ -111,51 +113,51 @@ defmodule Mobilizon.ActorsTest do end end - test "get_local_actor_by_name_with_everything!/1 returns the local actor with it's organized events", + test "get_local_actor_by_name_with_preload!/1 returns the local actor with it's organized events", %{ actor: actor } do - assert Actors.get_local_actor_by_name_with_everything(actor.preferred_username).organized_events == + assert Actors.get_local_actor_by_name_with_preload(actor.preferred_username).organized_events == [] event = insert(:event, organizer_actor: actor) event_found_id = - Actors.get_local_actor_by_name_with_everything(actor.preferred_username).organized_events + Actors.get_local_actor_by_name_with_preload(actor.preferred_username).organized_events |> hd |> Map.get(:id) assert event_found_id == event.id end - test "get_actor_by_name_with_everything!/1 returns the local actor with it's organized events", + test "get_actor_by_name_with_preload!/1 returns the local actor with it's organized events", %{ actor: actor } do - assert Actors.get_actor_by_name_with_everything(actor.preferred_username).organized_events == + assert Actors.get_actor_by_name_with_preload(actor.preferred_username).organized_events == [] event = insert(:event, organizer_actor: actor) event_found_id = - Actors.get_actor_by_name_with_everything(actor.preferred_username).organized_events + Actors.get_actor_by_name_with_preload(actor.preferred_username).organized_events |> hd |> Map.get(:id) assert event_found_id == event.id end - test "get_actor_by_name_with_everything!/1 returns the remote actor with it's organized events" do + test "get_actor_by_name_with_preload!/1 returns the remote actor with it's organized events" do use_cassette "actors/remote_actor_mastodon_tcit" do - with {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(@remote_account_url) do - assert Actors.get_actor_by_name_with_everything( + with {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(@remote_account_url) do + assert Actors.get_actor_by_name_with_preload( "#{actor.preferred_username}@#{actor.domain}" ).organized_events == [] event = insert(:event, organizer_actor: actor) event_found_id = - Actors.get_actor_by_name_with_everything( + Actors.get_actor_by_name_with_preload( "#{actor.preferred_username}@#{actor.domain}" ).organized_events |> hd @@ -166,42 +168,21 @@ defmodule Mobilizon.ActorsTest do end end - test "get_or_fetch_by_url/1 returns the local actor for the url", %{ - actor: %Actor{preferred_username: preferred_username} = actor - } do - with {:ok, %Actor{domain: domain} = actor} <- Actors.get_or_fetch_by_url(actor.url) do - assert preferred_username == actor.preferred_username - assert is_nil(domain) - end - end - - test "get_or_fetch_by_url/1 returns the remote actor for the url" do - use_cassette "actors/remote_actor_mastodon_tcit" do - with {:ok, %Actor{preferred_username: preferred_username, domain: domain}} <- - Actors.get_or_fetch_by_url!(@remote_account_url) do - assert preferred_username == @remote_account_username - assert domain == @remote_account_domain - end - end - end - - test "test find_local_by_username/1 returns local actors with similar usernames", %{ + test "test get_local_actor_by_username/1 returns local actors with similar usernames", %{ actor: actor } do actor2 = insert(:actor, preferred_username: "tcit") - [%Actor{id: actor_found_id} | tail] = Actors.find_local_by_username("tcit") + [%Actor{id: actor_found_id} | tail] = Actors.get_local_actor_by_username("tcit") %Actor{id: actor2_found_id} = hd(tail) assert MapSet.new([actor_found_id, actor2_found_id]) == MapSet.new([actor.id, actor2.id]) end - test "test find_and_count_actors_by_username_or_name/4 returns actors with similar usernames", - %{ - actor: %Actor{id: actor_id} - } do + test "test build_actors_by_username_or_name_page/4 returns actors with similar usernames", + %{actor: %Actor{id: actor_id}} do use_cassette "actors/remote_actor_mastodon_tcit" do - with {:ok, %Actor{id: actor2_id}} <- Actors.get_or_fetch_by_url(@remote_account_url) do - %{total: 2, elements: actors} = - Actors.find_and_count_actors_by_username_or_name("tcit", [:Person]) + with {:ok, %Actor{id: actor2_id}} <- ActivityPub.get_or_fetch_by_url(@remote_account_url) do + %Page{total: 2, elements: actors} = + Actors.build_actors_by_username_or_name_page("tcit", [:Person]) actors_ids = actors |> Enum.map(& &1.id) @@ -210,35 +191,13 @@ defmodule Mobilizon.ActorsTest do end end - test "test find_and_count_actors_by_username_or_name/4 returns actors with similar names" do + test "test build_actors_by_username_or_name_page/4 returns actors with similar names" do %{total: 0, elements: actors} = - Actors.find_and_count_actors_by_username_or_name("ohno", [:Person]) + Actors.build_actors_by_username_or_name_page("ohno", [:Person]) assert actors == [] end - test "test get_public_key_for_url/1 with local actor", %{actor: actor} do - assert Actor.get_public_key_for_url(actor.url) == - actor.keys |> Mobilizon.Actors.Actor.prepare_public_key() - end - - @remote_actor_key {:ok, - {:RSAPublicKey, - 20_890_513_599_005_517_665_557_846_902_571_022_168_782_075_040_010_449_365_706_450_877_170_130_373_892_202_874_869_873_999_284_399_697_282_332_064_948_148_602_583_340_776_692_090_472_558_740_998_357_203_838_580_321_412_679_020_304_645_826_371_196_718_081_108_049_114_160_630_664_514_340_729_769_453_281_682_773_898_619_827_376_232_969_899_348_462_205_389_310_883_299_183_817_817_999_273_916_446_620_095_414_233_374_619_948_098_516_821_650_069_821_783_810_210_582_035_456_563_335_930_330_252_551_528_035_801_173_640_288_329_718_719_895_926_309_416_142_129_926_226_047_930_429_802_084_560_488_897_717_417_403_272_782_469_039_131_379_953_278_833_320_195_233_761_955_815_307_522_871_787_339_192_744_439_894_317_730_207_141_881_699_363_391_788_150_650_217_284_777_541_358_381_165_360_697_136_307_663_640_904_621_178_632_289_787, - 65_537}} - test "test get_public_key_for_url/1 with remote actor" do - use_cassette "actors/remote_actor_mastodon_tcit" do - assert Actor.get_public_key_for_url(@remote_account_url) == @remote_actor_key - end - end - - test "test get_public_key_for_url/1 with remote actor and bad key" do - use_cassette "actors/remote_actor_mastodon_tcit_actor_deleted" do - assert Actor.get_public_key_for_url(@remote_account_url) == - {:error, :actor_fetch_error} - end - end - test "create_actor/1 with valid data creates a actor" do assert {:ok, %Actor{} = actor} = Actors.create_actor(@valid_attrs) assert actor.summary == "some description" @@ -351,10 +310,6 @@ defmodule Mobilizon.ActorsTest do "/" <> banner_path ) end - - test "change_actor/1 returns a actor changeset", %{actor: actor} do - assert %Ecto.Changeset{} = Actors.change_actor(actor) - end end describe "groups" do @@ -505,8 +460,8 @@ defmodule Mobilizon.ActorsTest do assert {:ok, %Follower{} = follower} = Actors.create_follower(valid_attrs) assert follower.approved == true - assert %{total: 1, elements: [target_actor]} = Actor.get_followings(actor) - assert %{total: 1, elements: [actor]} = Actor.get_followers(target_actor) + assert %{total: 1, elements: [target_actor]} = Actors.get_followings(actor) + assert %{total: 1, elements: [actor]} = Actors.get_followers(target_actor) end test "create_follower/1 with valid data but same actors fails to create a follower", %{ @@ -554,33 +509,28 @@ defmodule Mobilizon.ActorsTest do assert_raise Ecto.NoResultsError, fn -> Actors.get_follower!(follower.id) end end - test "change_follower/1 returns a follower changeset", context do - follower = create_test_follower(context) - assert %Ecto.Changeset{} = Actors.change_follower(follower) - end - test "follow/3 makes an actor follow another", %{actor: actor, target_actor: target_actor} do # Preloading followers/followings - actor = Actors.get_actor_with_everything(actor.id) - target_actor = Actors.get_actor_with_everything(target_actor.id) + actor = Actors.get_actor_with_preload(actor.id) + target_actor = Actors.get_actor_with_preload(target_actor.id) - {:ok, follower} = Actor.follow(target_actor, actor) + {:ok, follower} = Actors.follow(target_actor, actor) assert follower.actor.id == actor.id # Referesh followers/followings - actor = Actors.get_actor_with_everything(actor.id) - target_actor = Actors.get_actor_with_everything(target_actor.id) + actor = Actors.get_actor_with_preload(actor.id) + target_actor = Actors.get_actor_with_preload(target_actor.id) assert target_actor.followers |> Enum.map(& &1.actor_id) == [actor.id] assert actor.followings |> Enum.map(& &1.target_actor_id) == [target_actor.id] # Test if actor is already following target actor - assert {:error, :already_following, msg} = Actor.follow(target_actor, actor) + assert {:error, :already_following, msg} = Actors.follow(target_actor, actor) assert msg =~ "already following" # Test if target actor is suspended target_actor = %{target_actor | suspended: true} - assert {:error, :suspended, msg} = Actor.follow(target_actor, actor) + assert {:error, :suspended, msg} = Actors.follow(target_actor, actor) assert msg =~ "suspended" end end @@ -620,8 +570,8 @@ defmodule Mobilizon.ActorsTest do assert {:ok, %Member{} = member} = Actors.create_member(valid_attrs) assert member.role == :member - assert [group] = Actor.get_groups_member_of(actor) - assert [actor] = Actor.get_members_for_group(group) + assert [group] = Actors.get_groups_member_of(actor) + assert [actor] = Actors.get_members_for_group(group) end test "create_member/1 with valid data but same actors fails to create a member", %{ @@ -666,10 +616,5 @@ defmodule Mobilizon.ActorsTest do assert {:ok, %Member{}} = Actors.delete_member(member) assert_raise Ecto.NoResultsError, fn -> Actors.get_member!(member.id) end end - - test "change_member/1 returns a member changeset", context do - member = create_test_member(context) - assert %Ecto.Changeset{} = Actors.change_member(member) - end end end diff --git a/test/mobilizon/service/activity_pub/activity_pub_test.exs b/test/mobilizon/service/activity_pub/activity_pub_test.exs index c744d3d68..b0712cb87 100644 --- a/test/mobilizon/service/activity_pub/activity_pub_test.exs +++ b/test/mobilizon/service/activity_pub/activity_pub_test.exs @@ -11,7 +11,6 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do alias Mobilizon.Events alias Mobilizon.Events.Event alias Mobilizon.Actors.Actor - alias Mobilizon.Actors alias Mobilizon.Service.HTTPSignatures.Signature alias Mobilizon.Service.ActivityPub use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney @@ -48,7 +47,7 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do test "returns an actor from url" do use_cassette "activity_pub/fetch_framapiaf.org_users_tcit" do assert {:ok, %Actor{preferred_username: "tcit", domain: "framapiaf.org"}} = - Actors.get_or_fetch_by_url("https://framapiaf.org/users/tcit") + ActivityPub.get_or_fetch_by_url("https://framapiaf.org/users/tcit") end end end diff --git a/test/mobilizon/service/activity_pub/transmogrifier_test.exs b/test/mobilizon/service/activity_pub/transmogrifier_test.exs index cd636a3d9..1c515c6b9 100644 --- a/test/mobilizon/service/activity_pub/transmogrifier_test.exs +++ b/test/mobilizon/service/activity_pub/transmogrifier_test.exs @@ -222,8 +222,8 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do assert data["type"] == "Follow" assert data["id"] == "https://social.tcit.fr/users/tcit#follows/2" - actor = Actors.get_actor_with_everything(actor.id) - assert Actor.following?(Actors.get_actor_by_url!(data["actor"], true), actor) + actor = Actors.get_actor_with_preload(actor.id) + assert Actors.following?(Actors.get_actor_by_url!(data["actor"], true), actor) end # test "it works for incoming follow requests from hubzilla" do @@ -498,7 +498,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do assert data["actor"] == "https://social.tcit.fr/users/tcit" {:ok, followed} = Actors.get_actor_by_url(data["actor"]) - refute Actor.following?(followed, actor) + refute Actors.following?(followed, actor) end # test "it works for incoming blocks" do @@ -581,10 +581,10 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do follower = insert(:actor) followed = insert(:actor) - refute Actor.following?(follower, followed) + refute Actors.following?(follower, followed) {:ok, follow_activity, _} = ActivityPub.follow(follower, followed) - assert Actor.following?(follower, followed) + assert Actors.following?(follower, followed) accept_data = File.read!("test/fixtures/mastodon-accept-activity.json") @@ -605,7 +605,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do {:ok, follower} = Actors.get_actor_by_url(follower.url) - assert Actor.following?(follower, followed) + assert Actors.following?(follower, followed) end test "it works for incoming accepts which are referenced by IRI only" do @@ -627,7 +627,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do {:ok, follower} = Actors.get_actor_by_url(follower.url) - assert Actor.following?(follower, followed) + assert Actors.following?(follower, followed) end test "it fails for incoming accepts which cannot be correlated" do @@ -646,7 +646,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do {:ok, follower} = Actors.get_actor_by_url(follower.url) - refute Actor.following?(follower, followed) + refute Actors.following?(follower, followed) end test "it fails for incoming rejects which cannot be correlated" do @@ -665,7 +665,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do {:ok, follower} = Actors.get_actor_by_url(follower.url) - refute Actor.following?(follower, followed) + refute Actors.following?(follower, followed) end test "it works for incoming rejects which are referenced by IRI only" do @@ -674,7 +674,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do {:ok, follow_activity, _} = ActivityPub.follow(follower, followed) - assert Actor.following?(follower, followed) + assert Actors.following?(follower, followed) reject_data = File.read!("test/fixtures/mastodon-reject-activity.json") @@ -684,7 +684,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do {:ok, %Activity{data: _}, _} = Transmogrifier.handle_incoming(reject_data) - refute Actor.following?(follower, followed) + refute Actors.following?(follower, followed) end test "it rejects activities without a valid ID" do diff --git a/test/mobilizon_web/api/search_test.exs b/test/mobilizon_web/api/search_test.exs index 6f7395f68..d654a48e3 100644 --- a/test/mobilizon_web/api/search_test.exs +++ b/test/mobilizon_web/api/search_test.exs @@ -6,6 +6,8 @@ defmodule MobilizonWeb.API.SearchTest do alias Mobilizon.Actors alias Mobilizon.Actors.Actor alias Mobilizon.Service.ActivityPub + alias Mobilizon.Storage.Page + alias MobilizonWeb.API.Search import Mock @@ -13,7 +15,7 @@ defmodule MobilizonWeb.API.SearchTest do test "search an user by username" do with_mock ActivityPub, find_or_make_actor_from_nickname: fn "toto@domain.tld" -> {:ok, %Actor{id: 42}} end do - assert {:ok, %{total: 1, elements: [%Actor{id: 42}]}} == + assert {:ok, %Page{total: 1, elements: [%Actor{id: 42}]}} == Search.search_actors("toto@domain.tld", 1, 10, :Person) assert_called(ActivityPub.find_or_make_actor_from_nickname("toto@domain.tld")) @@ -23,7 +25,7 @@ defmodule MobilizonWeb.API.SearchTest do test "search something by URL" do with_mock ActivityPub, fetch_object_from_url: fn "https://social.tcit.fr/users/tcit" -> {:ok, %Actor{id: 42}} end do - assert {:ok, %{total: 1, elements: [%Actor{id: 42}]}} == + assert {:ok, %Page{total: 1, elements: [%Actor{id: 42}]}} == Search.search_actors("https://social.tcit.fr/users/tcit", 1, 10, :Person) assert_called(ActivityPub.fetch_object_from_url("https://social.tcit.fr/users/tcit")) @@ -32,20 +34,20 @@ defmodule MobilizonWeb.API.SearchTest do test "search actors" do with_mock Actors, - find_and_count_actors_by_username_or_name: fn "toto", _type, 1, 10 -> - %{total: 1, elements: [%Actor{id: 42}]} + build_actors_by_username_or_name_page: fn "toto", _type, 1, 10 -> + %Page{total: 1, elements: [%Actor{id: 42}]} end do assert {:ok, %{total: 1, elements: [%Actor{id: 42}]}} = Search.search_actors("toto", 1, 10, :Person) - assert_called(Actors.find_and_count_actors_by_username_or_name("toto", [:Person], 1, 10)) + assert_called(Actors.build_actors_by_username_or_name_page("toto", [:Person], 1, 10)) end end test "search events" do with_mock Events, find_and_count_events_by_name: fn "toto", 1, 10 -> - %{total: 1, elements: [%Event{title: "super toto event"}]} + %Page{total: 1, elements: [%Event{title: "super toto event"}]} end do assert {:ok, %{total: 1, elements: [%Event{title: "super toto event"}]}} = Search.search_events("toto", 1, 10) diff --git a/test/mobilizon_web/controllers/activity_pub_controller_test.exs b/test/mobilizon_web/controllers/activity_pub_controller_test.exs index 54cb958da..c53c420e8 100644 --- a/test/mobilizon_web/controllers/activity_pub_controller_test.exs +++ b/test/mobilizon_web/controllers/activity_pub_controller_test.exs @@ -177,7 +177,7 @@ defmodule MobilizonWeb.ActivityPubControllerTest do test "it returns the followers in a collection", %{conn: conn} do actor = insert(:actor, visibility: :public) actor2 = insert(:actor) - Actor.follow(actor, actor2) + Actors.follow(actor, actor2) result = conn @@ -190,7 +190,7 @@ defmodule MobilizonWeb.ActivityPubControllerTest do test "it returns no followers for a private actor", %{conn: conn} do actor = insert(:actor, visibility: :private) actor2 = insert(:actor) - Actor.follow(actor, actor2) + Actors.follow(actor, actor2) result = conn @@ -205,7 +205,7 @@ defmodule MobilizonWeb.ActivityPubControllerTest do Enum.each(1..15, fn _ -> other_actor = insert(:actor) - Actor.follow(actor, other_actor) + Actors.follow(actor, other_actor) end) result = @@ -229,7 +229,7 @@ defmodule MobilizonWeb.ActivityPubControllerTest do test "it returns the followings in a collection", %{conn: conn} do actor = insert(:actor) actor2 = insert(:actor, visibility: :public) - Actor.follow(actor, actor2) + Actors.follow(actor, actor2) result = conn @@ -242,7 +242,7 @@ defmodule MobilizonWeb.ActivityPubControllerTest do test "it returns no followings for a private actor", %{conn: conn} do actor = insert(:actor) actor2 = insert(:actor, visibility: :private) - Actor.follow(actor, actor2) + Actors.follow(actor, actor2) result = conn @@ -257,7 +257,7 @@ defmodule MobilizonWeb.ActivityPubControllerTest do Enum.each(1..15, fn _ -> other_actor = insert(:actor) - Actor.follow(other_actor, actor) + Actors.follow(other_actor, actor) end) result = @@ -322,7 +322,7 @@ defmodule MobilizonWeb.ActivityPubControllerTest do # Enum.each(1..15, fn _ -> # actor = Repo.get(Actor, actor.id) # other_actor = insert(:actor) - # Actor.follow(actor, other_actor) + # Actors.follow(actor, other_actor) # end) # result = diff --git a/test/support/factory.ex b/test/support/factory.ex index 97974657d..a11634d39 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -1,10 +1,13 @@ defmodule Mobilizon.Factory do @moduledoc """ - Factory for fixtures with ExMachina + Factory for fixtures with ExMachina. """ - # with Ecto + use ExMachina.Ecto, repo: Mobilizon.Storage.Repo + alias Mobilizon.Actors.Actor + alias Mobilizon.Crypto + alias MobilizonWeb.Router.Helpers, as: Routes alias MobilizonWeb.Endpoint alias MobilizonWeb.Upload @@ -21,10 +24,6 @@ defmodule Mobilizon.Factory do end def actor_factory do - key = :public_key.generate_key({:rsa, 2048, 65_537}) - entry = :public_key.pem_entry_encode(:RSAPrivateKey, key) - pem = [entry] |> :public_key.pem_encode() |> String.trim_trailing() - preferred_username = sequence("thomas") %Mobilizon.Actors.Actor{ @@ -32,7 +31,7 @@ defmodule Mobilizon.Factory do domain: nil, followers: [], followings: [], - keys: pem, + keys: Crypto.generate_rsa_2048_private_key(), type: :Person, avatar: build(:file, name: "Avatar"), banner: build(:file, name: "Banner"), diff --git a/test/tasks/relay_test.exs b/test/tasks/relay_test.exs index f35a0a5f4..22f675806 100644 --- a/test/tasks/relay_test.exs +++ b/test/tasks/relay_test.exs @@ -22,7 +22,7 @@ defmodule Mix.Tasks.Mobilizon.RelayTest do {:ok, target_actor} = Actors.get_actor_by_url(target_instance) refute is_nil(target_actor.domain) - assert Actor.following?(local_actor, target_actor) + assert Actors.following?(local_actor, target_actor) end end end @@ -36,11 +36,11 @@ defmodule Mix.Tasks.Mobilizon.RelayTest do %Actor{} = local_actor = Relay.get_actor() {:ok, %Actor{} = target_actor} = Actors.get_actor_by_url(target_instance) - assert %Follower{} = Actor.following?(local_actor, target_actor) + assert %Follower{} = Actors.following?(local_actor, target_actor) Mix.Tasks.Mobilizon.Relay.run(["unfollow", target_instance]) - refute Actor.following?(local_actor, target_actor) + refute Actors.following?(local_actor, target_actor) end end end From cc4a59ad1f489c8ab09ff780ffec5cca0cab5631 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Mon, 9 Sep 2019 09:35:50 +0200 Subject: [PATCH 13/29] Run mix format and fix a credo check Signed-off-by: Thomas Citharel --- lib/mobilizon/actors/member.ex | 2 +- lib/mobilizon/application.ex | 3 +-- lib/mobilizon_web/resolvers/user.ex | 3 ++- lib/service/activity_pub/activity_pub.ex | 1 - lib/service/http_signatures/signature.ex | 9 +++++---- test/mobilizon/actors/actors_test.exs | 4 +--- 6 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/mobilizon/actors/member.ex b/lib/mobilizon/actors/member.ex index 98951e6b3..b6963e546 100644 --- a/lib/mobilizon/actors/member.ex +++ b/lib/mobilizon/actors/member.ex @@ -8,7 +8,7 @@ defmodule Mobilizon.Actors.Member do import Ecto.Changeset alias Mobilizon.Actors.{Actor, Member, MemberRole} - + @type t :: %__MODULE__{ role: MemberRole.t(), parent: Actor.t(), diff --git a/lib/mobilizon/application.ex b/lib/mobilizon/application.ex index 5291896a8..19f3d9718 100644 --- a/lib/mobilizon/application.ex +++ b/lib/mobilizon/application.ex @@ -93,8 +93,7 @@ defmodule Mobilizon.Application do def named_version, do: @name <> " " <> @version def user_agent do - info = - "#{MobilizonWeb.Endpoint.url()} <#{Config.get([:instance, :email], "")}>" + info = "#{MobilizonWeb.Endpoint.url()} <#{Config.get([:instance, :email], "")}>" named_version() <> "; " <> info end diff --git a/lib/mobilizon_web/resolvers/user.ex b/lib/mobilizon_web/resolvers/user.ex index 5d954f35c..2f747abb3 100644 --- a/lib/mobilizon_web/resolvers/user.ex +++ b/lib/mobilizon_web/resolvers/user.ex @@ -112,7 +112,8 @@ defmodule MobilizonWeb.Resolvers.User do """ @spec create_user(any(), map(), any()) :: tuple() def create_user(_parent, args, _resolution) do - with {:registrations_open, true} <- {:registrations_open, Config.instance_registrations_open?()}, + with {:registrations_open, true} <- + {:registrations_open, Config.instance_registrations_open?()}, {:ok, %User{} = user} <- Users.register(args) do Activation.send_confirmation_email(user) {:ok, user} diff --git a/lib/service/activity_pub/activity_pub.ex b/lib/service/activity_pub/activity_pub.ex index cccdae211..c0733c66e 100644 --- a/lib/service/activity_pub/activity_pub.ex +++ b/lib/service/activity_pub/activity_pub.ex @@ -113,7 +113,6 @@ defmodule Mobilizon.Service.ActivityPub do end end - @doc """ Getting an actor from url, eventually creating it """ diff --git a/lib/service/http_signatures/signature.ex b/lib/service/http_signatures/signature.ex index 0012ea02c..0eabf6325 100644 --- a/lib/service/http_signatures/signature.ex +++ b/lib/service/http_signatures/signature.ex @@ -16,16 +16,17 @@ defmodule Mobilizon.Service.HTTPSignatures.Signature do require Logger def key_id_to_actor_url(key_id) do - %{path: path} = uri = + %{path: path} = + uri = key_id |> URI.parse() |> Map.put(:fragment, nil) uri = - if not is_nil(path) do - Map.put(uri, :path, String.trim_trailing(path, "/publickey")) - else + if is_nil(path) do uri + else + Map.put(uri, :path, String.trim_trailing(path, "/publickey")) end URI.to_string(uri) diff --git a/test/mobilizon/actors/actors_test.exs b/test/mobilizon/actors/actors_test.exs index 7147cda41..7c211f472 100644 --- a/test/mobilizon/actors/actors_test.exs +++ b/test/mobilizon/actors/actors_test.exs @@ -157,9 +157,7 @@ defmodule Mobilizon.ActorsTest do event = insert(:event, organizer_actor: actor) event_found_id = - Actors.get_actor_by_name_with_preload( - "#{actor.preferred_username}@#{actor.domain}" - ).organized_events + Actors.get_actor_by_name_with_preload("#{actor.preferred_username}@#{actor.domain}").organized_events |> hd |> Map.get(:id) From 8f50e88e1e57de5212ec2b1cb7edf5f656fa105e Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Mon, 9 Sep 2019 09:55:08 +0200 Subject: [PATCH 14/29] Fix failing test on updating actor Signed-off-by: Thomas Citharel --- lib/mobilizon/actors/actor.ex | 2 +- lib/service/activity_pub/converters/actor.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex index 7820af547..9d41d1494 100644 --- a/lib/mobilizon/actors/actor.ex +++ b/lib/mobilizon/actors/actor.ex @@ -65,7 +65,7 @@ defmodule Mobilizon.Actors.Actor do ] @attrs @required_attrs ++ @optional_attrs - @update_required_attrs @required_attrs + @update_required_attrs @required_attrs -- [:url] @update_optional_attrs [:name, :summary, :manually_approves_followers, :user_id] @update_attrs @update_required_attrs ++ @update_optional_attrs diff --git a/lib/service/activity_pub/converters/actor.ex b/lib/service/activity_pub/converters/actor.ex index 5e8c4ac3c..002e7c3e8 100644 --- a/lib/service/activity_pub/converters/actor.ex +++ b/lib/service/activity_pub/converters/actor.ex @@ -33,7 +33,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Actor do "type" => String.to_existing_atom(object["type"]), "preferred_username" => object["preferredUsername"], "summary" => object["summary"], - "url" => object["url"], + "url" => object["id"], "name" => object["name"], "avatar" => avatar, "banner" => banner, From 60707b8f8db38f28b4046c8eaec283f45899dc54 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Mon, 9 Sep 2019 09:55:33 +0200 Subject: [PATCH 15/29] Fix cleaning upload files after running tests See https://elixirforum.com/t/running-code-after-all-exunit-tests-are-done/9937/6 Signed-off-by: Thomas Citharel --- mix.exs | 8 ++++++-- script/clean_after_tests | 3 --- 2 files changed, 6 insertions(+), 5 deletions(-) delete mode 100755 script/clean_after_tests diff --git a/mix.exs b/mix.exs index 1435bc9d9..aa18d53c0 100644 --- a/mix.exs +++ b/mix.exs @@ -132,8 +132,7 @@ defmodule Mobilizon.Mixfile do test: [ "ecto.create --quiet", "ecto.migrate", - "test", - "cmd ./script/clean_after_tests" + &run_test/1 ], "phx.deps_migrate_serve": [ "deps.get", @@ -145,6 +144,11 @@ defmodule Mobilizon.Mixfile do ] end + defp run_test(args) do + Mix.Task.run("test", args) + File.rm_rf!("test/uploads") + end + defp docs() do [ source_ref: "v#{@version}", diff --git a/script/clean_after_tests b/script/clean_after_tests deleted file mode 100755 index 7344216e4..000000000 --- a/script/clean_after_tests +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -rm -rf test/uploads From e4a446003d890bd8d2089d2941b8f9f6ad5ded99 Mon Sep 17 00:00:00 2001 From: miffigriffy Date: Wed, 11 Sep 2019 03:16:37 +0200 Subject: [PATCH 16/29] Refactoring of Actors context --- lib/mix/tasks/mobilizon/create_bot.ex | 2 +- lib/mobilizon/actors/actors.ex | 1325 ++++++++--------- lib/mobilizon/actors/follower.ex | 7 +- lib/mobilizon/addresses/addresses.ex | 7 +- lib/mobilizon/media/media.ex | 7 +- lib/mobilizon/users/user.ex | 7 +- lib/mobilizon/users/users.ex | 14 +- lib/mobilizon_web/api/follows.ex | 2 +- lib/mobilizon_web/resolvers/member.ex | 2 +- lib/mobilizon_web/resolvers/person.ex | 2 +- .../views/activity_pub/actor_view.ex | 8 +- lib/service/activity_pub/activity_pub.ex | 2 +- lib/service/activity_pub/relay.ex | 2 +- lib/service/activity_pub/transmogrifier.ex | 2 +- test/mobilizon/actors/actors_test.exs | 17 +- .../activity_pub/transmogrifier_test.exs | 22 +- test/tasks/relay_test.exs | 6 +- 17 files changed, 671 insertions(+), 763 deletions(-) diff --git a/lib/mix/tasks/mobilizon/create_bot.ex b/lib/mix/tasks/mobilizon/create_bot.ex index c4b85dd22..13c321316 100644 --- a/lib/mix/tasks/mobilizon/create_bot.ex +++ b/lib/mix/tasks/mobilizon/create_bot.ex @@ -15,7 +15,7 @@ defmodule Mix.Tasks.Mobilizon.CreateBot do Mix.Task.run("app.start") with {:ok, %User{} = user} <- Users.get_user_by_email(email, true), - actor <- Actors.register_bot_account(%{name: name, summary: summary}), + actor <- Actors.register_bot(%{name: name, summary: summary}), {:ok, %Bot{} = bot} <- Actors.create_bot(%{ "type" => type, diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index eb4da0fbb..ca9553826 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -9,7 +9,7 @@ defmodule Mobilizon.Actors do alias Ecto.Multi alias Mobilizon.Actors.{Actor, Bot, Follower, Member} - alias Mobilizon.Crypto + alias Mobilizon.{Crypto, Events} alias Mobilizon.Media.File alias Mobilizon.Storage.{Page, Repo} @@ -45,6 +45,8 @@ defmodule Mobilizon.Actors do :creator ]) + @administrator_roles [:creator, :administrator] + @doc false @spec data :: Dataloader.Ecto.t() def data, do: Dataloader.Ecto.new(Repo, query: &query/2) @@ -61,7 +63,7 @@ defmodule Mobilizon.Actors do @doc """ Gets a single actor. - Raises `Ecto.NoResultsError` if the Actor does not exist. + Raises `Ecto.NoResultsError` if the actor does not exist. """ @spec get_actor!(integer | String.t()) :: Actor.t() def get_actor!(id), do: Repo.get!(Actor, id) @@ -166,40 +168,15 @@ defmodule Mobilizon.Actors do def get_cached_local_actor_by_name(name) do Cachex.fetch(:activity_pub, "actor_" <> name, fn "actor_" <> name -> case get_local_actor_by_name(name) do - nil -> {:ignore, nil} - %Actor{} = actor -> {:commit, actor} + nil -> + {:ignore, nil} + + %Actor{} = actor -> + {:commit, actor} end end) end - @doc """ - Gets local actors by their username. - """ - @spec get_local_actor_by_username(String.t()) :: [Actor.t()] - def get_local_actor_by_username(username) do - username - |> actor_by_username_query() - |> filter_local() - |> Repo.all() - |> Repo.preload(:organized_events) - end - - @doc """ - Builds a page struct for actors by their name or displayed name. - """ - @spec build_actors_by_username_or_name_page( - String.t(), - [ActorType.t()], - integer | nil, - integer | nil - ) :: Page.t() - def build_actors_by_username_or_name_page(username, types, page \\ nil, limit \\ nil) do - username - |> actor_by_username_or_name_query() - |> filter_by_types(types) - |> Page.build_page(page, limit) - end - @doc """ Creates an actor. """ @@ -210,6 +187,23 @@ defmodule Mobilizon.Actors do |> Repo.insert() end + @doc """ + Creates a new person actor. + """ + @spec new_person(map) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()} + def new_person(args) do + args = Map.put(args, :keys, Crypto.generate_rsa_2048_private_key()) + + with {:ok, %Actor{} = person} <- + %Actor{} + |> Actor.registration_changeset(args) + |> Repo.insert() do + Events.create_feed_token(%{"user_id" => args["user_id"], "actor_id" => person.id}) + + {:ok, person} + end + end + @doc """ Updates an actor. """ @@ -277,6 +271,34 @@ defmodule Mobilizon.Actors do @spec list_actors :: [Actor.t()] def list_actors, do: Repo.all(Actor) + @doc """ + Returns the list of local actors by their username. + """ + @spec list_local_actor_by_username(String.t()) :: [Actor.t()] + def list_local_actor_by_username(username) do + username + |> actor_by_username_query() + |> filter_local() + |> Repo.all() + |> Repo.preload(:organized_events) + end + + @doc """ + Builds a page struct for actors by their name or displayed name. + """ + @spec build_actors_by_username_or_name_page( + String.t(), + [ActorType.t()], + integer | nil, + integer | nil + ) :: Page.t() + def build_actors_by_username_or_name_page(username, types, page \\ nil, limit \\ nil) do + username + |> actor_by_username_or_name_query() + |> filter_by_types(types) + |> Page.build_page(page, limit) + end + @doc """ Gets a group by its title. """ @@ -297,11 +319,453 @@ defmodule Mobilizon.Actors do |> Repo.one() end + @doc """ + Gets a group by its actor id. + """ + @spec get_group_by_actor_id(integer | String.t()) :: + {:ok, Actor.t()} | {:error, :group_not_found} + def get_group_by_actor_id(actor_id) do + case Repo.get_by(Actor, id: actor_id, type: :Group) do + nil -> + {:error, :group_not_found} + + actor -> + {:ok, actor} + end + end + + @doc """ + Creates a group. + """ + @spec create_group(map) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()} + def create_group(attrs \\ %{}) do + %Actor{} + |> Actor.group_creation(attrs) + |> Repo.insert() + end + + @doc """ + Deletes a group. + """ + def delete_group!(%Actor{type: :Group} = group), do: Repo.delete!(group) + + @doc """ + Lists the groups. + """ + @spec list_groups(integer | nil, integer | nil) :: [Actor.t()] + def list_groups(page \\ nil, limit \\ nil) do + groups_query() + |> Page.paginate(page, limit) + |> Repo.all() + end + + @doc """ + Returns the list of groups an actor is member of. + """ + @spec list_groups_member_of(Actor.t()) :: [Actor.t()] + def list_groups_member_of(%Actor{id: actor_id}) do + actor_id + |> groups_member_of_query() + |> Repo.all() + end + + @doc """ + Gets a single member. + Raises `Ecto.NoResultsError` if the member does not exist. + """ + @spec get_member!(integer | String.t()) :: Member.t() + def get_member!(id), do: Repo.get!(Member, id) + + @doc """ + Gets a single member of an actor (for example a group). + """ + @spec get_member(integer | String.t(), integer | String.t()) :: + {:ok, Member.t()} | {:error, :member_not_found} + def get_member(actor_id, parent_id) do + case Repo.get_by(Member, actor_id: actor_id, parent_id: parent_id) do + nil -> + {:error, :member_not_found} + + member -> + {:ok, member} + end + end + + @doc """ + Creates a member. + """ + @spec create_member(map) :: {:ok, Member.t()} | {:error, Ecto.Changeset.t()} + def create_member(attrs \\ %{}) do + with {:ok, %Member{} = member} <- + %Member{} + |> Member.changeset(attrs) + |> Repo.insert() do + {:ok, Repo.preload(member, [:actor, :parent])} + end + end + + @doc """ + Updates a member. + """ + @spec update_member(Member.t(), map) :: {:ok, Member.t()} | {:error, Ecto.Changeset.t()} + def update_member(%Member{} = member, attrs) do + member + |> Member.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a member. + """ + @spec delete_member(Member.t()) :: {:ok, Member.t()} | {:error, Ecto.Changeset.t()} + def delete_member(%Member{} = member), do: Repo.delete(member) + + @doc """ + Returns the list of members for an actor. + """ + @spec list_members_for_actor(Actor.t()) :: [Member.t()] + def list_members_for_actor(%Actor{id: actor_id}) do + actor_id + |> members_for_actor_query() + |> Repo.all() + end + + @doc """ + Returns the list of members for a group. + """ + @spec list_members_for_group(Actor.t()) :: [Member.t()] + def list_members_for_group(%Actor{id: group_id, type: :Group}) do + group_id + |> members_for_group_query() + |> Repo.all() + end + + @doc """ + Returns the list of administrator members for a group. + """ + @spec list_administrator_members_for_group(integer | String.t(), integer | nil, integer | nil) :: + [Member.t()] + def list_administrator_members_for_group(id, page \\ nil, limit \\ nil) do + id + |> administrator_members_for_group_query() + |> Page.paginate(page, limit) + |> Repo.all() + end + + @doc """ + Returns the list of all group ids where the actor_id is the last administrator. + """ + @spec list_group_ids_where_last_administrator(integer | String.t()) :: [integer] + def list_group_ids_where_last_administrator(actor_id) do + actor_id + |> group_ids_where_last_administrator_query() + |> Repo.all() + end + + @doc """ + Gets a single bot. + Raises `Ecto.NoResultsError` if the bot does not exist. + """ + def get_bot!(id), do: Repo.get!(Bot, id) + + @doc """ + Gets the bot associated to an actor. + """ + @spec get_bot_for_actor(Actor.t()) :: Bot.t() + def get_bot_for_actor(%Actor{id: actor_id}) do + Repo.get_by!(Bot, actor_id: actor_id) + end + + @doc """ + Creates a bot. + """ + @spec create_bot(map) :: {:ok, Bot.t()} | {:error, Ecto.Changeset.t()} + def create_bot(attrs \\ %{}) do + %Bot{} + |> Bot.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Registers a new bot. + """ + @spec register_bot(map) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()} + def register_bot(%{name: name, summary: summary}) do + attrs = %{ + preferred_username: name, + domain: nil, + keys: Crypto.generate_rsa_2048_private_key(), + summary: summary, + type: :Service + } + + %Actor{} + |> Actor.registration_changeset(attrs) + |> Repo.insert() + end + + @spec get_or_create_actor_by_url(String.t(), String.t()) :: + {:ok, Actor.t()} | {:error, Ecto.Changeset.t()} + def get_or_create_actor_by_url(url, preferred_username \\ "relay") do + case get_actor_by_url(url) do + {:ok, %Actor{} = actor} -> + {:ok, actor} + + _ -> + %{url: url, preferred_username: preferred_username} + |> Actor.relay_creation_changeset() + |> Repo.insert() + end + end + + @doc """ + Updates a bot. + """ + @spec update_bot(Bot.t(), map) :: {:ok, Bot.t()} | {:error, Ecto.Changeset.t()} + def update_bot(%Bot{} = bot, attrs) do + bot + |> Bot.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a bot. + """ + @spec delete_bot(Bot.t()) :: {:ok, Bot.t()} | {:error, Ecto.Changeset.t()} + def delete_bot(%Bot{} = bot), do: Repo.delete(bot) + + @doc """ + Returns the list of bots. + """ + @spec list_bots :: [Bot.t()] + def list_bots, do: Repo.all(Bot) + + @doc """ + Gets a single follower. + Raises `Ecto.NoResultsError` if the follower does not exist. + """ + @spec get_follower!(integer | String.t()) :: Follower.t() + def get_follower!(id) do + Follower + |> Repo.get!(id) + |> Repo.preload([:actor, :target_actor]) + end + + @doc """ + Get a follower by the url. + """ + @spec get_follower_by_url(String.t()) :: Follower.t() + def get_follower_by_url(url) do + url + |> follower_by_url() + |> Repo.one() + end + + @doc """ + Gets a follower by the followed actor and following actor + """ + @spec get_follower_by_followed_and_following(Actor.t(), Actor.t()) :: Follower.t() | nil + def get_follower_by_followed_and_following(%Actor{id: followed_id}, %Actor{id: following_id}) do + followed_id + |> follower_by_followed_and_following_query(following_id) + |> Repo.one() + end + + @doc """ + Creates a follower. + """ + @spec create_follower(map) :: {:ok, Follower.t()} | {:error, Ecto.Changeset.t()} + def create_follower(attrs \\ %{}) do + with {:ok, %Follower{} = follower} <- + %Follower{} + |> Follower.changeset(attrs) + |> Repo.insert() do + {:ok, Repo.preload(follower, [:actor, :target_actor])} + end + end + + @doc """ + Updates a follower. + """ + @spec update_follower(Follower.t(), map) :: {:ok, Follower.t()} | {:error, Ecto.Changeset.t()} + def update_follower(%Follower{} = follower, attrs) do + follower + |> Follower.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a follower. + """ + @spec delete_follower(Follower.t()) :: {:ok, Follower.t()} | {:error, Ecto.Changeset.t()} + def delete_follower(%Follower{} = follower), do: Repo.delete(follower) + + @doc """ + Deletes a follower by followed and following actors. + """ + @spec delete_follower_by_followed_and_following(Actor.t(), Actor.t()) :: + {:ok, Follower.t()} | {:error, Ecto.Changeset.t()} + def delete_follower_by_followed_and_following(%Actor{} = followed, %Actor{} = following) do + followed + |> get_follower_by_followed_and_following(following) + |> Repo.delete() + end + + @doc """ + Returns the list of followers for an actor. + If actor A and C both follow actor B, actor B's followers are A and C. + """ + @spec list_followers_for_actor(Actor.t()) :: [Follower.t()] + def list_followers_for_actor(%Actor{id: actor_id}) do + actor_id + |> followers_for_actor_query() + |> Repo.all() + end + + @doc """ + Returns the list of external followers for an actor. + """ + @spec list_external_followers_for_actor(Actor.t()) :: [Follower.t()] + def list_external_followers_for_actor(%Actor{id: actor_id}) do + actor_id + |> followers_for_actor_query() + |> filter_external() + |> Repo.all() + end + + @doc """ + Build a page struct for followers of an actor. + """ + @spec build_followers_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t() + def build_followers_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do + actor_id + |> followers_for_actor_query() + |> Page.build_page(page, limit) + end + + @doc """ + Returns the list of followings for an actor. + If actor A follows actor B and C, actor A's followings are B and C. + """ + @spec list_followings_for_actor(Actor.t()) :: [Follower.t()] + def list_followings_for_actor(%Actor{id: actor_id}) do + actor_id + |> followings_for_actor_query() + |> Repo.all() + end + + @doc """ + Build a page struct for followings of an actor. + """ + @spec build_followings_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t() + def build_followings_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do + actor_id + |> followings_for_actor_query() + |> Page.build_page(page, limit) + end + + @doc """ + Makes an actor following another actor. + """ + @spec follow(Actor.t(), Actor.t(), String.t() | nil, boolean | nil) :: + {:ok, Follower.t()} | {:error, atom, String.t()} + def follow(%Actor{} = followed, %Actor{} = follower, url \\ nil, approved \\ true) do + with {:suspended, false} <- {:suspended, followed.suspended}, + # Check if followed has blocked follower + {:already_following, nil} <- {:already_following, is_following(follower, followed)} do + Logger.info( + "Making #{follower.preferred_username} follow #{followed.preferred_username} " <> + "(approved: #{approved})" + ) + + create_follower(%{ + "actor_id" => follower.id, + "target_actor_id" => followed.id, + "approved" => approved, + "url" => url + }) + else + {:already_following, %Follower{}} -> + {:error, :already_following, + "Could not follow actor: you are already following #{followed.preferred_username}"} + + {:suspended, _} -> + {:error, :suspended, + "Could not follow actor: #{followed.preferred_username} has been suspended"} + end + end + + @doc """ + Unfollows an actor (removes a Follower record). + """ + @spec unfollow(Actor.t(), Actor.t()) :: + {:ok, Follower.t()} | {:error, Ecto.Changeset.t() | String.t()} + def unfollow(%Actor{} = followed, %Actor{} = follower) do + case {:already_following, is_following(follower, followed)} do + {:already_following, %Follower{} = follow} -> + delete_follower(follow) + + {:already_following, nil} -> + {:error, "Could not unfollow actor: you are not following #{followed.preferred_username}"} + end + end + + @doc """ + Checks whether an actor is following another actor. + """ + @spec is_following(Actor.t(), Actor.t()) :: Follower.t() | nil + def is_following(%Actor{} = follower_actor, %Actor{} = followed_actor) do + get_follower_by_followed_and_following(followed_actor, follower_actor) + end + + @spec remove_banner(Actor.t()) :: {:ok, Actor.t()} + defp remove_banner(%Actor{banner: nil} = actor), do: {:ok, actor} + + defp remove_banner(%Actor{banner: %File{url: url}} = actor) do + safe_remove_file(url, actor) + end + + @spec remove_avatar(Actor.t()) :: {:ok, Actor.t()} + defp remove_avatar(%Actor{avatar: nil} = actor), do: {:ok, actor} + + defp remove_avatar(%Actor{avatar: %File{url: url}} = actor) do + safe_remove_file(url, actor) + end + + @spec safe_remove_file(String.t(), Actor.t()) :: {:ok, Actor.t()} + defp safe_remove_file(url, %Actor{} = actor) do + case MobilizonWeb.Upload.remove(url) do + {:ok, _value} -> + {:ok, actor} + + {:error, error} -> + Logger.error("Error while removing an upload file") + Logger.debug(inspect(error)) + + {:ok, actor} + end + end + + @spec delete_files_if_media_changed(Ecto.Changeset.t()) :: Ecto.Changeset.t() + defp delete_files_if_media_changed(%Ecto.Changeset{changes: changes, data: data} = changeset) do + Enum.each([:avatar, :banner], fn key -> + if Map.has_key?(changes, key) do + with %Ecto.Changeset{changes: %{url: new_url}} <- changes[key], + %{url: old_url} <- data |> Map.from_struct() |> Map.get(key), + false <- new_url == old_url do + MobilizonWeb.Upload.remove(old_url) + end + end + end) + + changeset + end + @spec actor_with_preload_query(integer | String.t()) :: Ecto.Query.t() - defp actor_with_preload_query(id) do + defp actor_with_preload_query(actor_id) do from( a in Actor, - where: a.id == ^id, + where: a.id == ^actor_id, preload: [:organized_events, :followers, :followings] ) end @@ -354,7 +818,113 @@ defmodule Mobilizon.Actors do @spec group_query :: Ecto.Query.t() defp group_query do - from(a in Actor, where: a.type == "Group") + from(a in Actor, where: a.type == ^:Group) + end + + @spec groups_member_of_query(integer | String.t()) :: Ecto.Query.t() + defp groups_member_of_query(actor_id) do + from( + a in Actor, + join: m in Member, + on: a.id == m.parent_id, + where: m.actor_id == ^actor_id + ) + end + + @spec groups_query :: Ecto.Query.t() + defp groups_query do + from( + a in Actor, + where: a.type == ^:Group, + where: a.visibility in ^[:public, :unlisted] + ) + end + + @spec members_for_actor_query(integer | String.t()) :: Ecto.Query.t() + defp members_for_actor_query(actor_id) do + from( + m in Member, + where: m.actor_id == ^actor_id, + preload: [:parent] + ) + end + + @spec members_for_group_query(integer | String.t()) :: Ecto.Query.t() + defp members_for_group_query(group_id) do + from( + m in Member, + where: m.parent_id == ^group_id, + preload: [:parent, :actor] + ) + end + + @spec administrator_members_for_group_query(integer | String.t()) :: Ecto.Query.t() + defp administrator_members_for_group_query(group_id) do + from( + m in Member, + where: m.parent_id == ^group_id and m.role in ^@administrator_roles, + preload: [:actor] + ) + end + + @spec administrator_members_for_actor_query(integer | String.t()) :: Ecto.Query.t() + defp administrator_members_for_actor_query(actor_id) do + from( + m in Member, + where: m.actor_id == ^actor_id and m.role in ^@administrator_roles, + select: m.parent_id + ) + end + + @spec group_ids_where_last_administrator_query(integer | String.t()) :: Ecto.Query.t() + defp group_ids_where_last_administrator_query(actor_id) do + from( + m in Member, + where: m.role in ^@administrator_roles, + join: m2 in subquery(administrator_members_for_actor_query(actor_id)), + on: m.parent_id == m2.parent_id, + group_by: m.parent_id, + select: m.parent_id, + having: count(m.actor_id) == 1 + ) + end + + @spec follower_by_url(String.t()) :: Ecto.Query.t() + defp follower_by_url(url) do + from( + f in Follower, + where: f.url == ^url, + preload: [:actor, :target_actor] + ) + end + + @spec follower_by_followed_and_following_query(integer | String.t(), integer | String.t()) :: + Ecto.Query.t() + defp follower_by_followed_and_following_query(followed_id, follower_id) do + from( + f in Follower, + where: f.target_actor_id == ^followed_id and f.actor_id == ^follower_id + ) + end + + @spec followers_for_actor_query(integer | String.t()) :: Ecto.Query.t() + defp followers_for_actor_query(actor_id) do + from( + a in Actor, + join: f in Follower, + on: a.id == f.actor_id, + where: f.target_actor_id == ^actor_id + ) + end + + @spec followings_for_actor_query(integer | String.t()) :: Ecto.Query.t() + defp followings_for_actor_query(actor_id) do + from( + a in Actor, + join: f in Follower, + on: a.id == f.target_actor_id, + where: f.actor_id == ^actor_id + ) end @spec filter_local(Ecto.Query.t()) :: Ecto.Query.t() @@ -362,6 +932,11 @@ defmodule Mobilizon.Actors do from(a in query, where: is_nil(a.domain)) end + @spec filter_external(Ecto.Query.t()) :: Ecto.Query.t() + defp filter_external(query) do + from(a in query, where: not is_nil(a.domain)) + end + @spec filter_by_type(Ecto.Query.t(), ActorType.t()) :: Ecto.Query.t() defp filter_by_type(query, type) when type in [:Person, :Group] do from(a in query, where: a.type == ^type) @@ -386,684 +961,4 @@ defmodule Mobilizon.Actors do @spec preload_followers(Actor.t(), boolean) :: Actor.t() defp preload_followers(actor, true), do: Repo.preload(actor, [:followers]) defp preload_followers(actor, false), do: actor - - ##### TODO: continue refactoring from here ##### - - @doc """ - Returns the groups an actor is member of - """ - @spec get_groups_member_of(struct()) :: list() - def get_groups_member_of(%Actor{id: actor_id}) do - Repo.all( - from( - a in Actor, - join: m in Member, - on: a.id == m.parent_id, - where: m.actor_id == ^actor_id - ) - ) - end - - @doc """ - Returns the members for a group actor - """ - @spec get_members_for_group(struct()) :: list() - def get_members_for_group(%Actor{id: actor_id}) do - Repo.all( - from( - a in Actor, - join: m in Member, - on: a.id == m.actor_id, - where: m.parent_id == ^actor_id - ) - ) - end - - @doc """ - Creates a group. - - ## Examples - - iex> create_group(%{name: "group name"}) - {:ok, %Mobilizon.Actors.Actor{}} - - iex> create_group(%{name: nil}) - {:error, %Ecto.Changeset{}} - - """ - def create_group(attrs \\ %{}) do - %Actor{} - |> Actor.group_creation(attrs) - |> Repo.insert() - end - - @doc """ - Delete a group - """ - def delete_group!(%Actor{type: :Group} = group) do - Repo.delete!(group) - end - - @doc """ - List the groups - """ - @spec list_groups(number(), number()) :: list(Actor.t()) - def list_groups(page \\ nil, limit \\ nil) do - Repo.all( - from( - a in Actor, - where: a.type == ^:Group, - where: a.visibility in [^:public, ^:unlisted] - ) - |> Page.paginate(page, limit) - ) - end - - @doc """ - Get a group by its actor id - """ - def get_group_by_actor_id(actor_id) do - case Repo.get_by(Actor, id: actor_id, type: :Group) do - nil -> {:error, :group_not_found} - actor -> {:ok, actor} - end - end - - @doc """ - Create a new person actor - """ - @spec new_person(map()) :: {:ok, Actor.t()} | any - def new_person(args) do - args = Map.put(args, :keys, Crypto.generate_rsa_2048_private_key()) - - with {:ok, %Actor{} = person} <- - %Actor{} - |> Actor.registration_changeset(args) - |> Repo.insert() do - Mobilizon.Events.create_feed_token(%{"user_id" => args["user_id"], "actor_id" => person.id}) - {:ok, person} - end - end - - @doc """ - Register a new bot actor. - """ - @spec register_bot_account(map()) :: Actor.t() - def register_bot_account(%{name: name, summary: summary}) do - actor = - Mobilizon.Actors.Actor.registration_changeset(%Mobilizon.Actors.Actor{}, %{ - preferred_username: name, - domain: nil, - keys: Crypto.generate_rsa_2048_private_key(), - summary: summary, - type: :Service - }) - - try do - Repo.insert!(actor) - rescue - e in Ecto.InvalidChangesetError -> - {:error, e.changeset} - end - end - - def get_or_create_service_actor_by_url(url, preferred_username \\ "relay") do - case get_actor_by_url(url) do - {:ok, %Actor{} = actor} -> - {:ok, actor} - - _ -> - %{url: url, preferred_username: preferred_username} - |> Actor.relay_creation_changeset() - |> Repo.insert() - end - end - - @doc """ - Gets a single member of an actor (for example a group) - """ - def get_member(actor_id, parent_id) do - case Repo.get_by(Member, actor_id: actor_id, parent_id: parent_id) do - nil -> {:error, :member_not_found} - member -> {:ok, member} - end - end - - @doc """ - Gets a single member. - - Raises `Ecto.NoResultsError` if the Member does not exist. - - ## Examples - - iex> get_member!(123) - %Mobilizon.Actors.Member{} - - iex> get_member!(456) - ** (Ecto.NoResultsError) - - """ - def get_member!(id), do: Repo.get!(Member, id) - - @doc """ - Creates a member. - - ## Examples - - iex> create_member(%{actor: %Actor{}}) - {:ok, %Mobilizon.Actors.Member{}} - - iex> create_member(%{actor: nil}) - {:error, %Ecto.Changeset{}} - - """ - def create_member(attrs \\ %{}) do - with {:ok, %Member{} = member} <- - %Member{} - |> Member.changeset(attrs) - |> Repo.insert() do - {:ok, Repo.preload(member, [:actor, :parent])} - end - end - - @doc """ - Updates a member. - - ## Examples - - iex> update_member(%Member{}, %{role: 3}) - {:ok, %Mobilizon.Actors.Member{}} - - iex> update_member(%Member{}, %{role: nil}) - {:error, %Ecto.Changeset{}} - - """ - def update_member(%Member{} = member, attrs) do - member - |> Member.changeset(attrs) - |> Repo.update() - end - - @doc """ - Deletes a Member. - - ## Examples - - iex> delete_member(%Member{}) - {:ok, %Mobilizon.Actors.Member{}} - - iex> delete_member(%Member{}) - {:error, %Ecto.Changeset{}} - - """ - def delete_member(%Member{} = member) do - Repo.delete(member) - end - - @doc """ - Returns the list of administrator members for a group. - """ - def list_administrator_members_for_group(id, page \\ nil, limit \\ nil) do - Repo.all( - from( - m in Member, - where: m.parent_id == ^id and (m.role == ^:creator or m.role == ^:administrator), - preload: [:actor] - ) - |> Page.paginate(page, limit) - ) - end - - @doc """ - Get all group ids where the actor_id is the last administrator - """ - def list_group_id_where_last_administrator(actor_id) do - in_query = - from( - m in Member, - where: m.actor_id == ^actor_id and (m.role == ^:creator or m.role == ^:administrator), - select: m.parent_id - ) - - Repo.all( - from( - m in Member, - where: m.role == ^:creator or m.role == ^:administrator, - join: m2 in subquery(in_query), - on: m.parent_id == m2.parent_id, - group_by: m.parent_id, - select: m.parent_id, - having: count(m.actor_id) == 1 - ) - ) - end - - @doc """ - Returns the memberships for an actor - """ - @spec groups_memberships_for_actor(Actor.t()) :: list(Member.t()) - def groups_memberships_for_actor(%Actor{id: id} = _actor) do - Repo.all( - from( - m in Member, - where: m.actor_id == ^id, - preload: [:parent] - ) - ) - end - - @doc """ - Returns the memberships for a group - """ - @spec memberships_for_group(Actor.t()) :: list(Member.t()) - def memberships_for_group(%Actor{type: :Group, id: id} = _group) do - Repo.all( - from( - m in Member, - where: m.parent_id == ^id, - preload: [:parent, :actor] - ) - ) - end - - alias Mobilizon.Actors.Bot - - @doc """ - Returns the list of bots. - - ## Examples - - iex> list_bots() - [%Mobilizon.Actors.Bot{}] - - """ - def list_bots do - Repo.all(Bot) - end - - @doc """ - Gets a single bot. - - Raises `Ecto.NoResultsError` if the Bot does not exist. - - ## Examples - - iex> get_bot!(123) - %Mobilizon.Actors.Bot{} - - iex> get_bot!(456) - ** (Ecto.NoResultsError) - - """ - def get_bot!(id), do: Repo.get!(Bot, id) - - @doc """ - Get the bot associated to an actor - """ - @spec get_bot_by_actor(Actor.t()) :: Bot.t() - def get_bot_by_actor(%Actor{} = actor) do - Repo.get_by!(Bot, actor_id: actor.id) - end - - @doc """ - Creates a bot. - - ## Examples - - iex> create_bot(%{source: "toto"}) - {:ok, %Mobilizon.Actors.Bot{}} - - iex> create_bot(%{source: nil}) - {:error, %Ecto.Changeset{}} - - """ - def create_bot(attrs \\ %{}) do - %Bot{} - |> Bot.changeset(attrs) - |> Repo.insert() - end - - @doc """ - Updates a bot. - - ## Examples - - iex> update_bot(%Bot{}, %{source: "new"}) - {:ok, %Mobilizon.Actors.Bot{}} - - iex> update_bot(%Bot{}, %{source: nil}) - {:error, %Ecto.Changeset{}} - - """ - def update_bot(%Bot{} = bot, attrs) do - bot - |> Bot.changeset(attrs) - |> Repo.update() - end - - @doc """ - Deletes a Bot. - - ## Examples - - iex> delete_bot(%Bot{}) - {:ok, %Mobilizon.Actors.Bot{}} - - iex> delete_bot(%Bot{}) - {:error, %Ecto.Changeset{}} - - """ - def delete_bot(%Bot{} = bot) do - Repo.delete(bot) - end - - @doc """ - Returns an `%Ecto.Changeset{}` for tracking bot changes. - - ## Examples - - iex> change_bot(%Bot{}) - %Ecto.Changeset{data: %Mobilizon.Actors.Bot{}} - - """ - def change_bot(%Bot{} = bot) do - Bot.changeset(bot, %{}) - end - - @doc """ - Gets a single follower. - - Raises `Ecto.NoResultsError` if the Follower does not exist. - - ## Examples - - iex> get_follower!(123) - %Mobilizon.Actors.Follower{} - - iex> get_follower!(456) - ** (Ecto.NoResultsError) - - """ - def get_follower!(id) do - Repo.get!(Follower, id) - |> Repo.preload([:actor, :target_actor]) - end - - @doc """ - Get a follow by the followed actor and following actor - """ - @spec get_follower(Actor.t(), Actor.t()) :: Follower.t() - def get_follower(%Actor{id: followed_id}, %Actor{id: follower_id}) do - Repo.one( - from(f in Follower, where: f.target_actor_id == ^followed_id and f.actor_id == ^follower_id) - ) - end - - @doc """ - Get a follow by the followed actor and following actor - """ - @spec get_follow_by_url(String.t()) :: Follower.t() - def get_follow_by_url(url) do - Repo.one( - from(f in Follower, - where: f.url == ^url, - preload: [:actor, :target_actor] - ) - ) - end - - @doc """ - Get followers from an actor - - If actor A and C both follow actor B, actor B's followers are A and C - """ - @spec get_followers(struct(), number(), number()) :: map() - def get_followers(%Actor{id: actor_id} = _actor, page \\ nil, limit \\ nil) do - query = - from( - a in Actor, - join: f in Follower, - on: a.id == f.actor_id, - where: f.target_actor_id == ^actor_id - ) - - total = Task.async(fn -> Repo.aggregate(query, :count, :id) end) - elements = Task.async(fn -> Repo.all(Page.paginate(query, page, limit)) end) - - %{total: Task.await(total), elements: Task.await(elements)} - end - - @spec get_full_followers(struct()) :: list() - def get_full_followers(%Actor{} = actor) do - actor - |> get_full_followers_query() - |> Repo.all() - end - - @spec get_full_external_followers(struct()) :: list() - def get_full_external_followers(%Actor{} = actor) do - actor - |> get_full_followers_query() - |> where([a], not is_nil(a.domain)) - |> Repo.all() - end - - @doc """ - Get followings from an actor - - If actor A follows actor B and C, actor A's followings are B and B - """ - @spec get_followings(struct(), number(), number()) :: list() - def get_followings(%Actor{id: actor_id} = _actor, page \\ nil, limit \\ nil) do - query = - from( - a in Actor, - join: f in Follower, - on: a.id == f.target_actor_id, - where: f.actor_id == ^actor_id - ) - - total = Task.async(fn -> Repo.aggregate(query, :count, :id) end) - elements = Task.async(fn -> Repo.all(Page.paginate(query, page, limit)) end) - - %{total: Task.await(total), elements: Task.await(elements)} - end - - @spec get_full_followings(struct()) :: list() - def get_full_followings(%Actor{id: actor_id} = _actor) do - Repo.all( - from( - a in Actor, - join: f in Follower, - on: a.id == f.target_actor_id, - where: f.actor_id == ^actor_id - ) - ) - end - - defp get_full_followers_query(%Actor{id: actor_id} = _actor) do - from( - a in Actor, - join: f in Follower, - on: a.id == f.actor_id, - where: f.target_actor_id == ^actor_id - ) - end - - @doc """ - Creates a follower. - - ## Examples - - iex> create_follower(%{actor: %Actor{}}) - {:ok, %Mobilizon.Actors.Follower{}} - - iex> create_follower(%{actor: nil}) - {:error, %Ecto.Changeset{}} - - """ - def create_follower(attrs \\ %{}) do - with {:ok, %Follower{} = follower} <- - %Follower{} - |> Follower.changeset(attrs) - |> Repo.insert() do - {:ok, Repo.preload(follower, [:actor, :target_actor])} - end - end - - @doc """ - Updates a follower. - - ## Examples - - iex> update_follower(Follower{}, %{approved: true}) - {:ok, %Mobilizon.Actors.Follower{}} - - iex> update_follower(Follower{}, %{approved: nil}) - {:error, %Ecto.Changeset{}} - - """ - def update_follower(%Follower{} = follower, attrs) do - follower - |> Follower.changeset(attrs) - |> Repo.update() - end - - @doc """ - Deletes a Follower. - - ## Examples - - iex> delete_follower(Follower{}) - {:ok, %Mobilizon.Actors.Follower{}} - - iex> delete_follower(Follower{}) - {:error, %Ecto.Changeset{}} - - """ - def delete_follower(%Follower{} = follower) do - Repo.delete(follower) - end - - @doc """ - Delete a follower by followed and follower actors - - ## Examples - - iex> delete_follower(%Actor{}, %Actor{}) - {:ok, %Mobilizon.Actors.Follower{}} - - iex> delete_follower(%Actor{}, %Actor{}) - {:error, %Ecto.Changeset{}} - - """ - @spec delete_follower(Actor.t(), Actor.t()) :: - {:ok, Follower.t()} | {:error, Ecto.Changeset.t()} - def delete_follower(%Actor{} = followed, %Actor{} = follower) do - get_follower(followed, follower) |> Repo.delete() - end - - @doc """ - Make an actor follow another - """ - @spec follow(struct(), struct(), boolean()) :: Follower.t() | {:error, String.t()} - def follow(%Actor{} = followed, %Actor{} = follower, url \\ nil, approved \\ true) do - with {:suspended, false} <- {:suspended, followed.suspended}, - # Check if followed has blocked follower - {:already_following, false} <- {:already_following, following?(follower, followed)} do - do_follow(follower, followed, approved, url) - else - {:already_following, %Follower{}} -> - {:error, :already_following, - "Could not follow actor: you are already following #{followed.preferred_username}"} - - {:suspended, _} -> - {:error, :suspended, - "Could not follow actor: #{followed.preferred_username} has been suspended"} - end - end - - @doc """ - Unfollow an actor (remove a `Mobilizon.Actors.Follower`) - """ - @spec unfollow(struct(), struct()) :: {:ok, Follower.t()} | {:error, Ecto.Changeset.t()} - def unfollow(%Actor{} = followed, %Actor{} = follower) do - case {:already_following, following?(follower, followed)} do - {:already_following, %Follower{} = follow} -> - delete_follower(follow) - - {:already_following, false} -> - {:error, "Could not unfollow actor: you are not following #{followed.preferred_username}"} - end - end - - @spec do_follow(struct(), struct(), boolean(), String.t()) :: - {:ok, Follower.t()} | {:error, Ecto.Changeset.t()} - defp do_follow(%Actor{} = follower, %Actor{} = followed, approved, url) do - Logger.info( - "Making #{follower.preferred_username} follow #{followed.preferred_username} (approved: #{ - approved - })" - ) - - create_follower(%{ - "actor_id" => follower.id, - "target_actor_id" => followed.id, - "approved" => approved, - "url" => url - }) - end - - @doc """ - Returns whether an actor is following another - """ - @spec following?(struct(), struct()) :: Follower.t() | false - def following?( - %Actor{} = follower_actor, - %Actor{} = followed_actor - ) do - case get_follower(followed_actor, follower_actor) do - nil -> false - %Follower{} = follow -> follow - end - end - - defp remove_banner(%Actor{banner: nil} = actor), do: {:ok, actor} - - defp remove_banner(%Actor{banner: %File{url: url}} = actor) do - safe_remove_file(url, actor) - end - - defp remove_avatar(%Actor{avatar: nil} = actor), do: {:ok, actor} - - defp remove_avatar(%Actor{avatar: %File{url: url}} = actor) do - safe_remove_file(url, actor) - end - - defp safe_remove_file(url, %Actor{} = actor) do - case MobilizonWeb.Upload.remove(url) do - {:ok, _value} -> - {:ok, actor} - - {:error, error} -> - Logger.error("Error while removing an upload file") - Logger.debug(inspect(error)) - {:ok, actor} - end - end - - @spec delete_files_if_media_changed(Ecto.Changeset.t()) :: Ecto.Changeset.t() - defp delete_files_if_media_changed(%Ecto.Changeset{changes: changes, data: data} = changeset) do - Enum.each([:avatar, :banner], fn key -> - if Map.has_key?(changes, key) do - with %Ecto.Changeset{changes: %{url: new_url}} <- changes[key], - %{url: old_url} <- data |> Map.from_struct() |> Map.get(key), - false <- new_url == old_url do - MobilizonWeb.Upload.remove(old_url) - end - end - end) - - changeset - end end diff --git a/lib/mobilizon/actors/follower.ex b/lib/mobilizon/actors/follower.ex index b8497e966..0164d39a3 100644 --- a/lib/mobilizon/actors/follower.ex +++ b/lib/mobilizon/actors/follower.ex @@ -44,8 +44,11 @@ defmodule Mobilizon.Actors.Follower do @spec ensure_url(Ecto.Changeset.t()) :: Ecto.Changeset.t() defp ensure_url(%Ecto.Changeset{data: %__MODULE__{url: nil}} = changeset) do case fetch_change(changeset, :url) do - {:ok, _url} -> changeset - :error -> generate_url(changeset) + {:ok, _url} -> + changeset + + :error -> + generate_url(changeset) end end diff --git a/lib/mobilizon/addresses/addresses.ex b/lib/mobilizon/addresses/addresses.ex index 7c8ac7b42..fdef1ca7b 100644 --- a/lib/mobilizon/addresses/addresses.ex +++ b/lib/mobilizon/addresses/addresses.ex @@ -86,8 +86,11 @@ defmodule Mobilizon.Addresses do |> filter_by_contry(Keyword.get(options, :country)) case Keyword.get(options, :single, false) do - true -> Repo.one(query) - false -> Repo.all(query) + true -> + Repo.one(query) + + false -> + Repo.all(query) end end diff --git a/lib/mobilizon/media/media.ex b/lib/mobilizon/media/media.ex index 73533934d..c19234438 100644 --- a/lib/mobilizon/media/media.ex +++ b/lib/mobilizon/media/media.ex @@ -75,8 +75,11 @@ defmodule Mobilizon.Media do |> Repo.transaction() case transaction do - {:ok, %{picture: %Picture{} = picture}} -> {:ok, picture} - {:error, :remove, error, _} -> {:error, error} + {:ok, %{picture: %Picture{} = picture}} -> + {:ok, picture} + + {:error, :remove, error, _} -> + {:error, error} end end diff --git a/lib/mobilizon/users/user.ex b/lib/mobilizon/users/user.ex index eb8895108..b9da2002c 100644 --- a/lib/mobilizon/users/user.ex +++ b/lib/mobilizon/users/user.ex @@ -154,8 +154,11 @@ defmodule Mobilizon.Users.User do case changeset do %Ecto.Changeset{valid?: true, changes: %{email: email}} -> case EmailChecker.valid?(email) do - false -> add_error(changeset, :email, "Email doesn't fit required format") - true -> changeset + false -> + add_error(changeset, :email, "Email doesn't fit required format") + + true -> + changeset end _ -> diff --git a/lib/mobilizon/users/users.ex b/lib/mobilizon/users/users.ex index 4fe5e8068..1c3ddabb9 100644 --- a/lib/mobilizon/users/users.ex +++ b/lib/mobilizon/users/users.ex @@ -59,8 +59,11 @@ defmodule Mobilizon.Users do query = user_by_email_query(email, activated) case Repo.one(query) do - nil -> {:error, :user_not_found} - user -> {:ok, user} + nil -> + {:error, :user_not_found} + + user -> + {:ok, user} end end @@ -147,8 +150,11 @@ defmodule Mobilizon.Users do case actor do nil -> case get_actors_for_user(user) do - [] -> nil - actors -> hd(actors) + [] -> + nil + + actors -> + hd(actors) end actor -> diff --git a/lib/mobilizon_web/api/follows.ex b/lib/mobilizon_web/api/follows.ex index a96b5c609..4cba7a5ee 100644 --- a/lib/mobilizon_web/api/follows.ex +++ b/lib/mobilizon_web/api/follows.ex @@ -32,7 +32,7 @@ defmodule MobilizonWeb.API.Follows do def accept(%Actor{} = follower, %Actor{} = followed) do with %Follower{approved: false, id: follow_id, url: follow_url} = follow <- - Actors.following?(follower, followed), + Actors.is_following(follower, followed), activity_follow_url <- "#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follow_id}", data <- ActivityPub.Utils.make_follow_data(followed, follower, follow_url), diff --git a/lib/mobilizon_web/resolvers/member.ex b/lib/mobilizon_web/resolvers/member.ex index b12430644..1cfd320a2 100644 --- a/lib/mobilizon_web/resolvers/member.ex +++ b/lib/mobilizon_web/resolvers/member.ex @@ -9,7 +9,7 @@ defmodule MobilizonWeb.Resolvers.Member do Find members for group """ def find_members_for_group(%Actor{} = actor, _args, _resolution) do - members = Actors.memberships_for_group(actor) + members = Actors.list_members_for_group(actor) {:ok, members} end end diff --git a/lib/mobilizon_web/resolvers/person.ex b/lib/mobilizon_web/resolvers/person.ex index 8f751b7e9..7c39d1250 100644 --- a/lib/mobilizon_web/resolvers/person.ex +++ b/lib/mobilizon_web/resolvers/person.ex @@ -207,7 +207,7 @@ defmodule MobilizonWeb.Resolvers.Person do # We check that the actor is not the last administrator/creator of a group @spec last_admin_of_a_group?(integer()) :: boolean() defp last_admin_of_a_group?(actor_id) do - length(Actors.list_group_id_where_last_administrator(actor_id)) > 0 + length(Actors.list_group_ids_where_last_administrator(actor_id)) > 0 end @spec proxify_avatar(Actor.t()) :: Actor.t() diff --git a/lib/mobilizon_web/views/activity_pub/actor_view.ex b/lib/mobilizon_web/views/activity_pub/actor_view.ex index 00dbd0416..5ecf495c7 100644 --- a/lib/mobilizon_web/views/activity_pub/actor_view.ex +++ b/lib/mobilizon_web/views/activity_pub/actor_view.ex @@ -49,7 +49,7 @@ defmodule MobilizonWeb.ActivityPub.ActorView do def render("following.json", %{actor: actor, page: page}) do %{total: total, elements: following} = if Actor.is_public_visibility(actor), - do: Actors.get_followings(actor, page), + do: Actors.build_followings_for_actor(actor, page), else: @private_visibility_empty_collection following @@ -60,7 +60,7 @@ defmodule MobilizonWeb.ActivityPub.ActorView do def render("following.json", %{actor: actor}) do %{total: total, elements: following} = if Actor.is_public_visibility(actor), - do: Actors.get_followings(actor), + do: Actors.build_followings_for_actor(actor), else: @private_visibility_empty_collection %{ @@ -75,7 +75,7 @@ defmodule MobilizonWeb.ActivityPub.ActorView do def render("followers.json", %{actor: actor, page: page}) do %{total: total, elements: followers} = if Actor.is_public_visibility(actor), - do: Actors.get_followers(actor, page), + do: Actors.build_followers_for_actor(actor, page), else: @private_visibility_empty_collection followers @@ -86,7 +86,7 @@ defmodule MobilizonWeb.ActivityPub.ActorView do def render("followers.json", %{actor: actor}) do %{total: total, elements: followers} = if Actor.is_public_visibility(actor), - do: Actors.get_followers(actor), + do: Actors.build_followers_for_actor(actor), else: @private_visibility_empty_collection %{ diff --git a/lib/service/activity_pub/activity_pub.ex b/lib/service/activity_pub/activity_pub.ex index c0733c66e..507768dbb 100644 --- a/lib/service/activity_pub/activity_pub.ex +++ b/lib/service/activity_pub/activity_pub.ex @@ -551,7 +551,7 @@ defmodule Mobilizon.Service.ActivityPub do followers = if actor.followers_url in activity.recipients do - Actors.get_full_external_followers(actor) + Actors.list_external_followers_for_actor(actor) else [] end diff --git a/lib/service/activity_pub/relay.ex b/lib/service/activity_pub/relay.ex index 96e2bedb6..3333be71b 100644 --- a/lib/service/activity_pub/relay.ex +++ b/lib/service/activity_pub/relay.ex @@ -17,7 +17,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do def get_actor do with {:ok, %Actor{} = actor} <- - Actors.get_or_create_service_actor_by_url("#{MobilizonWeb.Endpoint.url()}/relay") do + Actors.get_or_create_actor_by_url("#{MobilizonWeb.Endpoint.url()}/relay") do actor end end diff --git a/lib/service/activity_pub/transmogrifier.ex b/lib/service/activity_pub/transmogrifier.ex index a94548aa5..41d81c246 100644 --- a/lib/service/activity_pub/transmogrifier.ex +++ b/lib/service/activity_pub/transmogrifier.ex @@ -642,7 +642,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do defp get_follow(follow_object) do with follow_object_id when not is_nil(follow_object_id) <- Utils.get_url(follow_object), {:not_found, %Follower{} = follow} <- - {:not_found, Actors.get_follow_by_url(follow_object_id)} do + {:not_found, Actors.get_follower_by_url(follow_object_id)} do {:ok, follow} else {:not_found, _err} -> diff --git a/test/mobilizon/actors/actors_test.exs b/test/mobilizon/actors/actors_test.exs index 7c211f472..df5a5755b 100644 --- a/test/mobilizon/actors/actors_test.exs +++ b/test/mobilizon/actors/actors_test.exs @@ -166,11 +166,11 @@ defmodule Mobilizon.ActorsTest do end end - test "test get_local_actor_by_username/1 returns local actors with similar usernames", %{ + test "test list_local_actor_by_username/1 returns local actors with similar usernames", %{ actor: actor } do actor2 = insert(:actor, preferred_username: "tcit") - [%Actor{id: actor_found_id} | tail] = Actors.get_local_actor_by_username("tcit") + [%Actor{id: actor_found_id} | tail] = Actors.list_local_actor_by_username("tcit") %Actor{id: actor2_found_id} = hd(tail) assert MapSet.new([actor_found_id, actor2_found_id]) == MapSet.new([actor.id, actor2.id]) end @@ -416,11 +416,6 @@ defmodule Mobilizon.ActorsTest do assert {:ok, %Bot{}} = Actors.delete_bot(bot) assert_raise Ecto.NoResultsError, fn -> Actors.get_bot!(bot.id) end end - - test "change_bot/1 returns a bot changeset" do - bot = insert(:bot) - assert %Ecto.Changeset{} = Actors.change_bot(bot) - end end describe "followers" do @@ -458,8 +453,8 @@ defmodule Mobilizon.ActorsTest do assert {:ok, %Follower{} = follower} = Actors.create_follower(valid_attrs) assert follower.approved == true - assert %{total: 1, elements: [target_actor]} = Actors.get_followings(actor) - assert %{total: 1, elements: [actor]} = Actors.get_followers(target_actor) + assert %{total: 1, elements: [target_actor]} = Actors.build_followings_for_actor(actor) + assert %{total: 1, elements: [actor]} = Actors.build_followers_for_actor(target_actor) end test "create_follower/1 with valid data but same actors fails to create a follower", %{ @@ -568,8 +563,8 @@ defmodule Mobilizon.ActorsTest do assert {:ok, %Member{} = member} = Actors.create_member(valid_attrs) assert member.role == :member - assert [group] = Actors.get_groups_member_of(actor) - assert [actor] = Actors.get_members_for_group(group) + assert [group] = Actors.list_groups_member_of(actor) + assert [actor] = Actors.list_members_for_group(group) end test "create_member/1 with valid data but same actors fails to create a member", %{ diff --git a/test/mobilizon/service/activity_pub/transmogrifier_test.exs b/test/mobilizon/service/activity_pub/transmogrifier_test.exs index 1c515c6b9..7eb340ae4 100644 --- a/test/mobilizon/service/activity_pub/transmogrifier_test.exs +++ b/test/mobilizon/service/activity_pub/transmogrifier_test.exs @@ -223,7 +223,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do assert data["id"] == "https://social.tcit.fr/users/tcit#follows/2" actor = Actors.get_actor_with_preload(actor.id) - assert Actors.following?(Actors.get_actor_by_url!(data["actor"], true), actor) + assert Actors.is_following(Actors.get_actor_by_url!(data["actor"], true), actor) end # test "it works for incoming follow requests from hubzilla" do @@ -240,7 +240,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do # assert data["actor"] == "https://hubzilla.example.org/channel/kaniini" # assert data["type"] == "Follow" # assert data["id"] == "https://hubzilla.example.org/channel/kaniini#follows/2" - # assert User.following?(User.get_by_ap_id(data["actor"]), user) + # assert User.is_following(User.get_by_ap_id(data["actor"]), user) # end # test "it works for incoming likes" do @@ -498,7 +498,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do assert data["actor"] == "https://social.tcit.fr/users/tcit" {:ok, followed} = Actors.get_actor_by_url(data["actor"]) - refute Actors.following?(followed, actor) + refute Actors.is_following(followed, actor) end # test "it works for incoming blocks" do @@ -581,10 +581,10 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do follower = insert(:actor) followed = insert(:actor) - refute Actors.following?(follower, followed) + refute Actors.is_following(follower, followed) {:ok, follow_activity, _} = ActivityPub.follow(follower, followed) - assert Actors.following?(follower, followed) + assert Actors.is_following(follower, followed) accept_data = File.read!("test/fixtures/mastodon-accept-activity.json") @@ -605,7 +605,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do {:ok, follower} = Actors.get_actor_by_url(follower.url) - assert Actors.following?(follower, followed) + assert Actors.is_following(follower, followed) end test "it works for incoming accepts which are referenced by IRI only" do @@ -627,7 +627,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do {:ok, follower} = Actors.get_actor_by_url(follower.url) - assert Actors.following?(follower, followed) + assert Actors.is_following(follower, followed) end test "it fails for incoming accepts which cannot be correlated" do @@ -646,7 +646,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do {:ok, follower} = Actors.get_actor_by_url(follower.url) - refute Actors.following?(follower, followed) + refute Actors.is_following(follower, followed) end test "it fails for incoming rejects which cannot be correlated" do @@ -665,7 +665,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do {:ok, follower} = Actors.get_actor_by_url(follower.url) - refute Actors.following?(follower, followed) + refute Actors.is_following(follower, followed) end test "it works for incoming rejects which are referenced by IRI only" do @@ -674,7 +674,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do {:ok, follow_activity, _} = ActivityPub.follow(follower, followed) - assert Actors.following?(follower, followed) + assert Actors.is_following(follower, followed) reject_data = File.read!("test/fixtures/mastodon-reject-activity.json") @@ -684,7 +684,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do {:ok, %Activity{data: _}, _} = Transmogrifier.handle_incoming(reject_data) - refute Actors.following?(follower, followed) + refute Actors.is_following(follower, followed) end test "it rejects activities without a valid ID" do diff --git a/test/tasks/relay_test.exs b/test/tasks/relay_test.exs index 22f675806..c93e8ad47 100644 --- a/test/tasks/relay_test.exs +++ b/test/tasks/relay_test.exs @@ -22,7 +22,7 @@ defmodule Mix.Tasks.Mobilizon.RelayTest do {:ok, target_actor} = Actors.get_actor_by_url(target_instance) refute is_nil(target_actor.domain) - assert Actors.following?(local_actor, target_actor) + assert Actors.is_following(local_actor, target_actor) end end end @@ -36,11 +36,11 @@ defmodule Mix.Tasks.Mobilizon.RelayTest do %Actor{} = local_actor = Relay.get_actor() {:ok, %Actor{} = target_actor} = Actors.get_actor_by_url(target_instance) - assert %Follower{} = Actors.following?(local_actor, target_actor) + assert %Follower{} = Actors.is_following(local_actor, target_actor) Mix.Tasks.Mobilizon.Relay.run(["unfollow", target_instance]) - refute Actors.following?(local_actor, target_actor) + refute Actors.is_following(local_actor, target_actor) end end end From e358dcce772ac822f3818b23c63e0343dd6f5682 Mon Sep 17 00:00:00 2001 From: miffigriffy Date: Fri, 13 Sep 2019 01:01:17 +0200 Subject: [PATCH 17/29] Refactoring of Events context --- lib/mobilizon/events/comment.ex | 86 ++++---- lib/mobilizon/events/event.ex | 196 +++++++++--------- lib/mobilizon/events/event_offer.ex | 19 ++ lib/mobilizon/events/event_options.ex | 77 +++---- .../events/event_participation_condition.ex | 19 ++ lib/mobilizon/events/events.ex | 77 ++++++- lib/mobilizon/events/feed_token.ex | 23 +- lib/mobilizon/events/participant.ex | 110 +++++----- lib/mobilizon/events/session.ex | 61 +++--- lib/mobilizon/events/tag.ex | 36 ++-- lib/mobilizon/events/tag/title_slug.ex | 44 ++-- lib/mobilizon/events/tag_relations.ex | 41 ++-- lib/mobilizon/events/track.ex | 23 +- lib/mobilizon_web/resolvers/event.ex | 2 +- lib/service/activity_pub/activity_pub.ex | 3 +- mix.exs | 12 +- .../20190103150805_fix_event_visibility.exs | 18 +- ...plit_event_visibility_and_join_options.exs | 26 +-- ...07134142_move_participant_role_to_enum.exs | 8 +- 19 files changed, 528 insertions(+), 353 deletions(-) create mode 100644 lib/mobilizon/events/event_offer.ex create mode 100644 lib/mobilizon/events/event_participation_condition.ex diff --git a/lib/mobilizon/events/comment.ex b/lib/mobilizon/events/comment.ex index 5b2c038ee..4caf24219 100644 --- a/lib/mobilizon/events/comment.ex +++ b/lib/mobilizon/events/comment.ex @@ -1,33 +1,40 @@ -import EctoEnum - -defenum(Mobilizon.Events.CommentVisibilityEnum, :comment_visibility_type, [ - :public, - :unlisted, - :private, - :moderated, - :invite -]) - defmodule Mobilizon.Events.Comment do @moduledoc """ - An actor comment (for instance on an event or on a group) + Represents an actor comment (for instance on an event or on a group). """ use Ecto.Schema + import Ecto.Changeset - alias Mobilizon.Events.Event alias Mobilizon.Actors.Actor - alias Mobilizon.Events.Comment - alias MobilizonWeb.Router.Helpers, as: Routes - alias MobilizonWeb.Endpoint + alias Mobilizon.Config + alias Mobilizon.Events.{Comment, CommentVisibility, Event} + + @type t :: %__MODULE__{ + text: String.t(), + url: String.t(), + local: boolean, + visibility: CommentVisibility.t(), + uuid: Ecto.UUID.t(), + actor: Actor.t(), + attributed_to: Actor.t(), + event: Event.t(), + in_reply_to_comment: t, + origin_comment: t + } + + @required_attrs [:text, :actor_id, :url] + @optional_attrs [:event_id, :in_reply_to_comment_id, :origin_comment_id, :attributed_to_id] + @attrs @required_attrs ++ @optional_attrs schema "comments" do field(:text, :string) field(:url, :string) field(:local, :boolean, default: true) - field(:visibility, Mobilizon.Events.CommentVisibilityEnum, default: :public) + field(:visibility, CommentVisibility, default: :public) field(:uuid, Ecto.UUID) + belongs_to(:actor, Actor, foreign_key: :actor_id) belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id) belongs_to(:event, Event, foreign_key: :event_id) @@ -37,38 +44,27 @@ defmodule Mobilizon.Events.Comment do timestamps(type: :utc_datetime) end - @doc false - def changeset(comment, attrs) do - uuid = - if Map.has_key?(attrs, "uuid"), - do: attrs["uuid"], - else: Ecto.UUID.generate() - - # TODO : really change me right away - url = - if Map.has_key?(attrs, "url"), - do: attrs["url"], - else: Routes.page_url(Endpoint, :comment, uuid) - - comment - |> Ecto.Changeset.cast(attrs, [ - :url, - :text, - :actor_id, - :event_id, - :in_reply_to_comment_id, - :origin_comment_id, - :attributed_to_id - ]) - |> put_change(:uuid, uuid) - |> put_change(:url, url) - |> validate_required([:text, :actor_id, :url]) - end - @doc """ - Returns the id of the first comment in the conversation + Returns the id of the first comment in the conversation. """ + @spec get_thread_id(t) :: integer def get_thread_id(%Comment{id: id, origin_comment_id: origin_comment_id}) do origin_comment_id || id end + + @doc false + @spec changeset(t, map) :: Ecto.Changeset.t() + def changeset(%Comment{} = comment, attrs) do + uuid = attrs["uuid"] || Ecto.UUID.generate() + url = attrs["url"] || generate_url(uuid) + + comment + |> cast(attrs, @attrs) + |> put_change(:uuid, uuid) + |> put_change(:url, url) + |> validate_required(@required_attrs) + end + + @spec generate_url(String.t()) :: String.t() + defp generate_url(uuid), do: "#{Config.instance_hostname()}/comments/#{uuid}" end diff --git a/lib/mobilizon/events/event.ex b/lib/mobilizon/events/event.ex index 52d1c765d..fa5f5ad61 100644 --- a/lib/mobilizon/events/event.ex +++ b/lib/mobilizon/events/event.ex @@ -1,43 +1,88 @@ -import EctoEnum - -defenum(Mobilizon.Events.EventVisibilityEnum, :event_visibility_type, [ - :public, - :unlisted, - :restricted, - :private -]) - -defenum(Mobilizon.Events.JoinOptionsEnum, :event_join_options_type, [ - :free, - :restricted, - :invite -]) - -defenum(Mobilizon.Events.EventStatusEnum, :event_status_type, [ - :tentative, - :confirmed, - :cancelled -]) - -defenum(Mobilizon.Event.EventCategoryEnum, :event_category_type, [ - :business, - :conference, - :birthday, - :demonstration, - :meeting -]) - defmodule Mobilizon.Events.Event do @moduledoc """ - Represents an event + Represents an event. """ + use Ecto.Schema + import Ecto.Changeset - alias Mobilizon.Events.{Event, Participant, Tag, Session, Track} + alias Mobilizon.Actors.Actor - alias Mobilizon.Media.Picture alias Mobilizon.Addresses.Address + alias Mobilizon.Events.{ + Event, + EventOptions, + EventStatus, + EventVisibility, + JoinOptions, + Participant, + Tag, + Session, + Track + } + + alias Mobilizon.Media.Picture + + @type t :: %__MODULE__{ + url: String.t(), + local: boolean, + begins_on: DateTime.t(), + slug: String.t(), + description: String.t(), + ends_on: DateTime.t(), + title: String.t(), + status: EventStatus.t(), + visibility: EventVisibility.t(), + join_options: JoinOptions.t(), + publish_at: DateTime.t(), + uuid: Ecto.UUID.t(), + online_address: String.t(), + phone_address: String.t(), + category: String.t(), + options: EventOptions.t(), + organizer_actor: Actor.t(), + attributed_to: Actor.t(), + physical_address: Address.t(), + picture: Picture.t(), + tracks: [Track.t()], + sessions: [Session.t()], + tags: [Tag.t()], + participants: [Actor.t()] + } + + @required_attrs [:title, :begins_on, :organizer_actor_id, :url, :uuid] + @optional_attrs [ + :slug, + :description, + :ends_on, + :category, + :status, + :visibility, + :publish_at, + :online_address, + :phone_address, + :picture_id, + :physical_address_id + ] + @attrs @required_attrs ++ @optional_attrs + + @update_required_attrs @required_attrs + @update_optional_attrs [ + :slug, + :description, + :ends_on, + :category, + :status, + :visibility, + :publish_at, + :online_address, + :phone_address, + :picture_id, + :physical_address_id + ] + @update_attrs @update_required_attrs ++ @update_optional_attrs + schema "events" do field(:url, :string) field(:local, :boolean, default: true) @@ -46,96 +91,59 @@ defmodule Mobilizon.Events.Event do field(:description, :string) field(:ends_on, :utc_datetime) field(:title, :string) - field(:status, Mobilizon.Events.EventStatusEnum, default: :confirmed) - field(:visibility, Mobilizon.Events.EventVisibilityEnum, default: :public) - field(:join_options, Mobilizon.Events.JoinOptionsEnum, default: :free) + field(:status, EventStatus, default: :confirmed) + field(:visibility, EventVisibility, default: :public) + field(:join_options, JoinOptions, default: :free) field(:publish_at, :utc_datetime) field(:uuid, Ecto.UUID, default: Ecto.UUID.generate()) field(:online_address, :string) field(:phone_address, :string) field(:category, :string) - embeds_one(:options, Mobilizon.Events.EventOptions, on_replace: :update) + + embeds_one(:options, EventOptions, on_replace: :update) belongs_to(:organizer_actor, Actor, foreign_key: :organizer_actor_id) belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id) - many_to_many(:tags, Tag, join_through: "events_tags", on_replace: :delete) - many_to_many(:participants, Actor, join_through: Participant) - has_many(:tracks, Track) - has_many(:sessions, Session) belongs_to(:physical_address, Address) belongs_to(:picture, Picture) + has_many(:tracks, Track) + has_many(:sessions, Session) + many_to_many(:tags, Tag, join_through: "events_tags", on_replace: :delete) + many_to_many(:participants, Actor, join_through: Participant) timestamps(type: :utc_datetime) end @doc false + @spec changeset(t, map) :: Ecto.Changeset.t() def changeset(%Event{} = event, attrs) do event - |> Ecto.Changeset.cast(attrs, [ - :title, - :slug, - :description, - :url, - :begins_on, - :ends_on, - :organizer_actor_id, - :category, - :status, - :visibility, - :publish_at, - :online_address, - :phone_address, - :uuid, - :picture_id, - :physical_address_id - ]) + |> cast(attrs, @attrs) |> cast_embed(:options) - |> validate_required([ - :title, - :begins_on, - :organizer_actor_id, - :url, - :uuid - ]) + |> validate_required(@required_attrs) end @doc false + @spec update_changeset(t, map) :: Ecto.Changeset.t() def update_changeset(%Event{} = event, attrs) do event - |> Ecto.Changeset.cast(attrs, [ - :title, - :slug, - :description, - :begins_on, - :ends_on, - :category, - :status, - :visibility, - :publish_at, - :online_address, - :phone_address, - :picture_id, - :physical_address_id - ]) + |> Ecto.Changeset.cast(attrs, @update_attrs) |> cast_embed(:options) |> put_tags(attrs) - |> validate_required([ - :title, - :begins_on, - :organizer_actor_id, - :url, - :uuid - ]) + |> validate_required(@update_required_attrs) end - defp put_tags(changeset, %{"tags" => tags}), do: put_assoc(changeset, :tags, tags) - defp put_tags(changeset, _), do: changeset - - def can_event_be_managed_by(%Event{organizer_actor_id: organizer_actor_id}, actor_id) + @doc """ + Checks whether an event can be managed. + """ + @spec can_be_managed_by(t, integer | String.t()) :: boolean + def can_be_managed_by(%Event{organizer_actor_id: organizer_actor_id}, actor_id) when organizer_actor_id == actor_id do {:event_can_be_managed, true} end - def can_event_be_managed_by(_event, _actor) do - {:event_can_be_managed, false} - end + def can_be_managed_by(_event, _actor), do: {:event_can_be_managed, false} + + @spec put_tags(Ecto.Changeset.t(), map) :: Ecto.Changeset.t() + defp put_tags(changeset, %{"tags" => tags}), do: put_assoc(changeset, :tags, tags) + defp put_tags(changeset, _), do: changeset end diff --git a/lib/mobilizon/events/event_offer.ex b/lib/mobilizon/events/event_offer.ex new file mode 100644 index 000000000..c30f67b3e --- /dev/null +++ b/lib/mobilizon/events/event_offer.ex @@ -0,0 +1,19 @@ +defmodule Mobilizon.Events.EventOffer do + @moduledoc """ + Represents an event offer. + """ + + use Ecto.Schema + + @type t :: %__MODULE__{ + price: float, + price_currency: String.t(), + url: String.t() + } + + embedded_schema do + field(:price, :float) + field(:price_currency, :string) + field(:url, :string) + end +end diff --git a/lib/mobilizon/events/event_options.ex b/lib/mobilizon/events/event_options.ex index c6a02fafd..4764dde97 100644 --- a/lib/mobilizon/events/event_options.ex +++ b/lib/mobilizon/events/event_options.ex @@ -1,69 +1,58 @@ -import EctoEnum - -defenum(Mobilizon.Events.CommentModeration, :comment_moderation, [:allow_all, :moderated, :closed]) - -defmodule Mobilizon.Events.EventOffer do - @moduledoc """ - Represents an event offer - """ - use Ecto.Schema - - embedded_schema do - field(:price, :float) - field(:price_currency, :string) - field(:url, :string) - end -end - -defmodule Mobilizon.Events.EventParticipationCondition do - @moduledoc """ - Represents an event participation condition - """ - use Ecto.Schema - - embedded_schema do - field(:title, :string) - field(:content, :string) - field(:url, :string) - end -end - defmodule Mobilizon.Events.EventOptions do @moduledoc """ - Represents an event options + Represents an event options. """ + use Ecto.Schema + import Ecto.Changeset + alias Mobilizon.Events.{ - EventOptions, EventOffer, + EventOptions, EventParticipationCondition, CommentModeration } + @type t :: %__MODULE__{ + maximum_attendee_capacity: integer, + remaining_attendee_capacity: integer, + show_remaining_attendee_capacity: boolean, + attendees: [String.t()], + program: String.t(), + comment_moderation: CommentModeration.t(), + show_participation_price: boolean, + offers: [EventOffer.t()], + participation_condition: [EventParticipationCondition.t()] + } + + @attrs [ + :maximum_attendee_capacity, + :remaining_attendee_capacity, + :show_remaining_attendee_capacity, + :attendees, + :program, + :comment_moderation, + :show_participation_price + ] + @primary_key false embedded_schema do field(:maximum_attendee_capacity, :integer) field(:remaining_attendee_capacity, :integer) field(:show_remaining_attendee_capacity, :boolean) - embeds_many(:offers, EventOffer) - embeds_many(:participation_condition, EventParticipationCondition) field(:attendees, {:array, :string}) field(:program, :string) field(:comment_moderation, CommentModeration) field(:show_participation_price, :boolean) + + embeds_many(:offers, EventOffer) + embeds_many(:participation_condition, EventParticipationCondition) end + @doc false + @spec changeset(t, map) :: Ecto.Changeset.t() def changeset(%EventOptions{} = event_options, attrs) do - event_options - |> Ecto.Changeset.cast(attrs, [ - :maximum_attendee_capacity, - :remaining_attendee_capacity, - :show_remaining_attendee_capacity, - :attendees, - :program, - :comment_moderation, - :show_participation_price - ]) + cast(event_options, attrs, @attrs) end end diff --git a/lib/mobilizon/events/event_participation_condition.ex b/lib/mobilizon/events/event_participation_condition.ex new file mode 100644 index 000000000..9fd4bff1d --- /dev/null +++ b/lib/mobilizon/events/event_participation_condition.ex @@ -0,0 +1,19 @@ +defmodule Mobilizon.Events.EventParticipationCondition do + @moduledoc """ + Represents an event participation condition. + """ + + use Ecto.Schema + + @type t :: %__MODULE__{ + title: String.t(), + content: String.t(), + url: String.t() + } + + embedded_schema do + field(:title, :string) + field(:content, :string) + field(:url, :string) + end +end diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index a6256b294..29f483aae 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -4,6 +4,7 @@ defmodule Mobilizon.Events do """ import Ecto.Query + import EctoEnum import Mobilizon.Storage.Ecto @@ -13,13 +14,62 @@ defmodule Mobilizon.Events do alias Mobilizon.Storage.{Page, Repo} alias Mobilizon.Users.User - def data() do - Dataloader.Ecto.new(Repo, query: &query/2) - end + defenum(EventVisibility, :event_visibility, [ + :public, + :unlisted, + :restricted, + :private + ]) - def query(queryable, _params) do - queryable - end + defenum(JoinOptions, :join_options, [ + :free, + :restricted, + :invite + ]) + + defenum(EventStatus, :event_status, [ + :tentative, + :confirmed, + :cancelled + ]) + + defenum(EventCategory, :event_category, [ + :business, + :conference, + :birthday, + :demonstration, + :meeting + ]) + + defenum(CommentVisibility, :comment_visibility, [ + :public, + :unlisted, + :private, + :moderated, + :invite + ]) + + defenum(CommentModeration, :comment_moderation, [ + :allow_all, + :moderated, + :closed + ]) + + defenum(ParticipantRole, :participant_role, [ + :not_approved, + :participant, + :moderator, + :administrator, + :creator + ]) + + @doc false + @spec data :: Dataloader.Ecto.t() + def data, do: Dataloader.Ecto.new(Repo, query: &query/2) + + @doc false + @spec query(Ecto.Query.t(), map) :: Ecto.Query.t() + def query(queryable, _params), do: queryable def get_public_events_for_actor(%Actor{id: actor_id} = _actor, page \\ nil, limit \\ nil) do query = @@ -537,6 +587,18 @@ defmodule Mobilizon.Events do def get_tag(id), do: Repo.get(Tag, id) + + + def get_tag_by_slug(slug) do + query = + from( + t in Tag, + where: t.slug == ^slug + ) + + Repo.one(query) + end + @doc """ Get an existing tag or create one """ @@ -698,6 +760,9 @@ defmodule Mobilizon.Events do Repo.all(final_query) end + + + alias Mobilizon.Events.Participant @doc """ diff --git a/lib/mobilizon/events/feed_token.ex b/lib/mobilizon/events/feed_token.ex index f4b55e477..25d12a980 100644 --- a/lib/mobilizon/events/feed_token.ex +++ b/lib/mobilizon/events/feed_token.ex @@ -1,16 +1,30 @@ defmodule Mobilizon.Events.FeedToken do @moduledoc """ - Represents a Token for a Feed of events + Represents a token for a feed of events. """ + use Ecto.Schema + import Ecto.Changeset - alias Mobilizon.Events.FeedToken + alias Mobilizon.Actors.Actor + alias Mobilizon.Events.FeedToken alias Mobilizon.Users.User + @type t :: %__MODULE__{ + token: Ecto.UUID.t(), + actor: Actor.t(), + user: User.t() + } + + @required_attrs [:token, :user_id] + @optional_attrs [:actor_id] + @attrs @required_attrs ++ @optional_attrs + @primary_key false schema "feed_tokens" do field(:token, Ecto.UUID, primary_key: true) + belongs_to(:actor, Actor) belongs_to(:user, User) @@ -18,9 +32,10 @@ defmodule Mobilizon.Events.FeedToken do end @doc false + @spec changeset(t, map) :: Ecto.Changeset.t() def changeset(%FeedToken{} = feed_token, attrs) do feed_token - |> Ecto.Changeset.cast(attrs, [:token, :actor_id, :user_id]) - |> validate_required([:token, :user_id]) + |> cast(attrs, @attrs) + |> validate_required(@required_attrs) end end diff --git a/lib/mobilizon/events/participant.ex b/lib/mobilizon/events/participant.ex index 144dc7b38..5fc33bdf1 100644 --- a/lib/mobilizon/events/participant.ex +++ b/lib/mobilizon/events/participant.ex @@ -1,73 +1,46 @@ -import EctoEnum - -defenum(Mobilizon.Events.ParticipantRoleEnum, :participant_role_type, [ - :not_approved, - :participant, - :moderator, - :administrator, - :creator -]) - defmodule Mobilizon.Events.Participant do @moduledoc """ - Represents a participant, an actor participating to an event + Represents a participant, an actor participating to an event. """ + use Ecto.Schema + import Ecto.Changeset - alias Mobilizon.Events.{Participant, Event} + alias Mobilizon.Actors.Actor + alias Mobilizon.Config + alias Mobilizon.Events + alias Mobilizon.Events.{Event, Participant, ParticipantRole} + + @type t :: %__MODULE__{ + role: ParticipantRole.t(), + url: String.t(), + event: Event.t(), + actor: Actor.t() + } + + @required_attrs [:url, :role, :event_id, :actor_id] + @attrs @required_attrs @primary_key {:id, :binary_id, autogenerate: true} schema "participants" do - field(:role, Mobilizon.Events.ParticipantRoleEnum, default: :participant) + field(:role, ParticipantRole, default: :participant) field(:url, :string) + belongs_to(:event, Event, primary_key: true) belongs_to(:actor, Actor, primary_key: true) timestamps() end - @doc false - def changeset(%Participant{} = participant, attrs) do - participant - |> Ecto.Changeset.cast(attrs, [:url, :role, :event_id, :actor_id]) - |> generate_url() - |> validate_required([:url, :role, :event_id, :actor_id]) - end - - # If there's a blank URL that's because we're doing the first insert - defp generate_url(%Ecto.Changeset{data: %Participant{url: nil}} = changeset) do - case fetch_change(changeset, :url) do - {:ok, _url} -> changeset - :error -> do_generate_url(changeset) - end - end - - # Most time just go with the given URL - defp generate_url(%Ecto.Changeset{} = changeset), do: changeset - - defp do_generate_url(%Ecto.Changeset{} = changeset) do - uuid = Ecto.UUID.generate() - - changeset - |> put_change( - :url, - "#{MobilizonWeb.Endpoint.url()}/join/event/#{uuid}" - ) - |> put_change( - :id, - uuid - ) - end - @doc """ - We check that the actor asking to leave the event is not it's only organizer + We check that the actor asking to leave the event is not it's only organizer. We start by fetching the list of organizers and if there's only one of them - and that it's the actor requesting leaving the event we return true + and that it's the actor requesting leaving the event we return true. """ - @spec check_that_participant_is_not_only_organizer(integer(), integer()) :: boolean() - def check_that_participant_is_not_only_organizer(event_id, actor_id) do - case Mobilizon.Events.list_organizers_participants_for_event(event_id) do + @spec is_not_only_organizer(integer | String.t(), integer | String.t()) :: boolean + def is_not_only_organizer(event_id, actor_id) do + case Events.list_organizers_participants_for_event(event_id) do [%Participant{actor: %Actor{id: participant_actor_id}}] -> participant_actor_id == actor_id @@ -75,4 +48,39 @@ defmodule Mobilizon.Events.Participant do false end end + + @doc false + @spec changeset(t, map) :: Ecto.Changeset.t() + def changeset(%Participant{} = participant, attrs) do + participant + |> cast(attrs, @attrs) + |> ensure_url() + |> validate_required(@required_attrs) + end + + # If there's a blank URL that's because we're doing the first insert + @spec ensure_url(Ecto.Changeset.t()) :: Ecto.Changeset.t() + defp ensure_url(%Ecto.Changeset{data: %Participant{url: nil}} = changeset) do + case fetch_change(changeset, :url) do + {:ok, _url} -> + changeset + + :error -> + update_url(changeset) + end + end + + defp ensure_url(%Ecto.Changeset{} = changeset), do: changeset + + defp update_url(%Ecto.Changeset{} = changeset) do + uuid = Ecto.UUID.generate() + url = generate_url(uuid) + + changeset + |> put_change(:id, uuid) + |> put_change(:url, url) + end + + @spec generate_url(String.t()) :: String.t() + defp generate_url(uuid), do: "#{Config.instance_hostname()}/join/event/#{uuid}" end diff --git a/lib/mobilizon/events/session.ex b/lib/mobilizon/events/session.ex index 5f03ec00e..5d1510425 100644 --- a/lib/mobilizon/events/session.ex +++ b/lib/mobilizon/events/session.ex @@ -1,10 +1,41 @@ defmodule Mobilizon.Events.Session do @moduledoc """ - Represents a session for an event (such as a talk at a conference) + Represents a session for an event (such as a talk at a conference). """ + use Ecto.Schema + import Ecto.Changeset - alias Mobilizon.Events.{Session, Event, Track} + + alias Mobilizon.Events.{Event, Session, Track} + + @type t :: %__MODULE__{ + audios_urls: String.t(), + language: String.t(), + long_abstract: String.t(), + short_abstract: String.t(), + slides_url: String.t(), + subtitle: String.t(), + title: String.t(), + videos_urls: String.t(), + begins_on: DateTime.t(), + ends_on: DateTime.t(), + event: Event.t(), + track: Track.t() + } + + @required_attrs [ + :title, + :subtitle, + :short_abstract, + :long_abstract, + :language, + :slides_url, + :videos_urls, + :audios_urls + ] + @optional_attrs [:event_id, :track_id] + @attrs @required_attrs ++ @optional_attrs schema "sessions" do field(:audios_urls, :string) @@ -17,6 +48,7 @@ defmodule Mobilizon.Events.Session do field(:videos_urls, :string) field(:begins_on, :utc_datetime) field(:ends_on, :utc_datetime) + belongs_to(:event, Event) belongs_to(:track, Track) @@ -24,29 +56,10 @@ defmodule Mobilizon.Events.Session do end @doc false + @spec changeset(t, map) :: Ecto.Changeset.t() def changeset(%Session{} = session, attrs) do session - |> cast(attrs, [ - :title, - :subtitle, - :short_abstract, - :long_abstract, - :language, - :slides_url, - :videos_urls, - :audios_urls, - :event_id, - :track_id - ]) - |> validate_required([ - :title, - :subtitle, - :short_abstract, - :long_abstract, - :language, - :slides_url, - :videos_urls, - :audios_urls - ]) + |> cast(attrs, @attrs) + |> validate_required(@required_attrs) end end diff --git a/lib/mobilizon/events/tag.ex b/lib/mobilizon/events/tag.ex index 5d38dec3e..a342f1c68 100644 --- a/lib/mobilizon/events/tag.ex +++ b/lib/mobilizon/events/tag.ex @@ -1,40 +1,40 @@ defmodule Mobilizon.Events.Tag do @moduledoc """ - Represents a tag for events + Represents a tag for events. """ + use Ecto.Schema + import Ecto.Changeset - alias Mobilizon.Events.Tag + + alias Mobilizon.Events.{Tag, TagRelation} alias Mobilizon.Events.Tag.TitleSlug - alias Mobilizon.Events.TagRelation + + @type t :: %__MODULE__{ + title: String.t(), + slug: TitleSlug.Type.t(), + related_tags: [Tag.t()] + } + + @required_attrs [:title, :slug] + @attrs @required_attrs schema "tags" do field(:title, :string) field(:slug, TitleSlug.Type) + many_to_many(:related_tags, Tag, join_through: TagRelation) timestamps() end @doc false + @spec changeset(t, map) :: Ecto.Changeset.t() def changeset(%Tag{} = tag, attrs) do tag - |> cast(attrs, [:title]) + |> cast(attrs, @attrs) |> TitleSlug.maybe_generate_slug() - |> validate_required([:title, :slug]) + |> validate_required(@required_attrs) |> TitleSlug.unique_constraint() end - - def increment_slug(slug) do - case List.pop_at(String.split(slug, "-"), -1) do - {nil, _} -> - slug - - {suffix, slug_parts} -> - case Integer.parse(suffix) do - {id, _} -> Enum.join(slug_parts, "-") <> "-" <> Integer.to_string(id + 1) - :error -> slug <> "-1" - end - end - end end diff --git a/lib/mobilizon/events/tag/title_slug.ex b/lib/mobilizon/events/tag/title_slug.ex index 9c64841d0..5e3fcf3b1 100644 --- a/lib/mobilizon/events/tag/title_slug.ex +++ b/lib/mobilizon/events/tag/title_slug.ex @@ -1,33 +1,53 @@ defmodule Mobilizon.Events.Tag.TitleSlug do @moduledoc """ - Generates slugs for tags + Generates slugs for tags. """ - alias Mobilizon.Events.Tag - import Ecto.Query - alias Mobilizon.Storage.Repo use EctoAutoslugField.Slug, from: :title, to: :slug + alias Mobilizon.Events + + @slug_separator "-" + + @doc """ + Builds a slug. + """ + @spec build_slug(keyword, Ecto.Changeset.t()) :: String.t() def build_slug(sources, changeset) do slug = super(sources, changeset) + build_unique_slug(slug, changeset) end + @spec build_unique_slug(String.t(), Ecto.Changeset.t()) :: String.t() defp build_unique_slug(slug, changeset) do - query = - from( - t in Tag, - where: t.slug == ^slug - ) - - case Repo.one(query) do + case Events.get_tag_by_slug(slug) do nil -> slug _tag -> slug - |> Tag.increment_slug() + |> increment_slug() |> build_unique_slug(changeset) end end + + @spec increment_slug(String.t()) :: String.t() + defp increment_slug(slug) do + case List.pop_at(String.split(slug, @slug_separator), -1) do + {nil, _} -> + slug + + {suffix, slug_parts} -> + case Integer.parse(suffix) do + {id, _} -> + Enum.join(slug_parts, @slug_separator) <> + @slug_separator <> + Integer.to_string(id + 1) + + :error -> + "#{slug}#{@slug_separator}1" + end + end + end end diff --git a/lib/mobilizon/events/tag_relations.ex b/lib/mobilizon/events/tag_relations.ex index 65618cb19..7122a6876 100644 --- a/lib/mobilizon/events/tag_relations.ex +++ b/lib/mobilizon/events/tag_relations.ex @@ -1,36 +1,43 @@ defmodule Mobilizon.Events.TagRelation do @moduledoc """ - Represents a tag for events + Represents a tag relation. """ + use Ecto.Schema + import Ecto.Changeset - alias Mobilizon.Events.Tag - alias Mobilizon.Events.TagRelation + + alias Mobilizon.Events.{Tag, TagRelation} + + @type t :: %__MODULE__{ + weight: integer, + tag: Tag.t(), + link: Tag.t() + } + + @required_attrs [:tag_id, :link_id] + @optional_attrs [:weight] + @attrs @required_attrs ++ @optional_attrs @primary_key false schema "tag_relations" do + field(:weight, :integer, default: 1) + belongs_to(:tag, Tag, primary_key: true) belongs_to(:link, Tag, primary_key: true) - field(:weight, :integer, default: 1) end @doc false + @spec changeset(t, map) :: Ecto.Changeset.t() def changeset(%TagRelation{} = tag, attrs) do - changeset = - tag - |> cast(attrs, [:tag_id, :link_id, :weight]) - |> validate_required([:tag_id, :link_id]) - # Return if tag_id or link_id are not set because it will fail later otherwise - with %Ecto.Changeset{errors: []} <- changeset do - changes = changeset.changes - - changeset = - changeset - |> put_change(:tag_id, min(changes.tag_id, changes.link_id)) - |> put_change(:link_id, max(changes.tag_id, changes.link_id)) - + with %Ecto.Changeset{errors: [], changes: changes} = changeset <- + tag + |> cast(attrs, @attrs) + |> validate_required(@required_attrs) do changeset + |> put_change(:tag_id, min(changes.tag_id, changes.link_id)) + |> put_change(:link_id, max(changes.tag_id, changes.link_id)) |> unique_constraint(:tag_id, name: :tag_relations_pkey) |> check_constraint(:tag_id, name: :no_self_loops_check, diff --git a/lib/mobilizon/events/track.ex b/lib/mobilizon/events/track.ex index a3dab3709..a006fd2d6 100644 --- a/lib/mobilizon/events/track.ex +++ b/lib/mobilizon/events/track.ex @@ -1,15 +1,31 @@ defmodule Mobilizon.Events.Track do @moduledoc """ - Represents a track for an event (such as a theme) having multiple sessions + Represents a track for an event (such as a theme) having multiple sessions. """ + use Ecto.Schema + import Ecto.Changeset + alias Mobilizon.Events.{Track, Event, Session} + @type t :: %__MODULE__{ + color: String.t(), + description: String.t(), + name: String.t(), + event: Event.t(), + sessions: [Session.t()] + } + + @required_attrs [:name, :description, :color] + @optional_attrs [:event_id] + @attrs @required_attrs ++ @optional_attrs + schema "tracks" do field(:color, :string) field(:description, :string) field(:name, :string) + belongs_to(:event, Event) has_many(:sessions, Session) @@ -17,9 +33,10 @@ defmodule Mobilizon.Events.Track do end @doc false + @spec changeset(t, map) :: Ecto.Changeset.t() def changeset(%Track{} = track, attrs) do track - |> cast(attrs, [:name, :description, :color, :event_id]) - |> validate_required([:name, :description, :color]) + |> cast(attrs, @attrs) + |> validate_required(@required_attrs) end end diff --git a/lib/mobilizon_web/resolvers/event.ex b/lib/mobilizon_web/resolvers/event.ex index fee135bf5..339bbdcba 100644 --- a/lib/mobilizon_web/resolvers/event.ex +++ b/lib/mobilizon_web/resolvers/event.ex @@ -274,7 +274,7 @@ defmodule MobilizonWeb.Resolvers.Event do ) do with {:ok, %Event{} = event} <- Mobilizon.Events.get_event(event_id), {:is_owned, %Actor{}} <- User.owns_actor(user, actor_id), - {:event_can_be_managed, true} <- Event.can_event_be_managed_by(event, actor_id), + {:event_can_be_managed, true} <- Event.can_be_managed_by(event, actor_id), event <- Mobilizon.Events.delete_event!(event) do {:ok, %{id: event.id}} else diff --git a/lib/service/activity_pub/activity_pub.ex b/lib/service/activity_pub/activity_pub.ex index 507768dbb..7818cbfd8 100644 --- a/lib/service/activity_pub/activity_pub.ex +++ b/lib/service/activity_pub/activity_pub.ex @@ -461,8 +461,7 @@ defmodule Mobilizon.Service.ActivityPub do local ) do with {:only_organizer, false} <- - {:only_organizer, - Participant.check_that_participant_is_not_only_organizer(event_id, actor_id)}, + {:only_organizer, Participant.is_not_only_organizer(event_id, actor_id)}, {:ok, %Participant{} = participant} <- Mobilizon.Events.get_participant(event_id, actor_id), {:ok, %Participant{} = participant} <- Mobilizon.Events.delete_participant(participant), diff --git a/mix.exs b/mix.exs index aa18d53c0..67ad05c5f 100644 --- a/mix.exs +++ b/mix.exs @@ -205,12 +205,12 @@ defmodule Mobilizon.Mixfile do Mobilizon.Events.Tag, Mobilizon.Events.TagRelations, Mobilizon.Events.Track, - Mobilizon.Event.EventCategoryEnum, - Mobilizon.Events.CommentVisibilityEnum, - Mobilizon.Events.EventStatusEnum, - Mobilizon.Events.EventVisibilityEnum, - Mobilizon.Events.JoinOptionsEnum, - Mobilizon.Events.ParticipantRoleEnum, + Mobilizon.Event.EventCategory, + Mobilizon.Events.CommentVisibility, + Mobilizon.Events.EventStatus, + Mobilizon.Events.EventVisibility, + Mobilizon.Events.JoinOptions, + Mobilizon.Events.ParticipantRole, Mobilizon.Events.Tag.TitleSlug, Mobilizon.Events.Tag.TitleSlug.Type, Mobilizon.Events.TagRelation, diff --git a/priv/repo/migrations/20190103150805_fix_event_visibility.exs b/priv/repo/migrations/20190103150805_fix_event_visibility.exs index 0d1fd294b..e5eecdbf9 100644 --- a/priv/repo/migrations/20190103150805_fix_event_visibility.exs +++ b/priv/repo/migrations/20190103150805_fix_event_visibility.exs @@ -2,20 +2,20 @@ defmodule Mobilizon.Repo.Migrations.FixEventVisibility do use Ecto.Migration def up do - Mobilizon.Events.EventVisibilityEnum.create_type() - Mobilizon.Events.EventStatusEnum.create_type() - Mobilizon.Events.CommentVisibilityEnum.create_type() + Mobilizon.Events.EventVisibility.create_type() + Mobilizon.Events.EventStatus.create_type() + Mobilizon.Events.CommentVisibility.create_type() alter table(:events) do remove(:public) remove(:status) remove(:state) - add(:visibility, Mobilizon.Events.EventVisibilityEnum.type()) - add(:status, Mobilizon.Events.EventStatusEnum.type()) + add(:visibility, Mobilizon.Events.EventVisibility.type()) + add(:status, Mobilizon.Events.EventStatus.type()) end alter table(:comments) do - add(:visibility, Mobilizon.Events.CommentVisibilityEnum.type()) + add(:visibility, Mobilizon.Events.CommentVisibility.type()) end end @@ -32,8 +32,8 @@ defmodule Mobilizon.Repo.Migrations.FixEventVisibility do remove(:visibility) end - Mobilizon.Events.EventVisibilityEnum.drop_type() - Mobilizon.Events.EventStatusEnum.drop_type() - Mobilizon.Events.CommentVisibilityEnum.drop_type() + Mobilizon.Events.EventVisibility.drop_type() + Mobilizon.Events.EventStatus.drop_type() + Mobilizon.Events.CommentVisibility.drop_type() end end diff --git a/priv/repo/migrations/20190130151607_split_event_visibility_and_join_options.exs b/priv/repo/migrations/20190130151607_split_event_visibility_and_join_options.exs index a991ef11b..ecf464a9c 100644 --- a/priv/repo/migrations/20190130151607_split_event_visibility_and_join_options.exs +++ b/priv/repo/migrations/20190130151607_split_event_visibility_and_join_options.exs @@ -1,32 +1,32 @@ defmodule Mobilizon.Repo.Migrations.SplitEventVisibilityAndJoinOptions do use Ecto.Migration - alias Mobilizon.Events.EventVisibilityEnum - alias Mobilizon.Events.JoinOptionsEnum + alias Mobilizon.Events.EventVisibility + alias Mobilizon.Events.JoinOptions @doc """ - EventVisibilityEnum has dropped some possible values, so we need to recreate it + EventVisibility has dropped some possible values, so we need to recreate it Visibility allowed nullable values previously """ def up do execute("ALTER TABLE events ALTER COLUMN visibility TYPE VARCHAR USING visibility::text") - EventVisibilityEnum.drop_type() - EventVisibilityEnum.create_type() + EventVisibility.drop_type() + EventVisibility.create_type() execute( - "ALTER TABLE events ALTER COLUMN visibility TYPE event_visibility_type USING visibility::event_visibility_type" + "ALTER TABLE events ALTER COLUMN visibility TYPE event_visibility_type USING visibility::event_visibility" ) - JoinOptionsEnum.create_type() + JoinOptions.create_type() alter table(:events) do - add(:join_options, JoinOptionsEnum.type(), null: false, default: "free") + add(:join_options, JoinOptions.type(), null: false, default: "free") end execute("UPDATE events SET visibility = 'public' WHERE visibility IS NULL") alter table(:events) do - modify(:visibility, EventVisibilityEnum.type(), null: false, default: "public") + modify(:visibility, EventVisibility.type(), null: false, default: "public") end end @@ -35,14 +35,14 @@ defmodule Mobilizon.Repo.Migrations.SplitEventVisibilityAndJoinOptions do remove(:join_options) end - JoinOptionsEnum.drop_type() + JoinOptions.drop_type() execute("ALTER TABLE events ALTER COLUMN visibility TYPE VARCHAR USING visibility::text") - EventVisibilityEnum.drop_type() - EventVisibilityEnum.create_type() + EventVisibility.drop_type() + EventVisibility.create_type() execute( - "ALTER TABLE events ALTER COLUMN visibility TYPE event_visibility_type USING visibility::event_visibility_type" + "ALTER TABLE events ALTER COLUMN visibility TYPE event_visibility_type USING visibility::event_visibility" ) end end diff --git a/priv/repo/migrations/20190207134142_move_participant_role_to_enum.exs b/priv/repo/migrations/20190207134142_move_participant_role_to_enum.exs index 68e8a9713..065ff0991 100644 --- a/priv/repo/migrations/20190207134142_move_participant_role_to_enum.exs +++ b/priv/repo/migrations/20190207134142_move_participant_role_to_enum.exs @@ -1,12 +1,12 @@ defmodule Mobilizon.Repo.Migrations.MoveParticipantRoleToEnum do use Ecto.Migration - alias Mobilizon.Events.ParticipantRoleEnum + alias Mobilizon.Events.ParticipantRole def up do - ParticipantRoleEnum.create_type() + ParticipantRole.create_type() alter table(:participants) do - add(:role_tmp, ParticipantRoleEnum.type(), default: "participant") + add(:role_tmp, ParticipantRole.type(), default: "participant") end execute("UPDATE participants set role_tmp = 'not_approved' where role = 0") @@ -37,7 +37,7 @@ defmodule Mobilizon.Repo.Migrations.MoveParticipantRoleToEnum do remove(:role) end - ParticipantRoleEnum.drop_type() + ParticipantRole.drop_type() rename(table(:participants), :role_tmp, to: :role) end From d3f3553ae7ff2b7a637ed3ea91291f88211dcf34 Mon Sep 17 00:00:00 2001 From: miffigriffy Date: Fri, 13 Sep 2019 01:35:03 +0200 Subject: [PATCH 18/29] Move Activity under Events context --- lib/mobilizon/activity.ex | 7 ------- lib/mobilizon/events/activity.ex | 13 +++++++++++++ lib/mobilizon/events/events.ex | 5 ----- lib/mobilizon_web/api/reports.ex | 7 ++++--- lib/mobilizon_web/resolvers/comment.ex | 8 +++++--- lib/mobilizon_web/resolvers/event.ex | 3 +-- lib/mobilizon_web/resolvers/group.ex | 4 +++- .../views/activity_pub/actor_view.ex | 2 +- .../views/activity_pub/object_view.ex | 3 ++- lib/service/activity_pub/activity_pub.ex | 3 +-- lib/service/activity_pub/relay.ex | 4 +++- lib/service/activity_pub/utils.ex | 17 +++++++++-------- lib/service/activity_pub/visibility.ex | 3 ++- lib/service/federator.ex | 4 +++- mix.exs | 2 +- .../activity_pub/transmogrifier_test.exs | 7 +++---- test/mobilizon_web/api/report_test.exs | 17 ++++++++--------- 17 files changed, 59 insertions(+), 50 deletions(-) delete mode 100644 lib/mobilizon/activity.ex create mode 100644 lib/mobilizon/events/activity.ex diff --git a/lib/mobilizon/activity.ex b/lib/mobilizon/activity.ex deleted file mode 100644 index 2bcdc4fed..000000000 --- a/lib/mobilizon/activity.ex +++ /dev/null @@ -1,7 +0,0 @@ -defmodule Mobilizon.Activity do - @moduledoc """ - Represents an activity - """ - - defstruct [:data, :local, :actor, :recipients, :notifications] -end diff --git a/lib/mobilizon/events/activity.ex b/lib/mobilizon/events/activity.ex new file mode 100644 index 000000000..7daf805e7 --- /dev/null +++ b/lib/mobilizon/events/activity.ex @@ -0,0 +1,13 @@ +defmodule Mobilizon.Events.Activity do + @moduledoc """ + Represents an activity. + """ + + defstruct [ + :data, + :local, + :actor, + :recipients, + :notifications + ] +end diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index 29f483aae..24c93fa5e 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -587,8 +587,6 @@ defmodule Mobilizon.Events do def get_tag(id), do: Repo.get(Tag, id) - - def get_tag_by_slug(slug) do query = from( @@ -760,9 +758,6 @@ defmodule Mobilizon.Events do Repo.all(final_query) end - - - alias Mobilizon.Events.Participant @doc """ diff --git a/lib/mobilizon_web/api/reports.ex b/lib/mobilizon_web/api/reports.ex index 815471acd..171e70b04 100644 --- a/lib/mobilizon_web/api/reports.ex +++ b/lib/mobilizon_web/api/reports.ex @@ -3,17 +3,18 @@ defmodule MobilizonWeb.API.Reports do API for Reports """ + import MobilizonWeb.API.Utils + import Mobilizon.Service.Admin.ActionLogService + alias Mobilizon.Actors alias Mobilizon.Actors.Actor alias Mobilizon.Events - alias Mobilizon.Activity + alias Mobilizon.Events.Activity alias Mobilizon.Reports, as: ReportsAction alias Mobilizon.Reports.{Report, Note} alias Mobilizon.Service.ActivityPub alias Mobilizon.Users alias Mobilizon.Users.User - import MobilizonWeb.API.Utils - import Mobilizon.Service.Admin.ActionLogService @doc """ Create a report/flag on an actor, and optionally on an event or on comments. diff --git a/lib/mobilizon_web/resolvers/comment.ex b/lib/mobilizon_web/resolvers/comment.ex index c167b898f..cbe0b248b 100644 --- a/lib/mobilizon_web/resolvers/comment.ex +++ b/lib/mobilizon_web/resolvers/comment.ex @@ -2,12 +2,14 @@ defmodule MobilizonWeb.Resolvers.Comment do @moduledoc """ Handles the comment-related GraphQL calls """ - require Logger - alias Mobilizon.Events.Comment - alias Mobilizon.Activity + + alias Mobilizon.Events.{Activity, Comment} alias Mobilizon.Users.User + alias MobilizonWeb.API.Comments + require Logger + def create_comment(_parent, %{text: comment, actor_username: username}, %{ context: %{current_user: %User{} = _user} }) do diff --git a/lib/mobilizon_web/resolvers/event.ex b/lib/mobilizon_web/resolvers/event.ex index 339bbdcba..370b507a2 100644 --- a/lib/mobilizon_web/resolvers/event.ex +++ b/lib/mobilizon_web/resolvers/event.ex @@ -2,12 +2,11 @@ defmodule MobilizonWeb.Resolvers.Event do @moduledoc """ Handles the event-related GraphQL calls """ - alias Mobilizon.Activity alias Mobilizon.Actors.Actor alias Mobilizon.Addresses alias Mobilizon.Addresses.Address alias Mobilizon.Events - alias Mobilizon.Events.{Event, Participant} + alias Mobilizon.Events.{Activity, Event, Participant} alias Mobilizon.Media.Picture alias Mobilizon.Users.User alias MobilizonWeb.Resolvers.Person diff --git a/lib/mobilizon_web/resolvers/group.ex b/lib/mobilizon_web/resolvers/group.ex index 5aa35e861..e9422bb50 100644 --- a/lib/mobilizon_web/resolvers/group.ex +++ b/lib/mobilizon_web/resolvers/group.ex @@ -4,10 +4,12 @@ defmodule MobilizonWeb.Resolvers.Group do """ alias Mobilizon.Actors alias Mobilizon.Actors.{Actor, Member} + alias Mobilizon.Events.Activity alias Mobilizon.Users.User alias Mobilizon.Service.ActivityPub - alias Mobilizon.Activity + alias MobilizonWeb.Resolvers.Person + require Logger @doc """ diff --git a/lib/mobilizon_web/views/activity_pub/actor_view.ex b/lib/mobilizon_web/views/activity_pub/actor_view.ex index 5ecf495c7..c1679bb53 100644 --- a/lib/mobilizon_web/views/activity_pub/actor_view.ex +++ b/lib/mobilizon_web/views/activity_pub/actor_view.ex @@ -3,9 +3,9 @@ defmodule MobilizonWeb.ActivityPub.ActorView do alias Mobilizon.Actors alias Mobilizon.Actors.Actor + alias Mobilizon.Events.Activity alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub.Utils - alias Mobilizon.Activity @private_visibility_empty_collection %{elements: [], total: 0} diff --git a/lib/mobilizon_web/views/activity_pub/object_view.ex b/lib/mobilizon_web/views/activity_pub/object_view.ex index 9a1da8fca..576b9e751 100644 --- a/lib/mobilizon_web/views/activity_pub/object_view.ex +++ b/lib/mobilizon_web/views/activity_pub/object_view.ex @@ -1,7 +1,8 @@ defmodule MobilizonWeb.ActivityPub.ObjectView do use MobilizonWeb, :view + + alias Mobilizon.Events.Activity alias Mobilizon.Service.ActivityPub.Utils - alias Mobilizon.Activity def render("activity.json", %{activity: %Activity{local: local, data: data} = activity}) do %{ diff --git a/lib/service/activity_pub/activity_pub.ex b/lib/service/activity_pub/activity_pub.ex index 7818cbfd8..68e23ec59 100644 --- a/lib/service/activity_pub/activity_pub.ex +++ b/lib/service/activity_pub/activity_pub.ex @@ -12,10 +12,9 @@ defmodule Mobilizon.Service.ActivityPub do alias Mobilizon.Config alias Mobilizon.Events - alias Mobilizon.Events.{Event, Comment, Participant} + alias Mobilizon.Events.{Activity, Event, Comment, Participant} alias Mobilizon.Service.ActivityPub.Transmogrifier alias Mobilizon.Service.WebFinger - alias Mobilizon.Activity alias Mobilizon.Actors alias Mobilizon.Actors.{Actor, Follower} diff --git a/lib/service/activity_pub/relay.ex b/lib/service/activity_pub/relay.ex index 3333be71b..ef44fbbea 100644 --- a/lib/service/activity_pub/relay.ex +++ b/lib/service/activity_pub/relay.ex @@ -8,11 +8,13 @@ defmodule Mobilizon.Service.ActivityPub.Relay do Handles following and unfollowing relays and instances """ - alias Mobilizon.Activity alias Mobilizon.Actors alias Mobilizon.Actors.Actor + alias Mobilizon.Events.Activity alias Mobilizon.Service.ActivityPub + alias MobilizonWeb.API.Follows + require Logger def get_actor do diff --git a/lib/service/activity_pub/utils.ex b/lib/service/activity_pub/utils.ex index db8757bb6..ff7655231 100644 --- a/lib/service/activity_pub/utils.ex +++ b/lib/service/activity_pub/utils.ex @@ -10,25 +10,26 @@ defmodule Mobilizon.Service.ActivityPub.Utils do Various utils """ - alias Mobilizon.Storage.Repo + alias Ecto.Changeset + alias Mobilizon.Addresses alias Mobilizon.Addresses.Address alias Mobilizon.Actors alias Mobilizon.Actors.Actor - alias Mobilizon.Events.Event - alias Mobilizon.Events.Comment - alias Mobilizon.Media.Picture alias Mobilizon.Events - alias Mobilizon.Activity + alias Mobilizon.Events.{Activity, Comment, Event} + alias Mobilizon.Media.Picture alias Mobilizon.Reports alias Mobilizon.Reports.Report - alias Mobilizon.Users alias Mobilizon.Service.ActivityPub.Converters - alias Ecto.Changeset - require Logger + alias Mobilizon.Storage.Repo + alias Mobilizon.Users + alias MobilizonWeb.Router.Helpers, as: Routes alias MobilizonWeb.Endpoint + require Logger + @actor_types ["Group", "Person", "Application"] # Some implementations send the actor URI as the actor field, others send the entire actor object, diff --git a/lib/service/activity_pub/visibility.ex b/lib/service/activity_pub/visibility.ex index 93a5c5aba..756965910 100644 --- a/lib/service/activity_pub/visibility.ex +++ b/lib/service/activity_pub/visibility.ex @@ -7,7 +7,8 @@ defmodule Mobilizon.Service.ActivityPub.Visibility do @moduledoc """ Utility functions related to content visibility """ - alias Mobilizon.Activity + + alias Mobilizon.Events.Activity @public "https://www.w3.org/ns/activitystreams#Public" diff --git a/lib/service/federator.ex b/lib/service/federator.ex index 5e19e8a4c..4d8b99cd0 100644 --- a/lib/service/federator.ex +++ b/lib/service/federator.ex @@ -9,10 +9,12 @@ defmodule Mobilizon.Service.Federator do """ use GenServer + alias Mobilizon.Actors - alias Mobilizon.Activity + alias Mobilizon.Events.Activity alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub.Transmogrifier + require Logger @max_jobs 20 diff --git a/mix.exs b/mix.exs index 67ad05c5f..9386944c1 100644 --- a/mix.exs +++ b/mix.exs @@ -197,6 +197,7 @@ defmodule Mobilizon.Mixfile do Mobilizon.Addresses, Mobilizon.Addresses.Address, Mobilizon.Events, + Mobilizon.Events.Activity, Mobilizon.Events.Event, Mobilizon.Events.Comment, Mobilizon.Events.FeedToken, @@ -218,7 +219,6 @@ defmodule Mobilizon.Mixfile do Mobilizon.Users.User, Mobilizon.Users.UserRole, Mobilizon.Users.Guards, - Mobilizon.Activity, Mobilizon.Storage.Ecto, Mobilizon.Storage.Repo ], diff --git a/test/mobilizon/service/activity_pub/transmogrifier_test.exs b/test/mobilizon/service/activity_pub/transmogrifier_test.exs index 7eb340ae4..91ec98007 100644 --- a/test/mobilizon/service/activity_pub/transmogrifier_test.exs +++ b/test/mobilizon/service/activity_pub/transmogrifier_test.exs @@ -8,11 +8,10 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do import Mobilizon.Factory - alias Mobilizon.Activity alias Mobilizon.Actors alias Mobilizon.Actors.Actor alias Mobilizon.Events - alias Mobilizon.Events.{Comment, Event, Participant} + alias Mobilizon.Events.{Activity, Comment, Event, Participant} alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub.Utils alias Mobilizon.Service.ActivityPub.Transmogrifier @@ -26,7 +25,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do test "it works for incoming events" do data = File.read!("test/fixtures/mobilizon-post-activity.json") |> Jason.decode!() - {:ok, %Mobilizon.Activity{data: data, local: false}, %Event{} = event} = + {:ok, %Activity{data: data, local: false}, %Event{} = event} = Transmogrifier.handle_incoming(data) assert data["id"] == @@ -116,7 +115,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do test "it works for incoming notices" do data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!() - {:ok, %Mobilizon.Activity{data: data, local: false}, _} = + {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) assert data["id"] == "https://framapiaf.org/users/admin/statuses/99512778738411822/activity" diff --git a/test/mobilizon_web/api/report_test.exs b/test/mobilizon_web/api/report_test.exs index fa547024b..de6df7f7e 100644 --- a/test/mobilizon_web/api/report_test.exs +++ b/test/mobilizon_web/api/report_test.exs @@ -1,17 +1,16 @@ defmodule MobilizonWeb.API.ReportTest do use Mobilizon.DataCase - alias Mobilizon.Events.Event - alias Mobilizon.Events.Comment - alias Mobilizon.Actors.Actor - alias MobilizonWeb.API.Reports - alias Mobilizon.Reports.{Report, Note} - alias Mobilizon.Activity - alias Mobilizon.Users.User - alias Mobilizon.Users - import Mobilizon.Factory + alias Mobilizon.Actors.Actor + alias Mobilizon.Events.{Activity, Comment, Event} + alias Mobilizon.Reports.{Report, Note} + alias Mobilizon.Users + alias Mobilizon.Users.User + + alias MobilizonWeb.API.Reports + describe "reports" do test "creates a report on a event" do %Actor{id: reporter_id, url: reporter_url} = insert(:actor) From 56efb0acb1b1324d653e11fdfcd4a289495e5129 Mon Sep 17 00:00:00 2001 From: miffigriffy Date: Fri, 13 Sep 2019 01:35:56 +0200 Subject: [PATCH 19/29] Move MIME under MobilizonWeb context --- lib/{mobilizon => mobilizon_web}/mime.ex | 2 +- lib/mobilizon_web/upload.ex | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) rename lib/{mobilizon => mobilizon_web}/mime.ex (99%) diff --git a/lib/mobilizon/mime.ex b/lib/mobilizon_web/mime.ex similarity index 99% rename from lib/mobilizon/mime.ex rename to lib/mobilizon_web/mime.ex index 12ea8f46f..49739915a 100644 --- a/lib/mobilizon/mime.ex +++ b/lib/mobilizon_web/mime.ex @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only # Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/mime.ex -defmodule Mobilizon.MIME do +defmodule MobilizonWeb.MIME do @moduledoc """ Returns the mime-type of a binary and optionally a normalized file-name. """ diff --git a/lib/mobilizon_web/upload.ex b/lib/mobilizon_web/upload.ex index b245f74b6..71f16d6e7 100644 --- a/lib/mobilizon_web/upload.ex +++ b/lib/mobilizon_web/upload.ex @@ -36,6 +36,8 @@ defmodule MobilizonWeb.Upload do alias Mobilizon.Config + alias MobilizonWeb.MIME + require Logger @type source :: @@ -140,7 +142,7 @@ defmodule MobilizonWeb.Upload do defp prepare_upload(%Plug.Upload{} = file, opts) do with {:ok, size} <- check_file_size(file.path, opts.size_limit), - {:ok, content_type, name} <- Mobilizon.MIME.file_mime_type(file.path, file.filename) do + {:ok, content_type, name} <- MIME.file_mime_type(file.path, file.filename) do {:ok, %__MODULE__{ id: UUID.generate(), From 5967b30bb1264a9f240a191c99b9a256da2bd94c Mon Sep 17 00:00:00 2001 From: miffigriffy Date: Fri, 13 Sep 2019 01:55:45 +0200 Subject: [PATCH 20/29] Clean specs --- lib/mobilizon/actors/actor.ex | 54 +++++++++++++-------------- lib/mobilizon/actors/actors.ex | 2 +- lib/mobilizon/actors/bot.ex | 4 +- lib/mobilizon/actors/follower.ex | 2 +- lib/mobilizon/actors/member.ex | 12 +++--- lib/mobilizon/addresses/address.ex | 5 +-- lib/mobilizon/admin/action_log.ex | 4 +- lib/mobilizon/events/comment.ex | 4 +- lib/mobilizon/events/event.ex | 7 ++-- lib/mobilizon/events/event_options.ex | 3 +- lib/mobilizon/events/feed_token.ex | 3 +- lib/mobilizon/events/participant.ex | 8 ++-- lib/mobilizon/events/session.ex | 4 +- lib/mobilizon/events/tag.ex | 8 ++-- lib/mobilizon/events/tag_relations.ex | 4 +- lib/mobilizon/events/track.ex | 4 +- lib/mobilizon/media/file.ex | 4 +- lib/mobilizon/media/picture.ex | 4 +- lib/mobilizon/reports/note.ex | 4 +- lib/mobilizon/reports/report.ex | 8 ++-- lib/mobilizon/users/user.ex | 28 +++++++------- 21 files changed, 86 insertions(+), 90 deletions(-) diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex index 9d41d1494..4dd5a1d38 100644 --- a/lib/mobilizon/actors/actor.ex +++ b/lib/mobilizon/actors/actor.ex @@ -8,7 +8,7 @@ defmodule Mobilizon.Actors.Actor do import Ecto.Changeset alias Mobilizon.{Actors, Config, Crypto} - alias Mobilizon.Actors.{Actor, ActorOpenness, ActorType, ActorVisibility, Follower, Member} + alias Mobilizon.Actors.{ActorOpenness, ActorType, ActorVisibility, Follower, Member} alias Mobilizon.Events.{Event, FeedToken} alias Mobilizon.Media.File alias Mobilizon.Reports.{Report, Note} @@ -46,7 +46,7 @@ defmodule Mobilizon.Actors.Actor do created_reports: [Report.t()], subject_reports: [Report.t()], report_notes: [Note.t()], - memberships: [Actor.t()] + memberships: [t] } @required_attrs [:preferred_username, :keys, :suspended, :url] @@ -139,7 +139,7 @@ defmodule Mobilizon.Actors.Actor do has_many(:created_reports, Report, foreign_key: :reporter_id) has_many(:subject_reports, Report, foreign_key: :reported_id) has_many(:report_notes, Note, foreign_key: :moderator_id) - many_to_many(:memberships, Actor, join_through: Member) + many_to_many(:memberships, __MODULE__, join_through: Member) timestamps() end @@ -147,8 +147,8 @@ defmodule Mobilizon.Actors.Actor do @doc """ Checks whether actor visibility is public. """ - @spec is_public_visibility(Actor.t()) :: boolean - def is_public_visibility(%Actor{visibility: visibility}) do + @spec is_public_visibility(t) :: boolean + def is_public_visibility(%__MODULE__{visibility: visibility}) do visibility in [:public, :unlisted] end @@ -156,22 +156,22 @@ defmodule Mobilizon.Actors.Actor do Returns the display name if available, or the preferred username (with the eventual @domain suffix if it's a distant actor). """ - @spec display_name(Actor.t()) :: String.t() - def display_name(%Actor{name: name} = actor) when name in [nil, ""] do + @spec display_name(t) :: String.t() + def display_name(%__MODULE__{name: name} = actor) when name in [nil, ""] do preferred_username_and_domain(actor) end - def display_name(%Actor{name: name}), do: name + def display_name(%__MODULE__{name: name}), do: name @doc """ Returns display name and username. """ - @spec display_name_and_username(Actor.t()) :: String.t() - def display_name_and_username(%Actor{name: name} = actor) when name in [nil, ""] do + @spec display_name_and_username(t) :: String.t() + def display_name_and_username(%__MODULE__{name: name} = actor) when name in [nil, ""] do preferred_username_and_domain(actor) end - def display_name_and_username(%Actor{name: name} = actor) do + def display_name_and_username(%__MODULE__{name: name} = actor) do "#{name} (#{preferred_username_and_domain(actor)})" end @@ -179,18 +179,18 @@ defmodule Mobilizon.Actors.Actor do Returns the preferred username with the eventual @domain suffix if it's a distant actor. """ - @spec preferred_username_and_domain(Actor.t()) :: String.t() - def preferred_username_and_domain(%Actor{preferred_username: preferred_username, domain: nil}) do + @spec preferred_username_and_domain(t) :: String.t() + def preferred_username_and_domain(%__MODULE__{preferred_username: preferred_username, domain: nil}) do preferred_username end - def preferred_username_and_domain(%Actor{preferred_username: preferred_username, domain: domain}) do + def preferred_username_and_domain(%__MODULE__{preferred_username: preferred_username, domain: domain}) do "#{preferred_username}@#{domain}" end @doc false - @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() - def changeset(%Actor{} = actor, attrs) do + @spec changeset(t, map) :: Ecto.Changeset.t() + def changeset(%__MODULE__{} = actor, attrs) do actor |> cast(attrs, @attrs) |> build_urls() @@ -205,8 +205,8 @@ defmodule Mobilizon.Actors.Actor do end @doc false - @spec update_changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() - def update_changeset(%Actor{} = actor, attrs) do + @spec update_changeset(t, map) :: Ecto.Changeset.t() + def update_changeset(%__MODULE__{} = actor, attrs) do actor |> cast(attrs, @update_attrs) |> cast_embed(:avatar) @@ -221,8 +221,8 @@ defmodule Mobilizon.Actors.Actor do @doc """ Changeset for person registration. """ - @spec registration_changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() - def registration_changeset(%Actor{} = actor, attrs) do + @spec registration_changeset(t, map) :: Ecto.Changeset.t() + def registration_changeset(%__MODULE__{} = actor, attrs) do actor |> cast(attrs, @registration_attrs) |> build_urls() @@ -242,7 +242,7 @@ defmodule Mobilizon.Actors.Actor do @spec remote_actor_creation_changeset(map) :: Ecto.Changeset.t() def remote_actor_creation_changeset(attrs) do changeset = - %Actor{} + %__MODULE__{} |> cast(attrs, @remote_actor_creation_attrs) |> validate_required(@remote_actor_creation_required_attrs) |> cast_embed(:avatar) @@ -267,14 +267,14 @@ defmodule Mobilizon.Actors.Actor do def relay_creation_changeset(attrs) do relay_creation_attrs = build_relay_creation_attrs(attrs) - cast(%Actor{}, relay_creation_attrs, @relay_creation_attrs) + cast(%__MODULE__{}, relay_creation_attrs, @relay_creation_attrs) end @doc """ Changeset for group creation """ - @spec group_creation(struct(), map()) :: Ecto.Changeset.t() - def group_creation(%Actor{} = actor, params) do + @spec group_creation_changeset(t, map) :: Ecto.Changeset.t() + def group_creation_changeset(%__MODULE__{} = actor, params) do actor |> cast(params, @group_creation_attrs) |> cast_embed(:avatar) @@ -299,7 +299,7 @@ defmodule Mobilizon.Actors.Actor do %Ecto.Changeset{changes: %{preferred_username: username} = changes} = changeset ) do with nil <- Map.get(changes, :domain, nil), - %Actor{preferred_username: _} <- Actors.get_local_actor_by_name(username) do + %__MODULE__{preferred_username: _} <- Actors.get_local_actor_by_name(username) do add_error(changeset, :preferred_username, "Username is already taken") else _ -> changeset @@ -349,8 +349,8 @@ defmodule Mobilizon.Actors.Actor do Clear multiple caches for an actor """ # TODO: move to MobilizonWeb - @spec clear_cache(struct()) :: {:ok, true} - def clear_cache(%Actor{preferred_username: preferred_username, domain: nil}) do + @spec clear_cache(t) :: {:ok, true} + def clear_cache(%__MODULE__{preferred_username: preferred_username, domain: nil}) do Cachex.del(:activity_pub, "actor_" <> preferred_username) Cachex.del(:feed, "actor_" <> preferred_username) Cachex.del(:ics, "actor_" <> preferred_username) diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index ca9553826..a174f0045 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -340,7 +340,7 @@ defmodule Mobilizon.Actors do @spec create_group(map) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()} def create_group(attrs \\ %{}) do %Actor{} - |> Actor.group_creation(attrs) + |> Actor.group_creation_changeset(attrs) |> Repo.insert() end diff --git a/lib/mobilizon/actors/bot.ex b/lib/mobilizon/actors/bot.ex index 46d3c4b87..8312f44b7 100644 --- a/lib/mobilizon/actors/bot.ex +++ b/lib/mobilizon/actors/bot.ex @@ -32,8 +32,8 @@ defmodule Mobilizon.Actors.Bot do end @doc false - @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() - def changeset(bot, attrs) do + @spec changeset(t, map) :: Ecto.Changeset.t() + def changeset(%__MODULE__{} = bot, attrs) do bot |> cast(attrs, @attrs) |> validate_required(@required_attrs) diff --git a/lib/mobilizon/actors/follower.ex b/lib/mobilizon/actors/follower.ex index 0164d39a3..f38e00fd2 100644 --- a/lib/mobilizon/actors/follower.ex +++ b/lib/mobilizon/actors/follower.ex @@ -29,7 +29,7 @@ defmodule Mobilizon.Actors.Follower do end @doc false - @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() + @spec changeset(t, map) :: Ecto.Changeset.t() def changeset(follower, attrs) do follower |> cast(attrs, @attrs) diff --git a/lib/mobilizon/actors/member.ex b/lib/mobilizon/actors/member.ex index b6963e546..97327e6a4 100644 --- a/lib/mobilizon/actors/member.ex +++ b/lib/mobilizon/actors/member.ex @@ -7,7 +7,7 @@ defmodule Mobilizon.Actors.Member do import Ecto.Changeset - alias Mobilizon.Actors.{Actor, Member, MemberRole} + alias Mobilizon.Actors.{Actor, MemberRole} @type t :: %__MODULE__{ role: MemberRole.t(), @@ -44,13 +44,13 @@ defmodule Mobilizon.Actors.Member do @doc """ Checks whether the member is an administrator (admin or creator) of the group. """ - def is_administrator(%Member{role: :administrator}), do: {:is_admin, true} - def is_administrator(%Member{role: :creator}), do: {:is_admin, true} - def is_administrator(%Member{}), do: {:is_admin, false} + def is_administrator(%__MODULE__{role: :administrator}), do: {:is_admin, true} + def is_administrator(%__MODULE__{role: :creator}), do: {:is_admin, true} + def is_administrator(%__MODULE__{}), do: {:is_admin, false} @doc false - @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() - def changeset(member, attrs) do + @spec changeset(t, map) :: Ecto.Changeset.t() + def changeset(%__MODULE__{} = member, attrs) do member |> cast(attrs, @attrs) |> validate_required(@required_attrs) diff --git a/lib/mobilizon/addresses/address.ex b/lib/mobilizon/addresses/address.ex index 7fd4b3af7..585f1f6a1 100644 --- a/lib/mobilizon/addresses/address.ex +++ b/lib/mobilizon/addresses/address.ex @@ -7,7 +7,6 @@ defmodule Mobilizon.Addresses.Address do import Ecto.Changeset - alias Mobilizon.Addresses.Address alias Mobilizon.Events.Event @type t :: %__MODULE__{ @@ -56,8 +55,8 @@ defmodule Mobilizon.Addresses.Address do end @doc false - @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() - def changeset(%Address{} = address, attrs) do + @spec changeset(t, map) :: Ecto.Changeset.t() + def changeset(%__MODULE__{} = address, attrs) do address |> cast(attrs, @attrs) |> set_url() diff --git a/lib/mobilizon/admin/action_log.ex b/lib/mobilizon/admin/action_log.ex index 68d1f189b..59275307e 100644 --- a/lib/mobilizon/admin/action_log.ex +++ b/lib/mobilizon/admin/action_log.ex @@ -33,8 +33,8 @@ defmodule Mobilizon.Admin.ActionLog do end @doc false - @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() - def changeset(action_log, attrs) do + @spec changeset(t, map) :: Ecto.Changeset.t() + def changeset(%__MODULE__{} = action_log, attrs) do action_log |> cast(attrs, @attrs) |> validate_required(@required_attrs) diff --git a/lib/mobilizon/events/comment.ex b/lib/mobilizon/events/comment.ex index 4caf24219..3856d1c2c 100644 --- a/lib/mobilizon/events/comment.ex +++ b/lib/mobilizon/events/comment.ex @@ -48,13 +48,13 @@ defmodule Mobilizon.Events.Comment do Returns the id of the first comment in the conversation. """ @spec get_thread_id(t) :: integer - def get_thread_id(%Comment{id: id, origin_comment_id: origin_comment_id}) do + def get_thread_id(%__MODULE__{id: id, origin_comment_id: origin_comment_id}) do origin_comment_id || id end @doc false @spec changeset(t, map) :: Ecto.Changeset.t() - def changeset(%Comment{} = comment, attrs) do + def changeset(%__MODULE__{} = comment, attrs) do uuid = attrs["uuid"] || Ecto.UUID.generate() url = attrs["url"] || generate_url(uuid) diff --git a/lib/mobilizon/events/event.ex b/lib/mobilizon/events/event.ex index fa5f5ad61..8a5d2a3cc 100644 --- a/lib/mobilizon/events/event.ex +++ b/lib/mobilizon/events/event.ex @@ -11,7 +11,6 @@ defmodule Mobilizon.Events.Event do alias Mobilizon.Addresses.Address alias Mobilizon.Events.{ - Event, EventOptions, EventStatus, EventVisibility, @@ -115,7 +114,7 @@ defmodule Mobilizon.Events.Event do @doc false @spec changeset(t, map) :: Ecto.Changeset.t() - def changeset(%Event{} = event, attrs) do + def changeset(%__MODULE__{} = event, attrs) do event |> cast(attrs, @attrs) |> cast_embed(:options) @@ -124,7 +123,7 @@ defmodule Mobilizon.Events.Event do @doc false @spec update_changeset(t, map) :: Ecto.Changeset.t() - def update_changeset(%Event{} = event, attrs) do + def update_changeset(%__MODULE__{} = event, attrs) do event |> Ecto.Changeset.cast(attrs, @update_attrs) |> cast_embed(:options) @@ -136,7 +135,7 @@ defmodule Mobilizon.Events.Event do Checks whether an event can be managed. """ @spec can_be_managed_by(t, integer | String.t()) :: boolean - def can_be_managed_by(%Event{organizer_actor_id: organizer_actor_id}, actor_id) + def can_be_managed_by(%__MODULE__{organizer_actor_id: organizer_actor_id}, actor_id) when organizer_actor_id == actor_id do {:event_can_be_managed, true} end diff --git a/lib/mobilizon/events/event_options.ex b/lib/mobilizon/events/event_options.ex index 4764dde97..2e694f417 100644 --- a/lib/mobilizon/events/event_options.ex +++ b/lib/mobilizon/events/event_options.ex @@ -9,7 +9,6 @@ defmodule Mobilizon.Events.EventOptions do alias Mobilizon.Events.{ EventOffer, - EventOptions, EventParticipationCondition, CommentModeration } @@ -52,7 +51,7 @@ defmodule Mobilizon.Events.EventOptions do @doc false @spec changeset(t, map) :: Ecto.Changeset.t() - def changeset(%EventOptions{} = event_options, attrs) do + def changeset(%__MODULE__{} = event_options, attrs) do cast(event_options, attrs, @attrs) end end diff --git a/lib/mobilizon/events/feed_token.ex b/lib/mobilizon/events/feed_token.ex index 25d12a980..d2f255db6 100644 --- a/lib/mobilizon/events/feed_token.ex +++ b/lib/mobilizon/events/feed_token.ex @@ -8,7 +8,6 @@ defmodule Mobilizon.Events.FeedToken do import Ecto.Changeset alias Mobilizon.Actors.Actor - alias Mobilizon.Events.FeedToken alias Mobilizon.Users.User @type t :: %__MODULE__{ @@ -33,7 +32,7 @@ defmodule Mobilizon.Events.FeedToken do @doc false @spec changeset(t, map) :: Ecto.Changeset.t() - def changeset(%FeedToken{} = feed_token, attrs) do + def changeset(%__MODULE__{} = feed_token, attrs) do feed_token |> cast(attrs, @attrs) |> validate_required(@required_attrs) diff --git a/lib/mobilizon/events/participant.ex b/lib/mobilizon/events/participant.ex index 5fc33bdf1..93a5a2588 100644 --- a/lib/mobilizon/events/participant.ex +++ b/lib/mobilizon/events/participant.ex @@ -10,7 +10,7 @@ defmodule Mobilizon.Events.Participant do alias Mobilizon.Actors.Actor alias Mobilizon.Config alias Mobilizon.Events - alias Mobilizon.Events.{Event, Participant, ParticipantRole} + alias Mobilizon.Events.{Event, ParticipantRole} @type t :: %__MODULE__{ role: ParticipantRole.t(), @@ -41,7 +41,7 @@ defmodule Mobilizon.Events.Participant do @spec is_not_only_organizer(integer | String.t(), integer | String.t()) :: boolean def is_not_only_organizer(event_id, actor_id) do case Events.list_organizers_participants_for_event(event_id) do - [%Participant{actor: %Actor{id: participant_actor_id}}] -> + [%__MODULE__{actor: %Actor{id: participant_actor_id}}] -> participant_actor_id == actor_id _ -> @@ -51,7 +51,7 @@ defmodule Mobilizon.Events.Participant do @doc false @spec changeset(t, map) :: Ecto.Changeset.t() - def changeset(%Participant{} = participant, attrs) do + def changeset(%__MODULE__{} = participant, attrs) do participant |> cast(attrs, @attrs) |> ensure_url() @@ -60,7 +60,7 @@ defmodule Mobilizon.Events.Participant do # If there's a blank URL that's because we're doing the first insert @spec ensure_url(Ecto.Changeset.t()) :: Ecto.Changeset.t() - defp ensure_url(%Ecto.Changeset{data: %Participant{url: nil}} = changeset) do + defp ensure_url(%Ecto.Changeset{data: %__MODULE__{url: nil}} = changeset) do case fetch_change(changeset, :url) do {:ok, _url} -> changeset diff --git a/lib/mobilizon/events/session.ex b/lib/mobilizon/events/session.ex index 5d1510425..3804d8935 100644 --- a/lib/mobilizon/events/session.ex +++ b/lib/mobilizon/events/session.ex @@ -7,7 +7,7 @@ defmodule Mobilizon.Events.Session do import Ecto.Changeset - alias Mobilizon.Events.{Event, Session, Track} + alias Mobilizon.Events.{Event, Track} @type t :: %__MODULE__{ audios_urls: String.t(), @@ -57,7 +57,7 @@ defmodule Mobilizon.Events.Session do @doc false @spec changeset(t, map) :: Ecto.Changeset.t() - def changeset(%Session{} = session, attrs) do + def changeset(%__MODULE__{} = session, attrs) do session |> cast(attrs, @attrs) |> validate_required(@required_attrs) diff --git a/lib/mobilizon/events/tag.ex b/lib/mobilizon/events/tag.ex index a342f1c68..b1d0ec8d7 100644 --- a/lib/mobilizon/events/tag.ex +++ b/lib/mobilizon/events/tag.ex @@ -7,13 +7,13 @@ defmodule Mobilizon.Events.Tag do import Ecto.Changeset - alias Mobilizon.Events.{Tag, TagRelation} + alias Mobilizon.Events.TagRelation alias Mobilizon.Events.Tag.TitleSlug @type t :: %__MODULE__{ title: String.t(), slug: TitleSlug.Type.t(), - related_tags: [Tag.t()] + related_tags: [t] } @required_attrs [:title, :slug] @@ -23,14 +23,14 @@ defmodule Mobilizon.Events.Tag do field(:title, :string) field(:slug, TitleSlug.Type) - many_to_many(:related_tags, Tag, join_through: TagRelation) + many_to_many(:related_tags, __MODULE__, join_through: TagRelation) timestamps() end @doc false @spec changeset(t, map) :: Ecto.Changeset.t() - def changeset(%Tag{} = tag, attrs) do + def changeset(%__MODULE__{} = tag, attrs) do tag |> cast(attrs, @attrs) |> TitleSlug.maybe_generate_slug() diff --git a/lib/mobilizon/events/tag_relations.ex b/lib/mobilizon/events/tag_relations.ex index 7122a6876..2d038358f 100644 --- a/lib/mobilizon/events/tag_relations.ex +++ b/lib/mobilizon/events/tag_relations.ex @@ -7,7 +7,7 @@ defmodule Mobilizon.Events.TagRelation do import Ecto.Changeset - alias Mobilizon.Events.{Tag, TagRelation} + alias Mobilizon.Events.Tag @type t :: %__MODULE__{ weight: integer, @@ -29,7 +29,7 @@ defmodule Mobilizon.Events.TagRelation do @doc false @spec changeset(t, map) :: Ecto.Changeset.t() - def changeset(%TagRelation{} = tag, attrs) do + def changeset(%__MODULE__{} = tag, attrs) do # Return if tag_id or link_id are not set because it will fail later otherwise with %Ecto.Changeset{errors: [], changes: changes} = changeset <- tag diff --git a/lib/mobilizon/events/track.ex b/lib/mobilizon/events/track.ex index a006fd2d6..dd626a823 100644 --- a/lib/mobilizon/events/track.ex +++ b/lib/mobilizon/events/track.ex @@ -7,7 +7,7 @@ defmodule Mobilizon.Events.Track do import Ecto.Changeset - alias Mobilizon.Events.{Track, Event, Session} + alias Mobilizon.Events.{Event, Session} @type t :: %__MODULE__{ color: String.t(), @@ -34,7 +34,7 @@ defmodule Mobilizon.Events.Track do @doc false @spec changeset(t, map) :: Ecto.Changeset.t() - def changeset(%Track{} = track, attrs) do + def changeset(%__MODULE__{} = track, attrs) do track |> cast(attrs, @attrs) |> validate_required(@required_attrs) diff --git a/lib/mobilizon/media/file.ex b/lib/mobilizon/media/file.ex index 611cd367d..f82acc417 100644 --- a/lib/mobilizon/media/file.ex +++ b/lib/mobilizon/media/file.ex @@ -28,8 +28,8 @@ defmodule Mobilizon.Media.File do end @doc false - @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() - def changeset(file, attrs) do + @spec changeset(t, map) :: Ecto.Changeset.t() + def changeset(%__MODULE__{} = file, attrs) do file |> cast(attrs, @attrs) |> validate_required(@required_attrs) diff --git a/lib/mobilizon/media/picture.ex b/lib/mobilizon/media/picture.ex index 00f55e75d..bd1948307 100644 --- a/lib/mobilizon/media/picture.ex +++ b/lib/mobilizon/media/picture.ex @@ -23,8 +23,8 @@ defmodule Mobilizon.Media.Picture do end @doc false - @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() - def changeset(picture, attrs) do + @spec changeset(t, map) :: Ecto.Changeset.t() + def changeset(%__MODULE__{} = picture, attrs) do picture |> cast(attrs, [:actor_id]) |> cast_embed(:file) diff --git a/lib/mobilizon/reports/note.ex b/lib/mobilizon/reports/note.ex index c28d18df4..6446f3ece 100644 --- a/lib/mobilizon/reports/note.ex +++ b/lib/mobilizon/reports/note.ex @@ -30,8 +30,8 @@ defmodule Mobilizon.Reports.Note do end @doc false - @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() - def changeset(note, attrs) do + @spec changeset(t, map) :: Ecto.Changeset.t() + def changeset(%__MODULE__{} = note, attrs) do note |> cast(attrs, @attrs) |> validate_required(@required_attrs) diff --git a/lib/mobilizon/reports/report.ex b/lib/mobilizon/reports/report.ex index 768a5ed44..77ca72cf0 100644 --- a/lib/mobilizon/reports/report.ex +++ b/lib/mobilizon/reports/report.ex @@ -50,16 +50,16 @@ defmodule Mobilizon.Reports.Report do end @doc false - @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() - def changeset(report, attrs) do + @spec changeset(t, map) :: Ecto.Changeset.t() + def changeset(%__MODULE__{} = report, attrs) do report |> cast(attrs, @attrs) |> validate_required(@required_attrs) end @doc false - @spec creation_changeset(Report.t(), map) :: Ecto.Changeset.t() - def creation_changeset(report, attrs) do + @spec creation_changeset(t, map) :: Ecto.Changeset.t() + def creation_changeset(%__MODULE__{} = report, attrs) do report |> changeset(attrs) |> put_assoc(:comments, attrs["comments"]) diff --git a/lib/mobilizon/users/user.ex b/lib/mobilizon/users/user.ex index b9da2002c..981b9de4a 100644 --- a/lib/mobilizon/users/user.ex +++ b/lib/mobilizon/users/user.ex @@ -11,7 +11,7 @@ defmodule Mobilizon.Users.User do alias Mobilizon.Crypto alias Mobilizon.Events.FeedToken alias Mobilizon.Service.EmailChecker - alias Mobilizon.Users.{User, UserRole} + alias Mobilizon.Users.UserRole @type t :: %__MODULE__{ email: String.t(), @@ -66,8 +66,8 @@ defmodule Mobilizon.Users.User do end @doc false - @spec changeset(t | Ecto.Changeset.t(), map) :: Ecto.Changeset.t() - def changeset(%User{} = user, attrs) do + @spec changeset(t, map) :: Ecto.Changeset.t() + def changeset(%__MODULE__{} = user, attrs) do changeset = user |> cast(attrs, @attrs) @@ -84,8 +84,8 @@ defmodule Mobilizon.Users.User do end @doc false - @spec registration_changeset(User.t(), map) :: Ecto.Changeset.t() - def registration_changeset(%User{} = user, attrs) do + @spec registration_changeset(t, map) :: Ecto.Changeset.t() + def registration_changeset(%__MODULE__{} = user, attrs) do user |> changeset(attrs) |> cast_assoc(:default_actor) @@ -99,14 +99,14 @@ defmodule Mobilizon.Users.User do end @doc false - @spec send_password_reset_changeset(User.t(), map) :: Ecto.Changeset.t() - def send_password_reset_changeset(%User{} = user, attrs) do + @spec send_password_reset_changeset(t, map) :: Ecto.Changeset.t() + def send_password_reset_changeset(%__MODULE__{} = user, attrs) do cast(user, attrs, [:reset_password_token, :reset_password_sent_at]) end @doc false - @spec password_reset_changeset(User.t(), map) :: Ecto.Changeset.t() - def password_reset_changeset(%User{} = user, attrs) do + @spec password_reset_changeset(t, map) :: Ecto.Changeset.t() + def password_reset_changeset(%__MODULE__{} = user, attrs) do user |> cast(attrs, @password_reset_required_attrs) |> validate_length(:password, @@ -120,15 +120,15 @@ defmodule Mobilizon.Users.User do @doc """ Checks whether an user is confirmed. """ - @spec is_confirmed(User.t()) :: boolean - def is_confirmed(%User{confirmed_at: nil}), do: false - def is_confirmed(%User{}), do: true + @spec is_confirmed(t) :: boolean + def is_confirmed(%__MODULE__{confirmed_at: nil}), do: false + def is_confirmed(%__MODULE__{}), do: true @doc """ Returns whether an user owns an actor. """ - @spec owns_actor(User.t(), integer | String.t()) :: {:is_owned, Actor.t() | nil} - def owns_actor(%User{actors: actors}, actor_id) do + @spec owns_actor(t, integer | String.t()) :: {:is_owned, Actor.t() | nil} + def owns_actor(%__MODULE__{actors: actors}, actor_id) do user_actor = Enum.find(actors, fn actor -> "#{actor.id}" == "#{actor_id}" end) {:is_owned, user_actor} From f6800665e76b86992983cfe99b943ef5fa6bf4af Mon Sep 17 00:00:00 2001 From: miffigriffy Date: Fri, 13 Sep 2019 02:28:09 +0200 Subject: [PATCH 21/29] Apply formatting --- lib/mobilizon/actors/actor.ex | 10 ++++++++-- ...0151607_split_event_visibility_and_join_options.exs | 4 ++-- .../service/activity_pub/transmogrifier_test.exs | 3 +-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex index 4dd5a1d38..5e7add0d5 100644 --- a/lib/mobilizon/actors/actor.ex +++ b/lib/mobilizon/actors/actor.ex @@ -180,11 +180,17 @@ defmodule Mobilizon.Actors.Actor do a distant actor. """ @spec preferred_username_and_domain(t) :: String.t() - def preferred_username_and_domain(%__MODULE__{preferred_username: preferred_username, domain: nil}) do + def preferred_username_and_domain(%__MODULE__{ + preferred_username: preferred_username, + domain: nil + }) do preferred_username end - def preferred_username_and_domain(%__MODULE__{preferred_username: preferred_username, domain: domain}) do + def preferred_username_and_domain(%__MODULE__{ + preferred_username: preferred_username, + domain: domain + }) do "#{preferred_username}@#{domain}" end diff --git a/priv/repo/migrations/20190130151607_split_event_visibility_and_join_options.exs b/priv/repo/migrations/20190130151607_split_event_visibility_and_join_options.exs index ecf464a9c..0255ea2af 100644 --- a/priv/repo/migrations/20190130151607_split_event_visibility_and_join_options.exs +++ b/priv/repo/migrations/20190130151607_split_event_visibility_and_join_options.exs @@ -14,7 +14,7 @@ defmodule Mobilizon.Repo.Migrations.SplitEventVisibilityAndJoinOptions do EventVisibility.create_type() execute( - "ALTER TABLE events ALTER COLUMN visibility TYPE event_visibility_type USING visibility::event_visibility" + "ALTER TABLE events ALTER COLUMN visibility TYPE event_visibility USING visibility::event_visibility" ) JoinOptions.create_type() @@ -42,7 +42,7 @@ defmodule Mobilizon.Repo.Migrations.SplitEventVisibilityAndJoinOptions do EventVisibility.create_type() execute( - "ALTER TABLE events ALTER COLUMN visibility TYPE event_visibility_type USING visibility::event_visibility" + "ALTER TABLE events ALTER COLUMN visibility TYPE event_visibility USING visibility::event_visibility" ) end end diff --git a/test/mobilizon/service/activity_pub/transmogrifier_test.exs b/test/mobilizon/service/activity_pub/transmogrifier_test.exs index 91ec98007..4db06e085 100644 --- a/test/mobilizon/service/activity_pub/transmogrifier_test.exs +++ b/test/mobilizon/service/activity_pub/transmogrifier_test.exs @@ -115,8 +115,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do test "it works for incoming notices" do data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!() - {:ok, %Activity{data: data, local: false}, _} = - Transmogrifier.handle_incoming(data) + {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) assert data["id"] == "https://framapiaf.org/users/admin/statuses/99512778738411822/activity" From 6372599493072600aea7bd32a147b2ee6f5dbd46 Mon Sep 17 00:00:00 2001 From: miffigriffi Date: Mon, 16 Sep 2019 01:13:20 +0200 Subject: [PATCH 22/29] Refactoring of Application module --- lib/mobilizon.ex | 76 +++++++++++++++++ lib/mobilizon/application.ex | 100 ----------------------- lib/{ => mobilizon_web}/mobilizon_web.ex | 0 lib/mobilizon_web/reverse_proxy.ex | 2 +- lib/service/federator.ex | 2 +- mix.exs | 2 +- 6 files changed, 79 insertions(+), 103 deletions(-) delete mode 100644 lib/mobilizon/application.ex rename lib/{ => mobilizon_web}/mobilizon_web.ex (100%) diff --git a/lib/mobilizon.ex b/lib/mobilizon.ex index 36e0d6c61..7461a267c 100644 --- a/lib/mobilizon.ex +++ b/lib/mobilizon.ex @@ -9,4 +9,80 @@ defmodule Mobilizon do Mobilizon relies on `Guardian` for auth and `Geo`/Postgis for geographical information. """ + + use Application + + import Cachex.Spec + + alias Mobilizon.Config + alias Mobilizon.Service.Export.{Feed, ICalendar} + + @name Mix.Project.config()[:name] + @version Mix.Project.config()[:version] + + @spec named_version :: String.t() + def named_version, do: "#{@name} #{@version}" + + @spec user_agent :: String.t(:w) + def user_agent do + info = "#{MobilizonWeb.Endpoint.url()} <#{Config.get([:instance, :email], "")}>" + + "#{named_version()}; #{info}" + end + + @spec start(:normal | {:takeover, node} | {:failover, node}, term) :: + {:ok, pid} | {:ok, pid, term} | {:error, term} + def start(_type, _args) do + children = [ + # supervisors + Mobilizon.Storage.Repo, + MobilizonWeb.Endpoint, + # workers + Guardian.DB.Token.SweeperServer, + Mobilizon.Service.Federator, + cachex_spec(:feed, 2500, 60, 60, &Feed.create_cache/1), + cachex_spec(:ics, 2500, 60, 60, &ICalendar.create_cache/1), + cachex_spec(:activity_pub, 2500, 3, 15) + ] + + opts = [strategy: :one_for_one, name: Mobilizon.Supervisor] + + Supervisor.start_link(children, opts) + end + + @spec config_change(keyword, keyword, [atom]) :: :ok + def config_change(changed, _new, removed) do + MobilizonWeb.Endpoint.config_change(changed, removed) + + :ok + end + + @spec cachex_spec(atom, integer, integer, integer, function | nil) :: Supervisor.child_spec() + defp cachex_spec(name, limit, default, interval, fallback \\ nil) do + %{ + id: :"cache_#{name}", + start: + {Cachex, :start_link, + [ + name, + Keyword.merge(cachex_options(limit, default, interval), fallback_options(fallback)) + ]} + } + end + + @spec cachex_options(integer, integer, integer) :: keyword + defp cachex_options(limit, default, interval) do + [ + limit: limit, + expiration: + expiration( + default: :timer.minutes(default), + interval: :timer.seconds(interval) + ) + ] + end + + @spec fallback_options(function | nil) :: keyword + defp fallback_options(nil), do: [] + defp fallback_options(fallback), do: [fallback: fallback(default: fallback)] end diff --git a/lib/mobilizon/application.ex b/lib/mobilizon/application.ex deleted file mode 100644 index 19f3d9718..000000000 --- a/lib/mobilizon/application.ex +++ /dev/null @@ -1,100 +0,0 @@ -defmodule Mobilizon.Application do - @moduledoc """ - The Mobilizon application - """ - - use Application - - import Cachex.Spec - - alias Mobilizon.Config - alias Mobilizon.Service.Export.{Feed, ICalendar} - - @name Mix.Project.config()[:name] - @version Mix.Project.config()[:version] - - # See https://hexdocs.pm/elixir/Application.html - # for more information on OTP Applications - def start(_type, _args) do - import Supervisor.Spec - - # Define workers and child supervisors to be supervised - children = [ - # Start the Ecto repository - supervisor(Mobilizon.Storage.Repo, []), - # Start the endpoint when the application starts - supervisor(MobilizonWeb.Endpoint, []), - # Start your own worker by calling: Mobilizon.Worker.start_link(arg1, arg2, arg3) - # worker(Mobilizon.Worker, [arg1, arg2, arg3]), - worker( - Cachex, - [ - :feed, - [ - limit: 2500, - expiration: - expiration( - default: :timer.minutes(60), - interval: :timer.seconds(60) - ), - fallback: fallback(default: &Feed.create_cache/1) - ] - ], - id: :cache_feed - ), - worker( - Cachex, - [ - :ics, - [ - limit: 2500, - expiration: - expiration( - default: :timer.minutes(60), - interval: :timer.seconds(60) - ), - fallback: fallback(default: &ICalendar.create_cache/1) - ] - ], - id: :cache_ics - ), - worker( - Cachex, - [ - :activity_pub, - [ - limit: 2500, - expiration: - expiration( - default: :timer.minutes(3), - interval: :timer.seconds(15) - ) - ] - ], - id: :cache_activity_pub - ), - worker(Guardian.DB.Token.SweeperServer, []), - worker(Mobilizon.Service.Federator, []) - ] - - # See https://hexdocs.pm/elixir/Supervisor.html - # for other strategies and supported options - opts = [strategy: :one_for_one, name: Mobilizon.Supervisor] - Supervisor.start_link(children, opts) - end - - # Tell Phoenix to update the endpoint configuration - # whenever the application is updated. - def config_change(changed, _new, removed) do - MobilizonWeb.Endpoint.config_change(changed, removed) - :ok - end - - def named_version, do: @name <> " " <> @version - - def user_agent do - info = "#{MobilizonWeb.Endpoint.url()} <#{Config.get([:instance, :email], "")}>" - - named_version() <> "; " <> info - end -end diff --git a/lib/mobilizon_web.ex b/lib/mobilizon_web/mobilizon_web.ex similarity index 100% rename from lib/mobilizon_web.ex rename to lib/mobilizon_web/mobilizon_web.ex diff --git a/lib/mobilizon_web/reverse_proxy.ex b/lib/mobilizon_web/reverse_proxy.ex index 98c240d04..30347097a 100644 --- a/lib/mobilizon_web/reverse_proxy.ex +++ b/lib/mobilizon_web/reverse_proxy.ex @@ -260,7 +260,7 @@ defmodule MobilizonWeb.ReverseProxy do headers, "user-agent", 0, - {"user-agent", Mobilizon.Application.user_agent()} + {"user-agent", Mobilizon.user_agent()} ) else headers diff --git a/lib/service/federator.ex b/lib/service/federator.ex index 4d8b99cd0..041e87fcb 100644 --- a/lib/service/federator.ex +++ b/lib/service/federator.ex @@ -23,7 +23,7 @@ defmodule Mobilizon.Service.Federator do {:ok, args} end - def start_link do + def start_link(_) do spawn(fn -> # 1 minute Process.sleep(1000 * 60) diff --git a/mix.exs b/mix.exs index 9386944c1..34fd71674 100644 --- a/mix.exs +++ b/mix.exs @@ -36,7 +36,7 @@ defmodule Mobilizon.Mixfile do # Type `mix help compile.app` for more information. def application do [ - mod: {Mobilizon.Application, []}, + mod: {Mobilizon, []}, extra_applications: [:logger, :runtime_tools, :guardian, :bamboo, :geolix, :crypto, :cachex] ] end From ac77a7d28a7a53cbfc049e7852cbf2df7f0c219a Mon Sep 17 00:00:00 2001 From: miffigriffi Date: Mon, 16 Sep 2019 02:07:44 +0200 Subject: [PATCH 23/29] Refactoring of Events context --- lib/mobilizon.ex | 5 +- lib/mobilizon/actors/actors.ex | 3 +- lib/mobilizon/events/activity.ex | 12 +- lib/mobilizon/events/events.ex | 1929 ++++++++--------- .../{tag_relations.ex => tag_relation.ex} | 0 lib/mobilizon_web/api/reports.ex | 2 +- lib/mobilizon_web/api/search.ex | 2 +- .../controllers/page_controller.ex | 4 +- lib/mobilizon_web/resolvers/event.ex | 12 +- lib/mobilizon_web/resolvers/tag.ex | 4 +- lib/service/activity_pub/activity_pub.ex | 12 +- lib/service/export/feed.ex | 2 +- lib/service/export/icalendar.ex | 4 +- test/mobilizon/events/events_test.exs | 61 +- .../activity_pub/activity_pub_test.exs | 4 +- test/mobilizon_web/api/search_test.exs | 4 +- .../resolvers/report_resolver_test.exs | 4 +- .../resolvers/tag_resolver_test.exs | 4 +- 18 files changed, 910 insertions(+), 1158 deletions(-) rename lib/mobilizon/events/{tag_relations.ex => tag_relation.ex} (100%) diff --git a/lib/mobilizon.ex b/lib/mobilizon.ex index 7461a267c..d454b9669 100644 --- a/lib/mobilizon.ex +++ b/lib/mobilizon.ex @@ -65,7 +65,10 @@ defmodule Mobilizon do {Cachex, :start_link, [ name, - Keyword.merge(cachex_options(limit, default, interval), fallback_options(fallback)) + Keyword.merge( + cachex_options(limit, default, interval), + fallback_options(fallback) + ) ]} } end diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index a174f0045..a4f455dd7 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -45,6 +45,7 @@ defmodule Mobilizon.Actors do :creator ]) + @public_visibility [:public, :unlisted] @administrator_roles [:creator, :administrator] @doc false @@ -836,7 +837,7 @@ defmodule Mobilizon.Actors do from( a in Actor, where: a.type == ^:Group, - where: a.visibility in ^[:public, :unlisted] + where: a.visibility in ^@public_visibility ) end diff --git a/lib/mobilizon/events/activity.ex b/lib/mobilizon/events/activity.ex index 7daf805e7..f3febd324 100644 --- a/lib/mobilizon/events/activity.ex +++ b/lib/mobilizon/events/activity.ex @@ -3,11 +3,19 @@ defmodule Mobilizon.Events.Activity do Represents an activity. """ + @type t :: %__MODULE__{ + data: String.t(), + local: boolean, + actor: Actor.t(), + recipients: [String.t()] + # notifications: [???] + } + defstruct [ :data, :local, :actor, - :recipients, - :notifications + :recipients + # :notifications ] end diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index 24c93fa5e..45a84007d 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -3,6 +3,8 @@ defmodule Mobilizon.Events do The Events context. """ + import Geo.PostGIS + import Ecto.Query import EctoEnum @@ -10,7 +12,18 @@ defmodule Mobilizon.Events do alias Mobilizon.Actors.Actor alias Mobilizon.Addresses.Address - alias Mobilizon.Events.{Event, Comment, Participant} + + alias Mobilizon.Events.{ + Comment, + Event, + FeedToken, + Participant, + Session, + Tag, + TagRelation, + Track + } + alias Mobilizon.Storage.{Page, Repo} alias Mobilizon.Users.User @@ -63,6 +76,20 @@ defmodule Mobilizon.Events do :creator ]) + @public_visibility [:public, :unlisted] + + @event_preloads [ + :organizer_actor, + :sessions, + :tracks, + :tags, + :participants, + :physical_address, + :picture + ] + + @comment_preloads [:actor, :attributed_to, :in_reply_to_comment] + @doc false @spec data :: Dataloader.Ecto.t() def data, do: Dataloader.Ecto.new(Repo, query: &query/2) @@ -71,378 +98,154 @@ defmodule Mobilizon.Events do @spec query(Ecto.Query.t(), map) :: Ecto.Query.t() def query(queryable, _params), do: queryable - def get_public_events_for_actor(%Actor{id: actor_id} = _actor, page \\ nil, limit \\ nil) do - query = - from( - e in Event, - where: e.organizer_actor_id == ^actor_id and e.visibility in [^:public, ^:unlisted], - order_by: [desc: :id], - preload: [ - :organizer_actor, - :sessions, - :tracks, - :tags, - :participants, - :physical_address, - :picture - ] - ) - |> Page.paginate(page, limit) - - events = Repo.all(query) - - count_events = - Repo.one(from(e in Event, select: count(e.id), where: e.organizer_actor_id == ^actor_id)) - - {:ok, events, count_events} - end - @doc """ - Get an actor's eventual upcoming public event + Gets a single event. """ - @spec get_actor_upcoming_public_event(Actor.t(), String.t()) :: Event.t() | nil - def get_actor_upcoming_public_event(%Actor{id: actor_id} = _actor, not_event_uuid \\ nil) do - query = - from( - e in Event, - where: - e.organizer_actor_id == ^actor_id and e.visibility in [^:public, ^:unlisted] and - e.begins_on > ^DateTime.utc_now(), - order_by: [asc: :begins_on], - limit: 1, - preload: [ - :organizer_actor, - :tags, - :participants, - :physical_address - ] - ) + @spec get_event(integer | String.t()) :: {:ok, Event.t()} | {:error, :event_not_found} + def get_event(id) do + case Repo.get(Event, id) do + %Event{} = event -> + {:ok, event} - query = - if is_nil(not_event_uuid), - do: query, - else: from(q in query, where: q.uuid != ^not_event_uuid) - - Repo.one(query) - end - - def count_local_events do - Repo.one( - from( - e in Event, - select: count(e.id), - where: e.local == ^true and e.visibility in [^:public, ^:unlisted] - ) - ) - end - - def count_local_comments do - Repo.one( - from( - c in Comment, - select: count(c.id), - where: c.local == ^true and c.visibility in [^:public, ^:unlisted] - ) - ) - end - - import Geo.PostGIS - - @doc """ - Find close events to coordinates - - Radius is in meters and defaults to 50km. - """ - @spec find_close_events(number(), number(), number(), number()) :: list(Event.t()) - def find_close_events(lon, lat, radius \\ 50_000, srid \\ 4326) do - with {:ok, point} <- Geo.WKT.decode("SRID=#{srid};POINT(#{lon} #{lat})") do - Repo.all( - from( - e in Event, - join: a in Address, - on: a.id == e.physical_address_id, - where: e.visibility == ^:public and st_dwithin_in_meters(^point, a.geom, ^radius), - preload: :organizer_actor - ) - ) + nil -> + {:error, :event_not_found} end end @doc """ Gets a single event. - - Raises `Ecto.NoResultsError` if the Event does not exist. - - ## Examples - - iex> get_event!(123) - %Event{} - - iex> get_event!(456) - ** (Ecto.NoResultsError) - + Raises `Ecto.NoResultsError` if the event does not exist. """ + @spec get_event!(integer | String.t()) :: Event.t() def get_event!(id), do: Repo.get!(Event, id) @doc """ - Gets a single event. + Gets a single event, with all associations loaded. """ - def get_event(id) do + @spec get_event_with_preload(integer | String.t()) :: + {:ok, Event.t()} | {:error, :event_not_found} + def get_event_with_preload(id) do case Repo.get(Event, id) do - nil -> {:error, :event_not_found} - event -> {:ok, event} + %Event{} = event -> + {:ok, Repo.preload(event, @event_preloads)} + + nil -> + {:error, :event_not_found} end end @doc """ - Gets an event by it's URL + Gets a single event, with all associations loaded. + Raises `Ecto.NoResultsError` if the event does not exist. """ + @spec get_event_with_preload!(integer | String.t()) :: Event.t() + def get_event_with_preload!(id) do + Event + |> Repo.get!(id) + |> Repo.preload(@event_preloads) + end + + @doc """ + Gets an event by its URL. + """ + @spec get_event_by_url(String.t()) :: Event.t() | nil def get_event_by_url(url) do - Repo.get_by(Event, url: url) - end - - @doc """ - Gets an event by it's URL - """ - def get_event_by_url!(url) do - Repo.get_by!(Event, url: url) - end - - # @doc """ - # Gets an event by it's UUID - # """ - # @depreciated "Use get_event_full_by_uuid/3 instead" - # def get_event_by_uuid(uuid) do - # Repo.get_by(Event, uuid: uuid) - # end - - @doc """ - Gets a full event by it's UUID - """ - @spec get_event_full_by_uuid(String.t()) :: Event.t() - def get_event_full_by_uuid(uuid) do - from( - e in Event, - where: e.uuid == ^uuid and e.visibility in [^:public, ^:unlisted], - preload: [ - :organizer_actor, - :sessions, - :tracks, - :tags, - :participants, - :physical_address, - :picture - ] - ) + url + |> event_by_url_query() |> Repo.one() end - def get_cached_event_full_by_uuid(uuid) do + @doc """ + Gets an event by its URL. + Raises `Ecto.NoResultsError` if the event does not exist. + """ + @spec get_event_by_url!(String.t()) :: Event.t() + def get_event_by_url!(url) do + url + |> event_by_url_query() + |> Repo.one!() + end + + @doc """ + Gets an event by its URL, with all associations loaded. + """ + @spec get_public_event_by_url_with_preload(String.t()) :: + {:ok, Event.t()} | {:error, :event_not_found} + def get_public_event_by_url_with_preload(url) do + event = + url + |> event_by_url_query() + |> filter_public_visibility() + |> preload_for_event() + |> Repo.one() + + case event do + %Event{} = event -> + {:ok, event} + + nil -> + {:error, :event_not_found} + end + end + + @doc """ + Gets an event by its URL, with all associations loaded. + Raises `Ecto.NoResultsError` if the event does not exist. + """ + @spec get_public_event_by_url_with_preload(String.t()) :: Event.t() + def get_public_event_by_url_with_preload!(url) do + url + |> event_by_url_query() + |> filter_public_visibility() + |> preload_for_event() + |> Repo.one!() + end + + @doc """ + Gets an event by its UUID, with all associations loaded. + """ + @spec get_public_event_by_uuid_with_preload(String.t()) :: Event.t() | nil + def get_public_event_by_uuid_with_preload(uuid) do + uuid + |> event_by_uuid_query() + |> filter_public_visibility() + |> preload_for_event() + |> Repo.one() + end + + @doc """ + Gets an actor's eventual upcoming public event. + """ + @spec get_upcoming_public_event_for_actor(Actor.t(), String.t() | nil) :: Event.t() | nil + def get_upcoming_public_event_for_actor(%Actor{id: actor_id}, not_event_uuid \\ nil) do + actor_id + |> upcoming_public_event_for_actor_query() + |> filter_public_visibility() + |> filter_not_event_uuid(not_event_uuid) + |> Repo.one() + end + + # TODO: move to MobilizonWeb + @spec get_cached_public_event_by_uuid_with_preload(String.t()) :: + {:commit, Event.t()} | {:ignore, nil} + def get_cached_public_event_by_uuid_with_preload(uuid) do Cachex.fetch(:activity_pub, "event_" <> uuid, fn "event_" <> uuid -> - case get_event_full_by_uuid(uuid) do + case get_public_event_by_uuid_with_preload(uuid) do %Event{} = event -> {:commit, event} - _ -> + nil -> {:ignore, nil} end end) end @doc """ - Gets a single event, with all associations loaded. - """ - def get_event_full!(id) do - event = Repo.get!(Event, id) - - Repo.preload(event, [ - :organizer_actor, - :sessions, - :tracks, - :tags, - :participants, - :physical_address - ]) - end - - @doc """ - Gets a single event, with all associations loaded. - """ - def get_event_full(id) do - case Repo.get(Event, id) do - %Event{} = event -> - {:ok, - Repo.preload(event, [ - :organizer_actor, - :sessions, - :tracks, - :tags, - :participants, - :physical_address, - :picture - ])} - - _err -> - {:error, :event_not_found} - end - end - - @doc """ - Gets an event by it's URL - """ - def get_event_full_by_url(url) do - case Repo.one( - from(e in Event, - where: e.url == ^url and e.visibility in [^:public, ^:unlisted], - preload: [ - :organizer_actor, - :sessions, - :tracks, - :tags, - :participants, - :physical_address - ] - ) - ) do - nil -> {:error, :event_not_found} - event -> {:ok, event} - end - end - - @doc """ - Gets an event by it's URL - """ - def get_event_full_by_url!(url) do - Repo.one( - from(e in Event, - where: e.url == ^url and e.visibility in [^:public, ^:unlisted], - preload: [ - :organizer_actor, - :sessions, - :tracks, - :tags, - :participants, - :physical_address, - :picture - ] - ) - ) - end - - @doc """ - Returns the list of events. - - ## Examples - - iex> list_events() - [%Event{}, ...] - - """ - @spec list_events(integer(), integer(), atom(), atom()) :: list(Event.t()) - def list_events( - page \\ nil, - limit \\ nil, - sort \\ :begins_on, - direction \\ :asc, - unlisted \\ false, - future \\ true - ) do - query = - from( - e in Event, - preload: [:organizer_actor, :participants] - ) - |> Page.paginate(page, limit) - |> sort(sort, direction) - |> restrict_future_events(future) - |> allow_unlisted(unlisted) - - Repo.all(query) - end - - # Make sure we only show future events - @spec restrict_future_events(Ecto.Query.t(), boolean()) :: Ecto.Query.t() - defp restrict_future_events(query, true), - do: from(q in query, where: q.begins_on > ^DateTime.utc_now()) - - defp restrict_future_events(query, false), do: query - - # Make sure unlisted events don't show up where they're not allowed - @spec allow_unlisted(Ecto.Query.t(), boolean()) :: Ecto.Query.t() - defp allow_unlisted(query, true), - do: from(q in query, where: q.visibility in [^:public, ^:unlisted]) - - defp allow_unlisted(query, false), do: from(q in query, where: q.visibility == ^:public) - - @doc """ - Find events by name - """ - def find_and_count_events_by_name(name, page \\ nil, limit \\ nil) - - def find_and_count_events_by_name(name, page, limit) do - name = String.trim(name) - - query = - from(e in Event, - where: - e.visibility == ^:public and - fragment( - "f_unaccent(?) %> f_unaccent(?)", - e.title, - ^name - ), - order_by: - fragment( - "word_similarity(?, ?) desc", - e.title, - ^name - ), - preload: [:organizer_actor] - ) - |> Page.paginate(page, limit) - - total = Task.async(fn -> Repo.aggregate(query, :count, :id) end) - elements = Task.async(fn -> Repo.all(query) end) - - %{total: Task.await(total), elements: Task.await(elements)} - end - - @doc """ - Find events with the same tags - """ - @spec find_similar_events_by_common_tags(list(), integer()) :: {:ok, list(Event.t())} - def find_similar_events_by_common_tags(tags, limit \\ 2) do - tags_ids = Enum.map(tags, & &1.id) - - query = - from(e in Event, - distinct: e.uuid, - join: te in "events_tags", - on: e.id == te.event_id, - where: e.begins_on > ^DateTime.utc_now(), - where: e.visibility in [^:public, ^:unlisted], - where: te.tag_id in ^tags_ids, - order_by: [asc: e.begins_on], - limit: ^limit - ) - - Repo.all(query) - end - - @doc """ - Creates a event. - - ## Examples - - iex> create_event(%{field: value}) - {:ok, %Event{}} - - iex> create_event(%{field: bad_value}) - {:error, %Ecto.Changeset{}} - + Creates an event. """ + @spec create_event(map) :: {:ok, Event.t()} | {:error, Ecto.Changeset.t()} def create_event(attrs \\ %{}) do - with %Event{} = event <- do_create_event(attrs), + with {:ok, %Event{} = event} <- do_create_event(attrs), {:ok, %Participant{} = _participant} <- %Participant{} |> Participant.changeset(%{ @@ -455,10 +258,14 @@ defmodule Mobilizon.Events do end end + @spec do_create_event(map) :: {:ok, Event.t()} | {:error, Ecto.Changeset.t()} defp do_create_event(attrs) do - with {:ok, %Event{} = event} <- %Event{} |> Event.changeset(attrs) |> Repo.insert(), + with {:ok, %Event{} = event} <- + %Event{} + |> Event.changeset(attrs) + |> Repo.insert(), %Event{} = event <- - event |> Repo.preload([:tags, :organizer_actor, :physical_address, :picture]), + Repo.preload(event, [:tags, :organizer_actor, :physical_address, :picture]), {:has_tags, true, _} <- {:has_tags, Map.has_key?(attrs, "tags"), event} do event |> Ecto.Changeset.change() @@ -466,7 +273,7 @@ defmodule Mobilizon.Events do |> Repo.update() else {:has_tags, false, event} -> - event + {:ok, event} error -> error @@ -474,17 +281,9 @@ defmodule Mobilizon.Events do end @doc """ - Updates a event. - - ## Examples - - iex> update_event(event, %{field: new_value}) - {:ok, %Event{}} - - iex> update_event(event, %{field: bad_value}) - {:error, %Ecto.Changeset{}} - + Updates an event. """ + @spec update_event(Event.t(), map) :: {:ok, Event.t()} | {:error, Ecto.Changeset.t()} def update_event(%Event{} = event, attrs) do event |> Repo.preload(:tags) @@ -493,117 +292,132 @@ defmodule Mobilizon.Events do end @doc """ - Deletes a Event. - - ## Examples - - iex> delete_event(event) - {:ok, %Event{}} - - iex> delete_event(event) - {:error, %Ecto.Changeset{}} - + Deletes an event. """ - def delete_event(%Event{} = event) do - Repo.delete(event) - end + @spec delete_event(Event.t()) :: {:ok, Event.t()} | {:error, Ecto.Changeset.t()} + def delete_event(%Event{} = event), do: Repo.delete(event) @doc """ - Deletes a Event. - + Deletes an event. Raises an exception if it fails. """ - def delete_event!(%Event{} = event) do - Repo.delete!(event) + @spec delete_event(Event.t()) :: Event.t() + def delete_event!(%Event{} = event), do: Repo.delete!(event) + + @doc """ + Returns the list of events. + """ + @spec list_events(integer | nil, integer | nil, atom, atom, boolean, boolean) :: [Event.t()] + def list_events( + page \\ nil, + limit \\ nil, + sort \\ :begins_on, + direction \\ :asc, + is_unlisted \\ false, + is_future \\ true + ) do + from(e in Event, preload: [:organizer_actor, :participants]) + |> Page.paginate(page, limit) + |> sort(sort, direction) + |> filter_future_events(is_future) + |> filter_unlisted(is_unlisted) + |> Repo.all() end @doc """ - Returns an `%Ecto.Changeset{}` for tracking event changes. - - ## Examples - - iex> change_event(event) - %Ecto.Changeset{source: %Event{}} - + Returns the list of events with the same tags. """ - def change_event(%Event{} = event) do - Event.changeset(event, %{}) + @spec list_events_by_tags([Tag.t()], integer) :: [Event.t()] + def list_events_by_tags(tags, limit \\ 2) do + tags + |> Enum.map(& &1.id) + |> events_by_tags_query(limit) + |> Repo.all() end - alias Mobilizon.Events.Tag - @doc """ - Returns the list of tags. - - ## Examples - - iex> list_tags() - [%Tag{}, ...] - + Lists public events for the actor, with all associations loaded. """ - def list_tags(page \\ nil, limit \\ nil) do - Repo.all( - Tag + @spec list_public_events_for_actor(Actor.t(), integer | nil, integer | nil) :: + {:ok, [Event.t()], integer} + def list_public_events_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do + events = + actor_id + |> event_for_actor_query() + |> filter_public_visibility() + |> preload_for_event() |> Page.paginate(page, limit) - ) + |> Repo.all() + + events_count = + actor_id + |> count_events_for_actor_query() + |> Repo.one() + + {:ok, events, events_count} end @doc """ - Returns the list of tags for an event. - - ## Examples - - iex> list_tags_for_event(id) - [%Participant{}, ...] - + Finds close events to coordinates. + Radius is in meters and defaults to 50km. """ - def list_tags_for_event(id, page \\ nil, limit \\ nil) do - Repo.all( - from( - t in Tag, - join: e in "events_tags", - on: t.id == e.tag_id, - where: e.event_id == ^id - ) - |> Page.paginate(page, limit) - ) + @spec find_close_events(number, number, number, number) :: [Event.t()] + def find_close_events(lon, lat, radius \\ 50_000, srid \\ 4326) do + "SRID=#{srid};POINT(#{lon} #{lat})" + |> Geo.WKT.decode!() + |> close_events_query(radius) + |> Repo.all() + end + + @doc """ + Counts local events. + """ + @spec count_local_events :: integer + def count_local_events do + count_local_events_query() + |> filter_public_visibility() + |> Repo.one() + end + + @doc """ + Builds a page struct for events by their name. + """ + @spec build_events_by_name(String.t(), integer | nil, integer | nil) :: Page.t() + def build_events_by_name(name, page \\ nil, limit \\ nil) do + name + |> String.trim() + |> events_by_name_query() + |> Page.build_page(page, limit) end @doc """ Gets a single tag. - - Raises `Ecto.NoResultsError` if the Tag does not exist. - - ## Examples - - iex> get_tag!(123) - %Tag{} - - iex> get_tag!(456) - ** (Ecto.NoResultsError) - """ - def get_tag!(id), do: Repo.get!(Tag, id) - + @spec get_tag(integer | String.t()) :: Tag.t() | nil def get_tag(id), do: Repo.get(Tag, id) - def get_tag_by_slug(slug) do - query = - from( - t in Tag, - where: t.slug == ^slug - ) + @doc """ + Gets a single tag. + Raises `Ecto.NoResultsError` if the tag does not exist. + """ + @spec get_tag!(integer | String.t()) :: Tag.t() + def get_tag!(id), do: Repo.get!(Tag, id) - Repo.one(query) + @doc """ + Gets a tag by its slug. + """ + @spec get_tag_by_slug(String.t()) :: Tag.t() | nil + def get_tag_by_slug(slug) do + slug + |> tag_by_slug_query() + |> Repo.one() end @doc """ - Get an existing tag or create one + Gets an existing tag or creates the new one. """ - @spec get_or_create_tag(map()) :: {:ok, Tag.t()} | {:error, any()} - def get_or_create_tag(tag) do - "#" <> title = tag["name"] - + @spec get_or_create_tag(map) :: {:ok, Tag.t()} | {:error, Ecto.Changeset.t()} + def get_or_create_tag(%{"name" => "#" <> title}) do case Repo.get_by(Tag, title: title) do %Tag{} = tag -> {:ok, tag} @@ -615,16 +429,8 @@ defmodule Mobilizon.Events do @doc """ Creates a tag. - - ## Examples - - iex> create_tag(%{field: value}) - {:ok, %Tag{}} - - iex> create_tag(%{field: bad_value}) - {:error, %Ecto.Changeset{}} - """ + @spec create_tag(map) :: {:ok, Tag.t()} | {:error, Ecto.Changeset.t()} def create_tag(attrs \\ %{}) do %Tag{} |> Tag.changeset(attrs) @@ -633,16 +439,8 @@ defmodule Mobilizon.Events do @doc """ Updates a tag. - - ## Examples - - iex> update_tag(tag, %{field: new_value}) - {:ok, %Tag{}} - - iex> update_tag(tag, %{field: bad_value}) - {:error, %Ecto.Changeset{}} - """ + @spec update_tag(Tag.t(), map) :: {:ok, Tag.t()} | {:error, Ecto.Changeset.t()} def update_tag(%Tag{} = tag, attrs) do tag |> Tag.changeset(attrs) @@ -650,70 +448,72 @@ defmodule Mobilizon.Events do end @doc """ - Deletes a Tag. - - ## Examples - - iex> delete_tag(tag) - {:ok, %Tag{}} - - iex> delete_tag(tag) - {:error, %Ecto.Changeset{}} - + Deletes a tag. """ - def delete_tag(%Tag{} = tag) do - Repo.delete(tag) + @spec delete_tag(Tag.t()) :: {:ok, Tag.t()} | {:error, Ecto.Changeset.t()} + def delete_tag(%Tag{} = tag), do: Repo.delete(tag) + + @doc """ + Returns the list of tags. + """ + @spec list_tags(integer | nil, integer | nil) :: [Tag.t()] + def list_tags(page \\ nil, limit \\ nil) do + Tag + |> Page.paginate(page, limit) + |> Repo.all() end @doc """ - Returns an `%Ecto.Changeset{}` for tracking tag changes. - - ## Examples - - iex> change_tag(tag) - %Ecto.Changeset{source: %Tag{}} - + Returns the list of tags for the event. """ - def change_tag(%Tag{} = tag) do - Tag.changeset(tag, %{}) + @spec list_tags_for_event(integer | String.t(), integer | nil, integer | nil) :: [Tag.t()] + def list_tags_for_event(event_id, page \\ nil, limit \\ nil) do + event_id + |> tags_for_event_query() + |> Page.paginate(page, limit) + |> Repo.all() end - alias Mobilizon.Events.TagRelation + @doc """ + Checks whether two tags are linked or not. + """ + @spec are_tags_linked(Tag.t(), Tag.t()) :: boolean + def are_tags_linked(%Tag{id: tag1_id}, %Tag{id: tag2_id}) do + tag_relation = + tag1_id + |> tags_linked_query(tag2_id) + |> Repo.one() + + !!tag_relation + end @doc """ - Create a relation between two tags + Creates a relation between two tags. """ - @spec create_tag_relation(map()) :: {:ok, TagRelation.t()} | {:error, Ecto.Changeset.t()} + @spec create_tag_relation(map) :: {:ok, TagRelation.t()} | {:error, Ecto.Changeset.t()} def create_tag_relation(attrs \\ {}) do %TagRelation{} |> TagRelation.changeset(attrs) - |> Repo.insert(conflict_target: [:tag_id, :link_id], on_conflict: [inc: [weight: 1]]) + |> Repo.insert( + conflict_target: [:tag_id, :link_id], + on_conflict: [inc: [weight: 1]] + ) end @doc """ - Remove a tag relation + Removes a tag relation. """ + @spec delete_tag_relation(TagRelation.t()) :: + {:ok, TagRelation.t()} | {:error, Ecto.Changeset.t()} def delete_tag_relation(%TagRelation{} = tag_relation) do Repo.delete(tag_relation) end - @doc """ - Returns whether two tags are linked or not - """ - def are_tags_linked(%Tag{id: tag1_id}, %Tag{id: tag2_id}) do - case from(tr in TagRelation, - where: tr.tag_id == ^min(tag1_id, tag2_id) and tr.link_id == ^max(tag1_id, tag2_id) - ) - |> Repo.one() do - %TagRelation{} -> true - _ -> false - end - end - @doc """ Returns the tags neighbors for a given tag - We can't rely on the single many_to_many relation since we also want tags that link to our tag, not just tags linked by this one + We can't rely on the single many_to_many relation since we also want tags that + link to our tag, not just tags linked by this one. The SQL query looks like this: ```sql @@ -732,196 +532,74 @@ defmodule Mobilizon.Events do DESC; ``` """ - def tag_neighbors(%Tag{id: id}, relation_minimum \\ 1, limit \\ 10) do - query2 = - from(tr in TagRelation, - select: %{id: tr.tag_id, weight: tr.weight}, - where: tr.link_id == ^id - ) - - query = - from(tr in TagRelation, - select: %{id: tr.link_id, weight: tr.weight}, - union_all: ^query2, - where: tr.tag_id == ^id - ) - - final_query = - from(t in Tag, - right_join: q in subquery(query), - on: [id: t.id], - where: q.weight >= ^relation_minimum, - limit: ^limit, - order_by: [desc: q.weight] - ) - - Repo.all(final_query) - end - - alias Mobilizon.Events.Participant - - @doc """ - Returns the list of participants. - - ## Examples - - iex> list_participants() - [%Participant{}, ...] - - """ - def list_participants do - Repo.all(Participant) - end - - @doc """ - Returns the list of participants for an event. - - Default behaviour is to not return :not_approved participants - - ## Examples - - iex> list_participants_for_event(some_uuid) - [%Participant{}, ...] - - """ - def list_participants_for_event(uuid, page \\ nil, limit \\ nil, include_not_improved \\ false) - - def list_participants_for_event(uuid, page, limit, false) do - query = do_list_participants_for_event(uuid, page, limit) - query = from(p in query, where: p.role != ^:not_approved) - Repo.all(query) - end - - def list_participants_for_event(uuid, page, limit, true) do - query = do_list_participants_for_event(uuid, page, limit) - Repo.all(query) - end - - defp do_list_participants_for_event(uuid, page, limit) do - from( - p in Participant, - join: e in Event, - on: p.event_id == e.id, - where: e.uuid == ^uuid, - preload: [:actor] - ) - |> Page.paginate(page, limit) - end - - @doc """ - Returns the list of participations for an actor. - - Default behaviour is to not return :not_approved participants - - ## Examples - - iex> list_participants_for_actor(%Actor{}) - [%Participant{}, ...] - - """ - def list_event_participations_for_actor(%Actor{id: id}, page \\ nil, limit \\ nil) do - Repo.all( - from( - e in Event, - join: p in Participant, - join: a in Actor, - on: p.actor_id == a.id, - on: p.event_id == e.id, - where: a.id == ^id and p.role != ^:not_approved, - preload: [:picture, :tags] - ) - |> Page.paginate(page, limit) - ) - end - - @doc """ - Returns the list of organizers participants for an event. - - ## Examples - - iex> list_organizers_participants_for_event(id) - [%Participant{role: :creator}, ...] - - """ - def list_organizers_participants_for_event(id, page \\ nil, limit \\ nil) do - Repo.all( - from( - p in Participant, - where: p.event_id == ^id and p.role == ^:creator, - preload: [:actor] - ) - |> Page.paginate(page, limit) - ) + @spec list_tag_neighbors(Tag.t(), integer, integer) :: [Tag.t()] + def list_tag_neighbors(%Tag{id: tag_id}, relation_minimum \\ 1, limit \\ 10) do + tag_id + |> tag_relation_subquery() + |> tag_relation_union_subquery(tag_id) + |> tag_neighbors_query(relation_minimum, limit) + |> Repo.all() end @doc """ Gets a single participant. - - Raises `Ecto.NoResultsError` if the Participant does not exist. - - ## Examples - - iex> get_participant!(123) - %Participant{} - - iex> get_participant!(456) - ** (Ecto.NoResultsError) - """ + @spec get_participant(integer | String.t(), integer | String.t()) :: + {:ok, Participant.t()} | {:error, :participant_not_found} + def get_participant(event_id, actor_id) do + case Repo.get_by(Participant, event_id: event_id, actor_id: actor_id) do + %Participant{} = participant -> + {:ok, participant} + + nil -> + {:error, :participant_not_found} + end + end + + @doc """ + Gets a single participant. + Raises `Ecto.NoResultsError` if the participant does not exist. + """ + @spec get_participant!(integer | String.t(), integer | String.t()) :: Participant.t() def get_participant!(event_id, actor_id) do Repo.get_by!(Participant, event_id: event_id, actor_id: actor_id) end @doc """ - Get a single participant + Gets a participant by its URL. """ - def get_participant(event_id, actor_id) do - case Repo.get_by(Participant, event_id: event_id, actor_id: actor_id) do - nil -> {:error, :participant_not_found} - participant -> {:ok, participant} - end - end - + @spec get_participant_by_url(String.t()) :: Participant.t() | nil def get_participant_by_url(url) do - Repo.one( - from(p in Participant, - where: p.url == ^url, - preload: [:actor, :event] - ) - ) + url + |> participant_by_url_query() + |> Repo.one() end @doc """ - Creates a participant. - - ## Examples - - iex> create_participant(%{field: value}) - {:ok, %Participant{}} - - iex> create_participant(%{field: bad_value}) - {:error, %Ecto.Changeset{}} - + Gets the default participant role depending on the event join options. """ + @spec get_default_participant_role(Event.t()) :: :participant | :not_approved + def get_default_participant_role(%Event{join_options: :free}), do: :participant + def get_default_participant_role(%Event{join_options: _}), do: :not_approved + + @doc """ + Creates a participant. + """ + @spec create_participant(map) :: {:ok, Participant.t()} | {:error, Ecto.Changeset.t()} def create_participant(attrs \\ %{}) do with {:ok, %Participant{} = participant} <- - %Participant{} |> Participant.changeset(attrs) |> Repo.insert() do + %Participant{} + |> Participant.changeset(attrs) + |> Repo.insert() do {:ok, Repo.preload(participant, [:event, :actor])} end end @doc """ Updates a participant. - - ## Examples - - iex> update_participant(participant, %{field: new_value}) - {:ok, %Participant{}} - - iex> update_participant(participant, %{field: bad_value}) - {:error, %Ecto.Changeset{}} - """ + @spec update_participant(Participant.t(), map) :: + {:ok, Participant.t()} | {:error, Ecto.Changeset.t()} def update_participant(%Participant{} = participant, attrs) do participant |> Participant.changeset(attrs) @@ -929,112 +607,88 @@ defmodule Mobilizon.Events do end @doc """ - Deletes a Participant. - - ## Examples - - iex> delete_participant(participant) - {:ok, %Participant{}} - - iex> delete_participant(participant) - {:error, %Ecto.Changeset{}} - + Deletes a participant. """ - def delete_participant(%Participant{} = participant) do - Repo.delete(participant) - end + @spec delete_participant(Participant.t()) :: + {:ok, Participant.t()} | {:error, Ecto.Changeset.t()} + def delete_participant(%Participant{} = participant), do: Repo.delete(participant) @doc """ - Returns an `%Ecto.Changeset{}` for tracking participant changes. - - ## Examples - - iex> change_participant(participant) - %Ecto.Changeset{source: %Participant{}} - + Returns the list of participants. """ - def change_participant(%Participant{} = participant) do - Participant.changeset(participant, %{}) - end + @spec list_participants :: [Participant.t()] + def list_participants, do: Repo.all(Participant) @doc """ - Get the default participant role depending on the event join options + Returns the list of participants for an event. + Default behaviour is to not return :not_approved participants """ - def get_default_participant_role(%Event{} = event) do - case event.join_options do - # Participant - :free -> :participant - # Not approved - _ -> :not_approved - end - end - - @doc """ - List event participation requests for an actor - """ - @spec list_requests_for_actor(Actor.t()) :: list(Participant.t()) - def list_requests_for_actor(%Actor{id: actor_id}) do - Repo.all(from(p in Participant, where: p.actor_id == ^actor_id and p.role == ^:not_approved)) - end - - alias Mobilizon.Events.Session - - @doc """ - Returns the list of sessions. - - ## Examples - - iex> list_sessions() - [%Session{}, ...] - - """ - def list_sessions do - Repo.all(Session) - end - - @doc """ - Returns the list of sessions for an event - """ - @spec list_sessions_for_event(Event.t()) :: list(Session.t()) - def list_sessions_for_event(%Event{id: event_id}) do - Repo.all( - from( - s in Session, - join: e in Event, - on: s.event_id == e.id, - where: e.id == ^event_id + @spec list_participants_for_event(String.t(), integer | nil, integer | nil, boolean) :: + [Participant.t()] + def list_participants_for_event( + event_uuid, + page \\ nil, + limit \\ nil, + include_not_improved \\ false ) - ) + + def list_participants_for_event(event_uuid, page, limit, include_not_improved) do + event_uuid + |> participants_for_event() + |> filter_role(include_not_improved) + |> Page.paginate(page, limit) + |> Repo.all() + end + + @doc """ + Returns the list of organizers participants for an event. + """ + @spec list_organizers_participants_for_event( + integer | String.t(), + integer | nil, + integer | nil + ) :: + [Participant.t()] + def list_organizers_participants_for_event(event_id, page \\ nil, limit \\ nil) do + event_id + |> organizers_participants_for_event() + |> Page.paginate(page, limit) + |> Repo.all() + end + + @doc """ + Returns the list of event participation requests for an actor. + """ + @spec list_requests_for_actor(Actor.t()) :: [Participant.t()] + def list_requests_for_actor(%Actor{id: actor_id}) do + actor_id + |> requests_for_actor_query() + |> Repo.all() + end + + @doc """ + Returns the list of participations for an actor. + """ + @spec list_event_participations_for_actor(Actor.t(), integer | nil, integer | nil) :: + [Event.t()] + def list_event_participations_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do + actor_id + |> event_participations_for_actor_query() + |> Page.paginate(page, limit) + |> Repo.all() end @doc """ Gets a single session. - - Raises `Ecto.NoResultsError` if the Session does not exist. - - ## Examples - - iex> get_session!(123) - %Session{} - - iex> get_session!(456) - ** (Ecto.NoResultsError) - + Raises `Ecto.NoResultsError` if the session does not exist. """ + @spec get_session!(integer | String.t()) :: Session.t() def get_session!(id), do: Repo.get!(Session, id) @doc """ Creates a session. - - ## Examples - - iex> create_session(%{field: value}) - {:ok, %Session{}} - - iex> create_session(%{field: bad_value}) - {:error, %Ecto.Changeset{}} - """ + @spec create_session(map) :: {:ok, Session.t()} | {:error, Ecto.Changeset.t()} def create_session(attrs \\ %{}) do %Session{} |> Session.changeset(attrs) @@ -1043,16 +697,8 @@ defmodule Mobilizon.Events do @doc """ Updates a session. - - ## Examples - - iex> update_session(session, %{field: new_value}) - {:ok, %Session{}} - - iex> update_session(session, %{field: bad_value}) - {:error, %Ecto.Changeset{}} - """ + @spec update_session(Session.t(), map) :: {:ok, Session.t()} | {:error, Ecto.Changeset.t()} def update_session(%Session{} = session, attrs) do session |> Session.changeset(attrs) @@ -1060,85 +706,38 @@ defmodule Mobilizon.Events do end @doc """ - Deletes a Session. - - ## Examples - - iex> delete_session(session) - {:ok, %Session{}} - - iex> delete_session(session) - {:error, %Ecto.Changeset{}} - + Deletes a session. """ - def delete_session(%Session{} = session) do - Repo.delete(session) - end + @spec delete_session(Session.t()) :: {:ok, Session.t()} | {:error, Ecto.Changeset.t()} + def delete_session(%Session{} = session), do: Repo.delete(session) @doc """ - Returns an `%Ecto.Changeset{}` for tracking session changes. - - ## Examples - - iex> change_session(session) - %Ecto.Changeset{source: %Session{}} - + Returns the list of sessions. """ - def change_session(%Session{} = session) do - Session.changeset(session, %{}) - end - - alias Mobilizon.Events.Track + @spec list_sessions :: [Session.t()] + def list_sessions, do: Repo.all(Session) @doc """ - Returns the list of tracks. - - ## Examples - - iex> list_tracks() - [%Track{}, ...] - + Returns the list of sessions for the event. """ - def list_tracks do - Repo.all(Track) - end - - @doc """ - Returns the list of sessions for a track - """ - @spec list_sessions_for_track(Track.t()) :: list(Session.t()) - def list_sessions_for_track(%Track{id: track_id}) do - Repo.all(from(s in Session, where: s.track_id == ^track_id)) + @spec list_sessions_for_event(Event.t()) :: [Session.t()] + def list_sessions_for_event(%Event{id: event_id}) do + event_id + |> sessions_for_event_query() + |> Repo.all() end @doc """ Gets a single track. - - Raises `Ecto.NoResultsError` if the Track does not exist. - - ## Examples - - iex> get_track!(123) - %Track{} - - iex> get_track!(456) - ** (Ecto.NoResultsError) - + Raises `Ecto.NoResultsError` if the track does not exist. """ + @spec get_track!(integer | String.t()) :: Track.t() def get_track!(id), do: Repo.get!(Track, id) @doc """ Creates a track. - - ## Examples - - iex> create_track(%{field: value}) - {:ok, %Track{}} - - iex> create_track(%{field: bad_value}) - {:error, %Ecto.Changeset{}} - """ + @spec create_track(map) :: {:ok, Track.t()} | {:error, Ecto.Changeset.t()} def create_track(attrs \\ %{}) do %Track{} |> Track.changeset(attrs) @@ -1147,16 +746,8 @@ defmodule Mobilizon.Events do @doc """ Updates a track. - - ## Examples - - iex> update_track(track, %{field: new_value}) - {:ok, %Track{}} - - iex> update_track(track, %{field: bad_value}) - {:error, %Ecto.Changeset{}} - """ + @spec update_track(Track.t(), map) :: {:ok, Track.t()} | {:error, Ecto.Changeset.t()} def update_track(%Track{} = track, attrs) do track |> Track.changeset(attrs) @@ -1164,196 +755,121 @@ defmodule Mobilizon.Events do end @doc """ - Deletes a Track. - - ## Examples - - iex> delete_track(track) - {:ok, %Track{}} - - iex> delete_track(track) - {:error, %Ecto.Changeset{}} - + Deletes a track. """ - def delete_track(%Track{} = track) do - Repo.delete(track) - end + @spec delete_track(Track.t()) :: {:ok, Track.t()} | {:error, Ecto.Changeset.t()} + def delete_track(%Track{} = track), do: Repo.delete(track) @doc """ - Returns an `%Ecto.Changeset{}` for tracking track changes. - - ## Examples - - iex> change_track(track) - %Ecto.Changeset{source: %Track{}} - + Returns the list of tracks. """ - def change_track(%Track{} = track) do - Track.changeset(track, %{}) - end - - alias Mobilizon.Events.Comment + @spec list_tracks :: [Track.t()] + def list_tracks, do: Repo.all(Track) @doc """ - Returns the list of public comments. - - ## Examples - - iex> list_comments() - [%Comment{}, ...] - + Returns the list of sessions for the track. """ - def list_comments do - Repo.all(from(c in Comment, where: c.visibility == ^:public)) - end - - def get_public_comments_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do - query = - from( - c in Comment, - where: c.actor_id == ^actor_id and c.visibility in [^:public, ^:unlisted], - order_by: [desc: :id], - preload: [ - :actor, - :in_reply_to_comment, - :origin_comment, - :event - ] - ) - |> Page.paginate(page, limit) - - comments = Repo.all(query) - - count_comments = - Repo.one(from(c in Comment, select: count(c.id), where: c.actor_id == ^actor_id)) - - {:ok, comments, count_comments} + @spec list_sessions_for_track(Track.t()) :: [Session.t()] + def list_sessions_for_track(%Track{id: track_id}) do + track_id + |> sessions_for_track_query() + |> Repo.all() end @doc """ Gets a single comment. - - Raises `Ecto.NoResultsError` if the Comment does not exist. - - ## Examples - - iex> get_comment!(123) - %Comment{} - - iex> get_comment!(456) - ** (Ecto.NoResultsError) - + Raises `Ecto.NoResultsError` if the comment does not exist. """ + @spec get_comment!(integer | String.t()) :: Comment.t() def get_comment!(id), do: Repo.get!(Comment, id) - # @doc """ - # Gets a single comment from it's UUID + @doc """ + Gets a comment by its URL. + """ + @spec get_comment_from_url(String.t()) :: Comment.t() | nil + def get_comment_from_url(url), do: Repo.get_by(Comment, url: url) - # """ - # @spec get_comment_from_uuid(String.t) :: {:ok, Comment.t} | {:error, nil} - # def get_comment_from_uuid(uuid), do: Repo.get_by(Comment, uuid: uuid) + @doc """ + Gets a comment by its URL. + Raises `Ecto.NoResultsError` if the comment does not exist. + """ + @spec get_comment_from_url!(String.t()) :: Comment.t() + def get_comment_from_url!(url), do: Repo.get_by!(Comment, url: url) - # @doc """ - # Gets a single comment by it's UUID. + @doc """ + Gets a comment by its URL, with all associations loaded. + """ + @spec get_comment_from_url_with_preload(String.t()) :: + {:ok, Comment.t()} | {:error, :comment_not_found} + def get_comment_from_url_with_preload(url) do + comment = + from(c in Comment, where: c.url == ^url) + |> preload_for_comment() + |> Repo.one() - # Raises `Ecto.NoResultsError` if the Comment does not exist. + case comment do + %Comment{} = comment -> + {:ok, comment} - # ## Examples - - # iex> get_comment_from_uuid!("123AFV13") - # %Comment{} - - # iex> get_comment_from_uuid!("20R9HKDJHF") - # ** (Ecto.NoResultsError) - - # """ - # @spec get_comment_from_uuid(String.t) :: Comment.t - # def get_comment_from_uuid!(uuid), do: Repo.get_by!(Comment, uuid: uuid) - - def get_comment_full_from_uuid(uuid) do - with %Comment{} = comment <- Repo.get_by!(Comment, uuid: uuid) do - Repo.preload(comment, [:actor, :attributed_to, :in_reply_to_comment]) + nil -> + {:error, :comment_not_found} end end - def get_cached_comment_full_by_uuid(uuid) do + @doc """ + Gets a comment by its URL, with all associations loaded. + Raises `Ecto.NoResultsError` if the comment does not exist. + """ + @spec get_comment_from_url_with_preload(String.t()) :: Comment.t() + def get_comment_from_url_with_preload!(url) do + Comment + |> Repo.get_by!(url: url) + |> Repo.preload(@comment_preloads) + end + + @doc """ + Gets a comment by its UUID, with all associations loaded. + """ + @spec get_comment_from_uuid_with_preload(String.t()) :: Comment.t() + def get_comment_from_uuid_with_preload(uuid) do + Comment + |> Repo.get_by(uuid: uuid) + |> Repo.preload(@comment_preloads) + end + + # TODO: move to MobilizonWeb + @spec get_cached_comment_by_uuid_with_preload(String.t()) :: + {:commit, Comment.t()} | {:ignore, nil} + def get_cached_comment_by_uuid_with_preload(uuid) do Cachex.fetch(:activity_pub, "comment_" <> uuid, fn "comment_" <> uuid -> - case get_comment_full_from_uuid(uuid) do + case get_comment_from_uuid_with_preload(uuid) do %Comment{} = comment -> {:commit, comment} - _ -> + nil -> {:ignore, nil} end end) end - def get_comment_from_url(url), do: Repo.get_by(Comment, url: url) - - def get_comment_from_url!(url), do: Repo.get_by!(Comment, url: url) - - def get_comment_full_from_url(url) do - case Repo.one( - from(c in Comment, where: c.url == ^url, preload: [:actor, :in_reply_to_comment]) - ) do - nil -> {:error, :comment_not_found} - comment -> {:ok, comment} - end - end - - def get_comment_full_from_url!(url) do - with %Comment{} = comment <- Repo.get_by!(Comment, url: url) do - Repo.preload(comment, [:actor, :in_reply_to_comment]) - end - end - - @doc """ - Get all comments by an actor and a list of ids - """ - def get_all_comments_by_actor_and_ids(actor_id, comment_ids \\ []) - def get_all_comments_by_actor_and_ids(_actor_id, []), do: [] - - def get_all_comments_by_actor_and_ids(actor_id, comment_ids) do - Comment - |> where([c], c.id in ^comment_ids) - |> where([c], c.actor_id == ^actor_id) - |> Repo.all() - end - @doc """ Creates a comment. - - ## Examples - - iex> create_comment(%{field: value}) - {:ok, %Comment{}} - - iex> create_comment(%{field: bad_value}) - {:error, %Ecto.Changeset{}} - """ + @spec create_comment(map) :: {:ok, Comment.t()} | {:error, Ecto.Changeset.t()} def create_comment(attrs \\ %{}) do with {:ok, %Comment{} = comment} <- %Comment{} |> Comment.changeset(attrs) |> Repo.insert(), - %Comment{} = comment <- Repo.preload(comment, [:actor, :in_reply_to_comment]) do + %Comment{} = comment <- Repo.preload(comment, @comment_preloads) do {:ok, comment} end end @doc """ Updates a comment. - - ## Examples - - iex> update_comment(comment, %{field: new_value}) - {:ok, %Comment{}} - - iex> update_comment(comment, %{field: bad_value}) - {:error, %Ecto.Changeset{}} - """ + @spec update_comment(Comment.t(), map) :: {:ok, Comment.t()} | {:error, Ecto.Changeset.t()} def update_comment(%Comment{} = comment, attrs) do comment |> Comment.changeset(attrs) @@ -1361,114 +877,85 @@ defmodule Mobilizon.Events do end @doc """ - Deletes a Comment. - - ## Examples - - iex> delete_comment(comment) - {:ok, %Comment{}} - - iex> delete_comment(comment) - {:error, %Ecto.Changeset{}} - + Deletes a comment. """ - def delete_comment(%Comment{} = comment) do - Repo.delete(comment) + @spec delete_comment(Comment.t()) :: {:ok, Comment.t()} | {:error, Ecto.Changeset.t()} + def delete_comment(%Comment{} = comment), do: Repo.delete(comment) + + @doc """ + Returns the list of public comments. + """ + @spec list_comments :: [Comment.t()] + def list_comments do + Repo.all(from(c in Comment, where: c.visibility == ^:public)) end @doc """ - Returns an `%Ecto.Changeset{}` for tracking comment changes. - - ## Examples - - iex> change_comment(comment) - %Ecto.Changeset{source: %Comment{}} - + Returns the list of public comments for the actor. """ - def change_comment(%Comment{} = comment) do - Comment.changeset(comment, %{}) + @spec list_public_events_for_actor(Actor.t(), integer | nil, integer | nil) :: + {:ok, [Comment.t()], integer} + def list_public_comments_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do + comments = + actor_id + |> public_comments_for_actor_query() + |> Page.paginate(page, limit) + |> Repo.all() + + count_comments = + actor_id + |> count_comments_query() + |> Repo.one() + + {:ok, comments, count_comments} end - alias Mobilizon.Events.FeedToken + @doc """ + Returns the list of comments by an actor and a list of ids. + """ + @spec list_comments_by_actor_and_ids(integer | String.t(), [integer | String.t()]) :: + [Comment.t()] + def list_comments_by_actor_and_ids(actor_id, comment_ids \\ []) + def list_comments_by_actor_and_ids(_actor_id, []), do: [] + + def list_comments_by_actor_and_ids(actor_id, comment_ids) do + Comment + |> where([c], c.id in ^comment_ids) + |> where([c], c.actor_id == ^actor_id) + |> Repo.all() + end + + @doc """ + Counts local comments. + """ + @spec count_local_comments :: integer + def count_local_comments, do: Repo.one(count_local_comments_query()) @doc """ Gets a single feed token. - - ## Examples - - iex> get_feed_token("123") - {:ok, %FeedToken{}} - - iex> get_feed_token("456") - {:error, nil} - """ + @spec get_feed_token(String.t()) :: FeedToken.t() | nil def get_feed_token(token) do - from(ftk in FeedToken, where: ftk.token == ^token, preload: [:actor, :user]) + token + |> feed_token_query() |> Repo.one() end @doc """ Gets a single feed token. - - Raises `Ecto.NoResultsError` if the FeedToken does not exist. - - ## Examples - - iex> get_feed_token!(123) - %FeedToken{} - - iex> get_feed_token!(456) - ** (Ecto.NoResultsError) - + Raises `Ecto.NoResultsError` if the feed token does not exist. """ + @spec get_feed_token!(String.t()) :: FeedToken.t() def get_feed_token!(token) do - from( - tk in FeedToken, - where: tk.token == ^token, - preload: [:actor, :user] - ) + token + |> feed_token_query() |> Repo.one!() end - @doc """ - Get feed tokens for an user - """ - @spec get_feed_tokens(User.t()) :: list(FeedTokens.t()) - def get_feed_tokens(%User{id: id}) do - from( - tk in FeedToken, - where: tk.user_id == ^id, - preload: [:actor, :user] - ) - |> Repo.all() - end - - @doc """ - Get feed tokens for an actor - """ - @spec get_feed_tokens(Actor.t()) :: list(FeedTokens.t()) - def get_feed_tokens(%Actor{id: id, domain: nil}) do - from( - tk in FeedToken, - where: tk.actor_id == ^id, - preload: [:actor, :user] - ) - |> Repo.all() - end - @doc """ Creates a feed token. - - ## Examples - - iex> create_feed_token(%{field: value}) - {:ok, %FeedToken{}} - - iex> create_feed_token(%{field: bad_value}) - {:error, %Ecto.Changeset{}} - """ + @spec create_feed_token(map) :: {:ok, FeedToken.t()} | {:error, Ecto.Changeset.t()} def create_feed_token(attrs \\ %{}) do attrs = Map.put(attrs, "token", Ecto.UUID.generate()) @@ -1479,16 +966,9 @@ defmodule Mobilizon.Events do @doc """ Updates a feed token. - - ## Examples - - iex> update_feed_token(feed_token, %{field: new_value}) - {:ok, %FeedToken{}} - - iex> update_feed_token(feed_token, %{field: bad_value}) - {:error, %Ecto.Changeset{}} - """ + @spec update_feed_token(FeedToken.t(), map) :: + {:ok, FeedToken.t()} | {:error, Ecto.Changeset.t()} def update_feed_token(%FeedToken{} = feed_token, attrs) do feed_token |> FeedToken.changeset(attrs) @@ -1496,31 +976,318 @@ defmodule Mobilizon.Events do end @doc """ - Deletes a FeedToken. - - ## Examples - - iex> delete_feed_token(feed_token) - {:ok, %FeedToken{}} - - iex> delete_feed_token(feed_token) - {:error, %Ecto.Changeset{}} - + Deletes a feed token. """ - def delete_feed_token(%FeedToken{} = feed_token) do - Repo.delete(feed_token) + @spec delete_feed_token(FeedToken.t()) :: {:ok, FeedToken.t()} | {:error, Ecto.Changeset.t()} + def delete_feed_token(%FeedToken{} = feed_token), do: Repo.delete(feed_token) + + @doc """ + Returns the list of feed tokens for an user. + """ + @spec list_feed_tokens_for_user(User.t()) :: [FeedTokens.t()] + def list_feed_tokens_for_user(%User{id: user_id}) do + user_id + |> feed_token_for_user_query() + |> Repo.all() end @doc """ - Returns an `%Ecto.Changeset{}` for tracking feed_token changes. - - ## Examples - - iex> change_feed_token(feed_token) - %Ecto.Changeset{source: %FeedToken{}} - + Returns the list of feed tokens for an actor. """ - def change_feed_token(%FeedToken{} = feed_token) do - FeedToken.changeset(feed_token, %{}) + @spec list_feed_tokens_for_actor(Actor.t()) :: [FeedTokens.t()] + def list_feed_tokens_for_actor(%Actor{id: actor_id, domain: nil}) do + actor_id + |> feed_token_for_actor_query() + |> Repo.all() end + + @spec event_by_url_query(String.t()) :: Ecto.Query.t() + defp event_by_url_query(url) do + from(e in Event, where: e.url == ^url) + end + + @spec event_by_uuid_query(String.t()) :: Ecto.Query.t() + defp event_by_uuid_query(uuid) do + from(e in Event, where: e.uuid == ^uuid) + end + + @spec event_for_actor_query(integer | String.t()) :: Ecto.Query.t() + defp event_for_actor_query(actor_id) do + from( + e in Event, + where: e.organizer_actor_id == ^actor_id, + order_by: [desc: :id] + ) + end + + @spec upcoming_public_event_for_actor_query(integer | String.t()) :: Ecto.Query.t() + defp upcoming_public_event_for_actor_query(actor_id) do + from( + e in Event, + where: + e.organizer_actor_id == ^actor_id and + e.begins_on > ^DateTime.utc_now(), + order_by: [asc: :begins_on], + limit: 1, + preload: [ + :organizer_actor, + :tags, + :participants, + :physical_address + ] + ) + end + + @spec close_events_query(Geo.geometry(), number) :: Ecto.Query.t() + defp close_events_query(point, radius) do + from( + e in Event, + join: a in Address, + on: a.id == e.physical_address_id, + where: e.visibility == ^:public and st_dwithin_in_meters(^point, a.geom, ^radius), + preload: :organizer_actor + ) + end + + @spec events_by_name_query(String.t()) :: Ecto.Query.t() + defp events_by_name_query(name) do + from( + e in Event, + where: + e.visibility == ^:public and + fragment("f_unaccent(?) %> f_unaccent(?)", e.title, ^name), + order_by: fragment("word_similarity(?, ?) desc", e.title, ^name), + preload: [:organizer_actor] + ) + end + + @spec events_by_tags_query([integer], integer) :: Ecto.Query.t() + def events_by_tags_query(tags_ids, limit) do + from( + e in Event, + distinct: e.uuid, + join: te in "events_tags", + on: e.id == te.event_id, + where: e.begins_on > ^DateTime.utc_now(), + where: e.visibility in ^@public_visibility, + where: te.tag_id in ^tags_ids, + order_by: [asc: e.begins_on], + limit: ^limit + ) + end + + @spec count_events_for_actor_query(integer | String.t()) :: Ecto.Query.t() + defp count_events_for_actor_query(actor_id) do + from( + e in Event, + select: count(e.id), + where: e.organizer_actor_id == ^actor_id + ) + end + + @spec count_local_events_query :: Ecto.Query.t() + defp count_local_events_query do + from(e in Event, select: count(e.id), where: e.local == ^true) + end + + @spec tag_by_slug_query(String.t()) :: Ecto.Query.t() + defp tag_by_slug_query(slug) do + from(t in Tag, where: t.slug == ^slug) + end + + @spec tags_for_event_query(integer) :: Ecto.Query.t() + defp tags_for_event_query(event_id) do + from( + t in Tag, + join: e in "events_tags", + on: t.id == e.tag_id, + where: e.event_id == ^event_id + ) + end + + @spec tags_linked_query(integer, integer) :: Ecto.Query.t() + defp tags_linked_query(tag1_id, tag2_id) do + from( + tr in TagRelation, + where: + tr.tag_id == ^min(tag1_id, tag2_id) and + tr.link_id == ^max(tag1_id, tag2_id) + ) + end + + @spec tag_relation_subquery(integer) :: Ecto.Query.t() + defp tag_relation_subquery(tag_id) do + from( + tr in TagRelation, + select: %{id: tr.tag_id, weight: tr.weight}, + where: tr.link_id == ^tag_id + ) + end + + @spec tag_relation_union_subquery(Ecto.Query.t(), integer) :: Ecto.Query.t() + defp tag_relation_union_subquery(subquery, tag_id) do + from( + tr in TagRelation, + select: %{id: tr.link_id, weight: tr.weight}, + union_all: ^subquery, + where: tr.tag_id == ^tag_id + ) + end + + @spec tag_neighbors_query(Ecto.Query.t(), integer, integer) :: Ecto.Query.t() + defp tag_neighbors_query(subquery, relation_minimum, limit) do + from( + t in Tag, + right_join: q in subquery(subquery), + on: [id: t.id], + where: q.weight >= ^relation_minimum, + limit: ^limit, + order_by: [desc: q.weight] + ) + end + + @spec participant_by_url_query(String.t()) :: Ecto.Query.t() + defp participant_by_url_query(url) do + from( + p in Participant, + where: p.url == ^url, + preload: [:actor, :event] + ) + end + + @spec participants_for_event(String.t()) :: Ecto.Query.t() + defp participants_for_event(event_uuid) do + from( + p in Participant, + join: e in Event, + on: p.event_id == e.id, + where: e.uuid == ^event_uuid, + preload: [:actor] + ) + end + + defp organizers_participants_for_event(event_id) do + from( + p in Participant, + where: p.event_id == ^event_id and p.role == ^:creator, + preload: [:actor] + ) + end + + @spec requests_for_actor_query(integer) :: Ecto.Query.t() + defp requests_for_actor_query(actor_id) do + from(p in Participant, where: p.actor_id == ^actor_id and p.role == ^:not_approved) + end + + @spec event_participations_for_actor_query(integer) :: Ecto.Query.t() + def event_participations_for_actor_query(actor_id) do + from( + e in Event, + join: p in Participant, + join: a in Actor, + on: p.actor_id == a.id, + on: p.event_id == e.id, + where: a.id == ^actor_id and p.role != ^:not_approved, + preload: [:picture, :tags] + ) + end + + @spec sessions_for_event_query(integer) :: Ecto.Query.t() + defp sessions_for_event_query(event_id) do + from( + s in Session, + join: e in Event, + on: s.event_id == e.id, + where: e.id == ^event_id + ) + end + + @spec sessions_for_track_query(integer) :: Ecto.Query.t() + defp sessions_for_track_query(track_id) do + from(s in Session, where: s.track_id == ^track_id) + end + + defp public_comments_for_actor_query(actor_id) do + from( + c in Comment, + where: c.actor_id == ^actor_id and c.visibility in ^@public_visibility, + order_by: [desc: :id], + preload: [ + :actor, + :in_reply_to_comment, + :origin_comment, + :event + ] + ) + end + + @spec count_comments_query(integer) :: Ecto.Query.t() + defp count_comments_query(actor_id) do + from(c in Comment, select: count(c.id), where: c.actor_id == ^actor_id) + end + + @spec count_local_comments_query :: Ecto.Query.t() + defp count_local_comments_query do + from( + c in Comment, + select: count(c.id), + where: c.local == ^true and c.visibility in ^@public_visibility + ) + end + + @spec feed_token_query(String.t()) :: Ecto.Query.t() + defp feed_token_query(token) do + from(ftk in FeedToken, where: ftk.token == ^token, preload: [:actor, :user]) + end + + @spec feed_token_for_user_query(integer) :: Ecto.Query.t() + defp feed_token_for_user_query(user_id) do + from(tk in FeedToken, where: tk.user_id == ^user_id, preload: [:actor, :user]) + end + + @spec feed_token_for_actor_query(integer) :: Ecto.Query.t() + defp feed_token_for_actor_query(actor_id) do + from(tk in FeedToken, where: tk.actor_id == ^actor_id, preload: [:actor, :user]) + end + + @spec filter_public_visibility(Ecto.Query.t()) :: Ecto.Query.t() + defp filter_public_visibility(query) do + from(e in query, where: e.visibility in ^@public_visibility) + end + + @spec filter_not_event_uuid(Ecto.Query.t(), String.t() | nil) :: Ecto.Query.t() + defp filter_not_event_uuid(query, nil), do: query + + defp filter_not_event_uuid(query, not_event_uuid) do + from(e in query, where: e.uuid != ^not_event_uuid) + end + + @spec filter_future_events(Ecto.Query.t(), boolean) :: Ecto.Query.t() + defp filter_future_events(query, true) do + from(q in query, where: q.begins_on > ^DateTime.utc_now()) + end + + defp filter_future_events(query, false), do: query + + @spec filter_unlisted(Ecto.Query.t(), boolean) :: Ecto.Query.t() + defp filter_unlisted(query, true) do + from(q in query, where: q.visibility in ^@public_visibility) + end + + defp filter_unlisted(query, false) do + from(q in query, where: q.visibility == ^:public) + end + + @spec filter_role(Ecto.Query.t(), boolean) :: Ecto.Query.t() + defp filter_role(query, false) do + from(p in query, where: p.role != ^:not_approved) + end + + defp filter_role(query, true), do: query + + @spec preload_for_event(Ecto.Query.t()) :: Ecto.Query.t() + defp preload_for_event(query), do: preload(query, ^@event_preloads) + + @spec preload_for_comment(Ecto.Query.t()) :: Ecto.Query.t() + defp preload_for_comment(query), do: preload(query, ^@comment_preloads) end diff --git a/lib/mobilizon/events/tag_relations.ex b/lib/mobilizon/events/tag_relation.ex similarity index 100% rename from lib/mobilizon/events/tag_relations.ex rename to lib/mobilizon/events/tag_relation.ex diff --git a/lib/mobilizon_web/api/reports.ex b/lib/mobilizon_web/api/reports.ex index 171e70b04..57925562b 100644 --- a/lib/mobilizon_web/api/reports.ex +++ b/lib/mobilizon_web/api/reports.ex @@ -73,7 +73,7 @@ defmodule MobilizonWeb.API.Reports do defp get_report_comments(%Actor{id: actor_id}, comment_ids) do {:get_report_comments, - Events.get_all_comments_by_actor_and_ids(actor_id, comment_ids) |> Enum.map(& &1.url)} + Events.list_comments_by_actor_and_ids(actor_id, comment_ids) |> Enum.map(& &1.url)} end defp get_report_comments(_, _), do: {:get_report_comments, nil} diff --git a/lib/mobilizon_web/api/search.ex b/lib/mobilizon_web/api/search.ex index 67e9a2726..835bbce5a 100644 --- a/lib/mobilizon_web/api/search.ex +++ b/lib/mobilizon_web/api/search.ex @@ -68,7 +68,7 @@ defmodule MobilizonWeb.API.Search do end true -> - {:ok, Events.find_and_count_events_by_name(search, page, limit)} + {:ok, Events.build_events_by_name(search, page, limit)} end end diff --git a/lib/mobilizon_web/controllers/page_controller.ex b/lib/mobilizon_web/controllers/page_controller.ex index 0c608a6df..d09a4f4bf 100644 --- a/lib/mobilizon_web/controllers/page_controller.ex +++ b/lib/mobilizon_web/controllers/page_controller.ex @@ -17,12 +17,12 @@ defmodule MobilizonWeb.PageController do end def event(conn, %{"uuid" => uuid}) do - {status, event} = Events.get_cached_event_full_by_uuid(uuid) + {status, event} = Events.get_cached_public_event_by_uuid_with_preload(uuid) render_or_error(conn, &ok_status_and_is_visible?/2, status, :event, event) end def comment(conn, %{"uuid" => uuid}) do - {status, comment} = Events.get_cached_comment_full_by_uuid(uuid) + {status, comment} = Events.get_cached_comment_by_uuid_with_preload(uuid) render_or_error(conn, &ok_status_and_is_visible?/2, status, :comment, comment) end diff --git a/lib/mobilizon_web/resolvers/event.ex b/lib/mobilizon_web/resolvers/event.ex index 370b507a2..9fc527d68 100644 --- a/lib/mobilizon_web/resolvers/event.ex +++ b/lib/mobilizon_web/resolvers/event.ex @@ -25,7 +25,7 @@ defmodule MobilizonWeb.Resolvers.Event do end def find_event(_parent, %{uuid: uuid}, _resolution) do - case Mobilizon.Events.get_event_full_by_uuid(uuid) do + case Mobilizon.Events.get_public_event_by_uuid_with_preload(uuid) do nil -> {:error, "Event with UUID #{uuid} not found"} @@ -58,14 +58,14 @@ defmodule MobilizonWeb.Resolvers.Event do ) do # We get the organizer's next public event events = - [Events.get_actor_upcoming_public_event(organizer_actor, uuid)] + [Events.get_upcoming_public_event_for_actor(organizer_actor, uuid)] |> Enum.filter(&is_map/1) # We find similar events with the same tags # uniq_by : It's possible event_from_same_actor is inside events_from_tags events = events - |> Enum.concat(Events.find_similar_events_by_common_tags(tags, @number_of_related_events)) + |> Enum.concat(Events.list_events_by_tags(tags, @number_of_related_events)) |> uniq_events() # TODO: We should use tag_relations to find more appropriate events @@ -104,7 +104,7 @@ defmodule MobilizonWeb.Resolvers.Event do ) do with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id), {:has_event, {:ok, %Event{} = event}} <- - {:has_event, Mobilizon.Events.get_event_full(event_id)}, + {:has_event, Mobilizon.Events.get_event_with_preload(event_id)}, {:error, :participant_not_found} <- Mobilizon.Events.get_participant(event_id, actor_id), {:ok, _activity, participant} <- MobilizonWeb.API.Participations.join(event, actor), participant <- @@ -141,7 +141,7 @@ defmodule MobilizonWeb.Resolvers.Event do ) do with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id), {:has_event, {:ok, %Event{} = event}} <- - {:has_event, Mobilizon.Events.get_event_full(event_id)}, + {:has_event, Mobilizon.Events.get_event_with_preload(event_id)}, {:ok, _activity, _participant} <- MobilizonWeb.API.Participations.leave(event, actor) do {:ok, %{event: %{id: event_id}, actor: %{id: actor_id}}} else @@ -200,7 +200,7 @@ defmodule MobilizonWeb.Resolvers.Event do ) do # See https://github.com/absinthe-graphql/absinthe/issues/490 with args <- Map.put(args, :options, args[:options] || %{}), - {:ok, %Event{} = event} <- Mobilizon.Events.get_event_full(event_id), + {:ok, %Event{} = event} <- Mobilizon.Events.get_event_with_preload(event_id), {:is_owned, %Actor{} = organizer_actor} <- User.owns_actor(user, event.organizer_actor_id), {:ok, args} <- save_attached_picture(args), diff --git a/lib/mobilizon_web/resolvers/tag.ex b/lib/mobilizon_web/resolvers/tag.ex index ecbef7720..75f26a6cb 100644 --- a/lib/mobilizon_web/resolvers/tag.ex +++ b/lib/mobilizon_web/resolvers/tag.ex @@ -33,7 +33,7 @@ defmodule MobilizonWeb.Resolvers.Tag do # """ # def get_related_tags(_parent, %{tag_id: tag_id}, _resolution) do # with %Tag{} = tag <- Mobilizon.Events.get_tag!(tag_id), - # tags <- Mobilizon.Events.tag_neighbors(tag) do + # tags <- Mobilizon.Events.list_tag_neighbors(tag) do # {:ok, tags} # end # end @@ -42,7 +42,7 @@ defmodule MobilizonWeb.Resolvers.Tag do Retrieve the list of related tags for a parent tag """ def get_related_tags(%Tag{} = tag, _args, _resolution) do - with tags <- Mobilizon.Events.tag_neighbors(tag) do + with tags <- Mobilizon.Events.list_tag_neighbors(tag) do {:ok, tags} end end diff --git a/lib/service/activity_pub/activity_pub.ex b/lib/service/activity_pub/activity_pub.ex index 68e23ec59..e27123005 100644 --- a/lib/service/activity_pub/activity_pub.ex +++ b/lib/service/activity_pub/activity_pub.ex @@ -84,10 +84,10 @@ defmodule Mobilizon.Service.ActivityPub do {:ok, _activity, %{url: object_url} = _object} <- Transmogrifier.handle_incoming(params) do case data["type"] do "Event" -> - {:ok, Events.get_event_full_by_url!(object_url)} + {:ok, Events.get_public_event_by_url_with_preload!(object_url)} "Note" -> - {:ok, Events.get_comment_full_from_url!(object_url)} + {:ok, Events.get_comment_from_url_with_preload!(object_url)} "Actor" -> {:ok, Actors.get_actor_by_url!(object_url, true)} @@ -97,10 +97,10 @@ defmodule Mobilizon.Service.ActivityPub do end else {:existing_event, %Event{url: event_url}} -> - {:ok, Events.get_event_full_by_url!(event_url)} + {:ok, Events.get_public_event_by_url_with_preload!(event_url)} {:existing_comment, %Comment{url: comment_url}} -> - {:ok, Events.get_comment_full_from_url!(comment_url)} + {:ok, Events.get_comment_from_url_with_preload!(comment_url)} {:existing_actor, {:ok, %Actor{url: actor_url}}} -> {:ok, Actors.get_actor_by_url!(actor_url, true)} @@ -682,8 +682,8 @@ defmodule Mobilizon.Service.ActivityPub do """ @spec fetch_public_activities_for_actor(Actor.t(), integer(), integer()) :: map() def fetch_public_activities_for_actor(%Actor{} = actor, page \\ 1, limit \\ 10) do - {:ok, events, total_events} = Events.get_public_events_for_actor(actor, page, limit) - {:ok, comments, total_comments} = Events.get_public_comments_for_actor(actor, page, limit) + {:ok, events, total_events} = Events.list_public_events_for_actor(actor, page, limit) + {:ok, comments, total_comments} = Events.list_public_comments_for_actor(actor, page, limit) event_activities = Enum.map(events, &event_to_activity/1) diff --git a/lib/service/export/feed.ex b/lib/service/export/feed.ex index 39fe709d2..ce33e73bb 100644 --- a/lib/service/export/feed.ex +++ b/lib/service/export/feed.ex @@ -45,7 +45,7 @@ defmodule Mobilizon.Service.Export.Feed do defp fetch_actor_event_feed(name) do with %Actor{} = actor <- Actors.get_local_actor_by_name(name), {:visibility, true} <- {:visibility, Actor.is_public_visibility(actor)}, - {:ok, events, _count} <- Events.get_public_events_for_actor(actor) do + {:ok, events, _count} <- Events.list_public_events_for_actor(actor) do {:ok, build_actor_feed(actor, events)} else err -> diff --git a/lib/service/export/icalendar.ex b/lib/service/export/icalendar.ex index 6a67a3072..70d865f48 100644 --- a/lib/service/export/icalendar.ex +++ b/lib/service/export/icalendar.ex @@ -45,7 +45,7 @@ defmodule Mobilizon.Service.Export.ICalendar do @spec export_public_actor(Actor.t()) :: String.t() def export_public_actor(%Actor{} = actor) do with true <- Actor.is_public_visibility(actor), - {:ok, events, _} <- Events.get_public_events_for_actor(actor) do + {:ok, events, _} <- Events.list_public_events_for_actor(actor) do {:ok, %ICalendar{events: events |> Enum.map(&do_export_event/1)} |> ICalendar.to_ics()} end end @@ -74,7 +74,7 @@ defmodule Mobilizon.Service.Export.ICalendar do Create cache for an actor """ def create_cache("event_" <> uuid) do - with %Event{} = event <- Events.get_event_full_by_uuid(uuid), + with %Event{} = event <- Events.get_public_event_by_uuid_with_preload(uuid), {:ok, res} <- export_public_event(event) do {:commit, res} else diff --git a/test/mobilizon/events/events_test.exs b/test/mobilizon/events/events_test.exs index 29a396906..440c1029b 100644 --- a/test/mobilizon/events/events_test.exs +++ b/test/mobilizon/events/events_test.exs @@ -4,6 +4,7 @@ defmodule Mobilizon.EventsTest do import Mobilizon.Factory alias Mobilizon.Events + alias Mobilizon.Storage.Page @event_valid_attrs %{ begins_on: "2010-04-17 14:00:00Z", @@ -47,29 +48,29 @@ defmodule Mobilizon.EventsTest do refute Ecto.assoc_loaded?(Events.get_event!(event.id).organizer_actor) end - test "get_event_full!/1 returns the event with given id", %{event: event} do - assert Events.get_event_full!(event.id).organizer_actor.preferred_username == + test "get_event_with_preload!/1 returns the event with given id", %{event: event} do + assert Events.get_event_with_preload!(event.id).organizer_actor.preferred_username == event.organizer_actor.preferred_username - assert Events.get_event_full!(event.id).participants == [] + assert Events.get_event_with_preload!(event.id).participants == [] end - test "find_and_count_events_by_name/1 returns events for a given name", %{ + test "build_events_by_name/1 returns events for a given name", %{ event: %Event{title: title} = event } do - assert title == hd(Events.find_and_count_events_by_name(event.title).elements).title + assert title == hd(Events.build_events_by_name(event.title).elements).title %Event{} = event2 = insert(:event, title: "Special event") assert event2.title == - Events.find_and_count_events_by_name("Special").elements |> hd() |> Map.get(:title) + Events.build_events_by_name("Special").elements |> hd() |> Map.get(:title) assert event2.title == - Events.find_and_count_events_by_name(" Special ").elements + Events.build_events_by_name(" Special ").elements |> hd() |> Map.get(:title) - assert %{elements: [], total: 0} == Events.find_and_count_events_by_name("") + assert %Page{elements: [], total: 0} == Events.build_events_by_name("") end test "find_close_events/3 returns events in the area" do @@ -127,19 +128,15 @@ defmodule Mobilizon.EventsTest do assert_raise Ecto.NoResultsError, fn -> Events.get_event!(event.id) end end - test "change_event/1 returns a event changeset", %{event: event} do - assert %Ecto.Changeset{} = Events.change_event(event) - end - - test "get_public_events_for_actor/1", %{actor: actor, event: event} do - assert {:ok, [event_found], 1} = Events.get_public_events_for_actor(actor) + test "list_public_events_for_actor/1", %{actor: actor, event: event} do + assert {:ok, [event_found], 1} = Events.list_public_events_for_actor(actor) assert event_found.title == event.title end - test "get_public_events_for_actor/3", %{actor: actor, event: event} do + test "list_public_events_for_actor/3", %{actor: actor, event: event} do event1 = insert(:event, organizer_actor: actor) - case Events.get_public_events_for_actor(actor, 1, 10) do + case Events.list_public_events_for_actor(actor, 1, 10) do {:ok, events_found, 2} -> event_ids = MapSet.new(events_found |> Enum.map(& &1.id)) assert event_ids == MapSet.new([event.id, event1.id]) @@ -149,10 +146,10 @@ defmodule Mobilizon.EventsTest do end end - test "get_public_events_for_actor/3 with limited results", %{actor: actor, event: event} do + test "list_public_events_for_actor/3 with limited results", %{actor: actor, event: event} do event1 = insert(:event, organizer_actor: actor) - case Events.get_public_events_for_actor(actor, 1, 1) do + case Events.list_public_events_for_actor(actor, 1, 1) do {:ok, [%Event{id: event_found_id}], 2} -> assert event_found_id in [event.id, event1.id] @@ -229,11 +226,6 @@ defmodule Mobilizon.EventsTest do assert {:ok, %Tag{}} = Events.delete_tag(tag) assert_raise Ecto.NoResultsError, fn -> Events.get_tag!(tag.id) end end - - test "change_tag/1 returns a tag changeset" do - tag = insert(:tag) - assert %Ecto.Changeset{} = Events.change_tag(tag) - end end describe "tags_relations" do @@ -272,7 +264,7 @@ defmodule Mobilizon.EventsTest do assert {:ok, %TagRelation{}} = Events.delete_tag_relation(tag_relation) end - test "tag_neighbors/2 return the connected tags for a given tag", %{ + test "list_tag_neighbors/2 return the connected tags for a given tag", %{ tag1: %Tag{} = tag1, tag2: %Tag{} = tag2 } do @@ -307,7 +299,7 @@ defmodule Mobilizon.EventsTest do }} = Events.create_tag_relation(%{tag_id: tag1.id, link_id: tag1.id}) # The order is preserved, since tag4 has one more relation than tag2 - assert [tag4, tag2] == Events.tag_neighbors(tag1) + assert [tag4, tag2] == Events.list_tag_neighbors(tag1) end end @@ -383,10 +375,6 @@ defmodule Mobilizon.EventsTest do test "delete_participant/1 deletes the participant", %{participant: participant} do assert {:ok, %Participant{}} = Events.delete_participant(participant) end - - test "change_participant/1 returns a participant changeset", %{participant: participant} do - assert %Ecto.Changeset{} = Events.change_participant(participant) - end end describe "sessions" do @@ -481,11 +469,6 @@ defmodule Mobilizon.EventsTest do assert {:ok, %Session{}} = Events.delete_session(session) assert_raise Ecto.NoResultsError, fn -> Events.get_session!(session.id) end end - - test "change_session/1 returns a session changeset" do - session = insert(:session) - assert %Ecto.Changeset{} = Events.change_session(session) - end end describe "tracks" do @@ -548,11 +531,6 @@ defmodule Mobilizon.EventsTest do assert {:ok, %Track{}} = Events.delete_track(track) assert_raise Ecto.NoResultsError, fn -> Events.get_track!(track.id) end end - - test "change_track/1 returns a track changeset" do - track = insert(:track) - assert %Ecto.Changeset{} = Events.change_track(track) - end end describe "comments" do @@ -616,10 +594,5 @@ defmodule Mobilizon.EventsTest do assert {:ok, %Comment{}} = Events.delete_comment(comment) assert_raise Ecto.NoResultsError, fn -> Events.get_comment!(comment.id) end end - - test "change_comment/1 returns a comment changeset" do - comment = insert(:comment) - assert %Ecto.Changeset{} = Events.change_comment(comment) - end end end diff --git a/test/mobilizon/service/activity_pub/activity_pub_test.exs b/test/mobilizon/service/activity_pub/activity_pub_test.exs index b0712cb87..c3da687e6 100644 --- a/test/mobilizon/service/activity_pub/activity_pub_test.exs +++ b/test/mobilizon/service/activity_pub/activity_pub_test.exs @@ -113,7 +113,7 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do # TODO: The delete activity it relayed and fetched once again (and then not found /o\) test "it creates a delete activity and deletes the original event" do event = insert(:event) - event = Events.get_event_full_by_url!(event.url) + event = Events.get_public_event_by_url_with_preload!(event.url) {:ok, delete, _} = ActivityPub.delete(event) assert delete.data["type"] == "Delete" @@ -125,7 +125,7 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do test "it creates a delete activity and deletes the original comment" do comment = insert(:comment) - comment = Events.get_comment_full_from_url!(comment.url) + comment = Events.get_comment_from_url_with_preload!(comment.url) {:ok, delete, _} = ActivityPub.delete(comment) assert delete.data["type"] == "Delete" diff --git a/test/mobilizon_web/api/search_test.exs b/test/mobilizon_web/api/search_test.exs index d654a48e3..af26b4018 100644 --- a/test/mobilizon_web/api/search_test.exs +++ b/test/mobilizon_web/api/search_test.exs @@ -46,13 +46,13 @@ defmodule MobilizonWeb.API.SearchTest do test "search events" do with_mock Events, - find_and_count_events_by_name: fn "toto", 1, 10 -> + build_events_by_name: fn "toto", 1, 10 -> %Page{total: 1, elements: [%Event{title: "super toto event"}]} end do assert {:ok, %{total: 1, elements: [%Event{title: "super toto event"}]}} = Search.search_events("toto", 1, 10) - assert_called(Events.find_and_count_events_by_name("toto", 1, 10)) + assert_called(Events.build_events_by_name("toto", 1, 10)) end end end diff --git a/test/mobilizon_web/resolvers/report_resolver_test.exs b/test/mobilizon_web/resolvers/report_resolver_test.exs index cf369a4dd..71396041a 100644 --- a/test/mobilizon_web/resolvers/report_resolver_test.exs +++ b/test/mobilizon_web/resolvers/report_resolver_test.exs @@ -197,7 +197,9 @@ defmodule MobilizonWeb.Resolvers.ReportResolverTest do assert json_response(res, 200)["data"]["reports"] |> Enum.map(fn report -> Map.get(report, "id") end) |> Enum.sort() == - Enum.map([report_1_id, report_2_id, report_3_id], &to_string/1) + [report_1_id, report_2_id, report_3_id] + |> Enum.map(&to_string/1) + |> Enum.sort() query = """ { diff --git a/test/mobilizon_web/resolvers/tag_resolver_test.exs b/test/mobilizon_web/resolvers/tag_resolver_test.exs index fe5a1f081..ee84f0ee2 100644 --- a/test/mobilizon_web/resolvers/tag_resolver_test.exs +++ b/test/mobilizon_web/resolvers/tag_resolver_test.exs @@ -39,9 +39,7 @@ defmodule MobilizonWeb.Resolvers.TagResolverTest do |> Enum.map(fn tag -> tag["slug"] end) |> MapSet.new() == [tag2, tag3] - |> Enum.map(fn - tag -> tag.slug - end) + |> Enum.map(fn tag -> tag.slug end) |> MapSet.new() end end From da97c29340f769733773f4a3291ec3ba96b4d4b3 Mon Sep 17 00:00:00 2001 From: miffigriffi Date: Tue, 17 Sep 2019 02:45:32 +0200 Subject: [PATCH 24/29] Move Email context under MobilizonWeb --- config/dev.exs | 2 +- config/prod.exs | 2 +- config/test.exs | 2 +- lib/mobilizon/email/email.ex | 17 ----------------- .../email/admin.ex | 8 +++++--- lib/mobilizon_web/email/email.ex | 17 +++++++++++++++++ .../email/mailer.ex | 2 +- lib/{mobilizon => mobilizon_web}/email/user.ex | 8 +++++--- lib/mobilizon_web/views/email_view.ex | 2 +- lib/service/activity_pub/utils.ex | 6 +++--- lib/service/users/activation.ex | 8 ++++---- lib/service/users/reset_password.ex | 8 ++++---- mix.exs | 6 +++--- .../resolvers/user_resolver_test.exs | 18 +++++++++++------- 14 files changed, 57 insertions(+), 49 deletions(-) delete mode 100644 lib/mobilizon/email/email.ex rename lib/{mobilizon => mobilizon_web}/email/admin.ex (84%) create mode 100644 lib/mobilizon_web/email/email.ex rename lib/{mobilizon => mobilizon_web}/email/mailer.ex (69%) rename lib/{mobilizon => mobilizon_web}/email/user.ex (91%) diff --git a/config/dev.exs b/config/dev.exs index c7345d110..ae2ab7e5e 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -61,7 +61,7 @@ config :phoenix, :stacktrace_depth, 20 # Initialize plugs at runtime for faster development compilation config :phoenix, :plug_init_mode, :runtime -config :mobilizon, Mobilizon.Email.Mailer, adapter: Bamboo.LocalAdapter +config :mobilizon, MobilizonWeb.Email.Mailer, adapter: Bamboo.LocalAdapter # Configure your database config :mobilizon, Mobilizon.Storage.Repo, diff --git a/config/prod.exs b/config/prod.exs index d580935ca..334790b5e 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -21,7 +21,7 @@ config :mobilizon, Mobilizon.Storage.Repo, port: System.get_env("MOBILIZON_DATABASE_PORT") || "5432", pool_size: 15 -config :mobilizon, Mobilizon.Email.Mailer, +config :mobilizon, MobilizonWeb.Email.Mailer, adapter: Bamboo.SMTPAdapter, server: "localhost", hostname: "localhost", diff --git a/config/test.exs b/config/test.exs index 8d174e40b..c72f44b4d 100644 --- a/config/test.exs +++ b/config/test.exs @@ -30,7 +30,7 @@ config :mobilizon, Mobilizon.Storage.Repo, hostname: System.get_env("MOBILIZON_DATABASE_HOST") || "localhost", pool: Ecto.Adapters.SQL.Sandbox -config :mobilizon, Mobilizon.Email.Mailer, adapter: Bamboo.TestAdapter +config :mobilizon, MobilizonWeb.Email.Mailer, adapter: Bamboo.TestAdapter config :mobilizon, MobilizonWeb.Upload, filters: [], link_name: false diff --git a/lib/mobilizon/email/email.ex b/lib/mobilizon/email/email.ex deleted file mode 100644 index 0a849583c..000000000 --- a/lib/mobilizon/email/email.ex +++ /dev/null @@ -1,17 +0,0 @@ -defmodule Mobilizon.Email do - @moduledoc """ - The Email context. - """ - - use Bamboo.Phoenix, view: Mobilizon.EmailView - - alias Mobilizon.Config - - @spec base_email :: Bamboo.Email.t() - def base_email do - new_email() - |> from(Config.instance_email_from()) - |> put_html_layout({Mobilizon.EmailView, "email.html"}) - |> put_text_layout({Mobilizon.EmailView, "email.text"}) - end -end diff --git a/lib/mobilizon/email/admin.ex b/lib/mobilizon_web/email/admin.ex similarity index 84% rename from lib/mobilizon/email/admin.ex rename to lib/mobilizon_web/email/admin.ex index 5a2f78cf7..b50059c9d 100644 --- a/lib/mobilizon/email/admin.ex +++ b/lib/mobilizon_web/email/admin.ex @@ -1,18 +1,20 @@ -defmodule Mobilizon.Email.Admin do +defmodule MobilizonWeb.Email.Admin do @moduledoc """ Handles emails sent to admins. """ - use Bamboo.Phoenix, view: Mobilizon.EmailView + use Bamboo.Phoenix, view: MobilizonWeb.EmailView import Bamboo.{Email, Phoenix} import MobilizonWeb.Gettext - alias Mobilizon.{Config, Email} + alias Mobilizon.Config alias Mobilizon.Reports.Report alias Mobilizon.Users.User + alias MobilizonWeb.Email + @spec report(User.t(), Report.t(), String.t()) :: Bamboo.Email.t() def report(%User{email: email}, %Report{} = report, locale \\ "en") do Gettext.put_locale(locale) diff --git a/lib/mobilizon_web/email/email.ex b/lib/mobilizon_web/email/email.ex new file mode 100644 index 000000000..7277f6f56 --- /dev/null +++ b/lib/mobilizon_web/email/email.ex @@ -0,0 +1,17 @@ +defmodule MobilizonWeb.Email do + @moduledoc """ + The Email context. + """ + + use Bamboo.Phoenix, view: MobilizonWeb.EmailView + + alias Mobilizon.Config + + @spec base_email :: Bamboo.Email.t() + def base_email do + new_email() + |> from(Config.instance_email_from()) + |> put_html_layout({MobilizonWeb.EmailView, "email.html"}) + |> put_text_layout({MobilizonWeb.EmailView, "email.text"}) + end +end diff --git a/lib/mobilizon/email/mailer.ex b/lib/mobilizon_web/email/mailer.ex similarity index 69% rename from lib/mobilizon/email/mailer.ex rename to lib/mobilizon_web/email/mailer.ex index 027647609..246d20d05 100644 --- a/lib/mobilizon/email/mailer.ex +++ b/lib/mobilizon_web/email/mailer.ex @@ -1,4 +1,4 @@ -defmodule Mobilizon.Email.Mailer do +defmodule MobilizonWeb.Email.Mailer do @moduledoc """ Mobilizon Mailer. """ diff --git a/lib/mobilizon/email/user.ex b/lib/mobilizon_web/email/user.ex similarity index 91% rename from lib/mobilizon/email/user.ex rename to lib/mobilizon_web/email/user.ex index a05778254..c4895be2a 100644 --- a/lib/mobilizon/email/user.ex +++ b/lib/mobilizon_web/email/user.ex @@ -1,17 +1,19 @@ -defmodule Mobilizon.Email.User do +defmodule MobilizonWeb.Email.User do @moduledoc """ Handles emails sent to users. """ - use Bamboo.Phoenix, view: Mobilizon.EmailView + use Bamboo.Phoenix, view: MobilizonWeb.EmailView import Bamboo.{Email, Phoenix} import MobilizonWeb.Gettext - alias Mobilizon.{Config, Email} + alias Mobilizon.Config alias Mobilizon.Users.User + alias MobilizonWeb.Email + @spec confirmation_email(User.t(), String.t()) :: Bamboo.Email.t() def confirmation_email( %User{email: email, confirmation_token: confirmation_token}, diff --git a/lib/mobilizon_web/views/email_view.ex b/lib/mobilizon_web/views/email_view.ex index 7c2ecb679..d34589acd 100644 --- a/lib/mobilizon_web/views/email_view.ex +++ b/lib/mobilizon_web/views/email_view.ex @@ -1,3 +1,3 @@ -defmodule Mobilizon.EmailView do +defmodule MobilizonWeb.EmailView do use MobilizonWeb, :view end diff --git a/lib/service/activity_pub/utils.ex b/lib/service/activity_pub/utils.ex index ff7655231..49e1da753 100644 --- a/lib/service/activity_pub/utils.ex +++ b/lib/service/activity_pub/utils.ex @@ -25,8 +25,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do alias Mobilizon.Storage.Repo alias Mobilizon.Users + alias MobilizonWeb.{Email, Endpoint} alias MobilizonWeb.Router.Helpers, as: Routes - alias MobilizonWeb.Endpoint require Logger @@ -165,8 +165,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do {:ok, %Report{} = report} <- Reports.create_report(data) do Enum.each(Users.list_moderators(), fn moderator -> moderator - |> Mobilizon.Email.Admin.report(moderator, report) - |> Mobilizon.Email.Mailer.deliver_later() + |> Email.Admin.report(moderator, report) + |> Email.Mailer.deliver_later() end) {:ok, report} diff --git a/lib/service/users/activation.ex b/lib/service/users/activation.ex index a111c5698..1d03da1a2 100644 --- a/lib/service/users/activation.ex +++ b/lib/service/users/activation.ex @@ -1,12 +1,12 @@ defmodule Mobilizon.Service.Users.Activation do @moduledoc false - alias Mobilizon.Email.Mailer - alias Mobilizon.Email.User, as: UserEmail alias Mobilizon.Users alias Mobilizon.Users.User alias Mobilizon.Service.Users.Tools + alias MobilizonWeb.Email + require Logger @doc false @@ -40,7 +40,7 @@ defmodule Mobilizon.Service.Users.Activation do def send_confirmation_email(%User{} = user, locale \\ "en") do user - |> UserEmail.confirmation_email(locale) - |> Mailer.deliver_later() + |> Email.User.confirmation_email(locale) + |> Email.Mailer.deliver_later() end end diff --git a/lib/service/users/reset_password.ex b/lib/service/users/reset_password.ex index dd6341877..1fa744f54 100644 --- a/lib/service/users/reset_password.ex +++ b/lib/service/users/reset_password.ex @@ -1,13 +1,13 @@ defmodule Mobilizon.Service.Users.ResetPassword do @moduledoc false - alias Mobilizon.Email.Mailer - alias Mobilizon.Email.User, as: UserEmail alias Mobilizon.Service.Users.Tools alias Mobilizon.Storage.Repo alias Mobilizon.Users alias Mobilizon.Users.User + alias MobilizonWeb.Email + require Logger @doc """ @@ -51,8 +51,8 @@ defmodule Mobilizon.Service.Users.ResetPassword do ) do mail = user_updated - |> UserEmail.reset_password_email(locale) - |> Mailer.deliver_later() + |> Email.User.reset_password_email(locale) + |> Email.Mailer.deliver_later() {:ok, mail} else diff --git a/mix.exs b/mix.exs index 34fd71674..920da69ea 100644 --- a/mix.exs +++ b/mix.exs @@ -315,9 +315,9 @@ defmodule Mobilizon.Mixfile do Tools: [ Mobilizon.Application, Mobilizon.Factory, - Mobilizon.Email.Mailer, - Mobilizon.EmailView, - Mobilizon.Email.User + MobilizonWeb.Email.Mailer, + MobilizonWeb.Email.User, + MobilizonWeb.EmailView ] ] end diff --git a/test/mobilizon_web/resolvers/user_resolver_test.exs b/test/mobilizon_web/resolvers/user_resolver_test.exs index 52e37c6e9..5f483e9b8 100644 --- a/test/mobilizon_web/resolvers/user_resolver_test.exs +++ b/test/mobilizon_web/resolvers/user_resolver_test.exs @@ -1,15 +1,19 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do use MobilizonWeb.ConnCase - alias Mobilizon.{Actors, Config, Users} - alias Mobilizon.Actors.Actor - alias Mobilizon.Users.User - alias Mobilizon.Users - alias MobilizonWeb.AbsintheHelpers - alias Mobilizon.Service.Users.ResetPassword + import Mobilizon.Factory import Mock + use Bamboo.Test + alias Mobilizon.{Actors, Config, Users} + alias Mobilizon.Actors.Actor + alias Mobilizon.Service.Users.ResetPassword + alias Mobilizon.Users.User + alias Mobilizon.Users + + alias MobilizonWeb.{AbsintheHelpers, Email} + @valid_actor_params %{email: "test@test.tld", password: "testest", username: "test"} @valid_single_actor_params %{preferred_username: "test2", keys: "yolo"} @@ -503,7 +507,7 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) assert json_response(res, 200)["data"]["resendConfirmationEmail"] == user.email - assert_delivered_email(Mobilizon.Email.User.confirmation_email(user)) + assert_delivered_email(Email.User.confirmation_email(user)) end test "test resend_confirmation_email/3 with invalid email resends an validation email", From 8f580ce10cb590d8157cd1ad7f3a59578721896f Mon Sep 17 00:00:00 2001 From: miffigriffi Date: Tue, 17 Sep 2019 22:10:22 +0200 Subject: [PATCH 25/29] Returm generation of URL by usage of MobilizonWeb.Endpoint --- lib/mobilizon/events/comment.ex | 6 ++++-- lib/mobilizon/events/participant.ex | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/mobilizon/events/comment.ex b/lib/mobilizon/events/comment.ex index 3856d1c2c..a9c9b7869 100644 --- a/lib/mobilizon/events/comment.ex +++ b/lib/mobilizon/events/comment.ex @@ -8,9 +8,11 @@ defmodule Mobilizon.Events.Comment do import Ecto.Changeset alias Mobilizon.Actors.Actor - alias Mobilizon.Config alias Mobilizon.Events.{Comment, CommentVisibility, Event} + alias MobilizonWeb.Router.Helpers, as: Routes + alias MobilizonWeb.Endpoint + @type t :: %__MODULE__{ text: String.t(), url: String.t(), @@ -66,5 +68,5 @@ defmodule Mobilizon.Events.Comment do end @spec generate_url(String.t()) :: String.t() - defp generate_url(uuid), do: "#{Config.instance_hostname()}/comments/#{uuid}" + defp generate_url(uuid), do: Routes.page_url(Endpoint, :comment, uuid) end diff --git a/lib/mobilizon/events/participant.ex b/lib/mobilizon/events/participant.ex index 93a5a2588..c3d851e18 100644 --- a/lib/mobilizon/events/participant.ex +++ b/lib/mobilizon/events/participant.ex @@ -8,10 +8,11 @@ defmodule Mobilizon.Events.Participant do import Ecto.Changeset alias Mobilizon.Actors.Actor - alias Mobilizon.Config alias Mobilizon.Events alias Mobilizon.Events.{Event, ParticipantRole} + alias MobilizonWeb.Endpoint + @type t :: %__MODULE__{ role: ParticipantRole.t(), url: String.t(), @@ -82,5 +83,5 @@ defmodule Mobilizon.Events.Participant do end @spec generate_url(String.t()) :: String.t() - defp generate_url(uuid), do: "#{Config.instance_hostname()}/join/event/#{uuid}" + defp generate_url(uuid), do: "#{Endpoint.url()}/join/event/#{uuid}" end From 48dbec51f57bc2ea8650e6062c7ef9b76e992383 Mon Sep 17 00:00:00 2001 From: miffigriffi Date: Tue, 17 Sep 2019 23:39:26 +0200 Subject: [PATCH 26/29] Move caching to MobilizonWeb --- lib/mobilizon/actors/actor.ex | 11 --- lib/mobilizon/actors/actors.ex | 18 ----- lib/mobilizon/events/events.ex | 30 -------- lib/mobilizon_web/cache.ex | 24 +++++++ lib/mobilizon_web/cache/activity_pub.ex | 70 +++++++++++++++++++ .../controllers/activity_pub_controller.ex | 9 +-- .../controllers/feed_controller.ex | 20 +++--- .../controllers/page_controller.ex | 10 +-- mix.exs | 1 + 9 files changed, 112 insertions(+), 81 deletions(-) create mode 100644 lib/mobilizon_web/cache.ex create mode 100644 lib/mobilizon_web/cache/activity_pub.ex diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex index 5e7add0d5..610c68a73 100644 --- a/lib/mobilizon/actors/actor.ex +++ b/lib/mobilizon/actors/actor.ex @@ -351,17 +351,6 @@ defmodule Mobilizon.Actors.Actor do |> URI.decode() end - @doc """ - Clear multiple caches for an actor - """ - # TODO: move to MobilizonWeb - @spec clear_cache(t) :: {:ok, true} - def clear_cache(%__MODULE__{preferred_username: preferred_username, domain: nil}) do - Cachex.del(:activity_pub, "actor_" <> preferred_username) - Cachex.del(:feed, "actor_" <> preferred_username) - Cachex.del(:ics, "actor_" <> preferred_username) - end - @spec build_relay_creation_attrs(map) :: map defp build_relay_creation_attrs(%{url: url, preferred_username: preferred_username}) do %{ diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index a4f455dd7..8723d2550 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -160,24 +160,6 @@ defmodule Mobilizon.Actors do |> Repo.preload(:organized_events) end - @doc """ - Gets a cached local actor by username. - #TODO: move to MobilizonWeb layer - """ - @spec get_cached_local_actor_by_name(String.t()) :: - {:commit, Actor.t()} | {:ignore, any()} - def get_cached_local_actor_by_name(name) do - Cachex.fetch(:activity_pub, "actor_" <> name, fn "actor_" <> name -> - case get_local_actor_by_name(name) do - nil -> - {:ignore, nil} - - %Actor{} = actor -> - {:commit, actor} - end - end) - end - @doc """ Creates an actor. """ diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index 45a84007d..e3354b386 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -225,21 +225,6 @@ defmodule Mobilizon.Events do |> Repo.one() end - # TODO: move to MobilizonWeb - @spec get_cached_public_event_by_uuid_with_preload(String.t()) :: - {:commit, Event.t()} | {:ignore, nil} - def get_cached_public_event_by_uuid_with_preload(uuid) do - Cachex.fetch(:activity_pub, "event_" <> uuid, fn "event_" <> uuid -> - case get_public_event_by_uuid_with_preload(uuid) do - %Event{} = event -> - {:commit, event} - - nil -> - {:ignore, nil} - end - end) - end - @doc """ Creates an event. """ @@ -837,21 +822,6 @@ defmodule Mobilizon.Events do |> Repo.preload(@comment_preloads) end - # TODO: move to MobilizonWeb - @spec get_cached_comment_by_uuid_with_preload(String.t()) :: - {:commit, Comment.t()} | {:ignore, nil} - def get_cached_comment_by_uuid_with_preload(uuid) do - Cachex.fetch(:activity_pub, "comment_" <> uuid, fn "comment_" <> uuid -> - case get_comment_from_uuid_with_preload(uuid) do - %Comment{} = comment -> - {:commit, comment} - - nil -> - {:ignore, nil} - end - end) - end - @doc """ Creates a comment. """ diff --git a/lib/mobilizon_web/cache.ex b/lib/mobilizon_web/cache.ex new file mode 100644 index 000000000..70fba94f7 --- /dev/null +++ b/lib/mobilizon_web/cache.ex @@ -0,0 +1,24 @@ +defmodule MobilizonWeb.Cache do + @moduledoc """ + Facade module which provides access to all cached data. + """ + + alias Mobilizon.Actors.Actor + + alias MobilizonWeb.Cache.ActivityPub + + @caches [:activity_pub, :feed, :ics] + + @doc """ + Clears all caches for an actor. + """ + @spec clear_cache(Actor.t()) :: {:ok, true} + def clear_cache(%Actor{preferred_username: preferred_username, domain: nil}) do + Enum.each(@caches, &Cachex.del(&1, "actor_" <> preferred_username)) + end + + defdelegate get_local_actor_by_name(name), to: ActivityPub + defdelegate get_public_event_by_uuid_with_preload(uuid), to: ActivityPub + defdelegate get_comment_by_uuid_with_preload(uuid), to: ActivityPub + defdelegate get_relay, to: ActivityPub +end diff --git a/lib/mobilizon_web/cache/activity_pub.ex b/lib/mobilizon_web/cache/activity_pub.ex new file mode 100644 index 000000000..06e9e4311 --- /dev/null +++ b/lib/mobilizon_web/cache/activity_pub.ex @@ -0,0 +1,70 @@ +defmodule MobilizonWeb.Cache.ActivityPub do + @moduledoc """ + The ActivityPub related functions. + """ + + alias Mobilizon.{Actors, Events, Service} + alias Mobilizon.Actors.Actor + alias Mobilizon.Events.{Comment, Event} + + @cache :activity_pub + + @doc """ + Gets a local actor by username. + """ + @spec get_local_actor_by_name(String.t()) :: + {:commit, Actor.t()} | {:ignore, nil} + def get_local_actor_by_name(name) do + Cachex.fetch(@cache, "actor_" <> name, fn "actor_" <> name -> + case Actors.get_local_actor_by_name(name) do + %Actor{} = actor -> + {:commit, actor} + + nil -> + {:ignore, nil} + end + end) + end + + @doc """ + Gets a public event by its UUID, with all associations loaded. + """ + @spec get_public_event_by_uuid_with_preload(String.t()) :: + {:commit, Event.t()} | {:ignore, nil} + def get_public_event_by_uuid_with_preload(uuid) do + Cachex.fetch(@cache, "event_" <> uuid, fn "event_" <> uuid -> + case Events.get_public_event_by_uuid_with_preload(uuid) do + %Event{} = event -> + {:commit, event} + + nil -> + {:ignore, nil} + end + end) + end + + @doc """ + Gets a comment by its UUID, with all associations loaded. + """ + @spec get_comment_by_uuid_with_preload(String.t()) :: + {:commit, Comment.t()} | {:ignore, nil} + def get_comment_by_uuid_with_preload(uuid) do + Cachex.fetch(@cache, "comment_" <> uuid, fn "comment_" <> uuid -> + case Events.get_comment_from_uuid_with_preload(uuid) do + %Comment{} = comment -> + {:commit, comment} + + nil -> + {:ignore, nil} + end + end) + end + + @doc """ + Gets a relay. + """ + @spec get_relay :: {:commit, Actor.t()} | {:ignore, nil} + def get_relay do + Cachex.fetch(@cache, "relay_actor", &Service.ActivityPub.Relay.get_actor/0) + end +end diff --git a/lib/mobilizon_web/controllers/activity_pub_controller.ex b/lib/mobilizon_web/controllers/activity_pub_controller.ex index 6fa703f59..0c749c963 100644 --- a/lib/mobilizon_web/controllers/activity_pub_controller.ex +++ b/lib/mobilizon_web/controllers/activity_pub_controller.ex @@ -11,6 +11,7 @@ defmodule MobilizonWeb.ActivityPubController do alias Mobilizon.Service.Federator alias MobilizonWeb.ActivityPub.ActorView + alias MobilizonWeb.Cache require Logger @@ -113,13 +114,7 @@ defmodule MobilizonWeb.ActivityPubController do end def relay(conn, _params) do - with {status, actor} <- - Cachex.fetch( - :activity_pub, - "relay_actor", - &Mobilizon.Service.ActivityPub.Relay.get_actor/0 - ), - true <- status in [:ok, :commit] do + with {:commit, %Actor{} = actor} <- Cache.get_relay() do conn |> put_resp_header("content-type", "application/activity+json") |> json(ActorView.render("actor.json", %{actor: actor})) diff --git a/lib/mobilizon_web/controllers/feed_controller.ex b/lib/mobilizon_web/controllers/feed_controller.ex index 3b7a274be..4e4669b7d 100644 --- a/lib/mobilizon_web/controllers/feed_controller.ex +++ b/lib/mobilizon_web/controllers/feed_controller.ex @@ -8,60 +8,60 @@ defmodule MobilizonWeb.FeedController do def actor(conn, %{"name" => name, "format" => "atom"}) do case Cachex.fetch(:feed, "actor_" <> name) do - {status, data} when status in [:ok, :commit] -> + {:commit, data} -> conn |> put_resp_content_type("application/atom+xml") |> send_resp(200, data) - _err -> + _ -> {:error, :not_found} end end def actor(conn, %{"name" => name, "format" => "ics"}) do case Cachex.fetch(:ics, "actor_" <> name) do - {status, data} when status in [:ok, :commit] -> + {:commit, data} -> conn |> put_resp_content_type("text/calendar") |> send_resp(200, data) - _err -> + _ -> {:error, :not_found} end end def event(conn, %{"uuid" => uuid, "format" => "ics"}) do case Cachex.fetch(:ics, "event_" <> uuid) do - {status, data} when status in [:ok, :commit] -> + {:commit, data} -> conn |> put_resp_content_type("text/calendar") |> send_resp(200, data) - _err -> + _ -> {:error, :not_found} end end def going(conn, %{"token" => token, "format" => "ics"}) do case Cachex.fetch(:ics, "token_" <> token) do - {status, data} when status in [:ok, :commit] -> + {:commit, data} -> conn |> put_resp_content_type("text/calendar") |> send_resp(200, data) - _err -> + _ -> {:error, :not_found} end end def going(conn, %{"token" => token, "format" => "atom"}) do case Cachex.fetch(:feed, "token_" <> token) do - {status, data} when status in [:ok, :commit] -> + {:commit, data} -> conn |> put_resp_content_type("application/atom+xml") |> send_resp(200, data) - _err -> + {:ignore, _} -> {:error, :not_found} end end diff --git a/lib/mobilizon_web/controllers/page_controller.ex b/lib/mobilizon_web/controllers/page_controller.ex index d09a4f4bf..6e7f5fe89 100644 --- a/lib/mobilizon_web/controllers/page_controller.ex +++ b/lib/mobilizon_web/controllers/page_controller.ex @@ -3,8 +3,8 @@ defmodule MobilizonWeb.PageController do Controller to load our webapp """ use MobilizonWeb, :controller - alias Mobilizon.Actors - alias Mobilizon.Events + + alias MobilizonWeb.Cache plug(:put_layout, false) action_fallback(MobilizonWeb.FallbackController) @@ -12,17 +12,17 @@ defmodule MobilizonWeb.PageController do def index(conn, _params), do: render(conn, :index) def actor(conn, %{"name" => name}) do - {status, actor} = Actors.get_cached_local_actor_by_name(name) + {status, actor} = Cache.get_local_actor_by_name(name) render_or_error(conn, &ok_status?/2, status, :actor, actor) end def event(conn, %{"uuid" => uuid}) do - {status, event} = Events.get_cached_public_event_by_uuid_with_preload(uuid) + {status, event} = Cache.get_public_event_by_uuid_with_preload(uuid) render_or_error(conn, &ok_status_and_is_visible?/2, status, :event, event) end def comment(conn, %{"uuid" => uuid}) do - {status, comment} = Events.get_cached_comment_by_uuid_with_preload(uuid) + {status, comment} = Cache.get_comment_by_uuid_with_preload(uuid) render_or_error(conn, &ok_status_and_is_visible?/2, status, :comment, comment) end diff --git a/mix.exs b/mix.exs index 920da69ea..853a977ee 100644 --- a/mix.exs +++ b/mix.exs @@ -236,6 +236,7 @@ defmodule Mobilizon.Mixfile do MobilizonWeb.Router.Helpers, MobilizonWeb.AuthErrorHandler, MobilizonWeb.AuthPipeline, + MobilizonWeb.Cache, MobilizonWeb.ChangesetView, MobilizonWeb.Context, MobilizonWeb.Endpoint, From eaff073cd220ef87b8150454fc91fbbf796eddbc Mon Sep 17 00:00:00 2001 From: miffigriffi Date: Wed, 18 Sep 2019 00:18:45 +0200 Subject: [PATCH 27/29] Remove Dataloader related code from Mobilizon context --- lib/mobilizon/actors/actors.ex | 8 -------- lib/mobilizon/addresses/addresses.ex | 8 -------- lib/mobilizon/events/events.ex | 8 -------- lib/mobilizon/media/media.ex | 8 -------- lib/mobilizon/reports/reports.ex | 8 -------- lib/mobilizon/users/users.ex | 8 -------- lib/mobilizon_web/schema.ex | 14 +++++++++----- 7 files changed, 9 insertions(+), 53 deletions(-) diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index 8723d2550..b58138648 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -48,14 +48,6 @@ defmodule Mobilizon.Actors do @public_visibility [:public, :unlisted] @administrator_roles [:creator, :administrator] - @doc false - @spec data :: Dataloader.Ecto.t() - def data, do: Dataloader.Ecto.new(Repo, query: &query/2) - - @doc false - @spec query(Ecto.Query.t(), map) :: Ecto.Query.t() - def query(queryable, _params), do: queryable - @doc """ Gets a single actor. """ diff --git a/lib/mobilizon/addresses/addresses.ex b/lib/mobilizon/addresses/addresses.ex index fdef1ca7b..0c250e107 100644 --- a/lib/mobilizon/addresses/addresses.ex +++ b/lib/mobilizon/addresses/addresses.ex @@ -10,14 +10,6 @@ defmodule Mobilizon.Addresses do require Logger - @doc false - @spec data :: Dataloader.Ecto.t() - def data, do: Dataloader.Ecto.new(Repo, query: &query/2) - - @doc false - @spec query(Ecto.Query.t(), map) :: Ecto.Query.t() - def query(queryable, _params), do: queryable - @doc """ Gets a single address. """ diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index e3354b386..152ce813f 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -90,14 +90,6 @@ defmodule Mobilizon.Events do @comment_preloads [:actor, :attributed_to, :in_reply_to_comment] - @doc false - @spec data :: Dataloader.Ecto.t() - def data, do: Dataloader.Ecto.new(Repo, query: &query/2) - - @doc false - @spec query(Ecto.Query.t(), map) :: Ecto.Query.t() - def query(queryable, _params), do: queryable - @doc """ Gets a single event. """ diff --git a/lib/mobilizon/media/media.ex b/lib/mobilizon/media/media.ex index c19234438..2062ae074 100644 --- a/lib/mobilizon/media/media.ex +++ b/lib/mobilizon/media/media.ex @@ -10,14 +10,6 @@ defmodule Mobilizon.Media do alias Mobilizon.Media.{File, Picture} alias Mobilizon.Storage.Repo - @doc false - @spec data :: Dataloader.Ecto.t() - def data, do: Dataloader.Ecto.new(Repo, query: &query/2) - - @doc false - @spec query(Ecto.Query.t(), map) :: Ecto.Query.t() - def query(queryable, _params), do: queryable - @doc """ Gets a single picture. """ diff --git a/lib/mobilizon/reports/reports.ex b/lib/mobilizon/reports/reports.ex index 95c96f73f..21d7148a8 100644 --- a/lib/mobilizon/reports/reports.ex +++ b/lib/mobilizon/reports/reports.ex @@ -13,14 +13,6 @@ defmodule Mobilizon.Reports do defenum(ReportStatus, :report_status, [:open, :closed, :resolved]) - @doc false - @spec data :: Dataloader.Ecto.t() - def data, do: Dataloader.Ecto.new(Repo, query: &query/2) - - @doc false - @spec query(Ecto.Query.t(), map) :: Ecto.Query.t() - def query(queryable, _params), do: queryable - @doc """ Gets a single report. """ diff --git a/lib/mobilizon/users/users.ex b/lib/mobilizon/users/users.ex index 1c3ddabb9..2fe784980 100644 --- a/lib/mobilizon/users/users.ex +++ b/lib/mobilizon/users/users.ex @@ -20,14 +20,6 @@ defmodule Mobilizon.Users do defenum(UserRole, :user_role, [:administrator, :moderator, :user]) - @doc false - @spec data :: Dataloader.Ecto.t() - def data, do: Dataloader.Ecto.new(Repo, query: &query/2) - - @doc false - @spec query(Ecto.Query.t(), map) :: Ecto.Query.t() - def query(queryable, _params), do: queryable - @doc """ Registers an user. """ diff --git a/lib/mobilizon_web/schema.ex b/lib/mobilizon_web/schema.ex index c3819ab75..8411349aa 100644 --- a/lib/mobilizon_web/schema.ex +++ b/lib/mobilizon_web/schema.ex @@ -7,6 +7,7 @@ defmodule MobilizonWeb.Schema do alias Mobilizon.{Actors, Events, Users, Addresses, Media} alias Mobilizon.Actors.{Actor, Follower, Member} alias Mobilizon.Events.{Event, Comment, Participant} + alias Mobilizon.Storage.Repo import_types(MobilizonWeb.Schema.Custom.UUID) import_types(MobilizonWeb.Schema.Custom.Point) @@ -87,13 +88,16 @@ defmodule MobilizonWeb.Schema do end def context(ctx) do + default_query = fn queryable, _params -> queryable end + default_source = Dataloader.Ecto.new(Repo, query: default_query) + loader = Dataloader.new() - |> Dataloader.add_source(Actors, Actors.data()) - |> Dataloader.add_source(Users, Users.data()) - |> Dataloader.add_source(Events, Events.data()) - |> Dataloader.add_source(Addresses, Addresses.data()) - |> Dataloader.add_source(Media, Media.data()) + |> Dataloader.add_source(Actors, default_source) + |> Dataloader.add_source(Users, default_source) + |> Dataloader.add_source(Events, default_source) + |> Dataloader.add_source(Addresses, default_source) + |> Dataloader.add_source(Media, default_source) Map.put(ctx, :loader, loader) end From 64f42d412b8560a7d126935767da57c7a54e5068 Mon Sep 17 00:00:00 2001 From: miffigriffi Date: Wed, 18 Sep 2019 00:37:31 +0200 Subject: [PATCH 28/29] mix credo warnings in Mobilizon context --- lib/mobilizon/actors/actors.ex | 8 ++++++-- lib/mobilizon/events/events.ex | 8 ++++++-- test/mobilizon/events/events_test.exs | 4 ++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index b58138648..66b53fa9c 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -115,7 +115,9 @@ defmodule Mobilizon.Actors do """ @spec get_actor_by_name(String.t(), atom | nil) :: Actor.t() | nil def get_actor_by_name(name, type \\ nil) do - from(a in Actor) + query = from(a in Actor) + + query |> filter_by_type(type) |> filter_by_name(String.split(name, "@")) |> Repo.one() @@ -126,7 +128,9 @@ defmodule Mobilizon.Actors do """ @spec get_local_actor_by_name(String.t()) :: Actor.t() | nil def get_local_actor_by_name(name) do - from(a in Actor) + query = from(a in Actor) + + query |> filter_by_name([name]) |> Repo.one() end diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index 152ce813f..d85f88b51 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -293,7 +293,9 @@ defmodule Mobilizon.Events do is_unlisted \\ false, is_future \\ true ) do - from(e in Event, preload: [:organizer_actor, :participants]) + query = from(e in Event, preload: [:organizer_actor, :participants]) + + query |> Page.paginate(page, limit) |> sort(sort, direction) |> filter_future_events(is_future) @@ -779,8 +781,10 @@ defmodule Mobilizon.Events do @spec get_comment_from_url_with_preload(String.t()) :: {:ok, Comment.t()} | {:error, :comment_not_found} def get_comment_from_url_with_preload(url) do + query = from(c in Comment, where: c.url == ^url) + comment = - from(c in Comment, where: c.url == ^url) + query |> preload_for_comment() |> Repo.one() diff --git a/test/mobilizon/events/events_test.exs b/test/mobilizon/events/events_test.exs index 440c1029b..83e5dca53 100644 --- a/test/mobilizon/events/events_test.exs +++ b/test/mobilizon/events/events_test.exs @@ -419,7 +419,7 @@ defmodule Mobilizon.EventsTest do test "list_sessions_for_event/1 returns sessions for an event" do event = insert(:event) session = insert(:session, event: event) - assert Events.list_sessions_for_event(event) |> Enum.map(& &1.id) == [session.id] + assert event |> Events.list_sessions_for_event() |> Enum.map(& &1.id) == [session.id] end test "get_session!/1 returns the session with given id" do @@ -491,7 +491,7 @@ defmodule Mobilizon.EventsTest do event = insert(:event) track = insert(:track, event: event) session = insert(:session, track: track, event: event) - assert Events.list_sessions_for_track(track) |> Enum.map(& &1.id) == [session.id] + assert track |> Events.list_sessions_for_track() |> Enum.map(& &1.id) == [session.id] end test "get_track!/1 returns the track with given id" do From 2d087fb810072379e50064230f381808220942de Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Sun, 22 Sep 2019 09:24:18 +0200 Subject: [PATCH 29/29] Move Mobilizon.Events.Activity to Mobilizon.Service.ActivityPub.Activity It's only used for ActivityPub stuff, nothing DB related. Signed-off-by: Thomas Citharel --- lib/mobilizon_web/api/events.ex | 1 + lib/mobilizon_web/api/reports.ex | 2 +- lib/mobilizon_web/resolvers/comment.ex | 4 ++-- lib/mobilizon_web/resolvers/event.ex | 3 ++- lib/mobilizon_web/resolvers/group.ex | 2 +- lib/mobilizon_web/views/activity_pub/actor_view.ex | 2 +- lib/mobilizon_web/views/activity_pub/object_view.ex | 3 +-- lib/{mobilizon/events => service/activity_pub}/activity.ex | 2 +- lib/service/activity_pub/activity_pub.ex | 7 +++---- lib/service/activity_pub/relay.ex | 2 +- lib/service/activity_pub/transmogrifier.ex | 3 +-- lib/service/activity_pub/utils.ex | 4 ++-- lib/service/activity_pub/visibility.ex | 2 +- lib/service/federator.ex | 3 +-- mix.exs | 2 +- .../mobilizon/service/activity_pub/transmogrifier_test.exs | 4 ++-- test/mobilizon_web/api/report_test.exs | 3 ++- 17 files changed, 24 insertions(+), 25 deletions(-) rename lib/{mobilizon/events => service/activity_pub}/activity.ex (86%) diff --git a/lib/mobilizon_web/api/events.ex b/lib/mobilizon_web/api/events.ex index 6a9493a25..55a05dbde 100644 --- a/lib/mobilizon_web/api/events.ex +++ b/lib/mobilizon_web/api/events.ex @@ -5,6 +5,7 @@ defmodule MobilizonWeb.API.Events do alias Mobilizon.Events.Event alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils + alias Mobilizon.Service.ActivityPub.Activity alias MobilizonWeb.API.Utils @doc """ diff --git a/lib/mobilizon_web/api/reports.ex b/lib/mobilizon_web/api/reports.ex index 06480e944..ad5cfa0bf 100644 --- a/lib/mobilizon_web/api/reports.ex +++ b/lib/mobilizon_web/api/reports.ex @@ -9,7 +9,7 @@ defmodule MobilizonWeb.API.Reports do alias Mobilizon.Actors alias Mobilizon.Actors.Actor alias Mobilizon.Events - alias Mobilizon.Events.Activity + alias Mobilizon.Service.ActivityPub.Activity alias Mobilizon.Reports, as: ReportsAction alias Mobilizon.Reports.{Report, Note} alias Mobilizon.Service.ActivityPub diff --git a/lib/mobilizon_web/resolvers/comment.ex b/lib/mobilizon_web/resolvers/comment.ex index cbe0b248b..d00b610ce 100644 --- a/lib/mobilizon_web/resolvers/comment.ex +++ b/lib/mobilizon_web/resolvers/comment.ex @@ -3,9 +3,9 @@ defmodule MobilizonWeb.Resolvers.Comment do Handles the comment-related GraphQL calls """ - alias Mobilizon.Events.{Activity, Comment} + alias Mobilizon.Events.Comment alias Mobilizon.Users.User - + alias Mobilizon.Service.ActivityPub.Activity alias MobilizonWeb.API.Comments require Logger diff --git a/lib/mobilizon_web/resolvers/event.ex b/lib/mobilizon_web/resolvers/event.ex index c50089604..d3aff4f94 100644 --- a/lib/mobilizon_web/resolvers/event.ex +++ b/lib/mobilizon_web/resolvers/event.ex @@ -6,12 +6,13 @@ defmodule MobilizonWeb.Resolvers.Event do alias Mobilizon.Addresses alias Mobilizon.Addresses.Address alias Mobilizon.Events - alias Mobilizon.Events.{Activity, Event, Participant} + alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Media.Picture alias Mobilizon.Users.User alias Mobilizon.Actors alias Mobilizon.Actors.Actor alias MobilizonWeb.Resolvers.Person + alias Mobilizon.Service.ActivityPub.Activity import Mobilizon.Service.Admin.ActionLogService # We limit the max number of events that can be retrieved diff --git a/lib/mobilizon_web/resolvers/group.ex b/lib/mobilizon_web/resolvers/group.ex index ccc565aa7..a7330970f 100644 --- a/lib/mobilizon_web/resolvers/group.ex +++ b/lib/mobilizon_web/resolvers/group.ex @@ -4,7 +4,7 @@ defmodule MobilizonWeb.Resolvers.Group do """ alias Mobilizon.Actors alias Mobilizon.Actors.{Actor, Member} - alias Mobilizon.Events.Activity + alias Mobilizon.Service.ActivityPub.Activity alias Mobilizon.Users.User alias Mobilizon.Service.ActivityPub diff --git a/lib/mobilizon_web/views/activity_pub/actor_view.ex b/lib/mobilizon_web/views/activity_pub/actor_view.ex index c1679bb53..b37c5848b 100644 --- a/lib/mobilizon_web/views/activity_pub/actor_view.ex +++ b/lib/mobilizon_web/views/activity_pub/actor_view.ex @@ -3,7 +3,7 @@ defmodule MobilizonWeb.ActivityPub.ActorView do alias Mobilizon.Actors alias Mobilizon.Actors.Actor - alias Mobilizon.Events.Activity + alias Mobilizon.Service.ActivityPub.Activity alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub.Utils diff --git a/lib/mobilizon_web/views/activity_pub/object_view.ex b/lib/mobilizon_web/views/activity_pub/object_view.ex index 576b9e751..4ad2b9fee 100644 --- a/lib/mobilizon_web/views/activity_pub/object_view.ex +++ b/lib/mobilizon_web/views/activity_pub/object_view.ex @@ -1,8 +1,7 @@ defmodule MobilizonWeb.ActivityPub.ObjectView do use MobilizonWeb, :view - alias Mobilizon.Events.Activity - alias Mobilizon.Service.ActivityPub.Utils + alias Mobilizon.Service.ActivityPub.{Activity, Utils} def render("activity.json", %{activity: %Activity{local: local, data: data} = activity}) do %{ diff --git a/lib/mobilizon/events/activity.ex b/lib/service/activity_pub/activity.ex similarity index 86% rename from lib/mobilizon/events/activity.ex rename to lib/service/activity_pub/activity.ex index f3febd324..d0be2e2c0 100644 --- a/lib/mobilizon/events/activity.ex +++ b/lib/service/activity_pub/activity.ex @@ -1,4 +1,4 @@ -defmodule Mobilizon.Events.Activity do +defmodule Mobilizon.Service.ActivityPub.Activity do @moduledoc """ Represents an activity. """ diff --git a/lib/service/activity_pub/activity_pub.ex b/lib/service/activity_pub/activity_pub.ex index fbda9acd0..5dab5b12e 100644 --- a/lib/service/activity_pub/activity_pub.ex +++ b/lib/service/activity_pub/activity_pub.ex @@ -12,7 +12,7 @@ defmodule Mobilizon.Service.ActivityPub do alias Mobilizon.Config alias Mobilizon.Events - alias Mobilizon.Events.{Activity, Event, Comment, Participant} + alias Mobilizon.Events.{Event, Comment, Participant} alias Mobilizon.Service.ActivityPub.Transmogrifier alias Mobilizon.Service.WebFinger @@ -22,11 +22,10 @@ defmodule Mobilizon.Service.ActivityPub do alias Mobilizon.Service.Federator alias Mobilizon.Service.HTTPSignatures.Signature - alias Mobilizon.Service.ActivityPub.Convertible + alias Mobilizon.Service.ActivityPub.{Activity, Convertible} require Logger - import Mobilizon.Service.ActivityPub.Utils - import Mobilizon.Service.ActivityPub.Visibility + import Mobilizon.Service.ActivityPub.{Utils, Visibility} @doc """ Get recipients for an activity or object diff --git a/lib/service/activity_pub/relay.ex b/lib/service/activity_pub/relay.ex index ef44fbbea..7b4396a1f 100644 --- a/lib/service/activity_pub/relay.ex +++ b/lib/service/activity_pub/relay.ex @@ -10,7 +10,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do alias Mobilizon.Actors alias Mobilizon.Actors.Actor - alias Mobilizon.Events.Activity + alias Mobilizon.Service.ActivityPub.Activity alias Mobilizon.Service.ActivityPub alias MobilizonWeb.API.Follows diff --git a/lib/service/activity_pub/transmogrifier.ex b/lib/service/activity_pub/transmogrifier.ex index 41d81c246..ef34e1477 100644 --- a/lib/service/activity_pub/transmogrifier.ex +++ b/lib/service/activity_pub/transmogrifier.ex @@ -12,8 +12,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do alias Mobilizon.Events alias Mobilizon.Events.{Event, Comment, Participant} alias Mobilizon.Service.ActivityPub - alias Mobilizon.Service.ActivityPub.Utils - alias Mobilizon.Service.ActivityPub.Visibility + alias Mobilizon.Service.ActivityPub.{Visibility, Utils} require Logger diff --git a/lib/service/activity_pub/utils.ex b/lib/service/activity_pub/utils.ex index 04230abdc..9f9caf2c0 100644 --- a/lib/service/activity_pub/utils.ex +++ b/lib/service/activity_pub/utils.ex @@ -17,11 +17,11 @@ defmodule Mobilizon.Service.ActivityPub.Utils do alias Mobilizon.Actors alias Mobilizon.Actors.Actor alias Mobilizon.Events - alias Mobilizon.Events.{Activity, Comment, Event} + alias Mobilizon.Events.{Comment, Event} alias Mobilizon.Media.Picture alias Mobilizon.Reports alias Mobilizon.Reports.Report - alias Mobilizon.Service.ActivityPub.Converters + alias Mobilizon.Service.ActivityPub.{Activity, Converters} alias Mobilizon.Storage.Repo alias Mobilizon.Users diff --git a/lib/service/activity_pub/visibility.ex b/lib/service/activity_pub/visibility.ex index 756965910..adca3e2aa 100644 --- a/lib/service/activity_pub/visibility.ex +++ b/lib/service/activity_pub/visibility.ex @@ -8,7 +8,7 @@ defmodule Mobilizon.Service.ActivityPub.Visibility do Utility functions related to content visibility """ - alias Mobilizon.Events.Activity + alias Mobilizon.Service.ActivityPub.Activity @public "https://www.w3.org/ns/activitystreams#Public" diff --git a/lib/service/federator.ex b/lib/service/federator.ex index 041e87fcb..a0051706a 100644 --- a/lib/service/federator.ex +++ b/lib/service/federator.ex @@ -11,9 +11,8 @@ defmodule Mobilizon.Service.Federator do use GenServer alias Mobilizon.Actors - alias Mobilizon.Events.Activity alias Mobilizon.Service.ActivityPub - alias Mobilizon.Service.ActivityPub.Transmogrifier + alias Mobilizon.Service.ActivityPub.{Activity, Transmogrifier} require Logger diff --git a/mix.exs b/mix.exs index 853a977ee..68a3a619d 100644 --- a/mix.exs +++ b/mix.exs @@ -197,7 +197,7 @@ defmodule Mobilizon.Mixfile do Mobilizon.Addresses, Mobilizon.Addresses.Address, Mobilizon.Events, - Mobilizon.Events.Activity, + Mobilizon.Service.ActivityPub.Activity, Mobilizon.Events.Event, Mobilizon.Events.Comment, Mobilizon.Events.FeedToken, diff --git a/test/mobilizon/service/activity_pub/transmogrifier_test.exs b/test/mobilizon/service/activity_pub/transmogrifier_test.exs index 4db06e085..19b816139 100644 --- a/test/mobilizon/service/activity_pub/transmogrifier_test.exs +++ b/test/mobilizon/service/activity_pub/transmogrifier_test.exs @@ -11,9 +11,9 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do alias Mobilizon.Actors alias Mobilizon.Actors.Actor alias Mobilizon.Events - alias Mobilizon.Events.{Activity, Comment, Event, Participant} + alias Mobilizon.Events.{Comment, Event, Participant} alias Mobilizon.Service.ActivityPub - alias Mobilizon.Service.ActivityPub.Utils + alias Mobilizon.Service.ActivityPub.{Utils, Activity} alias Mobilizon.Service.ActivityPub.Transmogrifier use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney diff --git a/test/mobilizon_web/api/report_test.exs b/test/mobilizon_web/api/report_test.exs index 8c85e4fc7..9ff39b988 100644 --- a/test/mobilizon_web/api/report_test.exs +++ b/test/mobilizon_web/api/report_test.exs @@ -4,10 +4,11 @@ defmodule MobilizonWeb.API.ReportTest do import Mobilizon.Factory alias Mobilizon.Actors.Actor - alias Mobilizon.Events.{Activity, Comment, Event} + alias Mobilizon.Events.{Comment, Event} alias Mobilizon.Reports.{Report, Note} alias Mobilizon.Users alias Mobilizon.Users.User + alias Mobilizon.Service.ActivityPub.Activity alias MobilizonWeb.API.Reports