post and comment approval

This commit is contained in:
Luna D 2022-03-22 22:23:30 +01:00 committed by Luna D
parent a3db6f6eed
commit eeb2f851e0
No known key found for this signature in database
GPG key ID: 4B1C63448394F688
32 changed files with 484 additions and 28 deletions

View file

@ -197,6 +197,40 @@ defmodule Philomena.Comments do
|> Repo.update() |> Repo.update()
end 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}}} ->
Reports.reindex_reports(reports)
reindex_comment(comment)
{:ok, comment}
error ->
error
end
end
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 def migrate_comments(image, duplicate_of_image) do
{count, nil} = {count, nil} =
Comment Comment

View file

@ -4,6 +4,7 @@ defmodule Philomena.Comments.Comment do
alias Philomena.Images.Image alias Philomena.Images.Image
alias Philomena.Users.User alias Philomena.Users.User
alias Philomena.Schema.Approval
schema "comments" do schema "comments" do
belongs_to :user, User belongs_to :user, User
@ -22,7 +23,7 @@ defmodule Philomena.Comments.Comment do
field :deletion_reason, :string, default: "" field :deletion_reason, :string, default: ""
field :destroyed_content, :boolean, default: false field :destroyed_content, :boolean, default: false
field :name_at_post_time, :string field :name_at_post_time, :string
field :approved_at, :utc_datetime field :approved, :boolean
timestamps(inserted_at: :created_at, type: :utc_datetime) timestamps(inserted_at: :created_at, type: :utc_datetime)
end end
@ -35,6 +36,8 @@ defmodule Philomena.Comments.Comment do
|> validate_length(:body, min: 1, max: 300_000, count: :bytes) |> validate_length(:body, min: 1, max: 300_000, count: :bytes)
|> change(attribution) |> change(attribution)
|> put_name_at_post_time(attribution[:user]) |> put_name_at_post_time(attribution[:user])
|> Approval.maybe_put_approval(attribution[:user])
|> Approval.maybe_strip_images(attribution[:user])
end end
def changeset(comment, attrs, edited_at \\ nil) do def changeset(comment, attrs, edited_at \\ nil) do
@ -44,6 +47,7 @@ defmodule Philomena.Comments.Comment do
|> validate_required([:body]) |> validate_required([:body])
|> validate_length(:body, min: 1, max: 300_000, count: :bytes) |> validate_length(:body, min: 1, max: 300_000, count: :bytes)
|> validate_length(:edit_reason, max: 70, count: :bytes) |> validate_length(:edit_reason, max: 70, count: :bytes)
|> Approval.maybe_put_approval(comment.user)
end end
def hide_changeset(comment, attrs, user) do def hide_changeset(comment, attrs, user) do
@ -66,6 +70,11 @@ defmodule Philomena.Comments.Comment do
|> put_change(:body, "") |> put_change(:body, "")
end 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, nil), do: changeset
defp put_name_at_post_time(changeset, user), do: change(changeset, name_at_post_time: user.name) defp put_name_at_post_time(changeset, user), do: change(changeset, name_at_post_time: user.name)
end end

View file

@ -30,7 +30,8 @@ defmodule Philomena.Comments.ElasticsearchIndex do
anonymous: %{type: "keyword"}, anonymous: %{type: "keyword"},
# boolean # boolean
hidden_from_users: %{type: "keyword"}, 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), image_tag_ids: comment.image.tags |> Enum.map(& &1.id),
anonymous: comment.anonymous, anonymous: comment.anonymous,
hidden_from_users: comment.image.hidden_from_users || comment.hidden_from_users, 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 end

View file

@ -4,6 +4,7 @@ defmodule Philomena.Conversations.Message do
alias Philomena.Conversations.Conversation alias Philomena.Conversations.Conversation
alias Philomena.Users.User alias Philomena.Users.User
alias Philomena.Schema.Approval
schema "messages" do schema "messages" do
belongs_to :conversation, Conversation belongs_to :conversation, Conversation
@ -28,5 +29,6 @@ defmodule Philomena.Conversations.Message do
|> validate_required([:body]) |> validate_required([:body])
|> put_assoc(:from, user) |> put_assoc(:from, user)
|> validate_length(:body, max: 300_000, count: :bytes) |> validate_length(:body, max: 300_000, count: :bytes)
|> Approval.maybe_put_approval(user)
end end
end end

View file

@ -86,7 +86,8 @@ defmodule Philomena.Images.ElasticsearchIndex do
name_in_namespace: %{type: "keyword"}, name_in_namespace: %{type: "keyword"},
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)), hidden_by_users: image.hiders |> Enum.map(&String.downcase(&1.name)),
upvoters: image.upvoters |> Enum.map(&String.downcase(&1.name)), upvoters: image.upvoters |> Enum.map(&String.downcase(&1.name)),
downvoters: image.downvoters |> 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 end

View file

@ -82,7 +82,7 @@ defmodule Philomena.Images.Image do
field :hidden_image_key, :string field :hidden_image_key, :string
field :scratchpad, :string field :scratchpad, :string
field :hides_count, :integer, default: 0 field :hides_count, :integer, default: 0
field :approved_at, :utc_datetime field :approved, :boolean
# todo: can probably remove these now # todo: can probably remove these now
field :tag_list_cache, :string field :tag_list_cache, :string

View file

@ -117,6 +117,15 @@ defmodule Philomena.Posts do
Exq.enqueue(Exq, "notifications", NotificationWorker, ["Posts", post.id]) Exq.enqueue(Exq, "notifications", NotificationWorker, ["Posts", post.id])
end end
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 def perform_notify(post_id) do
post = get_post!(post_id) post = get_post!(post_id)
@ -237,6 +246,31 @@ defmodule Philomena.Posts do
|> reindex_after_update() |> reindex_after_update()
end 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}}} ->
Reports.reindex_reports(reports)
reindex_post(post)
{:ok, post}
error ->
error
end
end
@doc """ @doc """
Returns an `%Ecto.Changeset{}` for tracking post changes. Returns an `%Ecto.Changeset{}` for tracking post changes.

View file

@ -36,7 +36,8 @@ defmodule Philomena.Posts.ElasticsearchIndex do
created_at: %{type: "date"}, created_at: %{type: "date"},
deleted: %{type: "boolean"}, deleted: %{type: "boolean"},
access_level: %{type: "keyword"}, 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, updated_at: post.updated_at,
deleted: post.hidden_from_users, deleted: post.hidden_from_users,
access_level: post.topic.forum.access_level, access_level: post.topic.forum.access_level,
destroyed_content: post.destroyed_content destroyed_content: post.destroyed_content,
approved: post.topic.approved && post.approved
} }
end end

View file

@ -4,6 +4,7 @@ defmodule Philomena.Posts.Post do
alias Philomena.Users.User alias Philomena.Users.User
alias Philomena.Topics.Topic alias Philomena.Topics.Topic
alias Philomena.Schema.Approval
schema "posts" do schema "posts" do
belongs_to :user, User belongs_to :user, User
@ -23,7 +24,7 @@ defmodule Philomena.Posts.Post do
field :deletion_reason, :string, default: "" field :deletion_reason, :string, default: ""
field :destroyed_content, :boolean, default: false field :destroyed_content, :boolean, default: false
field :name_at_post_time, :string field :name_at_post_time, :string
field :approved_at, :utc_datetime field :approved, :boolean, default: false
timestamps(inserted_at: :created_at, type: :utc_datetime) timestamps(inserted_at: :created_at, type: :utc_datetime)
end end
@ -36,6 +37,7 @@ defmodule Philomena.Posts.Post do
|> validate_required([:body]) |> validate_required([:body])
|> validate_length(:body, min: 1, max: 300_000, count: :bytes) |> validate_length(:body, min: 1, max: 300_000, count: :bytes)
|> validate_length(:edit_reason, max: 70, count: :bytes) |> validate_length(:edit_reason, max: 70, count: :bytes)
|> Approval.maybe_put_approval(post.user)
end end
@doc false @doc false
@ -46,6 +48,8 @@ defmodule Philomena.Posts.Post do
|> validate_length(:body, min: 1, max: 300_000, count: :bytes) |> validate_length(:body, min: 1, max: 300_000, count: :bytes)
|> change(attribution) |> change(attribution)
|> put_name_at_post_time(attribution[:user]) |> put_name_at_post_time(attribution[:user])
|> Approval.maybe_put_approval(attribution[:user])
|> Approval.maybe_strip_images(attribution[:user])
end end
@doc false @doc false
@ -58,6 +62,8 @@ defmodule Philomena.Posts.Post do
|> change(attribution) |> change(attribution)
|> change(topic_position: 0) |> change(topic_position: 0)
|> put_name_at_post_time(attribution[:user]) |> put_name_at_post_time(attribution[:user])
|> Approval.maybe_put_approval(attribution[:user])
|> Approval.maybe_strip_images(attribution[:user])
end end
def hide_changeset(post, attrs, user) do def hide_changeset(post, attrs, user) do
@ -80,6 +86,11 @@ defmodule Philomena.Posts.Post do
|> put_change(:body, "") |> put_change(:body, "")
end 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, nil), do: changeset
defp put_name_at_post_time(changeset, user), do: change(changeset, name_at_post_time: user.name) defp put_name_at_post_time(changeset, user), do: change(changeset, name_at_post_time: user.name)
end end

View file

@ -60,6 +60,26 @@ defmodule Philomena.Reports do
|> reindex_after_update() |> reindex_after_update()
end 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 """ @doc """
Updates a report. Updates a report.

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

@ -31,7 +31,7 @@ defmodule Philomena.Topics.Topic do
field :slug, :string field :slug, :string
field :anonymous, :boolean, default: false field :anonymous, :boolean, default: false
field :hidden_from_users, :boolean, default: false field :hidden_from_users, :boolean, default: false
field :approved_at, :utc_datetime field :approved, :boolean
timestamps(inserted_at: :created_at, type: :utc_datetime) timestamps(inserted_at: :created_at, type: :utc_datetime)
end end

View file

@ -45,8 +45,14 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do
# View filters # View filters
def can?(%User{role: "moderator"}, :show, %Filter{}), do: true 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 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
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 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"}, :show, %Conversation{}), do: true
def can?(%User{role: "moderator"}, :approve, %Conversation{}), do: true
# View IP addresses and fingerprints # View IP addresses and fingerprints
def can?(%User{role: "moderator"}, :show, :ip_address), do: true 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"}, :edit, %Post{}), do: true
def can?(%User{role: "moderator"}, :hide, %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"}, :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"}, :edit, %Comment{}), do: true
def can?(%User{role: "moderator"}, :hide, %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"}, :delete, %Comment{}), do: true
def can?(%User{role: "moderator"}, :approve, %Comment{}), do: true
# Show the DNP list # Show the DNP list
def can?(%User{role: "moderator"}, _action, DnpEntry), do: true def can?(%User{role: "moderator"}, _action, DnpEntry), do: true
@ -198,6 +207,13 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do
), ),
do: true do: true
def can?(
%User{role: "assistant", role_map: %{"Image" => "moderator"}},
:approve,
%Image{}
),
do: true
# Dupe assistant actions # Dupe assistant actions
def can?( def can?(
%User{role: "assistant", role_map: %{"DuplicateReport" => "moderator"}}, %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{}), def can?(%User{role: "assistant", role_map: %{"Comment" => "moderator"}}, :hide, %Comment{}),
do: true do: true
def can?(%User{role: "assistant", role_map: %{"Comment" => "moderator"}}, :approve, %Comment{}),
do: true
# Topic assistant actions # Topic assistant actions
def can?(%User{role: "assistant", role_map: %{"Topic" => "moderator"}}, :show, %Topic{}), def can?(%User{role: "assistant", role_map: %{"Topic" => "moderator"}}, :show, %Topic{}),
do: true do: true
@ -254,6 +273,9 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do
def can?(%User{role: "assistant", role_map: %{"Topic" => "moderator"}}, :hide, %Topic{}), def can?(%User{role: "assistant", role_map: %{"Topic" => "moderator"}}, :hide, %Topic{}),
do: true do: true
def can?(%User{role: "assistant", role_map: %{"Topic" => "moderator"}}, :approve, %Topic{}),
do: true
def can?(%User{role: "assistant", role_map: %{"Topic" => "moderator"}}, :show, %Post{}), def can?(%User{role: "assistant", role_map: %{"Topic" => "moderator"}}, :show, %Post{}),
do: true do: true
@ -263,6 +285,9 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do
def can?(%User{role: "assistant", role_map: %{"Topic" => "moderator"}}, :hide, %Post{}), def can?(%User{role: "assistant", role_map: %{"Topic" => "moderator"}}, :hide, %Post{}),
do: true do: true
def can?(%User{role: "assistant", role_map: %{"Topic" => "moderator"}}, :approve, %Post{}),
do: true
# Tag assistant actions # Tag assistant actions
def can?(%User{role: "assistant", role_map: %{"Tag" => "moderator"}}, :edit, %Tag{}), do: true def can?(%User{role: "assistant", role_map: %{"Tag" => "moderator"}}, :edit, %Tag{}), do: true

View file

@ -0,0 +1,33 @@
defmodule PhilomenaWeb.Conversation.ApproveController do
use PhilomenaWeb, :controller
alias Philomena.Conversations.Conversation
alias Philomena.Conversations
plug PhilomenaWeb.CanaryMapPlug, create: :approve
plug :load_and_authorize_resource,
model: Conversation,
id_field: "slug",
id_name: "conversation_id",
persisted: true
def create(conn, _params) do
message = conn.assigns.message
user = conn.assigns.current_user
{:ok, _message} = Conversations.approve_conversation_message(message)
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

@ -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.image_path(conn, :show, image))
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

@ -81,7 +81,12 @@ defmodule PhilomenaWeb.Image.CommentController do
Comments.notify_comment(comment) Comments.notify_comment(comment)
Comments.reindex_comment(comment) Comments.reindex_comment(comment)
Images.reindex_image(conn.assigns.image) Images.reindex_image(conn.assigns.image)
UserStatistics.inc_stat(conn.assigns.current_user, :comments_posted)
if comment.approved do
UserStatistics.inc_stat(conn.assigns.current_user, :comments_posted)
else
Comments.report_non_approved(comment)
end
index(conn, %{"comment_id" => comment.id}) index(conn, %{"comment_id" => comment.id})

View file

@ -0,0 +1,45 @@
defmodule PhilomenaWeb.Topic.ApproveController do
import Plug.Conn
use PhilomenaWeb, :controller
alias Philomena.Forums.Forum
alias Philomena.Topics.Topic
alias Philomena.Topics
plug PhilomenaWeb.CanaryMapPlug, create: :show
plug :load_and_authorize_resource,
model: Forum,
id_name: "forum_id",
id_field: "short_name",
persisted: true
plug PhilomenaWeb.LoadTopicPlug
plug PhilomenaWeb.CanaryMapPlug, create: :approve
plug :authorize_resource, model: Topic, persisted: true
def create(conn, _params) do
topic = conn.assigns.topic
user = conn.assigns.current_user
case Topics.approve_topic(topic) do
{:ok, topic} ->
conn
|> put_flash(:info, "Topic successfully approved!")
|> moderation_log(details: &log_details/3, data: topic)
|> redirect(to: Routes.forum_topic_path(conn, :show, topic.forum, topic))
{:error, _changeset} ->
conn
|> put_flash(:error, "Unable to approve the topic!")
|> redirect(to: Routes.forum_topic_path(conn, :show, topic.forum, topic))
end
end
defp log_details(conn, action, topic) do
%{
body: "Approved topic '#{topic.title}' in #{topic.forum.name}",
subject_path: Routes.forum_topic_path(conn, :show, topic.forum, topic)
}
end
end

View file

@ -0,0 +1,51 @@
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} ->
UserStatistics.inc_stat(post.user(:forum_posts))
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

@ -36,7 +36,12 @@ defmodule PhilomenaWeb.Topic.PostController do
case Posts.create_post(topic, attributes, post_params) do case Posts.create_post(topic, attributes, post_params) do
{:ok, %{post: post}} -> {:ok, %{post: post}} ->
Posts.notify_post(post) Posts.notify_post(post)
UserStatistics.inc_stat(conn.assigns.current_user, :forum_posts)
if post.approved do
UserStatistics.inc_stat(conn.assigns.current_user, :forum_posts)
else
Posts.report_non_approved(post)
end
if forum.access_level == "normal" do if forum.access_level == "normal" do
PhilomenaWeb.Endpoint.broadcast!( PhilomenaWeb.Endpoint.broadcast!(

View file

@ -180,12 +180,14 @@ defmodule PhilomenaWeb.Router do
resources "/messages", Conversation.MessageController, only: [:create] resources "/messages", Conversation.MessageController, only: [:create]
resources "/read", Conversation.ReadController, only: [:create, :delete], singleton: true resources "/read", Conversation.ReadController, only: [:create, :delete], singleton: true
resources "/hide", Conversation.HideController, only: [:create, :delete], singleton: true resources "/hide", Conversation.HideController, only: [:create, :delete], singleton: true
resources "/approve", Conversation.ApproveController, only: [:create], singleton: true
end end
resources "/images", ImageController, only: [] do resources "/images", ImageController, only: [] do
resources "/vote", Image.VoteController, only: [:create, :delete], singleton: true resources "/vote", Image.VoteController, only: [:create, :delete], singleton: true
resources "/fave", Image.FaveController, only: [:create, :delete], singleton: true resources "/fave", Image.FaveController, only: [:create, :delete], singleton: true
resources "/hide", Image.HideController, 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, resources "/subscription", Image.SubscriptionController,
only: [:create, :delete], only: [:create, :delete],
@ -196,6 +198,7 @@ defmodule PhilomenaWeb.Router do
resources "/comments", Image.CommentController, only: [:edit, :update] do resources "/comments", Image.CommentController, only: [:edit, :update] do
resources "/hide", Image.Comment.HideController, only: [:create, :delete], singleton: true resources "/hide", Image.Comment.HideController, only: [:create, :delete], singleton: true
resources "/delete", Image.Comment.DeleteController, only: [:create], singleton: true resources "/delete", Image.Comment.DeleteController, only: [:create], singleton: true
resources "/approve", Image.Comment.ApproveController, only: [:create], singleton: true
end end
resources "/delete", Image.DeleteController, resources "/delete", Image.DeleteController,
@ -237,10 +240,12 @@ defmodule PhilomenaWeb.Router do
resources "/stick", Topic.StickController, only: [:create, :delete], singleton: true resources "/stick", Topic.StickController, only: [:create, :delete], singleton: true
resources "/lock", Topic.LockController, only: [:create, :delete], singleton: true resources "/lock", Topic.LockController, only: [:create, :delete], singleton: true
resources "/hide", Topic.HideController, only: [:create, :delete], singleton: true resources "/hide", Topic.HideController, only: [:create, :delete], singleton: true
resources "/approve", Topic.ApproveController, only: [:create], singleton: true
resources "/posts", Topic.PostController, only: [:edit, :update] do resources "/posts", Topic.PostController, only: [:edit, :update] do
resources "/hide", Topic.Post.HideController, only: [:create, :delete], singleton: true resources "/hide", Topic.Post.HideController, only: [:create, :delete], singleton: true
resources "/delete", Topic.Post.DeleteController, only: [:create], singleton: true resources "/delete", Topic.Post.DeleteController, only: [:create], singleton: true
resources "/approve", Topic.Post.ApproveController, only: [:create], singleton: true
end end
resources "/poll", Topic.PollController, only: [:edit, :update], singleton: true do resources "/poll", Topic.PollController, only: [:edit, :update], singleton: true do
@ -379,7 +384,9 @@ defmodule PhilomenaWeb.Router do
only: [:create, :delete], only: [:create, :delete],
singleton: true singleton: true
resources "/verification", User.VerificationController, only: [:create, :delete], singleton: true resources "/verification", User.VerificationController,
only: [:create, :delete],
singleton: true
resources "/unlock", User.UnlockController, only: [:create], singleton: true resources "/unlock", User.UnlockController, only: [:create], singleton: true
resources "/api_key", User.ApiKeyController, only: [:delete], singleton: true resources "/api_key", User.ApiKeyController, only: [:delete], singleton: true

View file

@ -1,4 +1,26 @@
article.block.communication id="comment_#{@comment.id}" 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) .block__content.flex.flex--no-wrap class=communication_body_class(@comment)
.flex__fixed.spacing-right .flex__fixed.spacing-right
= render PhilomenaWeb.UserAttributionView, "_anon_user_avatar.html", object: @comment, conn: @conn = render PhilomenaWeb.UserAttributionView, "_anon_user_avatar.html", object: @comment, conn: @conn

View file

@ -14,7 +14,7 @@ elixir:
i.fa.fa-sync i.fa.fa-sync
span.hide-mobile<> Refresh 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 = render PhilomenaWeb.CommentView, "_comment.html", comment: comment, body: body, conn: @conn
.block .block

View file

@ -1,4 +1,26 @@
article.block.communication id="post_#{@post.id}" 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) .block__content.flex.flex--no-wrap class=communication_body_class(@post)
.flex__fixed.spacing-right .flex__fixed.spacing-right
= render PhilomenaWeb.UserAttributionView, "_anon_user_avatar.html", object: @post, conn: @conn = render PhilomenaWeb.UserAttributionView, "_anon_user_avatar.html", object: @post, conn: @conn

View file

@ -59,7 +59,7 @@ h1 = @topic.title
/ The actual posts / The actual posts
.posts-area .posts-area
.post-list .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 = render PhilomenaWeb.PostView, "_post.html", conn: @conn, post: post, body: body
= if @conn.assigns.advert do = if @conn.assigns.advert do

View file

@ -56,6 +56,7 @@ defmodule PhilomenaWeb.Admin.UserView do
def description("admin", "Badge"), do: "Manage badges" def description("admin", "Badge"), do: "Manage badges"
def description("admin", "Advert"), do: "Manage ads" def description("admin", "Advert"), do: "Manage ads"
def description("admin", "StaticPage"), do: "Manage static pages" def description("admin", "StaticPage"), do: "Manage static pages"
def description("admin", "Image"), do: "Hard-delete images"
def description(_name, _resource_type), do: "(unknown permission)" def description(_name, _resource_type), do: "(unknown permission)"
@ -90,7 +91,8 @@ defmodule PhilomenaWeb.Admin.UserView do
["admin", "SiteNotice"], ["admin", "SiteNotice"],
["admin", "Badge"], ["admin", "Badge"],
["admin", "Advert"], ["admin", "Advert"],
["admin", "StaticPage"] ["admin", "StaticPage"],
["admin", "Image"]
] ]
end end
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(%{destroyed_content: true}), do: "communication--destroyed"
def communication_body_class(_communication), do: nil 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), def hide_staff_tools?(conn),
do: conn.cookies["hide_staff_tools"] == "true" do: conn.cookies["hide_staff_tools"] == "true"

View file

@ -7,19 +7,19 @@ defmodule Philomena.Repo.Migrations.AddApprovalQueue do
end end
alter table("images") do alter table("images") do
add :approved_at, :utc_datetime add :approved, :boolean, default: false
end end
alter table("comments") do alter table("comments") do
add :approved_at, :utc_datetime add :approved, :boolean, default: false
end end
alter table("posts") do alter table("posts") do
add :approved_at, :utc_datetime add :approved, :boolean, default: false
end end
alter table("topics") do alter table("topics") do
add :approved_at, :utc_datetime add :approved, :boolean, default: false
end end
alter table("users") do alter table("users") do

View file

@ -89,7 +89,8 @@
{"name": "batch_update", "resource_type": "Tag"}, {"name": "batch_update", "resource_type": "Tag"},
{"name": "moderator", "resource_type": "Topic"}, {"name": "moderator", "resource_type": "Topic"},
{"name": "admin", "resource_type": "Advert"}, {"name": "admin", "resource_type": "Advert"},
{"name": "admin", "resource_type": "StaticPage"} {"name": "admin", "resource_type": "StaticPage"},
{"name": "admin", "resource_type": "Image"}
], ],
"pages": [] "pages": []
} }

View file

@ -70,6 +70,7 @@ for image_def <- resources["remote_images"] do
) )
|> case do |> case do
{:ok, %{image: image}} -> {:ok, %{image: image}} ->
Images.approve_image(image)
Images.reindex_image(image) Images.reindex_image(image)
Tags.reindex_tags(image.added_tags) Tags.reindex_tags(image.added_tags)
@ -91,6 +92,7 @@ for comment_body <- resources["comments"] do
) )
|> case do |> case do
{:ok, %{comment: comment}} -> {:ok, %{comment: comment}} ->
Comments.approve_comment(comment, pleb)
Comments.reindex_comment(comment) Comments.reindex_comment(comment)
Images.reindex_image(image) Images.reindex_image(image)
@ -126,6 +128,7 @@ for %{"forum" => forum_name, "topics" => topics} <- resources["forum_posts"] do
) )
|> case do |> case do
{:ok, %{post: post}} -> {:ok, %{post: post}} ->
Posts.approve_post(post, pleb)
Posts.reindex_post(post) Posts.reindex_post(post)
{:error, :post, changeset, _so_far} -> {:error, :post, changeset, _so_far} ->

View file

@ -283,7 +283,7 @@ CREATE TABLE public.comments (
destroyed_content boolean DEFAULT false, destroyed_content boolean DEFAULT false,
name_at_post_time character varying, name_at_post_time character varying,
body character varying NOT NULL, body character varying NOT NULL,
approved_at timestamp(0) without time zone approved boolean DEFAULT false
); );
@ -973,7 +973,7 @@ CREATE TABLE public.images (
image_duration double precision, image_duration double precision,
description character varying DEFAULT ''::character varying NOT NULL, description character varying DEFAULT ''::character varying NOT NULL,
scratchpad character varying, scratchpad character varying,
approved_at timestamp(0) without time zone approved boolean DEFAULT false
); );
@ -1261,7 +1261,7 @@ CREATE TABLE public.posts (
destroyed_content boolean DEFAULT false NOT NULL, destroyed_content boolean DEFAULT false NOT NULL,
name_at_post_time character varying, name_at_post_time character varying,
body character varying NOT NULL, body character varying NOT NULL,
approved_at timestamp(0) without time zone approved boolean DEFAULT false
); );
@ -1681,7 +1681,7 @@ CREATE TABLE public.topics (
locked_by_id integer, locked_by_id integer,
last_post_id integer, last_post_id integer,
hidden_from_users boolean DEFAULT false NOT NULL, hidden_from_users boolean DEFAULT false NOT NULL,
approved_at timestamp(0) without time zone approved boolean DEFAULT false
); );