logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://hacktivis.me/git/pleroma.git

instance.ex (9284B)


  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. defmodule Pleroma.Instances.Instance do
  5. @moduledoc "Instance."
  6. alias Pleroma.Instances
  7. alias Pleroma.Instances.Instance
  8. alias Pleroma.Maps
  9. alias Pleroma.Repo
  10. alias Pleroma.User
  11. alias Pleroma.Workers.BackgroundWorker
  12. use Ecto.Schema
  13. import Ecto.Query
  14. import Ecto.Changeset
  15. require Logger
  16. schema "instances" do
  17. field(:host, :string)
  18. field(:unreachable_since, :naive_datetime_usec)
  19. field(:favicon, :string)
  20. field(:favicon_updated_at, :naive_datetime)
  21. embeds_one :metadata, Pleroma.Instances.Metadata, primary_key: false do
  22. field(:software_name, :string)
  23. field(:software_version, :string)
  24. field(:software_repository, :string)
  25. end
  26. field(:metadata_updated_at, :utc_datetime)
  27. timestamps()
  28. end
  29. defdelegate host(url_or_host), to: Instances
  30. def changeset(struct, params \\ %{}) do
  31. struct
  32. |> cast(params, __schema__(:fields) -- [:metadata])
  33. |> cast_embed(:metadata, with: &metadata_changeset/2)
  34. |> validate_required([:host])
  35. |> unique_constraint(:host)
  36. end
  37. def metadata_changeset(struct, params \\ %{}) do
  38. struct
  39. |> cast(params, [:software_name, :software_version, :software_repository])
  40. end
  41. def filter_reachable([]), do: %{}
  42. def filter_reachable(urls_or_hosts) when is_list(urls_or_hosts) do
  43. hosts =
  44. urls_or_hosts
  45. |> Enum.map(&(&1 && host(&1)))
  46. |> Enum.filter(&(to_string(&1) != ""))
  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. reachability_datetime_threshold = Instances.reachability_datetime_threshold()
  56. for entry <- Enum.filter(urls_or_hosts, &is_binary/1) do
  57. host = host(entry)
  58. unreachable_since = unreachable_since_by_host[host]
  59. if !unreachable_since ||
  60. NaiveDateTime.compare(unreachable_since, reachability_datetime_threshold) == :gt do
  61. {entry, unreachable_since}
  62. end
  63. end
  64. |> Enum.filter(& &1)
  65. |> Map.new(& &1)
  66. end
  67. def reachable?(url_or_host) when is_binary(url_or_host) do
  68. !Repo.one(
  69. from(i in Instance,
  70. where:
  71. i.host == ^host(url_or_host) and
  72. i.unreachable_since <= ^Instances.reachability_datetime_threshold(),
  73. select: true
  74. )
  75. )
  76. end
  77. def reachable?(url_or_host) when is_binary(url_or_host), do: true
  78. def set_reachable(url_or_host) when is_binary(url_or_host) do
  79. %Instance{host: host(url_or_host)}
  80. |> changeset(%{unreachable_since: nil})
  81. |> Repo.insert(on_conflict: {:replace, [:unreachable_since]}, conflict_target: :host)
  82. end
  83. def set_reachable(_), do: {:error, nil}
  84. def set_unreachable(url_or_host, unreachable_since \\ nil)
  85. def set_unreachable(url_or_host, unreachable_since) when is_binary(url_or_host) do
  86. unreachable_since = parse_datetime(unreachable_since) || NaiveDateTime.utc_now()
  87. host = host(url_or_host)
  88. existing_record = Repo.get_by(Instance, %{host: host})
  89. changes = %{unreachable_since: unreachable_since}
  90. cond do
  91. is_nil(existing_record) ->
  92. %Instance{}
  93. |> changeset(Map.put(changes, :host, host))
  94. |> Repo.insert()
  95. existing_record.unreachable_since &&
  96. NaiveDateTime.compare(existing_record.unreachable_since, unreachable_since) != :gt ->
  97. {:ok, existing_record}
  98. true ->
  99. existing_record
  100. |> changeset(changes)
  101. |> Repo.update()
  102. end
  103. end
  104. def set_unreachable(_, _), do: {:error, nil}
  105. def get_consistently_unreachable do
  106. reachability_datetime_threshold = Instances.reachability_datetime_threshold()
  107. from(i in Instance,
  108. where: ^reachability_datetime_threshold > i.unreachable_since,
  109. order_by: i.unreachable_since,
  110. select: {i.host, i.unreachable_since}
  111. )
  112. |> Repo.all()
  113. end
  114. defp parse_datetime(datetime) when is_binary(datetime) do
  115. NaiveDateTime.from_iso8601(datetime)
  116. end
  117. defp parse_datetime(datetime), do: datetime
  118. def get_or_update_favicon(%URI{host: host} = instance_uri) do
  119. existing_record = Repo.get_by(Instance, %{host: host})
  120. now = NaiveDateTime.utc_now()
  121. if existing_record && existing_record.favicon_updated_at &&
  122. NaiveDateTime.diff(now, existing_record.favicon_updated_at) < 86_400 do
  123. existing_record.favicon
  124. else
  125. favicon = scrape_favicon(instance_uri)
  126. if existing_record do
  127. existing_record
  128. |> changeset(%{favicon: favicon, favicon_updated_at: now})
  129. |> Repo.update()
  130. else
  131. %Instance{}
  132. |> changeset(%{host: host, favicon: favicon, favicon_updated_at: now})
  133. |> Repo.insert()
  134. end
  135. favicon
  136. end
  137. rescue
  138. e ->
  139. Logger.warning("Instance.get_or_update_favicon(\"#{host}\") error: #{inspect(e)}")
  140. nil
  141. end
  142. defp scrape_favicon(%URI{} = instance_uri) do
  143. try do
  144. with {_, true} <- {:reachable, reachable?(instance_uri.host)},
  145. {:ok, %Tesla.Env{body: html}} <-
  146. Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}], pool: :media),
  147. {_, [favicon_rel | _]} when is_binary(favicon_rel) <-
  148. {:parse,
  149. html |> Floki.parse_document!() |> Floki.attribute("link[rel=icon]", "href")},
  150. {_, favicon} when is_binary(favicon) <-
  151. {:merge, URI.merge(instance_uri, favicon_rel) |> to_string()} do
  152. favicon
  153. else
  154. {:reachable, false} ->
  155. Logger.debug(
  156. "Instance.scrape_favicon(\"#{to_string(instance_uri)}\") ignored unreachable host"
  157. )
  158. nil
  159. _ ->
  160. nil
  161. end
  162. rescue
  163. e ->
  164. Logger.warning(
  165. "Instance.scrape_favicon(\"#{to_string(instance_uri)}\") error: #{inspect(e)}"
  166. )
  167. nil
  168. end
  169. end
  170. def get_or_update_metadata(%URI{host: host} = instance_uri) do
  171. existing_record = Repo.get_by(Instance, %{host: host})
  172. now = NaiveDateTime.utc_now()
  173. if existing_record && existing_record.metadata_updated_at &&
  174. NaiveDateTime.diff(now, existing_record.metadata_updated_at) < 86_400 do
  175. existing_record.metadata
  176. else
  177. metadata = scrape_metadata(instance_uri)
  178. if existing_record do
  179. existing_record
  180. |> changeset(%{metadata: metadata, metadata_updated_at: now})
  181. |> Repo.update()
  182. else
  183. %Instance{}
  184. |> changeset(%{host: host, metadata: metadata, metadata_updated_at: now})
  185. |> Repo.insert()
  186. end
  187. metadata
  188. end
  189. end
  190. defp get_nodeinfo_uri(well_known) do
  191. links = Map.get(well_known, "links", [])
  192. nodeinfo21 =
  193. Enum.find(links, &(&1["rel"] == "http://nodeinfo.diaspora.software/ns/schema/2.1"))["href"]
  194. nodeinfo20 =
  195. Enum.find(links, &(&1["rel"] == "http://nodeinfo.diaspora.software/ns/schema/2.0"))["href"]
  196. cond do
  197. is_binary(nodeinfo21) -> {:ok, nodeinfo21}
  198. is_binary(nodeinfo20) -> {:ok, nodeinfo20}
  199. true -> {:error, :no_links}
  200. end
  201. end
  202. defp scrape_metadata(%URI{} = instance_uri) do
  203. try do
  204. with {_, true} <- {:reachable, reachable?(instance_uri.host)},
  205. {:ok, %Tesla.Env{body: well_known_body}} <-
  206. instance_uri
  207. |> URI.merge("/.well-known/nodeinfo")
  208. |> to_string()
  209. |> Pleroma.HTTP.get([{"accept", "application/json"}]),
  210. {:ok, well_known_json} <- Jason.decode(well_known_body),
  211. {:ok, nodeinfo_uri} <- get_nodeinfo_uri(well_known_json),
  212. {:ok, %Tesla.Env{body: nodeinfo_body}} <-
  213. Pleroma.HTTP.get(nodeinfo_uri, [{"accept", "application/json"}]),
  214. {:ok, nodeinfo} <- Jason.decode(nodeinfo_body) do
  215. # Can extract more metadata from NodeInfo but need to be careful about it's size,
  216. # can't just dump the entire thing
  217. software = Map.get(nodeinfo, "software", %{})
  218. %{
  219. software_name: software["name"],
  220. software_version: software["version"]
  221. }
  222. |> Maps.put_if_present(:software_repository, software["repository"])
  223. else
  224. {:reachable, false} ->
  225. Logger.debug(
  226. "Instance.scrape_metadata(\"#{to_string(instance_uri)}\") ignored unreachable host"
  227. )
  228. nil
  229. _ ->
  230. nil
  231. end
  232. rescue
  233. e ->
  234. Logger.warning(
  235. "Instance.scrape_metadata(\"#{to_string(instance_uri)}\") error: #{inspect(e)}"
  236. )
  237. nil
  238. end
  239. end
  240. @doc """
  241. Deletes all users from an instance in a background task, thus also deleting
  242. all of those users' activities and notifications.
  243. """
  244. def delete_users_and_activities(host) when is_binary(host) do
  245. BackgroundWorker.enqueue("delete_instance", %{"host" => host})
  246. end
  247. def perform(:delete_instance, host) when is_binary(host) do
  248. User.Query.build(%{nickname: "@#{host}"})
  249. |> Repo.chunk_stream(100, :batches)
  250. |> Stream.each(fn users ->
  251. users
  252. |> Enum.each(fn user ->
  253. User.perform(:delete, user)
  254. end)
  255. end)
  256. |> Stream.run()
  257. end
  258. end