Skip to content

Commit

Permalink
Add moderation tools
Browse files Browse the repository at this point in the history
  • Loading branch information
René Föhring committed Apr 8, 2018
1 parent 3fa2a2b commit bc628db
Show file tree
Hide file tree
Showing 14 changed files with 279 additions and 12 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ erl_crash.dump
/config/logger.extension.exs
.envrc
.tool-versions-e
lib/elixir_status_moderation.ex
7 changes: 7 additions & 0 deletions config/prod.secret.example.exs
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,10 @@ config :appsignal, :config,
push_api_key: "your-hex-appsignal-key",
env: :prod,
revision: Mix.Project.config()[:version]

# This function receives the posting and author as arguments and determines
# reasons why it requires moderation
# Return an empty list if no moderation is necessary
config :elixir_status,
:publisher_moderation_reasons,
&ElixirStatusModerationSample.moderation_reasons/2
71 changes: 62 additions & 9 deletions lib/elixir_status/publisher.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ defmodule ElixirStatus.Publisher do
if Guard.blocked?(new_posting, author) do
after_create_blocked(new_posting, author)
else
after_create_valid(new_posting, author)
if Guard.moderation_required?(new_posting, author) do
after_create_moderation_required(new_posting, author)
else
after_create_valid(new_posting, author)
end
end
end

Expand All @@ -35,26 +39,43 @@ defmodule ElixirStatus.Publisher do
Posting.unpublish(new_posting)
end

defp after_create_moderation_required(new_posting, author) do
new_posting
|> create_all_short_links
|> send_direct_message_moderation_required

reasons = Guard.moderation_reasons(new_posting, author)

Posting.require_moderation(new_posting, reasons)
end

defp after_create_valid(new_posting, author) do
new_posting
|> create_all_short_links
|> send_direct_message_valid

tweet_uid = post_to_twitter(new_posting, author.twitter_handle)
Posting.update_published_tweet_uid(new_posting, tweet_uid)
tweet_about_posting!(new_posting, author)
end

@doc """
Called when a posting is published during moderation by PostingController.
"""
def after_publish_moderated(posting, author) do
tweet_about_posting!(posting, author)
end

@doc """
Called when a posting is unpublished by PostingController.
"""
def after_unpublish(posting) do
if posting.published_tweet_uid do
spawn(fn ->
ExTwitter.destroy_status(posting.published_tweet_uid)
end)
remove_tweet!(posting)
end

Posting.update_published_tweet_uid(posting, nil)
end
@doc """
Called when a posting is marked as spam during moderation by PostingController.
"""
def after_mark_as_spam(posting) do
remove_tweet!(posting)
end

@doc """
Expand Down Expand Up @@ -89,6 +110,21 @@ defmodule ElixirStatus.Publisher do
"#{uid}-#{permatitle}"
end

def tweet_about_posting!(posting, author) do
tweet_uid = post_to_twitter(posting, author.twitter_handle)
Posting.update_published_tweet_uid(posting, tweet_uid)
end

defp remove_tweet!(posting) do
if posting.published_tweet_uid do
spawn(fn ->
ExTwitter.destroy_status(posting.published_tweet_uid)
end)

Posting.update_published_tweet_uid(posting, nil)
end
end

defp create_all_short_links(posting) do
posting
|> SharedUrls.for_posting()
Expand All @@ -104,6 +140,18 @@ defmodule ElixirStatus.Publisher do
send_on_twitter(text, Mix.env())
end

def send_direct_message_moderation_required(posting) do
text = """
***MODERATE***
#{short_title(posting.title)}
#{moderation_url(posting)}
"""

send_on_twitter(text, Mix.env())
end

defp send_direct_message_valid(%ElixirStatus.Posting{title: title, permalink: permalink}) do
text = "#{short_title(title)} #{short_url(permalink)}"

Expand Down Expand Up @@ -214,4 +262,9 @@ defmodule ElixirStatus.Publisher do

ElixirStatus.URL.from_path("/=#{uid}")
end

defp moderation_url(posting) do
"/moderate/#{posting.moderation_key}"
|> ElixirStatus.URL.from_path()
end
end
12 changes: 12 additions & 0 deletions lib/elixir_status/publisher/guard.ex
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
defmodule ElixirStatus.Publisher.Guard do
@publisher_blocked_urls Application.get_env(:elixir_status, :publisher_blocked_urls)
@publisher_blocked_user_names Application.get_env(:elixir_status, :publisher_blocked_user_names)
@publisher_moderation_reasons Application.get_env(:elixir_status, :publisher_moderation_reasons)

alias ElixirStatus.Publisher.SharedUrls

def blocked?(posting, author) do
blocked_author?(author) or blocked_posting?(posting)
end

def moderation_required?(posting, author) do
moderation_reasons(posting, author)
|> List.wrap()
|> Enum.any?()
end

def moderation_reasons(posting, author) do
@publisher_moderation_reasons.(posting, author)
|> Enum.reject(&is_nil/1)
end

def blocked_author?(author) do
blocked_user_names = all_blocked_user_names()

Expand Down
7 changes: 7 additions & 0 deletions lib/elixir_status_moderation_sample.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule ElixirStatusModerationSample do
def moderation_reasons(posting, author) do
[
String.starts_with?(posting.title, "!!! ") && "Title starts with three exclamation marks"
]
end
end
11 changes: 11 additions & 0 deletions priv/repo/migrations/20180408074127_add_moderation_to_postings.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defmodule ElixirStatus.Repo.Migrations.AddModerationToPostings do
use Ecto.Migration

def change do
alter table(:postings) do
add(:moderation_key, :string)
add(:awaiting_moderation, :boolean, default: false)
add(:metadata, :map)
end
end
end
34 changes: 34 additions & 0 deletions web/controllers/posting_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,37 @@ defmodule ElixirStatus.PostingController do
|> redirect(to: posting_path(conn, :index))
end

def moderate(conn, %{"moderation_key" => moderation_key}) do
assigns = [
moderation_key: moderation_key,
posting: ElixirStatus.Persistence.Posting.find_by_moderation_key(moderation_key)
]

render(conn, "moderate.html", assigns)
end

def moderate_mark_as_spam(conn, %{"moderation_key" => moderation_key}) do
posting = ElixirStatus.Persistence.Posting.find_by_moderation_key(moderation_key)

ElixirStatus.Persistence.Posting.mark_as_spam(posting)
Publisher.after_mark_as_spam(posting)

conn
|> put_flash(:info, "Posting was not published.")
|> redirect(to: posting_path(conn, :index))
end

def moderate_publish(conn, %{"moderation_key" => moderation_key}) do
posting = ElixirStatus.Persistence.Posting.find_by_moderation_key(moderation_key)

ElixirStatus.Persistence.Posting.publish_moderated(posting)
Publisher.after_publish_moderated(posting, posting.user)

conn
|> put_flash(:info, "Posting was successfully published.")
|> redirect(to: posting_path(conn, :index))
end

defp load_posting(conn, _) do
posting =
case conn do
Expand Down Expand Up @@ -301,7 +332,10 @@ defmodule ElixirStatus.PostingController do
title: params["title"],
scheduled_at: params["scheduled_at"],
published_at: Ecto.DateTime.utc(),
metadata: %{},
awaiting_moderation: false,
public: true,
moderation_key: Ecto.UUID.generate(),
type: PostingTypifier.run(tmp_post)["choice"] |> to_string,
referenced_urls: PostingUrlFinder.run(tmp_post) |> Poison.encode!()
}
Expand Down
5 changes: 4 additions & 1 deletion web/models/posting.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ defmodule ElixirStatus.Posting do
field(:published_at, Ecto.DateTime)
field(:published_tweet_uid, :string)
field(:public, :boolean, default: false)
field(:moderation_key, :string)
field(:awaiting_moderation, :boolean, default: false)
field(:metadata, :map)

field(:type, :string)
field(:referenced_urls, :string)
Expand All @@ -20,7 +23,7 @@ defmodule ElixirStatus.Posting do
end

@required_fields ~w(user_id uid permalink title text published_at public)
@optional_fields ~w(published_tweet_uid scheduled_at type referenced_urls)
@optional_fields ~w(published_tweet_uid scheduled_at type referenced_urls moderation_key awaiting_moderation metadata)

@doc """
Creates a changeset based on the `model` and `params`.
Expand Down
41 changes: 41 additions & 0 deletions web/persistence/posting.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ defmodule ElixirStatus.Persistence.Posting do
|> Repo.one()
end

def find_by_moderation_key(moderation_key) do
query = from(p in Posting, where: p.moderation_key == ^moderation_key)

query
|> Ecto.Query.preload(:user)
|> Repo.one()
end

def find_by_permalink(permalink) do
String.split(permalink, "-") |> Enum.at(0) |> find_by_uid
end
Expand Down Expand Up @@ -115,9 +123,42 @@ defmodule ElixirStatus.Persistence.Posting do
|> Repo.update!()
end

def mark_as_spam(posting) do
metadata = posting.metadata || %{}
metadata = Map.put(metadata, "marked_as_spam_at", Ecto.DateTime.utc())

attributes = %{public: false, awaiting_moderation: false, metadata: metadata}

posting
|> ElixirStatus.Posting.changeset(attributes)
|> Repo.update!()
end

def republish(posting) do
posting
|> ElixirStatus.Posting.changeset(%{public: true})
|> Repo.update!()
end

def publish_moderated(posting) do
metadata = posting.metadata || %{}
metadata = Map.put(metadata, "moderated_at", Ecto.DateTime.utc())

attributes = %{public: true, awaiting_moderation: false, metadata: metadata}

posting
|> ElixirStatus.Posting.changeset(attributes)
|> Repo.update!()
end

def require_moderation(posting, reasons) do
metadata = posting.metadata || %{}
metadata = Map.put(metadata, "moderation_reasons", reasons)

attributes = %{public: false, awaiting_moderation: true, metadata: metadata}

posting
|> ElixirStatus.Posting.changeset(attributes)
|> Repo.update!()
end
end
4 changes: 4 additions & 0 deletions web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ defmodule ElixirStatus.Router do
get("/p/:permalink", PostingController, :show, as: :permalink_posting)
get("/p/edit/:permalink", PostingController, :edit)

get("/moderate/:moderation_key", PostingController, :moderate)
post("/moderate/:moderation_key/publish", PostingController, :moderate_publish)
post("/moderate/:moderation_key/mark_as_spam", PostingController, :moderate_mark_as_spam)

get("/edit_profile", UserController, :edit, as: :edit_user)
put("/reset_twitter_handle", UserController, :reset_twitter_handle, as: :reset_twitter_handle)

Expand Down
29 changes: 29 additions & 0 deletions web/static/css/app/component.post.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
.post--unpublished {
opacity: 0.4;
}
.post--awaiting-moderation {
border: 1px solid #b066ff;
}

.post--preview {
margin-bottom: -2rem;
Expand Down Expand Up @@ -187,6 +190,32 @@ select.posting-type-selector {
@include clearAfter
}

.post__moderation-hint {
color: #b066ff;
border: 1px solid #b066ff;
padding: 1rem 1rem 0 1rem;
margin-bottom: 1rem;

h3 {
color: #fff;
background-color: #b066ff;
padding: 1rem;
margin: -1rem -1rem 1rem -1rem;
}
}

.post__not-public-hint {
color: #e62117;
border: 1px solid #e62117;
padding: 1rem 1rem 0 1rem;

h3 {
color: #fff;
background-color: #e62117;
padding: 1rem;
margin: -1rem -1rem 1rem -1rem;
}
}

.post__body {
a {
Expand Down
28 changes: 28 additions & 0 deletions web/templates/posting/moderate.html.eex
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<%= if @posting.metadata["moderated_at"] do %>
<p>
This post has already been moderated at <%= @posting.metadata["moderated_at"] |> human_readable_date(false) %>.
</p>
<% end %>
<%= if @posting.metadata["marked_as_spam_at"] do %>
<p>
This post has been marked as spam and unpublished at <%= @posting.metadata["marked_as_spam_at"] |> human_readable_date(false) %>.
</p>
<% end %>

<p>
<%= link "Publish", to: posting_path(@conn, :moderate_publish, @posting.moderation_key), class: "btn", method: :post %>
</p>
<p>
<%= link "Do not publish", to: posting_path(@conn, :moderate_mark_as_spam, @posting.moderation_key), class: "btn btn--danger", method: :post %>
</p>

<pre><%= inspect @posting.metadata["moderation_reasons"], pretty: true %></pre>


<%= render ElixirStatus.PostingView, "posting.html", conn: @conn,
posting: @posting,
belongs_to_me?: false,
just_created_by_me?: false
%>

<pre><%= inspect @posting, pretty: true %></pre>
Loading

0 comments on commit bc628db

Please sign in to comment.