First
[anni] / lib / pleroma / web / push / impl.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.Web.Push.Impl do
6   @moduledoc "The module represents implementation push web notification"
7
8   alias Pleroma.Activity
9   alias Pleroma.Notification
10   alias Pleroma.Object
11   alias Pleroma.Repo
12   alias Pleroma.User
13   alias Pleroma.Web.Metadata.Utils
14   alias Pleroma.Web.Push.Subscription
15
16   require Logger
17   import Ecto.Query
18
19   @types ["Create", "Follow", "Announce", "Like", "Move", "EmojiReact", "Update"]
20
21   @doc "Performs sending notifications for user subscriptions"
22   @spec perform(Notification.t()) :: list(any) | :error | {:error, :unknown_type}
23   def perform(
24         %{
25           activity: %{data: %{"type" => activity_type}} = activity,
26           user: %User{id: user_id}
27         } = notification
28       )
29       when activity_type in @types do
30     actor = User.get_cached_by_ap_id(notification.activity.data["actor"])
31
32     mastodon_type = notification.type
33     gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
34     avatar_url = User.avatar_url(actor)
35     object = Object.normalize(activity, fetch: false)
36     user = User.get_cached_by_id(user_id)
37     direct_conversation_id = Activity.direct_conversation_id(activity, user)
38
39     for subscription <- fetch_subscriptions(user_id),
40         Subscription.enabled?(subscription, mastodon_type) do
41       %{
42         access_token: subscription.token.token,
43         notification_id: notification.id,
44         notification_type: mastodon_type,
45         icon: avatar_url,
46         preferred_locale: "en",
47         pleroma: %{
48           activity_id: notification.activity.id,
49           direct_conversation_id: direct_conversation_id
50         }
51       }
52       |> Map.merge(build_content(notification, actor, object, mastodon_type))
53       |> Jason.encode!()
54       |> push_message(build_sub(subscription), gcm_api_key, subscription)
55     end
56     |> (&{:ok, &1}).()
57   end
58
59   def perform(_) do
60     Logger.warn("Unknown notification type")
61     {:error, :unknown_type}
62   end
63
64   @doc "Push message to web"
65   def push_message(body, sub, api_key, subscription) do
66     case WebPushEncryption.send_web_push(body, sub, api_key) do
67       {:ok, %{status: code}} when code in 400..499 ->
68         Logger.debug("Removing subscription record")
69         Repo.delete!(subscription)
70         :ok
71
72       {:ok, %{status: code}} when code in 200..299 ->
73         :ok
74
75       {:ok, %{status: code}} ->
76         Logger.error("Web Push Notification failed with code: #{code}")
77         :error
78
79       error ->
80         Logger.error("Web Push Notification failed with #{inspect(error)}")
81         :error
82     end
83   end
84
85   @doc "Gets user subscriptions"
86   def fetch_subscriptions(user_id) do
87     Subscription
88     |> where(user_id: ^user_id)
89     |> preload(:token)
90     |> Repo.all()
91   end
92
93   def build_sub(subscription) do
94     %{
95       keys: %{
96         p256dh: subscription.key_p256dh,
97         auth: subscription.key_auth
98       },
99       endpoint: subscription.endpoint
100     }
101   end
102
103   def build_content(notification, actor, object, mastodon_type \\ nil)
104
105   def build_content(
106         %{
107           user: %{notification_settings: %{hide_notification_contents: true}}
108         } = notification,
109         _actor,
110         _object,
111         mastodon_type
112       ) do
113     %{body: format_title(notification, mastodon_type)}
114   end
115
116   def build_content(notification, actor, object, mastodon_type) do
117     mastodon_type = mastodon_type || notification.type
118
119     %{
120       title: format_title(notification, mastodon_type),
121       body: format_body(notification, actor, object, mastodon_type)
122     }
123   end
124
125   def format_body(activity, actor, object, mastodon_type \\ nil)
126
127   def format_body(_activity, actor, %{data: %{"type" => "ChatMessage"} = data}, _) do
128     case data["content"] do
129       nil -> "@#{actor.nickname}: (Attachment)"
130       content -> "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
131     end
132   end
133
134   def format_body(
135         %{activity: %{data: %{"type" => "Create"}}},
136         actor,
137         %{data: %{"content" => content}},
138         _mastodon_type
139       ) do
140     "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
141   end
142
143   def format_body(
144         %{activity: %{data: %{"type" => "Announce"}}},
145         actor,
146         %{data: %{"content" => content}},
147         _mastodon_type
148       ) do
149     "@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}"
150   end
151
152   def format_body(
153         %{activity: %{data: %{"type" => "EmojiReact", "content" => content}}},
154         actor,
155         _object,
156         _mastodon_type
157       ) do
158     "@#{actor.nickname} reacted with #{content}"
159   end
160
161   def format_body(
162         %{activity: %{data: %{"type" => type}}} = notification,
163         actor,
164         _object,
165         mastodon_type
166       )
167       when type in ["Follow", "Like"] do
168     mastodon_type = mastodon_type || notification.type
169
170     case mastodon_type do
171       "follow" -> "@#{actor.nickname} has followed you"
172       "follow_request" -> "@#{actor.nickname} has requested to follow you"
173       "favourite" -> "@#{actor.nickname} has favorited your post"
174     end
175   end
176
177   def format_body(
178         %{activity: %{data: %{"type" => "Update"}}},
179         actor,
180         _object,
181         _mastodon_type
182       ) do
183     "@#{actor.nickname} edited a status"
184   end
185
186   def format_title(activity, mastodon_type \\ nil)
187
188   def format_title(%{activity: %{data: %{"directMessage" => true}}}, _mastodon_type) do
189     "New Direct Message"
190   end
191
192   def format_title(%{type: type}, mastodon_type) do
193     case mastodon_type || type do
194       "mention" -> "New Mention"
195       "follow" -> "New Follower"
196       "follow_request" -> "New Follow Request"
197       "reblog" -> "New Repeat"
198       "favourite" -> "New Favorite"
199       "update" -> "New Update"
200       "pleroma:chat_mention" -> "New Chat Message"
201       "pleroma:emoji_reaction" -> "New Reaction"
202       type -> "New #{String.capitalize(type || "event")}"
203     end
204   end
205 end