diff --git a/docker/app/run-development b/docker/app/run-development index d593f820..2990b9a5 100755 --- a/docker/app/run-development +++ b/docker/app/run-development @@ -9,6 +9,7 @@ background() { mix run -e 'Philomena.Release.update_channels()' mix run -e 'Philomena.Release.verify_artist_links()' mix run -e 'Philomena.Release.update_stats()' + mix run -e 'Philomena.Release.clean_moderation_logs()' sleep 300 done diff --git a/lib/philomena/moderation_logs.ex b/lib/philomena/moderation_logs.ex new file mode 100644 index 00000000..dcdb53f4 --- /dev/null +++ b/lib/philomena/moderation_logs.ex @@ -0,0 +1,83 @@ +defmodule Philomena.ModerationLogs do + @moduledoc """ + The ModerationLogs context. + """ + + import Ecto.Query, warn: false + alias Philomena.Repo + + alias Philomena.ModerationLogs.ModerationLog + + @doc """ + Returns the list of moderation_logs. + + ## Examples + + iex> list_moderation_logs() + [%ModerationLog{}, ...] + + """ + def list_moderation_logs(conn) do + ModerationLog + |> where([ml], ml.created_at > ago(2, "week")) + |> preload(:user) + |> order_by(desc: :created_at) + |> Repo.paginate(conn.assigns.scrivener) + end + + @doc """ + Gets a single moderation_log. + + Raises `Ecto.NoResultsError` if the Moderation log does not exist. + + ## Examples + + iex> get_moderation_log!(123) + %ModerationLog{} + + iex> get_moderation_log!(456) + ** (Ecto.NoResultsError) + + """ + def get_moderation_log!(id), do: Repo.get!(ModerationLog, id) + + @doc """ + Creates a moderation_log. + + ## Examples + + iex> create_moderation_log(%{field: value}) + {:ok, %ModerationLog{}} + + iex> create_moderation_log(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_moderation_log(user, type, subject_path, body) do + %ModerationLog{user_id: user.id} + |> ModerationLog.changeset(%{type: type, subject_path: subject_path, body: body}) + |> Repo.insert() + end + + @doc """ + Deletes a moderation_log. + + ## Examples + + iex> delete_moderation_log(moderation_log) + {:ok, %ModerationLog{}} + + iex> delete_moderation_log(moderation_log) + {:error, %Ecto.Changeset{}} + + """ + def delete_moderation_log(%ModerationLog{} = moderation_log) do + Repo.delete(moderation_log) + end + + def cleanup! do + ModerationLog + |> where([ml], ml.created_at < ago(2, "week")) + |> Repo.delete_all() + end +end diff --git a/lib/philomena/moderation_logs/moderation_log.ex b/lib/philomena/moderation_logs/moderation_log.ex new file mode 100644 index 00000000..e85ac6f0 --- /dev/null +++ b/lib/philomena/moderation_logs/moderation_log.ex @@ -0,0 +1,23 @@ +defmodule Philomena.ModerationLogs.ModerationLog do + use Ecto.Schema + import Ecto.Changeset + + alias Philomena.Users.User + + schema "moderation_logs" do + belongs_to :user, User + + field :body, :string + field :type, :string + field :subject_path, :string + + timestamps(inserted_at: :created_at, updated_at: false, type: :utc_datetime) + end + + @doc false + def changeset(moderation_log, attrs) do + moderation_log + |> cast(attrs, [:body, :type, :subject_path]) + |> validate_required([:body, :type, :subject_path]) + end +end diff --git a/lib/philomena/release.ex b/lib/philomena/release.ex index 4820775d..8963bff2 100644 --- a/lib/philomena/release.ex +++ b/lib/philomena/release.ex @@ -29,6 +29,11 @@ defmodule Philomena.Release do PhilomenaWeb.StatsUpdater.update_stats!() end + def clean_moderation_logs do + start_app() + Philomena.ModerationLogs.cleanup!() + end + defp repos do Application.fetch_env!(@app, :ecto_repos) end diff --git a/lib/philomena/users/ability.ex b/lib/philomena/users/ability.ex index 1789c974..ac8d471d 100644 --- a/lib/philomena/users/ability.ex +++ b/lib/philomena/users/ability.ex @@ -24,6 +24,7 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do alias Philomena.StaticPages.StaticPage alias Philomena.Adverts.Advert alias Philomena.SiteNotices.SiteNotice + alias Philomena.ModerationLogs.ModerationLog alias Philomena.Bans.User, as: UserBan alias Philomena.Bans.Subnet, as: SubnetBan @@ -127,6 +128,9 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do # Manage galleries def can?(%User{role: "moderator"}, _action, %Gallery{}), do: true + # See moderation logs + def can?(%User{role: "moderator"}, _action, ModerationLog), do: true + # And some privileged moderators can... # Manage site notices diff --git a/lib/philomena_web.ex b/lib/philomena_web.ex index 2fcddf06..52e299ab 100644 --- a/lib/philomena_web.ex +++ b/lib/philomena_web.ex @@ -24,6 +24,7 @@ defmodule PhilomenaWeb do import Plug.Conn import PhilomenaWeb.Gettext import Canary.Plugs + import PhilomenaWeb.ModerationLogPlug, only: [moderation_log: 2] alias PhilomenaWeb.Router.Helpers, as: Routes end end diff --git a/lib/philomena_web/controllers/admin/artist_link/contact_controller.ex b/lib/philomena_web/controllers/admin/artist_link/contact_controller.ex index c2ece9cb..98efb1f4 100644 --- a/lib/philomena_web/controllers/admin/artist_link/contact_controller.ex +++ b/lib/philomena_web/controllers/admin/artist_link/contact_controller.ex @@ -13,11 +13,19 @@ defmodule PhilomenaWeb.Admin.ArtistLink.ContactController do preload: [:user] def create(conn, _params) do - {:ok, _} = + {:ok, artist_link} = ArtistLinks.contact_artist_link(conn.assigns.artist_link, conn.assigns.current_user) conn |> put_flash(:info, "Artist successfully marked as contacted.") + |> moderation_log(details: &log_details/3, data: artist_link) |> redirect(to: Routes.admin_artist_link_path(conn, :index)) end + + defp log_details(conn, _action, artist_link) do + %{ + body: "Contacted artist #{artist_link.user.name} at #{artist_link.uri}", + subject_path: Routes.profile_artist_link_path(conn, :show, artist_link.user, artist_link) + } + end end diff --git a/lib/philomena_web/controllers/admin/artist_link/reject_controller.ex b/lib/philomena_web/controllers/admin/artist_link/reject_controller.ex index 7c1dbca3..9bf92139 100644 --- a/lib/philomena_web/controllers/admin/artist_link/reject_controller.ex +++ b/lib/philomena_web/controllers/admin/artist_link/reject_controller.ex @@ -13,10 +13,18 @@ defmodule PhilomenaWeb.Admin.ArtistLink.RejectController do preload: [:user] def create(conn, _params) do - {:ok, _} = ArtistLinks.reject_artist_link(conn.assigns.artist_link) + {:ok, artist_link} = ArtistLinks.reject_artist_link(conn.assigns.artist_link) conn |> put_flash(:info, "Artist link successfully marked as rejected.") + |> moderation_log(details: &log_details/3, data: artist_link) |> redirect(to: Routes.admin_artist_link_path(conn, :index)) end + + defp log_details(conn, _action, artist_link) do + %{ + body: "Rejected artist link #{artist_link.uri} created by #{artist_link.user.name}", + subject_path: Routes.profile_artist_link_path(conn, :show, artist_link.user, artist_link) + } + end end diff --git a/lib/philomena_web/controllers/admin/artist_link/verification_controller.ex b/lib/philomena_web/controllers/admin/artist_link/verification_controller.ex index 0b7d7d8e..a4cc1fce 100644 --- a/lib/philomena_web/controllers/admin/artist_link/verification_controller.ex +++ b/lib/philomena_web/controllers/admin/artist_link/verification_controller.ex @@ -13,10 +13,19 @@ defmodule PhilomenaWeb.Admin.ArtistLink.VerificationController do preload: [:user] def create(conn, _params) do - {:ok, _} = ArtistLinks.verify_artist_link(conn.assigns.artist_link, conn.assigns.current_user) + {:ok, result} = + ArtistLinks.verify_artist_link(conn.assigns.artist_link, conn.assigns.current_user) conn |> put_flash(:info, "Artist link successfully verified.") + |> moderation_log(details: &log_details/3, data: result.artist_link) |> redirect(to: Routes.admin_artist_link_path(conn, :index)) end + + defp log_details(conn, _action, artist_link) do + %{ + body: "Verified artist link #{artist_link.uri} created by #{artist_link.user.name}", + subject_path: Routes.profile_artist_link_path(conn, :show, artist_link.user, artist_link) + } + end end diff --git a/lib/philomena_web/controllers/admin/batch/tag_controller.ex b/lib/philomena_web/controllers/admin/batch/tag_controller.ex index 7287bed6..4ae3c714 100644 --- a/lib/philomena_web/controllers/admin/batch/tag_controller.ex +++ b/lib/philomena_web/controllers/admin/batch/tag_controller.ex @@ -9,8 +9,8 @@ defmodule PhilomenaWeb.Admin.Batch.TagController do plug :verify_authorized plug PhilomenaWeb.UserAttributionPlug - def update(conn, %{"tags" => tags, "image_ids" => image_ids}) do - tags = Tag.parse_tag_list(tags) + def update(conn, %{"tags" => tag_list, "image_ids" => image_ids}) do + tags = Tag.parse_tag_list(tag_list) added_tag_names = Enum.reject(tags, &String.starts_with?(&1, "-")) @@ -46,7 +46,15 @@ defmodule PhilomenaWeb.Admin.Batch.TagController do case Images.batch_update(image_ids, added_tags, removed_tags, attributes) do {:ok, _} -> - json(conn, %{succeeded: image_ids, failed: []}) + conn + |> moderation_log( + details: &log_details/3, + data: %{ + tag_list: tag_list, + image_count: Enum.count(image_ids) + } + ) + |> json(%{succeeded: image_ids, failed: []}) _error -> json(conn, %{succeeded: [], failed: image_ids}) @@ -59,4 +67,11 @@ defmodule PhilomenaWeb.Admin.Batch.TagController do _false -> PhilomenaWeb.NotAuthorizedPlug.call(conn) end end + + defp log_details(conn, _action, data) do + %{ + body: "Batch tagged '#{data.tag_list}' on #{data.image_count} images", + subject_path: Routes.profile_path(conn, :show, conn.assigns.current_user) + } + end end diff --git a/lib/philomena_web/controllers/admin/fingerprint_ban_controller.ex b/lib/philomena_web/controllers/admin/fingerprint_ban_controller.ex index ba14ff93..5cb4ccd0 100644 --- a/lib/philomena_web/controllers/admin/fingerprint_ban_controller.ex +++ b/lib/philomena_web/controllers/admin/fingerprint_ban_controller.ex @@ -44,9 +44,10 @@ defmodule PhilomenaWeb.Admin.FingerprintBanController do def create(conn, %{"fingerprint" => fingerprint_ban_params}) do case Bans.create_fingerprint(conn.assigns.current_user, fingerprint_ban_params) do - {:ok, _fingerprint_ban} -> + {:ok, fingerprint_ban} -> conn |> put_flash(:info, "Fingerprint was successfully banned.") + |> moderation_log(details: &log_details/3, data: fingerprint_ban) |> redirect(to: Routes.admin_fingerprint_ban_path(conn, :index)) {:error, changeset} -> @@ -61,9 +62,10 @@ defmodule PhilomenaWeb.Admin.FingerprintBanController do def update(conn, %{"fingerprint" => fingerprint_ban_params}) do case Bans.update_fingerprint(conn.assigns.fingerprint, fingerprint_ban_params) do - {:ok, _fingerprint_ban} -> + {:ok, fingerprint_ban} -> conn |> put_flash(:info, "Fingerprint ban successfully updated.") + |> moderation_log(details: &log_details/3, data: fingerprint_ban) |> redirect(to: Routes.admin_fingerprint_ban_path(conn, :index)) {:error, changeset} -> @@ -72,10 +74,11 @@ defmodule PhilomenaWeb.Admin.FingerprintBanController do end def delete(conn, _params) do - {:ok, _fingerprint_ban} = Bans.delete_fingerprint(conn.assigns.fingerprint) + {:ok, fingerprint_ban} = Bans.delete_fingerprint(conn.assigns.fingerprint) conn |> put_flash(:info, "Fingerprint ban successfully deleted.") + |> moderation_log(details: &log_details/3, data: fingerprint_ban) |> redirect(to: Routes.admin_fingerprint_ban_path(conn, :index)) end @@ -106,4 +109,15 @@ defmodule PhilomenaWeb.Admin.FingerprintBanController do false -> PhilomenaWeb.NotAuthorizedPlug.call(conn) end end + + defp log_details(conn, action, ban) do + body = + case action do + :create -> "Created a fingerprint ban #{ban.generated_ban_id}" + :update -> "Updated a fingerprint ban #{ban.generated_ban_id}" + :delete -> "Deleted a fingerprint ban #{ban.generated_ban_id}" + end + + %{body: body, subject_path: Routes.admin_fingerprint_ban_path(conn, :index)} + end end diff --git a/lib/philomena_web/controllers/admin/subnet_ban_controller.ex b/lib/philomena_web/controllers/admin/subnet_ban_controller.ex index 11f8bacf..93c42807 100644 --- a/lib/philomena_web/controllers/admin/subnet_ban_controller.ex +++ b/lib/philomena_web/controllers/admin/subnet_ban_controller.ex @@ -46,9 +46,10 @@ defmodule PhilomenaWeb.Admin.SubnetBanController do def create(conn, %{"subnet" => subnet_ban_params}) do case Bans.create_subnet(conn.assigns.current_user, subnet_ban_params) do - {:ok, _subnet_ban} -> + {:ok, subnet_ban} -> conn |> put_flash(:info, "Subnet was successfully banned.") + |> moderation_log(details: &log_details/3, data: subnet_ban) |> redirect(to: Routes.admin_subnet_ban_path(conn, :index)) {:error, changeset} -> @@ -63,9 +64,10 @@ defmodule PhilomenaWeb.Admin.SubnetBanController do def update(conn, %{"subnet" => subnet_ban_params}) do case Bans.update_subnet(conn.assigns.subnet, subnet_ban_params) do - {:ok, _subnet_ban} -> + {:ok, subnet_ban} -> conn |> put_flash(:info, "Subnet ban successfully updated.") + |> moderation_log(details: &log_details/3, data: subnet_ban) |> redirect(to: Routes.admin_subnet_ban_path(conn, :index)) {:error, changeset} -> @@ -74,10 +76,11 @@ defmodule PhilomenaWeb.Admin.SubnetBanController do end def delete(conn, _params) do - {:ok, _subnet_ban} = Bans.delete_subnet(conn.assigns.subnet) + {:ok, subnet_ban} = Bans.delete_subnet(conn.assigns.subnet) conn |> put_flash(:info, "Subnet ban successfully deleted.") + |> moderation_log(details: &log_details/3, data: subnet_ban) |> redirect(to: Routes.admin_subnet_ban_path(conn, :index)) end @@ -108,4 +111,15 @@ defmodule PhilomenaWeb.Admin.SubnetBanController do false -> PhilomenaWeb.NotAuthorizedPlug.call(conn) end end + + defp log_details(conn, action, ban) do + body = + case action do + :create -> "Created a subnet ban #{ban.generated_ban_id}" + :update -> "Updated a subnet ban #{ban.generated_ban_id}" + :delete -> "Deleted a subnet ban #{ban.generated_ban_id}" + end + + %{body: body, subject_path: Routes.admin_subnet_ban_path(conn, :index)} + end end diff --git a/lib/philomena_web/controllers/admin/user_ban_controller.ex b/lib/philomena_web/controllers/admin/user_ban_controller.ex index 1a1f2483..f98c3337 100644 --- a/lib/philomena_web/controllers/admin/user_ban_controller.ex +++ b/lib/philomena_web/controllers/admin/user_ban_controller.ex @@ -47,9 +47,10 @@ defmodule PhilomenaWeb.Admin.UserBanController do def create(conn, %{"user" => user_ban_params}) do case Bans.create_user(conn.assigns.current_user, user_ban_params) do - {:ok, _user_ban} -> + {:ok, user_ban} -> conn |> put_flash(:info, "User was successfully banned.") + |> moderation_log(details: &log_details/3, data: user_ban) |> redirect(to: Routes.admin_user_ban_path(conn, :index)) {:error, :user_ban, changeset, _changes} -> @@ -67,9 +68,10 @@ defmodule PhilomenaWeb.Admin.UserBanController do def update(conn, %{"user" => user_ban_params}) do case Bans.update_user(conn.assigns.user, user_ban_params) do - {:ok, _user_ban} -> + {:ok, user_ban} -> conn |> put_flash(:info, "User ban successfully updated.") + |> moderation_log(details: &log_details/3, data: user_ban) |> redirect(to: Routes.admin_user_ban_path(conn, :index)) {:error, changeset} -> @@ -78,10 +80,11 @@ defmodule PhilomenaWeb.Admin.UserBanController do end def delete(conn, _params) do - {:ok, _user_ban} = Bans.delete_user(conn.assigns.user) + {:ok, user_ban} = Bans.delete_user(conn.assigns.user) conn |> put_flash(:info, "User ban successfully deleted.") + |> moderation_log(details: &log_details/3, data: user_ban) |> redirect(to: Routes.admin_user_ban_path(conn, :index)) end @@ -112,4 +115,15 @@ defmodule PhilomenaWeb.Admin.UserBanController do false -> PhilomenaWeb.NotAuthorizedPlug.call(conn) end end + + defp log_details(conn, action, ban) do + body = + case action do + :create -> "Created a user ban #{ban.generated_ban_id}" + :update -> "Updated a user ban #{ban.generated_ban_id}" + :delete -> "Deleted a user ban #{ban.generated_ban_id}" + end + + %{body: body, subject_path: Routes.admin_user_ban_path(conn, :index)} + end end diff --git a/lib/philomena_web/controllers/admin/user_controller.ex b/lib/philomena_web/controllers/admin/user_controller.ex index 98159217..19b1616d 100644 --- a/lib/philomena_web/controllers/admin/user_controller.ex +++ b/lib/philomena_web/controllers/admin/user_controller.ex @@ -59,9 +59,10 @@ defmodule PhilomenaWeb.Admin.UserController do def update(conn, %{"user" => user_params}) do case Users.update_user(conn.assigns.user, user_params) do - {:ok, _user} -> + {:ok, user} -> conn |> put_flash(:info, "User successfully updated.") + |> moderation_log(details: &log_details/3, data: user) |> redirect(to: Routes.profile_path(conn, :show, conn.assigns.user)) {:error, %{user: changeset}} -> @@ -79,4 +80,11 @@ defmodule PhilomenaWeb.Admin.UserController do defp load_roles(conn, _opts) do assign(conn, :roles, Repo.all(Role)) end + + defp log_details(conn, _action, user) do + %{ + body: "Updated user details for #{user.name}", + subject_path: Routes.profile_path(conn, :show, user) + } + end end diff --git a/lib/philomena_web/controllers/duplicate_report/accept_controller.ex b/lib/philomena_web/controllers/duplicate_report/accept_controller.ex index ef789b37..2b21e306 100644 --- a/lib/philomena_web/controllers/duplicate_report/accept_controller.ex +++ b/lib/philomena_web/controllers/duplicate_report/accept_controller.ex @@ -17,9 +17,10 @@ defmodule PhilomenaWeb.DuplicateReport.AcceptController do user = conn.assigns.current_user case DuplicateReports.accept_duplicate_report(report, user) do - {:ok, _report} -> + {:ok, report} -> conn |> put_flash(:info, "Successfully accepted report.") + |> moderation_log(details: &log_details/3, data: report.duplicate_report) |> redirect(to: Routes.duplicate_report_path(conn, :index)) _error -> @@ -28,4 +29,12 @@ defmodule PhilomenaWeb.DuplicateReport.AcceptController do |> redirect(to: Routes.duplicate_report_path(conn, :index)) end end + + defp log_details(conn, _action, report) do + %{ + body: + "Accepted duplicate report, merged #{report.image.id} into #{report.duplicate_of_image.id}", + subject_path: Routes.image_path(conn, :show, report.image) + } + end end diff --git a/lib/philomena_web/controllers/duplicate_report/accept_reverse_controller.ex b/lib/philomena_web/controllers/duplicate_report/accept_reverse_controller.ex index 34953ffc..d112ce27 100644 --- a/lib/philomena_web/controllers/duplicate_report/accept_reverse_controller.ex +++ b/lib/philomena_web/controllers/duplicate_report/accept_reverse_controller.ex @@ -17,9 +17,10 @@ defmodule PhilomenaWeb.DuplicateReport.AcceptReverseController do user = conn.assigns.current_user case DuplicateReports.accept_reverse_duplicate_report(report, user) do - {:ok, _report} -> + {:ok, report} -> conn |> put_flash(:info, "Successfully accepted report in reverse.") + |> moderation_log(details: &log_details/3, data: report.duplicate_report) |> redirect(to: Routes.duplicate_report_path(conn, :index)) _error -> @@ -28,4 +29,12 @@ defmodule PhilomenaWeb.DuplicateReport.AcceptReverseController do |> redirect(to: Routes.duplicate_report_path(conn, :index)) end end + + defp log_details(conn, _action, report) do + %{ + body: + "Reverse-accepted duplicate report, merged #{report.image.id} into #{report.duplicate_of_image.id}", + subject_path: Routes.image_path(conn, :show, report.image) + } + end end diff --git a/lib/philomena_web/controllers/duplicate_report/claim_controller.ex b/lib/philomena_web/controllers/duplicate_report/claim_controller.ex index e7a0e637..d091381b 100644 --- a/lib/philomena_web/controllers/duplicate_report/claim_controller.ex +++ b/lib/philomena_web/controllers/duplicate_report/claim_controller.ex @@ -12,7 +12,7 @@ defmodule PhilomenaWeb.DuplicateReport.ClaimController do persisted: true def create(conn, _params) do - {:ok, _report} = + {:ok, report} = DuplicateReports.claim_duplicate_report( conn.assigns.duplicate_report, conn.assigns.current_user @@ -20,14 +20,29 @@ defmodule PhilomenaWeb.DuplicateReport.ClaimController do conn |> put_flash(:info, "Successfully claimed report.") + |> moderation_log(details: &log_details/3, data: report) |> redirect(to: Routes.duplicate_report_path(conn, :index)) end def delete(conn, _params) do - {:ok, _report} = DuplicateReports.unclaim_duplicate_report(conn.assigns.duplicate_report) + {:ok, report} = DuplicateReports.unclaim_duplicate_report(conn.assigns.duplicate_report) conn |> put_flash(:info, "Successfully released report.") + |> moderation_log(details: &log_details/3) |> redirect(to: Routes.duplicate_report_path(conn, :index)) end + + defp log_details(conn, action, _) do + body = + case action do + :create -> "Claimed a duplicate report" + :delete -> "Released a duplicate report" + end + + %{ + body: body, + subject_path: Routes.duplicate_report_path(conn, :index) + } + end end diff --git a/lib/philomena_web/controllers/duplicate_report/reject_controller.ex b/lib/philomena_web/controllers/duplicate_report/reject_controller.ex index 577e343b..e019fb2a 100644 --- a/lib/philomena_web/controllers/duplicate_report/reject_controller.ex +++ b/lib/philomena_web/controllers/duplicate_report/reject_controller.ex @@ -13,7 +13,7 @@ defmodule PhilomenaWeb.DuplicateReport.RejectController do preload: [:image, :duplicate_of_image] def create(conn, _params) do - {:ok, _report} = + {:ok, report} = DuplicateReports.reject_duplicate_report( conn.assigns.duplicate_report, conn.assigns.current_user @@ -21,6 +21,14 @@ defmodule PhilomenaWeb.DuplicateReport.RejectController do conn |> put_flash(:info, "Successfully rejected report.") + |> moderation_log(details: &log_details/3, data: report) |> redirect(to: Routes.duplicate_report_path(conn, :index)) end + + defp log_details(conn, _action, report) do + %{ + body: "Rejected duplicate report (#{report.image.id} -> #{report.duplicate_of_image.id})", + subject_path: Routes.duplicate_report_path(conn, :index) + } + end end diff --git a/lib/philomena_web/controllers/image/anonymous_controller.ex b/lib/philomena_web/controllers/image/anonymous_controller.ex index 57e4ea92..d74b1682 100644 --- a/lib/philomena_web/controllers/image/anonymous_controller.ex +++ b/lib/philomena_web/controllers/image/anonymous_controller.ex @@ -22,6 +22,7 @@ defmodule PhilomenaWeb.Image.AnonymousController do conn |> put_flash(:info, "Successfully updated anonymity.") + |> moderation_log(details: &log_details/3, data: image) |> redirect(to: Routes.image_path(conn, :show, image)) end @@ -31,4 +32,11 @@ defmodule PhilomenaWeb.Image.AnonymousController do _false -> PhilomenaWeb.NotAuthorizedPlug.call(conn) end end + + defp log_details(conn, _action, image) do + %{ + body: "Updated anonymity of image >>#{image.id}", + subject_path: Routes.image_path(conn, :show, image) + } + end end diff --git a/lib/philomena_web/controllers/image/comment/delete_controller.ex b/lib/philomena_web/controllers/image/comment/delete_controller.ex index 5cbf285d..4527e759 100644 --- a/lib/philomena_web/controllers/image/comment/delete_controller.ex +++ b/lib/philomena_web/controllers/image/comment/delete_controller.ex @@ -16,6 +16,7 @@ defmodule PhilomenaWeb.Image.Comment.DeleteController do conn |> put_flash(:info, "Comment successfully destroyed!") + |> moderation_log(details: &log_details/3, data: comment) |> redirect( to: Routes.image_path(conn, :show, comment.image_id) <> "#comment_#{comment.id}" ) @@ -28,4 +29,11 @@ defmodule PhilomenaWeb.Image.Comment.DeleteController do ) end end + + defp log_details(conn, _action, comment) do + %{ + body: "Destroyed comment on image >>#{comment.image_id}", + subject_path: Routes.image_path(conn, :show, comment.image_id) <> "#comment_#{comment.id}" + } + end end diff --git a/lib/philomena_web/controllers/image/comment/hide_controller.ex b/lib/philomena_web/controllers/image/comment/hide_controller.ex index 0bf32be0..1e55f86d 100644 --- a/lib/philomena_web/controllers/image/comment/hide_controller.ex +++ b/lib/philomena_web/controllers/image/comment/hide_controller.ex @@ -15,6 +15,7 @@ defmodule PhilomenaWeb.Image.Comment.HideController do {:ok, comment} -> conn |> put_flash(:info, "Comment successfully hidden!") + |> moderation_log(details: &log_details/3, data: comment) |> redirect( to: Routes.image_path(conn, :show, comment.image_id) <> "#comment_#{comment.id}" ) @@ -35,6 +36,7 @@ defmodule PhilomenaWeb.Image.Comment.HideController do {:ok, comment} -> conn |> put_flash(:info, "Comment successfully unhidden!") + |> moderation_log(details: &log_details/3, data: comment) |> redirect( to: Routes.image_path(conn, :show, comment.image_id) <> "#comment_#{comment.id}" ) @@ -47,4 +49,17 @@ defmodule PhilomenaWeb.Image.Comment.HideController do ) end end + + defp log_details(conn, action, comment) do + body = + case action do + :create -> "Hidden comment on image >>#{comment.image_id} (#{comment.deletion_reason})" + :delete -> "Restored comment on image >>#{comment.image_id}" + end + + %{ + body: body, + subject_path: Routes.image_path(conn, :show, comment.image_id) <> "#comment_#{comment.id}" + } + end end diff --git a/lib/philomena_web/controllers/image/comment_lock_controller.ex b/lib/philomena_web/controllers/image/comment_lock_controller.ex index fc47461c..e913bdb6 100644 --- a/lib/philomena_web/controllers/image/comment_lock_controller.ex +++ b/lib/philomena_web/controllers/image/comment_lock_controller.ex @@ -12,6 +12,7 @@ defmodule PhilomenaWeb.Image.CommentLockController do conn |> put_flash(:info, "Successfully locked comments.") + |> moderation_log(details: &log_details/3, data: image) |> redirect(to: Routes.image_path(conn, :show, image)) end @@ -20,6 +21,20 @@ defmodule PhilomenaWeb.Image.CommentLockController do conn |> put_flash(:info, "Successfully unlocked comments.") + |> moderation_log(details: &log_details/3, data: image) |> redirect(to: Routes.image_path(conn, :show, image)) end + + defp log_details(conn, action, image) do + body = + case action do + :create -> "Locked comments on image >>#{image.id}" + :delete -> "Unlocked comments on image >>#{image.id}" + end + + %{ + body: body, + subject_path: Routes.image_path(conn, :show, image) + } + end end diff --git a/lib/philomena_web/controllers/image/delete_controller.ex b/lib/philomena_web/controllers/image/delete_controller.ex index b3dde1bf..adae818c 100644 --- a/lib/philomena_web/controllers/image/delete_controller.ex +++ b/lib/philomena_web/controllers/image/delete_controller.ex @@ -16,9 +16,10 @@ defmodule PhilomenaWeb.Image.DeleteController do user = conn.assigns.current_user case Images.hide_image(image, user, image_params) do - {:ok, _image} -> + {:ok, result} -> conn |> put_flash(:info, "Image successfully hidden.") + |> moderation_log(details: &log_details/3, data: result.image) |> redirect(to: Routes.image_path(conn, :show, image)) _error -> @@ -35,6 +36,7 @@ defmodule PhilomenaWeb.Image.DeleteController do {:ok, image} -> conn |> put_flash(:info, "Hide reason updated.") + |> moderation_log(details: &log_details/3, data: image) |> redirect(to: Routes.image_path(conn, :show, image)) {:error, _changeset} -> @@ -60,10 +62,25 @@ defmodule PhilomenaWeb.Image.DeleteController do def delete(conn, _params) do image = conn.assigns.image - {:ok, _image} = Images.unhide_image(image) + {:ok, image} = Images.unhide_image(image) conn |> put_flash(:info, "Image successfully unhidden.") + |> moderation_log(details: &log_details/3, data: image) |> redirect(to: Routes.image_path(conn, :show, image)) end + + defp log_details(conn, action, image) do + body = + case action do + :create -> "Hidden image >>#{image.id} (#{image.deletion_reason})" + :update -> "Changed hide reason of >>#{image.id} (#{image.deletion_reason})" + :delete -> "Restored image >>#{image.id}" + end + + %{ + body: body, + subject_path: Routes.image_path(conn, :show, image) + } + end end diff --git a/lib/philomena_web/controllers/image/description_lock_controller.ex b/lib/philomena_web/controllers/image/description_lock_controller.ex index f509370c..ed8c9c87 100644 --- a/lib/philomena_web/controllers/image/description_lock_controller.ex +++ b/lib/philomena_web/controllers/image/description_lock_controller.ex @@ -12,6 +12,7 @@ defmodule PhilomenaWeb.Image.DescriptionLockController do conn |> put_flash(:info, "Successfully locked description.") + |> moderation_log(details: &log_details/3, data: image) |> redirect(to: Routes.image_path(conn, :show, image)) end @@ -20,6 +21,20 @@ defmodule PhilomenaWeb.Image.DescriptionLockController do conn |> put_flash(:info, "Successfully unlocked description.") + |> moderation_log(details: &log_details/3, data: image) |> redirect(to: Routes.image_path(conn, :show, image)) end + + defp log_details(conn, action, image) do + body = + case action do + :create -> "Locked description editing on image >>#{image.id}" + :delete -> "Unlocked description editing on image >>#{image.id}" + end + + %{ + body: body, + subject_path: Routes.image_path(conn, :show, image) + } + end end diff --git a/lib/philomena_web/controllers/image/destroy_controller.ex b/lib/philomena_web/controllers/image/destroy_controller.ex index 8caf631f..fe2f3e6e 100644 --- a/lib/philomena_web/controllers/image/destroy_controller.ex +++ b/lib/philomena_web/controllers/image/destroy_controller.ex @@ -15,6 +15,7 @@ defmodule PhilomenaWeb.Image.DestroyController do {:ok, image} -> conn |> put_flash(:info, "Image contents destroyed.") + |> moderation_log(details: &log_details/3, data: image) |> redirect(to: Routes.image_path(conn, :show, image)) _error -> @@ -36,4 +37,11 @@ defmodule PhilomenaWeb.Image.DestroyController do |> halt() end end + + defp log_details(conn, _action, image) do + %{ + body: "Hard-deleted image >>#{image.id}", + subject_path: Routes.image_path(conn, :show, image) + } + end end diff --git a/lib/philomena_web/controllers/image/feature_controller.ex b/lib/philomena_web/controllers/image/feature_controller.ex index 31ab6c7c..a16ee809 100644 --- a/lib/philomena_web/controllers/image/feature_controller.ex +++ b/lib/philomena_web/controllers/image/feature_controller.ex @@ -16,6 +16,7 @@ defmodule PhilomenaWeb.Image.FeatureController do conn |> put_flash(:info, "Image marked as featured image.") + |> moderation_log(details: &log_details/3, data: image) |> redirect(to: Routes.image_path(conn, :show, image)) end @@ -31,4 +32,11 @@ defmodule PhilomenaWeb.Image.FeatureController do conn end end + + defp log_details(conn, _action, image) do + %{ + body: "Featured image >>#{image.id}", + subject_path: Routes.image_path(conn, :show, image) + } + end end diff --git a/lib/philomena_web/controllers/image/hash_controller.ex b/lib/philomena_web/controllers/image/hash_controller.ex index 569ed12e..ad99b133 100644 --- a/lib/philomena_web/controllers/image/hash_controller.ex +++ b/lib/philomena_web/controllers/image/hash_controller.ex @@ -12,6 +12,14 @@ defmodule PhilomenaWeb.Image.HashController do conn |> put_flash(:info, "Successfully cleared hash.") + |> moderation_log(details: &log_details/3, data: image) |> redirect(to: Routes.image_path(conn, :show, image)) end + + defp log_details(conn, _action, image) do + %{ + body: "Cleared hash of image >>#{image.id}", + subject_path: Routes.image_path(conn, :show, image) + } + end end diff --git a/lib/philomena_web/controllers/image/repair_controller.ex b/lib/philomena_web/controllers/image/repair_controller.ex index 9e039eca..7e40835a 100644 --- a/lib/philomena_web/controllers/image/repair_controller.ex +++ b/lib/philomena_web/controllers/image/repair_controller.ex @@ -13,6 +13,14 @@ defmodule PhilomenaWeb.Image.RepairController do conn |> put_flash(:info, "Repair job enqueued.") + |> moderation_log(details: &log_details/3, data: conn.assigns.image) |> redirect(to: Routes.image_path(conn, :show, conn.assigns.image)) end + + defp log_details(conn, _action, image) do + %{ + body: "Repaired image >>#{image.id}", + subject_path: Routes.image_path(conn, :show, image) + } + end end diff --git a/lib/philomena_web/controllers/image/scratchpad_controller.ex b/lib/philomena_web/controllers/image/scratchpad_controller.ex index 80e7ce8b..863c0bc8 100644 --- a/lib/philomena_web/controllers/image/scratchpad_controller.ex +++ b/lib/philomena_web/controllers/image/scratchpad_controller.ex @@ -17,6 +17,14 @@ defmodule PhilomenaWeb.Image.ScratchpadController do conn |> put_flash(:info, "Successfully updated moderation notes.") + |> moderation_log(details: &log_details/3, data: image) |> redirect(to: Routes.image_path(conn, :show, image)) end + + defp log_details(conn, _action, image) do + %{ + body: "Updated mod notes on image >>#{image.id} (#{image.scratchpad})", + subject_path: Routes.image_path(conn, :show, image) + } + end end diff --git a/lib/philomena_web/controllers/image/source_history_controller.ex b/lib/philomena_web/controllers/image/source_history_controller.ex index 4a2ad5b7..24bab27f 100644 --- a/lib/philomena_web/controllers/image/source_history_controller.ex +++ b/lib/philomena_web/controllers/image/source_history_controller.ex @@ -14,6 +14,14 @@ defmodule PhilomenaWeb.Image.SourceHistoryController do conn |> put_flash(:info, "Successfully deleted source history.") + |> moderation_log(details: &log_details/3, data: image) |> redirect(to: Routes.image_path(conn, :show, image)) end + + defp log_details(conn, _action, image) do + %{ + body: "Deleted source history for image >>#{image.id}", + subject_path: Routes.image_path(conn, :show, image) + } + end end diff --git a/lib/philomena_web/controllers/image/tag_lock_controller.ex b/lib/philomena_web/controllers/image/tag_lock_controller.ex index 06017d38..2f71b4a3 100644 --- a/lib/philomena_web/controllers/image/tag_lock_controller.ex +++ b/lib/philomena_web/controllers/image/tag_lock_controller.ex @@ -23,6 +23,7 @@ defmodule PhilomenaWeb.Image.TagLockController do conn |> put_flash(:info, "Successfully updated list of locked tags.") + |> moderation_log(details: &log_details/3, data: image) |> redirect(to: Routes.image_path(conn, :show, image)) end @@ -31,6 +32,7 @@ defmodule PhilomenaWeb.Image.TagLockController do conn |> put_flash(:info, "Successfully locked tags.") + |> moderation_log(details: &log_details/3, data: image) |> redirect(to: Routes.image_path(conn, :show, image)) end @@ -39,6 +41,21 @@ defmodule PhilomenaWeb.Image.TagLockController do conn |> put_flash(:info, "Successfully unlocked tags.") + |> moderation_log(details: &log_details/3, data: image) |> redirect(to: Routes.image_path(conn, :show, image)) end + + defp log_details(conn, action, image) do + body = + case action do + :create -> "Locked tags on image >>#{image.id}" + :update -> "Updated list of locked tags on image >>#{image.id}" + :delete -> "Unlocked tags on image >>#{image.id}" + end + + %{ + body: body, + subject_path: Routes.image_path(conn, :show, image) + } + end end diff --git a/lib/philomena_web/controllers/image/tamper_controller.ex b/lib/philomena_web/controllers/image/tamper_controller.ex index 7a08809f..8eb66309 100644 --- a/lib/philomena_web/controllers/image/tamper_controller.ex +++ b/lib/philomena_web/controllers/image/tamper_controller.ex @@ -16,7 +16,7 @@ defmodule PhilomenaWeb.Image.TamperController do image = conn.assigns.image user = conn.assigns.user - {:ok, _result} = + {:ok, result} = ImageVotes.delete_vote_transaction(image, user) |> Repo.transaction() @@ -24,6 +24,26 @@ defmodule PhilomenaWeb.Image.TamperController do conn |> put_flash(:info, "Vote removed.") + |> moderation_log( + details: &log_details/3, + data: %{vote: result, image: image} + ) |> redirect(to: Routes.image_path(conn, :show, conn.assigns.image)) end + + defp log_details(conn, _action, data) do + image = data.image + + vote_type = + case data.vote do + %{undownvote: {1, _}} -> "downvote" + %{unupvote: {1, _}} -> "upvote" + _ -> "vote" + end + + %{ + body: "Deleted #{vote_type} by #{conn.assigns.user.name} on image >>#{data.image.id}", + subject_path: Routes.image_path(conn, :show, image) + } + end end diff --git a/lib/philomena_web/controllers/moderation_log_controller.ex b/lib/philomena_web/controllers/moderation_log_controller.ex new file mode 100644 index 00000000..3d9e7699 --- /dev/null +++ b/lib/philomena_web/controllers/moderation_log_controller.ex @@ -0,0 +1,15 @@ +defmodule PhilomenaWeb.ModerationLogController do + use PhilomenaWeb, :controller + + alias Philomena.ModerationLogs + alias Philomena.ModerationLogs.ModerationLog + + plug :load_and_authorize_resource, + model: ModerationLog, + preload: [:user] + + def index(conn, _params) do + moderation_logs = ModerationLogs.list_moderation_logs(conn) + render(conn, "index.html", title: "Moderation Logs", moderation_logs: moderation_logs) + end +end diff --git a/lib/philomena_web/controllers/tag/image_controller.ex b/lib/philomena_web/controllers/tag/image_controller.ex index a33633af..2f43ebe8 100644 --- a/lib/philomena_web/controllers/tag/image_controller.ex +++ b/lib/philomena_web/controllers/tag/image_controller.ex @@ -23,6 +23,7 @@ defmodule PhilomenaWeb.Tag.ImageController do {:ok, tag} -> conn |> put_flash(:info, "Tag image successfully updated.") + |> moderation_log(details: &log_details/3, data: tag) |> redirect(to: Routes.tag_path(conn, :show, tag)) {:error, :tag, changeset, _changes} -> @@ -31,10 +32,24 @@ defmodule PhilomenaWeb.Tag.ImageController do end def delete(conn, _params) do - {:ok, _tag} = Tags.remove_tag_image(conn.assigns.tag) + {:ok, tag} = Tags.remove_tag_image(conn.assigns.tag) conn |> put_flash(:info, "Tag image successfully removed.") + |> moderation_log(details: &log_details/3, data: tag) |> redirect(to: Routes.tag_path(conn, :show, conn.assigns.tag)) end + + defp log_details(conn, action, tag) do + body = + case action do + :update -> "Updated image on tag '#{tag.name}'" + :delete -> "Removed image on tag '#{tag.name}'" + end + + %{ + body: body, + subject_path: Routes.tag_path(conn, :show, tag) + } + end end diff --git a/lib/philomena_web/controllers/tag_controller.ex b/lib/philomena_web/controllers/tag_controller.ex index ce5a9b36..766508ae 100644 --- a/lib/philomena_web/controllers/tag_controller.ex +++ b/lib/philomena_web/controllers/tag_controller.ex @@ -97,6 +97,7 @@ defmodule PhilomenaWeb.TagController do {:ok, tag} -> conn |> put_flash(:info, "Tag successfully updated.") + |> moderation_log(details: &log_details/3, data: tag) |> redirect(to: Routes.tag_path(conn, :show, tag)) {:error, changeset} -> @@ -105,10 +106,11 @@ defmodule PhilomenaWeb.TagController do end def delete(conn, _params) do - {:ok, _tag} = Tags.delete_tag(conn.assigns.tag) + {:ok, tag} = Tags.delete_tag(conn.assigns.tag) conn |> put_flash(:info, "Tag queued for deletion.") + |> moderation_log(details: &log_details/3, data: tag) |> redirect(to: "/") end @@ -169,4 +171,17 @@ defmodule PhilomenaWeb.TagController do |> halt() end end + + defp log_details(conn, action, tag) do + body = + case action do + :update -> "Updated details on tag '#{tag.name}'" + :delete -> "Deleted tag '#{tag.name}'" + end + + %{ + body: body, + subject_path: Routes.tag_path(conn, :show, tag) + } + end end diff --git a/lib/philomena_web/controllers/topic/hide_controller.ex b/lib/philomena_web/controllers/topic/hide_controller.ex index eae39ed3..3149501d 100644 --- a/lib/philomena_web/controllers/topic/hide_controller.ex +++ b/lib/philomena_web/controllers/topic/hide_controller.ex @@ -27,6 +27,7 @@ defmodule PhilomenaWeb.Topic.HideController do {:ok, topic} -> conn |> put_flash(:info, "Topic successfully hidden!") + |> moderation_log(details: &log_details/3, data: topic) |> redirect(to: Routes.forum_topic_path(conn, :show, topic.forum, topic)) {:error, _changeset} -> @@ -43,6 +44,7 @@ defmodule PhilomenaWeb.Topic.HideController do {:ok, topic} -> conn |> put_flash(:info, "Topic successfully restored!") + |> moderation_log(details: &log_details/3, data: topic) |> redirect(to: Routes.forum_topic_path(conn, :show, topic.forum, topic)) {:error, _changeset} -> @@ -51,4 +53,20 @@ defmodule PhilomenaWeb.Topic.HideController do |> redirect(to: Routes.forum_topic_path(conn, :show, topic.forum, topic)) end end + + defp log_details(conn, action, topic) do + body = + case action do + :create -> + "Hidden topic '#{topic.title}' (#{topic.deletion_reason}) in #{topic.forum.name}" + + :delete -> + "Restored topic '#{topic.title}' in #{topic.forum.name}" + end + + %{ + body: body, + subject_path: Routes.forum_topic_path(conn, :show, topic.forum, topic) + } + end end diff --git a/lib/philomena_web/controllers/topic/lock_controller.ex b/lib/philomena_web/controllers/topic/lock_controller.ex index 8a96066b..88bf115b 100644 --- a/lib/philomena_web/controllers/topic/lock_controller.ex +++ b/lib/philomena_web/controllers/topic/lock_controller.ex @@ -26,6 +26,7 @@ defmodule PhilomenaWeb.Topic.LockController do {:ok, topic} -> conn |> put_flash(:info, "Topic successfully locked!") + |> moderation_log(details: &log_details/3, data: topic) |> redirect(to: Routes.forum_topic_path(conn, :show, topic.forum, topic)) {:error, _changeset} -> @@ -42,6 +43,7 @@ defmodule PhilomenaWeb.Topic.LockController do {:ok, topic} -> conn |> put_flash(:info, "Topic successfully unlocked!") + |> moderation_log(details: &log_details/3, data: topic) |> redirect(to: Routes.forum_topic_path(conn, :show, topic.forum, topic)) {:error, _changeset} -> @@ -50,4 +52,17 @@ defmodule PhilomenaWeb.Topic.LockController do |> redirect(to: Routes.forum_topic_path(conn, :show, topic.forum, topic)) end end + + defp log_details(conn, action, topic) do + body = + case action do + :create -> "Locked topic '#{topic.title}' (#{topic.lock_reason}) in #{topic.forum.name}" + :delete -> "Unlocked topic '#{topic.title}' in #{topic.forum.name}" + end + + %{ + body: body, + subject_path: Routes.forum_topic_path(conn, :show, topic.forum, topic) + } + end end diff --git a/lib/philomena_web/controllers/topic/move_controller.ex b/lib/philomena_web/controllers/topic/move_controller.ex index 50ded78a..f12af72e 100644 --- a/lib/philomena_web/controllers/topic/move_controller.ex +++ b/lib/philomena_web/controllers/topic/move_controller.ex @@ -29,6 +29,7 @@ defmodule PhilomenaWeb.Topic.MoveController do conn |> put_flash(:info, "Topic successfully moved!") + |> moderation_log(details: &log_details/3, data: topic) |> redirect(to: Routes.forum_topic_path(conn, :show, topic.forum, topic)) {:error, _changeset} -> @@ -37,4 +38,11 @@ defmodule PhilomenaWeb.Topic.MoveController do |> redirect(to: Routes.forum_topic_path(conn, :show, conn.assigns.forum, topic)) end end + + defp log_details(conn, _action, topic) do + %{ + body: "Topic '#{topic.title}' moved to #{topic.forum.name}", + subject_path: Routes.forum_topic_path(conn, :show, topic.forum, topic) + } + end end diff --git a/lib/philomena_web/controllers/topic/post/delete_controller.ex b/lib/philomena_web/controllers/topic/post/delete_controller.ex index 2bcc009b..9e6d7a45 100644 --- a/lib/philomena_web/controllers/topic/post/delete_controller.ex +++ b/lib/philomena_web/controllers/topic/post/delete_controller.ex @@ -19,6 +19,7 @@ defmodule PhilomenaWeb.Topic.Post.DeleteController do {:ok, post} -> conn |> put_flash(:info, "Post successfully destroyed!") + |> moderation_log(details: &log_details/3, data: post) |> redirect( to: Routes.forum_topic_path(conn, :show, post.topic.forum, post.topic, post_id: post.id) <> @@ -35,4 +36,13 @@ defmodule PhilomenaWeb.Topic.Post.DeleteController do ) end end + + defp log_details(conn, _action, post) do + %{ + body: "Destroyed forum post ##{post.id} in topic '#{post.topic.title}'", + subject_path: + Routes.forum_topic_path(conn, :show, post.topic.forum, post.topic, post_id: post.id) <> + "#post_#{post.id}" + } + end end diff --git a/lib/philomena_web/controllers/topic/post/hide_controller.ex b/lib/philomena_web/controllers/topic/post/hide_controller.ex index 68129468..9e449977 100644 --- a/lib/philomena_web/controllers/topic/post/hide_controller.ex +++ b/lib/philomena_web/controllers/topic/post/hide_controller.ex @@ -20,6 +20,7 @@ defmodule PhilomenaWeb.Topic.Post.HideController do {:ok, post} -> conn |> put_flash(:info, "Post successfully hidden.") + |> moderation_log(details: &log_details/3, data: post) |> redirect( to: Routes.forum_topic_path(conn, :show, post.topic.forum, post.topic, post_id: post.id) <> @@ -44,6 +45,7 @@ defmodule PhilomenaWeb.Topic.Post.HideController do {:ok, post} -> conn |> put_flash(:info, "Post successfully unhidden.") + |> moderation_log(details: &log_details/3, data: post) |> redirect( to: Routes.forum_topic_path(conn, :show, post.topic.forum, post.topic, post_id: post.id) <> @@ -60,4 +62,22 @@ defmodule PhilomenaWeb.Topic.Post.HideController do ) end end + + defp log_details(conn, action, post) do + body = + case action do + :create -> + "Hidden forum post ##{post.id} in topic '#{post.topic.title}' (#{post.deletion_reason})" + + :delete -> + "Restored forum post ##{post.id} in topic '#{post.topic.title}'" + end + + %{ + body: body, + subject_path: + Routes.forum_topic_path(conn, :show, post.topic.forum, post.topic, post_id: post.id) <> + "#post_#{post.id}" + } + end end diff --git a/lib/philomena_web/controllers/topic/stick_controller.ex b/lib/philomena_web/controllers/topic/stick_controller.ex index 8739bac0..27f92168 100644 --- a/lib/philomena_web/controllers/topic/stick_controller.ex +++ b/lib/philomena_web/controllers/topic/stick_controller.ex @@ -25,6 +25,7 @@ defmodule PhilomenaWeb.Topic.StickController do {:ok, topic} -> conn |> put_flash(:info, "Topic successfully stickied!") + |> moderation_log(details: &log_details/3, data: topic) |> redirect(to: Routes.forum_topic_path(conn, :show, topic.forum, topic)) {:error, _changeset} -> @@ -41,6 +42,7 @@ defmodule PhilomenaWeb.Topic.StickController do {:ok, topic} -> conn |> put_flash(:info, "Topic successfully unstickied!") + |> moderation_log(details: &log_details/3, data: topic) |> redirect(to: Routes.forum_topic_path(conn, :show, topic.forum, topic)) {:error, _changeset} -> @@ -49,4 +51,17 @@ defmodule PhilomenaWeb.Topic.StickController do |> redirect(to: Routes.forum_topic_path(conn, :show, topic.forum, topic)) end end + + defp log_details(conn, action, topic) do + body = + case action do + :create -> "Stickied topic '#{topic.title}' in #{topic.forum.name}" + :delete -> "Unstickied topic '#{topic.title}' in #{topic.forum.name}" + end + + %{ + body: body, + subject_path: Routes.forum_topic_path(conn, :show, topic.forum, topic) + } + end end diff --git a/lib/philomena_web/plugs/moderation_log_plug.ex b/lib/philomena_web/plugs/moderation_log_plug.ex new file mode 100644 index 00000000..4ee75938 --- /dev/null +++ b/lib/philomena_web/plugs/moderation_log_plug.ex @@ -0,0 +1,46 @@ +defmodule PhilomenaWeb.ModerationLogPlug do + @moduledoc """ + This plug writes moderation logs. + ## Example + + plug PhilomenaWeb.ModerationLogPlug, [details: &log_details/2] + """ + + @controller_regex ~r/PhilomenaWeb\.([\w\.]+)Controller\z/ + + alias Plug.Conn + alias Phoenix.Controller + alias Philomena.ModerationLogs + + @doc false + @spec init(any()) :: any() + def init(opts), do: opts + + @type log_details :: %{subject_path: String.t(), body: String.t()} + @type details_func :: (Plug.Conn.t(), atom(), any() -> log_details()) + @type call_opts :: [details: details_func, data: any()] + + @doc false + @spec call(Conn.t(), call_opts) :: Conn.t() + def call(conn, opts) do + details_func = Keyword.fetch!(opts, :details) + userdata = Keyword.get(opts, :data, nil) + + user = conn.assigns.current_user + action = Controller.action_name(conn) + + %{subject_path: subject_path, body: body} = details_func.(conn, action, userdata) + + mod = Controller.controller_module(conn) + [mod_name] = Regex.run(@controller_regex, to_string(mod), capture: :all_but_first) + type = "#{mod_name}:#{action}" + + ModerationLogs.create_moderation_log(user, type, subject_path, body) + + conn + end + + @doc false + @spec moderation_log(Conn.t(), call_opts()) :: Conn.t() + def moderation_log(conn, opts), do: call(conn, opts) +end diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index e0606dc8..36cf7efb 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -327,6 +327,8 @@ defmodule PhilomenaWeb.Router do resources "/source_changes", FingerprintProfile.SourceChangeController, only: [:index] end + resources "/moderation_logs", ModerationLogController, only: [:index] + scope "/admin", Admin, as: :admin do resources "/reports", ReportController, only: [:index, :show] do resources "/claim", Report.ClaimController, only: [:create, :delete], singleton: true diff --git a/lib/philomena_web/templates/layout/_header_staff_links.html.slime b/lib/philomena_web/templates/layout/_header_staff_links.html.slime index ca6b65ad..d59305b5 100644 --- a/lib/philomena_web/templates/layout/_header_staff_links.html.slime +++ b/lib/philomena_web/templates/layout/_header_staff_links.html.slime @@ -38,9 +38,14 @@ = if manages_mod_notes?(@conn) do = link to: Routes.admin_mod_note_path(@conn, :index), class: "header__link" do - i.fa.fa-fw.fa-sticky-note> + i.fa.fa-fw.fa-clipboard> ' Mod Notes + = if can_see_moderation_log?(@conn) do + = link to: Routes.moderation_log_path(@conn, :index), class: "header__link" do + i.fa.fa-fw.fa-list-alt> + ' Mod Logs + = if @duplicate_report_count do = link to: Routes.duplicate_report_path(@conn, :index), class: "header__link", title: "Duplicates" do ' D diff --git a/lib/philomena_web/templates/moderation_log/index.html.slime b/lib/philomena_web/templates/moderation_log/index.html.slime new file mode 100644 index 00000000..a1927305 --- /dev/null +++ b/lib/philomena_web/templates/moderation_log/index.html.slime @@ -0,0 +1,33 @@ +elixir: + route = fn p -> Routes.moderation_log_path(@conn, :index, p) end + pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @moderation_logs, route: route, conn: @conn + +h1 Listing Moderation Logs + +block + .block__header + .page__pagination = pagination + +table + thead + tr + th Moderator + th Type + th Body + th Creation time + th Actions + tbody + = for log <- @moderation_logs do + tr + td = render PhilomenaWeb.UserAttributionView, "_user.html", object: %{user: log.user}, conn: @conn + td = log.type + td = log.body + td = pretty_time(log.created_at) + td + = link to: log.subject_path do + i.fa.fa-eye> + ' View subject + +block + .block__header + .page__pagination = pagination diff --git a/lib/philomena_web/views/layout_view.ex b/lib/philomena_web/views/layout_view.ex index f53db5d5..c7d600c1 100644 --- a/lib/philomena_web/views/layout_view.ex +++ b/lib/philomena_web/views/layout_view.ex @@ -117,6 +117,9 @@ defmodule PhilomenaWeb.LayoutView do def manages_bans?(conn), do: can?(conn, :create, Philomena.Bans.User) + def can_see_moderation_log?(conn), + do: can?(conn, :index, Philomena.ModerationLogs.ModerationLog) + def viewport_meta_tag(conn) do ua = get_user_agent(conn) diff --git a/lib/philomena_web/views/moderation_log_view.ex b/lib/philomena_web/views/moderation_log_view.ex new file mode 100644 index 00000000..a3e29786 --- /dev/null +++ b/lib/philomena_web/views/moderation_log_view.ex @@ -0,0 +1,3 @@ +defmodule PhilomenaWeb.ModerationLogView do + use PhilomenaWeb, :view +end diff --git a/priv/repo/migrations/20211107130226_create_moderation_logs.exs b/priv/repo/migrations/20211107130226_create_moderation_logs.exs new file mode 100644 index 00000000..5e33115e --- /dev/null +++ b/priv/repo/migrations/20211107130226_create_moderation_logs.exs @@ -0,0 +1,20 @@ +defmodule Philomena.Repo.Migrations.CreateModerationLogs do + use Ecto.Migration + + def change do + create table(:moderation_logs) do + add :user_id, references(:users, on_delete: :delete_all), null: false + add :body, :varchar, null: false + add :subject_path, :varchar, null: false + add :type, :varchar, null: false + + timestamps(inserted_at: :created_at, updated_at: false, type: :utc_datetime) + end + + create index(:moderation_logs, [:user_id]) + create index(:moderation_logs, [:type]) + create index(:moderation_logs, [:created_at]) + create index(:moderation_logs, [:user_id, :created_at]) + create index(:moderation_logs, [:type, :created_at]) + end +end diff --git a/priv/repo/structure.sql b/priv/repo/structure.sql index d80f8bca..816fed60 100644 --- a/priv/repo/structure.sql +++ b/priv/repo/structure.sql @@ -1054,6 +1054,39 @@ CREATE SEQUENCE public.mod_notes_id_seq ALTER SEQUENCE public.mod_notes_id_seq OWNED BY public.mod_notes.id; +-- +-- Name: moderation_logs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.moderation_logs ( + id bigint NOT NULL, + user_id bigint NOT NULL, + body character varying NOT NULL, + subject_path character varying NOT NULL, + type character varying NOT NULL, + created_at timestamp(0) without time zone NOT NULL +); + + +-- +-- Name: moderation_logs_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.moderation_logs_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: moderation_logs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.moderation_logs_id_seq OWNED BY public.moderation_logs.id; + + -- -- Name: notifications; Type: TABLE; Schema: public; Owner: - -- @@ -2244,6 +2277,13 @@ ALTER TABLE ONLY public.messages ALTER COLUMN id SET DEFAULT nextval('public.mes ALTER TABLE ONLY public.mod_notes ALTER COLUMN id SET DEFAULT nextval('public.mod_notes_id_seq'::regclass); +-- +-- Name: moderation_logs id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.moderation_logs ALTER COLUMN id SET DEFAULT nextval('public.moderation_logs_id_seq'::regclass); + + -- -- Name: notifications id; Type: DEFAULT; Schema: public; Owner: - -- @@ -2603,6 +2643,14 @@ ALTER TABLE ONLY public.mod_notes ADD CONSTRAINT mod_notes_pkey PRIMARY KEY (id); +-- +-- Name: moderation_logs moderation_logs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.moderation_logs + ADD CONSTRAINT moderation_logs_pkey PRIMARY KEY (id); + + -- -- Name: notifications notifications_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -4001,6 +4049,41 @@ CREATE INDEX index_vpns_on_ip ON public.vpns USING gist (ip inet_ops); CREATE INDEX intensities_index ON public.images USING btree (se_intensity, sw_intensity, ne_intensity, nw_intensity, average_intensity); +-- +-- Name: moderation_logs_created_at_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX moderation_logs_created_at_index ON public.moderation_logs USING btree (created_at); + + +-- +-- Name: moderation_logs_type_created_at_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX moderation_logs_type_created_at_index ON public.moderation_logs USING btree (type, created_at); + + +-- +-- Name: moderation_logs_type_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX moderation_logs_type_index ON public.moderation_logs USING btree (type); + + +-- +-- Name: moderation_logs_user_id_created_at_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX moderation_logs_user_id_created_at_index ON public.moderation_logs USING btree (user_id, created_at); + + +-- +-- Name: moderation_logs_user_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX moderation_logs_user_id_index ON public.moderation_logs USING btree (user_id); + + -- -- Name: user_tokens_context_token_index; Type: INDEX; Schema: public; Owner: - -- @@ -4831,6 +4914,14 @@ ALTER TABLE ONLY public.image_tag_locks ADD CONSTRAINT image_tag_locks_tag_id_fkey FOREIGN KEY (tag_id) REFERENCES public.tags(id) ON DELETE CASCADE; +-- +-- Name: moderation_logs moderation_logs_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.moderation_logs + ADD CONSTRAINT moderation_logs_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + -- -- Name: user_tokens user_tokens_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4867,3 +4958,4 @@ INSERT INTO public."schema_migrations" (version) VALUES (20210912171343); INSERT INTO public."schema_migrations" (version) VALUES (20210917190346); INSERT INTO public."schema_migrations" (version) VALUES (20210921025336); INSERT INTO public."schema_migrations" (version) VALUES (20210929181319); +INSERT INTO public."schema_migrations" (version) VALUES (20211107130226); diff --git a/test/philomena/moderation_logs_test.exs b/test/philomena/moderation_logs_test.exs new file mode 100644 index 00000000..f8ae2966 --- /dev/null +++ b/test/philomena/moderation_logs_test.exs @@ -0,0 +1,30 @@ +defmodule Philomena.ModerationLogsTest do + use Philomena.DataCase + + alias Philomena.ModerationLogs + + describe "moderation_logs" do + alias Philomena.ModerationLogs.ModerationLog + + import Philomena.UsersFixtures + + test "create_moderation_log/4 with valid data creates a moderation_log" do + user = user_fixture() + + assert {:ok, %ModerationLog{} = _moderation_log} = + ModerationLogs.create_moderation_log( + user, + "User:update", + "/path/to/subject", + "Updated user" + ) + end + + test "create_moderation_log/4 with invalid data returns error changeset" do + user = user_fixture() + + assert {:error, %Ecto.Changeset{}} = + ModerationLogs.create_moderation_log(user, nil, nil, nil) + end + end +end