diff --git a/assets/css/common/_base.scss b/assets/css/common/_base.scss index f0b3a085..b5375e07 100644 --- a/assets/css/common/_base.scss +++ b/assets/css/common/_base.scss @@ -470,6 +470,7 @@ span.stat { @import "text"; @import "~views/adverts"; +@import "~views/approval"; @import "~views/badges"; @import "~views/channels"; @import "~views/comments"; diff --git a/assets/css/views/_approval.scss b/assets/css/views/_approval.scss new file mode 100644 index 00000000..97348d38 --- /dev/null +++ b/assets/css/views/_approval.scss @@ -0,0 +1,49 @@ +.approval-grid { + display: grid; + grid-template-columns: 1fr auto; + grid-template-rows: 1fr; + gap: var(--padding-small); + text-align: center; +} + +.approval-items--main { + display: grid; + grid-template-columns: auto 1fr 1fr 1fr; + gap: var(--padding-small); + justify-content: center; +} + +.approval-items--main span { + margin: auto; +} + +.approval-items--footer { + display: grid; + grid-template-columns: auto 1fr; + grid-template-rows: 1fr; + gap: var(--padding-normal); +} + +.approval-items--footer * { + height: 2rem; + margin-top: auto; + margin-bottom: auto; +} + +@media (max-width: $min_px_width_for_desktop_layout) { + .approval-grid { + grid-template-columns: 1fr; + grid-template-rows: auto; + grid-template-areas: + "main" + "footer"; + } + + .approval-items--main { + grid-area: main; + } + + .approval-items--footer { + grid-area: footer; + } +} diff --git a/assets/js/pmwarning.js b/assets/js/pmwarning.js new file mode 100644 index 00000000..0808b089 --- /dev/null +++ b/assets/js/pmwarning.js @@ -0,0 +1,26 @@ +/** + * PmWarning + * + * Warn users that their PM will be reviewed. + */ + +import { $ } from './utils/dom'; + +function warnAboutPMs() { + const textarea = $('.js-toolbar-input'); + const warning = $('.js-hidden-warning'); + const imageEmbedRegex = /!+\[/g; + + if (!warning || !textarea) return; + + textarea.addEventListener('input', () => { + const value = textarea.value; + + if (value.match(imageEmbedRegex)) + warning.classList.remove('hidden'); + else if (!warning.classList.contains('hidden')) + warning.classList.add('hidden'); + }); +} + +export { warnAboutPMs }; diff --git a/assets/js/when-ready.js b/assets/js/when-ready.js index 87e99737..eed3082a 100644 --- a/assets/js/when-ready.js +++ b/assets/js/when-ready.js @@ -34,6 +34,7 @@ import { setupSearch } from './search'; import { setupToolbar } from './markdowntoolbar'; import { hideStaffTools } from './staffhider'; import { pollOptionCreator } from './poll'; +import { warnAboutPMs } from './pmwarning'; whenReady(() => { @@ -66,5 +67,6 @@ whenReady(() => { setupToolbar(); hideStaffTools(); pollOptionCreator(); + warnAboutPMs(); }); diff --git a/lib/philomena/comments.ex b/lib/philomena/comments.ex index 62e7c260..b353a181 100644 --- a/lib/philomena/comments.ex +++ b/lib/philomena/comments.ex @@ -9,6 +9,7 @@ defmodule Philomena.Comments do alias Philomena.Elasticsearch alias Philomena.Reports.Report + alias Philomena.UserStatistics alias Philomena.Comments.Comment alias Philomena.Comments.ElasticsearchIndex, as: CommentIndex alias Philomena.IndexWorker @@ -212,6 +213,8 @@ defmodule Philomena.Comments do |> Repo.transaction() |> case do {:ok, %{comment: comment, reports: {_count, reports}}} -> + notify_comment(comment) + UserStatistics.inc_stat(comment.user, :comments_posted) Reports.reindex_reports(reports) reindex_comment(comment) @@ -222,6 +225,8 @@ defmodule Philomena.Comments do end end + def report_non_approved(%Comment{approved: true}), do: false + def report_non_approved(comment) do Reports.create_system_report( comment.id, diff --git a/lib/philomena/conversations.ex b/lib/philomena/conversations.ex index 5542c021..757c295b 100644 --- a/lib/philomena/conversations.ex +++ b/lib/philomena/conversations.ex @@ -6,7 +6,7 @@ defmodule Philomena.Conversations do import Ecto.Query, warn: false alias Ecto.Multi alias Philomena.Repo - + alias Philomena.Reports alias Philomena.Conversations.Conversation @doc """ @@ -187,6 +187,12 @@ defmodule Philomena.Conversations do Ecto.build_assoc(conversation, :messages) |> Message.creation_changeset(attrs, user) + show_as_read = + case message do + %{changes: %{approved: true}} -> false + _ -> true + end + conversation_query = Conversation |> where(id: ^conversation.id) @@ -196,11 +202,43 @@ defmodule Philomena.Conversations do Multi.new() |> Multi.insert(:message, message) |> Multi.update_all(:conversation, conversation_query, - set: [from_read: false, to_read: false, last_message_at: now] + set: [from_read: show_as_read, to_read: show_as_read, last_message_at: now] ) |> Repo.transaction() end + def approve_conversation_message(message) do + message_query = + message + |> Message.approve_changeset() + + conversation_query = + Conversation + |> where(id: ^message.conversation_id) + + now = DateTime.utc_now() + + Multi.new() + |> Multi.update(:message, message_query) + |> Multi.update_all(:conversation, conversation_query, set: [to_read: false]) + |> Repo.transaction() + end + + def report_non_approved(id) do + Reports.create_system_report( + id, + "Conversation", + "Approval", + "PM contains externally-embedded images and has been flagged for review." + ) + end + + def set_as_read(conversation) do + conversation + |> Conversation.to_read_changeset() + |> Repo.update() + end + @doc """ Updates a message. diff --git a/lib/philomena/conversations/conversation.ex b/lib/philomena/conversations/conversation.ex index 10b2540b..07e3f4ab 100644 --- a/lib/philomena/conversations/conversation.ex +++ b/lib/philomena/conversations/conversation.ex @@ -37,6 +37,11 @@ defmodule Philomena.Conversations.Conversation do |> cast(attrs, [:from_read, :to_read]) end + def to_read_changeset(conversation) do + change(conversation) + |> put_change(:to_read, true) + end + def hidden_changeset(conversation, attrs) do conversation |> cast(attrs, [:from_hidden, :to_hidden]) diff --git a/lib/philomena/conversations/message.ex b/lib/philomena/conversations/message.ex index 56188f0f..a9e6fefd 100644 --- a/lib/philomena/conversations/message.ex +++ b/lib/philomena/conversations/message.ex @@ -11,6 +11,7 @@ defmodule Philomena.Conversations.Message do belongs_to :from, User field :body, :string + field :approved, :boolean, default: false timestamps(inserted_at: :created_at, type: :utc_datetime) end @@ -31,4 +32,8 @@ defmodule Philomena.Conversations.Message do |> validate_length(:body, max: 300_000, count: :bytes) |> Approval.maybe_put_approval(user) end + + def approve_changeset(message) do + change(message, approved: true) + end end diff --git a/lib/philomena/images.ex b/lib/philomena/images.ex index c5fd4d79..a0e90b6c 100644 --- a/lib/philomena/images.ex +++ b/lib/philomena/images.ex @@ -115,7 +115,7 @@ defmodule Philomena.Images do repair_image(image) reindex_image(image) Tags.reindex_tags(image.added_tags) - UserStatistics.inc_stat(attribution[:user], :uploads) + maybe_approve_image(image, attribution[:user]) result @@ -135,6 +135,55 @@ defmodule Philomena.Images do multi end + def approve_image(image) do + image + |> Repo.preload(:user) + |> Image.approve_changeset() + |> Repo.update() + |> case do + {:ok, image} -> + reindex_image(image) + increment_user_stats(image.user) + maybe_suggest_user_verification(image.user) + + {:ok, image} + + error -> + error + end + end + + defp maybe_approve_image(image, nil), do: false + + defp maybe_approve_image(image, %User{verified: false, role: role}) when role == "user", + do: false + + defp maybe_approve_image(image, _user), do: approve_image(image) + + defp increment_user_stats(nil), do: false + + defp increment_user_stats(%User{} = user) do + UserStatistics.inc_stat(user, :uploads) + end + + defp maybe_suggest_user_verification(%User{id: id, uploads_count: 5, verified: false}) do + Reports.create_system_report( + id, + "User", + "Verification", + "User has uploaded enough approved images to be considered for verification." + ) + end + + defp maybe_suggest_user_verification(_user), do: false + + def count_pending_approvals() do + Image + |> where(hidden_from_users: false) + |> where(approved: false) + |> Repo.aggregate(:count) + end + def feature_image(featurer, %Image{} = image) do %ImageFeature{user_id: featurer.id, image_id: image.id} |> ImageFeature.changeset(%{}) diff --git a/lib/philomena/images/image.ex b/lib/philomena/images/image.ex index 8e27d4f5..34eeb64b 100644 --- a/lib/philomena/images/image.ex +++ b/lib/philomena/images/image.ex @@ -326,6 +326,12 @@ defmodule Philomena.Images.Image do cast(image, attrs, [:anonymous]) end + def approve_changeset(image) do + change(image) + |> put_change(:approved, true) + |> put_change(:first_seen_at, DateTime.truncate(DateTime.utc_now(), :second)) + end + def cache_changeset(image) do changeset = change(image) image = apply_changes(changeset) diff --git a/lib/philomena/posts.ex b/lib/philomena/posts.ex index 7e41b473..6bef4d1b 100644 --- a/lib/philomena/posts.ex +++ b/lib/philomena/posts.ex @@ -10,6 +10,7 @@ defmodule Philomena.Posts do alias Philomena.Elasticsearch alias Philomena.Topics.Topic alias Philomena.Topics + alias Philomena.UserStatistics alias Philomena.Posts.Post alias Philomena.Posts.ElasticsearchIndex, as: PostIndex alias Philomena.IndexWorker @@ -117,6 +118,8 @@ defmodule Philomena.Posts do Exq.enqueue(Exq, "notifications", NotificationWorker, ["Posts", post.id]) end + def report_non_approved(%Post{approved: true}), do: false + def report_non_approved(post) do Reports.create_system_report( post.id, @@ -261,6 +264,8 @@ defmodule Philomena.Posts do |> Repo.transaction() |> case do {:ok, %{post: post, reports: {_count, reports}}} -> + notify_post(post) + UserStatistics.inc_stat(post.user, :forum_posts) Reports.reindex_reports(reports) reindex_post(post) diff --git a/lib/philomena/reports/elasticsearch_index.ex b/lib/philomena/reports/elasticsearch_index.ex index 31f252e6..bafcf673 100644 --- a/lib/philomena/reports/elasticsearch_index.ex +++ b/lib/philomena/reports/elasticsearch_index.ex @@ -31,7 +31,8 @@ defmodule Philomena.Reports.ElasticsearchIndex do reportable_type: %{type: "keyword"}, reportable_id: %{type: "keyword"}, open: %{type: "boolean"}, - reason: %{type: "text", analyzer: "snowball"} + reason: %{type: "text", analyzer: "snowball"}, + system: %{type: "boolean"} } } } @@ -53,7 +54,8 @@ defmodule Philomena.Reports.ElasticsearchIndex do reportable_id: report.reportable_id, fingerprint: report.fingerprint, open: report.open, - reason: report.reason + reason: report.reason, + system: report.system } end diff --git a/lib/philomena/topics.ex b/lib/philomena/topics.ex index c4a02bdc..8adeb927 100644 --- a/lib/philomena/topics.ex +++ b/lib/philomena/topics.ex @@ -75,6 +75,7 @@ defmodule Philomena.Topics do |> case do {:ok, %{topic: topic}} = result -> Posts.reindex_post(hd(topic.posts)) + Posts.report_non_approved(hd(topic.posts)) result diff --git a/lib/philomena/topics/topic.ex b/lib/philomena/topics/topic.ex index 43b54002..efc49ce4 100644 --- a/lib/philomena/topics/topic.ex +++ b/lib/philomena/topics/topic.ex @@ -31,7 +31,6 @@ defmodule Philomena.Topics.Topic do field :slug, :string field :anonymous, :boolean, default: false field :hidden_from_users, :boolean, default: false - field :approved, :boolean timestamps(inserted_at: :created_at, type: :utc_datetime) end diff --git a/lib/philomena/users/ability.ex b/lib/philomena/users/ability.ex index cca29a3e..21b8ba1a 100644 --- a/lib/philomena/users/ability.ex +++ b/lib/philomena/users/ability.ex @@ -273,9 +273,6 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do def can?(%User{role: "assistant", role_map: %{"Topic" => "moderator"}}, :hide, %Topic{}), do: true - def can?(%User{role: "assistant", role_map: %{"Topic" => "moderator"}}, :approve, %Topic{}), - do: true - def can?(%User{role: "assistant", role_map: %{"Topic" => "moderator"}}, :show, %Post{}), do: true diff --git a/lib/philomena_web/controllers/admin/approval_controller.ex b/lib/philomena_web/controllers/admin/approval_controller.ex new file mode 100644 index 00000000..f5f8f2e9 --- /dev/null +++ b/lib/philomena_web/controllers/admin/approval_controller.ex @@ -0,0 +1,28 @@ +defmodule PhilomenaWeb.Admin.ApprovalController do + use PhilomenaWeb, :controller + + alias Philomena.Images.Image + alias Philomena.Repo + import Ecto.Query + + plug :verify_authorized + + def index(conn, _params) do + images = + Image + |> where(hidden_from_users: false) + |> where(approved: false) + |> order_by(desc: :id) + |> preload([:user, tags: [:aliases, :aliased_tag]]) + |> Repo.paginate(conn.assigns.scrivener) + + render(conn, "index.html", title: "Admin - Approval Queue", images: images) + end + + defp verify_authorized(conn, _opts) do + case Canada.Can.can?(conn.assigns.current_user, :approve, %Image{}) do + true -> conn + false -> PhilomenaWeb.NotAuthorizedPlug.call(conn) + end + end +end diff --git a/lib/philomena_web/controllers/admin/report_controller.ex b/lib/philomena_web/controllers/admin/report_controller.ex index 04e7735c..78a8bcc0 100644 --- a/lib/philomena_web/controllers/admin/report_controller.ex +++ b/lib/philomena_web/controllers/admin/report_controller.ex @@ -42,7 +42,10 @@ defmodule PhilomenaWeb.Admin.ReportController do %{ bool: %{ must: %{term: %{open: true}}, - must_not: %{term: %{admin_id: user.id}} + must_not: [ + %{term: %{admin_id: user.id}}, + %{term: %{system: true}} + ] } } ] @@ -59,11 +62,20 @@ defmodule PhilomenaWeb.Admin.ReportController do |> Repo.all() |> Polymorphic.load_polymorphic(reportable: [reportable_id: :reportable_type]) + system_reports = + Report + |> where(open: true, system: true) + |> preload([:admin, user: :linked_tags]) + |> order_by(desc: :created_at) + |> Repo.all() + |> Polymorphic.load_polymorphic(reportable: [reportable_id: :reportable_type]) + render(conn, "index.html", title: "Admin - Reports", layout_class: "layout--wide", reports: reports, - my_reports: my_reports + my_reports: my_reports, + system_reports: system_reports ) end diff --git a/lib/philomena_web/controllers/conversation/approve_controller.ex b/lib/philomena_web/controllers/conversation/message/approve_controller.ex similarity index 76% rename from lib/philomena_web/controllers/conversation/approve_controller.ex rename to lib/philomena_web/controllers/conversation/message/approve_controller.ex index 20529e14..c45dc9ec 100644 --- a/lib/philomena_web/controllers/conversation/approve_controller.ex +++ b/lib/philomena_web/controllers/conversation/message/approve_controller.ex @@ -1,16 +1,16 @@ -defmodule PhilomenaWeb.Conversation.ApproveController do +defmodule PhilomenaWeb.Conversation.Message.ApproveController do use PhilomenaWeb, :controller - alias Philomena.Conversations.Conversation + alias Philomena.Conversations.Message alias Philomena.Conversations plug PhilomenaWeb.CanaryMapPlug, create: :approve plug :load_and_authorize_resource, - model: Conversation, - id_field: "slug", - id_name: "conversation_id", - persisted: true + model: Message, + id_name: "message_id", + persisted: true, + preload: [:conversation] def create(conn, _params) do message = conn.assigns.message diff --git a/lib/philomena_web/controllers/conversation/message_controller.ex b/lib/philomena_web/controllers/conversation/message_controller.ex index 892091e6..7e238902 100644 --- a/lib/philomena_web/controllers/conversation/message_controller.ex +++ b/lib/philomena_web/controllers/conversation/message_controller.ex @@ -20,7 +20,11 @@ defmodule PhilomenaWeb.Conversation.MessageController do user = conn.assigns.current_user case Conversations.create_message(conversation, user, message_params) do - {:ok, _result} -> + {:ok, %{message: message}} -> + if not message.approved do + Conversations.report_non_approved(message.conversation_id) + end + count = Message |> where(conversation_id: ^conversation.id) diff --git a/lib/philomena_web/controllers/conversation_controller.ex b/lib/philomena_web/controllers/conversation_controller.ex index 6fe1a74d..5380b5f6 100644 --- a/lib/philomena_web/controllers/conversation_controller.ex +++ b/lib/philomena_web/controllers/conversation_controller.ex @@ -107,6 +107,11 @@ defmodule PhilomenaWeb.ConversationController do case Conversations.create_conversation(user, conversation_params) do {:ok, conversation} -> + if not hd(conversation.messages).approved do + Conversations.report_non_approved(conversation.id) + Conversations.set_as_read(conversation) + end + conn |> put_flash(:info, "Conversation successfully created.") |> redirect(to: Routes.conversation_path(conn, :show, conversation)) diff --git a/lib/philomena_web/controllers/image/approve_controller.ex b/lib/philomena_web/controllers/image/approve_controller.ex index 66e5551f..689339f8 100644 --- a/lib/philomena_web/controllers/image/approve_controller.ex +++ b/lib/philomena_web/controllers/image/approve_controller.ex @@ -15,7 +15,7 @@ defmodule PhilomenaWeb.Image.ApproveController do conn |> put_flash(:info, "Image has been approved.") |> moderation_log(details: &log_details/3, data: image) - |> redirect(to: Routes.image_path(conn, :show, image)) + |> redirect(to: Routes.admin_approval_path(conn, :index)) end defp log_details(conn, _action, image) do diff --git a/lib/philomena_web/controllers/image/comment_controller.ex b/lib/philomena_web/controllers/image/comment_controller.ex index 21002e4a..485a94fa 100644 --- a/lib/philomena_web/controllers/image/comment_controller.ex +++ b/lib/philomena_web/controllers/image/comment_controller.ex @@ -78,11 +78,11 @@ defmodule PhilomenaWeb.Image.CommentController do PhilomenaWeb.Api.Json.CommentView.render("show.json", %{comment: comment}) ) - Comments.notify_comment(comment) Comments.reindex_comment(comment) Images.reindex_image(conn.assigns.image) if comment.approved do + Comments.notify_comment(comment) UserStatistics.inc_stat(conn.assigns.current_user, :comments_posted) else Comments.report_non_approved(comment) diff --git a/lib/philomena_web/controllers/topic/approve_controller.ex b/lib/philomena_web/controllers/topic/approve_controller.ex deleted file mode 100644 index 5d98d540..00000000 --- a/lib/philomena_web/controllers/topic/approve_controller.ex +++ /dev/null @@ -1,45 +0,0 @@ -defmodule PhilomenaWeb.Topic.ApproveController do - import Plug.Conn - use PhilomenaWeb, :controller - - alias Philomena.Forums.Forum - alias Philomena.Topics.Topic - alias Philomena.Topics - - plug PhilomenaWeb.CanaryMapPlug, create: :show - - plug :load_and_authorize_resource, - model: Forum, - id_name: "forum_id", - id_field: "short_name", - persisted: true - - plug PhilomenaWeb.LoadTopicPlug - plug PhilomenaWeb.CanaryMapPlug, create: :approve - plug :authorize_resource, model: Topic, persisted: true - - def create(conn, _params) do - topic = conn.assigns.topic - user = conn.assigns.current_user - - case Topics.approve_topic(topic) do - {:ok, topic} -> - conn - |> put_flash(:info, "Topic successfully approved!") - |> moderation_log(details: &log_details/3, data: topic) - |> redirect(to: Routes.forum_topic_path(conn, :show, topic.forum, topic)) - - {:error, _changeset} -> - conn - |> put_flash(:error, "Unable to approve the topic!") - |> redirect(to: Routes.forum_topic_path(conn, :show, topic.forum, topic)) - end - end - - defp log_details(conn, action, topic) do - %{ - body: "Approved topic '#{topic.title}' in #{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/approve_controller.ex b/lib/philomena_web/controllers/topic/post/approve_controller.ex index 094edad0..17a1a4b9 100644 --- a/lib/philomena_web/controllers/topic/post/approve_controller.ex +++ b/lib/philomena_web/controllers/topic/post/approve_controller.ex @@ -18,8 +18,6 @@ defmodule PhilomenaWeb.Topic.Post.ApproveController do case Posts.approve_post(post, user) do {:ok, post} -> - UserStatistics.inc_stat(post.user(:forum_posts)) - conn |> put_flash(:info, "Post successfully approved.") |> moderation_log(details: &log_details/3, data: post) diff --git a/lib/philomena_web/controllers/topic/post_controller.ex b/lib/philomena_web/controllers/topic/post_controller.ex index 0fc29600..98d62698 100644 --- a/lib/philomena_web/controllers/topic/post_controller.ex +++ b/lib/philomena_web/controllers/topic/post_controller.ex @@ -35,9 +35,8 @@ defmodule PhilomenaWeb.Topic.PostController do case Posts.create_post(topic, attributes, post_params) do {:ok, %{post: post}} -> - Posts.notify_post(post) - if post.approved do + Posts.notify_post(post) UserStatistics.inc_stat(conn.assigns.current_user, :forum_posts) else Posts.report_non_approved(post) diff --git a/lib/philomena_web/image_loader.ex b/lib/philomena_web/image_loader.ex index bfd7b6fd..c6a33c2a 100644 --- a/lib/philomena_web/image_loader.ex +++ b/lib/philomena_web/image_loader.ex @@ -63,6 +63,7 @@ defmodule PhilomenaWeb.ImageLoader do ] |> maybe_show_deleted(show_hidden?, del) |> maybe_custom_hide(user, hidden) + |> hide_non_approved() end # Allow moderators to index hidden images @@ -94,6 +95,10 @@ defmodule PhilomenaWeb.ImageLoader do defp maybe_custom_hide(filters, _user, _param), do: filters + # Hide all images that aren't approved from all search queries. + defp hide_non_approved(filters), + do: [%{term: %{approved: false}} | filters] + # TODO: the search parser should try to optimize queries defp search_tag_name(%{term: %{"namespaced_tags.name" => tag_name}}), do: [tag_name] defp search_tag_name(_other_query), do: [] diff --git a/lib/philomena_web/image_sorter.ex b/lib/philomena_web/image_sorter.ex index d2124c77..83c78b80 100644 --- a/lib/philomena_web/image_sorter.ex +++ b/lib/philomena_web/image_sorter.ex @@ -75,7 +75,7 @@ defmodule PhilomenaWeb.ImageSorter do end defp parse_sf(_params, sd, query) do - %{query: query, sorts: [%{"id" => sd}]} + %{query: query, sorts: [%{"first_seen_at" => sd}]} end defp random_query(seed, sd, query) do diff --git a/lib/philomena_web/markdown_renderer.ex b/lib/philomena_web/markdown_renderer.ex index bf6e355c..d8893467 100644 --- a/lib/philomena_web/markdown_renderer.ex +++ b/lib/philomena_web/markdown_renderer.ex @@ -58,6 +58,9 @@ defmodule PhilomenaWeb.MarkdownRenderer do image.hidden_from_users -> " (deleted)" + not image.approved -> + " (pending approval)" + true -> "" end @@ -75,7 +78,7 @@ defmodule PhilomenaWeb.MarkdownRenderer do cond do img != nil -> case group do - [_id, "p"] when not img.hidden_from_users -> + [_id, "p"] when not img.hidden_from_users and img.approved -> Phoenix.View.render(@image_view, "_image_target.html", image: img, size: :medium, @@ -83,7 +86,7 @@ defmodule PhilomenaWeb.MarkdownRenderer do ) |> safe_to_string() - [_id, "t"] when not img.hidden_from_users -> + [_id, "t"] when not img.hidden_from_users and img.approved -> Phoenix.View.render(@image_view, "_image_target.html", image: img, size: :small, @@ -91,7 +94,7 @@ defmodule PhilomenaWeb.MarkdownRenderer do ) |> safe_to_string() - [_id, "s"] when not img.hidden_from_users -> + [_id, "s"] when not img.hidden_from_users and img.approved -> Phoenix.View.render(@image_view, "_image_target.html", image: img, size: :thumb_small, @@ -99,6 +102,9 @@ defmodule PhilomenaWeb.MarkdownRenderer do ) |> safe_to_string() + [id, suffix] when not img.approved -> + ">>#{img.id}#{suffix}#{link_suffix(img)}" + [_id, ""] -> link(">>#{img.id}#{link_suffix(img)}", to: "/images/#{img.id}") |> safe_to_string() diff --git a/lib/philomena_web/plugs/admin_counters_plug.ex b/lib/philomena_web/plugs/admin_counters_plug.ex index ac9cb938..e2da61a9 100644 --- a/lib/philomena_web/plugs/admin_counters_plug.ex +++ b/lib/philomena_web/plugs/admin_counters_plug.ex @@ -9,6 +9,7 @@ defmodule PhilomenaWeb.AdminCountersPlug do alias Philomena.Reports alias Philomena.ArtistLinks alias Philomena.DnpEntries + alias Philomena.Images import Plug.Conn, only: [assign: 3] @@ -31,12 +32,14 @@ defmodule PhilomenaWeb.AdminCountersPlug do defp maybe_assign_admin_metrics(conn, _user, false), do: conn defp maybe_assign_admin_metrics(conn, user, true) do + pending_approvals = Images.count_pending_approvals() duplicate_reports = DuplicateReports.count_duplicate_reports(user) reports = Reports.count_reports(user) artist_links = ArtistLinks.count_artist_links(user) dnps = DnpEntries.count_dnp_entries(user) conn + |> assign(:pending_approval_count, pending_approvals) |> assign(:duplicate_report_count, duplicate_reports) |> assign(:report_count, reports) |> assign(:artist_link_count, artist_links) diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index 3cec4363..c5d48d9d 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -177,10 +177,15 @@ defmodule PhilomenaWeb.Router do resources "/conversations", ConversationController, only: [:index, :show, :new, :create] do resources "/reports", Conversation.ReportController, only: [:new, :create] - resources "/messages", Conversation.MessageController, only: [:create] + + resources "/messages", Conversation.MessageController, only: [:create] do + resources "/approve", Conversation.Message.ApproveController, + only: [:create], + singleton: true + end + resources "/read", Conversation.ReadController, only: [:create, :delete], singleton: true resources "/hide", Conversation.HideController, only: [:create, :delete], singleton: true - resources "/approve", Conversation.ApproveController, only: [:create], singleton: true end resources "/images", ImageController, only: [] do @@ -240,7 +245,6 @@ defmodule PhilomenaWeb.Router do resources "/stick", Topic.StickController, only: [:create, :delete], singleton: true resources "/lock", Topic.LockController, only: [:create, :delete], singleton: true resources "/hide", Topic.HideController, only: [:create, :delete], singleton: true - resources "/approve", Topic.ApproveController, only: [:create], singleton: true resources "/posts", Topic.PostController, only: [:edit, :update] do resources "/hide", Topic.Post.HideController, only: [:create, :delete], singleton: true @@ -340,6 +344,8 @@ defmodule PhilomenaWeb.Router do resources "/close", Report.CloseController, only: [:create], singleton: true end + resources "/approvals", ApprovalController, only: [:index] + resources "/artist_links", ArtistLinkController, only: [:index] do resources "/verification", ArtistLink.VerificationController, only: [:create], diff --git a/lib/philomena_web/templates/admin/approval/_approvals.html.slime b/lib/philomena_web/templates/admin/approval/_approvals.html.slime new file mode 100644 index 00000000..44ef14cb --- /dev/null +++ b/lib/philomena_web/templates/admin/approval/_approvals.html.slime @@ -0,0 +1,32 @@ +.block + .block__header + .block__header__title.approval-grid + .approval-items--main + span ID + span Image + span Uploader + span Time + .approval-items--footer.hide-mobile + span.hide-mobile Actions + = for image <- @images do + .block__content.alternating-color + .approval-grid + .approval-items--main + span = link ">>#{image.id}", to: Routes.image_path(@conn, :show, image) + span = image_thumb(@conn, image) + span + = if image.user do + = link image.user.name, to: Routes.profile_path(@conn, :show, image.user) + - else + em> + = truncated_ip_link(@conn, image.ip) + = link_to_fingerprint(@conn, image.fingerprint) + span = pretty_time(image.created_at) + .approval-items--footer + = if can?(@conn, :approve, image) do + = button_to "Approve", Routes.image_approve_path(@conn, :create, image), method: "post", class: "button button--state-success" + = if can?(@conn, :hide, image) do + = form_for :image, Routes.image_delete_path(@conn, :create, image), [method: "post"], fn f -> + .field.field--inline + = text_input f, :deletion_reason, class: "input input--wide", placeholder: "Rule violation", required: true + = submit "Delete", class: "button button--state-danger button--separate-left" diff --git a/lib/philomena_web/templates/admin/approval/index.html.slime b/lib/philomena_web/templates/admin/approval/index.html.slime new file mode 100644 index 00000000..818f7ae1 --- /dev/null +++ b/lib/philomena_web/templates/admin/approval/index.html.slime @@ -0,0 +1,16 @@ +- route = fn p -> Routes.admin_approval_path(@conn, :index, p) end +- pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @images, route: route + +h1 Approval Queue + +.block + .block__header + = pagination + + = if Enum.any?(@images) do + = render PhilomenaWeb.Admin.ApprovalView, "_approvals.html", images: @images, conn: @conn + - else + ' No images are pending approval. Good job! + + .block__header.block__header--light + = pagination diff --git a/lib/philomena_web/templates/admin/report/index.html.slime b/lib/philomena_web/templates/admin/report/index.html.slime index 96fb053b..23f626f1 100644 --- a/lib/philomena_web/templates/admin/report/index.html.slime +++ b/lib/philomena_web/templates/admin/report/index.html.slime @@ -10,6 +10,13 @@ h1 Reports .block__content = render PhilomenaWeb.Admin.ReportView, "_reports.html", reports: @my_reports, conn: @conn += if Enum.any?(@system_reports) do + .block + .block__header.block--danger + span.block__header__title System Reports + .block__content + = render PhilomenaWeb.Admin.ReportView, "_reports.html", reports: @system_reports, conn: @conn + .block .block__header span.block__header__title All Reports diff --git a/lib/philomena_web/templates/conversation/new.html.slime b/lib/philomena_web/templates/conversation/new.html.slime index 040bb99b..a6800652 100644 --- a/lib/philomena_web/templates/conversation/new.html.slime +++ b/lib/philomena_web/templates/conversation/new.html.slime @@ -5,6 +5,21 @@ h1 New Conversation ' » span.block__header__title New Conversation += case DateTime.compare(DateTime.utc_now(), DateTime.add(@conn.assigns.current_user.created_at, 1_209_600)) do + - :lt -> + .block.block--fixed.block--warning.hidden.js-hidden-warning + h2 Warning! + p + strong> Your account is too new, so your PM will need to be reviewed by staff members. + ' This is because it contains an external image. If you are not okay with a moderator viewing this PM conversation, please consider linking the image instead of embedding it (change + code<> ![ + ' to + code< + | [ + | ). + - _ -> + / Nothing + = form_for @changeset, Routes.conversation_path(@conn, :create), fn f -> = if @changeset.action do .alert.alert-danger diff --git a/lib/philomena_web/templates/conversation/show.html.slime b/lib/philomena_web/templates/conversation/show.html.slime index c30f8c67..9c69b9bc 100644 --- a/lib/philomena_web/templates/conversation/show.html.slime +++ b/lib/philomena_web/templates/conversation/show.html.slime @@ -31,6 +31,21 @@ h1 = @conversation.title .block__header.block__header--light.page__header .page__pagination = pagination += case DateTime.compare(DateTime.utc_now(), DateTime.add(@conn.assigns.current_user.created_at, 1_209_600)) do + - :lt -> + .block.block--fixed.block--warning.hidden.js-hidden-warning + h2 Warning! + p + strong> Your account is too new, so your PM will need to be reviewed by staff members. + ' This is because it contains an external image. If you are not okay with a moderator viewing this PM conversation, please consider linking the image instead of embedding it (change + code<> ![ + ' to + code< + | [ + | ). + - _ -> + / Nothing + = cond do - @conn.assigns.current_ban -> = render PhilomenaWeb.BanView, "_ban_reason.html", conn: @conn diff --git a/lib/philomena_web/templates/image/_image_approval_banner.html.slime b/lib/philomena_web/templates/image/_image_approval_banner.html.slime new file mode 100644 index 00000000..9c4ec4f1 --- /dev/null +++ b/lib/philomena_web/templates/image/_image_approval_banner.html.slime @@ -0,0 +1,12 @@ += if not @image.approved and not @image.hidden_from_users do + .block.block--fixed.block--warning + h2 Hold up! + p This image is pending approval from a staff member. It will appear on the site once it's reviewed and approved. + p + ' Don't worry, + strong + ' the image will not lose any viewership, + ' it will appear on the homepage and in search results as normal (as if it was uploaded at the time of approval). + p + a href="/pages/approval" + strong Click here to learn more about image approval and verification. diff --git a/lib/philomena_web/templates/image/_options.html.slime b/lib/philomena_web/templates/image/_options.html.slime index 020a7bcb..be26ec4a 100644 --- a/lib/philomena_web/templates/image/_options.html.slime +++ b/lib/philomena_web/templates/image/_options.html.slime @@ -145,5 +145,7 @@ br .flex.flex--spaced-out = link "Lock specific tags", to: Routes.image_tag_lock_path(@conn, :show, @image), class: "button" + = if not @image.approved and can?(@conn, :approve, @image) do + = button_to "Approve image", Routes.image_approve_path(@conn, :create, @image), method: "post", class: "button button--state-success", data: [confirm: "Are you sure?"] = if @image.hidden_from_users and can?(@conn, :destroy, @image) do - = button_to "Destroy image", Routes.image_destroy_path(@conn, :create, @image), method: "post", class: "button button--state-danger", data: [confirm: "This action is IRREVERSIBLE. Are you sure?"] + = button_to "Destroy image", Routes.image_destroy_path(@conn, :create, @image), method: "post", class: "button button--state-danger", data: [confirm: "This action is IRREVERSIBLE. Are you sure?"] diff --git a/lib/philomena_web/templates/image/show.html.slime b/lib/philomena_web/templates/image/show.html.slime index 9a5fd499..0c566314 100644 --- a/lib/philomena_web/templates/image/show.html.slime +++ b/lib/philomena_web/templates/image/show.html.slime @@ -1,3 +1,4 @@ += render PhilomenaWeb.ImageView, "_image_approval_banner.html", image: @image, conn: @conn = render PhilomenaWeb.ImageView, "_image_meta.html", image: @image, watching: @watching, user_galleries: @user_galleries, changeset: @image_changeset, conn: @conn = render PhilomenaWeb.ImageView, "_image_page.html", image: @image, conn: @conn 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 d59305b5..52d6aef6 100644 --- a/lib/philomena_web/templates/layout/_header_staff_links.html.slime +++ b/lib/philomena_web/templates/layout/_header_staff_links.html.slime @@ -46,6 +46,12 @@ i.fa.fa-fw.fa-list-alt> ' Mod Logs + = if @pending_approval_count do + = link to: Routes.admin_approval_path(@conn, :index), class: "header__link", title: "Approval Queue" do + ' Q + span.header__counter__admin + = @pending_approval_count + = 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/message/_message.html.slime b/lib/philomena_web/templates/message/_message.html.slime index e22258d7..bf24ea6c 100644 --- a/lib/philomena_web/templates/message/_message.html.slime +++ b/lib/philomena_web/templates/message/_message.html.slime @@ -1,4 +1,18 @@ article.block.communication + = if not @message.approved and (can?(@conn, :approve, @message) or @message.from_id == @conn.assigns.current_user.id) do + .block__content + .block.block--fixed.block--danger + p + i.fas.fa-exclamation-triangle> + ' This private message is pending approval from a staff member. + = if can?(@conn, :approve, @message) do + p + ul.horizontal-list + li + = link(to: Routes.conversation_message_approve_path(@conn, :create, @message.conversation_id, @message), data: [confirm: "Are you sure?"], method: "post", class: "button") do + i.fas.fa-check> + ' Approve + .block__content.flex.flex--no-wrap .flex__fixed.spacing-right = render PhilomenaWeb.UserAttributionView, "_user_avatar.html", object: %{user: @message.from}, conn: @conn, class: "avatar--100px" diff --git a/lib/philomena_web/templates/search/_form.html.slime b/lib/philomena_web/templates/search/_form.html.slime index fa3dd74d..685528b9 100644 --- a/lib/philomena_web/templates/search/_form.html.slime +++ b/lib/philomena_web/templates/search/_form.html.slime @@ -133,9 +133,9 @@ h1 Search end sort_fields = [ + "Sort by initial post date": :first_seen_at, "Sort by image ID": :id, "Sort by last modification date": :updated_at, - "Sort by initial post date": :first_seen_at, "Sort by aspect ratio": :aspect_ratio, "Sort by fave count": :faves, "Sort by upvotes": :upvotes, diff --git a/lib/philomena_web/views/admin/approval_view.ex b/lib/philomena_web/views/admin/approval_view.ex new file mode 100644 index 00000000..d8a78b11 --- /dev/null +++ b/lib/philomena_web/views/admin/approval_view.ex @@ -0,0 +1,16 @@ +defmodule PhilomenaWeb.Admin.ApprovalView do + use PhilomenaWeb, :view + + alias PhilomenaWeb.Admin.ReportView + + # Shamelessly copied from ReportView + def truncated_ip_link(conn, ip), do: ReportView.truncated_ip_link(conn, ip) + + def image_thumb(conn, image) do + render(PhilomenaWeb.ImageView, "_image_container.html", + image: image, + size: :thumb_tiny, + conn: conn + ) + end +end diff --git a/priv/repo/migrations/20220321173359_add_approval_queue.exs b/priv/repo/migrations/20220321173359_add_approval_queue.exs index 4d1421ea..066123cd 100644 --- a/priv/repo/migrations/20220321173359_add_approval_queue.exs +++ b/priv/repo/migrations/20220321173359_add_approval_queue.exs @@ -18,12 +18,18 @@ defmodule Philomena.Repo.Migrations.AddApprovalQueue do add :approved, :boolean, default: false end - alter table("topics") do + alter table("messages") do add :approved, :boolean, default: false end alter table("users") do add :verified, :boolean, default: false end + + create index(:images, [:hidden_from_users, :approved], + where: "hidden_from_users = false and approved = false" + ) + + create index(:reports, [:system], where: "system = true") end end diff --git a/priv/repo/structure.sql b/priv/repo/structure.sql index a6e13a4f..1a412576 100644 --- a/priv/repo/structure.sql +++ b/priv/repo/structure.sql @@ -1007,7 +1007,8 @@ CREATE TABLE public.messages ( updated_at timestamp without time zone NOT NULL, from_id integer NOT NULL, conversation_id integer NOT NULL, - body character varying NOT NULL + body character varying NOT NULL, + approved boolean DEFAULT false ); @@ -1680,8 +1681,7 @@ CREATE TABLE public.topics ( deleted_by_id integer, locked_by_id integer, last_post_id integer, - hidden_from_users boolean DEFAULT false NOT NULL, - approved boolean DEFAULT false + hidden_from_users boolean DEFAULT false NOT NULL ); @@ -2903,6 +2903,13 @@ CREATE UNIQUE INDEX image_tag_locks_image_id_tag_id_index ON public.image_tag_lo CREATE INDEX image_tag_locks_tag_id_index ON public.image_tag_locks USING btree (tag_id); +-- +-- Name: images_hidden_from_users_approved_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX images_hidden_from_users_approved_index ON public.images USING btree (hidden_from_users, approved) WHERE ((hidden_from_users = false) AND (approved = false)); + + -- -- Name: index_adverts_on_restrictions; Type: INDEX; Schema: public; Owner: - -- @@ -4100,6 +4107,13 @@ CREATE INDEX moderation_logs_user_id_created_at_index ON public.moderation_logs CREATE INDEX moderation_logs_user_id_index ON public.moderation_logs USING btree (user_id); +-- +-- Name: reports_system_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX reports_system_index ON public.reports USING btree (system) WHERE (system = true); + + -- -- Name: user_tokens_context_token_index; Type: INDEX; Schema: public; Owner: - --