mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-12-18 15:08:00 +01:00
359 lines
9 KiB
Elixir
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
|