move to 2.5.5
[anni] / lib / pleroma / hashtag.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.Hashtag do
6   use Ecto.Schema
7
8   import Ecto.Changeset
9   import Ecto.Query
10
11   alias Ecto.Multi
12   alias Pleroma.Hashtag
13   alias Pleroma.Object
14   alias Pleroma.Repo
15
16   schema "hashtags" do
17     field(:name, :string)
18
19     many_to_many(:objects, Object, join_through: "hashtags_objects", on_replace: :delete)
20
21     timestamps()
22   end
23
24   def normalize_name(name) do
25     name
26     |> String.downcase()
27     |> String.trim()
28   end
29
30   def get_or_create_by_name(name) do
31     changeset = changeset(%Hashtag{}, %{name: name})
32
33     Repo.insert(
34       changeset,
35       on_conflict: [set: [name: get_field(changeset, :name)]],
36       conflict_target: :name,
37       returning: true
38     )
39   end
40
41   def get_or_create_by_names(names) when is_list(names) do
42     names = Enum.map(names, &normalize_name/1)
43     timestamp = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
44
45     structs =
46       Enum.map(names, fn name ->
47         %Hashtag{}
48         |> changeset(%{name: name})
49         |> Map.get(:changes)
50         |> Map.merge(%{inserted_at: timestamp, updated_at: timestamp})
51       end)
52
53     try do
54       with {:ok, %{query_op: hashtags}} <-
55              Multi.new()
56              |> Multi.insert_all(:insert_all_op, Hashtag, structs,
57                on_conflict: :nothing,
58                conflict_target: :name
59              )
60              |> Multi.run(:query_op, fn _repo, _changes ->
61                {:ok, Repo.all(from(ht in Hashtag, where: ht.name in ^names))}
62              end)
63              |> Repo.transaction() do
64         {:ok, hashtags}
65       else
66         {:error, _name, value, _changes_so_far} -> {:error, value}
67       end
68     rescue
69       e -> {:error, e}
70     end
71   end
72
73   def changeset(%Hashtag{} = struct, params) do
74     struct
75     |> cast(params, [:name])
76     |> update_change(:name, &normalize_name/1)
77     |> validate_required([:name])
78     |> unique_constraint(:name)
79   end
80
81   def unlink(%Object{id: object_id}) do
82     with {_, hashtag_ids} <-
83            from(hto in "hashtags_objects",
84              where: hto.object_id == ^object_id,
85              select: hto.hashtag_id
86            )
87            |> Repo.delete_all(),
88          {:ok, unreferenced_count} <- delete_unreferenced(hashtag_ids) do
89       {:ok, length(hashtag_ids), unreferenced_count}
90     end
91   end
92
93   @delete_unreferenced_query """
94   DELETE FROM hashtags WHERE id IN
95     (SELECT hashtags.id FROM hashtags
96       LEFT OUTER JOIN hashtags_objects
97         ON hashtags_objects.hashtag_id = hashtags.id
98       WHERE hashtags_objects.hashtag_id IS NULL AND hashtags.id = ANY($1));
99   """
100
101   def delete_unreferenced(ids) do
102     with {:ok, %{num_rows: deleted_count}} <- Repo.query(@delete_unreferenced_query, [ids]) do
103       {:ok, deleted_count}
104     end
105   end
106 end