1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Instances.Instance do
8 @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
10 alias Pleroma.Instances
11 alias Pleroma.Instances.Instance
14 alias Pleroma.Workers.BackgroundWorker
25 field(:unreachable_since, :naive_datetime_usec)
26 field(:favicon, :string)
27 field(:metadata_updated_at, :naive_datetime)
28 field(:nodeinfo, :map, default: %{})
29 field(:has_request_signatures, :boolean)
34 defdelegate host(url_or_host), to: Instances
36 def changeset(struct, params \\ %{}) do
44 :has_request_signatures
46 |> validate_required([:host])
47 |> unique_constraint(:host)
50 def filter_reachable([]), do: %{}
52 def filter_reachable(urls_or_hosts) when is_list(urls_or_hosts) do
55 |> Enum.map(&(&1 && host(&1)))
56 |> Enum.filter(&(to_string(&1) != ""))
58 unreachable_since_by_host =
61 where: i.host in ^hosts,
62 select: {i.host, i.unreachable_since}
67 reachability_datetime_threshold = Instances.reachability_datetime_threshold()
69 for entry <- Enum.filter(urls_or_hosts, &is_binary/1) do
71 unreachable_since = unreachable_since_by_host[host]
73 if !unreachable_since ||
74 NaiveDateTime.compare(unreachable_since, reachability_datetime_threshold) == :gt do
75 {entry, unreachable_since}
82 def reachable?(url_or_host) when is_binary(url_or_host) do
86 i.host == ^host(url_or_host) and
87 i.unreachable_since <= ^Instances.reachability_datetime_threshold(),
93 def reachable?(url_or_host) when is_binary(url_or_host), do: true
95 def set_reachable(url_or_host) when is_binary(url_or_host) do
96 with host <- host(url_or_host),
97 %Instance{} = existing_record <- Repo.get_by(Instance, %{host: host}) do
100 |> changeset(%{unreachable_since: nil})
105 def set_reachable(_), do: {:error, nil}
107 def set_unreachable(url_or_host, unreachable_since \\ nil)
109 def set_unreachable(url_or_host, unreachable_since) when is_binary(url_or_host) do
110 unreachable_since = parse_datetime(unreachable_since) || NaiveDateTime.utc_now()
111 host = host(url_or_host)
112 existing_record = Repo.get_by(Instance, %{host: host})
114 changes = %{unreachable_since: unreachable_since}
117 is_nil(existing_record) ->
119 |> changeset(Map.put(changes, :host, host))
122 existing_record.unreachable_since &&
123 NaiveDateTime.compare(existing_record.unreachable_since, unreachable_since) != :gt ->
124 {:ok, existing_record}
128 |> changeset(changes)
133 def set_unreachable(_, _), do: {:error, nil}
135 def get_consistently_unreachable do
136 reachability_datetime_threshold = Instances.reachability_datetime_threshold()
139 where: ^reachability_datetime_threshold > i.unreachable_since,
140 order_by: i.unreachable_since,
141 select: {i.host, i.unreachable_since}
146 defp parse_datetime(datetime) when is_binary(datetime) do
147 NaiveDateTime.from_iso8601(datetime)
150 defp parse_datetime(datetime), do: datetime
152 def needs_update(nil), do: true
154 def needs_update(%Instance{metadata_updated_at: nil}), do: true
156 def needs_update(%Instance{metadata_updated_at: metadata_updated_at}) do
157 now = NaiveDateTime.utc_now()
158 NaiveDateTime.diff(now, metadata_updated_at) > 86_400
163 host: Pleroma.Web.Endpoint.host(),
164 favicon: Pleroma.Web.Endpoint.url() <> "/favicon.png",
165 nodeinfo: Pleroma.Web.Nodeinfo.NodeinfoController.raw_nodeinfo()
169 def update_metadata(%URI{host: host} = uri) do
170 Logger.debug("Checking metadata for #{host}")
171 existing_record = Repo.get_by(Instance, %{host: host})
173 if reachable?(host) do
174 do_update_metadata(uri, existing_record)
176 {:discard, :unreachable}
180 defp do_update_metadata(%URI{host: host} = uri, existing_record) do
181 if existing_record do
182 if needs_update(existing_record) do
183 Logger.info("Updating metadata for #{host}")
184 favicon = scrape_favicon(uri)
185 nodeinfo = scrape_nodeinfo(uri)
192 metadata_updated_at: NaiveDateTime.utc_now()
196 {:discard, "Does not require update"}
199 favicon = scrape_favicon(uri)
200 nodeinfo = scrape_nodeinfo(uri)
202 Logger.info("Creating metadata for #{host}")
209 metadata_updated_at: NaiveDateTime.utc_now()
215 def get_favicon(%URI{host: host}) do
216 existing_record = Repo.get_by(Instance, %{host: host})
218 if existing_record do
219 existing_record.favicon
225 defp scrape_nodeinfo(%URI{} = instance_uri) do
226 with true <- Pleroma.Config.get([:instances_nodeinfo, :enabled]),
227 {_, true} <- {:reachable, reachable?(instance_uri.host)},
228 {:ok, %Tesla.Env{status: 200, body: body}} <-
230 "https://#{instance_uri.host}/.well-known/nodeinfo",
231 headers: [{"Accept", "application/json"}]
233 {:ok, json} <- Jason.decode(body),
234 {:ok, %{"links" => links}} <- {:ok, json},
235 {:ok, %{"href" => href}} <-
237 Enum.find(links, &(&1["rel"] == "http://nodeinfo.diaspora.software/ns/schema/2.0"))},
238 {:ok, %Tesla.Env{body: data}} <-
239 Pleroma.HTTP.get(href, [{"accept", "application/json"}], []),
240 {:length, true} <- {:length, String.length(data) < 50_000},
241 {:ok, nodeinfo} <- Jason.decode(data) do
244 {:reachable, false} ->
246 "Instance.scrape_nodeinfo(\"#{to_string(instance_uri)}\") ignored unreachable host"
253 "Instance.scrape_nodeinfo(\"#{to_string(instance_uri)}\") ignored too long body"
263 defp scrape_favicon(%URI{} = instance_uri) do
264 with true <- Pleroma.Config.get([:instances_favicons, :enabled]),
265 {_, true} <- {:reachable, reachable?(instance_uri.host)},
266 {:ok, %Tesla.Env{body: html}} <-
267 Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}], []),
268 {_, [favicon_rel | _]} when is_binary(favicon_rel) <-
269 {:parse, html |> Floki.parse_document!() |> Floki.attribute("link[rel=icon]", "href")},
270 {_, favicon} when is_binary(favicon) <-
271 {:merge, URI.merge(instance_uri, favicon_rel) |> to_string()},
272 {:length, true} <- {:length, String.length(favicon) < 255} do
275 {:reachable, false} ->
277 "Instance.scrape_favicon(\"#{to_string(instance_uri)}\") ignored unreachable host"
288 Deletes all users from an instance in a background task, thus also deleting
289 all of those users' activities and notifications.
291 def delete_users_and_activities(host) when is_binary(host) do
292 BackgroundWorker.enqueue("delete_instance", %{"host" => host})
295 def perform(:delete_instance, host) when is_binary(host) do
296 User.Query.build(%{nickname: "@#{host}"})
297 |> Repo.chunk_stream(100, :batches)
298 |> Stream.each(fn users ->
300 |> Enum.each(fn user ->
301 User.perform(:delete, user)
307 def get_by_url(url_or_host) do
308 url = host(url_or_host)
309 Repo.get_by(Instance, host: url)
312 def get_cached_by_url(url_or_host) do
313 url = host(url_or_host)
315 if url == Pleroma.Web.Endpoint.host() do
318 @cachex.fetch!(:instances_cache, "instances:#{url}", fn _ ->
319 with %Instance{} = instance <- get_by_url(url) do
320 {:commit, {:ok, instance}}
328 def set_request_signatures(url_or_host) when is_binary(url_or_host) do
329 host = host(url_or_host)
330 existing_record = Repo.get_by(Instance, %{host: host})
331 changes = %{has_request_signatures: true}
334 is_nil(existing_record) ->
336 |> changeset(Map.put(changes, :host, host))
341 |> changeset(changes)
346 def set_request_signatures(_), do: {:error, :invalid_input}