Merge pull request #161 from philomena-dev/approval

Approval Queue
This commit is contained in:
Meow 2022-03-25 08:56:23 +01:00 committed by GitHub
commit b6248e22ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 1031 additions and 44 deletions

View file

@ -470,6 +470,7 @@ span.stat {
@import "text";
@import "~views/adverts";
@import "~views/approval";
@import "~views/badges";
@import "~views/channels";
@import "~views/comments";

View file

@ -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;
}
}

28
assets/js/pmwarning.js Normal file
View file

@ -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 };

View file

@ -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();
});

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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])

View file

@ -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

View file

@ -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(%{})

View file

@ -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

View file

@ -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)

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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))

View file

@ -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

View file

@ -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

View file

@ -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",

View file

@ -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

View file

@ -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(

View file

@ -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: []

View file

@ -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

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -5,6 +5,21 @@ h1 New Conversation
' &raquo;
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

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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?"]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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": []
}

View file

@ -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} ->

View file

@ -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);