Add ability to follow hashtags (#336)
[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.User.HashtagFollow
14 alias Pleroma.Object
15 alias Pleroma.Repo
16
17 schema "hashtags" do
18 field(:name, :string)
19
20 many_to_many(:objects, Object, join_through: "hashtags_objects", on_replace: :delete)
21
22 timestamps()
23 end
24
25 def normalize_name(name) do
26 name
27 |> String.downcase()
28 |> String.trim()
29 end
30
31 def get_by_id(id) do
32 Repo.get(Hashtag, id)
33 end
34
35 def get_by_name(name) do
36 Repo.get_by(Hashtag, name: normalize_name(name))
37 end
38
39 def get_or_create_by_name(name) do
40 changeset = changeset(%Hashtag{}, %{name: name})
41
42 Repo.insert(
43 changeset,
44 on_conflict: [set: [name: get_field(changeset, :name)]],
45 conflict_target: :name,
46 returning: true
47 )
48 end
49
50 def get_or_create_by_names(names) when is_list(names) do
51 names = Enum.map(names, &normalize_name/1)
52 timestamp = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
53
54 structs =
55 Enum.map(names, fn name ->
56 %Hashtag{}
57 |> changeset(%{name: name})
58 |> Map.get(:changes)
59 |> Map.merge(%{inserted_at: timestamp, updated_at: timestamp})
60 end)
61
62 try do
63 with {:ok, %{query_op: hashtags}} <-
64 Multi.new()
65 |> Multi.insert_all(:insert_all_op, Hashtag, structs,
66 on_conflict: :nothing,
67 conflict_target: :name
68 )
69 |> Multi.run(:query_op, fn _repo, _changes ->
70 {:ok, Repo.all(from(ht in Hashtag, where: ht.name in ^names))}
71 end)
72 |> Repo.transaction() do
73 {:ok, hashtags}
74 else
75 {:error, _name, value, _changes_so_far} -> {:error, value}
76 end
77 rescue
78 e -> {:error, e}
79 end
80 end
81
82 def changeset(%Hashtag{} = struct, params) do
83 struct
84 |> cast(params, [:name])
85 |> update_change(:name, &normalize_name/1)
86 |> validate_required([:name])
87 |> unique_constraint(:name)
88 end
89
90 def unlink(%Object{id: object_id}) do
91 with {_, hashtag_ids} <-
92 from(hto in "hashtags_objects",
93 where: hto.object_id == ^object_id,
94 select: hto.hashtag_id
95 )
96 |> Repo.delete_all(),
97 {:ok, unreferenced_count} <- delete_unreferenced(hashtag_ids) do
98 {:ok, length(hashtag_ids), unreferenced_count}
99 end
100 end
101
102 @delete_unreferenced_query """
103 DELETE FROM hashtags WHERE id IN
104 (SELECT hashtags.id FROM hashtags
105 LEFT OUTER JOIN hashtags_objects
106 ON hashtags_objects.hashtag_id = hashtags.id
107 WHERE hashtags_objects.hashtag_id IS NULL AND hashtags.id = ANY($1));
108 """
109
110 def delete_unreferenced(ids) do
111 with {:ok, %{num_rows: deleted_count}} <- Repo.query(@delete_unreferenced_query, [ids]) do
112 {:ok, deleted_count}
113 end
114 end
115
116 def get_followers(%Hashtag{id: hashtag_id}) do
117 from(hf in HashtagFollow)
118 |> where([hf], hf.hashtag_id == ^hashtag_id)
119 |> join(:inner, [hf], u in assoc(hf, :user))
120 |> select([hf, u], u.id)
121 |> Repo.all()
122 end
123
124 def get_recipients_for_activity(%Pleroma.Activity{object: %{hashtags: tags}})
125 when is_list(tags) do
126 tags
127 |> Enum.map(&get_followers/1)
128 |> List.flatten()
129 |> Enum.uniq()
130 end
131
132 def get_recipients_for_activity(_activity), do: []
133 end