philomena/lib/philomena/conversations.ex

359 lines
9 KiB
Elixir

defmodule Philomena.Conversations do
@moduledoc """
The Conversations context.
"""
import Ecto.Query, warn: false
alias Ecto.Multi
alias Philomena.Repo
alias Philomena.Conversations.Conversation
alias Philomena.Conversations.Message
alias Philomena.Reports
alias Philomena.Users
@doc """
Returns the number of unread conversations for the given user.
Conversations hidden by the given user are not counted.
## Examples
iex> count_unread_conversations(user1)
0
iex> count_unread_conversations(user2)
7
"""
def count_unread_conversations(user) do
Conversation
|> where(
[c],
((c.to_id == ^user.id and c.to_read == false) or
(c.from_id == ^user.id and c.from_read == false)) and
not ((c.to_id == ^user.id and c.to_hidden == true) or
(c.from_id == ^user.id and c.from_hidden == true))
)
|> Repo.aggregate(:count)
end
@doc """
Returns a `m:Scrivener.Page` of conversations between the partner and the user.
## Examples
iex> list_conversations_with("123", %User{}, page_size: 10)
%Scrivener.Page{}
"""
def list_conversations_with(partner_id, user, pagination) do
query =
from c in Conversation,
where:
(c.from_id == ^partner_id and c.to_id == ^user.id) or
(c.to_id == ^partner_id and c.from_id == ^user.id)
list_conversations(query, user, pagination)
end
@doc """
Returns a `m:Scrivener.Page` of conversations sent by or received from the user.
## Examples
iex> list_conversations_with("123", %User{}, page_size: 10)
%Scrivener.Page{}
"""
def list_conversations(queryable \\ Conversation, user, pagination) do
query =
from c in queryable,
as: :conversations,
where:
(c.from_id == ^user.id and not c.from_hidden) or
(c.to_id == ^user.id and not c.to_hidden),
inner_lateral_join:
cnt in subquery(
from m in Message,
where: m.conversation_id == parent_as(:conversations).id,
select: %{count: count()}
),
on: true,
order_by: [desc: :last_message_at],
preload: [:to, :from],
select: %{c | message_count: cnt.count}
Repo.paginate(query, pagination)
end
@doc """
Creates a conversation.
## Examples
iex> create_conversation(from, to, %{field: value})
{:ok, %Conversation{}}
iex> create_conversation(from, to, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_conversation(from, attrs \\ %{}) do
to = Users.get_user_by_name(attrs["recipient"])
%Conversation{}
|> Conversation.creation_changeset(from, to, attrs)
|> Repo.insert()
|> case do
{:ok, conversation} ->
report_non_approved_message(hd(conversation.messages))
{:ok, conversation}
error ->
error
end
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking conversation changes.
## Examples
iex> change_conversation(conversation)
%Ecto.Changeset{source: %Conversation{}}
"""
def change_conversation(%Conversation{} = conversation) do
Conversation.changeset(conversation, %{})
end
@doc """
Marks a conversation as read or unread from the perspective of the given user.
## Examples
iex> mark_conversation_read(conversation, user, true)
{:ok, %Conversation{}}
iex> mark_conversation_read(conversation, user, false)
{:ok, %Conversation{}}
iex> mark_conversation_read(conversation, %User{}, true)
{:error, %Ecto.Changeset{}}
"""
def mark_conversation_read(%Conversation{} = conversation, user, read \\ true) do
changes =
%{}
|> put_conditional(:to_read, read, conversation.to_id == user.id)
|> put_conditional(:from_read, read, conversation.from_id == user.id)
conversation
|> Conversation.read_changeset(changes)
|> Repo.update()
end
@doc """
Marks a conversation as hidden or visible from the perspective of the given user.
Hidden conversations are not shown in the list of conversations for the user, and
are not counted when retrieving the number of unread conversations.
## Examples
iex> mark_conversation_hidden(conversation, user, true)
{:ok, %Conversation{}}
iex> mark_conversation_hidden(conversation, user, false)
{:ok, %Conversation{}}
iex> mark_conversation_hidden(conversation, %User{}, true)
{:error, %Ecto.Changeset{}}
"""
def mark_conversation_hidden(%Conversation{} = conversation, user, hidden \\ true) do
changes =
%{}
|> put_conditional(:to_hidden, hidden, conversation.to_id == user.id)
|> put_conditional(:from_hidden, hidden, conversation.from_id == user.id)
conversation
|> Conversation.hidden_changeset(changes)
|> Repo.update()
end
defp put_conditional(map, key, value, condition) do
if condition do
Map.put(map, key, value)
else
map
end
end
@doc """
Returns the number of messages in the given conversation.
## Example
iex> count_messages(%Conversation{})
3
"""
def count_messages(conversation) do
Message
|> where(conversation_id: ^conversation.id)
|> Repo.aggregate(:count)
end
@doc """
Returns a `m:Scrivener.Page` of 2-tuples of messages and rendered output
within a conversation.
Messages are ordered by user message preference (`messages_newest_first`).
When coerced to a list and rendered as Markdown, the result may look like:
[
{%Message{body: "hello *world*"}, "hello <strong>world</strong>"}
]
## Example
iex> list_messages(%Conversation{}, %User{}, & &1.body, page_size: 10)
%Scrivener.Page{}
"""
def list_messages(conversation, user, collection_renderer, pagination) do
direction =
if user.messages_newest_first do
:desc
else
:asc
end
query =
from m in Message,
where: m.conversation_id == ^conversation.id,
order_by: [{^direction, :created_at}],
preload: :from
messages = Repo.paginate(query, pagination)
rendered = collection_renderer.(messages)
put_in(messages.entries, Enum.zip(messages.entries, rendered))
end
@doc """
Creates a message within a conversation.
## Examples
iex> create_message(%Conversation{}, %User{}, %{field: value})
{:ok, %Message{}}
iex> create_message(%Conversation{}, %User{}, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_message(conversation, user, attrs \\ %{}) do
message_changeset =
conversation
|> Ecto.build_assoc(:messages)
|> Message.creation_changeset(attrs, user)
conversation_changeset =
Conversation.new_message_changeset(conversation)
Multi.new()
|> Multi.insert(:message, message_changeset)
|> Multi.update(:conversation, conversation_changeset)
|> Repo.transaction()
|> case do
{:ok, %{message: message}} ->
report_non_approved_message(message)
{:ok, message}
_error ->
{:error, message_changeset}
end
end
@doc """
Approves a previously-posted message which was not approved at post time.
## Examples
iex> approve_message(%Message{}, %User{})
{:ok, %Message{}}
iex> approve_message(%Message{}, %User{})
{:error, %Ecto.Changeset{}}
"""
def approve_message(message, approving_user) do
message_changeset = Message.approve_changeset(message)
conversation_update_query =
from c in Conversation,
where: c.id == ^message.conversation_id,
update: [set: [from_read: false, to_read: false]]
reports_query =
Reports.close_report_query({"Conversation", message.conversation_id}, approving_user)
Multi.new()
|> Multi.update(:message, message_changeset)
|> Multi.update_all(:conversation, conversation_update_query, [])
|> Multi.update_all(:reports, reports_query, [])
|> Repo.transaction()
|> case do
{:ok, %{reports: {_count, reports}, message: message}} ->
Reports.reindex_reports(reports)
message
_error ->
{:error, message_changeset}
end
end
@doc """
Generates a system report for an unapproved message.
This is called by `create_conversation/2` and `create_message/3`, so it normally does not
need to be called explicitly.
## Examples
iex> report_non_approved_message(%Message{approved: false})
{:ok, %Report{}}
iex> report_non_approved_message(%Message{approved: true})
{:ok, nil}
"""
def report_non_approved_message(message) do
if message.approved do
{:ok, nil}
else
Reports.create_system_report(
{"Conversation", message.conversation_id},
"Approval",
"PM contains externally-embedded images and has been flagged for review."
)
end
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking message changes.
## Examples
iex> change_message(message)
%Ecto.Changeset{source: %Message{}}
"""
def change_message(%Message{} = message) do
Message.changeset(message, %{})
end
end