commit
4987ee6256
@ -0,0 +1,150 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Helpers.MediaHelper do
|
||||||
|
@moduledoc """
|
||||||
|
Handles common media-related operations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Pleroma.HTTP
|
||||||
|
|
||||||
|
def image_resize(url, options) do
|
||||||
|
with executable when is_binary(executable) <- System.find_executable("convert"),
|
||||||
|
{:ok, args} <- prepare_image_resize_args(options),
|
||||||
|
{:ok, env} <- HTTP.get(url, [], pool: :media),
|
||||||
|
{:ok, fifo_path} <- mkfifo() do
|
||||||
|
args = List.flatten([fifo_path, args])
|
||||||
|
run_fifo(fifo_path, env, executable, args)
|
||||||
|
else
|
||||||
|
nil -> {:error, {:convert, :command_not_found}}
|
||||||
|
{:error, _} = error -> error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp prepare_image_resize_args(
|
||||||
|
%{max_width: max_width, max_height: max_height, format: "png"} = options
|
||||||
|
) do
|
||||||
|
quality = options[:quality] || 85
|
||||||
|
resize = Enum.join([max_width, "x", max_height, ">"])
|
||||||
|
|
||||||
|
args = [
|
||||||
|
"-resize",
|
||||||
|
resize,
|
||||||
|
"-quality",
|
||||||
|
to_string(quality),
|
||||||
|
"png:-"
|
||||||
|
]
|
||||||
|
|
||||||
|
{:ok, args}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp prepare_image_resize_args(%{max_width: max_width, max_height: max_height} = options) do
|
||||||
|
quality = options[:quality] || 85
|
||||||
|
resize = Enum.join([max_width, "x", max_height, ">"])
|
||||||
|
|
||||||
|
args = [
|
||||||
|
"-interlace",
|
||||||
|
"Plane",
|
||||||
|
"-resize",
|
||||||
|
resize,
|
||||||
|
"-quality",
|
||||||
|
to_string(quality),
|
||||||
|
"jpg:-"
|
||||||
|
]
|
||||||
|
|
||||||
|
{:ok, args}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp prepare_image_resize_args(_), do: {:error, :missing_options}
|
||||||
|
|
||||||
|
# Note: video thumbnail is intentionally not resized (always has original dimensions)
|
||||||
|
def video_framegrab(url) do
|
||||||
|
with executable when is_binary(executable) <- System.find_executable("ffmpeg"),
|
||||||
|
{:ok, env} <- HTTP.get(url, [], pool: :media),
|
||||||
|
{:ok, fifo_path} <- mkfifo(),
|
||||||
|
args = [
|
||||||
|
"-y",
|
||||||
|
"-i",
|
||||||
|
fifo_path,
|
||||||
|
"-vframes",
|
||||||
|
"1",
|
||||||
|
"-f",
|
||||||
|
"mjpeg",
|
||||||
|
"-loglevel",
|
||||||
|
"error",
|
||||||
|
"-"
|
||||||
|
] do
|
||||||
|
run_fifo(fifo_path, env, executable, args)
|
||||||
|
else
|
||||||
|
nil -> {:error, {:ffmpeg, :command_not_found}}
|
||||||
|
{:error, _} = error -> error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp run_fifo(fifo_path, env, executable, args) do
|
||||||
|
pid =
|
||||||
|
Port.open({:spawn_executable, executable}, [
|
||||||
|
:use_stdio,
|
||||||
|
:stream,
|
||||||
|
:exit_status,
|
||||||
|
:binary,
|
||||||
|
args: args
|
||||||
|
])
|
||||||
|
|
||||||
|
fifo = Port.open(to_charlist(fifo_path), [:eof, :binary, :stream, :out])
|
||||||
|
fix = Pleroma.Helpers.QtFastStart.fix(env.body)
|
||||||
|
true = Port.command(fifo, fix)
|
||||||
|
:erlang.port_close(fifo)
|
||||||
|
loop_recv(pid)
|
||||||
|
after
|
||||||
|
File.rm(fifo_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp mkfifo do
|
||||||
|
path = Path.join(System.tmp_dir!(), "pleroma-media-preview-pipe-#{Ecto.UUID.generate()}")
|
||||||
|
|
||||||
|
case System.cmd("mkfifo", [path]) do
|
||||||
|
{_, 0} ->
|
||||||
|
spawn(fifo_guard(path))
|
||||||
|
{:ok, path}
|
||||||
|
|
||||||
|
{_, err} ->
|
||||||
|
{:error, {:fifo_failed, err}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fifo_guard(path) do
|
||||||
|
pid = self()
|
||||||
|
|
||||||
|
fn ->
|
||||||
|
ref = Process.monitor(pid)
|
||||||
|
|
||||||
|
receive do
|
||||||
|
{:DOWN, ^ref, :process, ^pid, _} ->
|
||||||
|
File.rm(path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp loop_recv(pid) do
|
||||||
|
loop_recv(pid, <<>>)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp loop_recv(pid, acc) do
|
||||||
|
receive do
|
||||||
|
{^pid, {:data, data}} ->
|
||||||
|
loop_recv(pid, acc <> data)
|
||||||
|
|
||||||
|
{^pid, {:exit_status, 0}} ->
|
||||||
|
{:ok, acc}
|
||||||
|
|
||||||
|
{^pid, {:exit_status, status}} ->
|
||||||
|
{:error, status}
|
||||||
|
after
|
||||||
|
5000 ->
|
||||||
|
:erlang.port_close(pid)
|
||||||
|
{:error, :timeout}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,131 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Helpers.QtFastStart do
|
||||||
|
@moduledoc """
|
||||||
|
(WIP) Converts a "slow start" (data before metadatas) mov/mp4 file to a "fast start" one (metadatas before data).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO: Cleanup and optimizations
|
||||||
|
# Inspirations: https://www.ffmpeg.org/doxygen/3.4/qt-faststart_8c_source.html
|
||||||
|
# https://github.com/danielgtaylor/qtfaststart/blob/master/qtfaststart/processor.py
|
||||||
|
# ISO/IEC 14496-12:2015, ISO/IEC 15444-12:2015
|
||||||
|
# Paracetamol
|
||||||
|
|
||||||
|
def fix(<<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70, _::bits>> = binary) do
|
||||||
|
index = fix(binary, 0, nil, nil, [])
|
||||||
|
|
||||||
|
case index do
|
||||||
|
:abort -> binary
|
||||||
|
[{"ftyp", _, _, _, _}, {"mdat", _, _, _, _} | _] -> faststart(index)
|
||||||
|
[{"ftyp", _, _, _, _}, {"free", _, _, _, _}, {"mdat", _, _, _, _} | _] -> faststart(index)
|
||||||
|
_ -> binary
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fix(binary) do
|
||||||
|
binary
|
||||||
|
end
|
||||||
|
|
||||||
|
# MOOV have been seen before MDAT- abort
|
||||||
|
defp fix(<<_::bits>>, _, true, false, _) do
|
||||||
|
:abort
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fix(
|
||||||
|
<<size::integer-big-size(32), fourcc::bits-size(32), rest::bits>>,
|
||||||
|
pos,
|
||||||
|
got_moov,
|
||||||
|
got_mdat,
|
||||||
|
acc
|
||||||
|
) do
|
||||||
|
full_size = (size - 8) * 8
|
||||||
|
<<data::bits-size(full_size), rest::bits>> = rest
|
||||||
|
|
||||||
|
acc = [
|
||||||
|
{fourcc, pos, pos + size, size,
|
||||||
|
<<size::integer-big-size(32), fourcc::bits-size(32), data::bits>>}
|
||||||
|
| acc
|
||||||
|
]
|
||||||
|
|
||||||
|
fix(rest, pos + size, got_moov || fourcc == "moov", got_mdat || fourcc == "mdat", acc)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fix(<<>>, _pos, _, _, acc) do
|
||||||
|
:lists.reverse(acc)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp faststart(index) do
|
||||||
|
{{_ftyp, _, _, _, ftyp}, index} = List.keytake(index, "ftyp", 0)
|
||||||
|
|
||||||
|
# Skip re-writing the free fourcc as it's kind of useless.
|
||||||
|
# Why stream useless bytes when you can do without?
|
||||||
|
{free_size, index} =
|
||||||
|
case List.keytake(index, "free", 0) do
|
||||||
|
{{_, _, _, size, _}, index} -> {size, index}
|
||||||
|
_ -> {0, index}
|
||||||
|
end
|
||||||
|
|
||||||
|
{{_moov, _, _, moov_size, moov}, index} = List.keytake(index, "moov", 0)
|
||||||
|
offset = -free_size + moov_size
|
||||||
|
rest = for {_, _, _, _, data} <- index, do: data, into: []
|
||||||
|
<<moov_head::bits-size(64), moov_data::bits>> = moov
|
||||||
|
[ftyp, moov_head, fix_moov(moov_data, offset, []), rest]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fix_moov(
|
||||||
|
<<size::integer-big-size(32), fourcc::bits-size(32), rest::bits>>,
|
||||||
|
offset,
|
||||||
|
acc
|
||||||
|
) do
|
||||||
|
full_size = (size - 8) * 8
|
||||||
|
<<data::bits-size(full_size), rest::bits>> = rest
|
||||||
|
|
||||||
|
data =
|
||||||
|
cond do
|
||||||
|
fourcc in ["trak", "mdia", "minf", "stbl"] ->
|
||||||
|
# Theses contains sto or co64 part
|
||||||
|
[<<size::integer-big-size(32), fourcc::bits-size(32)>>, fix_moov(data, offset, [])]
|
||||||
|
|
||||||
|
fourcc in ["stco", "co64"] ->
|
||||||
|
# fix the damn thing
|
||||||
|
<<version::integer-big-size(32), count::integer-big-size(32), rest::bits>> = data
|
||||||
|
|
||||||
|
entry_size =
|
||||||
|
case fourcc do
|
||||||
|
"stco" -> 32
|
||||||
|
"co64" -> 64
|
||||||
|
end
|
||||||
|
|
||||||
|
[
|
||||||
|
<<size::integer-big-size(32), fourcc::bits-size(32), version::integer-big-size(32),
|
||||||
|
count::integer-big-size(32)>>,
|
||||||
|
rewrite_entries(entry_size, offset, rest, [])
|
||||||
|
]
|
||||||
|
|
||||||
|
true ->
|
||||||
|
[<<size::integer-big-size(32), fourcc::bits-size(32)>>, data]
|
||||||
|
end
|
||||||
|
|
||||||
|
acc = [acc | data]
|
||||||
|
fix_moov(rest, offset, acc)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fix_moov(<<>>, _, acc), do: acc
|
||||||
|
|
||||||
|
for size <- [32, 64] do
|
||||||
|
defp rewrite_entries(
|
||||||
|
unquote(size),
|
||||||
|
offset,
|
||||||
|
<<pos::integer-big-size(unquote(size)), rest::bits>>,
|
||||||
|
acc
|
||||||
|
) do
|
||||||
|
rewrite_entries(unquote(size), offset, rest, [
|
||||||
|
acc | <<pos + offset::integer-big-size(unquote(size))>>
|
||||||
|
])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp rewrite_entries(_, _, <<>>, acc), do: acc
|
||||||
|
end
|
@ -0,0 +1,85 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.AdminAPI.ChatController do
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
alias Pleroma.Activity
|
||||||
|
alias Pleroma.Chat
|
||||||
|
alias Pleroma.Chat.MessageReference
|
||||||
|
alias Pleroma.ModerationLog
|
||||||
|
alias Pleroma.Pagination
|
||||||
|
alias Pleroma.Plugs.OAuthScopesPlug
|
||||||
|
alias Pleroma.Web.AdminAPI
|
||||||
|
alias Pleroma.Web.CommonAPI
|
||||||
|
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
|
|
||||||
|
plug(
|
||||||
|
OAuthScopesPlug,
|
||||||
|
%{scopes: ["read:chats"], admin: true} when action in [:show, :messages]
|
||||||
|
)
|
||||||
|
|
||||||
|
plug(
|
||||||
|
OAuthScopesPlug,
|
||||||
|
%{scopes: ["write:chats"], admin: true} when action in [:delete_message]
|
||||||
|
)
|
||||||
|
|
||||||
|
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
|
||||||
|
|
||||||
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ChatOperation
|
||||||
|
|
||||||
|
def delete_message(%{assigns: %{user: user}} = conn, %{
|
||||||
|
message_id: message_id,
|
||||||
|
id: chat_id
|
||||||
|
}) do
|
||||||
|
with %MessageReference{object: %{data: %{"id" => object_ap_id}}} = cm_ref <-
|
||||||
|
MessageReference.get_by_id(message_id),
|
||||||
|
^chat_id <- to_string(cm_ref.chat_id),
|
||||||
|
%Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(object_ap_id),
|
||||||
|
{:ok, _} <- CommonAPI.delete(activity_id, user) do
|
||||||
|
ModerationLog.insert_log(%{
|
||||||
|
action: "chat_message_delete",
|
||||||
|
actor: user,
|
||||||
|
subject_id: message_id
|
||||||
|
})
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_view(MessageReferenceView)
|
||||||
|
|> render("show.json", chat_message_reference: cm_ref)
|
||||||
|
else
|
||||||
|
_e ->
|
||||||
|
{:error, :could_not_delete}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def messages(conn, %{id: id} = params) do
|
||||||
|
with %Chat{} = chat <- Chat.get_by_id(id) do
|
||||||
|
cm_refs =
|
||||||
|
chat
|
||||||
|
|> MessageReference.for_chat_query()
|
||||||
|
|> Pagination.fetch_paginated(params)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_view(MessageReferenceView)
|
||||||
|
|> render("index.json", chat_message_references: cm_refs)
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> json(%{error: "not found"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def show(conn, %{id: id}) do
|
||||||
|
with %Chat{} = chat <- Chat.get_by_id(id) do
|
||||||
|
conn
|
||||||
|
|> put_view(AdminAPI.ChatView)
|
||||||
|
|> render("show.json", chat: chat)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,41 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
alias Pleroma.Plugs.InstanceStatic
|
||||||
|
alias Pleroma.Plugs.OAuthScopesPlug
|
||||||
|
alias Pleroma.Web.InstanceDocument
|
||||||
|
|
||||||
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
|
|
||||||
|
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
|
||||||
|
|
||||||
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.InstanceDocumentOperation
|
||||||
|
|
||||||
|
plug(OAuthScopesPlug, %{scopes: ["read"], admin: true} when action == :show)
|
||||||
|
plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action in [:update, :delete])
|
||||||
|
|
||||||
|
def show(conn, %{name: document_name}) do
|
||||||
|
with {:ok, url} <- InstanceDocument.get(document_name),
|
||||||
|
{:ok, content} <- File.read(InstanceStatic.file_path(url)) do
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("text/html")
|
||||||
|
|> send_resp(200, content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(%{body_params: %{file: file}} = conn, %{name: document_name}) do
|
||||||
|
with {:ok, url} <- InstanceDocument.put(document_name, file.path) do
|
||||||
|
json(conn, %{"url" => url})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete(conn, %{name: document_name}) do
|
||||||
|
with :ok <- InstanceDocument.delete(document_name) do
|
||||||
|
json(conn, %{})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,30 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.AdminAPI.ChatView do
|
||||||
|
use Pleroma.Web, :view
|
||||||
|
|
||||||
|
alias Pleroma.Chat
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.MastodonAPI
|
||||||
|
alias Pleroma.Web.PleromaAPI
|
||||||
|
|
||||||
|
def render("index.json", %{chats: chats} = opts) do
|
||||||
|
render_many(chats, __MODULE__, "show.json", Map.delete(opts, :chats))
|
||||||
|
end
|
||||||
|
|
||||||
|
def render("show.json", %{chat: %Chat{user_id: user_id}} = opts) do
|
||||||
|
user = User.get_by_id(user_id)
|
||||||
|
sender = MastodonAPI.AccountView.render("show.json", user: user, skip_visibility_check: true)
|
||||||
|
|
||||||
|
serialized_chat = PleromaAPI.ChatView.render("show.json", opts)
|
||||||
|
|
||||||
|
serialized_chat
|
||||||
|
|> Map.put(:sender, sender)
|
||||||
|
|> Map.put(:receiver, serialized_chat[:account])
|
||||||
|
|> Map.delete(:account)
|
||||||
|
end
|
||||||
|
|
||||||
|
def render(view, opts), do: PleromaAPI.ChatView.render(view, opts)
|
||||||
|
end
|
@ -0,0 +1,96 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.Admin.ChatOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Chat
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.ChatMessage
|
||||||
|
|
||||||
|
import Pleroma.Web.ApiSpec.Helpers
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_message_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["admin", "chat"],
|
||||||
|
summary: "Delete an individual chat message",
|
||||||
|
operationId: "AdminAPI.ChatController.delete_message",
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(:id, :path, :string, "The ID of the Chat"),
|
||||||
|
Operation.parameter(:message_id, :path, :string, "The ID of the message")
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response(
|
||||||
|
"The deleted ChatMessage",
|
||||||
|
"application/json",
|
||||||
|
ChatMessage
|
||||||
|
)
|
||||||
|
},
|
||||||
|
security: [
|
||||||
|
%{
|
||||||
|
"oAuth" => ["write:chats"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def messages_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["admin", "chat"],
|
||||||
|
summary: "Get the most recent messages of the chat",
|
||||||
|
operationId: "AdminAPI.ChatController.messages",
|
||||||
|
parameters:
|
||||||
|
[Operation.parameter(:id, :path, :string, "The ID of the Chat")] ++
|
||||||
|
pagination_params(),
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response(
|
||||||
|
"The messages in the chat",
|
||||||
|
"application/json",
|
||||||
|
Pleroma.Web.ApiSpec.ChatOperation.chat_messages_response()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
security: [
|
||||||
|
%{
|
||||||
|
"oAuth" => ["read:chats"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["chat"],
|
||||||
|
summary: "Create a chat",
|
||||||
|
operationId: "AdminAPI.ChatController.show",
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(
|
||||||
|
:id,
|
||||||
|
:path,
|
||||||
|
:string,
|
||||||
|
"The id of the chat",
|
||||||
|
required: true,
|
||||||
|
example: "1234"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response(
|
||||||
|
"The existing chat",
|
||||||
|
"application/json",
|
||||||
|
Chat
|
||||||
|
)
|
||||||
|
},
|
||||||
|
security: [
|
||||||
|
%{
|
||||||
|
"oAuth" => ["read"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,115 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.Admin.InstanceDocumentOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Helpers
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "InstanceDocument"],
|
||||||
|
summary: "Get the instance document",
|
||||||
|
operationId: "AdminAPI.InstanceDocumentController.show",
|
||||||
|
security: [%{"oAuth" => ["read"]}],
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(:name, :path, %Schema{type: :string}, "The document name",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
| Helpers.admin_api_params()
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 => document_content(),
|
||||||
|
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||||
|
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "InstanceDocument"],
|
||||||
|
summary: "Update the instance document",
|
||||||
|
operationId: "AdminAPI.InstanceDocumentController.update",
|
||||||
|
security: [%{"oAuth" => ["write"]}],
|
||||||
|
requestBody: Helpers.request_body("Parameters", update_request()),
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(:name, :path, %Schema{type: :string}, "The document name",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
| Helpers.admin_api_params()
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("InstanceDocument", "application/json", instance_document()),
|
||||||
|
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||||
|
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp update_request do
|
||||||
|
%Schema{
|
||||||
|
title: "UpdateRequest",
|
||||||
|
description: "POST body for uploading the file",
|
||||||
|
type: :object,
|
||||||
|
required: [:file],
|
||||||
|
properties: %{
|
||||||
|
file: %Schema{
|
||||||
|
type: :string,
|
||||||
|
format: :binary,
|
||||||
|
description: "The file to be uploaded, using multipart form data."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "InstanceDocument"],
|
||||||
|
summary: "Get the instance document",
|
||||||
|
operationId: "AdminAPI.InstanceDocumentController.delete",
|
||||||
|
security: [%{"oAuth" => ["write"]}],
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(:name, :path, %Schema{type: :string}, "The document name",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
| Helpers.admin_api_params()
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("InstanceDocument", "application/json", instance_document()),
|
||||||
|
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||||
|
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp instance_document do
|
||||||
|
%Schema{
|
||||||
|
title: "InstanceDocument",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
url: %Schema{type: :string}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"url" => "https://example.com/static/terms-of-service.html"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp document_content do
|
||||||
|
Operation.response("InstanceDocumentContent", "text/html", %Schema{
|
||||||
|
type: :string,
|
||||||
|
example: "<h1>Instance panel</h1>"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,185 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.FedSockets.FedRegistry do
|
||||||
|
@moduledoc """
|
||||||
|
The FedRegistry stores the active FedSockets for quick retrieval.
|
||||||
|
|
||||||
|
The storage and retrieval portion of the FedRegistry is done in process through
|
||||||
|
elixir's `Registry` module for speed and its ability to monitor for terminated processes.
|
||||||
|
|
||||||
|
Dropped connections will be caught by `Registry` and deleted. Since the next
|
||||||
|
message will initiate a new connection there is no reason to try and reconnect at that point.
|
||||||
|
|
||||||
|
Normally outside modules should have no need to call or use the FedRegistry themselves.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Pleroma.Web.FedSockets.FedSocket
|
||||||
|
alias Pleroma.Web.FedSockets.SocketInfo
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@default_rejection_duration 15 * 60 * 1000
|
||||||
|
@rejections :fed_socket_rejections
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Retrieves a FedSocket from the Registry given it's origin.
|
||||||
|
|
||||||
|
The origin is expected to be a string identifying the endpoint "example.com" or "example2.com:8080"
|
||||||
|
|
||||||
|
Will return:
|
||||||
|
* {:ok, fed_socket} for working FedSockets
|
||||||
|
* {:error, :rejected} for origins that have been tried and refused within the rejection duration interval
|
||||||
|
* {:error, some_reason} usually :missing for unknown origins
|
||||||
|
"""
|
||||||
|
def get_fed_socket(origin) do
|
||||||
|
case get_registry_data(origin) do
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
|
||||||
|
{:ok, %{state: :connected} = socket_info} ->
|
||||||
|
{:ok, socket_info}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Adds a connected FedSocket to the Registry.
|
||||||
|
|
||||||
|
Always returns {:ok, fed_socket}
|
||||||
|
"""
|
||||||
|
def add_fed_socket(origin, pid \\ nil) do
|
||||||
|
origin
|
||||||
|
|> SocketInfo.build(pid)
|
||||||
|
|> SocketInfo.connect()
|
||||||
|
|> add_socket_info
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_socket_info(%{origin: origin, state: :connected} = socket_info) do
|
||||||
|
case Registry.register(FedSockets.Registry, origin, socket_info) do
|
||||||
|
{:ok, _owner} ->
|
||||||
|
clear_prior_rejection(origin)
|
||||||
|
Logger.debug("fedsocket added: #{inspect(origin)}")
|
||||||
|
|
||||||
|
{:ok, socket_info}
|
||||||
|
|
||||||
|
{:error, {:already_registered, _pid}} ->
|
||||||
|
FedSocket.close(socket_info)
|
||||||
|
existing_socket_info = Registry.lookup(FedSockets.Registry, origin)
|
||||||
|
|
||||||
|
{:ok, existing_socket_info}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, :error_adding_socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Mark this origin as having rejected a connection attempt.
|
||||||
|
This will keep it from getting additional connection attempts
|
||||||
|
for a period of time specified in the config.
|
||||||
|
|
||||||
|
Always returns {:ok, new_reg_data}
|
||||||
|
"""
|
||||||
|
def set_host_rejected(uri) do
|
||||||
|
new_reg_data =
|
||||||
|
uri
|
||||||
|
|> SocketInfo.origin()
|
||||||
|
|> get_or_create_registry_data()
|
||||||
|
|> set_to_rejected()
|
||||||
|
|> save_registry_data()
|
||||||
|
|
||||||
|
{:ok, new_reg_data}
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Retrieves the FedRegistryData from the Registry given it's origin.
|
||||||
|
|
||||||
|
The origin is expected to be a string identifying the endpoint "example.com" or "example2.com:8080"
|
||||||
|
|
||||||
|
Will return:
|
||||||
|
* {:ok, fed_registry_data} for known origins
|
||||||
|
* {:error, :missing} for uniknown origins
|
||||||
|
* {:error, :cache_error} indicating some low level runtime issues
|
||||||
|
"""
|
||||||
|
def get_registry_data(origin) do
|
||||||
|
case Registry.lookup(FedSockets.Registry, origin) do
|
||||||
|
[] ->
|
||||||
|
if is_rejected?(origin) do
|
||||||
|
Logger.debug("previously rejected fedsocket requested")
|
||||||
|
{:error, :rejected}
|
||||||
|
else
|
||||||
|
{:error, :missing}
|
||||||
|
end
|
||||||
|
|
||||||
|
[{_pid, %{state: :connected} = socket_info}] ->
|
||||||
|
{:ok, socket_info}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, :cache_error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Retrieves a map of all sockets from the Registry. The keys are the origins and the values are the corresponding SocketInfo
|
||||||
|
"""
|
||||||
|
def list_all do
|
||||||
|
(list_all_connected() ++ list_all_rejected())
|
||||||
|
|> Enum.into(%{})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp list_all_connected do
|
||||||
|
FedSockets.Registry
|
||||||
|
|> Registry.select([{{:"$1", :_, :"$3"}, [], [{{:"$1", :"$3"}}]}])
|
||||||
|
end
|
||||||
|
|
||||||
|
defp list_all_rejected do
|
||||||
|
{:ok, keys} = Cachex.keys(@rejections)
|
||||||
|
|
||||||
|
{:ok, registry_data} =
|
||||||
|
Cachex.execute(@rejections, fn worker ->
|
||||||
|
Enum.map(keys, fn k -> {k, Cachex.get!(worker, k)} end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
registry_data
|
||||||
|
end
|
||||||
|
|
||||||
|
defp clear_prior_rejection(origin),
|
||||||
|
do: Cachex.del(@rejections, origin)
|
||||||
|
|
||||||
|
defp is_rejected?(origin) do
|
||||||
|
case Cachex.get(@rejections, origin) do
|
||||||
|
{:ok, nil} ->
|
||||||
|
false
|
||||||
|
|
||||||
|
{:ok, _} ->
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_or_create_registry_data(origin) do
|
||||||
|
case get_registry_data(origin) do
|
||||||
|
{:error, :missing} ->
|
||||||
|
%SocketInfo{origin: origin}
|
||||||
|
|
||||||
|
{:ok, socket_info} ->
|
||||||
|
socket_info
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp save_registry_data(%SocketInfo{origin: origin, state: :connected} = socket_info) do
|
||||||
|
{:ok, true} = Registry.update_value(FedSockets.Registry, origin, fn _ -> socket_info end)
|
||||||
|
socket_info
|
||||||
|
end
|
||||||
|
|
||||||
|
defp save_registry_data(%SocketInfo{origin: origin, state: :rejected} = socket_info) do
|
||||||
|
rejection_expiration =
|
||||||
|
Pleroma.Config.get([:fed_sockets, :rejection_duration], @default_rejection_duration)
|
||||||
|
|
||||||
|
{:ok, true} = Cachex.put(@rejections, origin, socket_info, ttl: rejection_expiration)
|
||||||
|
socket_info
|
||||||
|
end
|
||||||
|
|
||||||
|
defp set_to_rejected(%SocketInfo{} = socket_info),
|
||||||
|
do: %SocketInfo{socket_info | state: :rejected}
|
||||||
|
end
|
@ -0,0 +1,137 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.FedSockets.FedSocket do
|
||||||
|
@moduledoc """
|
||||||
|
The FedSocket module abstracts the actions to be taken taken on connections regardless of
|
||||||
|
whether the connection started as inbound or outbound.
|
||||||
|
|
||||||
|
|
||||||
|
Normally outside modules will have no need to call the FedSocket module directly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Pleroma.Object
|
||||||
|
alias Pleroma.Object.Containment
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.ActivityPub.ObjectView
|
||||||
|
alias Pleroma.Web.ActivityPub.UserView
|
||||||
|
alias Pleroma.Web.ActivityPub.Visibility
|
||||||
|
alias Pleroma.Web.FedSockets.FetchRegistry
|
||||||
|
alias Pleroma.Web.FedSockets.IngesterWorker
|
||||||
|
alias Pleroma.Web.FedSockets.OutgoingHandler
|
||||||
|
alias Pleroma.Web.FedSockets.SocketInfo
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@shake "61dd18f7-f1e6-49a4-939a-a749fcdc1103"
|
||||||
|
|
||||||
|
def connect_to_host(uri) do
|
||||||
|
case OutgoingHandler.start_link(uri) do
|
||||||
|
{:ok, pid} ->
|
||||||
|
{:ok, pid}
|
||||||
|
|
||||||
|
error ->
|
||||||
|
{:error, error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def close(%SocketInfo{pid: socket_pid}),
|
||||||
|
do: Process.send(socket_pid, :close, [])
|
||||||
|
|
||||||
|
def publish(%SocketInfo{pid: socket_pid}, json) do
|
||||||
|
%{action: :publish, data: json}
|
||||||
|
|> Jason.encode!()
|
||||||
|
|> send_packet(socket_pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch(%SocketInfo{pid: socket_pid}, id) do
|
||||||
|
fetch_uuid = FetchRegistry.register_fetch(id)
|
||||||
|
|
||||||
|
%{action: :fetch, data: id, uuid: fetch_uuid}
|
||||||
|
|> Jason.encode!()
|
||||||
|
|> send_packet(socket_pid)
|
||||||
|
|
||||||
|
wait_for_fetch_to_return(fetch_uuid, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
def receive_package(%SocketInfo{} = fed_socket, json) do
|
||||||
|
json
|
||||||
|
|> Jason.decode!()
|
||||||
|
|> process_package(fed_socket)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp wait_for_fetch_to_return(uuid, cntr) do
|
||||||
|
case FetchRegistry.check_fetch(uuid) do
|
||||||
|
{:error, :waiting} ->
|
||||||
|
Process.sleep(:math.pow(cntr, 3) |> Kernel.trunc())
|
||||||
|
wait_for_fetch_to_return(uuid, cntr + 1)
|
||||||
|
|
||||||
|
{:error, :missing} ->
|
||||||
|
Logger.error("FedSocket fetch timed out - #{inspect(uuid)}")
|
||||||
|
{:error, :timeout}
|
||||||
|
|
||||||
|
{:ok, _fr} ->
|
||||||
|
FetchRegistry.pop_fetch(uuid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp process_package(%{"action" => "publish", "data" => data}, %{origin: origin} = _fed_socket) do
|
||||||
|
if Containment.contain_origin(origin, data) do
|
||||||
|
IngesterWorker.enqueue("ingest", %{"object" => data})
|
||||||
|
end
|
||||||
|
|
||||||
|
{:reply, %{"action" => "publish_reply", "status" => "processed"}}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp process_package(%{"action" => "fetch_reply", "uuid" => uuid, "data" => data}, _fed_socket) do
|
||||||
|
FetchRegistry.register_fetch_received(uuid, data)
|
||||||
|
{:noreply, nil}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp process_package(%{"action" => "fetch", "uuid" => uuid, "data" => ap_id}, _fed_socket) do
|
||||||
|
{:ok, data} = render_fetched_data(ap_id, uuid)
|
||||||
|
{:reply, data}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp process_package(%{"action" => "publish_reply"}, _fed_socket) do
|
||||||
|
{:noreply, nil}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp process_package(other, _fed_socket) do
|
||||||
|
Logger.warn("unknown json packages received #{inspect(other)}")
|
||||||
|
{:noreply, nil}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_fetched_data(ap_id, uuid) do
|
||||||
|
{:ok,
|
||||||
|
%{
|
||||||
|
"action" => "fetch_reply",
|
||||||
|
"status" => "processed",
|
||||||
|
"uuid" => uuid,
|
||||||
|
"data" => represent_item(ap_id)
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp represent_item(ap_id) do
|
||||||
|
case User.get_by_ap_id(ap_id) do
|
||||||
|
nil ->
|
||||||
|
object = Object.get_cached_by_ap_id(ap_id)
|
||||||
|
|
||||||
|
if Visibility.is_public?(object) do
|
||||||
|
Phoenix.View.render_to_string(ObjectView, "object.json", object: object)
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
user ->
|
||||||
|
Phoenix.View.render_to_string(UserView, "user.json", user: user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp send_packet(data, socket_pid) do
|
||||||
|
Process.send(socket_pid, {:send, data}, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def shake, do: @shake
|
||||||
|
end
|
@ -0,0 +1,185 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.FedSockets do
|
||||||
|
@moduledoc """
|
||||||
|
This documents the FedSockets framework. A framework for federating
|
||||||
|
ActivityPub objects between servers via persistant WebSocket connections.
|
||||||
|
|
||||||
|
FedSockets allow servers to authenticate on first contact and maintain that
|
||||||
|
connection, eliminating the need to authenticate every time data needs to be shared.
|
||||||
|
|
||||||
|
## Protocol
|
||||||
|
FedSockets currently support 2 types of data transfer:
|
||||||
|
* `publish` method which doesn't require a response
|
||||||
|
* `fetch` method requires a response be sent
|
||||||
|
|
||||||
|
### Publish
|
||||||
|
The publish operation sends a json encoded map of the shape:
|
||||||
|
%{action: :publish, data: json}
|
||||||
|
and accepts (but does not require) a reply of form:
|
||||||
|
%{"action" => "publish_reply"}
|
||||||
|
|
||||||
|
The outgoing params represent
|
||||||
|
* data: ActivityPub object encoded into json
|
||||||
|
|
||||||
|
|
||||||
|
### Fetch
|
||||||
|
The fetch operation sends a json encoded map of the shape:
|
||||||
|
%{action: :fetch, data: id, uuid: fetch_uuid}
|
||||||
|
and requires a reply of form:
|
||||||
|
%{"action" => "fetch_reply", "uuid" => uuid, "data" => data}
|
||||||
|
|
||||||
|
The outgoing params represent
|
||||||
|
* id: an ActivityPub object URI
|
||||||
|
* uuid: a unique uuid generated by the sender
|
||||||
|
|
||||||
|
The reply params represent
|
||||||
|
* data: an ActivityPub object encoded into json
|
||||||
|
* uuid: the uuid sent along with the fetch request
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
Clients of FedSocket transfers shouldn't need to use any of the functions outside of this module.
|
||||||
|
|
||||||
|
A typical publish operation can be performed through the following code, and a fetch operation in a similar manner.
|
||||||
|
|
||||||
|
case FedSockets.get_or_create_fed_socket(inbox) do
|
||||||
|
{:ok, fedsocket} ->
|
||||||
|
FedSockets.publish(fedsocket, json)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
alternative_publish(inbox, actor, json, params)
|
||||||
|
end
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
FedSockets have the following config settings
|
||||||
|
|
||||||
|
config :pleroma, :fed_sockets,
|
||||||
|
enabled: true,
|
||||||
|
ping_interval: :timer.seconds(15),
|
||||||
|
connection_duration: :timer.hours(1),
|
||||||
|
rejection_duration: :timer.hours(1),
|
||||||
|
fed_socket_fetches: [
|
||||||
|
default: 12_000,
|
||||||
|
interval: 3_000,
|
||||||
|
lazy: false
|
||||||
|
]
|
||||||
|
* enabled - turn FedSockets on or off with this flag. Can be toggled at runtime.
|
||||||
|
* connection_duration - How long a FedSocket can sit idle before it's culled.
|
||||||
|
* rejection_duration - After failing to make a FedSocket connection a host will be excluded
|
||||||
|
from further connections for this amount of time
|
||||||
|
* fed_socket_fetches - Use these parameters to pass options to the Cachex queue backing the FetchRegistry
|
||||||
|
* fed_socket_rejections - Use these parameters to pass options to the Cachex queue backing the FedRegistry
|
||||||
|
|
||||||
|
Cachex options are
|
||||||
|
* default: the minimum amount of time a fetch can wait before it times out.
|
||||||
|
* interval: the interval between checks for timed out entries. This plus the default represent the maximum time allowed
|
||||||
|
* lazy: leave at false for consistant and fast lookups, set to true for stricter timeout enforcement
|
||||||
|
|
||||||
|
"""
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias Pleroma.Web.FedSockets.FedRegistry
|
||||||
|
alias Pleroma.Web.FedSockets.FedSocket
|
||||||
|
alias Pleroma.Web.FedSockets.SocketInfo
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
returns a FedSocket for the given origin. Will reuse an existing one or create a new one.
|
||||||
|
|
||||||
|
address is expected to be a fully formed URL such as:
|
||||||
|
"http://www.example.com" or "http://www.example.com:8080"
|
||||||
|
|
||||||
|
It can and usually does include additional path parameters,
|
||||||
|
but these are ignored as the FedSockets are organized by host and port info alone.
|
||||||
|
"""
|
||||||
|
def get_or_create_fed_socket(address) do
|
||||||
|
with {:cache, {:error, :missing}} <- {:cache, get_fed_socket(address)},
|
||||||
|
{:connect, {:ok, _pid}} <- {:connect, FedSocket.connect_to_host(address)},
|
||||||
|
{:cache, {:ok, fed_socket}} <- {:cache, get_fed_socket(address)} do
|
||||||
|
Logger.debug("fedsocket created for - #{inspect(address)}")
|
||||||
|
{:ok, fed_socket}
|
||||||
|
else
|
||||||
|
{:cache, {:ok, socket}} ->
|
||||||
|
Logger.debug("fedsocket found in cache - #{inspect(address)}")
|
||||||
|
{:ok, socket}
|
||||||
|
|
||||||
|
{:cache, {:error, :rejected} = e} ->
|
||||||
|
e
|
||||||
|
|
||||||
|
{:connect, {:error, _host}} ->
|
||||||
|
Logger.debug("set host rejected for - #{inspect(address)}")
|
||||||
|
FedRegistry.set_host_rejected(address)
|
||||||
|
{:error, :rejected}
|
||||||
|
|
||||||
|
{_, {:error, :disabled}} ->
|
||||||
|
{:error, :disabled}
|
||||||
|
|
||||||
|
{_, {:error, reason}} ->
|
||||||
|
Logger.warn("get_or_create_fed_socket error - #{inspect(reason)}")
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
returns a FedSocket for the given origin. Will not create a new FedSocket if one does not exist.
|
||||||
|
|
||||||
|
address is expected to be a fully formed URL such as:
|
||||||
|
"http://www.example.com" or "http://www.example.com:8080"
|
||||||
|
"""
|
||||||
|
def get_fed_socket(address) do
|
||||||
|
origin = SocketInfo.origin(address)
|
||||||
|
|
||||||
|
with {:config, true} <- {:config, Pleroma.Config.get([:fed_sockets, :enabled], false)},
|
||||||
|
{:ok, socket} <- FedRegistry.get_fed_socket(origin) do
|
||||||
|
{:ok, socket}
|
||||||
|
else
|
||||||
|
{:config, _} ->
|
||||||
|
{:error, :disabled}
|
||||||
|
|
||||||
|
{:error, :rejected} ->
|
||||||
|
Logger.debug("FedSocket previously rejected - #{inspect(origin)}")
|
||||||
|
{:error, :rejected}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Sends the supplied data via the publish protocol.
|
||||||
|
It will not block waiting for a reply.
|
||||||
|
Returns :ok but this is not an indication of a successful transfer.
|
||||||
|
|
||||||
|
the data is expected to be JSON encoded binary data.
|
||||||
|
"""
|
||||||
|
def publish(%SocketInfo{} = fed_socket, json) do
|
||||||
|
FedSocket.publish(fed_socket, json)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Sends the supplied data via the fetch protocol.
|
||||||
|
It will block waiting for a reply or timeout.
|
||||||
|
|
||||||
|
Returns {:ok, object} where object is the requested object (or nil)
|
||||||
|
{:error, :timeout} in the event the message was not responded to
|
||||||
|
|
||||||
|
the id is expected to be the URI of an ActivityPub object.
|
||||||
|
"""
|
||||||
|
def fetch(%SocketInfo{} = fed_socket, id) do
|
||||||
|
FedSocket.fetch(fed_socket, id)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Disconnect all and restart FedSockets.
|
||||||
|
This is mainly used in development and testing but could be useful in production.
|
||||||
|
"""
|
||||||
|
def reset do
|
||||||
|
FedRegistry
|
||||||
|
|> Process.whereis()
|
||||||
|
|> Process.exit(:testing)
|
||||||
|
end
|
||||||
|
|
||||||
|
def uri_for_origin(origin),
|
||||||
|
do: "ws://#{origin}/api/fedsocket/v1"
|
||||||
|
end
|
@ -0,0 +1,151 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.FedSockets.FetchRegistry do
|
||||||
|
@moduledoc """
|
||||||
|
The FetchRegistry acts as a broker for fetch requests and return values.
|
||||||
|
This allows calling processes to block while waiting for a reply.
|
||||||
|
It doesn't impose it's own process instead using `Cachex` to handle fetches in process, allowing
|
||||||
|
multi threaded processes to avoid bottlenecking.
|
||||||
|
|
||||||
|
Normally outside modules will have no need to call or use the FetchRegistry themselves.
|
||||||
|
|
||||||
|
The `Cachex` parameters can be controlled from the config. Since exact timeout intervals
|
||||||
|
aren't necessary the following settings are used by default:
|
||||||
|
|
||||||
|
config :pleroma, :fed_sockets,
|
||||||
|
fed_socket_fetches: [
|
||||||
|
default: 12_000,
|
||||||
|
interval: 3_000,
|
||||||
|
lazy: false
|
||||||
|
]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
defmodule FetchRegistryData do
|
||||||
|
defstruct uuid: nil,
|
||||||
|
sent_json: nil,
|
||||||
|
received_json: nil,
|
||||||
|
sent_at: nil,
|
||||||
|
received_at: nil
|
||||||
|
end
|
||||||
|
|
||||||
|
alias Ecto.UUID
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@fetches :fed_socket_fetches
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Registers a json request wth the FetchRegistry and returns the identifying UUID.
|
||||||
|
"""
|
||||||
|
def register_fetch(json) do
|
||||||
|
%FetchRegistryData{uuid: uuid} =
|
||||||
|
json
|
||||||
|
|> new_registry_data
|
||||||
|
|> save_registry_data
|
||||||
|
|
||||||
|
uuid
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Reports on the status of a Fetch given the identifying UUID.
|
||||||
|
|
||||||
|
Will return
|
||||||
|
* {:ok, fetched_object} if a fetch has completed
|
||||||
|
* {:error, :waiting} if a fetch is still pending
|
||||||
|
* {:error, other_error} usually :missing to indicate a fetch that has timed out
|
||||||
|
"""
|
||||||
|
def check_fetch(uuid) do
|
||||||
|
case get_registry_data(uuid) do
|
||||||
|
{:ok, %FetchRegistryData{received_at: nil}} ->
|
||||||
|
{:error, :waiting}
|
||||||
|
|
||||||
|
{:ok, %FetchRegistryData{} = reg_data} ->
|
||||||
|
{:ok, reg_data}
|
||||||
|
|
||||||
|
e ->
|
||||||
|
e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Retrieves the response to a fetch given the identifying UUID.
|
||||||
|
The completed fetch will be deleted from the FetchRegistry
|
||||||
|
|
||||||
|
Will return
|
||||||
|
* {:ok, fetched_object} if a fetch has completed
|
||||||
|
* {:error, :waiting} if a fetch is still pending
|
||||||
|
* {:error, other_error} usually :missing to indicate a fetch that has timed out
|
||||||
|
"""
|
||||||
|
def pop_fetch(uuid) do
|
||||||
|
case check_fetch(uuid) do
|
||||||
|
{:ok, %FetchRegistryData{received_json: received_json}} ->
|
||||||
|
delete_registry_data(uuid)
|
||||||
|
{:ok, received_json}
|
||||||
|
|
||||||
|
e ->
|
||||||
|
e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
This is called to register a fetch has returned.
|
||||||
|
It expects the result data along with the UUID that was sent in the request
|
||||||
|
|
||||||
|
Will return the fetched object or :error
|
||||||
|
"""
|
||||||
|
def register_fetch_received(uuid, data) do
|
||||||
|
case get_registry_data(uuid) do
|
||||||
|
{:ok, %FetchRegistryData{received_at: nil} = reg_data} ->
|
||||||
|
reg_data
|
||||||
|
|> set_fetch_received(data)
|
||||||
|
|> save_registry_data()
|
||||||
|
|
||||||
|
{:ok, %FetchRegistryData{} = reg_data} ->
|
||||||
|
Logger.warn("tried to add fetched data twice - #{uuid}")
|
||||||
|
reg_data
|
||||||
|
|
||||||
|
{:error, _} ->
|
||||||
|
Logger.warn("Error adding fetch to registry - #{uuid}")
|
||||||
|
:error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp new_registry_data(json) do
|
||||||
|
%FetchRegistryData{
|
||||||
|
uuid: UUID.generate(),
|
||||||
|
sent_json: json,
|
||||||
|
sent_at: :erlang.monotonic_time(:millisecond)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_registry_data(origin) do
|
||||||
|
case Cachex.get(@fetches, origin) do
|
||||||
|
{:ok, nil} ->
|
||||||
|
{:error, :missing}
|
||||||
|
|
||||||
|
{:ok, reg_data} ->
|
||||||
|
{:ok, reg_data}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, :cache_error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp set_fetch_received(%FetchRegistryData{} = reg_data, data),
|
||||||
|
do: %FetchRegistryData{
|
||||||
|
reg_data
|
||||||
|
| received_at: :erlang.monotonic_time(:millisecond),
|
||||||
|
received_json: data
|
||||||
|
}
|
||||||
|
|
||||||
|
defp save_registry_data(%FetchRegistryData{uuid: uuid} = reg_data) do
|
||||||
|
{:ok, true} = Cachex.put(@fetches, uuid, reg_data)
|
||||||
|
reg_data
|
||||||
|
end
|
||||||
|
|
||||||
|
defp delete_registry_data(origin),
|
||||||
|
do: {:ok, true} = Cachex.del(@fetches, origin)
|
||||||
|
end
|
@ -0,0 +1,88 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.FedSockets.IncomingHandler do
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias Pleroma.Web.FedSockets.FedRegistry
|
||||||
|
alias Pleroma.Web.FedSockets.FedSocket
|
||||||
|
alias Pleroma.Web.FedSockets.SocketInfo
|
||||||
|
|
||||||
|
import HTTPSignatures, only: [validate_conn: 1, split_signature: 1]
|
||||||
|
|
||||||
|
@behaviour :cowboy_websocket
|
||||||
|
|
||||||
|
def init(req, state) do
|
||||||
|
shake = FedSocket.shake()
|
||||||
|
|
||||||
|
with true <- Pleroma.Config.get([:fed_sockets, :enabled]),
|
||||||
|
sec_protocol <- :cowboy_req.header("sec-websocket-protocol", req, nil),
|
||||||
|
headers = %{"(request-target)" => ^shake} <- :cowboy_req.headers(req),
|
||||||
|
true <- validate_conn(%{req_headers: headers}),
|
||||||
|
%{"keyId" => origin} <- split_signature(headers["signature"]) do
|
||||||
|
req =
|
||||||
|
if is_nil(sec_protocol) do
|
||||||
|
req
|
||||||
|
else
|
||||||
|
:cowboy_req.set_resp_header("sec-websocket-protocol", sec_protocol, req)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:cowboy_websocket, req, %{origin: origin}, %{}}
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
{:ok, req, state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def websocket_init(%{origin: origin}) do
|
||||||
|
case FedRegistry.add_fed_socket(origin) do
|
||||||
|
{:ok, socket_info} ->
|
||||||
|
{:ok, socket_info}
|
||||||
|
|
||||||
|
e ->
|
||||||
|
Logger.error("FedSocket websocket_init failed - #{inspect(e)}")
|
||||||
|
{:error, inspect(e)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Use the ping to check if the connection should be expired
|
||||||
|
def websocket_handle(:ping, socket_info) do
|
||||||
|
if SocketInfo.expired?(socket_info) do
|
||||||
|
{:stop, socket_info}
|
||||||
|
else
|
||||||
|
{:ok, socket_info, :hibernate}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def websocket_handle({:text, data}, socket_info) do
|
||||||
|
socket_info = SocketInfo.touch(socket_info)
|
||||||
|
|
||||||
|
case FedSocket.receive_package(socket_info, data) do
|
||||||
|
{:noreply, _} ->
|
||||||
|
{:ok, socket_info}
|
||||||
|
|
||||||
|
{:reply, reply} ->
|
||||||
|
{:reply, {:text, Jason.encode!(reply)}, socket_info}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.error("incoming error - receive_package: #{inspect(reason)}")
|
||||||
|
{:ok, socket_info}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def websocket_info({:send, message}, socket_info) do
|
||||||
|
socket_info = SocketInfo.touch(socket_info)
|
||||||
|
|
||||||
|
{:reply, {:text, message}, socket_info}
|
||||||
|
end
|
||||||
|
|
||||||
|
def websocket_info(:close, state) do
|
||||||
|
{:stop, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def websocket_info(message, state) do
|
||||||
|
Logger.debug("#{__MODULE__} unknown message #{inspect(message)}")
|
||||||
|
{:ok, state}
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,33 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.FedSockets.IngesterWorker do
|
||||||
|
use Pleroma.Workers.WorkerHelper, queue: "ingestion_queue"
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias Pleroma.Web.Federator
|
||||||
|
|
||||||
|
@impl Oban.Worker
|
||||||
|
def perform(%Job{args: %{"op" => "ingest", "object" => ingestee}}) do
|
||||||
|
try do
|
||||||
|
ingestee
|
||||||
|
|> Jason.decode!()
|
||||||
|
|> do_ingestion()
|
||||||
|
rescue
|
||||||
|
e ->
|
||||||
|
Logger.error("IngesterWorker error - #{inspect(e)}")
|
||||||
|
e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_ingestion(params) do
|
||||||
|
case Federator.incoming_ap_doc(params) do
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
|
||||||
|
{:ok, object} ->
|
||||||
|
{:ok, object}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,146 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.FedSockets.OutgoingHandler do
|
||||||
|
use GenServer
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias Pleroma.Web.ActivityPub.InternalFetchActor
|
||||||
|
alias Pleroma.Web.FedSockets
|
||||||
|
alias Pleroma.Web.FedSockets.FedRegistry
|
||||||
|
alias Pleroma.Web.FedSockets.FedSocket
|
||||||
|
alias Pleroma.Web.FedSockets.SocketInfo
|
||||||
|
|
||||||
|
def start_link(uri) do
|
||||||
|
GenServer.start_link(__MODULE__, %{uri: uri})
|
||||||
|
end
|
||||||
|
|
||||||
|
def init(%{uri: uri}) do
|
||||||
|
case initiate_connection(uri) do
|
||||||
|
{:ok, ws_origin, conn_pid} ->
|
||||||
|
FedRegistry.add_fed_socket(ws_origin, conn_pid)
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.debug("Outgoing connection failed - #{inspect(reason)}")
|
||||||
|
:ignore
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:gun_ws, conn_pid, _ref, {:text, data}}, socket_info) do
|
||||||
|
socket_info = SocketInfo.touch(socket_info)
|
||||||
|
|
||||||
|
case FedSocket.receive_package(socket_info, data) do
|
||||||
|
{:noreply, _} ->
|
||||||
|
{:noreply, socket_info}
|
||||||
|
|
||||||
|
{:reply, reply} ->
|
||||||
|
:gun.ws_send(conn_pid, {:text, Jason.encode!(reply)})
|
||||||
|
{:noreply, socket_info}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.error("incoming error - receive_package: #{inspect(reason)}")
|
||||||
|
{:noreply, socket_info}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info(:close, state) do
|
||||||
|
Logger.debug("Sending close frame !!!!!!!")
|
||||||
|
{:close, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:gun_down, _pid, _prot, :closed, _}, state) do
|
||||||
|
{:stop, :normal, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:send, data}, %{conn_pid: conn_pid} = socket_info) do
|
||||||
|
socket_info = SocketInfo.touch(socket_info)
|
||||||
|
:gun.ws_send(conn_pid, {:text, data})
|
||||||
|
{:noreply, socket_info}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:gun_ws, _, _, :pong}, state) do
|
||||||
|
{:noreply, state, :hibernate}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info(msg, state) do
|
||||||
|
Logger.debug("#{__MODULE__} unhandled event #{inspect(msg)}")
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def terminate(reason, state) do
|
||||||
|
Logger.debug(
|
||||||
|
"#{__MODULE__} terminating outgoing connection for #{inspect(state)} for #{inspect(reason)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def initiate_connection(uri) do
|
||||||
|
ws_uri =
|
||||||
|
uri
|
||||||
|
|> SocketInfo.origin()
|
||||||
|
|> FedSockets.uri_for_origin()
|
||||||
|
|
||||||
|
%{host: host, port: port, path: path} = URI.parse(ws_uri)
|
||||||
|
|
||||||
|
with {:ok, conn_pid} <- :gun.open(to_charlist(host), port),
|
||||||
|
{:ok, _} <- :gun.await_up(conn_pid),
|
||||||
|
reference <- :gun.get(conn_pid, to_charlist(path)),
|
||||||
|
{:response, :fin, 204, _} <- :gun.await(conn_pid, reference),
|
||||||
|
headers <- build_headers(uri),
|
||||||
|
ref <- :gun.ws_upgrade(conn_pid, to_charlist(path), headers, %{silence_pings: false}) do
|
||||||
|
receive do
|
||||||
|
{:gun_upgrade, ^conn_pid, ^ref, [<<"websocket">>], _} ->
|
||||||
|
{:ok, ws_uri, conn_pid}
|
||||||
|
after
|
||||||
|
15_000 ->
|
||||||
|
Logger.debug("Fedsocket timeout connecting to #{inspect(uri)}")
|
||||||
|
{:error, :timeout}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
{:response, :nofin, 404, _} ->
|
||||||
|
{:error, :fedsockets_not_supported}
|
||||||
|
|
||||||
|
e ->
|
||||||
|
Logger.debug("Fedsocket error connecting to #{inspect(uri)}")
|
||||||
|
{:error, e}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_headers(uri) do
|
||||||
|
host_for_sig = uri |> URI.parse() |> host_signature()
|
||||||
|
|
||||||
|
shake = FedSocket.shake()
|
||||||
|
digest = "SHA-256=" <> (:crypto.hash(:sha256, shake) |> Base.encode64())
|
||||||
|
date = Pleroma.Signature.signed_date()
|
||||||
|
shake_size = byte_size(shake)
|
||||||
|
|
||||||
|
signature_opts = %{
|
||||||
|
"(request-target)": shake,
|
||||||
|
"content-length": to_charlist("#{shake_size}"),
|
||||||
|
date: date,
|
||||||
|
digest: digest,
|
||||||
|
host: host_for_sig
|
||||||
|
}
|
||||||
|
|
||||||
|
signature = Pleroma.Signature.sign(InternalFetchActor.get_actor(), signature_opts)
|
||||||
|
|
||||||
|
[
|
||||||
|
{'signature', to_charlist(signature)},
|
||||||
|
{'date', date},
|
||||||
|
{'digest', to_charlist(digest)},
|
||||||
|
{'content-length', to_charlist("#{shake_size}")},
|
||||||
|
{to_charlist("(request-target)"), to_charlist(shake)}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp host_signature(%{host: host, scheme: scheme, port: port}) do
|
||||||
|
if port == URI.default_port(scheme) do
|
||||||
|
host
|
||||||
|
else
|
||||||
|
"#{host}:#{port}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,52 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.FedSockets.SocketInfo do
|
||||||
|
defstruct origin: nil,
|
||||||
|
pid: nil,
|
||||||
|
conn_pid: nil,
|
||||||
|
state: :default,
|
||||||
|
connected_until: nil
|
||||||
|
|
||||||
|
alias Pleroma.Web.FedSockets.SocketInfo
|
||||||
|
@default_connection_duration 15 * 60 * 1000
|
||||||
|
|
||||||
|
def build(uri, conn_pid \\ nil) do
|
||||||
|
uri
|
||||||
|
|> build_origin()
|
||||||
|
|> build_pids(conn_pid)
|
||||||
|
|> touch()
|
||||||
|
end
|
||||||
|
|
||||||
|
def touch(%SocketInfo{} = socket_info),
|
||||||
|
do: %{socket_info | connected_until: new_ttl()}
|
||||||
|
|
||||||
|
def connect(%SocketInfo{} = socket_info),
|
||||||
|
do: %{socket_info | state: :connected}
|
||||||
|
|
||||||
|
def expired?(%{connected_until: connected_until}),
|
||||||
|
do: connected_until < :erlang.monotonic_time(:millisecond)
|
||||||
|
|
||||||
|
def origin(uri),
|
||||||
|
do: build_origin(uri).origin
|
||||||
|
|
||||||
|
defp build_pids(socket_info, conn_pid),
|
||||||
|
do: struct(socket_info, pid: self(), conn_pid: conn_pid)
|
||||||
|
|
||||||
|
defp build_origin(uri) when is_binary(uri),
|
||||||
|
do: uri |> URI.parse() |> build_origin
|
||||||
|
|
||||||
|
defp build_origin(%{host: host, port: nil, scheme: scheme}),
|
||||||
|
do: build_origin(%{host: host, port: URI.default_port(scheme)})
|
||||||
|
|
||||||
|
defp build_origin(%{host: host, port: port}),
|
||||||
|
do: %SocketInfo{origin: "#{host}:#{port}"}
|
||||||
|
|
||||||
|
defp new_ttl do
|
||||||
|
connection_duration =
|
||||||
|
Pleroma.Config.get([:fed_sockets, :connection_duration], @default_connection_duration)
|
||||||
|
|
||||||
|
:erlang.monotonic_time(:millisecond) + connection_duration
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,59 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.FedSockets.Supervisor do
|
||||||
|
use Supervisor
|
||||||
|
import Cachex.Spec
|
||||||
|
|
||||||
|
def start_link(opts) do
|
||||||
|
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def init(args) do
|
||||||
|
children = [
|
||||||
|
build_cache(:fed_socket_fetches, args),
|
||||||
|
build_cache(:fed_socket_rejections, args),
|
||||||
|
{Registry, keys: :unique, name: FedSockets.Registry, meta: [rejected: %{}]}
|
||||||
|
]
|
||||||
|
|
||||||
|
opts = [strategy: :one_for_all, name: Pleroma.Web.Streamer.Supervisor]
|
||||||
|
Supervisor.init(children, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_cache(name, args) do
|
||||||
|
opts = get_opts(name, args)
|
||||||
|
|
||||||
|
%{
|
||||||
|
id: String.to_atom("#{name}_cache"),
|
||||||
|
start: {Cachex, :start_link, [name, opts]},
|
||||||
|
type: :worker
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_opts(cache_name, args)
|
||||||
|
when cache_name in [:fed_socket_fetches, :fed_socket_rejections] do
|
||||||
|
default = get_opts_or_config(args, cache_name, :default, 15_000)
|
||||||
|
interval = get_opts_or_config(args, cache_name, :interval, 3_000)
|
||||||
|
lazy = get_opts_or_config(args, cache_name, :lazy, false)
|
||||||
|
|
||||||
|
[expiration: expiration(default: default, interval: interval, lazy: lazy)]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_opts(name, args) do
|
||||||
|
Keyword.get(args, name, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_opts_or_config(args, name, key, default) do
|
||||||
|
args
|
||||||
|
|> Keyword.get(name, [])
|
||||||
|
|> Keyword.get(key)
|
||||||
|
|> case do
|
||||||
|
nil ->
|
||||||
|
Pleroma.Config.get([:fed_sockets, name, key], default)
|
||||||
|
|
||||||
|
value ->
|
||||||
|
value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,62 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.InstanceDocument do
|
||||||
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.Web.Endpoint
|
||||||
|
|
||||||
|
@instance_documents %{
|
||||||
|
"terms-of-service" => "/static/terms-of-service.html",
|
||||||
|
"instance-panel" => "/instance/panel.html"
|
||||||
|
}
|
||||||
|
|
||||||
|
@spec get(String.t()) :: {:ok, String.t()} | {:error, atom()}
|
||||||
|
def get(document_name) do
|
||||||
|
case Map.fetch(@instance_documents, document_name) do
|
||||||
|
{:ok, path} -> {:ok, path}
|
||||||
|
_ -> {:error, :not_found}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec put(String.t(), String.t()) :: {:ok, String.t()} | {:error, atom()}
|
||||||
|
def put(document_name, origin_path) do
|
||||||
|
with {_, {:ok, destination_path}} <-
|
||||||
|
{:instance_document, Map.fetch(@instance_documents, document_name)},
|
||||||
|
:ok <- put_file(origin_path, destination_path) do
|
||||||
|
{:ok, Path.join(Endpoint.url(), destination_path)}
|
||||||
|
else
|
||||||
|
{:instance_document, :error} -> {:error, :not_found}
|
||||||
|
error -> error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec delete(String.t()) :: :ok | {:error, atom()}
|
||||||
|
def delete(document_name) do
|
||||||
|
with {_, {:ok, path}} <- {:instance_document, Map.fetch(@instance_documents, document_name)},
|
||||||
|
instance_static_dir_path <- instance_static_dir(path),
|
||||||
|
:ok <- File.rm(instance_static_dir_path) do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
{:instance_document, :error} -> {:error, :not_found}
|
||||||
|
{:error, :enoent} -> {:error, :not_found}
|
||||||
|
error -> error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp put_file(origin_path, destination_path) do
|
||||||
|
with destination <- instance_static_dir(destination_path),
|
||||||
|
{_, :ok} <- {:mkdir_p, File.mkdir_p(Path.dirname(destination))},
|
||||||
|
{_, {:ok, _}} <- {:copy, File.copy(origin_path, destination)} do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
{error, _} -> {:error, error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp instance_static_dir(filename) do
|
||||||
|
[:instance, :static_dir]
|
||||||
|
|> Config.get!()
|
||||||
|
|> Path.join(filename)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,580 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2020-09-20 13:18+0000\n"
|
||||||
|
"PO-Revision-Date: 2020-09-20 14:48+0000\n"
|
||||||
|
"Last-Translator: Kana <gudzpoz@live.com>\n"
|
||||||
|
"Language-Team: Chinese (Simplified) <https://translate.pleroma.social/"
|
||||||
|
"projects/pleroma/pleroma/zh_Hans/>\n"
|
||||||
|
"Language: zh_Hans\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
|
"X-Generator: Weblate 4.0.4\n"
|
||||||
|
|
||||||
|
## This file is a PO Template file.
|
||||||
|
##
|
||||||
|
## `msgid`s here are often extracted from source code.
|
||||||
|
## Add new translations manually only if they're dynamic
|
||||||
|
## translations that can't be statically extracted.
|
||||||
|
##
|
||||||
|
## Run `mix gettext.extract` to bring this file up to
|
||||||
|
## date. Leave `msgstr`s empty as changing them here as no
|
||||||
|
## effect: edit them in PO (`.po`) files instead.
|
||||||
|
## From Ecto.Changeset.cast/4
|
||||||
|
msgid "can't be blank"
|
||||||
|
msgstr "不能为空"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.unique_constraint/3
|
||||||
|
msgid "has already been taken"
|
||||||
|
msgstr "已被占用"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.put_change/3
|
||||||
|
msgid "is invalid"
|
||||||
|
msgstr "不合法"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.validate_format/3
|
||||||
|
msgid "has invalid format"
|
||||||
|
msgstr "的格式不合法"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.validate_subset/3
|
||||||
|
msgid "has an invalid entry"
|
||||||
|
msgstr "中存在不合法的项目"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.validate_exclusion/3
|
||||||
|
msgid "is reserved"
|
||||||
|
msgstr "是被保留的"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.validate_confirmation/3
|
||||||
|
msgid "does not match confirmation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
## From Ecto.Changeset.no_assoc_constraint/3
|
||||||
|
msgid "is still associated with this entry"
|
||||||
|
msgstr "仍与该项目相关联"
|
||||||
|
|
||||||
|
msgid "are still associated with this entry"
|
||||||
|
msgstr "仍与该项目相关联"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.validate_length/3
|
||||||
|
msgid "should be %{count} character(s)"
|
||||||
|
msgid_plural "should be %{count} character(s)"
|
||||||
|
msgstr[0] "应为 %{count} 个字符"
|
||||||
|
|
||||||
|
msgid "should have %{count} item(s)"
|
||||||
|
msgid_plural "should have %{count} item(s)"
|
||||||
|
msgstr[0] "应有 %{item} 项"
|
||||||
|
|
||||||
|
msgid "should be at least %{count} character(s)"
|
||||||
|
msgid_plural "should be at least %{count} character(s)"
|
||||||
|
msgstr[0] "应至少有 %{count} 个字符"
|
||||||
|
|
||||||
|
msgid "should have at least %{count} item(s)"
|
||||||
|
msgid_plural "should have at least %{count} item(s)"
|
||||||
|
msgstr[0] "应至少有 %{count} 项"
|
||||||
|
|
||||||
|
msgid "should be at most %{count} character(s)"
|
||||||
|
msgid_plural "should be at most %{count} character(s)"
|
||||||
|
msgstr[0] "应至多有 %{count} 个字符"
|
||||||
|
|
||||||
|
msgid "should have at most %{count} item(s)"
|
||||||
|
msgid_plural "should have at most %{count} item(s)"
|
||||||
|
msgstr[0] "应至多有 %{count} 项"
|
||||||
|
|
||||||
|
## From Ecto.Changeset.validate_number/3
|
||||||
|
msgid "must be less than %{number}"
|
||||||
|
msgstr "必须小于 %{number}"
|
||||||
|
|
||||||
|
msgid "must be greater than %{number}"
|
||||||
|
msgstr "必须大于 %{number}"
|
||||||
|
|
||||||
|
msgid "must be less than or equal to %{number}"
|
||||||
|
msgstr "必须小于等于 %{number}"
|
||||||
|
|
||||||
|
msgid "must be greater than or equal to %{number}"
|
||||||
|
msgstr "必须大于等于 %{number}"
|
||||||
|
|
||||||
|
msgid "must be equal to %{number}"
|
||||||
|
msgstr "必须等于 %{number}"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:505
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Account not found"
|
||||||
|
msgstr "未找到账号"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:339
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Already voted"
|
||||||
|
msgstr "已经进行了投票"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:359
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Bad request"
|
||||||
|
msgstr "不正确的请求"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:426
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Can't delete object"
|
||||||
|
msgstr "不能删除对象"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/controller_helper.ex:105
|
||||||
|
#: lib/pleroma/web/controller_helper.ex:111
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Can't display this activity"
|
||||||
|
msgstr "不能显示该活动"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:285
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Can't find user"
|
||||||
|
msgstr "找不到用户"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:61
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Can't get favorites"
|
||||||
|
msgstr "不能获取收藏"
|
||||||
|
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:438
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Can't like object"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/utils.ex:563
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Cannot post an empty status without attachments"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/utils.ex:511
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Comment must be up to %{max_size} characters"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/config/config_db.ex:191
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Config with params %{params} not found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:181
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:185
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Could not delete"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:231
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Could not favorite"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:453
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Could not pin"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:278
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Could not unfavorite"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:463
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Could not unpin"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:216
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Could not unrepeat"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:512
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:521
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Could not update state"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:207
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Error."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/twitter_api.ex:106
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Invalid CAPTCHA"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:116
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:568
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Invalid credentials"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:38
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Invalid credentials."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:355
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Invalid indices"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:29
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Invalid parameters"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/utils.ex:414
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Invalid password."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:220
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Invalid request"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/twitter_api.ex:109
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Kocaptcha service unavailable"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:112
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Missing parameters"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/utils.ex:547
|
||||||
|
#, elixir-format
|
||||||
|
msgid "No such conversation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:388
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:414 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:456
|
||||||
|
#, elixir-format
|
||||||
|
msgid "No such permission_group"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/plugs/uploaded_media.ex:84
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:486 lib/pleroma/web/admin_api/controllers/fallback_controller.ex:11
|
||||||
|
#: lib/pleroma/web/feed/user_controller.ex:71 lib/pleroma/web/ostatus/ostatus_controller.ex:143
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Not found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:331
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Poll's author can't vote"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:20
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:37 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:49
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:50 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:306
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Record not found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:35
|
||||||
|
#: lib/pleroma/web/feed/user_controller.ex:77 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:36
|
||||||
|
#: lib/pleroma/web/ostatus/ostatus_controller.ex:149
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Something went wrong"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/activity_draft.ex:107
|
||||||
|
#, elixir-format
|
||||||
|
msgid "The message visibility must be direct"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/utils.ex:573
|
||||||
|
#, elixir-format
|
||||||
|
msgid "The status is over the character limit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31
|
||||||
|
#, elixir-format
|
||||||
|
msgid "This resource requires authentication."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Throttled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:356
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Too many choices"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:443
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Unhandled activity type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:485
|
||||||
|
#, elixir-format
|
||||||
|
msgid "You can't revoke your own admin status."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:221
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:308
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Your account is currently disabled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:183
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:331
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Your login is missing a confirmed e-mail address"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:390
|
||||||
|
#, elixir-format
|
||||||
|
msgid "can't read inbox of %{nickname} as %{as_nickname}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:473
|
||||||
|
#, elixir-format
|
||||||
|
msgid "can't update outbox of %{nickname} as %{as_nickname}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:471
|
||||||
|
#, elixir-format
|
||||||
|
msgid "conversation is already muted"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:314
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:492
|
||||||
|
#, elixir-format
|
||||||
|
msgid "error"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:32
|
||||||
|
#, elixir-format
|
||||||
|
msgid "mascots can only be images"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:62
|
||||||
|
#, elixir-format
|
||||||
|
msgid "not found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:394
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Bad OAuth request."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/twitter_api.ex:115
|
||||||
|
#, elixir-format
|
||||||
|
msgid "CAPTCHA already used"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/twitter_api.ex:112
|
||||||
|
#, elixir-format
|
||||||
|
msgid "CAPTCHA expired"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/plugs/uploaded_media.ex:57
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Failed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:410
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Failed to authenticate: %{message}."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:441
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Failed to set up user account."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/plugs/oauth_scopes_plug.ex:38
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Insufficient permissions: %{permissions}."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/plugs/uploaded_media.ex:104
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Internal Error"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/fallback_controller.ex:22
|
||||||
|
#: lib/pleroma/web/oauth/fallback_controller.ex:29
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Invalid Username/Password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/twitter_api.ex:118
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Invalid answer data"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:33
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Nodeinfo schema version not handled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:172
|
||||||
|
#, elixir-format
|
||||||
|
msgid "This action is outside the authorized scopes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/fallback_controller.ex:14
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Unknown error, please check the details and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:119
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:158
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Unlisted redirect_uri."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:390
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Unsupported OAuth provider: %{provider}."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/uploaders/uploader.ex:72
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Uploader callback timeout"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/uploader_controller.ex:23
|
||||||
|
#, elixir-format
|
||||||
|
msgid "bad request"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/twitter_api.ex:103
|
||||||
|
#, elixir-format
|
||||||
|
msgid "CAPTCHA Error"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:290
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Could not add reaction emoji"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/common_api/common_api.ex:301
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Could not remove reaction emoji"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/twitter_api/twitter_api.ex:129
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Invalid CAPTCHA (Missing parameter: %{name})"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92
|
||||||
|
#, elixir-format
|
||||||
|
msgid "List not found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:123
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Missing parameter: %{name}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:210
|
||||||
|
#: lib/pleroma/web/oauth/oauth_controller.ex:321
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Password reset is required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/tests/auth_test_controller.ex:9
|
||||||
|
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:6
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/config_controller.ex:6 lib/pleroma/web/admin_api/controllers/fallback_controller.ex:6
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/invite_controller.ex:6 lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex:6
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex:6 lib/pleroma/web/admin_api/controllers/relay_controller.ex:6
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/report_controller.ex:6 lib/pleroma/web/admin_api/controllers/status_controller.ex:6
|
||||||
|
#: lib/pleroma/web/controller_helper.ex:6 lib/pleroma/web/embed_controller.ex:6
|
||||||
|
#: lib/pleroma/web/fallback_redirect_controller.ex:6 lib/pleroma/web/feed/tag_controller.ex:6
|
||||||
|
#: lib/pleroma/web/feed/user_controller.ex:6 lib/pleroma/web/mailer/subscription_controller.ex:2
|
||||||
|
#: lib/pleroma/web/masto_fe_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/account_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/app_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/auth_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/filter_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/instance_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/list_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/marker_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex:14
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/media_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/notification_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/report_controller.ex:8
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/search_controller.ex:6
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/status_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:7
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:6
|
||||||
|
#: lib/pleroma/web/media_proxy/media_proxy_controller.ex:6 lib/pleroma/web/mongooseim/mongoose_im_controller.ex:6
|
||||||
|
#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:6 lib/pleroma/web/oauth/fallback_controller.ex:6
|
||||||
|
#: lib/pleroma/web/oauth/mfa_controller.ex:10 lib/pleroma/web/oauth/oauth_controller.ex:6
|
||||||
|
#: lib/pleroma/web/ostatus/ostatus_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/account_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/chat_controller.ex:5 lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:2 lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/notification_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex:6
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex:7 lib/pleroma/web/static_fe/static_fe_controller.ex:6
|
||||||
|
#: lib/pleroma/web/twitter_api/controllers/password_controller.ex:10 lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex:6
|
||||||
|
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:6 lib/pleroma/web/twitter_api/twitter_api_controller.ex:6
|
||||||
|
#: lib/pleroma/web/uploader_controller.ex:6 lib/pleroma/web/web_finger/web_finger_controller.ex:6
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:28
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Two-factor authentication enabled, you must use a access token."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:210
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Unexpected error occurred while adding file to pack."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:138
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Unexpected error occurred while creating pack."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:278
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Unexpected error occurred while removing file from pack."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:250
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Unexpected error occurred while updating file in pack."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:179
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Unexpected error occurred while updating pack metadata."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Web push subscription is disabled on this Pleroma instance"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:451
|
||||||
|
#, elixir-format
|
||||||
|
msgid "You can't revoke your own admin/moderator status."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:126
|
||||||
|
#, elixir-format
|
||||||
|
msgid "authorization required for timeline view"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24
|
||||||
|
#, elixir-format
|
||||||
|
msgid "Access denied"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:282
|
||||||
|
#, elixir-format
|
||||||
|
msgid "This API requires an authenticated user"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/pleroma/plugs/user_is_admin_plug.ex:21
|
||||||
|
#, elixir-format
|
||||||
|
msgid "User is not an admin."
|
||||||
|
msgstr ""
|
@ -1 +1 @@
|
|||||||
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link href=/static/css/app.77b1644622e3bae24b6b.css rel=stylesheet><link href=/static/fontello.1599568314856.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.90c4af83c1ae68f4cd95.js></script><script type=text/javascript src=/static/js/app.55d173dc5e39519aa518.js></script></body></html>
|
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link href=/static/css/app.77b1644622e3bae24b6b.css rel=stylesheet><link href=/static/fontello.1600365488745.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.90c4af83c1ae68f4cd95.js></script><script type=text/javascript src=/static/js/app.826c44232e0a76bbd9ba.js></script></body></html>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
<h2>Custom instance panel</h2>
|
After Width: | Height: | Size: 978 KiB |
After Width: | Height: | Size: 102 KiB |
@ -0,0 +1,219 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.AdminAPI.ChatControllerTest do
|
||||||
|
use Pleroma.Web.ConnCase
|
||||||
|
|
||||||
|
import Pleroma.Factory
|
||||||
|
|
||||||
|
alias Pleroma.Chat
|
||||||
|
alias Pleroma.Chat.MessageReference
|
||||||
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.ModerationLog
|
||||||
|
alias Pleroma.Object
|
||||||
|
alias Pleroma.Repo
|
||||||
|
alias Pleroma.Web.CommonAPI
|
||||||
|
|
||||||
|
defp admin_setup do
|
||||||
|
admin = insert(:user, is_admin: true)
|
||||||
|
token = insert(:oauth_admin_token, user: admin)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> assign(:user, admin)
|
||||||
|
|> assign(:token, token)
|
||||||
|
|
||||||
|
{:ok, %{admin: admin, token: token, conn: conn}}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "DELETE /api/pleroma/admin/chats/:id/messages/:message_id" do
|
||||||
|
setup do: admin_setup()
|
||||||
|
|
||||||
|
test "it deletes a message from the chat", %{conn: conn, admin: admin} do
|
||||||
|
user = insert(:user)
|
||||||
|
recipient = insert(:user)
|
||||||
|
|
||||||
|
{:ok, message} =
|
||||||
|
CommonAPI.post_chat_message(user, recipient, "Hello darkness my old friend")
|
||||||
|
|
||||||
|
object = Object.normalize(message, false)
|
||||||
|
|
||||||
|
chat = Chat.get(user.id, recipient.ap_id)
|
||||||
|
recipient_chat = Chat.get(recipient.id, user.ap_id)
|
||||||
|
|
||||||
|
cm_ref = MessageReference.for_chat_and_object(chat, object)
|
||||||
|
recipient_cm_ref = MessageReference.for_chat_and_object(recipient_chat, object)
|
||||||
|
|
||||||
|
result =
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
log_entry = Repo.one(ModerationLog)
|
||||||
|
|
||||||
|
assert ModerationLog.get_log_entry_message(log_entry) ==
|
||||||
|
"@#{admin.nickname} deleted chat message ##{cm_ref.id}"
|
||||||
|
|
||||||
|
assert result["id"] == cm_ref.id
|
||||||
|
refute MessageReference.get_by_id(cm_ref.id)
|
||||||
|
refute MessageReference.get_by_id(recipient_cm_ref.id)
|
||||||
|
assert %{data: %{"type" => "Tombstone"}} = Object.get_by_id(object.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GET /api/pleroma/admin/chats/:id/messages" do
|
||||||
|
setup do: admin_setup()
|
||||||
|
|
||||||
|
test "it paginates", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
recipient = insert(:user)
|
||||||
|
|
||||||
|
Enum.each(1..30, fn _ ->
|
||||||
|
{:ok, _} = CommonAPI.post_chat_message(user, recipient, "hey")
|
||||||
|
end)
|
||||||
|
|
||||||
|
chat = Chat.get(user.id, recipient.ap_id)
|
||||||
|
|
||||||
|
result =
|
||||||
|
conn
|
||||||
|
|> get("/api/pleroma/admin/chats/#{chat.id}/messages")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert length(result) == 20
|
||||||
|
|
||||||
|
result =
|
||||||
|
conn
|
||||||
|
|> get("/api/pleroma/admin/chats/#{chat.id}/messages?max_id=#{List.last(result)["id"]}")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert length(result) == 10
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it returns the messages for a given chat", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
third_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, _} = CommonAPI.post_chat_message(user, other_user, "hey")
|
||||||
|
{:ok, _} = CommonAPI.post_chat_message(user, third_user, "hey")
|
||||||
|
{:ok, _} = CommonAPI.post_chat_message(user, other_user, "how are you?")
|
||||||
|
{:ok, _} = CommonAPI.post_chat_message(other_user, user, "fine, how about you?")
|
||||||
|
|
||||||
|
chat = Chat.get(user.id, other_user.ap_id)
|
||||||
|
|
||||||
|
result =
|
||||||
|
conn
|
||||||
|
|> get("/api/pleroma/admin/chats/#{chat.id}/messages")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
result
|
||||||
|
|> Enum.each(fn message ->
|
||||||
|
assert message["chat_id"] == chat.id |> to_string()
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert length(result) == 3
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GET /api/pleroma/admin/chats/:id" do
|
||||||
|
setup do: admin_setup()
|
||||||
|
|
||||||
|
test "it returns a chat", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
|
||||||
|
|
||||||
|
result =
|
||||||
|
conn
|
||||||
|
|> get("/api/pleroma/admin/chats/#{chat.id}")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert result["id"] == to_string(chat.id)
|
||||||
|
assert %{} = result["sender"]
|
||||||
|
assert %{} = result["receiver"]
|
||||||
|
refute result["account"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "unauthorized chat moderation" do
|
||||||
|
setup do
|
||||||
|
user = insert(:user)
|
||||||
|
recipient = insert(:user)
|
||||||
|
|
||||||
|
{:ok, message} = CommonAPI.post_chat_message(user, recipient, "Yo")
|
||||||
|
object = Object.normalize(message, false)
|
||||||
|
chat = Chat.get(user.id, recipient.ap_id)
|
||||||
|
cm_ref = MessageReference.for_chat_and_object(chat, object)
|
||||||
|
|
||||||
|
%{conn: conn} = oauth_access(["read:chats", "write:chats"])
|
||||||
|
%{conn: conn, chat: chat, cm_ref: cm_ref}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "DELETE /api/pleroma/admin/chats/:id/messages/:message_id", %{
|
||||||
|
conn: conn,
|
||||||
|
chat: chat,
|
||||||
|
cm_ref: cm_ref
|
||||||
|
} do
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}")
|
||||||
|
|> json_response(403)
|
||||||
|
|
||||||
|
assert MessageReference.get_by_id(cm_ref.id) == cm_ref
|
||||||
|
end
|
||||||
|
|
||||||
|
test "GET /api/pleroma/admin/chats/:id/messages", %{conn: conn, chat: chat} do
|
||||||
|
conn
|
||||||
|
|> get("/api/pleroma/admin/chats/#{chat.id}/messages")
|
||||||
|
|> json_response(403)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "GET /api/pleroma/admin/chats/:id", %{conn: conn, chat: chat} do
|
||||||
|
conn
|
||||||
|
|> get("/api/pleroma/admin/chats/#{chat.id}")
|
||||||
|
|> json_response(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "unauthenticated chat moderation" do
|
||||||
|
setup do
|
||||||
|
user = insert(:user)
|
||||||
|
recipient = insert(:user)
|
||||||
|
|
||||||
|
{:ok, message} = CommonAPI.post_chat_message(user, recipient, "Yo")
|
||||||
|
object = Object.normalize(message, false)
|
||||||
|
chat = Chat.get(user.id, recipient.ap_id)
|
||||||
|
cm_ref = MessageReference.for_chat_and_object(chat, object)
|
||||||
|
|
||||||
|
%{conn: build_conn(), chat: chat, cm_ref: cm_ref}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "DELETE /api/pleroma/admin/chats/:id/messages/:message_id", %{
|
||||||
|
conn: conn,
|
||||||
|
chat: chat,
|
||||||
|
cm_ref: cm_ref
|
||||||
|
} do
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}")
|
||||||
|
|> json_response(403)
|
||||||
|
|
||||||
|
assert MessageReference.get_by_id(cm_ref.id) == cm_ref
|
||||||
|
end
|
||||||
|
|
||||||
|
test "GET /api/pleroma/admin/chats/:id/messages", %{conn: conn, chat: chat} do
|
||||||
|
conn
|
||||||
|
|> get("/api/pleroma/admin/chats/#{chat.id}/messages")
|
||||||
|
|> json_response(403)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "GET /api/pleroma/admin/chats/:id", %{conn: conn, chat: chat} do
|
||||||
|
conn
|
||||||
|
|> get("/api/pleroma/admin/chats/#{chat.id}")
|
||||||
|
|> json_response(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue