impl.ex 5.67 KB
Newer Older
Maksim's avatar
Maksim committed
1
# Pleroma: A lightweight social networking server
Haelwenn's avatar
Haelwenn committed
2
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
Maksim's avatar
Maksim committed
3 4 5 6 7 8
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.Push.Impl do
  @moduledoc "The module represents implementation push web notification"

  alias Pleroma.Activity
9
  alias Pleroma.Notification
Maksim's avatar
Maksim committed
10
  alias Pleroma.Object
11 12
  alias Pleroma.Repo
  alias Pleroma.User
Maksim's avatar
Maksim committed
13
  alias Pleroma.Web.Metadata.Utils
14
  alias Pleroma.Web.Push.Subscription
Maksim's avatar
Maksim committed
15 16 17 18

  require Logger
  import Ecto.Query

19
  @types ["Create", "Follow", "Announce", "Like", "Move", "EmojiReact"]
Maksim's avatar
Maksim committed
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 26
          activity: %{data: %{"type" => activity_type}} = activity,
          user: %User{id: user_id}
27
        } = notification
lain's avatar
lain committed
28
      )
Maksim's avatar
Maksim committed
29
      when activity_type in @types do
30
    actor = User.get_cached_by_ap_id(notification.activity.data["actor"])
Maksim's avatar
Maksim committed
31

32
    mastodon_type = notification.type
Maksim's avatar
Maksim committed
33 34
    gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
    avatar_url = User.avatar_url(actor)
35
    object = Object.normalize(activity, fetch: false)
36 37
    user = User.get_cached_by_id(user_id)
    direct_conversation_id = Activity.direct_conversation_id(activity, user)
Maksim's avatar
Maksim committed
38

39 40
    for subscription <- fetch_subscriptions(user_id),
        Subscription.enabled?(subscription, mastodon_type) do
Maksim's avatar
Maksim committed
41 42
      %{
        access_token: subscription.token.token,
43 44
        notification_id: notification.id,
        notification_type: mastodon_type,
Maksim's avatar
Maksim committed
45
        icon: avatar_url,
46 47
        preferred_locale: "en",
        pleroma: %{
48
          activity_id: notification.activity.id,
49
          direct_conversation_id: direct_conversation_id
50
        }
Maksim's avatar
Maksim committed
51
      }
52
      |> Map.merge(build_content(notification, actor, object, mastodon_type))
Maksim's avatar
Maksim committed
53 54 55
      |> Jason.encode!()
      |> push_message(build_sub(subscription), gcm_api_key, subscription)
    end
lain's avatar
lain committed
56
    |> (&{:ok, &1}).()
Maksim's avatar
Maksim committed
57 58
  end

59
  def perform(_) do
Maksim's avatar
Maksim committed
60
    Logger.warn("Unknown notification type")
lain's avatar
lain committed
61
    {:error, :unknown_type}
Maksim's avatar
Maksim committed
62 63 64 65 66
  end

  @doc "Push message to web"
  def push_message(body, sub, api_key, subscription) do
    case WebPushEncryption.send_web_push(body, sub, api_key) do
67
      {:ok, %{status: code}} when code in 400..499 ->
Maksim's avatar
Maksim committed
68 69 70 71
        Logger.debug("Removing subscription record")
        Repo.delete!(subscription)
        :ok

72
      {:ok, %{status: code}} when code in 200..299 ->
Maksim's avatar
Maksim committed
73 74
        :ok

75
      {:ok, %{status: code}} ->
Maksim's avatar
Maksim committed
76 77 78
        Logger.error("Web Push Notification failed with code: #{code}")
        :error

79 80
      error ->
        Logger.error("Web Push Notification failed with #{inspect(error)}")
Maksim's avatar
Maksim committed
81 82 83 84 85
        :error
    end
  end

  @doc "Gets user subscriptions"
86
  def fetch_subscriptions(user_id) do
Maksim's avatar
Maksim committed
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
    Subscription
    |> where(user_id: ^user_id)
    |> preload(:token)
    |> Repo.all()
  end

  def build_sub(subscription) do
    %{
      keys: %{
        p256dh: subscription.key_p256dh,
        auth: subscription.key_auth
      },
      endpoint: subscription.endpoint
    }
  end

103 104
  def build_content(notification, actor, object, mastodon_type \\ nil)

105 106
  def build_content(
        %{
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 115
  end

116
  def build_content(notification, actor, object, mastodon_type) do
117
    mastodon_type = mastodon_type || notification.type
118

119
    %{
120 121
      title: format_title(notification, mastodon_type),
      body: format_body(notification, actor, object, mastodon_type)
122 123 124
    }
  end

125 126
  def format_body(activity, actor, object, mastodon_type \\ nil)

127 128
  def format_body(_activity, actor, %{data: %{"type" => "ChatMessage"} = data}, _) do
    case data["content"] do
129 130 131 132 133
      nil -> "@#{actor.nickname}: (Attachment)"
      content -> "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
    end
  end

Maksim's avatar
Maksim committed
134
  def format_body(
135 136
        %{activity: %{data: %{"type" => "Create"}}},
        actor,
137 138
        %{data: %{"content" => content}},
        _mastodon_type
Maksim's avatar
Maksim committed
139 140 141 142 143
      ) do
    "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
  end

  def format_body(
144 145
        %{activity: %{data: %{"type" => "Announce"}}},
        actor,
146 147
        %{data: %{"content" => content}},
        _mastodon_type
Maksim's avatar
Maksim committed
148 149 150 151
      ) do
    "@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}"
  end

152 153 154 155 156 157
  def format_body(
        %{activity: %{data: %{"type" => "EmojiReact", "content" => content}}},
        actor,
        _object,
        _mastodon_type
      ) do
158
    "@#{actor.nickname} reacted with #{content}"
159 160
  end

Maksim's avatar
Maksim committed
161
  def format_body(
162
        %{activity: %{data: %{"type" => type}}} = notification,
163
        actor,
164 165
        _object,
        mastodon_type
Maksim's avatar
Maksim committed
166 167
      )
      when type in ["Follow", "Like"] do
168
    mastodon_type = mastodon_type || notification.type
169

170 171 172 173
    case mastodon_type do
      "follow" -> "@#{actor.nickname} has followed you"
      "follow_request" -> "@#{actor.nickname} has requested to follow you"
      "favourite" -> "@#{actor.nickname} has favorited your post"
Maksim's avatar
Maksim committed
174 175 176
    end
  end

177 178 179
  def format_title(activity, mastodon_type \\ nil)

  def format_title(%{activity: %{data: %{"directMessage" => true}}}, _mastodon_type) do
180 181 182
    "New Direct Message"
  end

183 184
  def format_title(%{type: type}, mastodon_type) do
    case mastodon_type || type do
185 186 187 188 189
      "mention" -> "New Mention"
      "follow" -> "New Follower"
      "follow_request" -> "New Follow Request"
      "reblog" -> "New Repeat"
      "favourite" -> "New Favorite"
190
      "pleroma:chat_mention" -> "New Chat Message"
191
      "pleroma:emoji_reaction" -> "New Reaction"
192
      type -> "New #{String.capitalize(type || "event")}"
Maksim's avatar
Maksim committed
193 194 195
    end
  end
end