diff options
Diffstat (limited to 'lib')
21 files changed, 457 insertions, 242 deletions
| diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index 057553e24..f701aaaa5 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -34,13 +34,16 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do    defp csp_string do      scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme] -    websocket_url = String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws") +    static_url = Pleroma.Web.Endpoint.static_url() +    websocket_url = String.replace(static_url, "http", "ws") + +    connect_src = "connect-src 'self' #{static_url} #{websocket_url}"      connect_src =        if Mix.env() == :dev do -        "connect-src 'self' http://localhost:3035/ " <> websocket_url +        connect_src <> " http://localhost:3035/"        else -        "connect-src 'self' " <> websocket_url +        connect_src        end      script_src = diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 91a5db8c5..1a97e9fde 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -85,6 +85,10 @@ defmodule Pleroma.Upload do      end    end +  def char_unescaped?(char) do +    URI.char_unreserved?(char) or char == ?/ +  end +    defp get_opts(opts) do      {size_limit, activity_type} =        case Keyword.get(opts, :type) do @@ -218,9 +222,7 @@ defmodule Pleroma.Upload do    defp url_from_spec(base_url, {:file, path}) do      path =        path -      |> URI.encode() -      |> String.replace("?", "%3F") -      |> String.replace(":", "%3A") +      |> URI.encode(&char_unescaped?/1)      [base_url, "media", path]      |> Path.join() diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 50e7e7ccd..f49ede149 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -532,6 +532,10 @@ defmodule Pleroma.User do        _e ->          with [_nick, _domain] <- String.split(nickname, "@"),               {:ok, user} <- fetch_by_nickname(nickname) do +          if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do +            {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user]) +          end +            user          else            _e -> nil @@ -539,6 +543,17 @@ defmodule Pleroma.User do      end    end +  @doc "Fetch some posts when the user has just been federated with" +  def fetch_initial_posts(user) do +    pages = Pleroma.Config.get!([:fetch_initial_posts, :pages]) + +    Enum.each( +      # Insert all the posts in reverse order, so they're in the right order on the timeline +      Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)), +      &Pleroma.Web.Federator.incoming_ap_doc/1 +    ) +  end +    def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do      from(        u in User, @@ -749,13 +764,41 @@ defmodule Pleroma.User do      Repo.all(query)    end -  @spec search_for_admin(binary(), %{ +  @spec search_for_admin(%{ +          local: boolean(), +          page: number(), +          page_size: number() +        }) :: {:ok, [Pleroma.User.t()], number()} +  def search_for_admin(%{query: nil, local: local, page: page, page_size: page_size}) do +    query = +      from(u in User, order_by: u.id) +      |> maybe_local_user_query(local) + +    paginated_query = +      query +      |> paginate(page, page_size) + +    count = +      query +      |> Repo.aggregate(:count, :id) + +    {:ok, Repo.all(paginated_query), count} +  end + +  @spec search_for_admin(%{ +          query: binary(),            admin: Pleroma.User.t(),            local: boolean(),            page: number(),            page_size: number()          }) :: {:ok, [Pleroma.User.t()], number()} -  def search_for_admin(term, %{admin: admin, local: local, page: page, page_size: page_size}) do +  def search_for_admin(%{ +        query: term, +        admin: admin, +        local: local, +        page: page, +        page_size: page_size +      }) do      term = String.trim_leading(term, "@")      local_paginated_query = @@ -774,21 +817,6 @@ defmodule Pleroma.User do      {:ok, do_search(search_query, admin), count}    end -  @spec all_for_admin(number(), number()) :: {:ok, [Pleroma.User.t()], number()} -  def all_for_admin(page, page_size) do -    query = from(u in User, order_by: u.id) - -    paginated_query = -      query -      |> paginate(page, page_size) - -    count = -      query -      |> Repo.aggregate(:count, :id) - -    {:ok, Repo.all(paginated_query), count} -  end -    def search(query, resolve \\ false, for_user \\ nil) do      # Strip the beginning @ off if there is a query      query = String.trim_leading(query, "@") @@ -1108,24 +1136,36 @@ defmodule Pleroma.User do    def html_filter_policy(_), do: @default_scrubbers +  def fetch_by_ap_id(ap_id) do +    ap_try = ActivityPub.make_user_from_ap_id(ap_id) + +    case ap_try do +      {:ok, user} -> +        user + +      _ -> +        case OStatus.make_user(ap_id) do +          {:ok, user} -> user +          _ -> {:error, "Could not fetch by AP id"} +        end +    end +  end +    def get_or_fetch_by_ap_id(ap_id) do      user = get_by_ap_id(ap_id)      if !is_nil(user) and !User.needs_update?(user) do        user      else -      ap_try = ActivityPub.make_user_from_ap_id(ap_id) - -      case ap_try do -        {:ok, user} -> -          user +      user = fetch_by_ap_id(ap_id) -        _ -> -          case OStatus.make_user(ap_id) do -            {:ok, user} -> user -            _ -> {:error, "Could not fetch by AP id"} -          end +      if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do +        with %User{} = user do +          {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user]) +        end        end + +      user      end    end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 783491b67..adb42b9ab 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -309,12 +309,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do      user = User.get_cached_by_ap_id(actor) +    to = object.data["to"] || [] ++ object.data["cc"] || []      data = %{        "type" => "Delete",        "actor" => actor,        "object" => id, -      "to" => [user.follower_address, "https://www.w3.org/ns/activitystreams#Public"] +      "to" => to      }      with {:ok, _} <- Object.delete(object), diff --git a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex index 7c6ad582a..34665a3a6 100644 --- a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex @@ -23,15 +23,21 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do    defp score_displayname(_), do: 0.0    defp determine_if_followbot(%User{nickname: nickname, name: displayname}) do +    # nickname will always be a binary string because it's generated by Pleroma.      nick_score =        nickname        |> String.downcase()        |> score_nickname() +    # displayname will either be a binary string or nil, if a displayname isn't set.      name_score = -      displayname -      |> String.downcase() -      |> score_displayname() +      if is_binary(displayname) do +        displayname +        |> String.downcase() +        |> score_displayname() +      else +        0.0 +      end      nick_score + name_score    end diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 88f4779c8..629c39315 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do    alias Pleroma.Web    alias Pleroma.Object    alias Pleroma.Activity +  alias Pleroma.Web.ActivityPub.Visibility    alias Pleroma.User    alias Pleroma.Notification    alias Pleroma.Web.Router.Helpers @@ -274,13 +275,31 @@ defmodule Pleroma.Web.ActivityPub.Utils do      Repo.all(query)    end -  def make_like_data(%User{ap_id: ap_id} = actor, %{data: %{"id" => id}} = object, activity_id) do +  def make_like_data( +        %User{ap_id: ap_id} = actor, +        %{data: %{"actor" => object_actor_id, "id" => id}} = object, +        activity_id +      ) do +    object_actor = User.get_cached_by_ap_id(object_actor_id) + +    to = +      if Visibility.is_public?(object) do +        [actor.follower_address, object.data["actor"]] +      else +        [object.data["actor"]] +      end + +    cc = +      (object.data["to"] ++ (object.data["cc"] || [])) +      |> List.delete(actor.ap_id) +      |> List.delete(object_actor.follower_address) +      data = %{        "type" => "Like",        "actor" => ap_id,        "object" => id, -      "to" => [actor.follower_address, object.data["actor"]], -      "cc" => ["https://www.w3.org/ns/activitystreams#Public"], +      "to" => to, +      "cc" => cc,        "context" => object.data["context"]      } @@ -614,4 +633,43 @@ defmodule Pleroma.Web.ActivityPub.Utils do      }      |> Map.merge(additional)    end + +  @doc """ +  Fetches the OrderedCollection/OrderedCollectionPage from `from`, limiting the amount of pages fetched after +  the first one to `pages_left` pages. +  If the amount of pages is higher than the collection has, it returns whatever was there. +  """ +  def fetch_ordered_collection(from, pages_left, acc \\ []) do +    with {:ok, response} <- Tesla.get(from), +         {:ok, collection} <- Poison.decode(response.body) do +      case collection["type"] do +        "OrderedCollection" -> +          # If we've encountered the OrderedCollection and not the page, +          # just call the same function on the page address +          fetch_ordered_collection(collection["first"], pages_left) + +        "OrderedCollectionPage" -> +          if pages_left > 0 do +            # There are still more pages +            if Map.has_key?(collection, "next") do +              # There are still more pages, go deeper saving what we have into the accumulator +              fetch_ordered_collection( +                collection["next"], +                pages_left - 1, +                acc ++ collection["orderedItems"] +              ) +            else +              # No more pages left, just return whatever we already have +              acc ++ collection["orderedItems"] +            end +          else +            # Got the amount of pages needed, add them all to the accumulator +            acc ++ collection["orderedItems"] +          end + +        _ -> +          {:error, "Not an OrderedCollection or OrderedCollectionPage"} +      end +    end +  end  end diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index aae02cab8..75c2c6061 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -63,28 +63,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do           do: json_response(conn, :no_content, "")    end -  def list_users(conn, params) do -    {page, page_size} = page_params(params) - -    with {:ok, users, count} <- User.all_for_admin(page, page_size), -         do: -           conn -           |> json( -             AccountView.render("index.json", -               users: users, -               count: count, -               page_size: page_size -             ) -           ) -  end - -  def search_users(%{assigns: %{user: admin}} = conn, %{"query" => query} = params) do +  def list_users(%{assigns: %{user: admin}} = conn, params) do      {page, page_size} = page_params(params)      with {:ok, users, count} <- -           User.search_for_admin(query, %{ +           User.search_for_admin(%{ +             query: params["query"],               admin: admin, -             local: params["local"] == "true", +             local: params["local_only"] == "true",               page: page,               page_size: page_size             }), diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 7114d6de6..07bd6548a 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -14,10 +14,23 @@ defmodule Pleroma.Web.CommonAPI do    import Pleroma.Web.CommonAPI.Utils +  def follow(follower, followed) do +    with {:ok, follower} <- User.maybe_direct_follow(follower, followed), +         {:ok, activity} <- ActivityPub.follow(follower, followed), +         {:ok, follower, followed} <- +           User.wait_and_refresh( +             Pleroma.Config.get([:activitypub, :follow_handshake_timeout]), +             follower, +             followed +           ) do +      {:ok, follower, followed, activity} +    end +  end +    def delete(activity_id, user) do      with %Activity{data: %{"object" => %{"id" => object_id}}} <- Repo.get(Activity, activity_id),           %Object{} = object <- Object.normalize(object_id), -         true <- user.info.is_moderator || user.ap_id == object.data["actor"], +         true <- User.superuser?(user) || user.ap_id == object.data["actor"],           {:ok, _} <- unpin(activity_id, user),           {:ok, delete} <- ActivityPub.delete(object) do        {:ok, delete} diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 056be49b0..8c58f4545 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -15,14 +15,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    alias Pleroma.Web    alias Pleroma.Web.CommonAPI    alias Pleroma.Web.MediaProxy -  alias Pleroma.Web.Push -  alias Push.Subscription    alias Pleroma.Web.MastodonAPI.AccountView    alias Pleroma.Web.MastodonAPI.FilterView    alias Pleroma.Web.MastodonAPI.ListView    alias Pleroma.Web.MastodonAPI.MastodonView -  alias Pleroma.Web.MastodonAPI.PushSubscriptionView    alias Pleroma.Web.MastodonAPI.StatusView    alias Pleroma.Web.MastodonAPI.ReportView    alias Pleroma.Web.ActivityPub.ActivityPub @@ -193,6 +190,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do +    params = +      conn.params +      |> Map.drop(["since_id", "max_id"]) +      |> Map.merge(params) +      last = List.last(activities)      first = List.first(activities) @@ -292,13 +294,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def dm_timeline(%{assigns: %{user: user}} = conn, params) do -    query = -      ActivityPub.fetch_activities_query( -        [user.ap_id], -        Map.merge(params, %{"type" => "Create", visibility: "direct"}) -      ) +    params = +      params +      |> Map.put("type", "Create") +      |> Map.put("blocking_user", user) +      |> Map.put("user", user) +      |> Map.put(:visibility, "direct") -    activities = Repo.all(query) +    activities = +      [user.ap_id] +      |> ActivityPub.fetch_activities_query(params) +      |> Repo.all()      conn      |> add_link_headers(:dm_timeline, activities) @@ -733,14 +739,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do      with %User{} = followed <- Repo.get(User, id), -         {:ok, follower} <- User.maybe_direct_follow(follower, followed), -         {:ok, _activity} <- ActivityPub.follow(follower, followed), -         {:ok, follower, followed} <- -           User.wait_and_refresh( -             Config.get([:activitypub, :follow_handshake_timeout]), -             follower, -             followed -           ) do +         {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do        conn        |> put_view(AccountView)        |> render("relationship.json", %{user: follower, target: followed}) @@ -754,8 +753,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do      with %User{} = followed <- Repo.get_by(User, nickname: uri), -         {:ok, follower} <- User.maybe_direct_follow(follower, followed), -         {:ok, _activity} <- ActivityPub.follow(follower, followed) do +         {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do        conn        |> put_view(AccountView)        |> render("account.json", %{user: followed, for: follower}) @@ -1424,37 +1422,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      json(conn, %{})    end -  def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do -    true = Push.enabled() -    Subscription.delete_if_exists(user, token) -    {:ok, subscription} = Subscription.create(user, token, params) -    view = PushSubscriptionView.render("push_subscription.json", subscription: subscription) -    json(conn, view) -  end - -  def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do -    true = Push.enabled() -    subscription = Subscription.get(user, token) -    view = PushSubscriptionView.render("push_subscription.json", subscription: subscription) -    json(conn, view) -  end - -  def update_push_subscription( -        %{assigns: %{user: user, token: token}} = conn, -        params -      ) do -    true = Push.enabled() -    {:ok, subscription} = Subscription.update(user, token, params) -    view = PushSubscriptionView.render("push_subscription.json", subscription: subscription) -    json(conn, view) -  end - -  def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do -    true = Push.enabled() -    {:ok, _response} = Subscription.delete(user, token) -    json(conn, %{}) -  end - +  # fallback action +  #    def errors(conn, _) do      conn      |> put_status(500) diff --git a/lib/pleroma/web/mastodon_api/subscription_controller.ex b/lib/pleroma/web/mastodon_api/subscription_controller.ex new file mode 100644 index 000000000..b6c8ff808 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/subscription_controller.ex @@ -0,0 +1,71 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.SubscriptionController do +  @moduledoc "The module represents functions to manage user subscriptions." +  use Pleroma.Web, :controller + +  alias Pleroma.Web.Push +  alias Pleroma.Web.Push.Subscription +  alias Pleroma.Web.MastodonAPI.PushSubscriptionView, as: View + +  action_fallback(:errors) + +  # Creates PushSubscription +  # POST /api/v1/push/subscription +  # +  def create(%{assigns: %{user: user, token: token}} = conn, params) do +    with true <- Push.enabled(), +         {:ok, _} <- Subscription.delete_if_exists(user, token), +         {:ok, subscription} <- Subscription.create(user, token, params) do +      view = View.render("push_subscription.json", subscription: subscription) +      json(conn, view) +    end +  end + +  # Gets PushSubscription +  # GET /api/v1/push/subscription +  # +  def get(%{assigns: %{user: user, token: token}} = conn, _params) do +    with true <- Push.enabled(), +         {:ok, subscription} <- Subscription.get(user, token) do +      view = View.render("push_subscription.json", subscription: subscription) +      json(conn, view) +    end +  end + +  # Updates PushSubscription +  # PUT /api/v1/push/subscription +  # +  def update(%{assigns: %{user: user, token: token}} = conn, params) do +    with true <- Push.enabled(), +         {:ok, subscription} <- Subscription.update(user, token, params) do +      view = View.render("push_subscription.json", subscription: subscription) +      json(conn, view) +    end +  end + +  # Deletes PushSubscription +  # DELETE /api/v1/push/subscription +  # +  def delete(%{assigns: %{user: user, token: token}} = conn, _params) do +    with true <- Push.enabled(), +         {:ok, _response} <- Subscription.delete(user, token), +         do: json(conn, %{}) +  end + +  # fallback action +  # +  def errors(conn, {:error, :not_found}) do +    conn +    |> put_status(404) +    |> json("Not found") +  end + +  def errors(conn, _) do +    conn +    |> put_status(500) +    |> json("Something went wrong") +  end +end diff --git a/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex b/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex index e86b789c5..021489711 100644 --- a/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex +++ b/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex @@ -4,6 +4,7 @@  defmodule Pleroma.Web.MastodonAPI.PushSubscriptionView do    use Pleroma.Web, :view +  alias Pleroma.Web.Push    def render("push_subscription.json", %{subscription: subscription}) do      %{ @@ -14,7 +15,5 @@ defmodule Pleroma.Web.MastodonAPI.PushSubscriptionView do      }    end -  defp server_key do -    Keyword.get(Application.get_env(:web_push_encryption, :vapid_details), :public_key) -  end +  defp server_key, do: Keyword.get(Push.vapid_config(), :public_key)  end diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex index a166800d4..5fc9c9e7b 100644 --- a/lib/pleroma/web/metadata/utils.ex +++ b/lib/pleroma/web/metadata/utils.ex @@ -17,14 +17,14 @@ defmodule Pleroma.Web.Metadata.Utils do      |> Formatter.truncate()    end -  def scrub_html_and_truncate(content) when is_binary(content) do +  def scrub_html_and_truncate(content, max_length \\ 200) when is_binary(content) do      content      # html content comes from DB already encoded, decode first and scrub after      |> HtmlEntities.decode()      |> String.replace(~r/<br\s?\/?>/, " ")      |> HTML.strip_tags()      |> Formatter.demojify() -    |> Formatter.truncate() +    |> Formatter.truncate(max_length)    end    def attachment_url(url) do diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index f4867d05b..8c775ce24 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -6,7 +6,6 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do    use Pleroma.Web, :controller    alias Pleroma.Config -  alias Pleroma.Repo    alias Pleroma.Stats    alias Pleroma.User    alias Pleroma.Web @@ -86,8 +85,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do        end      staff_accounts = -      User.moderator_user_query() -      |> Repo.all() +      User.all_superusers()        |> Enum.map(fn u -> u.ap_id end)      mrf_user_allowlist = diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex new file mode 100644 index 000000000..33f912d34 --- /dev/null +++ b/lib/pleroma/web/push/impl.ex @@ -0,0 +1,127 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Push.Impl do +  @moduledoc "The module represents implementation push web notification" + +  alias Pleroma.Repo +  alias Pleroma.User +  alias Pleroma.Activity +  alias Pleroma.Object +  alias Pleroma.Web.Push.Subscription +  alias Pleroma.Web.Metadata.Utils +  alias Pleroma.Notification + +  require Logger +  import Ecto.Query + +  @types ["Create", "Follow", "Announce", "Like"] + +  @doc "Performs sending notifications for user subscriptions" +  @spec perform_send(Notification.t()) :: list(any) +  def perform_send(%{activity: %{data: %{"type" => activity_type}}, user_id: user_id} = notif) +      when activity_type in @types do +    actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) + +    type = Activity.mastodon_notification_type(notif.activity) +    gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key) +    avatar_url = User.avatar_url(actor) + +    for subscription <- fetch_subsriptions(user_id), +        get_in(subscription.data, ["alerts", type]) do +      %{ +        title: format_title(notif), +        access_token: subscription.token.token, +        body: format_body(notif, actor), +        notification_id: notif.id, +        notification_type: type, +        icon: avatar_url, +        preferred_locale: "en" +      } +      |> Jason.encode!() +      |> push_message(build_sub(subscription), gcm_api_key, subscription) +    end +  end + +  def perform_send(_) do +    Logger.warn("Unknown notification type") +    :error +  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 +      {:ok, %{status_code: code}} when 400 <= code and code < 500 -> +        Logger.debug("Removing subscription record") +        Repo.delete!(subscription) +        :ok + +      {:ok, %{status_code: code}} when 200 <= code and code < 300 -> +        :ok + +      {:ok, %{status_code: code}} -> +        Logger.error("Web Push Notification failed with code: #{code}") +        :error + +      _ -> +        Logger.error("Web Push Notification failed with unknown error") +        :error +    end +  end + +  @doc "Gets user subscriptions" +  def fetch_subsriptions(user_id) do +    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 + +  def format_body( +        %{activity: %{data: %{"type" => "Create", "object" => %{"content" => content}}}}, +        actor +      ) do +    "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}" +  end + +  def format_body( +        %{activity: %{data: %{"type" => "Announce", "object" => activity_id}}}, +        actor +      ) do +    %Activity{data: %{"object" => %{"id" => object_id}}} = Activity.get_by_ap_id(activity_id) +    %Object{data: %{"content" => content}} = Object.get_by_ap_id(object_id) + +    "@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}" +  end + +  def format_body( +        %{activity: %{data: %{"type" => type}}}, +        actor +      ) +      when type in ["Follow", "Like"] do +    case type do +      "Follow" -> "@#{actor.nickname} has followed you" +      "Like" -> "@#{actor.nickname} has favorited your post" +    end +  end + +  def format_title(%{activity: %{data: %{"type" => type}}}) do +    case type do +      "Create" -> "New Mention" +      "Follow" -> "New Follower" +      "Announce" -> "New Repeat" +      "Like" -> "New Favorite" +    end +  end +end diff --git a/lib/pleroma/web/push/push.ex b/lib/pleroma/web/push/push.ex index ddd4fe037..951dab535 100644 --- a/lib/pleroma/web/push/push.ex +++ b/lib/pleroma/web/push/push.ex @@ -5,14 +5,13 @@  defmodule Pleroma.Web.Push do    use GenServer -  alias Pleroma.Repo -  alias Pleroma.User -  alias Pleroma.Web.Push.Subscription +  alias Pleroma.Web.Push.Impl    require Logger -  import Ecto.Query -  @types ["Create", "Follow", "Announce", "Like"] +  ############## +  # Client API # +  ##############    def start_link() do      GenServer.start_link(__MODULE__, :ok, name: __MODULE__) @@ -30,14 +29,18 @@ defmodule Pleroma.Web.Push do      end    end -  def send(notification) do -    if enabled() do -      GenServer.cast(Pleroma.Web.Push, {:send, notification}) -    end -  end +  def send(notification), +    do: GenServer.cast(__MODULE__, {:send, notification}) + +  #################### +  # Server Callbacks # +  #################### +  @impl true    def init(:ok) do -    if !enabled() do +    if enabled() do +      {:ok, nil} +    else        Logger.warn("""        VAPID key pair is not found. If you wish to enabled web push, please run @@ -47,93 +50,15 @@ defmodule Pleroma.Web.Push do        """)        :ignore -    else -      {:ok, nil}      end    end -  def handle_cast( -        {:send, %{activity: %{data: %{"type" => type}}, user_id: user_id} = notification}, -        state -      ) -      when type in @types do -    actor = User.get_cached_by_ap_id(notification.activity.data["actor"]) - -    type = Pleroma.Activity.mastodon_notification_type(notification.activity) - -    Subscription -    |> where(user_id: ^user_id) -    |> preload(:token) -    |> Repo.all() -    |> Enum.filter(fn subscription -> -      get_in(subscription.data, ["alerts", type]) || false -    end) -    |> Enum.each(fn subscription -> -      sub = %{ -        keys: %{ -          p256dh: subscription.key_p256dh, -          auth: subscription.key_auth -        }, -        endpoint: subscription.endpoint -      } - -      body = -        Jason.encode!(%{ -          title: format_title(notification), -          access_token: subscription.token.token, -          body: format_body(notification, actor), -          notification_id: notification.id, -          notification_type: type, -          icon: User.avatar_url(actor), -          preferred_locale: "en" -        }) - -      case WebPushEncryption.send_web_push( -             body, -             sub, -             Application.get_env(:web_push_encryption, :gcm_api_key) -           ) do -        {:ok, %{status_code: code}} when 400 <= code and code < 500 -> -          Logger.debug("Removing subscription record") -          Repo.delete!(subscription) -          :ok - -        {:ok, %{status_code: code}} when 200 <= code and code < 300 -> -          :ok - -        {:ok, %{status_code: code}} -> -          Logger.error("Web Push Notification failed with code: #{code}") -          :error - -        _ -> -          Logger.error("Web Push Notification failed with unknown error") -          :error -      end -    end) - -    {:noreply, state} -  end - -  def handle_cast({:send, _}, state) do -    Logger.warn("Unknown notification type") -    {:noreply, state} -  end - -  defp format_title(%{activity: %{data: %{"type" => type}}}) do -    case type do -      "Create" -> "New Mention" -      "Follow" -> "New Follower" -      "Announce" -> "New Repeat" -      "Like" -> "New Favorite" +  @impl true +  def handle_cast({:send, notification}, state) do +    if enabled() do +      Impl.perform_send(notification)      end -  end -  defp format_body(%{activity: %{data: %{"type" => type}}}, actor) do -    case type do -      "Create" -> "@#{actor.nickname} has mentioned you" -      "Follow" -> "@#{actor.nickname} has followed you" -      "Announce" -> "@#{actor.nickname} has repeated your post" -      "Like" -> "@#{actor.nickname} has favorited your post" -    end +    {:noreply, state}    end  end diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex index 242e30910..c90bd2bda 100644 --- a/lib/pleroma/web/push/subscription.ex +++ b/lib/pleroma/web/push/subscription.ex @@ -12,6 +12,8 @@ defmodule Pleroma.Web.Push.Subscription do    alias Pleroma.Web.OAuth.Token    alias Pleroma.Web.Push.Subscription +  @type t :: %__MODULE__{} +    schema "push_subscriptions" do      belongs_to(:user, User, type: Pleroma.FlakeId)      belongs_to(:token, Token) @@ -50,24 +52,32 @@ defmodule Pleroma.Web.Push.Subscription do      })    end +  @doc "Gets subsciption by user & token" +  @spec get(User.t(), Token.t()) :: {:ok, t()} | {:error, :not_found}    def get(%User{id: user_id}, %Token{id: token_id}) do -    Repo.get_by(Subscription, user_id: user_id, token_id: token_id) +    case Repo.get_by(Subscription, user_id: user_id, token_id: token_id) do +      nil -> {:error, :not_found} +      subscription -> {:ok, subscription} +    end    end    def update(user, token, params) do -    get(user, token) -    |> change(data: alerts(params)) -    |> Repo.update() +    with {:ok, subscription} <- get(user, token) do +      subscription +      |> change(data: alerts(params)) +      |> Repo.update() +    end    end    def delete(user, token) do -    Repo.delete(get(user, token)) +    with {:ok, subscription} <- get(user, token), +         do: Repo.delete(subscription)    end    def delete_if_exists(user, token) do      case get(user, token) do -      nil -> {:ok, nil} -      sub -> Repo.delete(sub) +      {:error, _} -> {:ok, nil} +      {:ok, sub} -> Repo.delete(sub)      end    end diff --git a/lib/pleroma/web/rel_me.ex b/lib/pleroma/web/rel_me.ex index a07db966f..ab29a36e3 100644 --- a/lib/pleroma/web/rel_me.ex +++ b/lib/pleroma/web/rel_me.ex @@ -28,7 +28,8 @@ defmodule Pleroma.Web.RelMe do      {:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: @hackney_options)      data = -      Floki.attribute(html, "link[rel=me]", "href") ++ Floki.attribute(html, "a[rel=me]", "href") +      Floki.attribute(html, "link[rel~=me]", "href") ++ +        Floki.attribute(html, "a[rel~=me]", "href")      {:ok, data}    rescue diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex index abb1cf7f2..8317a1162 100644 --- a/lib/pleroma/web/rich_media/helpers.ex +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -8,10 +8,24 @@ defmodule Pleroma.Web.RichMedia.Helpers do    alias Pleroma.HTML    alias Pleroma.Web.RichMedia.Parser +  defp validate_page_url(page_url) when is_binary(page_url) do +    if AutoLinker.Parser.is_url?(page_url, true) do +      URI.parse(page_url) |> validate_page_url +    else +      :error +    end +  end + +  defp validate_page_url(%URI{authority: nil}), do: :error +  defp validate_page_url(%URI{scheme: nil}), do: :error +  defp validate_page_url(%URI{}), do: :ok +  defp validate_page_url(_), do: :error +    def fetch_data_for_activity(%Activity{} = activity) do      with true <- Pleroma.Config.get([:rich_media, :enabled]),           %Object{} = object <- Object.normalize(activity.data["object"]),           {:ok, page_url} <- HTML.extract_first_external_url(object, object.data["content"]), +         :ok <- validate_page_url(page_url),           {:ok, rich_media} <- Parser.parse(page_url) do        %{page_url: page_url, rich_media: rich_media}      else diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 6fcb46878..65a90e31e 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -140,7 +140,6 @@ defmodule Pleroma.Web.Router do      pipe_through([:admin_api, :oauth_write])      get("/users", AdminAPIController, :list_users) -    get("/users/search", AdminAPIController, :search_users)      delete("/user", AdminAPIController, :user_delete)      patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)      post("/user", AdminAPIController, :user_create) @@ -304,10 +303,10 @@ defmodule Pleroma.Web.Router do      scope [] do        pipe_through(:oauth_push) -      post("/push/subscription", MastodonAPIController, :create_push_subscription) -      get("/push/subscription", MastodonAPIController, :get_push_subscription) -      put("/push/subscription", MastodonAPIController, :update_push_subscription) -      delete("/push/subscription", MastodonAPIController, :delete_push_subscription) +      post("/push/subscription", SubscriptionController, :create) +      get("/push/subscription", SubscriptionController, :get) +      put("/push/subscription", SubscriptionController, :update) +      delete("/push/subscription", SubscriptionController, :delete)      end    end diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index ab6470d78..dcb15b9a9 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -28,18 +28,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do    end    def follow(%User{} = follower, params) do -    with {:ok, %User{} = followed} <- get_user(params), -         {:ok, follower} <- User.maybe_direct_follow(follower, followed), -         {:ok, activity} <- ActivityPub.follow(follower, followed), -         {:ok, follower, followed} <- -           User.wait_and_refresh( -             Pleroma.Config.get([:activitypub, :follow_handshake_timeout]), -             follower, -             followed -           ) do -      {:ok, follower, followed, activity} -    else -      err -> err +    with {:ok, %User{} = followed} <- get_user(params) do +      CommonAPI.follow(follower, followed)      end    end diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index de7b9f24c..5e4ebb8e8 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -177,13 +177,16 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    end    def dm_timeline(%{assigns: %{user: user}} = conn, params) do -    query = -      ActivityPub.fetch_activities_query( -        [user.ap_id], -        Map.merge(params, %{"type" => "Create", "user" => user, visibility: "direct"}) -      ) +    params = +      params +      |> Map.put("type", "Create") +      |> Map.put("blocking_user", user) +      |> Map.put("user", user) +      |> Map.put(:visibility, "direct") -    activities = Repo.all(query) +    activities = +      ActivityPub.fetch_activities_query([user.ap_id], params) +      |> Repo.all()      conn      |> put_view(ActivityView) | 
