# 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
