[#3213] `rescue` around potentially-raising `Repo.insert_all/_` calls. Misc. improvem...
[akkoma] / lib / pleroma / hashtag.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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 get_by_name(name) do
25 Repo.get_by(Hashtag, name: name)
26 end
27
28 def get_or_create_by_name(name) when is_bitstring(name) do
29 with %Hashtag{} = hashtag <- get_by_name(name) do
30 {:ok, hashtag}
31 else
32 _ ->
33 %Hashtag{}
34 |> changeset(%{name: name})
35 |> Repo.insert()
36 end
37 end
38
39 def get_or_create_by_names(names) when is_list(names) do
40 timestamp = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
41
42 structs =
43 Enum.map(names, fn name ->
44 %Hashtag{}
45 |> changeset(%{name: name})
46 |> Map.get(:changes)
47 |> Map.merge(%{inserted_at: timestamp, updated_at: timestamp})
48 end)
49
50 try do
51 with {:ok, %{query_op: hashtags}} <-
52 Multi.new()
53 |> Multi.insert_all(:insert_all_op, Hashtag, structs, on_conflict: :nothing)
54 |> Multi.run(:query_op, fn _repo, _changes ->
55 {:ok, Repo.all(from(ht in Hashtag, where: ht.name in ^names))}
56 end)
57 |> Repo.transaction() do
58 {:ok, hashtags}
59 else
60 {:error, _name, value, _changes_so_far} -> {:error, value}
61 end
62 rescue
63 e -> {:error, e}
64 end
65 end
66
67 def changeset(%Hashtag{} = struct, params) do
68 struct
69 |> cast(params, [:name])
70 |> update_change(:name, &String.downcase/1)
71 |> validate_required([:name])
72 |> unique_constraint(:name)
73 end
74
75 def unlink(%Object{id: object_id}) do
76 with {_, hashtag_ids} <-
77 from(hto in "hashtags_objects",
78 where: hto.object_id == ^object_id,
79 select: hto.hashtag_id
80 )
81 |> Repo.delete_all(),
82 {:ok, unreferenced_count} <- delete_unreferenced(hashtag_ids) do
83 {:ok, length(hashtag_ids), unreferenced_count}
84 end
85 end
86
87 @delete_unreferenced_query """
88 DELETE FROM hashtags WHERE id IN
89 (SELECT hashtags.id FROM hashtags
90 LEFT OUTER JOIN hashtags_objects
91 ON hashtags_objects.hashtag_id = hashtags.id
92 WHERE hashtags_objects.hashtag_id IS NULL AND hashtags.id = ANY($1));
93 """
94
95 def delete_unreferenced(ids) do
96 with {:ok, %{num_rows: deleted_count}} <- Repo.query(@delete_unreferenced_query, [ids]) do
97 {:ok, deleted_count}
98 end
99 end
100 end