ChatMessageReference: Introduce and switch in chat controller.

This commit is contained in:
lain 2020-06-03 12:30:12 +02:00
parent 2c6ebe709a
commit aa22fce8f4
9 changed files with 205 additions and 45 deletions

View file

@ -72,6 +72,11 @@ def creation_cng(struct, params) do
|> unique_constraint(:user_id, name: :chats_user_id_recipient_index) |> unique_constraint(:user_id, name: :chats_user_id_recipient_index)
end end
def get_by_id(id) do
__MODULE__
|> Repo.get(id)
end
def get(user_id, recipient) do def get(user_id, recipient) do
__MODULE__ __MODULE__
|> Repo.get_by(user_id: user_id, recipient: recipient) |> Repo.get_by(user_id: user_id, recipient: recipient)

View file

@ -0,0 +1,80 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ChatMessageReference do
@moduledoc """
A reference that builds a relation between an AP chat message that a user can see and whether it has been seen
by them, or should be displayed to them. Used to build the chat view that is presented to the user.
"""
use Ecto.Schema
alias Pleroma.Chat
alias Pleroma.Object
alias Pleroma.Repo
import Ecto.Changeset
import Ecto.Query
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
schema "chat_message_references" do
belongs_to(:object, Object)
belongs_to(:chat, Chat)
field(:seen, :boolean, default: false)
timestamps()
end
def changeset(struct, params) do
struct
|> cast(params, [:object_id, :chat_id, :seen])
|> validate_required([:object_id, :chat_id, :seen])
end
def get_by_id(id) do
__MODULE__
|> Repo.get(id)
|> Repo.preload(:object)
end
def delete(cm_ref) do
cm_ref
|> Repo.delete()
end
def delete_for_object(%{id: object_id}) do
from(cr in __MODULE__,
where: cr.object_id == ^object_id
)
|> Repo.delete_all()
end
def for_chat_and_object(%{id: chat_id}, %{id: object_id}) do
__MODULE__
|> Repo.get_by(chat_id: chat_id, object_id: object_id)
|> Repo.preload(:object)
end
def for_chat_query(chat) do
from(cr in __MODULE__,
where: cr.chat_id == ^chat.id,
order_by: [desc: :id],
preload: [:object]
)
end
def create(chat, object, seen) do
params = %{
chat_id: chat.id,
object_id: object.id,
seen: seen
}
%__MODULE__{}
|> changeset(params)
|> Repo.insert()
end
end

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
""" """
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Chat alias Pleroma.Chat
alias Pleroma.ChatMessageReference
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
@ -104,6 +105,8 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object,
Object.decrease_replies_count(in_reply_to) Object.decrease_replies_count(in_reply_to)
end end
ChatMessageReference.delete_for_object(deleted_object)
ActivityPub.stream_out(object) ActivityPub.stream_out(object)
ActivityPub.stream_out_participations(deleted_object, user) ActivityPub.stream_out_participations(deleted_object, user)
:ok :ok
@ -137,9 +140,11 @@ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do
|> Enum.each(fn [user, other_user] -> |> Enum.each(fn [user, other_user] ->
if user.local do if user.local do
if user.ap_id == actor.ap_id do if user.ap_id == actor.ap_id do
Chat.get_or_create(user.id, other_user.ap_id) {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
ChatMessageReference.create(chat, object, true)
else else
Chat.bump_or_create(user.id, other_user.ap_id) {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id)
ChatMessageReference.create(chat, object, false)
end end
end end
end) end)

View file

@ -6,14 +6,15 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Chat alias Pleroma.Chat
alias Pleroma.ChatMessageReference
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Pagination alias Pleroma.Pagination
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.PleromaAPI.ChatMessageView
alias Pleroma.Web.PleromaAPI.ChatView alias Pleroma.Web.PleromaAPI.ChatView
alias Pleroma.Web.PleromaAPI.ChatMessageReferenceView
import Ecto.Query import Ecto.Query
import Pleroma.Web.ActivityPub.ObjectValidator, only: [stringify_keys: 1] import Pleroma.Web.ActivityPub.ObjectValidator, only: [stringify_keys: 1]
@ -35,28 +36,38 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation
def delete_message(%{assigns: %{user: %{ap_id: actor} = user}} = conn, %{ def delete_message(%{assigns: %{user: %{id: user_id} = user}} = conn, %{
message_id: id message_id: message_id,
id: chat_id
}) do }) do
with %Object{ with %ChatMessageReference{} = cm_ref <-
data: %{ ChatMessageReference.get_by_id(message_id),
"actor" => ^actor, ^chat_id <- cm_ref.chat_id |> to_string(),
"id" => object, %Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id),
"to" => [recipient], {:ok, _} <- remove_or_delete(cm_ref, user) do
"type" => "ChatMessage"
}
} = message <- Object.get_by_id(id),
%Chat{} = chat <- Chat.get(user.id, recipient),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object),
{:ok, _delete} <- CommonAPI.delete(activity.id, user) do
conn conn
|> put_view(ChatMessageView) |> put_view(ChatMessageReferenceView)
|> render("show.json", for: user, object: message, chat: chat) |> render("show.json", chat_message_reference: cm_ref)
else else
_e -> {:error, :could_not_delete} _e ->
{:error, :could_not_delete}
end end
end end
defp remove_or_delete(
%{object: %{data: %{"actor" => actor, "id" => id}}},
%{ap_id: actor} = user
) do
with %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
CommonAPI.delete(activity.id, user)
end
end
defp remove_or_delete(cm_ref, _) do
cm_ref
|> ChatMessageReference.delete()
end
def post_chat_message( def post_chat_message(
%{body_params: params, assigns: %{user: %{id: user_id} = user}} = conn, %{body_params: params, assigns: %{user: %{id: user_id} = user}} = conn,
%{ %{
@ -69,10 +80,11 @@ def post_chat_message(
CommonAPI.post_chat_message(user, recipient, params[:content], CommonAPI.post_chat_message(user, recipient, params[:content],
media_id: params[:media_id] media_id: params[:media_id]
), ),
message <- Object.normalize(activity) do message <- Object.normalize(activity, false),
cm_ref <- ChatMessageReference.for_chat_and_object(chat, message) do
conn conn
|> put_view(ChatMessageView) |> put_view(ChatMessageReferenceView)
|> render("show.json", for: user, object: message, chat: chat) |> render("show.json", for: user, chat_message_reference: cm_ref)
end end
end end
@ -87,14 +99,14 @@ def mark_as_read(%{assigns: %{user: %{id: user_id}}} = conn, %{id: id}) do
def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: id} = params) do def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: id} = params) do
with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do
messages = cm_refs =
chat chat
|> Chat.messages_for_chat_query() |> ChatMessageReference.for_chat_query()
|> Pagination.fetch_paginated(params |> stringify_keys()) |> Pagination.fetch_paginated(params |> stringify_keys())
conn conn
|> put_view(ChatMessageView) |> put_view(ChatMessageReferenceView)
|> render("index.json", for: user, objects: messages, chat: chat) |> render("index.json", for: user, chat_message_references: cm_refs)
else else
_ -> _ ->
conn conn

View file

@ -2,10 +2,9 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.ChatMessageView do defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceView do
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.Chat
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
@ -13,8 +12,12 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageView do
def render( def render(
"show.json", "show.json",
%{ %{
object: %{id: id, data: %{"type" => "ChatMessage"} = chat_message}, chat_message_reference: %{
chat: %Chat{id: chat_id} id: id,
object: %{data: chat_message},
chat_id: chat_id,
seen: seen
}
} }
) do ) do
%{ %{
@ -26,11 +29,17 @@ def render(
emojis: StatusView.build_emojis(chat_message["emoji"]), emojis: StatusView.build_emojis(chat_message["emoji"]),
attachment: attachment:
chat_message["attachment"] && chat_message["attachment"] &&
StatusView.render("attachment.json", attachment: chat_message["attachment"]) StatusView.render("attachment.json", attachment: chat_message["attachment"]),
seen: seen
} }
end end
def render("index.json", opts) do def render("index.json", opts) do
render_many(opts[:objects], __MODULE__, "show.json", Map.put(opts, :as, :object)) render_many(
opts[:chat_message_references],
__MODULE__,
"show.json",
Map.put(opts, :as, :chat_message_reference)
)
end end
end end

View file

@ -0,0 +1,20 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Repo.Migrations.CreateChatMessageReference do
use Ecto.Migration
def change do
create table(:chat_message_references, primary_key: false) do
add(:id, :uuid, primary_key: true)
add(:chat_id, references(:chats, on_delete: :delete_all), null: false)
add(:object_id, references(:objects, on_delete: :delete_all), null: false)
add(:seen, :boolean, default: false, null: false)
timestamps()
end
create(index(:chat_message_references, [:chat_id, "id desc"]))
end
end

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Chat alias Pleroma.Chat
alias Pleroma.ChatMessageReference
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
@ -330,7 +331,7 @@ test "it streams the created ChatMessage" do
end end
end end
test "it creates a Chat for the local users and bumps the unread count, except for the author" do test "it creates a Chat and ChatMessageReferences for the local users and bumps the unread count, except for the author" do
author = insert(:user, local: true) author = insert(:user, local: true)
recipient = insert(:user, local: true) recipient = insert(:user, local: true)
@ -347,8 +348,18 @@ test "it creates a Chat for the local users and bumps the unread count, except f
chat = Chat.get(author.id, recipient.ap_id) chat = Chat.get(author.id, recipient.ap_id)
assert chat.unread == 0 assert chat.unread == 0
[cm_ref] = ChatMessageReference.for_chat_query(chat) |> Repo.all()
assert cm_ref.object.data["content"] == "hey"
assert cm_ref.seen == true
chat = Chat.get(recipient.id, author.ap_id) chat = Chat.get(recipient.id, author.ap_id)
assert chat.unread == 1 assert chat.unread == 1
[cm_ref] = ChatMessageReference.for_chat_query(chat) |> Repo.all()
assert cm_ref.object.data["content"] == "hey"
assert cm_ref.seen == false
end end
test "it creates a Chat for the local users and bumps the unread count" do test "it creates a Chat for the local users and bumps the unread count" do

View file

@ -5,6 +5,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do
use Pleroma.Web.ConnCase, async: true use Pleroma.Web.ConnCase, async: true
alias Pleroma.Chat alias Pleroma.Chat
alias Pleroma.ChatMessageReference
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
@ -95,7 +96,7 @@ test "it works with an attachment", %{conn: conn, user: user} do
describe "DELETE /api/v1/pleroma/chats/:id/messages/:message_id" do describe "DELETE /api/v1/pleroma/chats/:id/messages/:message_id" do
setup do: oauth_access(["write:statuses"]) setup do: oauth_access(["write:statuses"])
test "it deletes a message for the author of the message", %{conn: conn, user: user} do test "it deletes a message from the chat", %{conn: conn, user: user} do
recipient = insert(:user) recipient = insert(:user)
{:ok, message} = {:ok, message} =
@ -107,23 +108,32 @@ test "it deletes a message for the author of the message", %{conn: conn, user: u
chat = Chat.get(user.id, recipient.ap_id) chat = Chat.get(user.id, recipient.ap_id)
cm_ref = ChatMessageReference.for_chat_and_object(chat, object)
# Deleting your own message removes the message and the reference
result = result =
conn conn
|> put_req_header("content-type", "application/json") |> put_req_header("content-type", "application/json")
|> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{object.id}") |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}")
|> json_response_and_validate_schema(200) |> json_response_and_validate_schema(200)
assert result["id"] == to_string(object.id) assert result["id"] == cm_ref.id
refute ChatMessageReference.get_by_id(cm_ref.id)
assert %{data: %{"type" => "Tombstone"}} = Object.get_by_id(object.id)
# Deleting other people's messages just removes the reference
object = Object.normalize(other_message, false) object = Object.normalize(other_message, false)
cm_ref = ChatMessageReference.for_chat_and_object(chat, object)
result = result =
conn conn
|> put_req_header("content-type", "application/json") |> put_req_header("content-type", "application/json")
|> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{object.id}") |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}")
|> json_response(400) |> json_response_and_validate_schema(200)
assert result == %{"error" => "could_not_delete"} assert result["id"] == cm_ref.id
refute ChatMessageReference.get_by_id(cm_ref.id)
assert Object.get_by_id(object.id)
end end
end end

View file

@ -2,14 +2,15 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.ChatMessageViewTest do defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceViewTest do
use Pleroma.DataCase use Pleroma.DataCase
alias Pleroma.Chat alias Pleroma.Chat
alias Pleroma.ChatMessageReference
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.PleromaAPI.ChatMessageView alias Pleroma.Web.PleromaAPI.ChatMessageReferenceView
import Pleroma.Factory import Pleroma.Factory
@ -30,25 +31,32 @@ test "it displays a chat message" do
object = Object.normalize(activity) object = Object.normalize(activity)
chat_message = ChatMessageView.render("show.json", object: object, for: user, chat: chat) cm_ref = ChatMessageReference.for_chat_and_object(chat, object)
assert chat_message[:id] == object.id |> to_string() chat_message = ChatMessageReferenceView.render("show.json", chat_message_reference: cm_ref)
assert chat_message[:id] == cm_ref.id
assert chat_message[:content] == "kippis :firefox:" assert chat_message[:content] == "kippis :firefox:"
assert chat_message[:account_id] == user.id assert chat_message[:account_id] == user.id
assert chat_message[:chat_id] assert chat_message[:chat_id]
assert chat_message[:created_at] assert chat_message[:created_at]
assert chat_message[:seen] == true
assert match?([%{shortcode: "firefox"}], chat_message[:emojis]) assert match?([%{shortcode: "firefox"}], chat_message[:emojis])
{:ok, activity} = CommonAPI.post_chat_message(recipient, user, "gkgkgk", media_id: upload.id) {:ok, activity} = CommonAPI.post_chat_message(recipient, user, "gkgkgk", media_id: upload.id)
object = Object.normalize(activity) object = Object.normalize(activity)
chat_message_two = ChatMessageView.render("show.json", object: object, for: user, chat: chat) cm_ref = ChatMessageReference.for_chat_and_object(chat, object)
assert chat_message_two[:id] == object.id |> to_string() chat_message_two =
ChatMessageReferenceView.render("show.json", chat_message_reference: cm_ref)
assert chat_message_two[:id] == cm_ref.id
assert chat_message_two[:content] == "gkgkgk" assert chat_message_two[:content] == "gkgkgk"
assert chat_message_two[:account_id] == recipient.id assert chat_message_two[:account_id] == recipient.id
assert chat_message_two[:chat_id] == chat_message[:chat_id] assert chat_message_two[:chat_id] == chat_message[:chat_id]
assert chat_message_two[:attachment] assert chat_message_two[:attachment]
assert chat_message_two[:seen] == false
end end
end end