diff options
Diffstat (limited to 'lib')
54 files changed, 1117 insertions, 279 deletions
| diff --git a/lib/mix/tasks/benchmark.ex b/lib/mix/tasks/benchmark.ex new file mode 100644 index 000000000..0fbb4dbb1 --- /dev/null +++ b/lib/mix/tasks/benchmark.ex @@ -0,0 +1,25 @@ +defmodule Mix.Tasks.Pleroma.Benchmark do +  use Mix.Task +  alias Mix.Tasks.Pleroma.Common + +  def run(["search"]) do +    Common.start_pleroma() + +    Benchee.run(%{ +      "search" => fn -> +        Pleroma.Web.MastodonAPI.MastodonAPIController.status_search(nil, "cofe") +      end +    }) +  end + +  def run(["tag"]) do +    Common.start_pleroma() + +    Benchee.run(%{ +      "tag" => fn -> +        %{"type" => "Create", "tag" => "cofe"} +        |> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities() +      end +    }) +  end +end diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index b396ff0de..6a83a8c0d 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -126,7 +126,7 @@ defmodule Mix.Tasks.Pleroma.User do      proceed? = assume_yes? or Mix.shell().yes?("Continue?") -    unless not proceed? do +    if proceed? do        Common.start_pleroma()        params = %{ @@ -163,7 +163,7 @@ defmodule Mix.Tasks.Pleroma.User do      Common.start_pleroma()      with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do -      User.delete(user) +      User.perform(:delete, user)        Mix.shell().info("User #{nickname} deleted.")      else        _ -> @@ -380,7 +380,7 @@ defmodule Mix.Tasks.Pleroma.User do      Common.start_pleroma()      with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do -      User.delete_user_activities(user) +      {:ok, _} = User.delete_user_activities(user)        Mix.shell().info("User #{nickname} statuses deleted.")      else        _ -> diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 4a2ded518..2b661edc1 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -6,14 +6,18 @@ defmodule Pleroma.Activity do    use Ecto.Schema    alias Pleroma.Activity +  alias Pleroma.Bookmark    alias Pleroma.Notification    alias Pleroma.Object    alias Pleroma.Repo +  alias Pleroma.User    import Ecto.Changeset    import Ecto.Query    @type t :: %__MODULE__{} +  @type actor :: String.t() +    @primary_key {:id, Pleroma.FlakeId, autogenerate: true}    # https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19 @@ -33,6 +37,8 @@ defmodule Pleroma.Activity do      field(:local, :boolean, default: true)      field(:actor, :string)      field(:recipients, {:array, :string}, default: []) +    # This is a fake relation, do not use outside of with_preloaded_bookmark/get_bookmark +    has_one(:bookmark, Bookmark)      has_many(:notifications, Notification, on_delete: :delete_all)      # Attention: this is a fake relation, don't try to preload it blindly and expect it to work! @@ -71,6 +77,16 @@ defmodule Pleroma.Activity do      |> preload([activity, object], object: object)    end +  def with_preloaded_bookmark(query, %User{} = user) do +    from([a] in query, +      left_join: b in Bookmark, +      on: b.user_id == ^user.id and b.activity_id == a.id, +      preload: [bookmark: b] +    ) +  end + +  def with_preloaded_bookmark(query, _), do: query +    def get_by_ap_id(ap_id) do      Repo.one(        from( @@ -80,6 +96,16 @@ defmodule Pleroma.Activity do      )    end +  def get_bookmark(%Activity{} = activity, %User{} = user) do +    if Ecto.assoc_loaded?(activity.bookmark) do +      activity.bookmark +    else +      Bookmark.get(user.id, activity.id) +    end +  end + +  def get_bookmark(_, _), do: nil +    def change(struct, params \\ %{}) do      struct      |> cast(params, [:data]) @@ -260,4 +286,9 @@ defmodule Pleroma.Activity do      |> where([s], s.actor == ^actor)      |> Repo.all()    end + +  @spec query_by_actor(actor()) :: Ecto.Query.t() +  def query_by_actor(actor) do +    from(a in Activity, where: a.actor == ^actor) +  end  end diff --git a/lib/pleroma/bbs/authenticator.ex b/lib/pleroma/bbs/authenticator.ex new file mode 100644 index 000000000..a2c153720 --- /dev/null +++ b/lib/pleroma/bbs/authenticator.ex @@ -0,0 +1,16 @@ +defmodule Pleroma.BBS.Authenticator do +  use Sshd.PasswordAuthenticator +  alias Comeonin.Pbkdf2 +  alias Pleroma.User + +  def authenticate(username, password) do +    username = to_string(username) +    password = to_string(password) + +    with %User{} = user <- User.get_by_nickname(username) do +      Pbkdf2.checkpw(password, user.password_hash) +    else +      _e -> false +    end +  end +end diff --git a/lib/pleroma/bbs/handler.ex b/lib/pleroma/bbs/handler.ex new file mode 100644 index 000000000..106fe5d18 --- /dev/null +++ b/lib/pleroma/bbs/handler.ex @@ -0,0 +1,147 @@ +defmodule Pleroma.BBS.Handler do +  use Sshd.ShellHandler +  alias Pleroma.Activity +  alias Pleroma.Web.ActivityPub.ActivityPub +  alias Pleroma.Web.CommonAPI + +  def on_shell(username, _pubkey, _ip, _port) do +    :ok = IO.puts("Welcome to #{Pleroma.Config.get([:instance, :name])}!") +    user = Pleroma.User.get_cached_by_nickname(to_string(username)) +    Logger.debug("#{inspect(user)}") +    loop(run_state(user: user)) +  end + +  def on_connect(username, ip, port, method) do +    Logger.debug(fn -> +      """ +      Incoming SSH shell #{inspect(self())} requested for #{username} from #{inspect(ip)}:#{ +        inspect(port) +      } using #{inspect(method)} +      """ +    end) +  end + +  def on_disconnect(username, ip, port) do +    Logger.debug(fn -> +      "Disconnecting SSH shell for #{username} from #{inspect(ip)}:#{inspect(port)}" +    end) +  end + +  defp loop(state) do +    self_pid = self() +    counter = state.counter +    prefix = state.prefix +    user = state.user + +    input = spawn(fn -> io_get(self_pid, prefix, counter, user.nickname) end) +    wait_input(state, input) +  end + +  def puts_activity(activity) do +    status = Pleroma.Web.MastodonAPI.StatusView.render("status.json", %{activity: activity}) +    IO.puts("-- #{status.id} by #{status.account.display_name} (#{status.account.acct})") +    IO.puts(HtmlSanitizeEx.strip_tags(status.content)) +    IO.puts("") +  end + +  def handle_command(state, "help") do +    IO.puts("Available commands:") +    IO.puts("help - This help") +    IO.puts("home - Show the home timeline") +    IO.puts("p <text> - Post the given text") +    IO.puts("r <id> <text> - Reply to the post with the given id") +    IO.puts("quit - Quit") + +    state +  end + +  def handle_command(%{user: user} = state, "r " <> text) do +    text = String.trim(text) +    [activity_id, rest] = String.split(text, " ", parts: 2) + +    with %Activity{} <- Activity.get_by_id(activity_id), +         {:ok, _activity} <- +           CommonAPI.post(user, %{"status" => rest, "in_reply_to_status_id" => activity_id}) do +      IO.puts("Replied!") +    else +      _e -> IO.puts("Could not reply...") +    end + +    state +  end + +  def handle_command(%{user: user} = state, "p " <> text) do +    text = String.trim(text) + +    with {:ok, _activity} <- CommonAPI.post(user, %{"status" => text}) do +      IO.puts("Posted!") +    else +      _e -> IO.puts("Could not post...") +    end + +    state +  end + +  def handle_command(state, "home") do +    user = state.user + +    params = +      %{} +      |> Map.put("type", ["Create"]) +      |> Map.put("blocking_user", user) +      |> Map.put("muting_user", user) +      |> Map.put("user", user) + +    activities = +      [user.ap_id | user.following] +      |> ActivityPub.fetch_activities(params) +      |> ActivityPub.contain_timeline(user) + +    Enum.each(activities, fn activity -> +      puts_activity(activity) +    end) + +    state +  end + +  def handle_command(state, command) do +    IO.puts("Unknown command '#{command}'") +    state +  end + +  defp wait_input(state, input) do +    receive do +      {:input, ^input, "quit\n"} -> +        IO.puts("Exiting...") + +      {:input, ^input, code} when is_binary(code) -> +        code = String.trim(code) + +        state = handle_command(state, code) + +        loop(%{state | counter: state.counter + 1}) + +      {:error, :interrupted} -> +        IO.puts("Caught Ctrl+C...") +        loop(%{state | counter: state.counter + 1}) + +      {:input, ^input, msg} -> +        :ok = Logger.warn("received unknown message: #{inspect(msg)}") +        loop(%{state | counter: state.counter + 1}) +    end +  end + +  defp run_state(opts) do +    %{prefix: "pleroma", counter: 1, user: opts[:user]} +  end + +  defp io_get(pid, prefix, counter, username) do +    prompt = prompt(prefix, counter, username) +    send(pid, {:input, self(), IO.gets(:stdio, prompt)}) +  end + +  defp prompt(prefix, counter, username) do +    prompt = "#{username}@#{prefix}:#{counter}>" +    prompt <> " " +  end +end diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex new file mode 100644 index 000000000..6e26c5fd4 --- /dev/null +++ b/lib/pleroma/conversation.ex @@ -0,0 +1,75 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Conversation do +  alias Pleroma.Conversation.Participation +  alias Pleroma.Repo +  alias Pleroma.User +  use Ecto.Schema +  import Ecto.Changeset + +  schema "conversations" do +    # This is the context ap id. +    field(:ap_id, :string) +    has_many(:participations, Participation) +    has_many(:users, through: [:participations, :user]) + +    timestamps() +  end + +  def creation_cng(struct, params) do +    struct +    |> cast(params, [:ap_id]) +    |> validate_required([:ap_id]) +    |> unique_constraint(:ap_id) +  end + +  def create_for_ap_id(ap_id) do +    %__MODULE__{} +    |> creation_cng(%{ap_id: ap_id}) +    |> Repo.insert( +      on_conflict: [set: [updated_at: NaiveDateTime.utc_now()]], +      returning: true, +      conflict_target: :ap_id +    ) +  end + +  def get_for_ap_id(ap_id) do +    Repo.get_by(__MODULE__, ap_id: ap_id) +  end + +  @doc """ +  This will +  1. Create a conversation if there isn't one already +  2. Create a participation for all the people involved who don't have one already +  3. Bump all relevant participations to 'unread' +  """ +  def create_or_bump_for(activity) do +    with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity), +         object <- Pleroma.Object.normalize(activity), +         "Create" <- activity.data["type"], +         "Note" <- object.data["type"], +         ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"] do +      {:ok, conversation} = create_for_ap_id(ap_id) + +      users = User.get_users_from_set(activity.recipients, false) + +      participations = +        Enum.map(users, fn user -> +          {:ok, participation} = +            Participation.create_for_user_and_conversation(user, conversation) + +          participation +        end) + +      {:ok, +       %{ +         conversation +         | participations: participations +       }} +    else +      e -> {:error, e} +    end +  end +end diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex new file mode 100644 index 000000000..61021fb18 --- /dev/null +++ b/lib/pleroma/conversation/participation.ex @@ -0,0 +1,81 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Conversation.Participation do +  use Ecto.Schema +  alias Pleroma.Conversation +  alias Pleroma.Repo +  alias Pleroma.User +  alias Pleroma.Web.ActivityPub.ActivityPub +  import Ecto.Changeset +  import Ecto.Query + +  schema "conversation_participations" do +    belongs_to(:user, User, type: Pleroma.FlakeId) +    belongs_to(:conversation, Conversation) +    field(:read, :boolean, default: false) +    field(:last_activity_id, Pleroma.FlakeId, virtual: true) + +    timestamps() +  end + +  def creation_cng(struct, params) do +    struct +    |> cast(params, [:user_id, :conversation_id]) +    |> validate_required([:user_id, :conversation_id]) +  end + +  def create_for_user_and_conversation(user, conversation) do +    %__MODULE__{} +    |> creation_cng(%{user_id: user.id, conversation_id: conversation.id}) +    |> Repo.insert( +      on_conflict: [set: [read: false, updated_at: NaiveDateTime.utc_now()]], +      returning: true, +      conflict_target: [:user_id, :conversation_id] +    ) +  end + +  def read_cng(struct, params) do +    struct +    |> cast(params, [:read]) +    |> validate_required([:read]) +  end + +  def mark_as_read(participation) do +    participation +    |> read_cng(%{read: true}) +    |> Repo.update() +  end + +  def mark_as_unread(participation) do +    participation +    |> read_cng(%{read: false}) +    |> Repo.update() +  end + +  def for_user(user, params \\ %{}) do +    from(p in __MODULE__, +      where: p.user_id == ^user.id, +      order_by: [desc: p.updated_at] +    ) +    |> Pleroma.Pagination.fetch_paginated(params) +    |> Repo.preload(conversation: [:users]) +  end + +  def for_user_with_last_activity_id(user, params \\ %{}) do +    for_user(user, params) +    |> Enum.map(fn participation -> +      activity_id = +        ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{ +          "user" => user, +          "blocking_user" => user +        }) + +      %{ +        participation +        | last_activity_id: activity_id +      } +    end) +  end +end diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index dab8910c1..3d7c36d21 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -113,9 +113,7 @@ defmodule Pleroma.Formatter do        html =          if not strip do -          "<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{ -            MediaProxy.url(file) -          }' />" +          "<img class='emoji' alt='#{emoji}' title='#{emoji}' src='#{MediaProxy.url(file)}' />"          else            ""          end @@ -130,12 +128,23 @@ defmodule Pleroma.Formatter do    def demojify(text, nil), do: text +  @doc "Outputs a list of the emoji-shortcodes in a text"    def get_emoji(text) when is_binary(text) do      Enum.filter(Emoji.get_all(), fn {emoji, _, _} -> String.contains?(text, ":#{emoji}:") end)    end    def get_emoji(_), do: [] +  @doc "Outputs a list of the emoji-Maps in a text" +  def get_emoji_map(text) when is_binary(text) do +    get_emoji(text) +    |> Enum.reduce(%{}, fn {name, file, _group}, acc -> +      Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}") +    end) +  end + +  def get_emoji_map(_), do: [] +    def html_escape({text, mentions, hashtags}, type) do      {html_escape(text, type), mentions, hashtags}    end diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index cf6c0ee0a..d1da746de 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -28,12 +28,18 @@ defmodule Pleroma.HTML do    def filter_tags(html), do: filter_tags(html, nil)    def strip_tags(html), do: Scrubber.scrub(html, Scrubber.StripTags) -  def get_cached_scrubbed_html_for_activity(content, scrubbers, activity, key \\ "") do +  def get_cached_scrubbed_html_for_activity( +        content, +        scrubbers, +        activity, +        key \\ "", +        callback \\ fn x -> x end +      ) do      key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}"      Cachex.fetch!(:scrubber_cache, key, fn _key ->        object = Pleroma.Object.normalize(activity) -      ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false) +      ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false, callback)      end)    end @@ -42,24 +48,27 @@ defmodule Pleroma.HTML do        content,        HtmlSanitizeEx.Scrubber.StripTags,        activity, -      key +      key, +      &HtmlEntities.decode/1      )    end    def ensure_scrubbed_html(          content,          scrubbers, -        false = _fake -      ) do -    {:commit, filter_tags(content, scrubbers)} -  end - -  def ensure_scrubbed_html( -        content, -        scrubbers, -        true = _fake +        fake, +        callback        ) do -    {:ignore, filter_tags(content, scrubbers)} +    content = +      content +      |> filter_tags(scrubbers) +      |> callback.() + +    if fake do +      {:ignore, content} +    else +      {:commit, content} +    end    end    defp generate_scrubber_signature(scrubber) when is_atom(scrubber) do @@ -142,6 +151,7 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do      Meta.allow_tag_with_these_attributes("img", [        "width",        "height", +      "class",        "title",        "alt"      ]) @@ -212,6 +222,7 @@ defmodule Pleroma.HTML.Scrubber.Default do      Meta.allow_tag_with_these_attributes("img", [        "width",        "height", +      "class",        "title",        "alt"      ]) diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex index 25bd911fb..2f4687fa2 100644 --- a/lib/pleroma/object/containment.ex +++ b/lib/pleroma/object/containment.ex @@ -1,7 +1,5 @@  defmodule Pleroma.Object.Containment do    @moduledoc """ -  # Object Containment -    This module contains some useful functions for containing objects to specific    origins and determining those origins.  They previously lived in the    ActivityPub `Transmogrifier` module. diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index f701aaaa5..a476f1d49 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -35,7 +35,7 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do    defp csp_string do      scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme]      static_url = Pleroma.Web.Endpoint.static_url() -    websocket_url = String.replace(static_url, "http", "ws") +    websocket_url = Pleroma.Web.Endpoint.websocket_url()      connect_src = "connect-src 'self' #{static_url} #{websocket_url}" diff --git a/lib/pleroma/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex index 5888d596a..9d43732eb 100644 --- a/lib/pleroma/plugs/oauth_plug.ex +++ b/lib/pleroma/plugs/oauth_plug.ex @@ -16,6 +16,16 @@ defmodule Pleroma.Plugs.OAuthPlug do    def call(%{assigns: %{user: %User{}}} = conn, _), do: conn +  def call(%{params: %{"access_token" => access_token}} = conn, _) do +    with {:ok, user, token_record} <- fetch_user_and_token(access_token) do +      conn +      |> assign(:token, token_record) +      |> assign(:user, user) +    else +      _ -> conn +    end +  end +    def call(conn, _) do      with {:ok, token_str} <- fetch_token_str(conn),           {:ok, user, token_record} <- fetch_user_and_token(token_str) do diff --git a/lib/pleroma/repo.ex b/lib/pleroma/repo.ex index aa5d427ae..f57e088bc 100644 --- a/lib/pleroma/repo.ex +++ b/lib/pleroma/repo.ex @@ -19,4 +19,32 @@ defmodule Pleroma.Repo do    def init(_, opts) do      {:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))}    end + +  @doc "find resource based on prepared query" +  @spec find_resource(Ecto.Query.t()) :: {:ok, struct()} | {:error, :not_found} +  def find_resource(%Ecto.Query{} = query) do +    case __MODULE__.one(query) do +      nil -> {:error, :not_found} +      resource -> {:ok, resource} +    end +  end + +  def find_resource(_query), do: {:error, :not_found} + +  @doc """ +  Gets association from cache or loads if need + +  ## Examples + +    iex> Repo.get_assoc(token, :user) +    %User{} + +  """ +  @spec get_assoc(struct(), atom()) :: {:ok, struct()} | {:error, :not_found} +  def get_assoc(resource, association) do +    case __MODULE__.preload(resource, association) do +      %{^association => assoc} when not is_nil(assoc) -> {:ok, assoc} +      _ -> {:error, :not_found} +    end +  end  end diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index f72334930..c47d65241 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -4,7 +4,7 @@  defmodule Pleroma.Upload do    @moduledoc """ -  # Upload +  Manage user uploads    Options:    * `:type`: presets for activity type (defaults to Document) and size limits from app configuration diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 4417a12dd..6e5473177 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -10,8 +10,6 @@ defmodule Pleroma.User do    alias Comeonin.Pbkdf2    alias Pleroma.Activity -  alias Pleroma.Bookmark -  alias Pleroma.Formatter    alias Pleroma.Notification    alias Pleroma.Object    alias Pleroma.Registration @@ -56,7 +54,6 @@ defmodule Pleroma.User do      field(:tags, {:array, :string}, default: [])      field(:last_refreshed_at, :naive_datetime_usec)      field(:last_digest_emailed_at, :naive_datetime) -    has_many(:bookmarks, Bookmark)      has_many(:notifications, Notification)      has_many(:registrations, Registration)      embeds_one(:info, Pleroma.User.Info) @@ -424,7 +421,7 @@ defmodule Pleroma.User do      Enum.map(        followed_identifiers,        fn followed_identifier -> -        with %User{} = followed <- get_or_fetch(followed_identifier), +        with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),               {:ok, follower} <- maybe_direct_follow(follower, followed),               {:ok, _} <- ActivityPub.follow(follower, followed) do            followed @@ -508,7 +505,15 @@ defmodule Pleroma.User do    def get_cached_by_nickname(nickname) do      key = "nickname:#{nickname}" -    Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end) + +    Cachex.fetch!(:user_cache, key, fn -> +      user_result = get_or_fetch_by_nickname(nickname) + +      case user_result do +        {:ok, user} -> {:commit, user} +        {:error, _error} -> {:ignore, nil} +      end +    end)    end    def get_cached_by_nickname_or_id(nickname_or_id) do @@ -544,7 +549,7 @@ defmodule Pleroma.User do    def get_or_fetch_by_nickname(nickname) do      with %User{} = user <- get_by_nickname(nickname) do -      user +      {:ok, user}      else        _e ->          with [_nick, _domain] <- String.split(nickname, "@"), @@ -554,9 +559,9 @@ defmodule Pleroma.User do              {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])            end -          user +          {:ok, user}          else -          _e -> nil +          _e -> {:error, "not found " <> nickname}          end      end    end @@ -903,7 +908,7 @@ defmodule Pleroma.User do      Enum.map(        blocked_identifiers,        fn blocked_identifier -> -        with %User{} = blocked <- get_or_fetch(blocked_identifier), +        with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),               {:ok, blocker} <- block(blocker, blocked),               {:ok, _} <- ActivityPub.block(blocker, blocked) do            blocked @@ -1158,7 +1163,12 @@ defmodule Pleroma.User do      |> update_and_set_cache()    end -  def delete(%User{} = user) do +  @spec delete(User.t()) :: :ok +  def delete(%User{} = user), +    do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user]) + +  @spec perform(atom(), User.t()) :: {:ok, User.t()} +  def perform(:delete, %User{} = user) do      {:ok, user} = User.deactivate(user)      # Remove all relationships @@ -1174,22 +1184,23 @@ defmodule Pleroma.User do    end    def delete_user_activities(%User{ap_id: ap_id} = user) do -    Activity -    |> where(actor: ^ap_id) -    |> Activity.with_preloaded_object() -    |> Repo.all() -    |> Enum.each(fn -      %{data: %{"type" => "Create"}} = activity -> -        activity |> Object.normalize() |> ActivityPub.delete() +    stream = +      ap_id +      |> Activity.query_by_actor() +      |> Activity.with_preloaded_object() +      |> Repo.stream() -      # TODO: Do something with likes, follows, repeats. -      _ -> -        "Doing nothing" -    end) +    Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity)      {:ok, user}    end +  defp delete_activity(%{data: %{"type" => "Create"}} = activity) do +    Object.normalize(activity) |> ActivityPub.delete() +  end + +  defp delete_activity(_activity), do: "Doing nothing" +    def html_filter_policy(%User{info: %{no_rich_text: true}}) do      Pleroma.HTML.Scrubber.TwitterText    end @@ -1203,11 +1214,11 @@ defmodule Pleroma.User do      case ap_try do        {:ok, user} -> -        user +        {:ok, user}        _ ->          case OStatus.make_user(ap_id) do -          {:ok, user} -> user +          {:ok, user} -> {:ok, user}            _ -> {:error, "Could not fetch by AP id"}          end      end @@ -1217,20 +1228,20 @@ defmodule Pleroma.User do      user = get_cached_by_ap_id(ap_id)      if !is_nil(user) and !User.needs_update?(user) do -      user +      {:ok, user}      else        # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)        should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled]) -      user = fetch_by_ap_id(ap_id) +      resp = fetch_by_ap_id(ap_id)        if should_fetch_initial do -        with %User{} = user do +        with {:ok, %User{} = user} = resp do            {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])          end        end -      user +      resp      end    end @@ -1272,7 +1283,7 @@ defmodule Pleroma.User do    end    def get_public_key_for_ap_id(ap_id) do -    with %User{} = user <- get_or_fetch_by_ap_id(ap_id), +    with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),           {:ok, public_key} <- public_key_from_info(user.info) do        {:ok, public_key}      else @@ -1324,18 +1335,15 @@ defmodule Pleroma.User do      end    end -  def parse_bio(bio, user \\ %User{info: %{source_data: %{}}}) -  def parse_bio(nil, _user), do: "" -  def parse_bio(bio, _user) when bio == "", do: bio +  def parse_bio(bio) when is_binary(bio) and bio != "" do +    bio +    |> CommonUtils.format_input("text/plain", mentions_format: :full) +    |> elem(0) +  end -  def parse_bio(bio, user) do -    emoji = -      (user.info.source_data["tag"] || []) -      |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end) -      |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} -> -        {String.trim(name, ":"), url} -      end) +  def parse_bio(_), do: "" +  def parse_bio(bio, user) when is_binary(bio) and bio != "" do      # TODO: get profile URLs other than user.ap_id      profile_urls = [user.ap_id] @@ -1345,9 +1353,10 @@ defmodule Pleroma.User do        rel: &RelMe.maybe_put_rel_me(&1, profile_urls)      )      |> elem(0) -    |> Formatter.emojify(emoji)    end +  def parse_bio(_, _), do: "" +    def tag(user_identifiers, tags) when is_list(user_identifiers) do      Repo.transaction(fn ->        for user_identifier <- user_identifiers, do: tag(user_identifier, tags) diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 2d360d650..ab4e81134 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -44,6 +44,7 @@ defmodule Pleroma.User.Info do      field(:pinned_activities, {:array, :string}, default: [])      field(:flavour, :string, default: nil)      field(:email_notifications, :map, default: %{"digest" => false}) +    field(:emoji, {:array, :map}, default: [])      field(:notification_settings, :map,        default: %{"remote" => true, "local" => true, "followers" => true, "follows" => true} diff --git a/lib/pleroma/user_invite_token.ex b/lib/pleroma/user_invite_token.ex index 86f0a5486..fadc89891 100644 --- a/lib/pleroma/user_invite_token.ex +++ b/lib/pleroma/user_invite_token.ex @@ -24,7 +24,7 @@ defmodule Pleroma.UserInviteToken do      timestamps()    end -  @spec create_invite(map()) :: UserInviteToken.t() +  @spec create_invite(map()) :: {:ok, UserInviteToken.t()}    def create_invite(params \\ %{}) do      %UserInviteToken{}      |> cast(params, [:max_use, :expires_at]) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 604ffae7b..8f8c23a9b 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -4,6 +4,7 @@  defmodule Pleroma.Web.ActivityPub.ActivityPub do    alias Pleroma.Activity +  alias Pleroma.Conversation    alias Pleroma.Instances    alias Pleroma.Notification    alias Pleroma.Object @@ -141,7 +142,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        end)        Notification.create_notifications(activity) + +      participations = +        activity +        |> Conversation.create_or_bump_for() +        |> get_participations() +        stream_out(activity) +      stream_out_participations(participations)        {:ok, activity}      else        %Activity{} = activity -> @@ -164,11 +172,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end +  defp get_participations({:ok, %{participations: participations}}), do: participations +  defp get_participations(_), do: [] + +  def stream_out_participations(participations) do +    participations = +      participations +      |> Repo.preload(:user) + +    Enum.each(participations, fn participation -> +      Pleroma.Web.Streamer.stream("participation", participation) +    end) +  end +    def stream_out(activity) do      public = "https://www.w3.org/ns/activitystreams#Public"      if activity.data["type"] in ["Create", "Announce", "Delete"] do -      object = Object.normalize(activity)        Pleroma.Web.Streamer.stream("user", activity)        Pleroma.Web.Streamer.stream("list", activity) @@ -180,6 +200,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do          end          if activity.data["type"] in ["Create"] do +          object = Object.normalize(activity) +            object.data            |> Map.get("tag", [])            |> Enum.filter(fn tag -> is_bitstring(tag) end) @@ -194,6 +216,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do            end          end        else +        # TODO: Write test, replace with visibility test          if !Enum.member?(activity.data["cc"] || [], public) &&               !Enum.member?(                 activity.data["to"], @@ -456,35 +479,44 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end -  def fetch_activities_for_context(context, opts \\ %{}) do +  defp fetch_activities_for_context_query(context, opts) do      public = ["https://www.w3.org/ns/activitystreams#Public"]      recipients =        if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public -    query = from(activity in Activity) - -    query = -      query -      |> restrict_blocked(opts) -      |> restrict_recipients(recipients, opts["user"]) - -    query = -      from( -        activity in query, -        where: -          fragment( -            "?->>'type' = ? and ?->>'context' = ?", -            activity.data, -            "Create", -            activity.data, -            ^context -          ), -        order_by: [desc: :id] +    from(activity in Activity) +    |> restrict_blocked(opts) +    |> restrict_recipients(recipients, opts["user"]) +    |> where( +      [activity], +      fragment( +        "?->>'type' = ? and ?->>'context' = ?", +        activity.data, +        "Create", +        activity.data, +        ^context        ) -      |> Activity.with_preloaded_object() +    ) +    |> order_by([activity], desc: activity.id) +  end -    Repo.all(query) +  @spec fetch_activities_for_context(String.t(), keyword() | map()) :: [Activity.t()] +  def fetch_activities_for_context(context, opts \\ %{}) do +    context +    |> fetch_activities_for_context_query(opts) +    |> Activity.with_preloaded_object() +    |> Repo.all() +  end + +  @spec fetch_latest_activity_id_for_context(String.t(), keyword() | map()) :: +          Pleroma.FlakeId.t() | nil +  def fetch_latest_activity_id_for_context(context, opts \\ %{}) do +    context +    |> fetch_activities_for_context_query(opts) +    |> limit(1) +    |> select([a], a.id) +    |> Repo.one()    end    def fetch_public_activities(opts \\ %{}) do @@ -783,11 +815,32 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      |> Activity.with_preloaded_object()    end +  defp maybe_preload_bookmarks(query, %{"skip_preload" => true}), do: query + +  defp maybe_preload_bookmarks(query, opts) do +    query +    |> Activity.with_preloaded_bookmark(opts["user"]) +  end + +  defp maybe_order(query, %{order: :desc}) do +    query +    |> order_by(desc: :id) +  end + +  defp maybe_order(query, %{order: :asc}) do +    query +    |> order_by(asc: :id) +  end + +  defp maybe_order(query, _), do: query +    def fetch_activities_query(recipients, opts \\ %{}) do      base_query = from(activity in Activity)      base_query      |> maybe_preload_objects(opts) +    |> maybe_preload_bookmarks(opts) +    |> maybe_order(opts)      |> restrict_recipients(recipients, opts["user"])      |> restrict_tag(opts)      |> restrict_tag_reject(opts) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 0b80566bf..c967ab7a9 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -155,7 +155,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do      with %User{} = recipient <- User.get_cached_by_nickname(nickname), -         %User{} = actor <- User.get_or_fetch_by_ap_id(params["actor"]), +         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),           true <- Utils.recipient_in_message(recipient, actor, params),           params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do        Federator.incoming_ap_doc(params) 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 34665a3a6..87fa514c3 100644 --- a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex @@ -5,6 +5,8 @@  defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do    alias Pleroma.User +  @moduledoc "Prevent followbots from following with a bit of heuristic" +    @behaviour Pleroma.Web.ActivityPub.MRF    # XXX: this should become User.normalize_by_ap_id() or similar, really. diff --git a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex index a93ccf386..b8d38aae6 100644 --- a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex @@ -4,6 +4,7 @@  defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do    require Logger +  @moduledoc "Drop and log everything received"    @behaviour Pleroma.Web.ActivityPub.MRF    @impl true diff --git a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex index 895376c9d..15d8514be 100644 --- a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex +++ b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex @@ -5,6 +5,7 @@  defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do    alias Pleroma.Object +  @moduledoc "Ensure a re: is prepended on replies to a post with a Subject"    @behaviour Pleroma.Web.ActivityPub.MRF    @reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless]) diff --git a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex index 6736f3cb9..a699f6a7e 100644 --- a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex @@ -4,6 +4,8 @@  defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do    alias Pleroma.User +  @moduledoc "Block messages with too much mentions (configurable)" +    @behaviour Pleroma.Web.ActivityPub.MRF    defp delist_message(message, threshold) when threshold > 0 do diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex index e8dfba672..d5c341433 100644 --- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex @@ -3,6 +3,8 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do +  @moduledoc "Reject or Word-Replace messages with a keyword or regex" +    @behaviour Pleroma.Web.ActivityPub.MRF    defp string_matches?(string, _) when not is_binary(string) do      false diff --git a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex index 081456046..f30fee0d5 100644 --- a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex @@ -3,6 +3,7 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do +  @moduledoc "Ensure no content placeholder is present (such as the dot from mastodon)"    @behaviour Pleroma.Web.ActivityPub.MRF    @impl true diff --git a/lib/pleroma/web/activity_pub/mrf/noop_policy.ex b/lib/pleroma/web/activity_pub/mrf/noop_policy.ex index 40f37bdb1..c47cb3298 100644 --- a/lib/pleroma/web/activity_pub/mrf/noop_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/noop_policy.ex @@ -3,6 +3,7 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.ActivityPub.MRF.NoOpPolicy do +  @moduledoc "Does nothing (lets the messages go through unmodified)"    @behaviour Pleroma.Web.ActivityPub.MRF    @impl true diff --git a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex index 3d13cdb32..9c87c6963 100644 --- a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex +++ b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex @@ -3,6 +3,7 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do +  @moduledoc "Scrub configured hypertext markup"    alias Pleroma.HTML    @behaviour Pleroma.Web.ActivityPub.MRF diff --git a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex index 4197be847..ea3df1b4d 100644 --- a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex +++ b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex @@ -4,6 +4,7 @@  defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do    alias Pleroma.User +  @moduledoc "Rejects non-public (followers-only, direct) activities"    @behaviour Pleroma.Web.ActivityPub.MRF    @impl true diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index 798ba9687..2f105700b 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -4,6 +4,7 @@  defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do    alias Pleroma.User +  @moduledoc "Filter activities depending on their origin instance"    @behaviour Pleroma.Web.ActivityPub.MRF    defp check_accept(%{host: actor_host} = _actor_info, object) do diff --git a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex index b242e44e6..b52be30e7 100644 --- a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex @@ -5,6 +5,19 @@  defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do    alias Pleroma.User    @behaviour Pleroma.Web.ActivityPub.MRF +  @moduledoc """ +     Apply policies based on user tags + +     This policy applies policies on a user activities depending on their tags +     on your instance. + +     - `mrf_tag:media-force-nsfw`: Mark as sensitive on presence of attachments +     - `mrf_tag:media-strip`: Remove attachments +     - `mrf_tag:force-unlisted`: Mark as unlisted (removes from the federated timeline) +     - `mrf_tag:sandbox`: Remove from public (local and federated) timelines +     - `mrf_tag:disable-remote-subscription`: Reject non-local follow requests +     - `mrf_tag:disable-any-subscription`: Reject any follow requests +  """    defp get_tags(%User{tags: tags}) when is_list(tags), do: tags    defp get_tags(_), do: [] diff --git a/lib/pleroma/web/activity_pub/mrf/user_allowlist.ex b/lib/pleroma/web/activity_pub/mrf/user_allowlist.ex index a3b1f8aa0..f5078d818 100644 --- a/lib/pleroma/web/activity_pub/mrf/user_allowlist.ex +++ b/lib/pleroma/web/activity_pub/mrf/user_allowlist.ex @@ -5,6 +5,7 @@  defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do    alias Pleroma.Config +  @moduledoc "Accept-list of users from specified instances"    @behaviour Pleroma.Web.ActivityPub.MRF    defp filter_by_list(object, []), do: {:ok, object} diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex index a7a20ca37..93808517b 100644 --- a/lib/pleroma/web/activity_pub/relay.ex +++ b/lib/pleroma/web/activity_pub/relay.ex @@ -15,7 +15,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do    def follow(target_instance) do      with %User{} = local_user <- get_actor(), -         %User{} = target_user <- User.get_or_fetch_by_ap_id(target_instance), +         {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),           {:ok, activity} <- ActivityPub.follow(local_user, target_user) do        Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")        {:ok, activity} @@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do    def unfollow(target_instance) do      with %User{} = local_user <- get_actor(), -         %User{} = target_user <- User.get_or_fetch_by_ap_id(target_instance), +         {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),           {:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do        Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}")        {:ok, activity} diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index b1e859d7c..508f3532f 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -126,7 +126,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    def fix_implicit_addressing(object, _), do: object    def fix_addressing(object) do -    %User{} = user = User.get_or_fetch_by_ap_id(object["actor"]) +    {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])      followers_collection = User.ap_followers(user)      object @@ -407,7 +407,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do        |> fix_addressing      with nil <- Activity.get_create_by_object_ap_id(object["id"]), -         %User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do +         {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do        object = fix_object(data["object"])        params = %{ @@ -436,7 +436,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do          %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data        ) do      with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), -         %User{} = follower <- User.get_or_fetch_by_ap_id(follower), +         {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),           {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do        with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),             {:user_blocked, false} <- @@ -485,7 +485,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do          %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data        ) do      with actor <- Containment.get_actor(data), -         %User{} = followed <- User.get_or_fetch_by_ap_id(actor), +         {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),           {:ok, follow_activity} <- get_follow_activity(follow_object, followed),           {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),           %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), @@ -511,7 +511,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do          %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data        ) do      with actor <- Containment.get_actor(data), -         %User{} = followed <- User.get_or_fetch_by_ap_id(actor), +         {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),           {:ok, follow_activity} <- get_follow_activity(follow_object, followed),           {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),           %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), @@ -535,7 +535,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do          %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data        ) do      with actor <- Containment.get_actor(data), -         %User{} = actor <- User.get_or_fetch_by_ap_id(actor), +         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),           {:ok, object} <- get_obj_helper(object_id),           {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do        {:ok, activity} @@ -548,7 +548,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do          %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data        ) do      with actor <- Containment.get_actor(data), -         %User{} = actor <- User.get_or_fetch_by_ap_id(actor), +         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),           {:ok, object} <- get_obj_helper(object_id),           public <- Visibility.is_public?(data),           {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do @@ -603,7 +603,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      object_id = Utils.get_ap_id(object_id)      with actor <- Containment.get_actor(data), -         %User{} = actor <- User.get_or_fetch_by_ap_id(actor), +         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),           {:ok, object} <- get_obj_helper(object_id),           :ok <- Containment.contain_origin(actor.ap_id, object.data),           {:ok, activity} <- ActivityPub.delete(object, false) do @@ -622,7 +622,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do          } = data        ) do      with actor <- Containment.get_actor(data), -         %User{} = actor <- User.get_or_fetch_by_ap_id(actor), +         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),           {:ok, object} <- get_obj_helper(object_id),           {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do        {:ok, activity} @@ -640,7 +640,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do          } = _data        ) do      with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), -         %User{} = follower <- User.get_or_fetch_by_ap_id(follower), +         {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),           {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do        User.unfollow(follower, followed)        {:ok, activity} @@ -659,7 +659,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do        ) do      with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),           %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked), -         %User{} = blocker <- User.get_or_fetch_by_ap_id(blocker), +         {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),           {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do        User.unblock(blocker, blocked)        {:ok, activity} @@ -673,7 +673,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do        ) do      with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),           %User{local: true} = blocked = User.get_cached_by_ap_id(blocked), -         %User{} = blocker = User.get_or_fetch_by_ap_id(blocker), +         {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),           {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do        User.unfollow(blocker, blocked)        User.block(blocker, blocked) @@ -692,7 +692,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do          } = data        ) do      with actor <- Containment.get_actor(data), -         %User{} = actor <- User.get_or_fetch_by_ap_id(actor), +         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),           {:ok, object} <- get_obj_helper(object_id),           {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do        {:ok, activity} @@ -856,10 +856,16 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      |> Map.put("tag", tags ++ mentions)    end +  def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do +    user_info = add_emoji_tags(user_info) + +    object +    |> Map.put(:info, user_info) +  end +    # TODO: we should probably send mtime instead of unix epoch time for updated -  def add_emoji_tags(object) do +  def add_emoji_tags(%{"emoji" => emoji} = object) do      tags = object["tag"] || [] -    emoji = object["emoji"] || []      out =        emoji @@ -877,6 +883,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      |> Map.put("tag", tags ++ out)    end +  def add_emoji_tags(object) do +    object +  end +    def set_conversation(object) do      Map.put(object, "conversation", object["context"])    end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 5926a3294..1254fdf6c 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -69,6 +69,11 @@ defmodule Pleroma.Web.ActivityPub.UserView do      endpoints = render("endpoints.json", %{user: user}) +    user_tags = +      user +      |> Transmogrifier.add_emoji_tags() +      |> Map.get("tag", []) +      %{        "id" => user.ap_id,        "type" => "Person", @@ -87,7 +92,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do          "publicKeyPem" => public_key        },        "endpoints" => endpoints, -      "tag" => user.info.source_data["tag"] || [] +      "tag" => (user.info.source_data["tag"] || []) ++ user_tags      }      |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))      |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) diff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex index b02f595dc..d4e0ffa80 100644 --- a/lib/pleroma/web/auth/authenticator.ex +++ b/lib/pleroma/web/auth/authenticator.ex @@ -42,4 +42,30 @@ defmodule Pleroma.Web.Auth.Authenticator do      implementation().oauth_consumer_template() ||        Pleroma.Config.get([:auth, :oauth_consumer_template], "consumer.html")    end + +  @doc "Gets user by nickname or email for auth." +  @spec fetch_user(String.t()) :: User.t() | nil +  def fetch_user(name) do +    User.get_by_nickname_or_email(name) +  end + +  # Gets name and password from conn +  # +  @spec fetch_credentials(Plug.Conn.t() | map()) :: +          {:ok, {name :: any, password :: any}} | {:error, :invalid_credentials} +  def fetch_credentials(%Plug.Conn{params: params} = _), +    do: fetch_credentials(params) + +  def fetch_credentials(params) do +    case params do +      %{"authorization" => %{"name" => name, "password" => password}} -> +        {:ok, {name, password}} + +      %{"grant_type" => "password", "username" => name, "password" => password} -> +        {:ok, {name, password}} + +      _ -> +        {:error, :invalid_credentials} +    end +  end  end diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex index 363c99597..177c05636 100644 --- a/lib/pleroma/web/auth/ldap_authenticator.ex +++ b/lib/pleroma/web/auth/ldap_authenticator.ex @@ -7,6 +7,9 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do    require Logger +  import Pleroma.Web.Auth.Authenticator, +    only: [fetch_credentials: 1, fetch_user: 1] +    @behaviour Pleroma.Web.Auth.Authenticator    @base Pleroma.Web.Auth.PleromaAuthenticator @@ -20,30 +23,20 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do    defdelegate oauth_consumer_template, to: @base    def get_user(%Plug.Conn{} = conn) do -    if Pleroma.Config.get([:ldap, :enabled]) do -      {name, password} = -        case conn.params do -          %{"authorization" => %{"name" => name, "password" => password}} -> -            {name, password} - -          %{"grant_type" => "password", "username" => name, "password" => password} -> -            {name, password} -        end - -      case ldap_user(name, password) do -        %User{} = user -> -          {:ok, user} +    with {:ldap, true} <- {:ldap, Pleroma.Config.get([:ldap, :enabled])}, +         {:ok, {name, password}} <- fetch_credentials(conn), +         %User{} = user <- ldap_user(name, password) do +      {:ok, user} +    else +      {:error, {:ldap_connection_error, _}} -> +        # When LDAP is unavailable, try default authenticator +        @base.get_user(conn) -        {:error, {:ldap_connection_error, _}} -> -          # When LDAP is unavailable, try default authenticator -          @base.get_user(conn) +      {:ldap, _} -> +        @base.get_user(conn) -        error -> -          error -      end -    else -      # Fall back to default authenticator -      @base.get_user(conn) +      error -> +        error      end    end @@ -94,7 +87,7 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do      case :eldap.simple_bind(connection, "#{uid}=#{name},#{base}", password) do        :ok -> -        case User.get_by_nickname_or_email(name) do +        case fetch_user(name) do            %User{} = user ->              user diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index d647f1e05..dd79cdcf7 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -8,19 +8,14 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do    alias Pleroma.Repo    alias Pleroma.User +  import Pleroma.Web.Auth.Authenticator, +    only: [fetch_credentials: 1, fetch_user: 1] +    @behaviour Pleroma.Web.Auth.Authenticator    def get_user(%Plug.Conn{} = conn) do -    {name, password} = -      case conn.params do -        %{"authorization" => %{"name" => name, "password" => password}} -> -          {name, password} - -        %{"grant_type" => "password", "username" => name, "password" => password} -> -          {name, password} -      end - -    with {_, %User{} = user} <- {:user, User.get_by_nickname_or_email(name)}, +    with {:ok, {name, password}} <- fetch_credentials(conn), +         {_, %User{} = user} <- {:user, fetch_user(name)},           {_, true} <- {:checkpw, Pbkdf2.checkpw(password, user.password_hash)} do        {:ok, user}      else diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index ecd183110..b53869c75 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -151,8 +151,8 @@ defmodule Pleroma.Web.CommonAPI do             ),           {to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility),           context <- make_context(in_reply_to), -         cw <- data["spoiler_text"], -         full_payload <- String.trim(status <> (data["spoiler_text"] || "")), +         cw <- data["spoiler_text"] || "", +         full_payload <- String.trim(status <> cw),           length when length in 1..limit <- String.length(full_payload),           object <-             make_note_data( @@ -170,10 +170,7 @@ defmodule Pleroma.Web.CommonAPI do             Map.put(               object,               "emoji", -             (Formatter.get_emoji(status) ++ Formatter.get_emoji(data["spoiler_text"])) -             |> Enum.reduce(%{}, fn {name, file, _}, acc -> -               Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}") -             end) +             Formatter.get_emoji_map(full_payload)             ) do        res =          ActivityPub.create( diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 811a45c79..83ad90989 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -8,7 +8,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    alias Pleroma.Activity    alias Pleroma.Bookmark    alias Pleroma.Config +  alias Pleroma.Conversation.Participation    alias Pleroma.Filter +  alias Pleroma.Formatter    alias Pleroma.Notification    alias Pleroma.Object    alias Pleroma.Object.Fetcher @@ -23,6 +25,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    alias Pleroma.Web.CommonAPI    alias Pleroma.Web.MastodonAPI.AccountView    alias Pleroma.Web.MastodonAPI.AppView +  alias Pleroma.Web.MastodonAPI.ConversationView    alias Pleroma.Web.MastodonAPI.FilterView    alias Pleroma.Web.MastodonAPI.ListView    alias Pleroma.Web.MastodonAPI.MastodonAPI @@ -86,7 +89,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      user_params =        %{}        |> add_if_present(params, "display_name", :name) -      |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end) +      |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)        |> add_if_present(params, "avatar", :avatar, fn value ->          with %Plug.Upload{} <- value,               {:ok, object} <- ActivityPub.upload(value, type: :avatar) do @@ -96,6 +99,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do          end        end) +    emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "") + +    user_info_emojis = +      ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text)) +      |> Enum.dedup() +      info_params =        [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]        |> Enum.reduce(%{}, fn key, acc -> @@ -112,6 +121,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do            _ -> :error          end        end) +      |> Map.put(:emoji, user_info_emojis)      info_cng = User.Info.profile_update(user.info, info_params) @@ -157,7 +167,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      end    end -  @mastodon_api_level "2.5.0" +  @mastodon_api_level "2.6.5"    def masto_instance(conn, _params) do      instance = Config.get(:instance) @@ -285,8 +295,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        |> ActivityPub.contain_timeline(user)        |> Enum.reverse() -    user = Repo.preload(user, bookmarks: :activity) -      conn      |> add_link_headers(:home_timeline, activities)      |> put_view(StatusView) @@ -305,8 +313,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        |> ActivityPub.fetch_public_activities()        |> Enum.reverse() -    user = Repo.preload(user, bookmarks: :activity) -      conn      |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})      |> put_view(StatusView) @@ -314,8 +320,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do -    with %User{} = user <- User.get_cached_by_id(params["id"]), -         reading_user <- Repo.preload(reading_user, :bookmarks) do +    with %User{} = user <- User.get_cached_by_id(params["id"]) do        activities = ActivityPub.fetch_user_activities(user, reading_user, params)        conn @@ -342,8 +347,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        |> ActivityPub.fetch_activities_query(params)        |> Pagination.fetch_paginated(params) -    user = Repo.preload(user, bookmarks: :activity) -      conn      |> add_link_headers(:dm_timeline, activities)      |> put_view(StatusView) @@ -353,8 +356,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do      with %Activity{} = activity <- Activity.get_by_id_with_object(id),           true <- Visibility.visible_for_user?(activity, user) do -      user = Repo.preload(user, bookmarks: :activity) -        conn        |> put_view(StatusView)        |> try_render("status.json", %{activity: activity, for: user}) @@ -504,8 +505,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do      with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),           %Activity{} = announce <- Activity.normalize(announce.data) do -      user = Repo.preload(user, bookmarks: :activity) -        conn        |> put_view(StatusView)        |> try_render("status.json", %{activity: announce, for: user, as: :activity}) @@ -515,8 +514,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do      with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),           %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do -      user = Repo.preload(user, bookmarks: :activity) -        conn        |> put_view(StatusView)        |> try_render("status.json", %{activity: activity, for: user, as: :activity}) @@ -567,8 +564,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do           %User{} = user <- User.get_cached_by_nickname(user.nickname),           true <- Visibility.visible_for_user?(activity, user),           {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do -      user = Repo.preload(user, bookmarks: :activity) -        conn        |> put_view(StatusView)        |> try_render("status.json", %{activity: activity, for: user, as: :activity}) @@ -580,8 +575,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do           %User{} = user <- User.get_cached_by_nickname(user.nickname),           true <- Visibility.visible_for_user?(activity, user),           {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do -      user = Repo.preload(user, bookmarks: :activity) -        conn        |> put_view(StatusView)        |> try_render("status.json", %{activity: activity, for: user, as: :activity}) @@ -704,7 +697,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      end    end -  def favourited_by(conn, %{"id" => id}) do +  def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do      with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),           %Object{data: %{"likes" => likes}} <- Object.normalize(object) do        q = from(u in User, where: u.ap_id in ^likes) @@ -712,13 +705,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        conn        |> put_view(AccountView) -      |> render(AccountView, "accounts.json", %{users: users, as: :user}) +      |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})      else        _ -> json(conn, [])      end    end -  def reblogged_by(conn, %{"id" => id}) do +  def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do      with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),           %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do        q = from(u in User, where: u.ap_id in ^announces) @@ -726,7 +719,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        conn        |> put_view(AccountView) -      |> render("accounts.json", %{users: users, as: :user}) +      |> render("accounts.json", %{for: user, users: users, as: :user})      else        _ -> json(conn, [])      end @@ -783,7 +776,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        conn        |> add_link_headers(:followers, followers, user)        |> put_view(AccountView) -      |> render("accounts.json", %{users: followers, as: :user}) +      |> render("accounts.json", %{for: for_user, users: followers, as: :user})      end    end @@ -800,7 +793,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        conn        |> add_link_headers(:following, followers, user)        |> put_view(AccountView) -      |> render("accounts.json", %{users: followers, as: :user}) +      |> render("accounts.json", %{for: for_user, users: followers, as: :user})      end    end @@ -808,7 +801,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      with {:ok, follow_requests} <- User.get_follow_requests(followed) do        conn        |> put_view(AccountView) -      |> render("accounts.json", %{users: follow_requests, as: :user}) +      |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})      end    end @@ -1102,8 +1095,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        ActivityPub.fetch_activities([], params)        |> Enum.reverse() -    user = Repo.preload(user, bookmarks: :activity) -      conn      |> add_link_headers(:favourites, activities)      |> put_view(StatusView) @@ -1149,7 +1140,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def bookmarks(%{assigns: %{user: user}} = conn, params) do      user = User.get_cached_by_id(user.id) -    user = Repo.preload(user, bookmarks: :activity)      bookmarks =        Bookmark.for_user_query(user.id) @@ -1157,7 +1147,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      activities =        bookmarks -      |> Enum.map(fn b -> b.activity end) +      |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)      conn      |> add_link_headers(:bookmarks, bookmarks) @@ -1235,7 +1225,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do           {:ok, users} = Pleroma.List.get_following(list) do        conn        |> put_view(AccountView) -      |> render("accounts.json", %{users: users, as: :user}) +      |> render("accounts.json", %{for: user, users: users, as: :user})      end    end @@ -1266,8 +1256,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do          |> ActivityPub.fetch_activities_bounded(following, params)          |> Enum.reverse() -      user = Repo.preload(user, bookmarks: :activity) -        conn        |> put_view(StatusView)        |> render("index.json", %{activities: activities, for: user, as: :activity}) @@ -1295,8 +1283,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        initial_state =          %{            meta: %{ -            streaming_api_base_url: -              String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"), +            streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),              access_token: token,              locale: "en",              domain: Pleroma.Web.Endpoint.host(), @@ -1653,7 +1640,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do                x,                "id",                case User.get_or_fetch(x["acct"]) do -                %{id: id} -> id +                {:ok, %User{id: id}} -> id                  _ -> 0                end              ) @@ -1705,6 +1692,31 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      end    end +  def conversations(%{assigns: %{user: user}} = conn, params) do +    participations = Participation.for_user_with_last_activity_id(user, params) + +    conversations = +      Enum.map(participations, fn participation -> +        ConversationView.render("participation.json", %{participation: participation, user: user}) +      end) + +    conn +    |> add_link_headers(:conversations, participations) +    |> json(conversations) +  end + +  def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do +    with %Participation{} = participation <- +           Repo.get_by(Participation, id: participation_id, user_id: user.id), +         {:ok, participation} <- Participation.mark_as_read(participation) do +      participation_view = +        ConversationView.render("participation.json", %{participation: participation, user: user}) + +      conn +      |> json(participation_view) +    end +  end +    def try_render(conn, target, params)        when is_binary(target) do      res = render(conn, target, params) diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex new file mode 100644 index 000000000..8e8f7cf31 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex @@ -0,0 +1,38 @@ +defmodule Pleroma.Web.MastodonAPI.ConversationView do +  use Pleroma.Web, :view + +  alias Pleroma.Activity +  alias Pleroma.Repo +  alias Pleroma.Web.ActivityPub.ActivityPub +  alias Pleroma.Web.MastodonAPI.AccountView +  alias Pleroma.Web.MastodonAPI.StatusView + +  def render("participation.json", %{participation: participation, user: user}) do +    participation = Repo.preload(participation, conversation: :users) + +    last_activity_id = +      with nil <- participation.last_activity_id do +        ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{ +          "user" => user, +          "blocking_user" => user +        }) +      end + +    activity = Activity.get_by_id_with_object(last_activity_id) + +    last_status = StatusView.render("status.json", %{activity: activity, for: user}) + +    accounts = +      AccountView.render("accounts.json", %{ +        users: participation.conversation.users, +        as: :user +      }) + +    %{ +      id: participation.id |> to_string(), +      accounts: accounts, +      unread: !participation.read, +      last_status: last_status +    } +  end +end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 62d064d71..bd2372944 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -75,18 +75,22 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do    def render(          "status.json", -        %{activity: %{data: %{"type" => "Announce", "object" => object}} = activity} = opts +        %{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts        ) do      user = get_user(activity.data["actor"])      created_at = Utils.to_masto_date(activity.data["published"]) +    activity_object = Object.normalize(activity) + +    reblogged_activity = +      Activity.create_by_object_ap_id(activity_object.data["id"]) +      |> Activity.with_preloaded_bookmark(opts[:for]) +      |> Repo.one() -    reblogged_activity = Activity.get_create_by_object_ap_id(object)      reblogged = render("status.json", Map.put(opts, :activity, reblogged_activity)) -    activity_object = Object.normalize(activity)      favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || []) -    bookmarked = opts[:for] && CommonAPI.bookmarked?(opts[:for], reblogged_activity) +    bookmarked = Activity.get_bookmark(reblogged_activity, opts[:for]) != nil      mentions =        activity.recipients @@ -96,8 +100,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      %{        id: to_string(activity.id), -      uri: object, -      url: object, +      uri: activity_object.data["id"], +      url: activity_object.data["id"],        account: AccountView.render("account.json", %{user: user}),        in_reply_to_id: nil,        in_reply_to_account_id: nil, @@ -149,7 +153,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || []) -    bookmarked = opts[:for] && CommonAPI.bookmarked?(opts[:for], activity) +    bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil      attachment_data = object.data["attachment"] || []      attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment) diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/oauth/app.ex index 3476da484..bccc2ac96 100644 --- a/lib/pleroma/web/oauth/app.ex +++ b/lib/pleroma/web/oauth/app.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.App do    use Ecto.Schema    import Ecto.Changeset +  @type t :: %__MODULE__{}    schema "apps" do      field(:client_name, :string)      field(:redirect_uris, :string) diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex index 3461f9983..ca3901cc4 100644 --- a/lib/pleroma/web/oauth/authorization.ex +++ b/lib/pleroma/web/oauth/authorization.ex @@ -13,6 +13,7 @@ defmodule Pleroma.Web.OAuth.Authorization do    import Ecto.Changeset    import Ecto.Query +  @type t :: %__MODULE__{}    schema "oauth_authorizations" do      field(:token, :string)      field(:scopes, {:array, :string}, default: []) @@ -63,4 +64,11 @@ defmodule Pleroma.Web.OAuth.Authorization do      )      |> Repo.delete_all()    end + +  @doc "gets auth for app by token" +  @spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} +  def get_by_token(%App{id: app_id} = _app, token) do +    from(t in __MODULE__, where: t.app_id == ^app_id and t.token == ^token) +    |> Repo.find_resource() +  end  end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 688eaca11..e3c01217d 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -13,11 +13,15 @@ defmodule Pleroma.Web.OAuth.OAuthController do    alias Pleroma.Web.OAuth.App    alias Pleroma.Web.OAuth.Authorization    alias Pleroma.Web.OAuth.Token +  alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken +  alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken    import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]    if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) +  @expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600) +    plug(:fetch_session)    plug(:fetch_flash) @@ -138,25 +142,33 @@ defmodule Pleroma.Web.OAuth.OAuthController do      Authenticator.handle_error(conn, error)    end +  @doc "Renew access_token with refresh_token" +  def token_exchange( +        conn, +        %{"grant_type" => "refresh_token", "refresh_token" => token} = params +      ) do +    with %App{} = app <- get_app_from_request(conn, params), +         {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token), +         {:ok, token} <- RefreshToken.grant(token) do +      response_attrs = %{created_at: Token.Utils.format_created_at(token)} + +      json(conn, response_token(user, token, response_attrs)) +    else +      _error -> +        put_status(conn, 400) +        |> json(%{error: "Invalid credentials"}) +    end +  end +    def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do      with %App{} = app <- get_app_from_request(conn, params), -         fixed_token = fix_padding(params["code"]), -         %Authorization{} = auth <- -           Repo.get_by(Authorization, token: fixed_token, app_id: app.id), +         fixed_token = Token.Utils.fix_padding(params["code"]), +         {:ok, auth} <- Authorization.get_by_token(app, fixed_token),           %User{} = user <- User.get_cached_by_id(auth.user_id), -         {:ok, token} <- Token.exchange_token(app, auth), -         {:ok, inserted_at} <- DateTime.from_naive(token.inserted_at, "Etc/UTC") do -      response = %{ -        token_type: "Bearer", -        access_token: token.token, -        refresh_token: token.refresh_token, -        created_at: DateTime.to_unix(inserted_at), -        expires_in: 60 * 10, -        scope: Enum.join(token.scopes, " "), -        me: user.ap_id -      } - -      json(conn, response) +         {:ok, token} <- Token.exchange_token(app, auth) do +      response_attrs = %{created_at: Token.Utils.format_created_at(token)} + +      json(conn, response_token(user, token, response_attrs))      else        _error ->          put_status(conn, 400) @@ -177,16 +189,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do           true <- Enum.any?(scopes),           {:ok, auth} <- Authorization.create_authorization(app, user, scopes),           {:ok, token} <- Token.exchange_token(app, auth) do -      response = %{ -        token_type: "Bearer", -        access_token: token.token, -        refresh_token: token.refresh_token, -        expires_in: 60 * 10, -        scope: Enum.join(token.scopes, " "), -        me: user.ap_id -      } - -      json(conn, response) +      json(conn, response_token(user, token))      else        {:auth_active, false} ->          # Per https://github.com/tootsuite/mastodon/blob/ @@ -218,10 +221,12 @@ defmodule Pleroma.Web.OAuth.OAuthController do      token_exchange(conn, params)    end -  def token_revoke(conn, %{"token" => token} = params) do +  # Bad request +  def token_exchange(conn, params), do: bad_request(conn, params) + +  def token_revoke(conn, %{"token" => _token} = params) do      with %App{} = app <- get_app_from_request(conn, params), -         %Token{} = token <- Repo.get_by(Token, token: token, app_id: app.id), -         {:ok, %Token{}} <- Repo.delete(token) do +         {:ok, _token} <- RevokeToken.revoke(app, params) do        json(conn, %{})      else        _error -> @@ -230,6 +235,15 @@ defmodule Pleroma.Web.OAuth.OAuthController do      end    end +  def token_revoke(conn, params), do: bad_request(conn, params) + +  # Response for bad request +  defp bad_request(conn, _) do +    conn +    |> put_status(500) +    |> json(%{error: "Bad request"}) +  end +    @doc "Prepares OAuth request to provider for Ueberauth"    def prepare_request(conn, %{"provider" => provider, "authorization" => auth_attrs}) do      scope = @@ -278,25 +292,22 @@ defmodule Pleroma.Web.OAuth.OAuthController do      params = callback_params(params)      with {:ok, registration} <- Authenticator.get_registration(conn) do -      user = Repo.preload(registration, :user).user        auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state)) -      if user do -        create_authorization( -          conn, -          %{"authorization" => auth_attrs}, -          user: user -        ) -      else -        registration_params = -          Map.merge(auth_attrs, %{ -            "nickname" => Registration.nickname(registration), -            "email" => Registration.email(registration) -          }) +      case Repo.get_assoc(registration, :user) do +        {:ok, user} -> +          create_authorization(conn, %{"authorization" => auth_attrs}, user: user) -        conn -        |> put_session(:registration_id, registration.id) -        |> registration_details(%{"authorization" => registration_params}) +        _ -> +          registration_params = +            Map.merge(auth_attrs, %{ +              "nickname" => Registration.nickname(registration), +              "email" => Registration.email(registration) +            }) + +          conn +          |> put_session(:registration_id, registration.id) +          |> registration_details(%{"authorization" => registration_params})        end      else        _ -> @@ -399,36 +410,30 @@ defmodule Pleroma.Web.OAuth.OAuthController do      end    end -  # XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be -  # decoding it.  Investigate sometime. -  defp fix_padding(token) do -    token -    |> URI.decode() -    |> Base.url_decode64!(padding: false) -    |> Base.url_encode64(padding: false) +  defp get_app_from_request(conn, params) do +    conn +    |> fetch_client_credentials(params) +    |> fetch_client    end -  defp get_app_from_request(conn, params) do -    # Per RFC 6749, HTTP Basic is preferred to body params -    {client_id, client_secret} = -      with ["Basic " <> encoded] <- get_req_header(conn, "authorization"), -           {:ok, decoded} <- Base.decode64(encoded), -           [id, secret] <- -             String.split(decoded, ":") -             |> Enum.map(fn s -> URI.decode_www_form(s) end) do -        {id, secret} -      else -        _ -> {params["client_id"], params["client_secret"]} -      end +  defp fetch_client({id, secret}) when is_binary(id) and is_binary(secret) do +    Repo.get_by(App, client_id: id, client_secret: secret) +  end -    if client_id && client_secret do -      Repo.get_by( -        App, -        client_id: client_id, -        client_secret: client_secret -      ) +  defp fetch_client({_id, _secret}), do: nil + +  defp fetch_client_credentials(conn, params) do +    # Per RFC 6749, HTTP Basic is preferred to body params +    with ["Basic " <> encoded] <- get_req_header(conn, "authorization"), +         {:ok, decoded} <- Base.decode64(encoded), +         [id, secret] <- +           Enum.map( +             String.split(decoded, ":"), +             fn s -> URI.decode_www_form(s) end +           ) do +      {id, secret}      else -      nil +      _ -> {params["client_id"], params["client_secret"]}      end    end @@ -441,4 +446,16 @@ defmodule Pleroma.Web.OAuth.OAuthController do    defp put_session_registration_id(conn, registration_id),      do: put_session(conn, :registration_id, registration_id) + +  defp response_token(%User{} = user, token, opts \\ %{}) do +    %{ +      token_type: "Bearer", +      access_token: token.token, +      refresh_token: token.refresh_token, +      expires_in: @expires_in, +      scope: Enum.join(token.scopes, " "), +      me: user.ap_id +    } +    |> Map.merge(opts) +  end  end diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index 399140003..4e5d1d118 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.Token do    use Ecto.Schema    import Ecto.Query +  import Ecto.Changeset    alias Pleroma.Repo    alias Pleroma.User @@ -13,6 +14,9 @@ defmodule Pleroma.Web.OAuth.Token do    alias Pleroma.Web.OAuth.Authorization    alias Pleroma.Web.OAuth.Token +  @expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600) +  @type t :: %__MODULE__{} +    schema "oauth_tokens" do      field(:token, :string)      field(:refresh_token, :string) @@ -24,28 +28,67 @@ defmodule Pleroma.Web.OAuth.Token do      timestamps()    end +  @doc "Gets token for app by access token" +  @spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} +  def get_by_token(%App{id: app_id} = _app, token) do +    from(t in __MODULE__, where: t.app_id == ^app_id and t.token == ^token) +    |> Repo.find_resource() +  end + +  @doc "Gets token for app by refresh token" +  @spec get_by_refresh_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} +  def get_by_refresh_token(%App{id: app_id} = _app, token) do +    from(t in __MODULE__, +      where: t.app_id == ^app_id and t.refresh_token == ^token, +      preload: [:user] +    ) +    |> Repo.find_resource() +  end +    def exchange_token(app, auth) do      with {:ok, auth} <- Authorization.use_token(auth),           true <- auth.app_id == app.id do -      create_token(app, User.get_cached_by_id(auth.user_id), auth.scopes) +      create_token( +        app, +        User.get_cached_by_id(auth.user_id), +        %{scopes: auth.scopes} +      )      end    end -  def create_token(%App{} = app, %User{} = user, scopes \\ nil) do -    scopes = scopes || app.scopes -    token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) -    refresh_token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) - -    token = %Token{ -      token: token, -      refresh_token: refresh_token, -      scopes: scopes, -      user_id: user.id, -      app_id: app.id, -      valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10) -    } - -    Repo.insert(token) +  defp put_token(changeset) do +    changeset +    |> change(%{token: Token.Utils.generate_token()}) +    |> validate_required([:token]) +    |> unique_constraint(:token) +  end + +  defp put_refresh_token(changeset, attrs) do +    refresh_token = Map.get(attrs, :refresh_token, Token.Utils.generate_token()) + +    changeset +    |> change(%{refresh_token: refresh_token}) +    |> validate_required([:refresh_token]) +    |> unique_constraint(:refresh_token) +  end + +  defp put_valid_until(changeset, attrs) do +    expires_in = +      Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), @expires_in)) + +    changeset +    |> change(%{valid_until: expires_in}) +    |> validate_required([:valid_until]) +  end + +  def create_token(%App{} = app, %User{} = user, attrs \\ %{}) do +    %__MODULE__{user_id: user.id, app_id: app.id} +    |> cast(%{scopes: attrs[:scopes] || app.scopes}, [:scopes]) +    |> validate_required([:scopes, :user_id, :app_id]) +    |> put_valid_until(attrs) +    |> put_token +    |> put_refresh_token(attrs) +    |> Repo.insert()    end    def delete_user_tokens(%User{id: user_id}) do @@ -73,4 +116,10 @@ defmodule Pleroma.Web.OAuth.Token do      |> Repo.all()      |> Repo.preload(:app)    end + +  def is_expired?(%__MODULE__{valid_until: valid_until}) do +    NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0 +  end + +  def is_expired?(_), do: false  end diff --git a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex b/lib/pleroma/web/oauth/token/strategy/refresh_token.ex new file mode 100644 index 000000000..7df0be14e --- /dev/null +++ b/lib/pleroma/web/oauth/token/strategy/refresh_token.ex @@ -0,0 +1,54 @@ +defmodule Pleroma.Web.OAuth.Token.Strategy.RefreshToken do +  @moduledoc """ +  Functions for dealing with refresh token strategy. +  """ + +  alias Pleroma.Config +  alias Pleroma.Repo +  alias Pleroma.Web.OAuth.Token +  alias Pleroma.Web.OAuth.Token.Strategy.Revoke + +  @doc """ +  Will grant access token by refresh token. +  """ +  @spec grant(Token.t()) :: {:ok, Token.t()} | {:error, any()} +  def grant(token) do +    access_token = Repo.preload(token, [:user, :app]) + +    result = +      Repo.transaction(fn -> +        token_params = %{ +          app: access_token.app, +          user: access_token.user, +          scopes: access_token.scopes +        } + +        access_token +        |> revoke_access_token() +        |> create_access_token(token_params) +      end) + +    case result do +      {:ok, {:error, reason}} -> {:error, reason} +      {:ok, {:ok, token}} -> {:ok, token} +      {:error, reason} -> {:error, reason} +    end +  end + +  defp revoke_access_token(token) do +    Revoke.revoke(token) +  end + +  defp create_access_token({:error, error}, _), do: {:error, error} + +  defp create_access_token({:ok, token}, %{app: app, user: user} = token_params) do +    Token.create_token(app, user, add_refresh_token(token_params, token.refresh_token)) +  end + +  defp add_refresh_token(params, token) do +    case Config.get([:oauth2, :issue_new_refresh_token], false) do +      true -> Map.put(params, :refresh_token, token) +      false -> params +    end +  end +end diff --git a/lib/pleroma/web/oauth/token/strategy/revoke.ex b/lib/pleroma/web/oauth/token/strategy/revoke.ex new file mode 100644 index 000000000..dea63ca54 --- /dev/null +++ b/lib/pleroma/web/oauth/token/strategy/revoke.ex @@ -0,0 +1,22 @@ +defmodule Pleroma.Web.OAuth.Token.Strategy.Revoke do +  @moduledoc """ +  Functions for dealing with revocation. +  """ + +  alias Pleroma.Repo +  alias Pleroma.Web.OAuth.App +  alias Pleroma.Web.OAuth.Token + +  @doc "Finds and revokes access token for app and by token" +  @spec revoke(App.t(), map()) :: {:ok, Token.t()} | {:error, :not_found | Ecto.Changeset.t()} +  def revoke(%App{} = app, %{"token" => token} = _attrs) do +    with {:ok, token} <- Token.get_by_token(app, token), +         do: revoke(token) +  end + +  @doc "Revokes access token" +  @spec revoke(Token.t()) :: {:ok, Token.t()} | {:error, Ecto.Changeset.t()} +  def revoke(%Token{} = token) do +    Repo.delete(token) +  end +end diff --git a/lib/pleroma/web/oauth/token/utils.ex b/lib/pleroma/web/oauth/token/utils.ex new file mode 100644 index 000000000..a81560a1c --- /dev/null +++ b/lib/pleroma/web/oauth/token/utils.ex @@ -0,0 +1,30 @@ +defmodule Pleroma.Web.OAuth.Token.Utils do +  @moduledoc """ +  Auxiliary functions for dealing with tokens. +  """ + +  @doc "convert token inserted_at to unix timestamp" +  def format_created_at(%{inserted_at: inserted_at} = _token) do +    inserted_at +    |> DateTime.from_naive!("Etc/UTC") +    |> DateTime.to_unix() +  end + +  @doc false +  @spec generate_token(keyword()) :: binary() +  def generate_token(opts \\ []) do +    opts +    |> Keyword.get(:size, 32) +    |> :crypto.strong_rand_bytes() +    |> Base.url_encode64(padding: false) +  end + +  # XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be +  # decoding it.  Investigate sometime. +  def fix_padding(token) do +    token +    |> URI.decode() +    |> Base.url_decode64!(padding: false) +    |> Base.url_encode64(padding: false) +  end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 9b833bc48..2b2e21c48 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -276,6 +276,9 @@ defmodule Pleroma.Web.Router do        get("/suggestions", MastodonAPIController, :suggestions) +      get("/conversations", MastodonAPIController, :conversations) +      post("/conversations/:id/read", MastodonAPIController, :conversation_read) +        get("/endorsements", MastodonAPIController, :empty_array)        get("/pleroma/flavour", MastodonAPIController, :get_flavour) diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index 72eaf2084..133decfc4 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.Streamer do    use GenServer    require Logger    alias Pleroma.Activity +  alias Pleroma.Conversation.Participation    alias Pleroma.Notification    alias Pleroma.Object    alias Pleroma.User @@ -71,6 +72,15 @@ defmodule Pleroma.Web.Streamer do      {:noreply, topics}    end +  def handle_cast(%{action: :stream, topic: "participation", item: participation}, topics) do +    user_topic = "direct:#{participation.user_id}" +    Logger.debug("Trying to push a conversation participation to #{user_topic}\n\n") + +    push_to_socket(topics, user_topic, participation) + +    {:noreply, topics} +  end +    def handle_cast(%{action: :stream, topic: "list", item: item}, topics) do      # filter the recipient list if the activity is not public, see #270.      recipient_lists = @@ -192,6 +202,19 @@ defmodule Pleroma.Web.Streamer do      |> Jason.encode!()    end +  def represent_conversation(%Participation{} = participation) do +    %{ +      event: "conversation", +      payload: +        Pleroma.Web.MastodonAPI.ConversationView.render("participation.json", %{ +          participation: participation, +          user: participation.user +        }) +        |> Jason.encode!() +    } +    |> Jason.encode!() +  end +    def push_to_socket(topics, topic, %Activity{data: %{"type" => "Announce"}} = item) do      Enum.each(topics[topic] || [], fn socket ->        # Get the current user so we have up-to-date blocks etc. @@ -214,6 +237,12 @@ defmodule Pleroma.Web.Streamer do      end)    end +  def push_to_socket(topics, topic, %Participation{} = participation) do +    Enum.each(topics[topic] || [], fn socket -> +      send(socket.transport_pid, {:text, represent_conversation(participation)}) +    end) +  end +    def push_to_socket(topics, topic, %Activity{          data: %{"type" => "Delete", "deleted_activity_id" => deleted_activity_id}        }) do diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 1122e6c5d..c03f8ab3a 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -352,7 +352,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    def delete_account(%{assigns: %{user: user}} = conn, params) do      case CommonAPI.Utils.confirm_current_password(user, params["password"]) do        {:ok, user} -> -        Task.start(fn -> User.delete(user) end) +        User.delete(user)          json(conn, %{status: "success"})        {:error, msg} -> diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index adeac6f3c..3a7774647 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -293,7 +293,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do    end    def get_external_profile(for_user, uri) do -    with %User{} = user <- User.get_or_fetch(uri) do +    with {:ok, %User{} = user} <- User.get_or_fetch(uri) do        {:ok, UserView.render("show.json", %{user: user, for: for_user})}      else        _e -> diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 79ed9dad2..21e6c555a 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    alias Ecto.Changeset    alias Pleroma.Activity +  alias Pleroma.Formatter    alias Pleroma.Notification    alias Pleroma.Object    alias Pleroma.Repo @@ -181,6 +182,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do        |> Map.put("blocking_user", user)        |> Map.put("user", user)        |> Map.put(:visibility, "direct") +      |> Map.put(:order, :desc)      activities =        ActivityPub.fetch_activities_query([user.ap_id], params) @@ -653,7 +655,22 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    defp parse_profile_bio(user, params) do      if bio = params["description"] do -      Map.put(params, "bio", User.parse_bio(bio, user)) +      emojis_text = (params["description"] || "") <> " " <> (params["name"] || "") + +      emojis = +        ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text)) +        |> Enum.dedup() + +      user_info = +        user.info +        |> Map.put( +          "emoji", +          emojis +        ) + +      params +      |> Map.put("bio", User.parse_bio(bio, user)) +      |> Map.put("info", user_info)      else        params      end diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex index ea015b8f0..f0a4ddbd3 100644 --- a/lib/pleroma/web/twitter_api/views/user_view.ex +++ b/lib/pleroma/web/twitter_api/views/user_view.ex @@ -67,6 +67,13 @@ defmodule Pleroma.Web.TwitterAPI.UserView do          {String.trim(name, ":"), url}        end) +    emoji = Enum.dedup(emoji ++ user.info.emoji) + +    description_html = +      (user.bio || "") +      |> HTML.filter_tags(User.html_filter_policy(for_user)) +      |> Formatter.emojify(emoji) +      # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.      # For example: [{"name": "Pronoun", "value": "she/her"}, …]      fields = @@ -78,7 +85,7 @@ defmodule Pleroma.Web.TwitterAPI.UserView do        %{          "created_at" => user.inserted_at |> Utils.format_naive_asctime(),          "description" => HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")), -        "description_html" => HTML.filter_tags(user.bio, User.html_filter_policy(for_user)), +        "description_html" => description_html,          "favourites_count" => 0,          "followers_count" => user_info[:follower_count],          "following" => following, | 
