533dbbb825789ef72936b0131b821a54a4529a24
[akkoma] / lib / pleroma / instances / instance.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 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"}], []),
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