diff options
Diffstat (limited to 'lib')
38 files changed, 982 insertions, 349 deletions
diff --git a/lib/healthcheck.ex b/lib/healthcheck.ex new file mode 100644 index 000000000..646fb3b9d --- /dev/null +++ b/lib/healthcheck.ex @@ -0,0 +1,60 @@ +defmodule Pleroma.Healthcheck do +  @moduledoc """ +  Module collects metrics about app and assign healthy status. +  """ +  alias Pleroma.Healthcheck +  alias Pleroma.Repo + +  defstruct pool_size: 0, +            active: 0, +            idle: 0, +            memory_used: 0, +            healthy: true + +  @type t :: %__MODULE__{ +          pool_size: non_neg_integer(), +          active: non_neg_integer(), +          idle: non_neg_integer(), +          memory_used: number(), +          healthy: boolean() +        } + +  @spec system_info() :: t() +  def system_info do +    %Healthcheck{ +      memory_used: Float.round(:erlang.memory(:total) / 1024 / 1024, 2) +    } +    |> assign_db_info() +    |> check_health() +  end + +  defp assign_db_info(healthcheck) do +    database = Application.get_env(:pleroma, Repo)[:database] + +    query = +      "select state, count(pid) from pg_stat_activity where datname = '#{database}' group by state;" + +    result = Repo.query!(query) +    pool_size = Application.get_env(:pleroma, Repo)[:pool_size] + +    db_info = +      Enum.reduce(result.rows, %{active: 0, idle: 0}, fn [state, cnt], states -> +        if state == "active" do +          Map.put(states, :active, states.active + cnt) +        else +          Map.put(states, :idle, states.idle + cnt) +        end +      end) +      |> Map.put(:pool_size, pool_size) + +    Map.merge(healthcheck, db_info) +  end + +  @spec check_health(Healthcheck.t()) :: Healthcheck.t() +  def check_health(%{pool_size: pool_size, active: active} = check) +      when active >= pool_size do +    %{check | healthy: false} +  end + +  def check_health(check), do: check +end diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex new file mode 100644 index 000000000..cced73226 --- /dev/null +++ b/lib/mix/tasks/pleroma/emoji.ex @@ -0,0 +1,293 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.Emoji do +  use Mix.Task + +  @shortdoc "Manages emoji packs" +  @moduledoc """ +  Manages emoji packs + +  ## ls-packs + +      mix pleroma.emoji ls-packs [OPTION...] + +  Lists the emoji packs and metadata specified in the manifest. + +  ### Options + +  - `-m, --manifest PATH/URL` - path to a custom manifest, it can +    either be an URL starting with `http`, in that case the +    manifest will be fetched from that address, or a local path + +  ## get-packs + +      mix pleroma.emoji get-packs [OPTION...] PACKS + +  Fetches, verifies and installs the specified PACKS from the +  manifest into the `STATIC-DIR/emoji/PACK-NAME` + +  ### Options + +  - `-m, --manifest PATH/URL` - same as ls-packs + +  ## gen-pack + +      mix pleroma.emoji gen-pack PACK-URL + +  Creates a new manifest entry and a file list from the specified +  remote pack file. Currently, only .zip archives are recognized +  as remote pack files and packs are therefore assumed to be zip +  archives. This command is intended to run interactively and will +  first ask you some basic questions about the pack, then download +  the remote file and generate an SHA256 checksum for it, then +  generate an emoji file list for you. + +  The manifest entry will either be written to a newly created +  `index.json` file or appended to the existing one, *replacing* +  the old pack with the same name if it was in the file previously. + +  The file list will be written to the file specified previously, +  *replacing* that file. You _should_ check that the file list doesn't +  contain anything you don't need in the pack, that is, anything that is +  not an emoji (the whole pack is downloaded, but only emoji files +  are extracted). +  """ + +  @default_manifest Pleroma.Config.get!([:emoji, :default_manifest]) + +  def run(["ls-packs" | args]) do +    Application.ensure_all_started(:hackney) + +    {options, [], []} = parse_global_opts(args) + +    manifest = +      fetch_manifest(if options[:manifest], do: options[:manifest], else: @default_manifest) + +    Enum.each(manifest, fn {name, info} -> +      to_print = [ +        {"Name", name}, +        {"Homepage", info["homepage"]}, +        {"Description", info["description"]}, +        {"License", info["license"]}, +        {"Source", info["src"]} +      ] + +      for {param, value} <- to_print do +        IO.puts(IO.ANSI.format([:bright, param, :normal, ": ", value])) +      end + +      # A newline +      IO.puts("") +    end) +  end + +  def run(["get-packs" | args]) do +    Application.ensure_all_started(:hackney) + +    {options, pack_names, []} = parse_global_opts(args) + +    manifest_url = if options[:manifest], do: options[:manifest], else: @default_manifest + +    manifest = fetch_manifest(manifest_url) + +    for pack_name <- pack_names do +      if Map.has_key?(manifest, pack_name) do +        pack = manifest[pack_name] +        src_url = pack["src"] + +        IO.puts( +          IO.ANSI.format([ +            "Downloading ", +            :bright, +            pack_name, +            :normal, +            " from ", +            :underline, +            src_url +          ]) +        ) + +        binary_archive = Tesla.get!(src_url).body +        archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16() + +        sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright] + +        if archive_sha == String.upcase(pack["src_sha256"]) do +          IO.puts(IO.ANSI.format(sha_status_text ++ [:green, "OK"])) +        else +          IO.puts(IO.ANSI.format(sha_status_text ++ [:red, "BAD"])) + +          raise "Bad SHA256 for #{pack_name}" +        end + +        # The url specified in files should be in the same directory +        files_url = Path.join(Path.dirname(manifest_url), pack["files"]) + +        IO.puts( +          IO.ANSI.format([ +            "Fetching the file list for ", +            :bright, +            pack_name, +            :normal, +            " from ", +            :underline, +            files_url +          ]) +        ) + +        files = Tesla.get!(files_url).body |> Poison.decode!() + +        IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name])) + +        pack_path = +          Path.join([ +            Pleroma.Config.get!([:instance, :static_dir]), +            "emoji", +            pack_name +          ]) + +        files_to_unzip = +          Enum.map( +            files, +            fn {_, f} -> to_charlist(f) end +          ) + +        {:ok, _} = +          :zip.unzip(binary_archive, +            cwd: pack_path, +            file_list: files_to_unzip +          ) + +        IO.puts(IO.ANSI.format(["Writing emoji.txt for ", :bright, pack_name])) + +        emoji_txt_str = +          Enum.map( +            files, +            fn {shortcode, path} -> +              emojo_path = Path.join("/emoji/#{pack_name}", path) +              "#{shortcode}, #{emojo_path}" +            end +          ) +          |> Enum.join("\n") + +        File.write!(Path.join(pack_path, "emoji.txt"), emoji_txt_str) +      else +        IO.puts(IO.ANSI.format([:bright, :red, "No pack named \"#{pack_name}\" found"])) +      end +    end +  end + +  def run(["gen-pack", src]) do +    Application.ensure_all_started(:hackney) + +    proposed_name = Path.basename(src) |> Path.rootname() +    name = String.trim(IO.gets("Pack name [#{proposed_name}]: ")) +    # If there's no name, use the default one +    name = if String.length(name) > 0, do: name, else: proposed_name + +    license = String.trim(IO.gets("License: ")) +    homepage = String.trim(IO.gets("Homepage: ")) +    description = String.trim(IO.gets("Description: ")) + +    proposed_files_name = "#{name}.json" +    files_name = String.trim(IO.gets("Save file list to [#{proposed_files_name}]: ")) +    files_name = if String.length(files_name) > 0, do: files_name, else: proposed_files_name + +    default_exts = [".png", ".gif"] +    default_exts_str = Enum.join(default_exts, " ") + +    exts = +      String.trim( +        IO.gets("Emoji file extensions (separated with spaces) [#{default_exts_str}]: ") +      ) + +    exts = +      if String.length(exts) > 0 do +        String.split(exts, " ") +        |> Enum.filter(fn e -> e |> String.trim() |> String.length() > 0 end) +      else +        default_exts +      end + +    IO.puts("Downloading the pack and generating SHA256") + +    binary_archive = Tesla.get!(src).body +    archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16() + +    IO.puts("SHA256 is #{archive_sha}") + +    pack_json = %{ +      name => %{ +        license: license, +        homepage: homepage, +        description: description, +        src: src, +        src_sha256: archive_sha, +        files: files_name +      } +    } + +    tmp_pack_dir = Path.join(System.tmp_dir!(), "emoji-pack-#{name}") + +    {:ok, _} = +      :zip.unzip( +        binary_archive, +        cwd: tmp_pack_dir +      ) + +    emoji_map = Pleroma.Emoji.make_shortcode_to_file_map(tmp_pack_dir, exts) + +    File.write!(files_name, Poison.encode!(emoji_map, pretty: true)) + +    IO.puts(""" + +    #{files_name} has been created and contains the list of all found emojis in the pack. +    Please review the files in the remove those not needed. +    """) + +    if File.exists?("index.json") do +      existing_data = File.read!("index.json") |> Poison.decode!() + +      File.write!( +        "index.json", +        Poison.encode!( +          Map.merge( +            existing_data, +            pack_json +          ), +          pretty: true +        ) +      ) + +      IO.puts("index.json file has been update with the #{name} pack") +    else +      File.write!("index.json", Poison.encode!(pack_json, pretty: true)) + +      IO.puts("index.json has been created with the #{name} pack") +    end +  end + +  defp fetch_manifest(from) do +    Poison.decode!( +      if String.starts_with?(from, "http") do +        Tesla.get!(from).body +      else +        File.read!(from) +      end +    ) +  end + +  defp parse_global_opts(args) do +    OptionParser.parse( +      args, +      strict: [ +        manifest: :string +      ], +      aliases: [ +        m: :manifest +      ] +    ) +  end +end diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 441168df2..b396ff0de 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -162,7 +162,7 @@ defmodule Mix.Tasks.Pleroma.User do    def run(["rm", nickname]) do      Common.start_pleroma() -    with %User{local: true} = user <- User.get_by_nickname(nickname) do +    with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do        User.delete(user)        Mix.shell().info("User #{nickname} deleted.")      else @@ -174,7 +174,7 @@ defmodule Mix.Tasks.Pleroma.User do    def run(["toggle_activated", nickname]) do      Common.start_pleroma() -    with %User{} = user <- User.get_by_nickname(nickname) do +    with %User{} = user <- User.get_cached_by_nickname(nickname) do        {:ok, user} = User.deactivate(user, !user.info.deactivated)        Mix.shell().info( @@ -189,7 +189,7 @@ defmodule Mix.Tasks.Pleroma.User do    def run(["reset_password", nickname]) do      Common.start_pleroma() -    with %User{local: true} = user <- User.get_by_nickname(nickname), +    with %User{local: true} = user <- User.get_cached_by_nickname(nickname),           {:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do        Mix.shell().info("Generated password reset token for #{user.nickname}") @@ -211,14 +211,14 @@ defmodule Mix.Tasks.Pleroma.User do    def run(["unsubscribe", nickname]) do      Common.start_pleroma() -    with %User{} = user <- User.get_by_nickname(nickname) do +    with %User{} = user <- User.get_cached_by_nickname(nickname) do        Mix.shell().info("Deactivating #{user.nickname}")        User.deactivate(user)        {:ok, friends} = User.get_friends(user)        Enum.each(friends, fn friend -> -        user = User.get_by_id(user.id) +        user = User.get_cached_by_id(user.id)          Mix.shell().info("Unsubscribing #{friend.nickname} from #{user.nickname}")          User.unfollow(user, friend) @@ -226,7 +226,7 @@ defmodule Mix.Tasks.Pleroma.User do        :timer.sleep(500) -      user = User.get_by_id(user.id) +      user = User.get_cached_by_id(user.id)        if Enum.empty?(user.following) do          Mix.shell().info("Successfully unsubscribed all followers from #{user.nickname}") @@ -250,7 +250,7 @@ defmodule Mix.Tasks.Pleroma.User do          ]        ) -    with %User{local: true} = user <- User.get_by_nickname(nickname) do +    with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do        user =          case Keyword.get(options, :moderator) do            nil -> user @@ -277,7 +277,7 @@ defmodule Mix.Tasks.Pleroma.User do    def run(["tag", nickname | tags]) do      Common.start_pleroma() -    with %User{} = user <- User.get_by_nickname(nickname) do +    with %User{} = user <- User.get_cached_by_nickname(nickname) do        user = user |> User.tag(tags)        Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}") @@ -290,7 +290,7 @@ defmodule Mix.Tasks.Pleroma.User do    def run(["untag", nickname | tags]) do      Common.start_pleroma() -    with %User{} = user <- User.get_by_nickname(nickname) do +    with %User{} = user <- User.get_cached_by_nickname(nickname) do        user = user |> User.untag(tags)        Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}") @@ -379,7 +379,7 @@ defmodule Mix.Tasks.Pleroma.User do    def run(["delete_activities", nickname]) do      Common.start_pleroma() -    with %User{local: true} = user <- User.get_by_nickname(nickname) do +    with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do        User.delete_user_activities(user)        Mix.shell().info("User #{nickname} statuses deleted.")      else diff --git a/lib/pleroma/PasswordResetToken.ex b/lib/pleroma/PasswordResetToken.ex index 7afbc8751..f31ea5bc5 100644 --- a/lib/pleroma/PasswordResetToken.ex +++ b/lib/pleroma/PasswordResetToken.ex @@ -39,7 +39,7 @@ defmodule Pleroma.PasswordResetToken do    def reset_password(token, data) do      with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}), -         %User{} = user <- User.get_by_id(token.user_id), +         %User{} = user <- User.get_cached_by_id(token.user_id),           {:ok, _user} <- User.reset_password(user, data),           {:ok, token} <- Repo.update(used_changeset(token)) do        {:ok, token} diff --git a/lib/pleroma/bookmark.ex b/lib/pleroma/bookmark.ex new file mode 100644 index 000000000..7f8fd43b6 --- /dev/null +++ b/lib/pleroma/bookmark.ex @@ -0,0 +1,60 @@ +defmodule Pleroma.Bookmark do +  use Ecto.Schema + +  import Ecto.Changeset +  import Ecto.Query + +  alias Pleroma.Activity +  alias Pleroma.Bookmark +  alias Pleroma.FlakeId +  alias Pleroma.Repo +  alias Pleroma.User + +  @type t :: %__MODULE__{} + +  schema "bookmarks" do +    belongs_to(:user, User, type: FlakeId) +    belongs_to(:activity, Activity, type: FlakeId) + +    timestamps() +  end + +  @spec create(FlakeId.t(), FlakeId.t()) :: {:ok, Bookmark.t()} | {:error, Changeset.t()} +  def create(user_id, activity_id) do +    attrs = %{ +      user_id: user_id, +      activity_id: activity_id +    } + +    %Bookmark{} +    |> cast(attrs, [:user_id, :activity_id]) +    |> validate_required([:user_id, :activity_id]) +    |> unique_constraint(:activity_id, name: :bookmarks_user_id_activity_id_index) +    |> Repo.insert() +  end + +  @spec for_user_query(FlakeId.t()) :: Ecto.Query.t() +  def for_user_query(user_id) do +    Bookmark +    |> where(user_id: ^user_id) +    |> join(:inner, [b], activity in assoc(b, :activity)) +    |> preload([b, a], activity: a) +  end + +  def get(user_id, activity_id) do +    Bookmark +    |> where(user_id: ^user_id) +    |> where(activity_id: ^activity_id) +    |> Repo.one() +  end + +  @spec destroy(FlakeId.t(), FlakeId.t()) :: {:ok, Bookmark.t()} | {:error, Changeset.t()} +  def destroy(user_id, activity_id) do +    from(b in Bookmark, +      where: b.user_id == ^user_id, +      where: b.activity_id == ^activity_id +    ) +    |> Repo.one() +    |> Repo.delete() +  end +end diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex index 87c7f2cec..6390cce4c 100644 --- a/lib/pleroma/emoji.ex +++ b/lib/pleroma/emoji.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Emoji do    @moduledoc """    The emojis are loaded from: -    * the built-in Finmojis (if enabled in configuration), +    * emoji packs in INSTANCE-DIR/emoji      * the files: `config/emoji.txt` and `config/custom_emoji.txt`      * glob paths, nested folder is used as tag name for grouping e.g. priv/static/emoji/custom/nested_folder @@ -14,6 +14,8 @@ defmodule Pleroma.Emoji do    """    use GenServer +  require Logger +    @type pattern :: Regex.t() | module() | String.t()    @type patterns :: pattern() | [pattern()]    @type group_patterns :: keyword(patterns()) @@ -79,95 +81,94 @@ defmodule Pleroma.Emoji do    end    defp load do -    finmoji_enabled = Keyword.get(Application.get_env(:pleroma, :instance), :finmoji_enabled) +    emoji_dir_path = +      Path.join( +        Pleroma.Config.get!([:instance, :static_dir]), +        "emoji" +      ) + +    case File.ls(emoji_dir_path) do +      {:error, :enoent} -> +        # The custom emoji directory doesn't exist, +        # don't do anything +        nil + +      {:error, e} -> +        # There was some other error +        Logger.error("Could not access the custom emoji directory #{emoji_dir_path}: #{e}") + +      {:ok, packs} -> +        # Print the packs we've found +        Logger.info("Found emoji packs: #{Enum.join(packs, ", ")}") + +        emojis = +          Enum.flat_map( +            packs, +            fn pack -> load_pack(Path.join(emoji_dir_path, pack)) end +          ) + +        true = :ets.insert(@ets, emojis) +    end + +    # Compat thing for old custom emoji handling & default emoji, +    # it should run even if there are no emoji packs      shortcode_globs = Application.get_env(:pleroma, :emoji)[:shortcode_globs] || []      emojis = -      (load_finmoji(finmoji_enabled) ++ -         load_from_file("config/emoji.txt") ++ +      (load_from_file("config/emoji.txt") ++           load_from_file("config/custom_emoji.txt") ++           load_from_globs(shortcode_globs))        |> Enum.reject(fn value -> value == nil end)      true = :ets.insert(@ets, emojis) +      :ok    end -  @finmoji [ -    "a_trusted_friend", -    "alandislands", -    "association", -    "auroraborealis", -    "baby_in_a_box", -    "bear", -    "black_gold", -    "christmasparty", -    "crosscountryskiing", -    "cupofcoffee", -    "education", -    "fashionista_finns", -    "finnishlove", -    "flag", -    "forest", -    "four_seasons_of_bbq", -    "girlpower", -    "handshake", -    "happiness", -    "headbanger", -    "icebreaker", -    "iceman", -    "joulutorttu", -    "kaamos", -    "kalsarikannit_f", -    "kalsarikannit_m", -    "karjalanpiirakka", -    "kicksled", -    "kokko", -    "lavatanssit", -    "losthopes_f", -    "losthopes_m", -    "mattinykanen", -    "meanwhileinfinland", -    "moominmamma", -    "nordicfamily", -    "out_of_office", -    "peacemaker", -    "perkele", -    "pesapallo", -    "polarbear", -    "pusa_hispida_saimensis", -    "reindeer", -    "sami", -    "sauna_f", -    "sauna_m", -    "sauna_whisk", -    "sisu", -    "stuck", -    "suomimainittu", -    "superfood", -    "swan", -    "the_cap", -    "the_conductor", -    "the_king", -    "the_voice", -    "theoriginalsanta", -    "tomoffinland", -    "torillatavataan", -    "unbreakable", -    "waiting", -    "white_nights", -    "woollysocks" -  ] - -  defp load_finmoji(true) do -    Enum.map(@finmoji, fn finmoji -> -      file_name = "/finmoji/128px/#{finmoji}-128.png" -      group = match_extra(@groups, file_name) -      {finmoji, file_name, to_string(group)} -    end) +  defp load_pack(pack_dir) do +    pack_name = Path.basename(pack_dir) + +    emoji_txt = Path.join(pack_dir, "emoji.txt") + +    if File.exists?(emoji_txt) do +      load_from_file(emoji_txt) +    else +      Logger.info( +        "No emoji.txt found for pack \"#{pack_name}\", assuming all .png files are emoji" +      ) + +      make_shortcode_to_file_map(pack_dir, [".png"]) +      |> Enum.map(fn {shortcode, rel_file} -> +        filename = Path.join("/emoji/#{pack_name}", rel_file) + +        {shortcode, filename, [to_string(match_extra(@groups, filename))]} +      end) +    end +  end + +  def make_shortcode_to_file_map(pack_dir, exts) do +    find_all_emoji(pack_dir, exts) +    |> Enum.map(&Path.relative_to(&1, pack_dir)) +    |> Enum.map(fn f -> {f |> Path.basename() |> Path.rootname(), f} end) +    |> Enum.into(%{})    end -  defp load_finmoji(_), do: [] +  def find_all_emoji(dir, exts) do +    Enum.reduce( +      File.ls!(dir), +      [], +      fn f, acc -> +        filepath = Path.join(dir, f) + +        if File.dir?(filepath) do +          acc ++ find_all_emoji(filepath, exts) +        else +          acc ++ [filepath] +        end +      end +    ) +    |> Enum.filter(fn f -> Path.extname(f) in exts end) +  end    defp load_from_file(file) do      if File.exists?(file) do @@ -182,11 +183,11 @@ defmodule Pleroma.Emoji do      |> Stream.map(&String.trim/1)      |> Stream.map(fn line ->        case String.split(line, ~r/,\s*/) do -        [name, file, tags] -> -          {name, file, tags} -          [name, file] -> -          {name, file, to_string(match_extra(@groups, file))} +          {name, file, [to_string(match_extra(@groups, file))]} + +        [name, file | tags] -> +          {name, file, tags}          _ ->            nil @@ -209,7 +210,7 @@ defmodule Pleroma.Emoji do        tag = match_extra(@groups, Path.join("/", Path.relative_to(path, static_path)))        shortcode = Path.basename(path, Path.extname(path))        external_path = Path.join("/", Path.relative_to(path, static_path)) -      {shortcode, external_path, to_string(tag)} +      {shortcode, external_path, [to_string(tag)]}      end)    end diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex index 2ebc5d5f7..1d2e0785c 100644 --- a/lib/pleroma/gopher/server.ex +++ b/lib/pleroma/gopher/server.ex @@ -76,7 +76,7 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do      |> Enum.map(fn activity ->        user = User.get_cached_by_ap_id(activity.data["actor"]) -      object = Object.normalize(activity.data["object"]) +      object = Object.normalize(activity)        like_count = object["like_count"] || 0        announcement_count = object["announcement_count"] || 0 diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index 4b42d8c9b..cf6c0ee0a 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -106,7 +106,14 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do    # links    Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes) -  Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"]) + +  Meta.allow_tag_with_this_attribute_values("a", "class", [ +    "hashtag", +    "u-url", +    "mention", +    "u-url mention", +    "mention u-url" +  ])    Meta.allow_tag_with_this_attribute_values("a", "rel", [      "tag", @@ -115,12 +122,15 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do      "noreferrer"    ]) +  Meta.allow_tag_with_these_attributes("a", ["name", "title"]) +    # paragraphs and linebreaks    Meta.allow_tag_with_these_attributes("br", [])    Meta.allow_tag_with_these_attributes("p", [])    # microformats -  Meta.allow_tag_with_these_attributes("span", ["class"]) +  Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card"]) +  Meta.allow_tag_with_these_attributes("span", [])    # allow inline images for custom emoji    @allow_inline_images Keyword.get(@markup, :allow_inline_images) @@ -155,7 +165,14 @@ defmodule Pleroma.HTML.Scrubber.Default do    Meta.strip_comments()    Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes) -  Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"]) + +  Meta.allow_tag_with_this_attribute_values("a", "class", [ +    "hashtag", +    "u-url", +    "mention", +    "u-url mention", +    "mention u-url" +  ])    Meta.allow_tag_with_this_attribute_values("a", "rel", [      "tag", @@ -164,6 +181,8 @@ defmodule Pleroma.HTML.Scrubber.Default do      "noreferrer"    ]) +  Meta.allow_tag_with_these_attributes("a", ["name", "title"]) +    Meta.allow_tag_with_these_attributes("abbr", ["title"])    Meta.allow_tag_with_these_attributes("b", []) @@ -177,11 +196,13 @@ defmodule Pleroma.HTML.Scrubber.Default do    Meta.allow_tag_with_these_attributes("ol", [])    Meta.allow_tag_with_these_attributes("p", [])    Meta.allow_tag_with_these_attributes("pre", []) -  Meta.allow_tag_with_these_attributes("span", ["class"])    Meta.allow_tag_with_these_attributes("strong", [])    Meta.allow_tag_with_these_attributes("u", [])    Meta.allow_tag_with_these_attributes("ul", []) +  Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card"]) +  Meta.allow_tag_with_these_attributes("span", []) +    @allow_inline_images Keyword.get(@markup, :allow_inline_images)    if @allow_inline_images do diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex index 110be8355..a5b1cad68 100644 --- a/lib/pleroma/list.ex +++ b/lib/pleroma/list.ex @@ -80,7 +80,7 @@ defmodule Pleroma.List do    # Get lists to which the account belongs.    def get_lists_account_belongs(%User{} = owner, account_id) do -    user = User.get_by_id(account_id) +    user = User.get_cached_by_id(account_id)      query =        from( diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index d79f0f563..bf45be961 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -220,7 +220,7 @@ defmodule Pleroma.Notification do    def skip?(:follows, activity, %{info: %{notification_settings: %{"follows" => false}}} = user) do      actor = activity.data["actor"] -    followed = User.get_by_ap_id(actor) +    followed = User.get_cached_by_ap_id(actor)      User.following?(user, followed)    end diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 138e7866f..8d4bcc95e 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -39,7 +39,7 @@ defmodule Pleroma.Object.Fetcher do            Logger.info("Couldn't get object via AP, trying out OStatus fetching...")            case OStatus.fetch_activity_from_url(id) do -            {:ok, [activity | _]} -> {:ok, Object.normalize(activity.data["object"], false)} +            {:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)}              e -> e            end        end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 2509d2366..4417a12dd 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -10,6 +10,7 @@ defmodule Pleroma.User do    alias Comeonin.Pbkdf2    alias Pleroma.Activity +  alias Pleroma.Bookmark    alias Pleroma.Formatter    alias Pleroma.Notification    alias Pleroma.Object @@ -53,9 +54,9 @@ defmodule Pleroma.User do      field(:search_rank, :float, virtual: true)      field(:search_type, :integer, virtual: true)      field(:tags, {:array, :string}, default: []) -    field(:bookmarks, {: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) @@ -270,6 +271,7 @@ defmodule Pleroma.User do    def register(%Ecto.Changeset{} = changeset) do      with {:ok, user} <- Repo.insert(changeset),           {:ok, user} <- autofollow_users(user), +         {:ok, user} <- set_cache(user),           {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),           {:ok, _} <- try_send_confirmation_email(user) do        {:ok, user} @@ -454,10 +456,13 @@ defmodule Pleroma.User do      name = List.last(String.split(ap_id, "/"))      nickname = "#{name}@#{domain}" -    get_by_nickname(nickname) +    get_cached_by_nickname(nickname)    end -  def set_cache(user) do +  def set_cache({:ok, user}), do: set_cache(user) +  def set_cache({:error, err}), do: {:error, err} + +  def set_cache(%User{} = user) do      Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)      Cachex.put(:user_cache, "nickname:#{user.nickname}", user)      Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user)) @@ -545,6 +550,7 @@ defmodule Pleroma.User do          with [_nick, _domain] <- String.split(nickname, "@"),               {:ok, user} <- fetch_by_nickname(nickname) do            if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do +            # TODO turn into job              {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])            end @@ -1003,7 +1009,7 @@ defmodule Pleroma.User do    # helper to handle the block given only an actor's AP id    def block(blocker, %{ap_id: ap_id}) do -    block(blocker, User.get_by_ap_id(ap_id)) +    block(blocker, get_cached_by_ap_id(ap_id))    end    def unblock(blocker, %{ap_id: ap_id}) do @@ -1033,7 +1039,7 @@ defmodule Pleroma.User do    end    def subscribed_to?(user, %{ap_id: ap_id}) do -    with %User{} = target <- User.get_by_ap_id(ap_id) do +    with %User{} = target <- get_cached_by_ap_id(ap_id) do        Enum.member?(target.info.subscribers, user.ap_id)      end    end @@ -1208,7 +1214,7 @@ defmodule Pleroma.User do    end    def get_or_fetch_by_ap_id(ap_id) do -    user = get_by_ap_id(ap_id) +    user = get_cached_by_ap_id(ap_id)      if !is_nil(user) and !User.needs_update?(user) do        user @@ -1231,7 +1237,7 @@ defmodule Pleroma.User do    def get_or_create_instance_user do      relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay" -    if user = get_by_ap_id(relay_uri) do +    if user = get_cached_by_ap_id(relay_uri) do        user      else        changes = @@ -1278,13 +1284,11 @@ defmodule Pleroma.User do    defp blank?(n), do: n    def insert_or_update_user(data) do -    data = -      data -      |> Map.put(:name, blank?(data[:name]) || data[:nickname]) - -    cs = User.remote_user_creation(data) - -    Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname) +    data +    |> Map.put(:name, blank?(data[:name]) || data[:nickname]) +    |> remote_user_creation() +    |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname) +    |> set_cache()    end    def ap_enabled?(%User{local: true}), do: true @@ -1300,8 +1304,8 @@ defmodule Pleroma.User do    # this is because we have synchronous follow APIs and need to simulate them    # with an async handshake    def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do -    with %User{} = a <- User.get_by_id(a.id), -         %User{} = b <- User.get_by_id(b.id) do +    with %User{} = a <- User.get_cached_by_id(a.id), +         %User{} = b <- User.get_cached_by_id(b.id) do        {:ok, a, b}      else        _e -> @@ -1311,8 +1315,8 @@ defmodule Pleroma.User do    def wait_and_refresh(timeout, %User{} = a, %User{} = b) do      with :ok <- :timer.sleep(timeout), -         %User{} = a <- User.get_by_id(a.id), -         %User{} = b <- User.get_by_id(b.id) do +         %User{} = a <- User.get_cached_by_id(a.id), +         %User{} = b <- User.get_cached_by_id(b.id) do        {:ok, a, b}      else        _e -> @@ -1351,7 +1355,7 @@ defmodule Pleroma.User do    end    def tag(nickname, tags) when is_binary(nickname), -    do: tag(User.get_by_nickname(nickname), tags) +    do: tag(get_by_nickname(nickname), tags)    def tag(%User{} = user, tags),      do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags))) @@ -1363,7 +1367,7 @@ defmodule Pleroma.User do    end    def untag(nickname, tags) when is_binary(nickname), -    do: untag(User.get_by_nickname(nickname), tags) +    do: untag(get_by_nickname(nickname), tags)    def untag(%User{} = user, tags),      do: update_tags(user, (user.tags || []) -- normalize_tags(tags)) @@ -1377,22 +1381,6 @@ defmodule Pleroma.User do      updated_user    end -  def bookmark(%User{} = user, status_id) do -    bookmarks = Enum.uniq(user.bookmarks ++ [status_id]) -    update_bookmarks(user, bookmarks) -  end - -  def unbookmark(%User{} = user, status_id) do -    bookmarks = Enum.uniq(user.bookmarks -- [status_id]) -    update_bookmarks(user, bookmarks) -  end - -  def update_bookmarks(%User{} = user, bookmarks) do -    user -    |> change(%{bookmarks: bookmarks}) -    |> update_and_set_cache -  end -    defp normalize_tags(tags) do      [tags]      |> List.flatten() diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index d827293b8..2d360d650 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -40,6 +40,7 @@ defmodule Pleroma.User.Info do      field(:salmon, :string, default: nil)      field(:hide_followers, :boolean, default: false)      field(:hide_follows, :boolean, default: false) +    field(:hide_favorites, :boolean, default: true)      field(:pinned_activities, {:array, :string}, default: [])      field(:flavour, :string, default: nil)      field(:email_notifications, :map, default: %{"digest" => false}) @@ -229,6 +230,7 @@ defmodule Pleroma.User.Info do        :banner,        :hide_follows,        :hide_followers, +      :hide_favorites,        :background,        :show_role      ]) @@ -252,14 +254,6 @@ defmodule Pleroma.User.Info do      cast(info, params, [:confirmation_pending, :confirmation_token])    end -  def mastodon_profile_update(info, params) do -    info -    |> cast(params, [ -      :locked, -      :banner -    ]) -  end -    def mastodon_settings_update(info, settings) do      params = %{settings: settings} diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index e77b2b72d..604ffae7b 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -168,7 +168,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      public = "https://www.w3.org/ns/activitystreams#Public"      if activity.data["type"] in ["Create", "Announce", "Delete"] do -      object = Object.normalize(activity.data["object"]) +      object = Object.normalize(activity)        Pleroma.Web.Streamer.stream("user", activity)        Pleroma.Web.Streamer.stream("list", activity) @@ -197,7 +197,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do          if !Enum.member?(activity.data["cc"] || [], public) &&               !Enum.member?(                 activity.data["to"], -               User.get_by_ap_id(activity.data["actor"]).follower_address +               User.get_cached_by_ap_id(activity.data["actor"]).follower_address               ),             do: Pleroma.Web.Streamer.stream("direct", activity)        end @@ -889,7 +889,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    end    def make_user_from_ap_id(ap_id) do -    if _user = User.get_by_ap_id(ap_id) do +    if _user = User.get_cached_by_ap_id(ap_id) do        Transmogrifier.upgrade_user_from_ap_id(ap_id)      else        with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index a80aa52c6..b1e859d7c 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -438,20 +438,46 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),           %User{} = follower <- User.get_or_fetch_by_ap_id(follower),           {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do -      if not User.locked?(followed) do +      with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]), +           {:user_blocked, false} <- +             {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked}, +           {:user_locked, false} <- {:user_locked, User.locked?(followed)}, +           {:follow, {:ok, follower}} <- {:follow, User.follow(follower, followed)} do          ActivityPub.accept(%{            to: [follower.ap_id],            actor: followed,            object: data,            local: true          }) - -        User.follow(follower, followed) +      else +        {:user_blocked, true} -> +          {:ok, _} = Utils.update_follow_state(activity, "reject") + +          ActivityPub.reject(%{ +            to: [follower.ap_id], +            actor: followed, +            object: data, +            local: true +          }) + +        {:follow, {:error, _}} -> +          {:ok, _} = Utils.update_follow_state(activity, "reject") + +          ActivityPub.reject(%{ +            to: [follower.ap_id], +            actor: followed, +            object: data, +            local: true +          }) + +        {:user_locked, true} -> +          :noop        end        {:ok, activity}      else -      _e -> :error +      _e -> +        :error      end    end @@ -537,7 +563,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do            data        )        when object_type in ["Person", "Application", "Service", "Organization"] do -    with %User{ap_id: ^actor_id} = actor <- User.get_by_ap_id(object["id"]) do +    with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do        {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)        banner = new_user_data[:info]["banner"] @@ -964,7 +990,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    end    def upgrade_user_from_ap_id(ap_id) do -    with %User{local: false} = user <- User.get_by_ap_id(ap_id), +    with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),           {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),           already_ap <- User.ap_enabled?(user),           {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index c436715d5..711f233a6 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    action_fallback(:errors)    def user_delete(conn, %{"nickname" => nickname}) do -    User.get_by_nickname(nickname) +    User.get_cached_by_nickname(nickname)      |> User.delete()      conn @@ -27,8 +27,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    end    def user_follow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do -    with %User{} = follower <- User.get_by_nickname(follower_nick), -         %User{} = followed <- User.get_by_nickname(followed_nick) do +    with %User{} = follower <- User.get_cached_by_nickname(follower_nick), +         %User{} = followed <- User.get_cached_by_nickname(followed_nick) do        User.follow(follower, followed)      end @@ -37,8 +37,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    end    def user_unfollow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do -    with %User{} = follower <- User.get_by_nickname(follower_nick), -         %User{} = followed <- User.get_by_nickname(followed_nick) do +    with %User{} = follower <- User.get_cached_by_nickname(follower_nick), +         %User{} = followed <- User.get_cached_by_nickname(followed_nick) do        User.unfollow(follower, followed)      end @@ -67,7 +67,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    end    def user_show(conn, %{"nickname" => nickname}) do -    with %User{} = user <- User.get_by_nickname(nickname) do +    with %User{} = user <- User.get_cached_by_nickname(nickname) do        conn        |> json(AccountView.render("show.json", %{user: user}))      else @@ -76,7 +76,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    end    def user_toggle_activation(conn, %{"nickname" => nickname}) do -    user = User.get_by_nickname(nickname) +    user = User.get_cached_by_nickname(nickname)      {:ok, updated_user} = User.deactivate(user, !user.info.deactivated) @@ -131,7 +131,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    def right_add(conn, %{"permission_group" => permission_group, "nickname" => nickname})        when permission_group in ["moderator", "admin"] do -    user = User.get_by_nickname(nickname) +    user = User.get_cached_by_nickname(nickname)      info =        %{} @@ -156,7 +156,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    end    def right_get(conn, %{"nickname" => nickname}) do -    user = User.get_by_nickname(nickname) +    user = User.get_cached_by_nickname(nickname)      conn      |> json(%{ @@ -178,7 +178,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do        |> put_status(403)        |> json(%{error: "You can't revoke your own admin status."})      else -      user = User.get_by_nickname(nickname) +      user = User.get_cached_by_nickname(nickname)        info =          %{} @@ -204,7 +204,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    def set_activation_status(conn, %{"nickname" => nickname, "status" => status}) do      with {:ok, status} <- Ecto.Type.cast(:boolean, status), -         %User{} = user <- User.get_by_nickname(nickname), +         %User{} = user <- User.get_cached_by_nickname(nickname),           {:ok, _} <- User.deactivate(user, !status),           do: json_response(conn, :no_content, "")    end @@ -277,7 +277,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    @doc "Get a password reset token (base64 string) for given nickname"    def get_password_reset(conn, %{"nickname" => nickname}) do -    (%User{local: true} = user) = User.get_by_nickname(nickname) +    (%User{local: true} = user) = User.get_cached_by_nickname(nickname)      {:ok, token} = Pleroma.PasswordResetToken.create_token(user)      conn diff --git a/lib/pleroma/web/channels/user_socket.ex b/lib/pleroma/web/channels/user_socket.ex index 6503979a1..8e2759e3b 100644 --- a/lib/pleroma/web/channels/user_socket.ex +++ b/lib/pleroma/web/channels/user_socket.ex @@ -24,7 +24,7 @@ defmodule Pleroma.Web.UserSocket do    def connect(%{"token" => token}, socket) do      with true <- Pleroma.Config.get([:chat, :enabled]),           {:ok, user_id} <- Phoenix.Token.verify(socket, "user socket", token, max_age: 84_600), -         %User{} = user <- Pleroma.User.get_by_id(user_id) do +         %User{} = user <- Pleroma.User.get_cached_by_id(user_id) do        {:ok, assign(socket, :user_name, user.nickname)}      else        _e -> :error diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 6458a3449..ecd183110 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -4,6 +4,7 @@  defmodule Pleroma.Web.CommonAPI do    alias Pleroma.Activity +  alias Pleroma.Bookmark    alias Pleroma.Formatter    alias Pleroma.Object    alias Pleroma.ThreadMute @@ -282,9 +283,18 @@ defmodule Pleroma.Web.CommonAPI do      end    end +  def bookmarked?(user, activity) do +    with %Bookmark{} <- Bookmark.get(user.id, activity.id) do +      true +    else +      _ -> +        false +    end +  end +    def report(user, data) do      with {:account_id, %{"account_id" => account_id}} <- {:account_id, data}, -         {:account, %User{} = account} <- {:account, User.get_by_id(account_id)}, +         {:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)},           {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),           {:ok, statuses} <- get_report_statuses(account, data),           {:ok, activity} <- diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 25f498fcb..1dfe50b40 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -183,6 +183,18 @@ defmodule Pleroma.Web.CommonAPI.Utils do    end    @doc """ +  Formatting text as BBCode. +  """ +  def format_input(text, "text/bbcode", options) do +    text +    |> String.replace(~r/\r/, "") +    |> Formatter.html_escape("text/plain") +    |> BBCode.to_html() +    |> (fn {:ok, html} -> html end).() +    |> Formatter.linkify(options) +  end + +  @doc """    Formatting text to html.    """    def format_input(text, "text/html", options) do @@ -226,7 +238,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do      }      if in_reply_to do -      in_reply_to_object = Object.normalize(in_reply_to.data["object"]) +      in_reply_to_object = Object.normalize(in_reply_to)        object        |> Map.put("inReplyTo", in_reply_to_object.data["id"]) @@ -284,7 +296,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do    end    def confirm_current_password(user, password) do -    with %User{local: true} = db_user <- User.get_by_id(user.id), +    with %User{local: true} = db_user <- User.get_cached_by_id(user.id),           true <- Pbkdf2.checkpw(password, db_user.password_hash) do        {:ok, db_user}      else diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex index 1b4deb6dc..29e178ba9 100644 --- a/lib/pleroma/web/federator/federator.ex +++ b/lib/pleroma/web/federator/federator.ex @@ -186,7 +186,7 @@ defmodule Pleroma.Web.Federator do    end    def ap_enabled_actor(id) do -    user = User.get_by_ap_id(id) +    user = User.get_cached_by_ap_id(id)      if User.ap_enabled?(user) do        {:ok, user} diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index 382f07e6b..3a3ec7c2a 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -7,6 +7,31 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do    alias Pleroma.Pagination    alias Pleroma.ScheduledActivity    alias Pleroma.User +  alias Pleroma.Web.CommonAPI + +  def follow(follower, followed, params \\ %{}) do +    options = cast_params(params) +    reblogs = options[:reblogs] + +    result = +      if not User.following?(follower, followed) do +        CommonAPI.follow(follower, followed) +      else +        {:ok, follower, followed, nil} +      end + +    with {:ok, follower, followed, _} <- result do +      reblogs +      |> case do +        false -> CommonAPI.hide_reblogs(follower, followed) +        _ -> CommonAPI.show_reblogs(follower, followed) +      end +      |> case do +        {:ok, follower} -> {:ok, follower} +        _ -> {:ok, follower} +      end +    end +  end    def get_followers(user, params \\ %{}) do      user @@ -37,7 +62,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do    defp cast_params(params) do      param_types = %{ -      exclude_types: {:array, :string} +      exclude_types: {:array, :string}, +      reblogs: :boolean      }      changeset = cast({%{}, param_types}, params, Map.keys(param_types)) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 3916d7c41..811a45c79 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    use Pleroma.Web, :controller    alias Ecto.Changeset    alias Pleroma.Activity +  alias Pleroma.Bookmark    alias Pleroma.Config    alias Pleroma.Filter    alias Pleroma.Notification @@ -35,7 +36,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    alias Pleroma.Web.OAuth.Authorization    alias Pleroma.Web.OAuth.Token -  import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2] +  alias Pleroma.Web.ControllerHelper    import Ecto.Query    require Logger @@ -46,7 +47,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    action_fallback(:errors)    def create_app(conn, params) do -    scopes = oauth_scopes(params, ["read"]) +    scopes = ControllerHelper.oauth_scopes(params, ["read"])      app_attrs =        params @@ -96,8 +97,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        end)      info_params = -      %{} -      |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end) +      [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role] +      |> Enum.reduce(%{}, fn key, acc -> +        add_if_present(acc, params, to_string(key), key, fn value -> +          {:ok, ControllerHelper.truthy_param?(value)} +        end) +      end) +      |> add_if_present(params, "default_scope", :default_scope)        |> add_if_present(params, "header", :banner, fn value ->          with %Plug.Upload{} <- value,               {:ok, object} <- ActivityPub.upload(value, type: :banner) do @@ -107,7 +113,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do          end        end) -    info_cng = User.Info.mastodon_profile_update(user.info, info_params) +    info_cng = User.Info.profile_update(user.info, info_params)      with changeset <- User.update_changeset(user, user_params),           changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng), @@ -190,7 +196,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do          "static_url" => url,          "visible_in_picker" => true,          "url" => url, -        "tags" => String.split(tags, ",") +        "tags" => tags        }      end)    end @@ -279,6 +285,8 @@ 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) @@ -297,6 +305,8 @@ 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) @@ -304,7 +314,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do -    with %User{} = user <- User.get_by_id(params["id"]) do +    with %User{} = user <- User.get_cached_by_id(params["id"]), +         reading_user <- Repo.preload(reading_user, :bookmarks) do        activities = ActivityPub.fetch_user_activities(user, reading_user, params)        conn @@ -331,6 +342,8 @@ 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) @@ -338,8 +351,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do -    with %Activity{} = activity <- Activity.get_by_id(id), +    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}) @@ -487,7 +502,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do -    with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) 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}) @@ -496,7 +514,9 @@ 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(id) do +         %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}) @@ -544,10 +564,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do      with %Activity{} = activity <- Activity.get_by_id_with_object(id), -         %Object{} = object <- Object.normalize(activity), -         %User{} = user <- User.get_by_nickname(user.nickname), +         %User{} = user <- User.get_cached_by_nickname(user.nickname),           true <- Visibility.visible_for_user?(activity, user), -         {:ok, user} <- User.bookmark(user, object.data["id"]) do +         {: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}) @@ -556,10 +577,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do      with %Activity{} = activity <- Activity.get_by_id_with_object(id), -         %Object{} = object <- Object.normalize(activity), -         %User{} = user <- User.get_by_nickname(user.nickname), +         %User{} = user <- User.get_cached_by_nickname(user.nickname),           true <- Visibility.visible_for_user?(activity, user), -         {:ok, user} <- User.unbookmark(user, object.data["id"]) do +         {: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}) @@ -749,7 +771,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do -    with %User{} = user <- User.get_by_id(id), +    with %User{} = user <- User.get_cached_by_id(id),           followers <- MastodonAPI.get_followers(user, params) do        followers =          cond do @@ -766,7 +788,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do -    with %User{} = user <- User.get_by_id(id), +    with %User{} = user <- User.get_cached_by_id(id),           followers <- MastodonAPI.get_friends(user, params) do        followers =          cond do @@ -791,7 +813,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do -    with %User{} = follower <- User.get_by_id(id), +    with %User{} = follower <- User.get_cached_by_id(id),           {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do        conn        |> put_view(AccountView) @@ -805,7 +827,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do -    with %User{} = follower <- User.get_by_id(id), +    with %User{} = follower <- User.get_cached_by_id(id),           {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do        conn        |> put_view(AccountView) @@ -821,8 +843,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do      with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},           {_, true} <- {:followed, follower.id != followed.id}, -         false <- User.following?(follower, followed), -         {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do +         {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do        conn        |> put_view(AccountView)        |> render("relationship.json", %{user: follower, target: followed}) @@ -830,19 +851,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        {:followed, _} ->          {:error, :not_found} -      true -> -        followed = User.get_cached_by_id(id) - -        {:ok, follower} = -          case conn.params["reblogs"] do -            true -> CommonAPI.show_reblogs(follower, followed) -            false -> CommonAPI.hide_reblogs(follower, followed) -          end - -        conn -        |> put_view(AccountView) -        |> render("relationship.json", %{user: follower, target: followed}) -        {:error, message} ->          conn          |> put_resp_content_type("application/json") @@ -885,7 +893,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do -    with %User{} = muted <- User.get_by_id(id), +    with %User{} = muted <- User.get_cached_by_id(id),           {:ok, muter} <- User.mute(muter, muted) do        conn        |> put_view(AccountView) @@ -899,7 +907,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do -    with %User{} = muted <- User.get_by_id(id), +    with %User{} = muted <- User.get_cached_by_id(id),           {:ok, muter} <- User.unmute(muter, muted) do        conn        |> put_view(AccountView) @@ -920,7 +928,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do -    with %User{} = blocked <- User.get_by_id(id), +    with %User{} = blocked <- User.get_cached_by_id(id),           {:ok, blocker} <- User.block(blocker, blocked),           {:ok, _activity} <- ActivityPub.block(blocker, blocked) do        conn @@ -935,7 +943,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do -    with %User{} = blocked <- User.get_by_id(id), +    with %User{} = blocked <- User.get_cached_by_id(id),           {:ok, blocker} <- User.unblock(blocker, blocked),           {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do        conn @@ -1094,21 +1102,65 @@ 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)      |> render("index.json", %{activities: activities, for: user, as: :activity})    end -  def bookmarks(%{assigns: %{user: user}} = conn, _) do -    user = User.get_by_id(user.id) +  def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do +    with %User{} = user <- User.get_by_id(id), +         false <- user.info.hide_favorites do +      params = +        params +        |> Map.put("type", "Create") +        |> Map.put("favorited_by", user.ap_id) +        |> Map.put("blocking_user", for_user) + +      recipients = +        if for_user do +          ["https://www.w3.org/ns/activitystreams#Public"] ++ +            [for_user.ap_id | for_user.following] +        else +          ["https://www.w3.org/ns/activitystreams#Public"] +        end + +      activities = +        recipients +        |> ActivityPub.fetch_activities(params) +        |> Enum.reverse() + +      conn +      |> add_link_headers(:favourites, activities) +      |> put_view(StatusView) +      |> render("index.json", %{activities: activities, for: for_user, as: :activity}) +    else +      nil -> +        {:error, :not_found} + +      true -> +        conn +        |> put_status(403) +        |> json(%{error: "Can't get favorites"}) +    end +  end + +  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) +      |> Pagination.fetch_paginated(params)      activities = -      user.bookmarks -      |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end) -      |> Enum.reverse() +      bookmarks +      |> Enum.map(fn b -> b.activity end)      conn +    |> add_link_headers(:bookmarks, bookmarks)      |> put_view(StatusView)      |> render("index.json", %{activities: activities, for: user, as: :activity})    end @@ -1158,7 +1210,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      accounts      |> Enum.each(fn account_id ->        with %Pleroma.List{} = list <- Pleroma.List.get(id, user), -           %User{} = followed <- User.get_by_id(account_id) do +           %User{} = followed <- User.get_cached_by_id(account_id) do          Pleroma.List.follow(list, followed)        end      end) @@ -1170,7 +1222,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      accounts      |> Enum.each(fn account_id ->        with %Pleroma.List{} = list <- Pleroma.List.get(id, user), -           %User{} = followed <- Pleroma.User.get_by_id(account_id) do +           %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do          Pleroma.List.unfollow(list, followed)        end      end) @@ -1214,6 +1266,8 @@ 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}) @@ -1463,7 +1517,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do      Logger.debug("Unimplemented, returning unmodified relationship") -    with %User{} = target <- User.get_by_id(id) do +    with %User{} = target <- User.get_cached_by_id(id) do        conn        |> put_view(AccountView)        |> render("relationship.json", %{user: user, target: target}) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index af56c4149..779b9a382 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -68,7 +68,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do    defp do_render("account.json", %{user: user} = opts) do      image = User.avatar_url(user) |> MediaProxy.url()      header = User.banner_url(user) |> MediaProxy.url() -    user_info = User.user_info(user) +    user_info = User.get_cached_user_info(user)      bot = (user.info.source_data["type"] || "Person") in ["Application", "Service"]      emojis = @@ -113,21 +113,23 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do        bot: bot,        source: %{          note: "", -        privacy: user_info.default_scope, -        sensitive: false +        sensitive: false, +        pleroma: %{}        },        # Pleroma extension -      pleroma: -        %{ -          confirmation_pending: user_info.confirmation_pending, -          tags: user.tags, -          is_moderator: user.info.is_moderator, -          is_admin: user.info.is_admin, -          relationship: relationship -        } -        |> with_notification_settings(user, opts[:for]) +      pleroma: %{ +        confirmation_pending: user_info.confirmation_pending, +        tags: user.tags, +        hide_followers: user.info.hide_followers, +        hide_follows: user.info.hide_follows, +        hide_favorites: user.info.hide_favorites, +        relationship: relationship +      }      } +    |> maybe_put_role(user, opts[:for]) +    |> maybe_put_settings(user, opts[:for], user_info) +    |> maybe_put_notification_settings(user, opts[:for])    end    defp username_from_nickname(string) when is_binary(string) do @@ -136,9 +138,37 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do    defp username_from_nickname(_), do: nil -  defp with_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do -    Map.put(data, :notification_settings, user.info.notification_settings) +  defp maybe_put_settings( +         data, +         %User{id: user_id} = user, +         %User{id: user_id}, +         user_info +       ) do +    data +    |> Kernel.put_in([:source, :privacy], user_info.default_scope) +    |> Kernel.put_in([:source, :pleroma, :show_role], user.info.show_role) +    |> Kernel.put_in([:source, :pleroma, :no_rich_text], user.info.no_rich_text) +  end + +  defp maybe_put_settings(data, _, _, _), do: data + +  defp maybe_put_role(data, %User{info: %{show_role: true}} = user, _) do +    data +    |> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin) +    |> Kernel.put_in([:pleroma, :is_moderator], user.info.is_moderator) +  end + +  defp maybe_put_role(data, %User{id: user_id} = user, %User{id: user_id}) do +    data +    |> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin) +    |> Kernel.put_in([:pleroma, :is_moderator], user.info.is_moderator) +  end + +  defp maybe_put_role(data, _, _), do: data + +  defp maybe_put_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do +    Kernel.put_in(data, [:pleroma, :notification_settings], user.info.notification_settings)    end -  defp with_notification_settings(data, _, _), do: data +  defp maybe_put_notification_settings(data, _, _), do: data  end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index f8961eb6c..62d064d71 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -31,7 +31,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      |> Activity.create_by_object_ap_id()      |> Repo.all()      |> Enum.reduce(%{}, fn activity, acc -> -      object = Object.normalize(activity.data["object"]) +      object = Object.normalize(activity)        Map.put(acc, object.data["id"], activity)      end)    end @@ -83,6 +83,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      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) +      mentions =        activity.recipients        |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end) @@ -103,8 +108,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        replies_count: 0,        favourites_count: 0,        reblogged: reblogged?(reblogged_activity, opts[:for]), -      favourited: false, -      bookmarked: false, +      favourited: present?(favorited), +      bookmarked: present?(bookmarked),        muted: false,        pinned: pinned?(activity, user),        sensitive: false, @@ -144,7 +149,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || []) -    bookmarked = opts[:for] && object.data["id"] in opts[:for].bookmarks +    bookmarked = opts[:for] && CommonAPI.bookmarked?(opts[:for], activity)      attachment_data = object.data["attachment"] || []      attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment) @@ -234,6 +239,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        pleroma: %{          local: activity.local,          conversation_id: get_context_id(activity), +        in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,          content: %{"text/plain" => content_plaintext},          spoiler_text: %{"text/plain" => summary_plaintext}        } @@ -312,7 +318,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do    end    def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do -    object = Object.normalize(activity.data["object"]) +    object = Object.normalize(activity)      with nil <- replied_to_activities[object.data["inReplyTo"]] do        # If user didn't participate in the thread diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index 1b3721e2b..abfa26754 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -90,7 +90,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do    # Authenticated streams.    defp allow_request(stream, {"access_token", access_token}) when stream in @streams do      with %Token{user_id: user_id} <- Repo.get_by(Token, token: access_token), -         user = %User{} <- User.get_by_id(user_id) do +         user = %User{} <- User.get_cached_by_id(user_id) do        {:ok, user}      else        _ -> {:error, 403} diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index 3bd2affe9..5762e767b 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -13,32 +13,44 @@ defmodule Pleroma.Web.MediaProxy do    def url(url) do      config = Application.get_env(:pleroma, :media_proxy, []) +    domain = URI.parse(url).host -    if !Keyword.get(config, :enabled, false) or String.starts_with?(url, Pleroma.Web.base_url()) do -      url -    else -      secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base] - -      # Must preserve `%2F` for compatibility with S3 -      # https://git.pleroma.social/pleroma/pleroma/issues/580 -      replacement = get_replacement(url, ":2F:") - -      # The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice. -      base64 = +    cond do +      !Keyword.get(config, :enabled, false) or String.starts_with?(url, Pleroma.Web.base_url()) ->          url -        |> String.replace("%2F", replacement) -        |> URI.decode() -        |> URI.encode() -        |> String.replace(replacement, "%2F") -        |> Base.url_encode64(@base64_opts) -      sig = :crypto.hmac(:sha, secret, base64) -      sig64 = sig |> Base.url_encode64(@base64_opts) +      Enum.any?(Pleroma.Config.get([:media_proxy, :whitelist]), fn pattern -> +        String.equivalent?(domain, pattern) +      end) -> +        url -      build_url(sig64, base64, filename(url)) +      true -> +        encode_url(url)      end    end +  def encode_url(url) do +    secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base] + +    # Must preserve `%2F` for compatibility with S3 +    # https://git.pleroma.social/pleroma/pleroma/issues/580 +    replacement = get_replacement(url, ":2F:") + +    # The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice. +    base64 = +      url +      |> String.replace("%2F", replacement) +      |> URI.decode() +      |> URI.encode() +      |> String.replace(replacement, "%2F") +      |> Base.url_encode64(@base64_opts) + +    sig = :crypto.hmac(:sha, secret, base64) +    sig64 = sig |> Base.url_encode64(@base64_opts) + +    build_url(sig64, base64, filename(url)) +  end +    def decode_url(sig, url) do      secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]      sig = Base.url_decode64!(sig, @base64_opts) diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 9874bac23..688eaca11 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -23,6 +23,12 @@ defmodule Pleroma.Web.OAuth.OAuthController do    action_fallback(Pleroma.Web.OAuth.FallbackController) +  # Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg +  def authorize(conn, %{"authorization" => _} = params) do +    {auth_attrs, params} = Map.pop(params, "authorization") +    authorize(conn, Map.merge(params, auth_attrs)) +  end +    def authorize(%{assigns: %{token: %Token{} = token}} = conn, params) do      if ControllerHelper.truthy_param?(params["force_login"]) do        do_authorize(conn, params) @@ -44,21 +50,20 @@ defmodule Pleroma.Web.OAuth.OAuthController do    def authorize(conn, params), do: do_authorize(conn, params) -  defp do_authorize(conn, %{"authorization" => auth_attrs}), do: do_authorize(conn, auth_attrs) - -  defp do_authorize(conn, auth_attrs) do -    app = Repo.get_by(App, client_id: auth_attrs["client_id"]) +  defp do_authorize(conn, params) do +    app = Repo.get_by(App, client_id: params["client_id"])      available_scopes = (app && app.scopes) || [] -    scopes = oauth_scopes(auth_attrs, nil) || available_scopes +    scopes = oauth_scopes(params, nil) || available_scopes +    # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template      render(conn, Authenticator.auth_template(), %{ -      response_type: auth_attrs["response_type"], -      client_id: auth_attrs["client_id"], +      response_type: params["response_type"], +      client_id: params["client_id"],        available_scopes: available_scopes,        scopes: scopes, -      redirect_uri: auth_attrs["redirect_uri"], -      state: auth_attrs["state"], -      params: auth_attrs +      redirect_uri: params["redirect_uri"], +      state: params["state"], +      params: params      })    end @@ -138,7 +143,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do           fixed_token = fix_padding(params["code"]),           %Authorization{} = auth <-             Repo.get_by(Authorization, token: fixed_token, app_id: app.id), -         %User{} = user <- User.get_by_id(auth.user_id), +         %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 = %{ diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index 2b5ad9b94..399140003 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -27,7 +27,7 @@ defmodule Pleroma.Web.OAuth.Token do    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_by_id(auth.user_id), auth.scopes) +      create_token(app, User.get_cached_by_id(auth.user_id), auth.scopes)      end    end diff --git a/lib/pleroma/web/ostatus/activity_representer.ex b/lib/pleroma/web/ostatus/activity_representer.ex index b11a2b5ce..166691a09 100644 --- a/lib/pleroma/web/ostatus/activity_representer.ex +++ b/lib/pleroma/web/ostatus/activity_representer.ex @@ -84,7 +84,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do    def to_simple_form(%{data: %{"type" => "Create"}} = activity, user, with_author) do      h = fn str -> [to_charlist(str)] end -    object = Object.normalize(activity.data["object"]) +    object = Object.normalize(activity)      updated_at = object.data["published"]      inserted_at = object.data["published"] diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex index 9a34d7ad5..4744c6d83 100644 --- a/lib/pleroma/web/ostatus/ostatus.ex +++ b/lib/pleroma/web/ostatus/ostatus.ex @@ -294,7 +294,7 @@ defmodule Pleroma.Web.OStatus do        }        with false <- update, -           %User{} = user <- User.get_by_ap_id(data.ap_id) do +           %User{} = user <- User.get_cached_by_ap_id(data.ap_id) do          {:ok, user}        else          _e -> User.insert_or_update_user(data) diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index 2233480c5..35d3ff07c 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -21,8 +21,10 @@ defmodule Pleroma.Web.Push.Impl do    @doc "Performs sending notifications for user subscriptions"    @spec perform(Notification.t()) :: list(any) | :error    def perform( -        %{activity: %{data: %{"type" => activity_type}, id: activity_id}, user_id: user_id} = -          notif +        %{ +          activity: %{data: %{"type" => activity_type}, id: activity_id} = activity, +          user_id: user_id +        } = notif        )        when activity_type in @types do      actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) @@ -30,13 +32,14 @@ defmodule Pleroma.Web.Push.Impl do      type = Activity.mastodon_notification_type(notif.activity)      gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)      avatar_url = User.avatar_url(actor) +    object = Object.normalize(activity)      for subscription <- fetch_subsriptions(user_id),          get_in(subscription.data, ["alerts", type]) do        %{          title: format_title(notif),          access_token: subscription.token.token, -        body: format_body(notif, actor), +        body: format_body(notif, actor, object),          notification_id: notif.id,          notification_type: type,          icon: avatar_url, @@ -95,25 +98,25 @@ defmodule Pleroma.Web.Push.Impl do    end    def format_body( -        %{activity: %{data: %{"type" => "Create", "object" => %{"content" => content}}}}, -        actor +        %{activity: %{data: %{"type" => "Create"}}}, +        actor, +        %{data: %{"content" => content}}        ) do      "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"    end    def format_body( -        %{activity: %{data: %{"type" => "Announce", "object" => activity_id}}}, -        actor +        %{activity: %{data: %{"type" => "Announce"}}}, +        actor, +        %{data: %{"content" => content}}        ) do -    %Activity{data: %{"object" => %{"id" => object_id}}} = Activity.get_by_ap_id(activity_id) -    %Object{data: %{"content" => content}} = Object.get_by_ap_id(object_id) -      "@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}"    end    def format_body(          %{activity: %{data: %{"type" => type}}}, -        actor +        actor, +        _object        )        when type in ["Follow", "Like"] do      case type do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 09e51e602..9b833bc48 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -135,6 +135,7 @@ defmodule Pleroma.Web.Router do      post("/password_reset", UtilController, :password_reset)      get("/emoji", UtilController, :emoji)      get("/captcha", UtilController, :captcha) +    get("/healthcheck", UtilController, :healthcheck)    end    scope "/api/pleroma", Pleroma.Web do @@ -394,6 +395,8 @@ defmodule Pleroma.Web.Router do        get("/accounts/:id", MastodonAPIController, :user)        get("/search", MastodonAPIController, :search) + +      get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites)      end    end diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index a82109f92..72eaf2084 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -81,7 +81,7 @@ defmodule Pleroma.Web.Streamer do          _ ->            Pleroma.List.get_lists_from_activity(item)            |> Enum.filter(fn list -> -            owner = User.get_by_id(list.user_id) +            owner = User.get_cached_by_id(list.user_id)              Visibility.visible_for_user?(item, owner)            end) diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 9441984c7..1122e6c5d 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -22,7 +22,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    def show_password_reset(conn, %{"token" => token}) do      with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}), -         %User{} = user <- User.get_by_id(token.user_id) do +         %User{} = user <- User.get_cached_by_id(token.user_id) do        render(conn, "password_reset.html", %{          token: token,          user: user @@ -113,13 +113,13 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    def do_remote_follow(conn, %{          "authorization" => %{"name" => username, "password" => password, "id" => id}        }) do -    followee = User.get_by_id(id) +    followee = User.get_cached_by_id(id)      avatar = User.avatar_url(followee)      name = followee.nickname      with %User{} = user <- User.get_cached_by_nickname(username),           true <- Pbkdf2.checkpw(password, user.password_hash), -         %User{} = _followed <- User.get_by_id(id), +         %User{} = _followed <- User.get_cached_by_id(id),           {:ok, follower} <- User.follow(user, followee),           {:ok, _activity} <- ActivityPub.follow(follower, followee) do        conn @@ -141,7 +141,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    end    def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}}) do -    with %User{} = followee <- User.get_by_id(id), +    with %User{} = followee <- User.get_cached_by_id(id),           {:ok, follower} <- User.follow(user, followee),           {:ok, _activity} <- ActivityPub.follow(follower, followee) do        conn @@ -286,7 +286,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do      emoji =        Emoji.get_all()        |> Enum.map(fn {short_code, path, tags} -> -        {short_code, %{image_url: path, tags: String.split(tags, ",")}} +        {short_code, %{image_url: path, tags: tags}}        end)        |> Enum.into(%{}) @@ -363,4 +363,22 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    def captcha(conn, _params) do      json(conn, Pleroma.Captcha.new())    end + +  def healthcheck(conn, _params) do +    info = +      if Pleroma.Config.get([:instance, :healthcheck]) do +        Pleroma.Healthcheck.system_info() +      else +        %{} +      end + +    conn = +      if info[:healthy] do +        conn +      else +        Plug.Conn.put_status(conn, :service_unavailable) +      end + +    json(conn, info) +  end  end diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index 8e44dbeb8..adeac6f3c 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -240,7 +240,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do          end        %{"screen_name" => nickname} -> -        case User.get_by_nickname(nickname) do +        case User.get_cached_by_nickname(nickname) do            nil -> {:error, "No user with such screen_name"}            target -> {:ok, target}          end diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index a7ec9949c..79ed9dad2 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -434,7 +434,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    end    def confirm_email(conn, %{"user_id" => uid, "token" => token}) do -    with %User{} = user <- User.get_by_id(uid), +    with %User{} = user <- User.get_cached_by_id(uid),           true <- user.local,           true <- user.info.confirmation_pending,           true <- user.info.confirmation_token == token, @@ -587,7 +587,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    def approve_friend_request(conn, %{"user_id" => uid} = _params) do      with followed <- conn.assigns[:user], -         %User{} = follower <- User.get_by_id(uid), +         %User{} = follower <- User.get_cached_by_id(uid),           {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do        conn        |> put_view(UserView) @@ -599,7 +599,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    def deny_friend_request(conn, %{"user_id" => uid} = _params) do      with followed <- conn.assigns[:user], -         %User{} = follower <- User.get_by_id(uid), +         %User{} = follower <- User.get_cached_by_id(uid),           {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do        conn        |> put_view(UserView) @@ -632,7 +632,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    defp build_info_cng(user, params) do      info_params = -      ["no_rich_text", "locked", "hide_followers", "hide_follows", "show_role"] +      ["no_rich_text", "locked", "hide_followers", "hide_follows", "hide_favorites", "show_role"]        |> Enum.reduce(%{}, fn key, res ->          if value = params[key] do            Map.put(res, key, value == "true") diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex index 0791ed760..ea015b8f0 100644 --- a/lib/pleroma/web/twitter_api/views/user_view.ex +++ b/lib/pleroma/web/twitter_api/views/user_view.ex @@ -74,58 +74,49 @@ defmodule Pleroma.Web.TwitterAPI.UserView do        |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)        |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end) -    data = %{ -      "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)), -      "favourites_count" => 0, -      "followers_count" => user_info[:follower_count], -      "following" => following, -      "follows_you" => follows_you, -      "statusnet_blocking" => statusnet_blocking, -      "friends_count" => user_info[:following_count], -      "id" => user.id, -      "name" => user.name || user.nickname, -      "name_html" => -        if(user.name, -          do: HTML.strip_tags(user.name) |> Formatter.emojify(emoji), -          else: user.nickname -        ), -      "profile_image_url" => image, -      "profile_image_url_https" => image, -      "profile_image_url_profile_size" => image, -      "profile_image_url_original" => image, -      "rights" => %{ -        "delete_others_notice" => !!user.info.is_moderator, -        "admin" => !!user.info.is_admin -      }, -      "screen_name" => user.nickname, -      "statuses_count" => user_info[:note_count], -      "statusnet_profile_url" => user.ap_id, -      "cover_photo" => User.banner_url(user) |> MediaProxy.url(), -      "background_image" => image_url(user.info.background) |> MediaProxy.url(), -      "is_local" => user.local, -      "locked" => user.info.locked, -      "default_scope" => user.info.default_scope, -      "no_rich_text" => user.info.no_rich_text, -      "hide_followers" => user.info.hide_followers, -      "hide_follows" => user.info.hide_follows, -      "fields" => fields, - -      # Pleroma extension -      "pleroma" => -        %{ -          "confirmation_pending" => user_info.confirmation_pending, -          "tags" => user.tags -        } -        |> maybe_with_activation_status(user, for_user) -    } -      data = -      if(user.info.is_admin || user.info.is_moderator, -        do: maybe_with_role(data, user, for_user), -        else: data -      ) +      %{ +        "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)), +        "favourites_count" => 0, +        "followers_count" => user_info[:follower_count], +        "following" => following, +        "follows_you" => follows_you, +        "statusnet_blocking" => statusnet_blocking, +        "friends_count" => user_info[:following_count], +        "id" => user.id, +        "name" => user.name || user.nickname, +        "name_html" => +          if(user.name, +            do: HTML.strip_tags(user.name) |> Formatter.emojify(emoji), +            else: user.nickname +          ), +        "profile_image_url" => image, +        "profile_image_url_https" => image, +        "profile_image_url_profile_size" => image, +        "profile_image_url_original" => image, +        "screen_name" => user.nickname, +        "statuses_count" => user_info[:note_count], +        "statusnet_profile_url" => user.ap_id, +        "cover_photo" => User.banner_url(user) |> MediaProxy.url(), +        "background_image" => image_url(user.info.background) |> MediaProxy.url(), +        "is_local" => user.local, +        "locked" => user.info.locked, +        "hide_followers" => user.info.hide_followers, +        "hide_follows" => user.info.hide_follows, +        "fields" => fields, + +        # Pleroma extension +        "pleroma" => +          %{ +            "confirmation_pending" => user_info.confirmation_pending, +            "tags" => user.tags +          } +          |> maybe_with_activation_status(user, for_user) +      } +      |> maybe_with_user_settings(user, for_user) +      |> maybe_with_role(user, for_user)      if assigns[:token] do        Map.put(data, "token", token_string(assigns[:token])) @@ -141,15 +132,35 @@ defmodule Pleroma.Web.TwitterAPI.UserView do    defp maybe_with_activation_status(data, _, _), do: data    defp maybe_with_role(data, %User{id: id} = user, %User{id: id}) do -    Map.merge(data, %{"role" => role(user), "show_role" => user.info.show_role}) +    Map.merge(data, %{ +      "role" => role(user), +      "show_role" => user.info.show_role, +      "rights" => %{ +        "delete_others_notice" => !!user.info.is_moderator, +        "admin" => !!user.info.is_admin +      } +    })    end    defp maybe_with_role(data, %User{info: %{show_role: true}} = user, _user) do -    Map.merge(data, %{"role" => role(user)}) +    Map.merge(data, %{ +      "role" => role(user), +      "rights" => %{ +        "delete_others_notice" => !!user.info.is_moderator, +        "admin" => !!user.info.is_admin +      } +    })    end    defp maybe_with_role(data, _, _), do: data +  defp maybe_with_user_settings(data, %User{info: info, id: id} = _user, %User{id: id}) do +    data +    |> Kernel.put_in(["default_scope"], info.default_scope) +    |> Kernel.put_in(["no_rich_text"], info.no_rich_text) +  end + +  defp maybe_with_user_settings(data, _, _), do: data    defp role(%User{info: %{:is_admin => true}}), do: "admin"    defp role(%User{info: %{:is_moderator => true}}), do: "moderator"    defp role(_), do: "member" diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex index 32c3455f5..a3b0bf999 100644 --- a/lib/pleroma/web/web_finger/web_finger.ex +++ b/lib/pleroma/web/web_finger/web_finger.ex @@ -37,7 +37,7 @@ defmodule Pleroma.Web.WebFinger do      regex = ~r/(acct:)?(?<username>\w+)@#{host}/      with %{"username" => username} <- Regex.named_captures(regex, resource), -         %User{} = user <- User.get_by_nickname(username) do +         %User{} = user <- User.get_cached_by_nickname(username) do        {:ok, represent_user(user, fmt)}      else        _e ->  | 
