move to 2.5.5
[anni] / lib / pleroma / instances / instance.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Instances.Instance do
6   @moduledoc "Instance."
7
8   alias Pleroma.Instances
9   alias Pleroma.Instances.Instance
10   alias Pleroma.Repo
11   alias Pleroma.User
12   alias Pleroma.Workers.BackgroundWorker
13
14   use Ecto.Schema
15
16   import Ecto.Query
17   import Ecto.Changeset
18
19   require Logger
20
21   schema "instances" do
22     field(:host, :string)
23     field(:unreachable_since, :naive_datetime_usec)
24     field(:favicon, :string)
25     field(:favicon_updated_at, :naive_datetime)
26
27     timestamps()
28   end
29
30   defdelegate host(url_or_host), to: Instances
31
32   def changeset(struct, params \\ %{}) do
33     struct
34     |> cast(params, [:host, :unreachable_since, :favicon, :favicon_updated_at])
35     |> validate_required([:host])
36     |> unique_constraint(:host)
37   end
38
39   def filter_reachable([]), do: %{}
40
41   def filter_reachable(urls_or_hosts) when is_list(urls_or_hosts) do
42     hosts =
43       urls_or_hosts
44       |> Enum.map(&(&1 && host(&1)))
45       |> Enum.filter(&(to_string(&1) != ""))
46
47     unreachable_since_by_host =
48       Repo.all(
49         from(i in Instance,
50           where: i.host in ^hosts,
51           select: {i.host, i.unreachable_since}
52         )
53       )
54       |> Map.new(& &1)
55
56     reachability_datetime_threshold = Instances.reachability_datetime_threshold()
57
58     for entry <- Enum.filter(urls_or_hosts, &is_binary/1) do
59       host = host(entry)
60       unreachable_since = unreachable_since_by_host[host]
61
62       if !unreachable_since ||
63            NaiveDateTime.compare(unreachable_since, reachability_datetime_threshold) == :gt do
64         {entry, unreachable_since}
65       end
66     end
67     |> Enum.filter(& &1)
68     |> Map.new(& &1)
69   end
70
71   def reachable?(url_or_host) when is_binary(url_or_host) do
72     !Repo.one(
73       from(i in Instance,
74         where:
75           i.host == ^host(url_or_host) and
76             i.unreachable_since <= ^Instances.reachability_datetime_threshold(),
77         select: true
78       )
79     )
80   end
81
82   def reachable?(url_or_host) when is_binary(url_or_host), do: true
83
84   def set_reachable(url_or_host) when is_binary(url_or_host) do
85     with host <- host(url_or_host),
86          %Instance{} = existing_record <- Repo.get_by(Instance, %{host: host}) do
87       {:ok, _instance} =
88         existing_record
89         |> changeset(%{unreachable_since: nil})
90         |> Repo.update()
91     end
92   end
93
94   def set_reachable(_), do: {:error, nil}
95
96   def set_unreachable(url_or_host, unreachable_since \\ nil)
97
98   def set_unreachable(url_or_host, unreachable_since) when is_binary(url_or_host) do
99     unreachable_since = parse_datetime(unreachable_since) || NaiveDateTime.utc_now()
100     host = host(url_or_host)
101     existing_record = Repo.get_by(Instance, %{host: host})
102
103     changes = %{unreachable_since: unreachable_since}
104
105     cond do
106       is_nil(existing_record) ->
107         %Instance{}
108         |> changeset(Map.put(changes, :host, host))
109         |> Repo.insert()
110
111       existing_record.unreachable_since &&
112           NaiveDateTime.compare(existing_record.unreachable_since, unreachable_since) != :gt ->
113         {:ok, existing_record}
114
115       true ->
116         existing_record
117         |> changeset(changes)
118         |> Repo.update()
119     end
120   end
121
122   def set_unreachable(_, _), do: {:error, nil}
123
124   def get_consistently_unreachable do
125     reachability_datetime_threshold = Instances.reachability_datetime_threshold()
126
127     from(i in Instance,
128       where: ^reachability_datetime_threshold > i.unreachable_since,
129       order_by: i.unreachable_since,
130       select: {i.host, i.unreachable_since}
131     )
132     |> Repo.all()
133   end
134
135   defp parse_datetime(datetime) when is_binary(datetime) do
136     NaiveDateTime.from_iso8601(datetime)
137   end
138
139   defp parse_datetime(datetime), do: datetime
140
141   def get_or_update_favicon(%URI{host: host} = instance_uri) do
142     existing_record = Repo.get_by(Instance, %{host: host})
143     now = NaiveDateTime.utc_now()
144
145     if existing_record && existing_record.favicon_updated_at &&
146          NaiveDateTime.diff(now, existing_record.favicon_updated_at) < 86_400 do
147       existing_record.favicon
148     else
149       favicon = scrape_favicon(instance_uri)
150
151       if existing_record do
152         existing_record
153         |> changeset(%{favicon: favicon, favicon_updated_at: now})
154         |> Repo.update()
155       else
156         %Instance{}
157         |> changeset(%{host: host, favicon: favicon, favicon_updated_at: now})
158         |> Repo.insert()
159       end
160
161       favicon
162     end
163   rescue
164     e ->
165       Logger.warn("Instance.get_or_update_favicon(\"#{host}\") error: #{inspect(e)}")
166       nil
167   end
168
169   defp scrape_favicon(%URI{} = instance_uri) do
170     try do
171       with {_, true} <- {:reachable, reachable?(instance_uri.host)},
172            {:ok, %Tesla.Env{body: html}} <-
173              Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}], pool: :media),
174            {_, [favicon_rel | _]} when is_binary(favicon_rel) <-
175              {:parse,
176               html |> Floki.parse_document!() |> Floki.attribute("link[rel=icon]", "href")},
177            {_, favicon} when is_binary(favicon) <-
178              {:merge, URI.merge(instance_uri, favicon_rel) |> to_string()} do
179         favicon
180       else
181         {:reachable, false} ->
182           Logger.debug(
183             "Instance.scrape_favicon(\"#{to_string(instance_uri)}\") ignored unreachable host"
184           )
185
186           nil
187
188         _ ->
189           nil
190       end
191     rescue
192       e ->
193         Logger.warn(
194           "Instance.scrape_favicon(\"#{to_string(instance_uri)}\") error: #{inspect(e)}"
195         )
196
197         nil
198     end
199   end
200
201   @doc """
202   Deletes all users from an instance in a background task, thus also deleting
203   all of those users' activities and notifications.
204   """
205   def delete_users_and_activities(host) when is_binary(host) do
206     BackgroundWorker.enqueue("delete_instance", %{"host" => host})
207   end
208
209   def perform(:delete_instance, host) when is_binary(host) do
210     User.Query.build(%{nickname: "@#{host}"})
211     |> Repo.chunk_stream(100, :batches)
212     |> Stream.each(fn users ->
213       users
214       |> Enum.each(fn user ->
215         User.perform(:delete, user)
216       end)
217     end)
218     |> Stream.run()
219   end
220 end