commit
6c59fe259d
@ -0,0 +1,79 @@
|
|||||||
|
defmodule Pleroma.Gun.ConnectionPool do
|
||||||
|
@registry __MODULE__
|
||||||
|
|
||||||
|
alias Pleroma.Gun.ConnectionPool.WorkerSupervisor
|
||||||
|
|
||||||
|
def children do
|
||||||
|
[
|
||||||
|
{Registry, keys: :unique, name: @registry},
|
||||||
|
Pleroma.Gun.ConnectionPool.WorkerSupervisor
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_conn(uri, opts) do
|
||||||
|
key = "#{uri.scheme}:#{uri.host}:#{uri.port}"
|
||||||
|
|
||||||
|
case Registry.lookup(@registry, key) do
|
||||||
|
# The key has already been registered, but connection is not up yet
|
||||||
|
[{worker_pid, nil}] ->
|
||||||
|
get_gun_pid_from_worker(worker_pid, true)
|
||||||
|
|
||||||
|
[{worker_pid, {gun_pid, _used_by, _crf, _last_reference}}] ->
|
||||||
|
GenServer.cast(worker_pid, {:add_client, self(), false})
|
||||||
|
{:ok, gun_pid}
|
||||||
|
|
||||||
|
[] ->
|
||||||
|
# :gun.set_owner fails in :connected state for whatevever reason,
|
||||||
|
# so we open the connection in the process directly and send it's pid back
|
||||||
|
# We trust gun to handle timeouts by itself
|
||||||
|
case WorkerSupervisor.start_worker([key, uri, opts, self()]) do
|
||||||
|
{:ok, worker_pid} ->
|
||||||
|
get_gun_pid_from_worker(worker_pid, false)
|
||||||
|
|
||||||
|
{:error, {:already_started, worker_pid}} ->
|
||||||
|
get_gun_pid_from_worker(worker_pid, true)
|
||||||
|
|
||||||
|
err ->
|
||||||
|
err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_gun_pid_from_worker(worker_pid, register) do
|
||||||
|
# GenServer.call will block the process for timeout length if
|
||||||
|
# the server crashes on startup (which will happen if gun fails to connect)
|
||||||
|
# so instead we use cast + monitor
|
||||||
|
|
||||||
|
ref = Process.monitor(worker_pid)
|
||||||
|
if register, do: GenServer.cast(worker_pid, {:add_client, self(), true})
|
||||||
|
|
||||||
|
receive do
|
||||||
|
{:conn_pid, pid} ->
|
||||||
|
Process.demonitor(ref)
|
||||||
|
{:ok, pid}
|
||||||
|
|
||||||
|
{:DOWN, ^ref, :process, ^worker_pid, reason} ->
|
||||||
|
case reason do
|
||||||
|
{:shutdown, error} -> error
|
||||||
|
_ -> {:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def release_conn(conn_pid) do
|
||||||
|
# :ets.fun2ms(fn {_, {worker_pid, {gun_pid, _, _, _}}} when gun_pid == conn_pid ->
|
||||||
|
# worker_pid end)
|
||||||
|
query_result =
|
||||||
|
Registry.select(@registry, [
|
||||||
|
{{:_, :"$1", {:"$2", :_, :_, :_}}, [{:==, :"$2", conn_pid}], [:"$1"]}
|
||||||
|
])
|
||||||
|
|
||||||
|
case query_result do
|
||||||
|
[worker_pid] ->
|
||||||
|
GenServer.cast(worker_pid, {:remove_client, self()})
|
||||||
|
|
||||||
|
[] ->
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,85 @@
|
|||||||
|
defmodule Pleroma.Gun.ConnectionPool.Reclaimer do
|
||||||
|
use GenServer, restart: :temporary
|
||||||
|
|
||||||
|
@registry Pleroma.Gun.ConnectionPool
|
||||||
|
|
||||||
|
def start_monitor do
|
||||||
|
pid =
|
||||||
|
case :gen_server.start(__MODULE__, [], name: {:via, Registry, {@registry, "reclaimer"}}) do
|
||||||
|
{:ok, pid} ->
|
||||||
|
pid
|
||||||
|
|
||||||
|
{:error, {:already_registered, pid}} ->
|
||||||
|
pid
|
||||||
|
end
|
||||||
|
|
||||||
|
{pid, Process.monitor(pid)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(_) do
|
||||||
|
{:ok, nil, {:continue, :reclaim}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_continue(:reclaim, _) do
|
||||||
|
max_connections = Pleroma.Config.get([:connections_pool, :max_connections])
|
||||||
|
|
||||||
|
reclaim_max =
|
||||||
|
[:connections_pool, :reclaim_multiplier]
|
||||||
|
|> Pleroma.Config.get()
|
||||||
|
|> Kernel.*(max_connections)
|
||||||
|
|> round
|
||||||
|
|> max(1)
|
||||||
|
|
||||||
|
:telemetry.execute([:pleroma, :connection_pool, :reclaim, :start], %{}, %{
|
||||||
|
max_connections: max_connections,
|
||||||
|
reclaim_max: reclaim_max
|
||||||
|
})
|
||||||
|
|
||||||
|
# :ets.fun2ms(
|
||||||
|
# fn {_, {worker_pid, {_, used_by, crf, last_reference}}} when used_by == [] ->
|
||||||
|
# {worker_pid, crf, last_reference} end)
|
||||||
|
unused_conns =
|
||||||
|
Registry.select(
|
||||||
|
@registry,
|
||||||
|
[
|
||||||
|
{{:_, :"$1", {:_, :"$2", :"$3", :"$4"}}, [{:==, :"$2", []}], [{{:"$1", :"$3", :"$4"}}]}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
case unused_conns do
|
||||||
|
[] ->
|
||||||
|
:telemetry.execute(
|
||||||
|
[:pleroma, :connection_pool, :reclaim, :stop],
|
||||||
|
%{reclaimed_count: 0},
|
||||||
|
%{
|
||||||
|
max_connections: max_connections
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
{:stop, :no_unused_conns, nil}
|
||||||
|
|
||||||
|
unused_conns ->
|
||||||
|
reclaimed =
|
||||||
|
unused_conns
|
||||||
|
|> Enum.sort(fn {_pid1, crf1, last_reference1}, {_pid2, crf2, last_reference2} ->
|
||||||
|
crf1 <= crf2 and last_reference1 <= last_reference2
|
||||||
|
end)
|
||||||
|
|> Enum.take(reclaim_max)
|
||||||
|
|
||||||
|
reclaimed
|
||||||
|
|> Enum.each(fn {pid, _, _} ->
|
||||||
|
DynamicSupervisor.terminate_child(Pleroma.Gun.ConnectionPool.WorkerSupervisor, pid)
|
||||||
|
end)
|
||||||
|
|
||||||
|
:telemetry.execute(
|
||||||
|
[:pleroma, :connection_pool, :reclaim, :stop],
|
||||||
|
%{reclaimed_count: Enum.count(reclaimed)},
|
||||||
|
%{max_connections: max_connections}
|
||||||
|
)
|
||||||
|
|
||||||
|
{:stop, :normal, nil}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,127 @@
|
|||||||
|
defmodule Pleroma.Gun.ConnectionPool.Worker do
|
||||||
|
alias Pleroma.Gun
|
||||||
|
use GenServer, restart: :temporary
|
||||||
|
|
||||||
|
@registry Pleroma.Gun.ConnectionPool
|
||||||
|
|
||||||
|
def start_link([key | _] = opts) do
|
||||||
|
GenServer.start_link(__MODULE__, opts, name: {:via, Registry, {@registry, key}})
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init([_key, _uri, _opts, _client_pid] = opts) do
|
||||||
|
{:ok, nil, {:continue, {:connect, opts}}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_continue({:connect, [key, uri, opts, client_pid]}, _) do
|
||||||
|
with {:ok, conn_pid} <- Gun.Conn.open(uri, opts),
|
||||||
|
Process.link(conn_pid) do
|
||||||
|
time = :erlang.monotonic_time(:millisecond)
|
||||||
|
|
||||||
|
{_, _} =
|
||||||
|
Registry.update_value(@registry, key, fn _ ->
|
||||||
|
{conn_pid, [client_pid], 1, time}
|
||||||
|
end)
|
||||||
|
|
||||||
|
send(client_pid, {:conn_pid, conn_pid})
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
%{key: key, timer: nil, client_monitors: %{client_pid => Process.monitor(client_pid)}},
|
||||||
|
:hibernate}
|
||||||
|
else
|
||||||
|
err ->
|
||||||
|
{:stop, {:shutdown, err}, nil}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_cast({:add_client, client_pid, send_pid_back}, %{key: key} = state) do
|
||||||
|
time = :erlang.monotonic_time(:millisecond)
|
||||||
|
|
||||||
|
{{conn_pid, _, _, _}, _} =
|
||||||
|
Registry.update_value(@registry, key, fn {conn_pid, used_by, crf, last_reference} ->
|
||||||
|
{conn_pid, [client_pid | used_by], crf(time - last_reference, crf), time}
|
||||||
|
end)
|
||||||
|
|
||||||
|
if send_pid_back, do: send(client_pid, {:conn_pid, conn_pid})
|
||||||
|
|
||||||
|
state =
|
||||||
|
if state.timer != nil do
|
||||||
|
Process.cancel_timer(state[:timer])
|
||||||
|
%{state | timer: nil}
|
||||||
|
else
|
||||||
|
state
|
||||||
|
end
|
||||||
|
|
||||||
|
ref = Process.monitor(client_pid)
|
||||||
|
|
||||||
|
state = put_in(state.client_monitors[client_pid], ref)
|
||||||
|
{:noreply, state, :hibernate}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_cast({:remove_client, client_pid}, %{key: key} = state) do
|
||||||
|
{{_conn_pid, used_by, _crf, _last_reference}, _} =
|
||||||
|
Registry.update_value(@registry, key, fn {conn_pid, used_by, crf, last_reference} ->
|
||||||
|
{conn_pid, List.delete(used_by, client_pid), crf, last_reference}
|
||||||
|
end)
|
||||||
|
|
||||||
|
{ref, state} = pop_in(state.client_monitors[client_pid])
|
||||||
|
Process.demonitor(ref)
|
||||||
|
|
||||||
|
timer =
|
||||||
|
if used_by == [] do
|
||||||
|
max_idle = Pleroma.Config.get([:connections_pool, :max_idle_time], 30_000)
|
||||||
|
Process.send_after(self(), :idle_close, max_idle)
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, %{state | timer: timer}, :hibernate}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info(:idle_close, state) do
|
||||||
|
# Gun monitors the owner process, and will close the connection automatically
|
||||||
|
# when it's terminated
|
||||||
|
{:stop, :normal, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Gracefully shutdown if the connection got closed without any streams left
|
||||||
|
@impl true
|
||||||
|
def handle_info({:gun_down, _pid, _protocol, _reason, []}, state) do
|
||||||
|
{:stop, :normal, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Otherwise, shutdown with an error
|
||||||
|
@impl true
|
||||||
|
def handle_info({:gun_down, _pid, _protocol, _reason, _killed_streams} = down_message, state) do
|
||||||
|
{:stop, {:error, down_message}, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:DOWN, _ref, :process, pid, reason}, state) do
|
||||||
|
# Sometimes the client is dead before we demonitor it in :remove_client, so the message
|
||||||
|
# arrives anyway
|
||||||
|
|
||||||
|
case state.client_monitors[pid] do
|
||||||
|
nil ->
|
||||||
|
{:noreply, state, :hibernate}
|
||||||
|
|
||||||
|
_ref ->
|
||||||
|
:telemetry.execute(
|
||||||
|
[:pleroma, :connection_pool, :client_death],
|
||||||
|
%{client_pid: pid, reason: reason},
|
||||||
|
%{key: state.key}
|
||||||
|
)
|
||||||
|
|
||||||
|
handle_cast({:remove_client, pid}, state)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# LRFU policy: https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.55.1478
|
||||||
|
defp crf(time_delta, prev_crf) do
|
||||||
|
1 + :math.pow(0.5, 0.0001 * time_delta) * prev_crf
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,45 @@
|
|||||||
|
defmodule Pleroma.Gun.ConnectionPool.WorkerSupervisor do
|
||||||
|
@moduledoc "Supervisor for pool workers. Does not do anything except enforce max connection limit"
|
||||||
|
|
||||||
|
use DynamicSupervisor
|
||||||
|
|
||||||
|
def start_link(opts) do
|
||||||
|
DynamicSupervisor.start_link(__MODULE__, opts, name: __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def init(_opts) do
|
||||||
|
DynamicSupervisor.init(
|
||||||
|
strategy: :one_for_one,
|
||||||
|
max_children: Pleroma.Config.get([:connections_pool, :max_connections])
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_worker(opts, retry \\ false) do
|
||||||
|
case DynamicSupervisor.start_child(__MODULE__, {Pleroma.Gun.ConnectionPool.Worker, opts}) do
|
||||||
|
{:error, :max_children} ->
|
||||||
|
if retry or free_pool() == :error do
|
||||||
|
:telemetry.execute([:pleroma, :connection_pool, :provision_failure], %{opts: opts})
|
||||||
|
{:error, :pool_full}
|
||||||
|
else
|
||||||
|
start_worker(opts, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
res ->
|
||||||
|
res
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp free_pool do
|
||||||
|
wait_for_reclaimer_finish(Pleroma.Gun.ConnectionPool.Reclaimer.start_monitor())
|
||||||
|
end
|
||||||
|
|
||||||
|
defp wait_for_reclaimer_finish({pid, mon}) do
|
||||||
|
receive do
|
||||||
|
{:DOWN, ^mon, :process, ^pid, :no_unused_conns} ->
|
||||||
|
:error
|
||||||
|
|
||||||
|
{:DOWN, ^mon, :process, ^pid, :normal} ->
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,14 @@
|
|||||||
|
defmodule Pleroma.HTTP.AdapterHelper.Default do
|
||||||
|
alias Pleroma.HTTP.AdapterHelper
|
||||||
|
|
||||||
|
@behaviour Pleroma.HTTP.AdapterHelper
|
||||||
|
|
||||||
|
@spec options(keyword(), URI.t()) :: keyword()
|
||||||
|
def options(opts, _uri) do
|
||||||
|
proxy = Pleroma.Config.get([:http, :proxy_url], nil)
|
||||||
|
AdapterHelper.maybe_add_proxy(opts, AdapterHelper.format_proxy(proxy))
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec get_conn(URI.t(), keyword()) :: {:ok, keyword()}
|
||||||
|
def get_conn(_uri, opts), do: {:ok, opts}
|
||||||
|
end
|
@ -1,124 +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.HTTP.Connection do
|
|
||||||
@moduledoc """
|
|
||||||
Configure Tesla.Client with default and customized adapter options.
|
|
||||||
"""
|
|
||||||
|
|
||||||
alias Pleroma.Config
|
|
||||||
alias Pleroma.HTTP.AdapterHelper
|
|
||||||
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
@defaults [pool: :federation]
|
|
||||||
|
|
||||||
@type ip_address :: ipv4_address() | ipv6_address()
|
|
||||||
@type ipv4_address :: {0..255, 0..255, 0..255, 0..255}
|
|
||||||
@type ipv6_address ::
|
|
||||||
{0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535}
|
|
||||||
@type proxy_type() :: :socks4 | :socks5
|
|
||||||
@type host() :: charlist() | ip_address()
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Merge default connection & adapter options with received ones.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@spec options(URI.t(), keyword()) :: keyword()
|
|
||||||
def options(%URI{} = uri, opts \\ []) do
|
|
||||||
@defaults
|
|
||||||
|> pool_timeout()
|
|
||||||
|> Keyword.merge(opts)
|
|
||||||
|> adapter_helper().options(uri)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp pool_timeout(opts) do
|
|
||||||
{config_key, default} =
|
|
||||||
if adapter() == Tesla.Adapter.Gun do
|
|
||||||
{:pools, Config.get([:pools, :default, :timeout])}
|
|
||||||
else
|
|
||||||
{:hackney_pools, 10_000}
|
|
||||||
end
|
|
||||||
|
|
||||||
timeout = Config.get([config_key, opts[:pool], :timeout], default)
|
|
||||||
|
|
||||||
Keyword.merge(opts, timeout: timeout)
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec after_request(keyword()) :: :ok
|
|
||||||
def after_request(opts), do: adapter_helper().after_request(opts)
|
|
||||||
|
|
||||||
defp adapter, do: Application.get_env(:tesla, :adapter)
|
|
||||||
|
|
||||||
defp adapter_helper do
|
|
||||||
case adapter() do
|
|
||||||
Tesla.Adapter.Gun -> AdapterHelper.Gun
|
|
||||||
Tesla.Adapter.Hackney -> AdapterHelper.Hackney
|
|
||||||
_ -> AdapterHelper
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec parse_proxy(String.t() | tuple() | nil) ::
|
|
||||||
{:ok, host(), pos_integer()}
|
|
||||||
| {:ok, proxy_type(), host(), pos_integer()}
|
|
||||||
| {:error, atom()}
|
|
||||||
| nil
|
|
||||||
|
|
||||||
def parse_proxy(nil), do: nil
|
|
||||||
|
|
||||||
def parse_proxy(proxy) when is_binary(proxy) do
|
|
||||||
with [host, port] <- String.split(proxy, ":"),
|
|
||||||
{port, ""} <- Integer.parse(port) do
|
|
||||||
{:ok, parse_host(host), port}
|
|
||||||
else
|
|
||||||
{_, _} ->
|
|
||||||
Logger.warn("Parsing port failed #{inspect(proxy)}")
|
|
||||||
{:error, :invalid_proxy_port}
|
|
||||||
|
|
||||||
:error ->
|
|
||||||
Logger.warn("Parsing port failed #{inspect(proxy)}")
|
|
||||||
{:error, :invalid_proxy_port}
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
Logger.warn("Parsing proxy failed #{inspect(proxy)}")
|
|
||||||
{:error, :invalid_proxy}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse_proxy(proxy) when is_tuple(proxy) do
|
|
||||||
with {type, host, port} <- proxy do
|
|
||||||
{:ok, type, parse_host(host), port}
|
|
||||||
else
|
|
||||||
_ ->
|
|
||||||
Logger.warn("Parsing proxy failed #{inspect(proxy)}")
|
|
||||||
{:error, :invalid_proxy}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec parse_host(String.t() | atom() | charlist()) :: charlist() | ip_address()
|
|
||||||
def parse_host(host) when is_list(host), do: host
|
|
||||||
def parse_host(host) when is_atom(host), do: to_charlist(host)
|
|
||||||
|
|
||||||
def parse_host(host) when is_binary(host) do
|
|
||||||
host = to_charlist(host)
|
|
||||||
|
|
||||||
case :inet.parse_address(host) do
|
|
||||||
{:error, :einval} -> host
|
|
||||||
{:ok, ip} -> ip
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec format_host(String.t()) :: charlist()
|
|
||||||
def format_host(host) do
|
|
||||||
host_charlist = to_charlist(host)
|
|
||||||
|
|
||||||
case :inet.parse_address(host_charlist) do
|
|
||||||
{:error, :einval} ->
|
|
||||||
:idna.encode(host_charlist)
|
|
||||||
|
|
||||||
{:ok, _ip} ->
|
|
||||||
host_charlist
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,283 +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.Pool.Connections do
|
|
||||||
use GenServer
|
|
||||||
|
|
||||||
alias Pleroma.Config
|
|
||||||
alias Pleroma.Gun
|
|
||||||
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
@type domain :: String.t()
|
|
||||||
@type conn :: Pleroma.Gun.Conn.t()
|
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
|
||||||
conns: %{domain() => conn()},
|
|
||||||
opts: keyword()
|
|
||||||
}
|
|
||||||
|
|
||||||
defstruct conns: %{}, opts: []
|
|
||||||
|
|
||||||
@spec start_link({atom(), keyword()}) :: {:ok, pid()}
|
|
||||||
def start_link({name, opts}) do
|
|
||||||
GenServer.start_link(__MODULE__, opts, name: name)
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def init(opts), do: {:ok, %__MODULE__{conns: %{}, opts: opts}}
|
|
||||||
|
|
||||||
@spec checkin(String.t() | URI.t(), atom()) :: pid() | nil
|
|
||||||
def checkin(url, name)
|
|
||||||
def checkin(url, name) when is_binary(url), do: checkin(URI.parse(url), name)
|
|
||||||
|
|
||||||
def checkin(%URI{} = uri, name) do
|
|
||||||
timeout = Config.get([:connections_pool, :checkin_timeout], 250)
|
|
||||||
|
|
||||||
GenServer.call(name, {:checkin, uri}, timeout)
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec alive?(atom()) :: boolean()
|
|
||||||
def alive?(name) do
|
|
||||||
if pid = Process.whereis(name) do
|
|
||||||
Process.alive?(pid)
|
|
||||||
else
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec get_state(atom()) :: t()
|
|
||||||
def get_state(name) do
|
|
||||||
GenServer.call(name, :state)
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec count(atom()) :: pos_integer()
|
|
||||||
def count(name) do
|
|
||||||
GenServer.call(name, :count)
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec get_unused_conns(atom()) :: [{domain(), conn()}]
|
|
||||||
def get_unused_conns(name) do
|
|
||||||
GenServer.call(name, :unused_conns)
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec checkout(pid(), pid(), atom()) :: :ok
|
|
||||||
def checkout(conn, pid, name) do
|
|
||||||
GenServer.cast(name, {:checkout, conn, pid})
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec add_conn(atom(), String.t(), Pleroma.Gun.Conn.t()) :: :ok
|
|
||||||
def add_conn(name, key, conn) do
|
|
||||||
GenServer.cast(name, {:add_conn, key, conn})
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec remove_conn(atom(), String.t()) :: :ok
|
|
||||||
def remove_conn(name, key) do
|
|
||||||
GenServer.cast(name, {:remove_conn, key})
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_cast({:add_conn, key, conn}, state) do
|
|
||||||
state = put_in(state.conns[key], conn)
|
|
||||||
|
|
||||||
Process.monitor(conn.conn)
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_cast({:checkout, conn_pid, pid}, state) do
|
|
||||||
state =
|
|
||||||
with true <- Process.alive?(conn_pid),
|
|
||||||
{key, conn} <- find_conn(state.conns, conn_pid),
|
|
||||||
used_by <- List.keydelete(conn.used_by, pid, 0) do
|
|
||||||
conn_state = if used_by == [], do: :idle, else: conn.conn_state
|
|
||||||
|
|
||||||
put_in(state.conns[key], %{conn | conn_state: conn_state, used_by: used_by})
|
|
||||||
else
|
|
||||||
false ->
|
|
||||||
Logger.debug("checkout for closed conn #{inspect(conn_pid)}")
|
|
||||||
state
|
|
||||||
|
|
||||||
nil ->
|
|
||||||
Logger.debug("checkout for alive conn #{inspect(conn_pid)}, but is not in state")
|
|
||||||
state
|
|
||||||
end
|
|
||||||
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_cast({:remove_conn, key}, state) do
|
|
||||||
state = put_in(state.conns, Map.delete(state.conns, key))
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_call({:checkin, uri}, from, state) do
|
|
||||||
key = "#{uri.scheme}:#{uri.host}:#{uri.port}"
|
|
||||||
|
|
||||||
case state.conns[key] do
|
|
||||||
%{conn: pid, gun_state: :up} = conn ->
|
|
||||||
time = :os.system_time(:second)
|
|
||||||
last_reference = time - conn.last_reference
|
|
||||||
crf = crf(last_reference, 100, conn.crf)
|
|
||||||
|
|
||||||
state =
|
|
||||||
put_in(state.conns[key], %{
|
|
||||||
conn
|
|
||||||
| last_reference: time,
|
|
||||||
crf: crf,
|
|
||||||
conn_state: :active,
|
|
||||||
used_by: [from | conn.used_by]
|
|
||||||
})
|
|
||||||
|
|
||||||
{:reply, pid, state}
|
|
||||||
|
|
||||||
%{gun_state: :down} ->
|
|
||||||
{:reply, nil, state}
|
|
||||||
|
|
||||||
nil ->
|
|
||||||
{:reply, nil, state}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_call(:state, _from, state), do: {:reply, state, state}
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_call(:count, _from, state) do
|
|
||||||
{:reply, Enum.count(state.conns), state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_call(:unused_conns, _from, state) do
|
|
||||||
unused_conns =
|
|
||||||
state.conns
|
|
||||||
|> Enum.filter(&filter_conns/1)
|
|
||||||
|> Enum.sort(&sort_conns/2)
|
|
||||||
|
|
||||||
{:reply, unused_conns, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp filter_conns({_, %{conn_state: :idle, used_by: []}}), do: true
|
|
||||||
defp filter_conns(_), do: false
|
|
||||||
|
|
||||||
defp sort_conns({_, c1}, {_, c2}) do
|
|
||||||
c1.crf <= c2.crf and c1.last_reference <= c2.last_reference
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:gun_up, conn_pid, _protocol}, state) do
|
|
||||||
%{origin_host: host, origin_scheme: scheme, origin_port: port} = Gun.info(conn_pid)
|
|
||||||
|
|
||||||
host =
|
|
||||||
case :inet.ntoa(host) do
|
|
||||||
{:error, :einval} -> host
|
|
||||||
ip -> ip
|
|
||||||
end
|
|
||||||
|
|
||||||
key = "#{scheme}:#{host}:#{port}"
|
|
||||||
|
|
||||||
state =
|
|
||||||
with {key, conn} <- find_conn(state.conns, conn_pid, key),
|
|
||||||
{true, key} <- {Process.alive?(conn_pid), key} do
|
|
||||||
put_in(state.conns[key], %{
|
|
||||||
conn
|
|
||||||
| gun_state: :up,
|
|
||||||
conn_state: :active,
|
|
||||||
retries: 0
|
|
||||||
})
|
|
||||||
else
|
|
||||||
{false, key} ->
|
|
||||||
put_in(
|
|
||||||
state.conns,
|
|
||||||
Map.delete(state.conns, key)
|
|
||||||
)
|
|
||||||
|
|
||||||
nil ->
|
|
||||||
:ok = Gun.close(conn_pid)
|
|
||||||
|
|
||||||
state
|
|
||||||
end
|
|
||||||
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:gun_down, conn_pid, _protocol, _reason, _killed}, state) do
|
|
||||||
retries = Config.get([:connections_pool, :retry], 1)
|
|
||||||
# we can't get info on this pid, because pid is dead
|
|
||||||
state =
|
|
||||||
with {key, conn} <- find_conn(state.conns, conn_pid),
|
|
||||||
{true, key} <- {Process.alive?(conn_pid), key} do
|
|
||||||
if conn.retries == retries do
|
|
||||||
:ok = Gun.close(conn.conn)
|
|
||||||
|
|
||||||
put_in(
|
|
||||||
state.conns,
|
|
||||||
Map.delete(state.conns, key)
|
|
||||||
)
|
|
||||||
else
|
|
||||||
put_in(state.conns[key], %{
|
|
||||||
conn
|
|
||||||
| gun_state: :down,
|
|
||||||
retries: conn.retries + 1
|
|
||||||
})
|
|
||||||
end
|
|
||||||
else
|
|
||||||
{false, key} ->
|
|
||||||
put_in(
|
|
||||||
state.conns,
|
|
||||||
Map.delete(state.conns, key)
|
|
||||||
)
|
|
||||||
|
|
||||||
nil ->
|
|
||||||
Logger.debug(":gun_down for conn which isn't found in state")
|
|
||||||
|
|
||||||
state
|
|
||||||
end
|
|
||||||
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:DOWN, _ref, :process, conn_pid, reason}, state) do
|
|
||||||
Logger.debug("received DOWN message for #{inspect(conn_pid)} reason -> #{inspect(reason)}")
|
|
||||||
|
|
||||||
state =
|
|
||||||
with {key, conn} <- find_conn(state.conns, conn_pid) do
|
|
||||||
Enum.each(conn.used_by, fn {pid, _ref} ->
|
|
||||||
Process.exit(pid, reason)
|
|
||||||
end)
|
|
||||||
|
|
||||||
put_in(
|
|
||||||
state.conns,
|
|
||||||
Map.delete(state.conns, key)
|
|
||||||
)
|
|
||||||
else
|
|
||||||
nil ->
|
|
||||||
Logger.debug(":DOWN for conn which isn't found in state")
|
|
||||||
|
|
||||||
state
|
|
||||||
end
|
|
||||||
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp find_conn(conns, conn_pid) do
|
|
||||||
Enum.find(conns, fn {_key, conn} ->
|
|
||||||
conn.conn == conn_pid
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp find_conn(conns, conn_pid, conn_key) do
|
|
||||||
Enum.find(conns, fn {key, conn} ->
|
|
||||||
key == conn_key and conn.conn == conn_pid
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
def crf(current, steps, crf) do
|
|
||||||
1 + :math.pow(0.5, current / steps) * crf
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,22 +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.Pool do
|
|
||||||
def child_spec(opts) do
|
|
||||||
poolboy_opts =
|
|
||||||
opts
|
|
||||||
|> Keyword.put(:worker_module, Pleroma.Pool.Request)
|
|
||||||
|> Keyword.put(:name, {:local, opts[:name]})
|
|
||||||
|> Keyword.put(:size, opts[:size])
|
|
||||||
|> Keyword.put(:max_overflow, opts[:max_overflow])
|
|
||||||
|
|
||||||
%{
|
|
||||||
id: opts[:id] || {__MODULE__, make_ref()},
|
|
||||||
start: {:poolboy, :start_link, [poolboy_opts, [name: opts[:name]]]},
|
|
||||||
restart: :permanent,
|
|
||||||
shutdown: 5000,
|
|
||||||
type: :worker
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,65 +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.Pool.Request do
|
|
||||||
use GenServer
|
|
||||||
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
def start_link(args) do
|
|
||||||
GenServer.start_link(__MODULE__, args)
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def init(_), do: {:ok, []}
|
|
||||||
|
|
||||||
@spec execute(pid() | atom(), Tesla.Client.t(), keyword(), pos_integer()) ::
|
|
||||||
{:ok, Tesla.Env.t()} | {:error, any()}
|
|
||||||
def execute(pid, client, request, timeout) do
|
|
||||||
GenServer.call(pid, {:execute, client, request}, timeout)
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_call({:execute, client, request}, _from, state) do
|
|
||||||
response = Pleroma.HTTP.request(client, request)
|
|
||||||
|
|
||||||
{:reply, response, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:gun_data, _conn, _stream, _, _}, state) do
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:gun_up, _conn, _protocol}, state) do
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:gun_down, _conn, _protocol, _reason, _killed}, state) do
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:gun_error, _conn, _stream, _error}, state) do
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:gun_push, _conn, _stream, _new_stream, _method, _uri, _headers}, state) do
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:gun_response, _conn, _stream, _, _status, _headers}, state) do
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info(msg, state) do
|
|
||||||
Logger.warn("Received unexpected message #{inspect(__MODULE__)} #{inspect(msg)}")
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,42 +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.Pool.Supervisor do
|
|
||||||
use Supervisor
|
|
||||||
|
|
||||||
alias Pleroma.Config
|
|
||||||
alias Pleroma.Pool
|
|
||||||
|
|
||||||
def start_link(args) do
|
|
||||||
Supervisor.start_link(__MODULE__, args, name: __MODULE__)
|
|
||||||
end
|
|
||||||
|
|
||||||
def init(_) do
|
|
||||||
conns_child = %{
|
|
||||||
id: Pool.Connections,
|
|
||||||
start:
|
|
||||||
{Pool.Connections, :start_link, [{:gun_connections, Config.get([:connections_pool])}]}
|
|
||||||
}
|
|
||||||
|
|
||||||
Supervisor.init([conns_child | pools()], strategy: :one_for_one)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp pools do
|
|
||||||
pools = Config.get(:pools)
|
|
||||||
|
|
||||||
pools =
|
|
||||||
if Config.get([Pleroma.Upload, :proxy_remote]) == false do
|
|
||||||
Keyword.delete(pools, :upload)
|
|
||||||
else
|
|
||||||
pools
|
|
||||||
end
|
|
||||||
|
|
||||||
for {pool_name, pool_opts} <- pools do
|
|
||||||
pool_opts
|
|
||||||
|> Keyword.put(:id, {Pool, pool_name})
|
|
||||||
|> Keyword.put(:name, pool_name)
|
|
||||||
|> Pool.child_spec()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -0,0 +1,76 @@
|
|||||||
|
defmodule Pleroma.Telemetry.Logger do
|
||||||
|
@moduledoc "Transforms Pleroma telemetry events to logs"
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@events [
|
||||||
|
[:pleroma, :connection_pool, :reclaim, :start],
|
||||||
|
[:pleroma, :connection_pool, :reclaim, :stop],
|
||||||
|
[:pleroma, :connection_pool, :provision_failure],
|
||||||
|
[:pleroma, :connection_pool, :client_death]
|
||||||
|
]
|
||||||
|
def attach do
|
||||||
|
:telemetry.attach_many("pleroma-logger", @events, &handle_event/4, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Passing anonymous functions instead of strings to logger is intentional,
|
||||||
|
# that way strings won't be concatenated if the message is going to be thrown
|
||||||
|
# out anyway due to higher log level configured
|
||||||
|
|
||||||
|
def handle_event(
|
||||||
|
[:pleroma, :connection_pool, :reclaim, :start],
|
||||||
|
_,
|
||||||
|
%{max_connections: max_connections, reclaim_max: reclaim_max},
|
||||||
|
_
|
||||||
|
) do
|
||||||
|
Logger.debug(fn ->
|
||||||
|
"Connection pool is exhausted (reached #{max_connections} connections). Starting idle connection cleanup to reclaim as much as #{
|
||||||
|
reclaim_max
|
||||||
|
} connections"
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event(
|
||||||
|
[:pleroma, :connection_pool, :reclaim, :stop],
|
||||||
|
%{reclaimed_count: 0},
|
||||||
|
_,
|
||||||
|
_
|
||||||
|
) do
|
||||||
|
Logger.error(fn ->
|
||||||
|
"Connection pool failed to reclaim any connections due to all of them being in use. It will have to drop requests for opening connections to new hosts"
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event(
|
||||||
|
[:pleroma, :connection_pool, :reclaim, :stop],
|
||||||
|
%{reclaimed_count: reclaimed_count},
|
||||||
|
_,
|
||||||
|
_
|
||||||
|
) do
|
||||||
|
Logger.debug(fn -> "Connection pool cleaned up #{reclaimed_count} idle connections" end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event(
|
||||||
|
[:pleroma, :connection_pool, :provision_failure],
|
||||||
|
%{opts: [key | _]},
|
||||||
|
_,
|
||||||
|
_
|
||||||
|
) do
|
||||||
|
Logger.error(fn ->
|
||||||
|
"Connection pool had to refuse opening a connection to #{key} due to connection limit exhaustion"
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event(
|
||||||
|
[:pleroma, :connection_pool, :client_death],
|
||||||
|
%{client_pid: client_pid, reason: reason},
|
||||||
|
%{key: key},
|
||||||
|
_
|
||||||
|
) do
|
||||||
|
Logger.warn(fn ->
|
||||||
|
"Pool worker for #{key}: Client #{inspect(client_pid)} died before releasing the connection with #{
|
||||||
|
inspect(reason)
|
||||||
|
}"
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,110 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2015-2020 Tymon Tobolski <https://github.com/teamon/tesla/blob/master/lib/tesla/middleware/follow_redirects.ex>
|
||||||
|
# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.HTTP.Middleware.FollowRedirects do
|
||||||
|
@moduledoc """
|
||||||
|
Pool-aware version of https://github.com/teamon/tesla/blob/master/lib/tesla/middleware/follow_redirects.ex
|
||||||
|
|
||||||
|
Follow 3xx redirects
|
||||||
|
## Options
|
||||||
|
- `:max_redirects` - limit number of redirects (default: `5`)
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Pleroma.Gun.ConnectionPool
|
||||||
|
|
||||||
|
@behaviour Tesla.Middleware
|
||||||
|
|
||||||
|
@max_redirects 5
|
||||||
|
@redirect_statuses [301, 302, 303, 307, 308]
|
||||||
|
|
||||||
|
@impl Tesla.Middleware
|
||||||
|
def call(env, next, opts \\ []) do
|
||||||
|
max = Keyword.get(opts, :max_redirects, @max_redirects)
|
||||||
|
|
||||||
|
redirect(env, next, max)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp redirect(env, next, left) do
|
||||||
|
opts = env.opts[:adapter]
|
||||||
|
|
||||||
|
case Tesla.run(env, next) do
|
||||||
|
{:ok, %{status: status} = res} when status in @redirect_statuses and left > 0 ->
|
||||||
|
release_conn(opts)
|
||||||
|
|
||||||
|
case Tesla.get_header(res, "location") do
|
||||||
|
nil ->
|
||||||
|
{:ok, res}
|
||||||
|
|
||||||
|
location ->
|
||||||
|
location = parse_location(location, res)
|
||||||
|
|
||||||
|
case get_conn(location, opts) do
|
||||||
|
{:ok, opts} ->
|
||||||
|
%{env | opts: Keyword.put(env.opts, :adapter, opts)}
|
||||||
|
|> new_request(res.status, location)
|
||||||
|
|> redirect(next, left - 1)
|
||||||
|
|
||||||
|
e ->
|
||||||
|
e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, %{status: status}} when status in @redirect_statuses ->
|
||||||
|
release_conn(opts)
|
||||||
|
{:error, {__MODULE__, :too_many_redirects}}
|
||||||
|
|
||||||
|
{:error, _} = e ->
|
||||||
|
release_conn(opts)
|
||||||
|
e
|
||||||
|
|
||||||
|
other ->
|
||||||
|
unless opts[:body_as] == :chunks do
|
||||||
|
release_conn(opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
other
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_conn(location, opts) do
|
||||||
|
uri = URI.parse(location)
|
||||||
|
|
||||||
|
case ConnectionPool.get_conn(uri, opts) do
|
||||||
|
{:ok, conn} ->
|
||||||
|
{:ok, Keyword.merge(opts, conn: conn)}
|
||||||
|
|
||||||
|
e ->
|
||||||
|
e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp release_conn(opts) do
|
||||||
|
ConnectionPool.release_conn(opts[:conn])
|
||||||
|
end
|
||||||
|
|
||||||
|
# The 303 (See Other) redirect was added in HTTP/1.1 to indicate that the originally
|
||||||
|
# requested resource is not available, however a related resource (or another redirect)
|
||||||
|
# available via GET is available at the specified location.
|
||||||
|
# https://tools.ietf.org/html/rfc7231#section-6.4.4
|
||||||
|
defp new_request(env, 303, location), do: %{env | url: location, method: :get, query: []}
|
||||||
|
|
||||||
|
# The 307 (Temporary Redirect) status code indicates that the target
|
||||||
|
# resource resides temporarily under a different URI and the user agent
|
||||||
|
# MUST NOT change the request method (...)
|
||||||
|
# https://tools.ietf.org/html/rfc7231#section-6.4.7
|
||||||
|
defp new_request(env, 307, location), do: %{env | url: location}
|
||||||
|
|
||||||
|
defp new_request(env, _, location), do: %{env | url: location, query: []}
|
||||||
|
|
||||||
|
defp parse_location("https://" <> _rest = location, _env), do: location
|
||||||
|
defp parse_location("http://" <> _rest = location, _env), do: location
|
||||||
|
|
||||||
|
defp parse_location(location, env) do
|
||||||
|
env.url
|
||||||
|
|> URI.parse()
|
||||||
|
|> URI.merge(location)
|
||||||
|
|> URI.to_string()
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,19 @@
|
|||||||
|
defmodule Pleroma.Repo.Migrations.RenameNotificationPrivacyOption do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
execute(
|
||||||
|
"UPDATE users SET notification_settings = notification_settings - 'privacy_option' || jsonb_build_object('hide_notification_contents', notification_settings->'privacy_option')
|
||||||
|
where notification_settings ? 'privacy_option'
|
||||||
|
and local"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
execute(
|
||||||
|
"UPDATE users SET notification_settings = notification_settings - 'hide_notification_contents' || jsonb_build_object('privacy_option', notification_settings->'hide_notification_contents')
|
||||||
|
where notification_settings ? 'hide_notification_contents'
|
||||||
|
and local"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,27 @@
|
|||||||
|
defmodule Elixir.Pleroma.Repo.Migrations.Oban20ConfigChanges do
|
||||||
|
use Ecto.Migration
|
||||||
|
import Ecto.Query
|
||||||
|
alias Pleroma.ConfigDB
|
||||||
|
alias Pleroma.Repo
|
||||||
|
|
||||||
|
def change do
|
||||||
|
config_entry =
|
||||||
|
from(c in ConfigDB, where: c.group == ^":pleroma" and c.key == ^"Oban")
|
||||||
|
|> select([c], struct(c, [:value, :id]))
|
||||||
|
|> Repo.one()
|
||||||
|
|
||||||
|
if config_entry do
|
||||||
|
%{value: value} = config_entry
|
||||||
|
|
||||||
|
value =
|
||||||
|
case Keyword.fetch(value, :verbose) do
|
||||||
|
{:ok, log} -> Keyword.put_new(value, :log, log)
|
||||||
|
_ -> value
|
||||||
|
end
|
||||||
|
|> Keyword.drop([:verbose, :prune])
|
||||||
|
|
||||||
|
Ecto.Changeset.change(config_entry, %{value: value})
|
||||||
|
|> Repo.update()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -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.1594374054351.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.247dc52c7abe6a0dab87.js></script><script type=text/javascript src=/static/js/app.1e68e208590653dab5aa.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.6dbc7dea4fc148c85860.css rel=stylesheet><link href=/static/fontello.1594823398494.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.9e24ed238da5a8538f50.js></script><script type=text/javascript src=/static/js/app.31bba9f1e242ff273dcb.js></script></body></html>
|
File diff suppressed because one or more lines are too long
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.
@ -1,144 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: "Icons";
|
|
||||||
src: url("./font/fontello.1589385935077.eot");
|
|
||||||
src: url("./font/fontello.1589385935077.eot") format("embedded-opentype"),
|
|
||||||
url("./font/fontello.1589385935077.woff2") format("woff2"),
|
|
||||||
url("./font/fontello.1589385935077.woff") format("woff"),
|
|
||||||
url("./font/fontello.1589385935077.ttf") format("truetype"),
|
|
||||||
url("./font/fontello.1589385935077.svg") format("svg");
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
[class^="icon-"]::before,
|
|
||||||
[class*=" icon-"]::before {
|
|
||||||
font-family: "Icons";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: normal;
|
|
||||||
speak: none;
|
|
||||||
display: inline-block;
|
|
||||||
text-decoration: inherit;
|
|
||||||
width: 1em;
|
|
||||||
margin-right: .2em;
|
|
||||||
text-align: center;
|
|
||||||
font-variant: normal;
|
|
||||||
text-transform: none;
|
|
||||||
line-height: 1em;
|
|
||||||
margin-left: .2em;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-spin4::before { content: "\e834"; }
|
|
||||||
|
|
||||||
.icon-cancel::before { content: "\e800"; }
|
|
||||||
|
|
||||||
.icon-upload::before { content: "\e801"; }
|
|
||||||
|
|
||||||
.icon-spin3::before { content: "\e832"; }
|
|
||||||
|
|
||||||
.icon-reply::before { content: "\f112"; }
|
|
||||||
|
|
||||||
.icon-star::before { content: "\e802"; }
|
|
||||||
|
|
||||||
.icon-star-empty::before { content: "\e803"; }
|
|
||||||
|
|
||||||
.icon-retweet::before { content: "\e804"; }
|
|
||||||
|
|
||||||
.icon-eye-off::before { content: "\e805"; }
|
|
||||||
|
|
||||||
.icon-binoculars::before { content: "\f1e5"; }
|
|
||||||
|
|
||||||
.icon-cog::before { content: "\e807"; }
|
|
||||||
|
|
||||||
.icon-user-plus::before { content: "\f234"; }
|
|
||||||
|
|
||||||
.icon-menu::before { content: "\f0c9"; }
|
|
||||||
|
|
||||||
.icon-logout::before { content: "\e808"; }
|
|
||||||
|
|
||||||
.icon-down-open::before { content: "\e809"; }
|
|
||||||
|
|
||||||
.icon-attach::before { content: "\e80a"; }
|
|
||||||
|
|
||||||
.icon-link-ext::before { content: "\f08e"; }
|
|
||||||
|
|
||||||
.icon-link-ext-alt::before { content: "\f08f"; }
|
|
||||||
|
|
||||||
.icon-picture::before { content: "\e80b"; }
|
|
||||||
|
|
||||||
.icon-video::before { content: "\e80c"; }
|
|
||||||
|
|
||||||
.icon-right-open::before { content: "\e80d"; }
|
|
||||||
|
|
||||||
.icon-left-open::before { content: "\e80e"; }
|
|
||||||
|
|
||||||
.icon-up-open::before { content: "\e80f"; }
|
|
||||||
|
|
||||||
.icon-comment-empty::before { content: "\f0e5"; }
|
|
||||||
|
|
||||||
.icon-mail-alt::before { content: "\f0e0"; }
|
|
||||||
|
|
||||||
.icon-lock::before { content: "\e811"; }
|
|
||||||
|
|
||||||
.icon-lock-open-alt::before { content: "\f13e"; }
|
|
||||||
|
|
||||||
.icon-globe::before { content: "\e812"; }
|
|
||||||
|
|
||||||
.icon-brush::before { content: "\e813"; }
|
|
||||||
|
|
||||||
.icon-search::before { content: "\e806"; }
|
|
||||||
|
|
||||||
.icon-adjust::before { content: "\e816"; }
|
|
||||||
|
|
||||||
.icon-thumbs-up-alt::before { content: "\f164"; }
|
|
||||||
|
|
||||||
.icon-attention::before { content: "\e814"; }
|
|
||||||
|
|
||||||
.icon-plus-squared::before { content: "\f0fe"; }
|
|
||||||
|
|
||||||
.icon-plus::before { content: "\e815"; }
|
|
||||||
|
|
||||||
.icon-edit::before { content: "\e817"; }
|
|
||||||
|
|
||||||
.icon-play-circled::before { content: "\f144"; }
|
|
||||||
|
|
||||||
.icon-pencil::before { content: "\e818"; }
|
|
||||||
|
|
||||||
.icon-chart-bar::before { content: "\e81b"; }
|
|
||||||
|
|
||||||
.icon-smile::before { content: "\f118"; }
|
|
||||||
|
|
||||||
.icon-bell-alt::before { content: "\f0f3"; }
|
|
||||||
|
|
||||||
.icon-wrench::before { content: "\e81a"; }
|
|
||||||
|
|
||||||
.icon-pin::before { content: "\e819"; }
|
|
||||||
|
|
||||||
.icon-ellipsis::before { content: "\f141"; }
|
|
||||||
|
|
||||||
.icon-bell-ringing-o::before { content: "\e810"; }
|
|
||||||
|
|
||||||
.icon-zoom-in::before { content: "\e81c"; }
|
|
||||||
|
|
||||||
.icon-gauge::before { content: "\f0e4"; }
|
|
||||||
|
|
||||||
.icon-users::before { content: "\e81d"; }
|
|
||||||
|
|
||||||
.icon-info-circled::before { content: "\e81f"; }
|
|
||||||
|
|
||||||
.icon-home-2::before { content: "\e821"; }
|
|
||||||
|
|
||||||
.icon-chat::before { content: "\e81e"; }
|
|
||||||
|
|
||||||
.icon-login::before { content: "\e820"; }
|
|
||||||
|
|
||||||
.icon-arrow-curved::before { content: "\e822"; }
|
|
||||||
|
|
||||||
.icon-link::before { content: "\e823"; }
|
|
||||||
|
|
||||||
.icon-share::before { content: "\f1e0"; }
|
|
||||||
|
|
||||||
.icon-user::before { content: "\e824"; }
|
|
||||||
|
|
||||||
.icon-ok::before { content: "\e827"; }
|
|
@ -1,152 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: "Icons";
|
|
||||||
src: url("./font/fontello.1594030805019.eot");
|
|
||||||
src: url("./font/fontello.1594030805019.eot") format("embedded-opentype"),
|
|
||||||
url("./font/fontello.1594030805019.woff2") format("woff2"),
|
|
||||||
url("./font/fontello.1594030805019.woff") format("woff"),
|
|
||||||
url("./font/fontello.1594030805019.ttf") format("truetype"),
|
|
||||||
url("./font/fontello.1594030805019.svg") format("svg");
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
[class^="icon-"]::before,
|
|
||||||
[class*=" icon-"]::before {
|
|
||||||
font-family: "Icons";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: normal;
|
|
||||||
speak: none;
|
|
||||||
display: inline-block;
|
|
||||||
text-decoration: inherit;
|
|
||||||
width: 1em;
|
|
||||||
margin-right: .2em;
|
|
||||||
text-align: center;
|
|
||||||
font-variant: normal;
|
|
||||||
text-transform: none;
|
|
||||||
line-height: 1em;
|
|
||||||
margin-left: .2em;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-spin4::before { content: "\e834"; }
|
|
||||||
|
|
||||||
.icon-cancel::before { content: "\e800"; }
|
|
||||||
|
|
||||||
.icon-upload::before { content: "\e801"; }
|
|
||||||
|
|
||||||
.icon-spin3::before { content: "\e832"; }
|
|
||||||
|
|
||||||
.icon-reply::before { content: "\f112"; }
|
|
||||||
|
|
||||||
.icon-star::before { content: "\e802"; }
|
|
||||||
|
|
||||||
.icon-star-empty::before { content: "\e803"; }
|
|
||||||
|
|
||||||
.icon-retweet::before { content: "\e804"; }
|
|
||||||
|
|
||||||
.icon-eye-off::before { content: "\e805"; }
|
|
||||||
|
|
||||||
.icon-binoculars::before { content: "\f1e5"; }
|
|
||||||
|
|
||||||
.icon-cog::before { content: "\e807"; }
|
|
||||||
|
|
||||||
.icon-user-plus::before { content: "\f234"; }
|
|
||||||
|
|
||||||
.icon-menu::before { content: "\f0c9"; }
|
|
||||||
|
|
||||||
.icon-logout::before { content: "\e808"; }
|
|
||||||
|
|
||||||
.icon-down-open::before { content: "\e809"; }
|
|
||||||
|
|
||||||
.icon-attach::before { content: "\e80a"; }
|
|
||||||
|
|
||||||
.icon-link-ext::before { content: "\f08e"; }
|
|
||||||
|
|
||||||
.icon-link-ext-alt::before { content: "\f08f"; }
|
|
||||||
|
|
||||||
.icon-picture::before { content: "\e80b"; }
|
|
||||||
|
|
||||||
.icon-video::before { content: "\e80c"; }
|
|
||||||
|
|
||||||
.icon-right-open::before { content: "\e80d"; }
|
|
||||||
|
|
||||||
.icon-left-open::before { content: "\e80e"; }
|
|
||||||
|
|
||||||
.icon-up-open::before { content: "\e80f"; }
|
|
||||||
|
|
||||||
.icon-comment-empty::before { content: "\f0e5"; }
|
|
||||||
|
|
||||||
.icon-mail-alt::before { content: "\f0e0"; }
|
|
||||||
|
|
||||||
.icon-lock::before { content: "\e811"; }
|
|
||||||
|
|
||||||
.icon-lock-open-alt::before { content: "\f13e"; }
|
|
||||||
|
|
||||||
.icon-globe::before { content: "\e812"; }
|
|
||||||
|
|
||||||
.icon-brush::before { content: "\e813"; }
|
|
||||||
|
|
||||||
.icon-search::before { content: "\e806"; }
|
|
||||||
|
|
||||||
.icon-adjust::before { content: "\e816"; }
|
|
||||||
|
|
||||||
.icon-thumbs-up-alt::before { content: "\f164"; }
|
|
||||||
|
|
||||||
.icon-attention::before { content: "\e814"; }
|
|
||||||
|
|
||||||
.icon-plus-squared::before { content: "\f0fe"; }
|
|
||||||
|
|
||||||
.icon-plus::before { content: "\e815"; }
|
|
||||||
|
|
||||||
.icon-edit::before { content: "\e817"; }
|
|
||||||
|
|
||||||
.icon-play-circled::before { content: "\f144"; }
|
|
||||||
|
|
||||||
.icon-pencil::before { content: "\e818"; }
|
|
||||||
|
|
||||||
.icon-chart-bar::before { content: "\e81b"; }
|
|
||||||
|
|
||||||
.icon-smile::before { content: "\f118"; }
|
|
||||||
|
|
||||||
.icon-bell-alt::before { content: "\f0f3"; }
|
|
||||||
|
|
||||||
.icon-wrench::before { content: "\e81a"; }
|
|
||||||
|
|
||||||
.icon-pin::before { content: "\e819"; }
|
|
||||||
|
|
||||||
.icon-ellipsis::before { content: "\f141"; }
|
|
||||||
|
|
||||||
.icon-bell-ringing-o::before { content: "\e810"; }
|
|
||||||
|
|
||||||
.icon-zoom-in::before { content: "\e81c"; }
|
|
||||||
|
|
||||||
.icon-gauge::before { content: "\f0e4"; }
|
|
||||||
|
|
||||||
.icon-users::before { content: "\e81d"; }
|
|
||||||
|
|
||||||
.icon-info-circled::before { content: "\e81f"; }
|
|
||||||
|
|
||||||
.icon-home-2::before { content: "\e821"; }
|
|
||||||
|
|
||||||
.icon-chat::before { content: "\e81e"; }
|
|
||||||
|
|
||||||
.icon-login::before { content: "\e820"; }
|
|
||||||
|
|
||||||
.icon-arrow-curved::before { content: "\e822"; }
|
|
||||||
|
|
||||||
.icon-link::before { content: "\e823"; }
|
|
||||||
|
|
||||||
.icon-share::before { content: "\f1e0"; }
|
|
||||||
|
|
||||||
.icon-user::before { content: "\e824"; }
|
|
||||||
|
|
||||||
.icon-ok::before { content: "\e827"; }
|
|
||||||
|
|
||||||
.icon-filter::before { content: "\f0b0"; }
|
|
||||||
|
|
||||||
.icon-download::before { content: "\e825"; }
|
|
||||||
|
|
||||||
.icon-bookmark::before { content: "\e826"; }
|
|
||||||
|
|
||||||
.icon-bookmark-empty::before { content: "\f097"; }
|
|
@ -1,156 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: "Icons";
|
|
||||||
src: url("./font/fontello.1594134783339.eot");
|
|
||||||
src: url("./font/fontello.1594134783339.eot") format("embedded-opentype"),
|
|
||||||
url("./font/fontello.1594134783339.woff2") format("woff2"),
|
|
||||||
url("./font/fontello.1594134783339.woff") format("woff"),
|
|
||||||
url("./font/fontello.1594134783339.ttf") format("truetype"),
|
|
||||||
url("./font/fontello.1594134783339.svg") format("svg");
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
[class^="icon-"]::before,
|
|
||||||
[class*=" icon-"]::before {
|
|
||||||
font-family: "Icons";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: normal;
|
|
||||||
speak: none;
|
|
||||||
display: inline-block;
|
|
||||||
text-decoration: inherit;
|
|
||||||
width: 1em;
|
|
||||||
margin-right: .2em;
|
|
||||||
text-align: center;
|
|
||||||
font-variant: normal;
|
|
||||||
text-transform: none;
|
|
||||||
line-height: 1em;
|
|
||||||
margin-left: .2em;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-spin4::before { content: "\e834"; }
|
|
||||||
|
|
||||||
.icon-cancel::before { content: "\e800"; }
|
|
||||||
|
|
||||||
.icon-upload::before { content: "\e801"; }
|
|
||||||
|
|
||||||
.icon-spin3::before { content: "\e832"; }
|
|
||||||
|
|
||||||
.icon-reply::before { content: "\f112"; }
|
|
||||||
|
|
||||||
.icon-star::before { content: "\e802"; }
|
|
||||||
|
|
||||||
.icon-star-empty::before { content: "\e803"; }
|
|
||||||
|
|
||||||
.icon-retweet::before { content: "\e804"; }
|
|
||||||
|
|
||||||
.icon-eye-off::before { content: "\e805"; }
|
|
||||||
|
|
||||||
.icon-binoculars::before { content: "\f1e5"; }
|
|
||||||
|
|
||||||
.icon-cog::before { content: "\e807"; }
|
|
||||||
|
|
||||||
.icon-user-plus::before { content: "\f234"; }
|
|
||||||
|
|
||||||
.icon-menu::before { content: "\f0c9"; }
|
|
||||||
|
|
||||||
.icon-logout::before { content: "\e808"; }
|
|
||||||
|
|
||||||
.icon-down-open::before { content: "\e809"; }
|
|
||||||
|
|
||||||
.icon-attach::before { content: "\e80a"; }
|
|
||||||
|
|
||||||
.icon-link-ext::before { content: "\f08e"; }
|
|
||||||
|
|
||||||
.icon-link-ext-alt::before { content: "\f08f"; }
|
|
||||||
|
|
||||||
.icon-picture::before { content: "\e80b"; }
|
|
||||||
|
|
||||||
.icon-video::before { content: "\e80c"; }
|
|
||||||
|
|
||||||
.icon-right-open::before { content: "\e80d"; }
|
|
||||||
|
|
||||||
.icon-left-open::before { content: "\e80e"; }
|
|
||||||
|
|
||||||
.icon-up-open::before { content: "\e80f"; }
|
|
||||||
|
|
||||||
.icon-comment-empty::before { content: "\f0e5"; }
|
|
||||||
|
|
||||||
.icon-mail-alt::before { content: "\f0e0"; }
|
|
||||||
|
|
||||||
.icon-lock::before { content: "\e811"; }
|
|
||||||
|
|
||||||
.icon-lock-open-alt::before { content: "\f13e"; }
|
|
||||||
|
|
||||||
.icon-globe::before { content: "\e812"; }
|
|
||||||
|
|
||||||
.icon-brush::before { content: "\e813"; }
|
|
||||||
|
|
||||||
.icon-search::before { content: "\e806"; }
|
|
||||||
|
|
||||||
.icon-adjust::before { content: "\e816"; }
|
|
||||||
|
|
||||||
.icon-thumbs-up-alt::before { content: "\f164"; }
|
|
||||||
|
|
||||||
.icon-attention::before { content: "\e814"; }
|
|
||||||
|
|
||||||
.icon-plus-squared::before { content: "\f0fe"; }
|
|
||||||
|
|
||||||
.icon-plus::before { content: "\e815"; }
|
|
||||||
|
|
||||||
.icon-edit::before { content: "\e817"; }
|
|
||||||
|
|
||||||
.icon-play-circled::before { content: "\f144"; }
|
|
||||||
|
|
||||||
.icon-pencil::before { content: "\e818"; }
|
|
||||||
|
|
||||||
.icon-chart-bar::before { content: "\e81b"; }
|
|
||||||
|
|
||||||
.icon-smile::before { content: "\f118"; }
|
|
||||||
|
|
||||||
.icon-bell-alt::before { content: "\f0f3"; }
|
|
||||||
|
|
||||||
.icon-wrench::before { content: "\e81a"; }
|
|
||||||
|
|
||||||
.icon-pin::before { content: "\e819"; }
|
|
||||||
|
|
||||||
.icon-ellipsis::before { content: "\f141"; }
|
|
||||||
|
|
||||||
.icon-bell-ringing-o::before { content: "\e810"; }
|
|
||||||
|
|
||||||
.icon-zoom-in::before { content: "\e81c"; }
|
|
||||||
|
|
||||||
.icon-gauge::before { content: "\f0e4"; }
|
|
||||||
|
|
||||||
.icon-users::before { content: "\e81d"; }
|
|
||||||
|
|
||||||
.icon-info-circled::before { content: "\e81f"; }
|
|
||||||
|
|
||||||
.icon-home-2::before { content: "\e821"; }
|
|
||||||
|
|
||||||
.icon-chat::before { content: "\e81e"; }
|
|
||||||
|
|
||||||
.icon-login::before { content: "\e820"; }
|
|
||||||
|
|
||||||
.icon-arrow-curved::before { content: "\e822"; }
|
|
||||||
|
|
||||||
.icon-link::before { content: "\e823"; }
|
|
||||||
|
|
||||||
.icon-share::before { content: "\f1e0"; }
|
|
||||||
|
|
||||||
.icon-user::before { content: "\e824"; }
|
|
||||||
|
|
||||||
.icon-ok::before { content: "\e827"; }
|
|
||||||
|
|
||||||
.icon-filter::before { content: "\f0b0"; }
|
|
||||||
|
|
||||||
.icon-download::before { content: "\e825"; }
|
|
||||||
|
|
||||||
.icon-bookmark::before { content: "\e826"; }
|
|
||||||
|
|
||||||
.icon-bookmark-empty::before { content: "\f097"; }
|
|
||||||
|
|
||||||
.icon-music::before { content: "\e828"; }
|
|
||||||
|
|
||||||
.icon-doc::before { content: "\e829"; }
|
|
@ -1,11 +1,11 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Icons";
|
font-family: "Icons";
|
||||||
src: url("./font/fontello.1594374054351.eot");
|
src: url("./font/fontello.1594823398494.eot");
|
||||||
src: url("./font/fontello.1594374054351.eot") format("embedded-opentype"),
|
src: url("./font/fontello.1594823398494.eot") format("embedded-opentype"),
|
||||||
url("./font/fontello.1594374054351.woff2") format("woff2"),
|
url("./font/fontello.1594823398494.woff2") format("woff2"),
|
||||||
url("./font/fontello.1594374054351.woff") format("woff"),
|
url("./font/fontello.1594823398494.woff") format("woff"),
|
||||||
url("./font/fontello.1594374054351.ttf") format("truetype"),
|
url("./font/fontello.1594823398494.ttf") format("truetype"),
|
||||||
url("./font/fontello.1594374054351.svg") format("svg");
|
url("./font/fontello.1594823398494.svg") format("svg");
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
|||||||
{"version":3,"sources":[],"names":[],"mappings":"","file":"static/js/10.2823375ec309b971aaea.js","sourceRoot":""}
|
{"version":3,"sources":[],"names":[],"mappings":"","file":"static/js/10.5ef4671883649cf93524.js","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
|||||||
{"version":3,"sources":[],"names":[],"mappings":"","file":"static/js/11.2cb4b0f72a4654070a58.js","sourceRoot":""}
|
{"version":3,"sources":[],"names":[],"mappings":"","file":"static/js/11.c5b938b4349f87567338.js","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
|||||||
{"version":3,"sources":[],"names":[],"mappings":"","file":"static/js/12.500b3e4676dd47599a58.js","sourceRoot":""}
|
{"version":3,"sources":[],"names":[],"mappings":"","file":"static/js/12.ab82f9512fa85e78c114.js","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
|||||||
{"version":3,"sources":[],"names":[],"mappings":"","file":"static/js/13.3ef79a2643680080d28f.js","sourceRoot":""}
|
{"version":3,"sources":[],"names":[],"mappings":"","file":"static/js/13.40e59c5015d3307b94ad.js","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
|||||||
{"version":3,"sources":[],"names":[],"mappings":"","file":"static/js/14.b7f6eb3ea71d2ac2bb41.js","sourceRoot":""}
|
|
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"sources":[],"names":[],"mappings":"","file":"static/js/14.de791a47ee5249a526b1.js","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
|||||||
{"version":3,"sources":[],"names":[],"mappings":"","file":"static/js/15.d814a29a970070494722.js","sourceRoot":""}
|
|
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"sources":[],"names":[],"mappings":"","file":"static/js/15.e24854297ad682aec45a.js","sourceRoot":""}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue