Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions apps/cf/lib/comments/comments.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,25 @@ defmodule CF.Comments do
source fetcher should be moved to a job.
"""
def add_comment(user, video_id, params, source_url \\ nil, source_fetch_callback \\ nil) do
# TODO [Security] What if reply_to_id refer to a comment that is on a different statement ?
UserPermissions.check!(user, :create, :comment)
source_url = source_url && Source.prepare_url(source_url)

# Handle the case where reply_to_id refer to a comment that is on a different statement
if Map.get(params, :reply_to_id) do
reply_to = Repo.get!(Comment, Map.get(params, :reply_to_id))

cond do
is_nil(reply_to) ->
raise "Reply to comment not found"

reply_to.statement_id != params.statement_id ->
raise "Reply to comment on a different statement"

true ->
true
end
end

# Load source from DB or create a changeset to make a new one
source =
source_url &&
Expand Down Expand Up @@ -78,8 +93,10 @@ defmodule CF.Comments do
full_comment = comment

# If new source, fetch metadata
if source && is_nil(Map.get(source, :id)),
do: fetch_source_metadata_and_update_comment(comment, source_fetch_callback)
if source && is_nil(Map.get(source, :id)) do
callback = source_fetch_callback || fn _comment -> :ok end
fetch_source_metadata_and_update_comment(comment, callback)
end

# Return comment
full_comment
Expand Down Expand Up @@ -251,11 +268,14 @@ defmodule CF.Comments do
defp reverse_vote_type(:vote_down), do: :revert_vote_down
defp reverse_vote_type(:self_vote), do: :revert_self_vote

defp fetch_source_metadata_and_update_comment(%Comment{source: nil}, _), do: nil
defp fetch_source_metadata_and_update_comment(%Comment{source: nil}, _callback), do: nil

defp fetch_source_metadata_and_update_comment(comment = %Comment{source: base_source}, callback) do
Sources.update_source_metadata(base_source, fn updated_source ->
callback.(Map.merge(comment, %{source: updated_source, source_id: updated_source.id}))
updated_comment =
Map.merge(comment, %{source: updated_source, source_id: updated_source.id})

callback.(updated_comment)
end)
end
end
2 changes: 1 addition & 1 deletion apps/cf/lib/sources/fetcher.ex
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ defmodule CF.Sources.Fetcher do
defp do_fetch_source_metadata(url, mime_types) when mime_types in @fetchable_mime_types do
case HTTPoison.get(
url,
[{"User-Agent", "CaptainFact/2.0"}],
[{"User-Agent", "CaptainFact/#{CF.Application.version()}"}],
follow_redirect: true,
max_redirect: 5,
hackney: [pool: pool_name()]
Expand Down
2 changes: 1 addition & 1 deletion apps/cf/lib/statements/statements.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ defmodule CF.Statements do
"""
def update!(user_id, statement = %Statement{is_removed: false}, changes) do
UserPermissions.check!(user_id, :update, :statement)
changeset = Statement.changeset(statement, changes)
changeset = Statement.changeset_update(statement, changes)

if changeset.changes == %{} do
Result.ok(statement)
Expand Down
2 changes: 2 additions & 0 deletions apps/cf/lib/video_debate/history.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule CF.VideoDebate.History do
|> preload(:user)
|> where([a], a.video_id == ^video_id)
|> where([a], a.entity in ^@allowed_entities)
|> order_by([a], desc: a.inserted_at)
|> Repo.all()
end

Expand All @@ -19,6 +20,7 @@ defmodule CF.VideoDebate.History do
|> preload(:user)
|> where([a], a.entity == ^:statement)
|> where([a], a.statement_id == ^statement_id)
|> order_by([a], desc: a.inserted_at)
|> Repo.all()
end
end
3 changes: 2 additions & 1 deletion apps/cf_graphql/lib/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ defmodule CF.Graphql.Application do
# Start the PubSub system
{Phoenix.PubSub, name: CF.Graphql.PubSub},
# Start the endpoint when the application starts
{CF.GraphQLWeb.Endpoint, []}
{CF.GraphQLWeb.Endpoint, []},
{Absinthe.Subscription, CF.GraphQLWeb.Endpoint}
]

# See https://hexdocs.pm/elixir/Supervisor.html
Expand Down
48 changes: 48 additions & 0 deletions apps/cf_graphql/lib/custom_absinthe_plug.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
defmodule CF.Graphql.CustomAbsinthePlug do
@moduledoc """
Custom Absinthe plug that catches all exceptions and converts them
to proper GraphQL errors instead of letting Phoenix return HTML error pages.
"""

import Plug.Conn
alias CF.Accounts.UserPermissions.PermissionsError

def init(opts) do
Absinthe.Plug.init(opts)
end

@spec call(Plug.Conn.t(), any()) :: Plug.Conn.t()
def call(conn, opts) do
try do
Absinthe.Plug.call(conn, opts)
rescue
e in PermissionsError ->
# Convert the PermissionsError to a GraphQL error response
conn
|> put_resp_content_type("application/json")
|> send_resp(200, build_graphql_error_response(e.message, "FORBIDDEN"))

exception ->
# Convert any other exception to a GraphQL error response
error_message = Exception.message(exception)

conn
|> put_resp_content_type("application/json")
|> send_resp(200, build_graphql_error_response(error_message, "INTERNAL_ERROR"))
end
end

defp build_graphql_error_response(message, code) do
Jason.encode!(%{
"errors" => [
%{
"message" => message,
"extensions" => %{
"code" => code
}
}
],
"data" => nil
})
end
end
3 changes: 3 additions & 0 deletions apps/cf_graphql/lib/endpoint.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
defmodule CF.GraphQLWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :cf_graphql
use Absinthe.Phoenix.Endpoint

socket("/socket", CF.GraphQLWeb.UserSocket, websocket: true, longpoll: false)

plug(Plug.RequestId)
plug(Plug.Logger)
Expand Down
93 changes: 93 additions & 0 deletions apps/cf_graphql/lib/resolvers/comments.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ defmodule CF.Graphql.Resolvers.Comments do
import Ecto.Query
alias DB.Repo
alias DB.Schema.Vote
alias DB.Schema.Comment
alias DB.Schema.Statement
alias CF.Comments
alias CF.Graphql.Subscriptions
alias CF.Moderation.Flagger

def score(comment, _args, _info) do
batch({__MODULE__, :comments_scores}, comment.id, fn results ->
Expand All @@ -18,4 +23,92 @@ defmodule CF.Graphql.Resolvers.Comments do
|> Repo.all()
|> Enum.into(%{})
end

# Mutations

def create(_root, args = %{statement_id: statement_id}, %{context: %{user: user}}) do
# Get statement to find video_id
statement = Repo.get!(Statement, statement_id)
video_id = statement.video_id
reply_to_id = Map.get(args, :reply_to_id)

params = %{
"statement_id" => statement_id,
"text" => Map.get(args, :text),
"reply_to_id" => reply_to_id,
"approve" => Map.get(args, :approve)
}

source_url = Map.get(args, :source)

# Callback to broadcast comment_updated when source metadata is fetched
update_callback = fn updated_comment ->
updated_comment = Repo.preload(updated_comment, [:source, :user, :statement])
Subscriptions.publish_comment_updated(updated_comment, video_id)
end

try do
case Comments.add_comment(user, video_id, params, source_url, update_callback) do
{:error, reason} ->
{:error, reason}

comment ->
comment = Repo.preload(comment, [:source, :user, :statement])
Subscriptions.publish_comment_added(comment, video_id)
{:ok, comment}
end
rescue
exception ->
{:error, exception}
end
end

def delete(_root, %{id: id}, %{context: %{user: user}}) do
comment = Repo.get!(Comment, id) |> Repo.preload(:statement)
video_id = comment.statement.video_id

case Comments.delete_comment(user, video_id, comment) do
nil ->
{:ok, %{id: id, statement_id: comment.statement_id, reply_to_id: comment.reply_to_id}}

_ ->
Subscriptions.publish_comment_removed(comment, video_id)
{:ok, %{id: id, statement_id: comment.statement_id, reply_to_id: comment.reply_to_id}}
end
end

def vote(_root, %{comment_id: comment_id, value: value}, %{context: %{user: user}}) do
# Get comment and preload statement to access video_id
comment = Repo.get!(Comment, comment_id) |> Repo.preload(:statement)
video_id = comment.statement.video_id

case Comments.vote!(user, video_id, comment_id, value) do
{:ok, comment, vote, prev_value} ->
# Calculate score diff (same logic as comments_channel.ex)
diff = value_diff(prev_value, vote.value)

# Publish score diff via subscription
Subscriptions.publish_comment_score_diff(comment, diff, video_id)

# Preload associations for GraphQL response
comment = Repo.preload(comment, [:source, :user, :statement])
{:ok, comment}

{:error, reason} ->
{:error, reason}
end
end

def flag(_root, %{comment_id: comment_id, reason: reason}, %{context: %{user: user}}) do
# Get comment and preload statement to access video_id
comment = Repo.get!(Comment, comment_id) |> Repo.preload(:statement)
video_id = comment.statement.video_id

Flagger.flag!(user.id, video_id, comment_id, reason)
{:ok, %{id: comment_id}}
end

# Helper function to calculate vote value diff (matches comments_channel.ex logic)
defp value_diff(0, new_value), do: new_value
defp value_diff(prev_value, new_value), do: new_value - prev_value
end
17 changes: 17 additions & 0 deletions apps/cf_graphql/lib/resolvers/history.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule CF.Graphql.Resolvers.History do
@moduledoc """
Resolvers for history-related queries
"""

alias CF.VideoDebate.History

def video_history_actions(_root, %{video_id: video_id}, _info) do
{:ok, History.video_history(video_id)}
end

def statement_history_actions(_root, %{statement_id: statement_id}, _info) do
{:ok, History.statement_history(statement_id)}
end
end


Loading
Loading