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..104bd09c --- /dev/null +++ b/assets/js/pmwarning.js @@ -0,0 +1,28 @@ +/** + * 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 34527361..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 @@ -197,6 +198,44 @@ defmodule Philomena.Comments do |> Repo.update() end + def approve_comment(%Comment{} = comment, user) do + reports = + Report + |> where(reportable_type: "Comment", reportable_id: ^comment.id) + |> select([r], r.id) + |> update(set: [open: false, state: "closed", admin_id: ^user.id]) + + comment = Comment.approve_changeset(comment) + + Multi.new() + |> Multi.update(:comment, comment) + |> Multi.update_all(:reports, reports, []) + |> 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) + + {:ok, comment} + + error -> + error + end + end + + def report_non_approved(%Comment{approved: true}), do: false + + def report_non_approved(comment) do + Reports.create_system_report( + comment.id, + "Comment", + "Approval", + "Comment contains externally-embedded images and has been flagged for review." + ) + end + def migrate_comments(image, duplicate_of_image) do {count, nil} = Comment diff --git a/lib/philomena/comments/comment.ex b/lib/philomena/comments/comment.ex index 180aaaaa..9571b450 100644 --- a/lib/philomena/comments/comment.ex +++ b/lib/philomena/comments/comment.ex @@ -4,6 +4,7 @@ defmodule Philomena.Comments.Comment do alias Philomena.Images.Image alias Philomena.Users.User + alias Philomena.Schema.Approval schema "comments" do belongs_to :user, User @@ -22,6 +23,7 @@ defmodule Philomena.Comments.Comment do field :deletion_reason, :string, default: "" field :destroyed_content, :boolean, default: false field :name_at_post_time, :string + field :approved, :boolean timestamps(inserted_at: :created_at, type: :utc_datetime) end @@ -34,6 +36,8 @@ defmodule Philomena.Comments.Comment do |> validate_length(:body, min: 1, max: 300_000, count: :bytes) |> change(attribution) |> put_name_at_post_time(attribution[:user]) + |> Approval.maybe_put_approval(attribution[:user]) + |> Approval.maybe_strip_images(attribution[:user]) end def changeset(comment, attrs, edited_at \\ nil) do @@ -43,6 +47,7 @@ defmodule Philomena.Comments.Comment do |> validate_required([:body]) |> validate_length(:body, min: 1, max: 300_000, count: :bytes) |> validate_length(:edit_reason, max: 70, count: :bytes) + |> Approval.maybe_put_approval(comment.user) end def hide_changeset(comment, attrs, user) do @@ -65,6 +70,11 @@ defmodule Philomena.Comments.Comment do |> put_change(:body, "") end + def approve_changeset(comment) do + change(comment) + |> put_change(:approved, true) + end + defp put_name_at_post_time(changeset, nil), do: changeset defp put_name_at_post_time(changeset, user), do: change(changeset, name_at_post_time: user.name) end diff --git a/lib/philomena/comments/elasticsearch_index.ex b/lib/philomena/comments/elasticsearch_index.ex index 54012b04..82d65bcc 100644 --- a/lib/philomena/comments/elasticsearch_index.ex +++ b/lib/philomena/comments/elasticsearch_index.ex @@ -30,7 +30,8 @@ defmodule Philomena.Comments.ElasticsearchIndex do anonymous: %{type: "keyword"}, # boolean hidden_from_users: %{type: "keyword"}, - body: %{type: "text", analyzer: "snowball"} + body: %{type: "text", analyzer: "snowball"}, + approved: %{type: "boolean"} } } } @@ -49,7 +50,8 @@ defmodule Philomena.Comments.ElasticsearchIndex do image_tag_ids: comment.image.tags |> Enum.map(& &1.id), anonymous: comment.anonymous, hidden_from_users: comment.image.hidden_from_users || comment.hidden_from_users, - body: comment.body + body: comment.body, + approved: comment.image.approved && comment.approved } end diff --git a/lib/philomena/conversations.ex b/lib/philomena/conversations.ex index 5542c021..597b64f7 100644 --- a/lib/philomena/conversations.ex +++ b/lib/philomena/conversations.ex @@ -6,7 +6,8 @@ defmodule Philomena.Conversations do import Ecto.Query, warn: false alias Ecto.Multi alias Philomena.Repo - + alias Philomena.Reports + alias Philomena.Reports.Report alias Philomena.Conversations.Conversation @doc """ @@ -187,6 +188,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 +203,57 @@ 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, user) do + reports_query = + Report + |> where(reportable_type: "Conversation", reportable_id: ^message.conversation_id) + |> select([r], r.id) + |> update(set: [open: false, state: "closed", admin_id: ^user.id]) + + message_query = + message + |> Message.approve_changeset() + + conversation_query = + Conversation + |> where(id: ^message.conversation_id) + + Multi.new() + |> Multi.update(:message, message_query) + |> Multi.update_all(:conversation, conversation_query, set: [to_read: false]) + |> Multi.update_all(:reports, reports_query, []) + |> Repo.transaction() + |> case do + {:ok, %{reports: {_count, reports}} = result} -> + Reports.reindex_reports(reports) + + {:ok, result} + + error -> + error + end + 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 f1dc1129..a9e6fefd 100644 --- a/lib/philomena/conversations/message.ex +++ b/lib/philomena/conversations/message.ex @@ -4,12 +4,14 @@ defmodule Philomena.Conversations.Message do alias Philomena.Conversations.Conversation alias Philomena.Users.User + alias Philomena.Schema.Approval schema "messages" do belongs_to :conversation, Conversation belongs_to :from, User field :body, :string + field :approved, :boolean, default: false timestamps(inserted_at: :created_at, type: :utc_datetime) end @@ -28,5 +30,10 @@ defmodule Philomena.Conversations.Message do |> validate_required([:body]) |> put_assoc(:from, user) |> 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..9feda6ab 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/elasticsearch_index.ex b/lib/philomena/images/elasticsearch_index.ex index 54154c7b..0936656f 100644 --- a/lib/philomena/images/elasticsearch_index.ex +++ b/lib/philomena/images/elasticsearch_index.ex @@ -86,7 +86,8 @@ defmodule Philomena.Images.ElasticsearchIndex do name_in_namespace: %{type: "keyword"}, namespace: %{type: "keyword"} } - } + }, + approved: %{type: "boolean"} } } } @@ -149,7 +150,8 @@ defmodule Philomena.Images.ElasticsearchIndex do hidden_by_users: image.hiders |> Enum.map(&String.downcase(&1.name)), upvoters: image.upvoters |> Enum.map(&String.downcase(&1.name)), downvoters: image.downvoters |> Enum.map(&String.downcase(&1.name)), - deleted_by_user: if(!!image.deleter, do: image.deleter.name) + deleted_by_user: if(!!image.deleter, do: image.deleter.name), + approved: image.approved } end diff --git a/lib/philomena/images/image.ex b/lib/philomena/images/image.ex index 908301c4..34eeb64b 100644 --- a/lib/philomena/images/image.ex +++ b/lib/philomena/images/image.ex @@ -82,6 +82,7 @@ defmodule Philomena.Images.Image do field :hidden_image_key, :string field :scratchpad, :string field :hides_count, :integer, default: 0 + field :approved, :boolean # todo: can probably remove these now field :tag_list_cache, :string @@ -325,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 01b32452..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,17 @@ 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, + "Post", + "Approval", + "Post contains externally-embedded images and has been flagged for review." + ) + end + def perform_notify(post_id) do post = get_post!(post_id) @@ -237,6 +249,33 @@ defmodule Philomena.Posts do |> reindex_after_update() end + def approve_post(%Post{} = post, user) do + reports = + Report + |> where(reportable_type: "Post", reportable_id: ^post.id) + |> select([r], r.id) + |> update(set: [open: false, state: "closed", admin_id: ^user.id]) + + post = Post.approve_changeset(post) + + Multi.new() + |> Multi.update(:post, post) + |> Multi.update_all(:reports, reports, []) + |> 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) + + {:ok, post} + + error -> + error + end + end + @doc """ Returns an `%Ecto.Changeset{}` for tracking post changes. diff --git a/lib/philomena/posts/elasticsearch_index.ex b/lib/philomena/posts/elasticsearch_index.ex index 827d819c..43a14284 100644 --- a/lib/philomena/posts/elasticsearch_index.ex +++ b/lib/philomena/posts/elasticsearch_index.ex @@ -36,7 +36,8 @@ defmodule Philomena.Posts.ElasticsearchIndex do created_at: %{type: "date"}, deleted: %{type: "boolean"}, access_level: %{type: "keyword"}, - destroyed_content: %{type: "boolean"} + destroyed_content: %{type: "boolean"}, + approved: %{type: "boolean"} } } } @@ -63,7 +64,8 @@ defmodule Philomena.Posts.ElasticsearchIndex do updated_at: post.updated_at, deleted: post.hidden_from_users, access_level: post.topic.forum.access_level, - destroyed_content: post.destroyed_content + destroyed_content: post.destroyed_content, + approved: post.approved } end diff --git a/lib/philomena/posts/post.ex b/lib/philomena/posts/post.ex index b3c3c42f..55d5b401 100644 --- a/lib/philomena/posts/post.ex +++ b/lib/philomena/posts/post.ex @@ -4,6 +4,7 @@ defmodule Philomena.Posts.Post do alias Philomena.Users.User alias Philomena.Topics.Topic + alias Philomena.Schema.Approval schema "posts" do belongs_to :user, User @@ -23,6 +24,7 @@ defmodule Philomena.Posts.Post do field :deletion_reason, :string, default: "" field :destroyed_content, :boolean, default: false field :name_at_post_time, :string + field :approved, :boolean, default: false timestamps(inserted_at: :created_at, type: :utc_datetime) end @@ -35,6 +37,7 @@ defmodule Philomena.Posts.Post do |> validate_required([:body]) |> validate_length(:body, min: 1, max: 300_000, count: :bytes) |> validate_length(:edit_reason, max: 70, count: :bytes) + |> Approval.maybe_put_approval(post.user) end @doc false @@ -45,6 +48,8 @@ defmodule Philomena.Posts.Post do |> validate_length(:body, min: 1, max: 300_000, count: :bytes) |> change(attribution) |> put_name_at_post_time(attribution[:user]) + |> Approval.maybe_put_approval(attribution[:user]) + |> Approval.maybe_strip_images(attribution[:user]) end @doc false @@ -57,6 +62,8 @@ defmodule Philomena.Posts.Post do |> change(attribution) |> change(topic_position: 0) |> put_name_at_post_time(attribution[:user]) + |> Approval.maybe_put_approval(attribution[:user]) + |> Approval.maybe_strip_images(attribution[:user]) end def hide_changeset(post, attrs, user) do @@ -79,6 +86,11 @@ defmodule Philomena.Posts.Post do |> put_change(:body, "") end + def approve_changeset(post) do + change(post) + |> put_change(:approved, true) + end + defp put_name_at_post_time(changeset, nil), do: changeset defp put_name_at_post_time(changeset, user), do: change(changeset, name_at_post_time: user.name) end diff --git a/lib/philomena/reports.ex b/lib/philomena/reports.ex index 904222ca..25a04e74 100644 --- a/lib/philomena/reports.ex +++ b/lib/philomena/reports.ex @@ -60,6 +60,26 @@ defmodule Philomena.Reports do |> reindex_after_update() end + def create_system_report(reportable_id, reportable_type, category, reason) do + attrs = %{ + reason: reason, + category: category + } + + attributes = %{ + system: true, + ip: %Postgrex.INET{address: {127, 0, 0, 1}, netmask: 32}, + fingerprint: "ffff", + user_agent: + "Mozilla/5.0 (X11; Philomena; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0" + } + + %Report{reportable_id: reportable_id, reportable_type: reportable_type} + |> Report.creation_changeset(attrs, attributes) + |> Repo.insert() + |> reindex_after_update() + end + @doc """ Updates a report. 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/reports/report.ex b/lib/philomena/reports/report.ex index fae713cd..461b6eea 100644 --- a/lib/philomena/reports/report.ex +++ b/lib/philomena/reports/report.ex @@ -15,6 +15,7 @@ defmodule Philomena.Reports.Report do field :reason, :string field :state, :string, default: "open" field :open, :boolean, default: true + field :system, :boolean, default: false # fixme: rails polymorphic relation field :reportable_id, :integer diff --git a/lib/philomena/schema/approval.ex b/lib/philomena/schema/approval.ex new file mode 100644 index 00000000..512c5aab --- /dev/null +++ b/lib/philomena/schema/approval.ex @@ -0,0 +1,45 @@ +defmodule Philomena.Schema.Approval do + alias Philomena.Users.User + import Ecto.Changeset + + @image_embed_regex ~r/!+\[/ + + def maybe_put_approval(changeset, nil), + do: change(changeset, approved: true) + + def maybe_put_approval(changeset, %{role: role}) + when role != "user", + do: change(changeset, approved: true) + + def maybe_put_approval( + %{changes: %{body: body}, valid?: true} = changeset, + %User{} = user + ) do + now = now_time() + # 14 * 24 * 60 * 60 + two_weeks = 1_209_600 + + case String.match?(body, @image_embed_regex) do + true -> + case DateTime.compare(now, DateTime.add(user.created_at, two_weeks)) do + :gt -> change(changeset, approved: true) + _ -> change(changeset, approved: false) + end + + _ -> + change(changeset, approved: true) + end + end + + def maybe_put_approval(changeset, _user), do: changeset + + def maybe_strip_images( + %{changes: %{body: body}, valid?: true} = changeset, + nil + ), + do: change(changeset, body: Regex.replace(@image_embed_regex, body, "[")) + + def maybe_strip_images(changeset, _user), do: changeset + + defp now_time(), do: DateTime.truncate(DateTime.utc_now(), :second) +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/users.ex b/lib/philomena/users.ex index 252d67e7..0ce7a825 100644 --- a/lib/philomena/users.ex +++ b/lib/philomena/users.ex @@ -671,6 +671,18 @@ defmodule Philomena.Users do |> setup_roles() end + def verify_user(%User{} = user) do + user + |> User.verify_changeset() + |> Repo.update() + end + + def unverify_user(%User{} = user) do + user + |> User.unverify_changeset() + |> Repo.update() + end + defp setup_roles(nil), do: nil defp setup_roles(user) do diff --git a/lib/philomena/users/ability.ex b/lib/philomena/users/ability.ex index ac8d471d..21b8ba1a 100644 --- a/lib/philomena/users/ability.ex +++ b/lib/philomena/users/ability.ex @@ -45,8 +45,14 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do # View filters def can?(%User{role: "moderator"}, :show, %Filter{}), do: true - # Manage images + # Privileged mods can hard-delete images + def can?(%User{role: "moderator", role_map: %{"Image" => "admin"}}, :destroy, %Image{}), + do: true + + # ...but normal ones cannot def can?(%User{role: "moderator"}, :destroy, %Image{}), do: false + + # Manage images def can?(%User{role: "moderator"}, _action, Image), do: true def can?(%User{role: "moderator"}, _action, %Image{}), do: true @@ -62,8 +68,9 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do def can?(%User{role: "moderator"}, :show, %Topic{hidden_from_users: true}), do: true - # View conversations + # View and approve conversations def can?(%User{role: "moderator"}, :show, %Conversation{}), do: true + def can?(%User{role: "moderator"}, :approve, %Conversation{}), do: true # View IP addresses and fingerprints def can?(%User{role: "moderator"}, :show, :ip_address), do: true @@ -90,9 +97,11 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do def can?(%User{role: "moderator"}, :edit, %Post{}), do: true def can?(%User{role: "moderator"}, :hide, %Post{}), do: true def can?(%User{role: "moderator"}, :delete, %Post{}), do: true + def can?(%User{role: "moderator"}, :approve, %Post{}), do: true def can?(%User{role: "moderator"}, :edit, %Comment{}), do: true def can?(%User{role: "moderator"}, :hide, %Comment{}), do: true def can?(%User{role: "moderator"}, :delete, %Comment{}), do: true + def can?(%User{role: "moderator"}, :approve, %Comment{}), do: true # Show the DNP list def can?(%User{role: "moderator"}, _action, DnpEntry), do: true @@ -198,6 +207,13 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do ), do: true + def can?( + %User{role: "assistant", role_map: %{"Image" => "moderator"}}, + :approve, + %Image{} + ), + do: true + # Dupe assistant actions def can?( %User{role: "assistant", role_map: %{"DuplicateReport" => "moderator"}}, @@ -244,6 +260,9 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do def can?(%User{role: "assistant", role_map: %{"Comment" => "moderator"}}, :hide, %Comment{}), do: true + def can?(%User{role: "assistant", role_map: %{"Comment" => "moderator"}}, :approve, %Comment{}), + do: true + # Topic assistant actions def can?(%User{role: "assistant", role_map: %{"Topic" => "moderator"}}, :show, %Topic{}), do: true @@ -263,6 +282,9 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do def can?(%User{role: "assistant", role_map: %{"Topic" => "moderator"}}, :hide, %Post{}), do: true + def can?(%User{role: "assistant", role_map: %{"Topic" => "moderator"}}, :approve, %Post{}), + do: true + # Tag assistant actions def can?(%User{role: "assistant", role_map: %{"Tag" => "moderator"}}, :edit, %Tag{}), do: true diff --git a/lib/philomena/users/user.ex b/lib/philomena/users/user.ex index 3a4ee253..1972241d 100644 --- a/lib/philomena/users/user.ex +++ b/lib/philomena/users/user.ex @@ -119,6 +119,7 @@ defmodule Philomena.Users.User do field :hide_default_role, :boolean, default: false field :senior_staff, :boolean, default: false field :bypass_rate_limits, :boolean, default: false + field :verified, :boolean, default: false # For avatar validation/persistence field :avatar_width, :integer, virtual: true @@ -446,6 +447,14 @@ defmodule Philomena.Users.User do change(user, forced_filter_id: nil) end + def verify_changeset(user) do + change(user, verified: true) + end + + def unverify_changeset(user) do + change(user, verified: false) + end + def create_totp_secret_changeset(user) do secret = :crypto.strong_rand_bytes(15) |> Base.encode32() data = Philomena.Users.Encryptor.encrypt_model(secret) 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..74141507 --- /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(asc: :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/admin/user/verification_controller.ex b/lib/philomena_web/controllers/admin/user/verification_controller.ex new file mode 100644 index 00000000..982a520c --- /dev/null +++ b/lib/philomena_web/controllers/admin/user/verification_controller.ex @@ -0,0 +1,44 @@ +defmodule PhilomenaWeb.Admin.User.VerificationController do + use PhilomenaWeb, :controller + + alias Philomena.Users.User + alias Philomena.Users + + plug :verify_authorized + plug :load_resource, model: User, id_name: "user_id", id_field: "slug", persisted: true + + def create(conn, _params) do + {:ok, user} = Users.verify_user(conn.assigns.user) + + conn + |> put_flash(:info, "User verification granted.") + |> moderation_log(details: &log_details/3, data: user) + |> redirect(to: Routes.profile_path(conn, :show, user)) + end + + def delete(conn, _params) do + {:ok, user} = Users.unverify_user(conn.assigns.user) + + conn + |> put_flash(:info, "User verification revoked.") + |> moderation_log(details: &log_details/3, data: user) + |> redirect(to: Routes.profile_path(conn, :show, user)) + end + + defp verify_authorized(conn, _opts) do + case Canada.Can.can?(conn.assigns.current_user, :index, User) do + true -> conn + _false -> PhilomenaWeb.NotAuthorizedPlug.call(conn) + end + end + + defp log_details(conn, action, user) do + body = + case action do + :create -> "Granted verification to #{user.name}" + :delete -> "Revoked verification from #{user.name}" + end + + %{body: body, subject_path: Routes.profile_path(conn, :show, user)} + end +end diff --git a/lib/philomena_web/controllers/conversation/message/approve_controller.ex b/lib/philomena_web/controllers/conversation/message/approve_controller.ex new file mode 100644 index 00000000..3ecade26 --- /dev/null +++ b/lib/philomena_web/controllers/conversation/message/approve_controller.ex @@ -0,0 +1,33 @@ +defmodule PhilomenaWeb.Conversation.Message.ApproveController do + use PhilomenaWeb, :controller + + alias Philomena.Conversations.Message + alias Philomena.Conversations + + plug PhilomenaWeb.CanaryMapPlug, create: :approve + + plug :load_and_authorize_resource, + model: Message, + id_name: "message_id", + persisted: true, + preload: [:conversation] + + def create(conn, _params) do + message = conn.assigns.message + + {:ok, _message} = + Conversations.approve_conversation_message(message, conn.assigns.current_user) + + conn + |> put_flash(:info, "Conversation message approved.") + |> moderation_log(details: &log_details/3, data: message) + |> redirect(to: "/") + end + + defp log_details(_conn, _action, message) do + %{ + body: "Approved private message in conversation ##{message.conversation_id}", + subject_path: "/" + } + end +end 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 new file mode 100644 index 00000000..689339f8 --- /dev/null +++ b/lib/philomena_web/controllers/image/approve_controller.ex @@ -0,0 +1,24 @@ +defmodule PhilomenaWeb.Image.ApproveController do + use PhilomenaWeb, :controller + + alias Philomena.Images.Image + alias Philomena.Images + + plug PhilomenaWeb.CanaryMapPlug, create: :approve + plug :load_and_authorize_resource, model: Image, id_name: "image_id", persisted: true + + def create(conn, _params) do + image = conn.assigns.image + + {:ok, _comment} = Images.approve_image(image) + + conn + |> put_flash(:info, "Image has been approved.") + |> moderation_log(details: &log_details/3, data: image) + |> redirect(to: Routes.admin_approval_path(conn, :index)) + end + + defp log_details(conn, _action, image) do + %{body: "Approved image #{image.id}", subject_path: Routes.image_path(conn, :show, image)} + end +end diff --git a/lib/philomena_web/controllers/image/comment/approve_controller.ex b/lib/philomena_web/controllers/image/comment/approve_controller.ex new file mode 100644 index 00000000..699d0cc2 --- /dev/null +++ b/lib/philomena_web/controllers/image/comment/approve_controller.ex @@ -0,0 +1,35 @@ +defmodule PhilomenaWeb.Image.Comment.ApproveController do + use PhilomenaWeb, :controller + + alias Philomena.Comments.Comment + alias Philomena.Comments + alias Philomena.UserStatistics + + plug PhilomenaWeb.CanaryMapPlug, create: :approve + + plug :load_and_authorize_resource, + model: Comment, + id_name: "comment_id", + persisted: true, + preload: [:user] + + def create(conn, _params) do + comment = conn.assigns.comment + + {:ok, _comment} = Comments.approve_comment(comment, conn.assigns.current_user) + + UserStatistics.inc_stat(comment.user, :comments_posted) + + conn + |> put_flash(:info, "Comment has been approved.") + |> moderation_log(details: &log_details/3, data: comment) + |> redirect(to: Routes.image_path(conn, :show, comment.image_id) <> "#comment_#{comment.id}") + end + + defp log_details(conn, _action, comment) do + %{ + body: "Approved 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_controller.ex b/lib/philomena_web/controllers/image/comment_controller.ex index adff5887..1c97ad1f 100644 --- a/lib/philomena_web/controllers/image/comment_controller.ex +++ b/lib/philomena_web/controllers/image/comment_controller.ex @@ -78,10 +78,15 @@ 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) - UserStatistics.inc_stat(conn.assigns.current_user, :comments_posted) + + if comment.approved do + Comments.notify_comment(comment) + UserStatistics.inc_stat(conn.assigns.current_user, :comments_posted) + else + Comments.report_non_approved(comment) + end index(conn, %{"comment_id" => comment.id}) @@ -107,6 +112,10 @@ defmodule PhilomenaWeb.Image.CommentController do def update(conn, %{"comment" => comment_params}) do case Comments.update_comment(conn.assigns.comment, conn.assigns.current_user, comment_params) do {:ok, %{comment: comment}} -> + if not comment.approved do + Comments.report_non_approved(comment) + end + PhilomenaWeb.Endpoint.broadcast!( "firehose", "comment:update", diff --git a/lib/philomena_web/controllers/topic/post/approve_controller.ex b/lib/philomena_web/controllers/topic/post/approve_controller.ex new file mode 100644 index 00000000..90bf3948 --- /dev/null +++ b/lib/philomena_web/controllers/topic/post/approve_controller.ex @@ -0,0 +1,49 @@ +defmodule PhilomenaWeb.Topic.Post.ApproveController do + use PhilomenaWeb, :controller + + alias Philomena.Posts.Post + alias Philomena.Posts + + plug PhilomenaWeb.CanaryMapPlug, create: :approve + + plug :load_and_authorize_resource, + model: Post, + id_name: "post_id", + persisted: true, + preload: [:topic, :user, topic: :forum] + + def create(conn, _params) do + post = conn.assigns.post + user = conn.assigns.current_user + + case Posts.approve_post(post, user) do + {:ok, post} -> + conn + |> put_flash(:info, "Post successfully approved.") + |> 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) <> + "#post_#{post.id}" + ) + + {:error, _changeset} -> + conn + |> put_flash(:error, "Unable to approve post!") + |> redirect( + to: + Routes.forum_topic_path(conn, :show, post.topic.forum, post.topic, post_id: post.id) <> + "#post_#{post.id}" + ) + end + end + + defp log_details(conn, _action, post) do + %{ + body: "Approved 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_controller.ex b/lib/philomena_web/controllers/topic/post_controller.ex index 43478cbb..342ceddd 100644 --- a/lib/philomena_web/controllers/topic/post_controller.ex +++ b/lib/philomena_web/controllers/topic/post_controller.ex @@ -35,8 +35,12 @@ defmodule PhilomenaWeb.Topic.PostController do case Posts.create_post(topic, attributes, post_params) do {:ok, %{post: post}} -> - Posts.notify_post(post) - UserStatistics.inc_stat(conn.assigns.current_user, :forum_posts) + if post.approved do + Posts.notify_post(post) + UserStatistics.inc_stat(conn.assigns.current_user, :forum_posts) + else + Posts.report_non_approved(post) + end if forum.access_level == "normal" do PhilomenaWeb.Endpoint.broadcast!( @@ -75,7 +79,11 @@ defmodule PhilomenaWeb.Topic.PostController do user = conn.assigns.current_user case Posts.update_post(post, user, post_params) do - {:ok, _post} -> + {:ok, post} -> + if not post.approved do + Posts.report_non_approved(post) + end + conn |> put_flash(:info, "Post successfully edited.") |> redirect( 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..97c63fb4 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 ad7f6a24..c5d48d9d 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -177,7 +177,13 @@ 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 end @@ -186,6 +192,7 @@ defmodule PhilomenaWeb.Router do resources "/vote", Image.VoteController, only: [:create, :delete], singleton: true resources "/fave", Image.FaveController, only: [:create, :delete], singleton: true resources "/hide", Image.HideController, only: [:create, :delete], singleton: true + resources "/approve", Image.ApproveController, only: [:create], singleton: true resources "/subscription", Image.SubscriptionController, only: [:create, :delete], @@ -196,6 +203,7 @@ defmodule PhilomenaWeb.Router do resources "/comments", Image.CommentController, only: [:edit, :update] do resources "/hide", Image.Comment.HideController, only: [:create, :delete], singleton: true resources "/delete", Image.Comment.DeleteController, only: [:create], singleton: true + resources "/approve", Image.Comment.ApproveController, only: [:create], singleton: true end resources "/delete", Image.DeleteController, @@ -241,6 +249,7 @@ defmodule PhilomenaWeb.Router do resources "/posts", Topic.PostController, only: [:edit, :update] do resources "/hide", Topic.Post.HideController, only: [:create, :delete], singleton: true resources "/delete", Topic.Post.DeleteController, only: [:create], singleton: true + resources "/approve", Topic.Post.ApproveController, only: [:create], singleton: true end resources "/poll", Topic.PollController, only: [:edit, :update], singleton: true do @@ -335,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], @@ -379,6 +390,10 @@ defmodule PhilomenaWeb.Router do only: [:create, :delete], singleton: true + resources "/verification", User.VerificationController, + only: [:create, :delete], + singleton: true + resources "/unlock", User.UnlockController, only: [:create], singleton: true resources "/api_key", User.ApiKeyController, only: [:delete], singleton: true resources "/downvotes", User.DownvoteController, only: [:delete], singleton: true 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/comment/_comment.html.slime b/lib/philomena_web/templates/comment/_comment.html.slime index 5bc4b1ee..e79f3852 100644 --- a/lib/philomena_web/templates/comment/_comment.html.slime +++ b/lib/philomena_web/templates/comment/_comment.html.slime @@ -1,4 +1,26 @@ article.block.communication id="comment_#{@comment.id}" + = if not @comment.approved and not @comment.hidden_from_users and (can?(@conn, :hide, @comment) or @comment.user_id == @conn.assigns.current_user.id) do + .block__content + .block.block--fixed.block--danger + p + i.fas.fa-exclamation-triangle> + ' This comment is pending approval from a staff member. + = if can?(@conn, :approve, @comment) do + p + ul.horizontal-list + li + = link(to: Routes.image_comment_approve_path(@conn, :create, @comment.image_id, @comment), data: [confirm: "Are you sure?"], method: "post", class: "button") do + i.fas.fa-check> + ' Approve + li + a.button.togglable-delete-form-link href="#" data-click-toggle="#inline-reject-form-comment-#{@comment.id}" + i.fa.fa-times> + ' Reject + + = form_for :comment, Routes.image_comment_hide_path(@conn, :create, @comment.image_id, @comment), [class: "togglable-delete-form hidden flex", id: "inline-reject-form-comment-#{@comment.id}"], fn f -> + = text_input f, :deletion_reason, class: "input input--wide", placeholder: "Deletion Reason", id: "inline-reject-reason-comment-#{@comment.id}", required: true + = submit "Delete", class: "button" + .block__content.flex.flex--no-wrap class=communication_body_class(@comment) .flex__fixed.spacing-right = render PhilomenaWeb.UserAttributionView, "_anon_user_avatar.html", object: @comment, conn: @conn 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/duplicate_report/_list.html.slime b/lib/philomena_web/templates/duplicate_report/_list.html.slime index 263ec700..87de9fc0 100644 --- a/lib/philomena_web/templates/duplicate_report/_list.html.slime +++ b/lib/philomena_web/templates/duplicate_report/_list.html.slime @@ -118,10 +118,15 @@ td.danger Different rating tags tr - = if forward_merge?(report) do - td.warning Target newer - - else - td.success Target older + = cond do + - not source_approved?(report) -> + td.danger Source is not approved + - not target_approved?(report) -> + td.danger Target is not approved + - forward_merge?(report) -> + td.warning Target newer + - true -> + td.success Target older .flex.flex--column.grid--dupe-report-list__cell.border-vertical id="report_options_#{report.id}" .dr__status-options class=background_class 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/comment/index.html.slime b/lib/philomena_web/templates/image/comment/index.html.slime index 75d5e0e3..e0c3eee6 100644 --- a/lib/philomena_web/templates/image/comment/index.html.slime +++ b/lib/philomena_web/templates/image/comment/index.html.slime @@ -14,7 +14,7 @@ elixir: i.fa.fa-sync span.hide-mobile<> Refresh -= for {comment, body} <- @comments, not comment.destroyed_content or (can?(@conn, :show, comment) and not hide_staff_tools?(@conn)) do += for {comment, body} <- @comments, can_view_communication?(@conn, comment) do = render PhilomenaWeb.CommentView, "_comment.html", comment: comment, body: body, conn: @conn .block diff --git a/lib/philomena_web/templates/image/comment/show.html.slime b/lib/philomena_web/templates/image/comment/show.html.slime index 9497f8db..ead59411 100644 --- a/lib/philomena_web/templates/image/comment/show.html.slime +++ b/lib/philomena_web/templates/image/comment/show.html.slime @@ -1 +1 @@ -= render PhilomenaWeb.CommentView, "_comment.html", comment: @comment, body: @body, conn: @conn \ No newline at end of file += render PhilomenaWeb.CommentView, "_comment.html", comment: @comment, body: @body, conn: @conn 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/post/_post.html.slime b/lib/philomena_web/templates/post/_post.html.slime index 1293fa1d..64712c84 100644 --- a/lib/philomena_web/templates/post/_post.html.slime +++ b/lib/philomena_web/templates/post/_post.html.slime @@ -1,4 +1,26 @@ article.block.communication id="post_#{@post.id}" + = if not @post.approved and not @post.hidden_from_users and (can?(@conn, :hide, @post) or @post.user_id == @conn.assigns.current_user.id) do + .block__content + .block.block--fixed.block--danger + p + i.fas.fa-exclamation-triangle> + ' This post is pending approval from a staff member. + = if can?(@conn, :approve, @post) do + p + ul.horizontal-list + li + = link(to: Routes.forum_topic_post_approve_path(@conn, :create, @post.topic.forum, @post.topic, @post), data: [confirm: "Are you sure?"], method: "post", class: "button") do + i.fas.fa-check> + ' Approve + li + a.button.togglable-delete-form-link href="#" data-click-toggle="#inline-reject-form-post-#{@post.id}" + i.fa.fa-times> + ' Reject + + = form_for :post, Routes.forum_topic_post_hide_path(@conn, :create, @post.topic.forum, @post.topic, @post), [class: "togglable-delete-form hidden flex", id: "inline-reject-form-post-#{@post.id}"], fn f -> + = text_input f, :deletion_reason, class: "input input--wide", placeholder: "Deletion Reason", id: "inline-reject-reason-post-#{@post.id}", required: true + = submit "Delete", class: "button" + .block__content.flex.flex--no-wrap class=communication_body_class(@post) .flex__fixed.spacing-right = render PhilomenaWeb.UserAttributionView, "_anon_user_avatar.html", object: @post, conn: @conn diff --git a/lib/philomena_web/templates/profile/_admin_block.html.slime b/lib/philomena_web/templates/profile/_admin_block.html.slime index 81ddaf04..fdb3bd53 100644 --- a/lib/philomena_web/templates/profile/_admin_block.html.slime +++ b/lib/philomena_web/templates/profile/_admin_block.html.slime @@ -153,8 +153,19 @@ a.label.label--primary.label--block href="#" data-click-toggle=".js-admin__optio i.fa.fa-fw.fa-ban span.admin__button Ban this sucker + ul.profile-admin__options__column = if can?(@conn, :index, Philomena.Users.User) do li = link to: Routes.admin_user_api_key_path(@conn, :delete, @user), data: [confirm: "Are you really, really sure?", method: "delete"] do i.fas.fa-fw.fa-key span.admin__button Reset API key + + li + = if @user.verified do + = link to: Routes.admin_user_verification_path(@conn, :delete, @user), data: [confirm: "Are you really, really sure?", method: "delete"] do + i.fas.fa-fw.fa-user-times + span.admin__button Revoke Verification + - else + = link to: Routes.admin_user_verification_path(@conn, :create, @user), data: [confirm: "Are you really, really sure?", method: "create"] do + i.fas.fa-fw.fa-user-check + span.admin__button Grant Verification 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/templates/topic/show.html.slime b/lib/philomena_web/templates/topic/show.html.slime index 058a6592..ff54dffd 100644 --- a/lib/philomena_web/templates/topic/show.html.slime +++ b/lib/philomena_web/templates/topic/show.html.slime @@ -59,7 +59,7 @@ h1 = @topic.title / The actual posts .posts-area .post-list - = for {post, body} <- @posts, (!post.destroyed_content or can?(@conn, :hide, post)) do + = for {post, body} <- @posts, can_view_communication?(@conn, post) do = render PhilomenaWeb.PostView, "_post.html", conn: @conn, post: post, body: body = if @conn.assigns.advert do 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/lib/philomena_web/views/admin/user_view.ex b/lib/philomena_web/views/admin/user_view.ex index db9e189f..19f33b48 100644 --- a/lib/philomena_web/views/admin/user_view.ex +++ b/lib/philomena_web/views/admin/user_view.ex @@ -56,6 +56,7 @@ defmodule PhilomenaWeb.Admin.UserView do def description("admin", "Badge"), do: "Manage badges" def description("admin", "Advert"), do: "Manage ads" def description("admin", "StaticPage"), do: "Manage static pages" + def description("admin", "Image"), do: "Hard-delete images" def description(_name, _resource_type), do: "(unknown permission)" @@ -90,7 +91,8 @@ defmodule PhilomenaWeb.Admin.UserView do ["admin", "SiteNotice"], ["admin", "Badge"], ["admin", "Advert"], - ["admin", "StaticPage"] + ["admin", "StaticPage"], + ["admin", "Image"] ] end end diff --git a/lib/philomena_web/views/app_view.ex b/lib/philomena_web/views/app_view.ex index cdf1e5ec..5c5fb83c 100644 --- a/lib/philomena_web/views/app_view.ex +++ b/lib/philomena_web/views/app_view.ex @@ -162,6 +162,21 @@ defmodule PhilomenaWeb.AppView do def communication_body_class(%{destroyed_content: true}), do: "communication--destroyed" def communication_body_class(_communication), do: nil + def can_view_communication?(conn, communication) do + user_id = + case conn.assigns.current_user do + nil -> -1 + user -> user.id + end + + cond do + can?(conn, :hide, communication) and not hide_staff_tools?(conn) -> true + communication.destroyed_content -> false + not communication.approved and communication.user_id != user_id -> false + true -> true + end + end + def hide_staff_tools?(conn), do: conn.cookies["hide_staff_tools"] == "true" diff --git a/lib/philomena_web/views/duplicate_report_view.ex b/lib/philomena_web/views/duplicate_report_view.ex index c3b22ccf..34192974 100644 --- a/lib/philomena_web/views/duplicate_report_view.ex +++ b/lib/philomena_web/views/duplicate_report_view.ex @@ -104,7 +104,16 @@ defmodule PhilomenaWeb.DuplicateReportView do def mergeable?(%{image: image, duplicate_of_image: duplicate_of_image} = report) do same_rating_tags?(report) and not image.hidden_from_users and - not duplicate_of_image.hidden_from_users + not duplicate_of_image.hidden_from_users and image.approved and + duplicate_of_image.approved + end + + def source_approved?(%{image: image}) do + image.approved + end + + def target_approved?(%{duplicate_of_image: image}) do + image.approved end defp artist_tags(%{tags: tags}) do diff --git a/priv/repo/migrations/20220321173359_add_approval_queue.exs b/priv/repo/migrations/20220321173359_add_approval_queue.exs new file mode 100644 index 00000000..98023598 --- /dev/null +++ b/priv/repo/migrations/20220321173359_add_approval_queue.exs @@ -0,0 +1,41 @@ +defmodule Philomena.Repo.Migrations.AddApprovalQueue do + use Ecto.Migration + + def change do + alter table("reports") do + add :system, :boolean, default: false + end + + alter table("images") do + add :approved, :boolean, default: false + end + + alter table("comments") do + add :approved, :boolean, default: false + end + + alter table("posts") do + add :approved, :boolean, default: false + end + + 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") + + execute("update images set approved = true;") + execute("update posts set approved = true;") + execute("update comments set approved = true;") + execute("update messages set approved = true;") + execute("update users set verified = true where created_at < '2022-03-01';") + end +end diff --git a/priv/repo/seeds.json b/priv/repo/seeds.json index 80c945ec..3c5f06d9 100644 --- a/priv/repo/seeds.json +++ b/priv/repo/seeds.json @@ -89,7 +89,8 @@ {"name": "batch_update", "resource_type": "Tag"}, {"name": "moderator", "resource_type": "Topic"}, {"name": "admin", "resource_type": "Advert"}, - {"name": "admin", "resource_type": "StaticPage"} + {"name": "admin", "resource_type": "StaticPage"}, + {"name": "admin", "resource_type": "Image"} ], "pages": [] } diff --git a/priv/repo/seeds_development.exs b/priv/repo/seeds_development.exs index 74f71aa7..466b3111 100644 --- a/priv/repo/seeds_development.exs +++ b/priv/repo/seeds_development.exs @@ -70,6 +70,7 @@ for image_def <- resources["remote_images"] do ) |> case do {:ok, %{image: image}} -> + Images.approve_image(image) Images.reindex_image(image) Tags.reindex_tags(image.added_tags) @@ -91,6 +92,7 @@ for comment_body <- resources["comments"] do ) |> case do {:ok, %{comment: comment}} -> + Comments.approve_comment(comment, pleb) Comments.reindex_comment(comment) Images.reindex_image(image) @@ -126,6 +128,7 @@ for %{"forum" => forum_name, "topics" => topics} <- resources["forum_posts"] do ) |> case do {:ok, %{post: post}} -> + Posts.approve_post(post, pleb) Posts.reindex_post(post) {:error, :post, changeset, _so_far} -> diff --git a/priv/repo/structure.sql b/priv/repo/structure.sql index 0a05672e..1a412576 100644 --- a/priv/repo/structure.sql +++ b/priv/repo/structure.sql @@ -282,7 +282,8 @@ CREATE TABLE public.comments ( deletion_reason character varying DEFAULT ''::character varying NOT NULL, destroyed_content boolean DEFAULT false, name_at_post_time character varying, - body character varying NOT NULL + body character varying NOT NULL, + approved boolean DEFAULT false ); @@ -971,7 +972,8 @@ CREATE TABLE public.images ( hides_count integer DEFAULT 0 NOT NULL, image_duration double precision, description character varying DEFAULT ''::character varying NOT NULL, - scratchpad character varying + scratchpad character varying, + approved boolean DEFAULT false ); @@ -1005,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 ); @@ -1258,7 +1261,8 @@ CREATE TABLE public.posts ( deletion_reason character varying DEFAULT ''::character varying NOT NULL, destroyed_content boolean DEFAULT false NOT NULL, name_at_post_time character varying, - body character varying NOT NULL + body character varying NOT NULL, + approved boolean DEFAULT false ); @@ -1300,7 +1304,8 @@ CREATE TABLE public.reports ( admin_id integer, reportable_id integer NOT NULL, reportable_type character varying NOT NULL, - reason character varying NOT NULL + reason character varying NOT NULL, + system boolean DEFAULT false ); @@ -2050,7 +2055,8 @@ CREATE TABLE public.users ( description character varying, scratchpad character varying, bypass_rate_limits boolean DEFAULT false, - scale_large_images character varying(255) DEFAULT 'true'::character varying NOT NULL + scale_large_images character varying(255) DEFAULT 'true'::character varying NOT NULL, + verified boolean DEFAULT false ); @@ -2897,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: - -- @@ -4094,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: - -- @@ -4970,3 +4990,4 @@ INSERT INTO public."schema_migrations" (version) VALUES (20210921025336); INSERT INTO public."schema_migrations" (version) VALUES (20210929181319); INSERT INTO public."schema_migrations" (version) VALUES (20211107130226); INSERT INTO public."schema_migrations" (version) VALUES (20211219194836); +INSERT INTO public."schema_migrations" (version) VALUES (20220321173359);