commit
f3a1f9c3bb
@ -1,74 +0,0 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.ActivityExpiration do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.ActivityExpiration
|
||||
alias Pleroma.Repo
|
||||
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
@min_activity_lifetime :timer.hours(1)
|
||||
|
||||
schema "activity_expirations" do
|
||||
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
|
||||
field(:scheduled_at, :naive_datetime)
|
||||
end
|
||||
|
||||
def changeset(%ActivityExpiration{} = expiration, attrs, validate_scheduled_at) do
|
||||
expiration
|
||||
|> cast(attrs, [:scheduled_at])
|
||||
|> validate_required([:scheduled_at])
|
||||
|> validate_scheduled_at(validate_scheduled_at)
|
||||
end
|
||||
|
||||
def get_by_activity_id(activity_id) do
|
||||
ActivityExpiration
|
||||
|> where([exp], exp.activity_id == ^activity_id)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
def create(%Activity{} = activity, scheduled_at, validate_scheduled_at \\ true) do
|
||||
%ActivityExpiration{activity_id: activity.id}
|
||||
|> changeset(%{scheduled_at: scheduled_at}, validate_scheduled_at)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
def due_expirations(offset \\ 0) do
|
||||
naive_datetime =
|
||||
NaiveDateTime.utc_now()
|
||||
|> NaiveDateTime.add(offset, :millisecond)
|
||||
|
||||
ActivityExpiration
|
||||
|> where([exp], exp.scheduled_at < ^naive_datetime)
|
||||
|> limit(50)
|
||||
|> preload(:activity)
|
||||
|> Repo.all()
|
||||
|> Enum.reject(fn %{activity: activity} ->
|
||||
Activity.pinned_by_actor?(activity)
|
||||
end)
|
||||
end
|
||||
|
||||
def validate_scheduled_at(changeset, false), do: changeset
|
||||
|
||||
def validate_scheduled_at(changeset, true) do
|
||||
validate_change(changeset, :scheduled_at, fn _, scheduled_at ->
|
||||
if not expires_late_enough?(scheduled_at) do
|
||||
[scheduled_at: "an ephemeral activity must live for at least one hour"]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def expires_late_enough?(scheduled_at) do
|
||||
now = NaiveDateTime.utc_now()
|
||||
diff = NaiveDateTime.diff(scheduled_at, now, :millisecond)
|
||||
diff > @min_activity_lifetime
|
||||
end
|
||||
end
|
@ -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
|
@ -1,34 +0,0 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.RepoStreamer do
|
||||
alias Pleroma.Repo
|
||||
import Ecto.Query
|
||||
|
||||
def chunk_stream(query, chunk_size) do
|
||||
Stream.unfold(0, fn
|
||||
:halt ->
|
||||
{[], :halt}
|
||||
|
||||
last_id ->
|
||||
query
|
||||
|> order_by(asc: :id)
|
||||
|> where([r], r.id > ^last_id)
|
||||
|> limit(^chunk_size)
|
||||
|> Repo.all()
|
||||
|> case do
|
||||
[] ->
|
||||
{[], :halt}
|
||||
|
||||
records ->
|
||||
last_id = List.last(records).id
|
||||
{records, last_id}
|
||||
end
|
||||
end)
|
||||
|> Stream.take_while(fn
|
||||
[] -> false
|
||||
_ -> true
|
||||
end)
|
||||
end
|
||||
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.User.Import do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Workers.BackgroundWorker
|
||||
|
||||
require Logger
|
||||
|
||||
@spec perform(atom(), User.t(), list()) :: :ok | list() | {:error, any()}
|
||||
def perform(:mutes_import, %User{} = user, [_ | _] = identifiers) do
|
||||
Enum.map(
|
||||
identifiers,
|
||||
fn identifier ->
|
||||
with {:ok, %User{} = muted_user} <- User.get_or_fetch(identifier),
|
||||
{:ok, _} <- User.mute(user, muted_user) do
|
||||
muted_user
|
||||
else
|
||||
error -> handle_error(:mutes_import, identifier, error)
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def perform(:blocks_import, %User{} = blocker, [_ | _] = identifiers) do
|
||||
Enum.map(
|
||||
identifiers,
|
||||
fn identifier ->
|
||||
with {:ok, %User{} = blocked} <- User.get_or_fetch(identifier),
|
||||
{:ok, _block} <- CommonAPI.block(blocker, blocked) do
|
||||
blocked
|
||||
else
|
||||
error -> handle_error(:blocks_import, identifier, error)
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def perform(:follow_import, %User{} = follower, [_ | _] = identifiers) do
|
||||
Enum.map(
|
||||
identifiers,
|
||||
fn identifier ->
|
||||
with {:ok, %User{} = followed} <- User.get_or_fetch(identifier),
|
||||
{:ok, follower} <- User.maybe_direct_follow(follower, followed),
|
||||
{:ok, _, _, _} <- CommonAPI.follow(follower, followed) do
|
||||
followed
|
||||
else
|
||||
error -> handle_error(:follow_import, identifier, error)
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def perform(_, _, _), do: :ok
|
||||
|
||||
defp handle_error(op, user_id, error) do
|
||||
Logger.debug("#{op} failed for #{user_id} with: #{inspect(error)}")
|
||||
error
|
||||
end
|
||||
|
||||
def blocks_import(%User{} = blocker, [_ | _] = identifiers) do
|
||||
BackgroundWorker.enqueue(
|
||||
"blocks_import",
|
||||
%{"user_id" => blocker.id, "identifiers" => identifiers}
|
||||
)
|
||||
end
|
||||
|
||||
def follow_import(%User{} = follower, [_ | _] = identifiers) do
|
||||
BackgroundWorker.enqueue(
|
||||
"follow_import",
|
||||
%{"user_id" => follower.id, "identifiers" => identifiers}
|
||||
)
|
||||
end
|
||||
|
||||
def mutes_import(%User{} = user, [_ | _] = identifiers) do
|
||||
BackgroundWorker.enqueue(
|
||||
"mutes_import",
|
||||
%{"user_id" => user.id, "identifiers" => identifiers}
|
||||
)
|
||||
end
|
||||
end
|
@ -0,0 +1,134 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.EarmarkRenderer
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key false
|
||||
@derive Jason.Encoder
|
||||
|
||||
embedded_schema do
|
||||
field(:id, ObjectValidators.ObjectID, primary_key: true)
|
||||
field(:to, ObjectValidators.Recipients, default: [])
|
||||
field(:cc, ObjectValidators.Recipients, default: [])
|
||||
field(:bto, ObjectValidators.Recipients, default: [])
|
||||
field(:bcc, ObjectValidators.Recipients, default: [])
|
||||
# TODO: Write type
|
||||
field(:tag, {:array, :map}, default: [])
|
||||
field(:type, :string)
|
||||
|
||||
field(:name, :string)
|
||||
field(:summary, :string)
|
||||
field(:content, :string)
|
||||
|
||||
field(:context, :string)
|
||||
# short identifier for PleromaFE to group statuses by context
|
||||
field(:context_id, :integer)
|
||||
|
||||
# TODO: Remove actor on objects
|
||||
field(:actor, ObjectValidators.ObjectID)
|
||||
|
||||
field(:attributedTo, ObjectValidators.ObjectID)
|
||||
field(:published, ObjectValidators.DateTime)
|
||||
field(:emoji, ObjectValidators.Emoji, default: %{})
|
||||
field(:sensitive, :boolean, default: false)
|
||||
embeds_many(:attachment, AttachmentValidator)
|
||||
field(:replies_count, :integer, default: 0)
|
||||
field(:like_count, :integer, default: 0)
|
||||
field(:announcement_count, :integer, default: 0)
|
||||
field(:inReplyTo, ObjectValidators.ObjectID)
|
||||
field(:url, ObjectValidators.Uri)
|
||||
|
||||
field(:likes, {:array, ObjectValidators.ObjectID}, default: [])
|
||||
field(:announcements, {:array, ObjectValidators.ObjectID}, default: [])
|
||||
end
|
||||
|
||||
def cast_and_apply(data) do
|
||||
data
|
||||
|> cast_data
|
||||
|> apply_action(:insert)
|
||||
end
|
||||
|
||||
def cast_and_validate(data) do
|
||||
data
|
||||
|> cast_data()
|
||||
|> validate_data()
|
||||
end
|
||||
|
||||
def cast_data(data) do
|
||||
%__MODULE__{}
|
||||
|> changeset(data)
|
||||
end
|
||||
|
||||
defp fix_url(%{"url" => url} = data) when is_list(url) do
|
||||
attachment =
|
||||
Enum.find(url, fn x ->
|
||||
mime_type = x["mimeType"] || x["mediaType"] || ""
|
||||
|
||||
is_map(x) and String.starts_with?(mime_type, ["video/", "audio/"])
|
||||
end)
|
||||
|
||||
link_element =
|
||||
Enum.find(url, fn x ->
|
||||
mime_type = x["mimeType"] || x["mediaType"] || ""
|
||||
|
||||
is_map(x) and mime_type == "text/html"
|
||||
end)
|
||||
|
||||
data
|
||||
|> Map.put("attachment", [attachment])
|
||||
|> Map.put("url", link_element["href"])
|
||||
end
|
||||
|
||||
defp fix_url(data), do: data
|
||||
|
||||
defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = data)
|
||||
when is_binary(content) do
|
||||
content =
|
||||
content
|
||||
|> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer})
|
||||
|> Pleroma.HTML.filter_tags()
|
||||
|
||||
Map.put(data, "content", content)
|
||||
end
|
||||
|
||||
defp fix_content(data), do: data
|
||||
|
||||
defp fix(data) do
|
||||
data
|
||||
|> CommonFixes.fix_defaults()
|
||||
|> CommonFixes.fix_attribution()
|
||||
|> CommonFixes.fix_actor()
|
||||
|> Transmogrifier.fix_emoji()
|
||||
|> fix_url()
|
||||
|> fix_content()
|
||||
end
|
||||
|
||||
def changeset(struct, data) do
|
||||
data = fix(data)
|
||||
|
||||
struct
|
||||
|> cast(data, __schema__(:fields) -- [:attachment])
|
||||
|> cast_embed(:attachment)
|
||||
end
|
||||
|
||||
def validate_data(data_cng) do
|
||||
data_cng
|
||||
|> validate_inclusion(:type, ["Audio", "Video"])
|
||||
|> validate_required([:id, :actor, :attributedTo, :type, :context, :attachment])
|
||||
|> CommonValidations.validate_any_presence([:cc, :to])
|
||||
|> CommonValidations.validate_fields_match([:actor, :attributedTo])
|
||||
|> CommonValidations.validate_actor_presence()
|
||||
|> CommonValidations.validate_host_match()
|
||||
end
|
||||
end
|
@ -1,73 +0,0 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:id, ObjectValidators.ObjectID, primary_key: true)
|
||||
field(:to, ObjectValidators.Recipients, default: [])
|
||||
field(:cc, ObjectValidators.Recipients, default: [])
|
||||
field(:bto, ObjectValidators.Recipients, default: [])
|
||||
field(:bcc, ObjectValidators.Recipients, default: [])
|
||||
# TODO: Write type
|
||||
field(:tag, {:array, :map}, default: [])
|
||||
field(:type, :string)
|
||||
|
||||
field(:name, :string)
|
||||
field(:summary, :string)
|
||||
field(:content, :string)
|
||||
|
||||
field(:context, :string)
|
||||
# short identifier for PleromaFE to group statuses by context
|
||||
field(:context_id, :integer)
|
||||
|
||||
field(:actor, ObjectValidators.ObjectID)
|
||||
field(:attributedTo, ObjectValidators.ObjectID)
|
||||
field(:published, ObjectValidators.DateTime)
|
||||
field(:emoji, ObjectValidators.Emoji, default: %{})
|
||||
field(:sensitive, :boolean, default: false)
|
||||
# TODO: Write type
|
||||
field(:attachment, {:array, :map}, default: [])
|
||||
field(:replies_count, :integer, default: 0)
|
||||
field(:like_count, :integer, default: 0)
|
||||
field(:announcement_count, :integer, default: 0)
|
||||
field(:inReplyTo, ObjectValidators.ObjectID)
|
||||
field(:url, ObjectValidators.Uri)
|
||||
|
||||
field(:likes, {:array, :string}, default: [])
|
||||
field(:announcements, {:array, :string}, default: [])
|
||||
end
|
||||
|
||||
def cast_and_validate(data) do
|
||||
data
|
||||
|> cast_data()
|
||||
|> validate_data()
|
||||
end
|
||||
|
||||
defp fix(data) do
|
||||
data
|
||||
|> Transmogrifier.fix_emoji()
|
||||
end
|
||||
|
||||
def cast_data(data) do
|
||||
data = fix(data)
|
||||
|
||||
%__MODULE__{}
|
||||
|> cast(data, __schema__(:fields))
|
||||
end
|
||||
|
||||
def validate_data(data_cng) do
|
||||
data_cng
|
||||
|> validate_inclusion(:type, ["Note"])
|
||||
|> validate_required([:id, :actor, :to, :cc, :type, :content, :context])
|
||||
end
|
||||
end
|
@ -1,24 +0,0 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
|
||||
import Ecto.Changeset
|
||||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:type, :string)
|
||||
field(:href, ObjectValidators.Uri)
|
||||
field(:mediaType, :string, default: "application/octet-stream")
|
||||
end
|
||||
|
||||
def changeset(struct, data) do
|
||||
struct
|
||||
|> cast(data, __schema__(:fields))
|
||||
|> validate_required([:type, :href, :mediaType])
|
||||
end
|
||||
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,139 @@
|
||||
# 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.PleromaEmojiFileOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
summary: "Add new file to the pack",
|
||||
operationId: "PleromaAPI.EmojiPackController.add_file",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
requestBody: request_body("Parameters", create_request(), required: true),
|
||||
parameters: [name_param()],
|
||||
responses: %{
|
||||
200 => Operation.response("Files Object", "application/json", files_object()),
|
||||
422 => Operation.response("Unprocessable Entity", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
409 => Operation.response("Conflict", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp create_request do
|
||||
%Schema{
|
||||
type: :object,
|
||||
required: [:file],
|
||||
properties: %{
|
||||
file: %Schema{
|
||||
description:
|
||||
"File needs to be uploaded with the multipart request or link to remote file",
|
||||
anyOf: [
|
||||
%Schema{type: :string, format: :binary},
|
||||
%Schema{type: :string, format: :uri}
|
||||
]
|
||||
},
|
||||
shortcode: %Schema{
|
||||
type: :string,
|
||||
description:
|
||||
"Shortcode for new emoji, must be unique for all emoji. If not sended, shortcode will be taken from original filename."
|
||||
},
|
||||
filename: %Schema{
|
||||
type: :string,
|
||||
description:
|
||||
"New emoji file name. If not specified will be taken from original filename."
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
summary: "Add new file to the pack",
|
||||
operationId: "PleromaAPI.EmojiPackController.update_file",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
requestBody: request_body("Parameters", update_request(), required: true),
|
||||
parameters: [name_param()],
|
||||
responses: %{
|
||||
200 => Operation.response("Files Object", "application/json", files_object()),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
409 => Operation.response("Conflict", "application/json", ApiError),
|
||||
422 => Operation.response("Unprocessable Entity", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp update_request do
|
||||
%Schema{
|
||||
type: :object,
|
||||
required: [:shortcode, :new_shortcode, :new_filename],
|
||||
properties: %{
|
||||
shortcode: %Schema{
|
||||
type: :string,
|
||||
description: "Emoji file shortcode"
|
||||
},
|
||||
new_shortcode: %Schema{
|
||||
type: :string,
|
||||
description: "New emoji file shortcode"
|
||||
},
|
||||
new_filename: %Schema{
|
||||
type: :string,
|
||||
description: "New filename for emoji file"
|
||||
},
|
||||
force: %Schema{
|
||||
type: :boolean,
|
||||
description: "With true value to overwrite existing emoji with new shortcode",
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
summary: "Delete emoji file from pack",
|
||||
operationId: "PleromaAPI.EmojiPackController.delete_file",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
parameters: [
|
||||
name_param(),
|
||||
Operation.parameter(:shortcode, :query, :string, "File shortcode",
|
||||
example: "cofe",
|
||||
required: true
|
||||
)
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("Files Object", "application/json", files_object()),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError),
|
||||
422 => Operation.response("Unprocessable Entity", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp name_param do
|
||||
Operation.parameter(:name, :path, :string, "Pack Name", example: "cofe", required: true)
|
||||
end
|
||||
|
||||
defp files_object do
|
||||
%Schema{
|
||||
type: :object,
|
||||
additionalProperties: %Schema{type: :string},
|
||||
description: "Object with emoji names as keys and filenames as values"
|
||||
}
|
||||
end
|
||||
end
|
@ -0,0 +1,80 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.UserImportOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
@spec open_api_operation(atom) :: Operation.t()
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def follow_operation do
|
||||
%Operation{
|
||||
tags: ["follow_import"],
|
||||
summary: "Imports your follows.",
|
||||
operationId: "UserImportController.follow",
|
||||
requestBody: request_body("Parameters", import_request(), required: true),
|
||||
responses: %{
|
||||
200 => ok_response(),
|
||||
500 => Operation.response("Error", "application/json", ApiError)
|
||||
},
|
||||
security: [%{"oAuth" => ["write:follow"]}]
|
||||
}
|
||||
end
|
||||
|
||||
def blocks_operation do
|
||||
%Operation{
|
||||
tags: ["blocks_import"],
|
||||
summary: "Imports your blocks.",
|
||||
operationId: "UserImportController.blocks",
|
||||
requestBody: request_body("Parameters", import_request(), required: true),
|
||||
responses: %{
|
||||
200 => ok_response(),
|
||||
500 => Operation.response("Error", "application/json", ApiError)
|
||||
},
|
||||
security: [%{"oAuth" => ["write:blocks"]}]
|
||||
}
|
||||
end
|
||||
|
||||
def mutes_operation do
|
||||
%Operation{
|
||||
tags: ["mutes_import"],
|
||||
summary: "Imports your mutes.",
|
||||
operationId: "UserImportController.mutes",
|
||||
requestBody: request_body("Parameters", import_request(), required: true),
|
||||
responses: %{
|
||||
200 => ok_response(),
|
||||
500 => Operation.response("Error", "application/json", ApiError)
|
||||
},
|
||||
security: [%{"oAuth" => ["write:mutes"]}]
|
||||
}
|
||||
end
|
||||
|
||||
defp import_request do
|
||||
%Schema{
|
||||
type: :object,
|
||||
required: [:list],
|
||||
properties: %{
|
||||
list: %Schema{
|
||||
description:
|
||||
"STRING or FILE containing a whitespace-separated list of accounts to import.",
|
||||
anyOf: [
|
||||
%Schema{type: :string, format: :binary},
|
||||
%Schema{type: :string}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp ok_response do
|
||||
Operation.response("Ok", "application/json", %Schema{type: :string, example: "ok"})
|
||||
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,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.OutgoingHandler do
|
||||
use GenServer
|
||||
|
||||
require Logger
|
||||
|
||||
alias Pleroma.Application
|
||||
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, %{protocols: [:http]}),
|
||||
{:ok, _} <- :gun.await_up(conn_pid),
|
||||
reference <-
|
||||
:gun.get(conn_pid, to_charlist(path), [
|
||||
{'user-agent', to_charlist(Application.user_agent())}
|
||||
]),
|
||||
{: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)},
|
||||
{'user-agent', to_charlist(Application.user_agent())}
|
||||
]
|
||||
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
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue