diff options
Diffstat (limited to 'lib')
43 files changed, 1060 insertions, 521 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/database.ex b/lib/mix/tasks/pleroma/database.ex new file mode 100644 index 000000000..ab9a3a7ff --- /dev/null +++ b/lib/mix/tasks/pleroma/database.ex @@ -0,0 +1,51 @@ +# 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.Database do + alias Mix.Tasks.Pleroma.Common + require Logger + use Mix.Task + + @shortdoc "A collection of database related tasks" + @moduledoc """ + A collection of database related tasks + + ## Replace embedded objects with their references + + Replaces embedded objects with references to them in the `objects` table. Only needs to be ran once. The reason why this is not a migration is because it could significantly increase the database size after being ran, however after this `VACUUM FULL` will be able to reclaim about 20% (really depends on what is in the database, your mileage may vary) of the db size before the migration. + + mix pleroma.database remove_embedded_objects + + Options: + - `--vacuum` - run `VACUUM FULL` after the embedded objects are replaced with their references + """ + def run(["remove_embedded_objects" | args]) do + {options, [], []} = + OptionParser.parse( + args, + strict: [ + vacuum: :boolean + ] + ) + + Common.start_pleroma() + Logger.info("Removing embedded objects") + + Pleroma.Repo.query!( + "update activities set data = jsonb_set(data, '{object}'::text[], data->'object'->'id') where data->'object'->>'id' is not null;", + [], + timeout: :infinity + ) + + if Keyword.get(options, :vacuum) do + Logger.info("Runnning VACUUM FULL") + + Pleroma.Repo.query!( + "vacuum full;", + [], + timeout: :infinity + ) + end + end +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/activity.ex b/lib/pleroma/activity.ex index e6507e5ca..4a2ded518 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Activity do alias Pleroma.Object alias Pleroma.Repo + import Ecto.Changeset import Ecto.Query @type t :: %__MODULE__{} @@ -79,6 +80,13 @@ defmodule Pleroma.Activity do ) end + def change(struct, params \\ %{}) do + struct + |> cast(params, [:data]) + |> validate_required([:data]) + |> unique_constraint(:ap_id, name: :activities_unique_apid_index) + end + def get_by_ap_id_with_object(ap_id) do Repo.one( from( @@ -196,21 +204,27 @@ defmodule Pleroma.Activity do def create_by_object_ap_id_with_object(_), do: nil - def get_create_by_object_ap_id_with_object(ap_id) do + def get_create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do ap_id |> create_by_object_ap_id_with_object() |> Repo.one() end - def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"]) - def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id) - def normalize(_), do: nil + def get_create_by_object_ap_id_with_object(_), do: nil - def get_in_reply_to_activity(%Activity{data: %{"object" => %{"inReplyTo" => ap_id}}}) do - get_create_by_object_ap_id(ap_id) + defp get_in_reply_to_activity_from_object(%Object{data: %{"inReplyTo" => ap_id}}) do + get_create_by_object_ap_id_with_object(ap_id) end - def get_in_reply_to_activity(_), do: nil + defp get_in_reply_to_activity_from_object(_), do: nil + + def get_in_reply_to_activity(%Activity{data: %{"object" => object}}) do + get_in_reply_to_activity_from_object(Object.normalize(object)) + end + + def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"]) + def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id) + def normalize(_), do: nil def delete_by_ap_id(id) when is_binary(id) do by_object_ap_id(id) @@ -218,6 +232,7 @@ defmodule Pleroma.Activity do |> Repo.delete_all() |> elem(1) |> Enum.find(fn + %{data: %{"type" => "Create", "object" => ap_id}} when is_binary(ap_id) -> ap_id == id %{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id _ -> nil end) @@ -245,54 +260,4 @@ defmodule Pleroma.Activity do |> where([s], s.actor == ^actor) |> Repo.all() end - - def increase_replies_count(nil), do: nil - - def increase_replies_count(object_ap_id) do - from(a in create_by_object_ap_id(object_ap_id), - update: [ - set: [ - data: - fragment( - """ - jsonb_set(?, '{object, repliesCount}', - (coalesce((?->'object'->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true) - """, - a.data, - a.data - ) - ] - ] - ) - |> Repo.update_all([]) - |> case do - {1, [activity]} -> activity - _ -> {:error, "Not found"} - end - end - - def decrease_replies_count(nil), do: nil - - def decrease_replies_count(object_ap_id) do - from(a in create_by_object_ap_id(object_ap_id), - update: [ - set: [ - data: - fragment( - """ - jsonb_set(?, '{object, repliesCount}', - (greatest(0, (?->'object'->>'repliesCount')::int - 1))::varchar::jsonb, true) - """, - a.data, - a.data - ) - ] - ] - ) - |> Repo.update_all([]) - |> case do - {1, [activity]} -> activity - _ -> {:error, "Not found"} - end - 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 6a56a6f67..1d2e0785c 100644 --- a/lib/pleroma/gopher/server.ex +++ b/lib/pleroma/gopher/server.ex @@ -38,6 +38,7 @@ end defmodule Pleroma.Gopher.Server.ProtocolHandler do alias Pleroma.Activity alias Pleroma.HTML + alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Visibility @@ -75,14 +76,14 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do |> Enum.map(fn activity -> user = User.get_cached_by_ap_id(activity.data["actor"]) - object = activity.data["object"] + object = Object.normalize(activity) like_count = object["like_count"] || 0 announcement_count = object["announcement_count"] || 0 link("Post ##{activity.id} by #{user.nickname}", "/notices/#{activity.id}") <> info("#{like_count} likes, #{announcement_count} repeats") <> "i\tfake\t(NULL)\t0\r\n" <> - info(HTML.strip_tags(String.replace(activity.data["object"]["content"], "<br>", "\r"))) + info(HTML.strip_tags(String.replace(object["content"], "<br>", "\r"))) end) |> Enum.join("i\tfake\t(NULL)\t0\r\n") end diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index 2c701adb5..cf6c0ee0a 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -32,7 +32,8 @@ defmodule Pleroma.HTML do key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}" Cachex.fetch!(:scrubber_cache, key, fn _key -> - ensure_scrubbed_html(content, scrubbers, activity.data["object"]["fake"] || false) + object = Pleroma.Object.normalize(activity) + ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false) end) end 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 b357d5399..dd274cf6b 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -196,7 +196,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.ex b/lib/pleroma/object.ex index 013d62157..740d687a3 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Object do alias Pleroma.Activity alias Pleroma.Object + alias Pleroma.Object.Fetcher alias Pleroma.ObjectTombstone alias Pleroma.Repo alias Pleroma.User @@ -40,41 +41,44 @@ defmodule Pleroma.Object do Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id))) end + def normalize(_, fetch_remote \\ true) # If we pass an Activity to Object.normalize(), we can try to use the preloaded object. # Use this whenever possible, especially when walking graphs in an O(N) loop! - def normalize(%Activity{object: %Object{} = object}), do: object + def normalize(%Object{} = object, _), do: object + def normalize(%Activity{object: %Object{} = object}, _), do: object # A hack for fake activities - def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}) do + def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _) do %Object{id: "pleroma:fake_object_id", data: data} end # Catch and log Object.normalize() calls where the Activity's child object is not # preloaded. - def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}) do + def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, fetch_remote) do Logger.debug( "Object.normalize() called without preloaded object (#{ap_id}). Consider preloading the object!" ) Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}") - normalize(ap_id) + normalize(ap_id, fetch_remote) end - def normalize(%Activity{data: %{"object" => ap_id}}) do + def normalize(%Activity{data: %{"object" => ap_id}}, fetch_remote) do Logger.debug( "Object.normalize() called without preloaded object (#{ap_id}). Consider preloading the object!" ) Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}") - normalize(ap_id) + normalize(ap_id, fetch_remote) end # Old way, try fetching the object through cache. - def normalize(%{"id" => ap_id}), do: normalize(ap_id) - def normalize(ap_id) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id) - def normalize(_), do: nil + def normalize(%{"id" => ap_id}, fetch_remote), do: normalize(ap_id, fetch_remote) + def normalize(ap_id, false) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id) + def normalize(ap_id, true) when is_binary(ap_id), do: Fetcher.fetch_object_from_id!(ap_id) + def normalize(_, _), do: nil # Owned objects can only be mutated by their owner def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}), diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex new file mode 100644 index 000000000..25bd911fb --- /dev/null +++ b/lib/pleroma/object/containment.ex @@ -0,0 +1,61 @@ +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. + + Object containment is an important step in validating remote objects to prevent + spoofing, therefore removal of object containment functions is NOT recommended. + """ + def get_actor(%{"actor" => actor}) when is_binary(actor) do + actor + end + + def get_actor(%{"actor" => actor}) when is_list(actor) do + if is_binary(Enum.at(actor, 0)) do + Enum.at(actor, 0) + else + Enum.find(actor, fn %{"type" => type} -> type in ["Person", "Service", "Application"] end) + |> Map.get("id") + end + end + + def get_actor(%{"actor" => %{"id" => id}}) when is_bitstring(id) do + id + end + + def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do + get_actor(%{"actor" => actor}) + end + + @doc """ + Checks that an imported AP object's actor matches the domain it came from. + """ + def contain_origin(_id, %{"actor" => nil}), do: :error + + def contain_origin(id, %{"actor" => _actor} = params) do + id_uri = URI.parse(id) + actor_uri = URI.parse(get_actor(params)) + + if id_uri.host == actor_uri.host do + :ok + else + :error + end + end + + def contain_origin_from_id(_id, %{"id" => nil}), do: :error + + def contain_origin_from_id(id, %{"id" => other_id} = _params) do + id_uri = URI.parse(id) + other_uri = URI.parse(other_id) + + if id_uri.host == other_uri.host do + :ok + else + :error + end + end +end diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex new file mode 100644 index 000000000..8d4bcc95e --- /dev/null +++ b/lib/pleroma/object/fetcher.ex @@ -0,0 +1,75 @@ +defmodule Pleroma.Object.Fetcher do + alias Pleroma.Object + alias Pleroma.Object.Containment + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.OStatus + + require Logger + + @httpoison Application.get_env(:pleroma, :httpoison) + + # TODO: + # This will create a Create activity, which we need internally at the moment. + def fetch_object_from_id(id) do + if object = Object.get_cached_by_ap_id(id) do + {:ok, object} + else + Logger.info("Fetching #{id} via AP") + + with {:ok, data} <- fetch_and_contain_remote_object_from_id(id), + nil <- Object.normalize(data, false), + params <- %{ + "type" => "Create", + "to" => data["to"], + "cc" => data["cc"], + "actor" => data["actor"] || data["attributedTo"], + "object" => data + }, + :ok <- Containment.contain_origin(id, params), + {:ok, activity} <- Transmogrifier.handle_incoming(params) do + {:ok, Object.normalize(activity, false)} + else + {:error, {:reject, nil}} -> + {:reject, nil} + + object = %Object{} -> + {:ok, object} + + _e -> + 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, false)} + e -> e + end + end + end + end + + def fetch_object_from_id!(id) do + with {:ok, object} <- fetch_object_from_id(id) do + object + else + _e -> + nil + end + end + + def fetch_and_contain_remote_object_from_id(id) do + Logger.info("Fetching object #{id} via AP") + + with true <- String.starts_with?(id, "http"), + {:ok, %{body: body, status: code}} when code in 200..299 <- + @httpoison.get( + id, + [{:Accept, "application/activity+json"}] + ), + {:ok, data} <- Jason.decode(body), + :ok <- Containment.contain_origin_from_id(id, data) do + {:ok, data} + else + e -> + {:error, e} + end + end +end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 78eb29ddd..f1feab279 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -269,6 +269,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} @@ -453,10 +454,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)) @@ -544,6 +548,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 @@ -1002,7 +1007,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 @@ -1032,7 +1037,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 @@ -1207,7 +1212,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 @@ -1230,7 +1235,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 = @@ -1277,13 +1282,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 @@ -1299,8 +1302,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 -> @@ -1310,8 +1313,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 -> @@ -1350,7 +1353,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))) @@ -1362,7 +1365,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)) diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 5afa7988c..7f22a45b5 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -38,6 +38,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) @@ -202,6 +203,7 @@ defmodule Pleroma.User.Info do :banner, :hide_follows, :hide_followers, + :hide_favorites, :background, :show_role ]) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index cb88ba308..604ffae7b 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.Instances alias Pleroma.Notification alias Pleroma.Object + alias Pleroma.Object.Fetcher alias Pleroma.Pagination alias Pleroma.Repo alias Pleroma.Upload @@ -14,7 +15,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.Federator - alias Pleroma.Web.OStatus alias Pleroma.Web.WebFinger import Ecto.Query @@ -95,7 +95,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do "type" => "Create" }) do if is_public?(object) do - Activity.increase_replies_count(reply_ap_id) Object.increase_replies_count(reply_ap_id) end end @@ -106,7 +105,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do data: %{"inReplyTo" => reply_ap_id} = object }) do if is_public?(object) do - Activity.decrease_replies_count(reply_ap_id) Object.decrease_replies_count(reply_ap_id) end end @@ -121,7 +119,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do {:ok, map} <- MRF.filter(map), {recipients, _, _} = get_recipients(map), {:fake, false, map, recipients} <- {:fake, fake, map, recipients}, - {:ok, object} <- insert_full_object(map) do + {:ok, map, object} <- insert_full_object(map) do {:ok, activity} = Repo.insert(%Activity{ data: map, @@ -170,6 +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) Pleroma.Web.Streamer.stream("user", activity) Pleroma.Web.Streamer.stream("list", activity) @@ -181,12 +180,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end if activity.data["type"] in ["Create"] do - activity.data["object"] + object.data |> Map.get("tag", []) |> Enum.filter(fn tag -> is_bitstring(tag) end) |> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end) - if activity.data["object"]["attachment"] != [] do + if object.data["attachment"] != [] do Pleroma.Web.Streamer.stream("public:media", activity) if activity.local do @@ -198,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 @@ -572,37 +571,49 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_since(query, _), do: query + defp restrict_tag_reject(_query, %{"tag_reject" => _tag_reject, "skip_preload" => true}) do + raise "Can't use the child object without preloading!" + end + defp restrict_tag_reject(query, %{"tag_reject" => tag_reject}) when is_list(tag_reject) and tag_reject != [] do from( - activity in query, - where: fragment(~s(\(not \(? #> '{"object","tag"}'\) \\?| ?\)), activity.data, ^tag_reject) + [_activity, object] in query, + where: fragment("not (?)->'tag' \\?| (?)", object.data, ^tag_reject) ) end defp restrict_tag_reject(query, _), do: query + defp restrict_tag_all(_query, %{"tag_all" => _tag_all, "skip_preload" => true}) do + raise "Can't use the child object without preloading!" + end + defp restrict_tag_all(query, %{"tag_all" => tag_all}) when is_list(tag_all) and tag_all != [] do from( - activity in query, - where: fragment(~s(\(? #> '{"object","tag"}'\) \\?& ?), activity.data, ^tag_all) + [_activity, object] in query, + where: fragment("(?)->'tag' \\?& (?)", object.data, ^tag_all) ) end defp restrict_tag_all(query, _), do: query + defp restrict_tag(_query, %{"tag" => _tag, "skip_preload" => true}) do + raise "Can't use the child object without preloading!" + end + defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do from( - activity in query, - where: fragment(~s(\(? #> '{"object","tag"}'\) \\?| ?), activity.data, ^tag) + [_activity, object] in query, + where: fragment("(?)->'tag' \\?| (?)", object.data, ^tag) ) end defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do from( - activity in query, - where: fragment(~s(? <@ (? #> '{"object","tag"}'\)), ^tag, activity.data) + [_activity, object] in query, + where: fragment("(?)->'tag' \\? (?)", object.data, ^tag) ) end @@ -667,10 +678,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_favorited_by(query, _), do: query + defp restrict_media(_query, %{"only_media" => _val, "skip_preload" => true}) do + raise "Can't use the child object without preloading!" + end + defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do from( - activity in query, - where: fragment(~s(not (? #> '{"object","attachment"}' = ?\)), activity.data, ^[]) + [_activity, object] in query, + where: fragment("not (?)->'attachment' = (?)", object.data, ^[]) ) end @@ -866,7 +881,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end def fetch_and_prepare_user_from_ap_id(ap_id) do - with {:ok, data} <- fetch_and_contain_remote_object_from_id(ap_id) do + with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id) do user_data_from_user_object(data) else e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}") @@ -874,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 @@ -976,60 +991,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end end - # TODO: - # This will create a Create activity, which we need internally at the moment. - def fetch_object_from_id(id) do - if object = Object.get_cached_by_ap_id(id) do - {:ok, object} - else - with {:ok, data} <- fetch_and_contain_remote_object_from_id(id), - nil <- Object.normalize(data), - params <- %{ - "type" => "Create", - "to" => data["to"], - "cc" => data["cc"], - "actor" => data["actor"] || data["attributedTo"], - "object" => data - }, - :ok <- Transmogrifier.contain_origin(id, params), - {:ok, activity} <- Transmogrifier.handle_incoming(params) do - {:ok, Object.normalize(activity)} - else - {:error, {:reject, nil}} -> - {:reject, nil} - - object = %Object{} -> - {:ok, object} - - _e -> - 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)} - e -> e - end - end - end - end - - def fetch_and_contain_remote_object_from_id(id) do - Logger.info("Fetching object #{id} via AP") - - with true <- String.starts_with?(id, "http"), - {:ok, %{body: body, status: code}} when code in 200..299 <- - @httpoison.get( - id, - [{:Accept, "application/activity+json"}] - ), - {:ok, data} <- Jason.decode(body), - :ok <- Transmogrifier.contain_origin_from_id(id, data) do - {:ok, data} - else - e -> - {:error, e} - end - end - # filter out broken threads def contain_broken_threads(%Activity{} = activity, %User{} = user) do entire_thread_visible_for_user?(activity, user) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 3331ebebd..0b80566bf 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do alias Pleroma.Activity alias Pleroma.Object + alias Pleroma.Object.Fetcher alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ObjectView @@ -173,7 +174,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do "Signature missing or not from author, relayed Create message, fetching object from source" ) - ActivityPub.fetch_object_from_id(params["object"]["id"]) + Fetcher.fetch_object_from_id(params["object"]["id"]) json(conn, "ok") end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 39cd31921..52666a409 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -8,8 +8,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do """ alias Pleroma.Activity alias Pleroma.Object + alias Pleroma.Object.Containment alias Pleroma.Repo alias Pleroma.User + alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility @@ -18,56 +20,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do require Logger - def get_actor(%{"actor" => actor}) when is_binary(actor) do - actor - end - - def get_actor(%{"actor" => actor}) when is_list(actor) do - if is_binary(Enum.at(actor, 0)) do - Enum.at(actor, 0) - else - Enum.find(actor, fn %{"type" => type} -> type in ["Person", "Service", "Application"] end) - |> Map.get("id") - end - end - - def get_actor(%{"actor" => %{"id" => id}}) when is_bitstring(id) do - id - end - - def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do - get_actor(%{"actor" => actor}) - end - - @doc """ - Checks that an imported AP object's actor matches the domain it came from. - """ - def contain_origin(_id, %{"actor" => nil}), do: :error - - def contain_origin(id, %{"actor" => _actor} = params) do - id_uri = URI.parse(id) - actor_uri = URI.parse(get_actor(params)) - - if id_uri.host == actor_uri.host do - :ok - else - :error - end - end - - def contain_origin_from_id(_id, %{"id" => nil}), do: :error - - def contain_origin_from_id(id, %{"id" => other_id} = _params) do - id_uri = URI.parse(id) - other_uri = URI.parse(other_id) - - if id_uri.host == other_uri.host do - :ok - else - :error - end - end - @doc """ Modifies an incoming AP object (mastodon format) to our internal format. """ @@ -188,7 +140,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def fix_actor(%{"attributedTo" => actor} = object) do object - |> Map.put("actor", get_actor(%{"actor" => actor})) + |> Map.put("actor", Containment.get_actor(%{"actor" => actor})) end # Check for standardisation @@ -223,7 +175,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do "" end - case fetch_obj_helper(in_reply_to_id) do + case get_obj_helper(in_reply_to_id) do {:ok, replied_object} -> with %Activity{} = _activity <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do @@ -448,7 +400,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do # - emoji def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data) when objtype in ["Article", "Note", "Video", "Page"] do - actor = get_actor(data) + actor = Containment.get_actor(data) data = Map.put(data, "actor", actor) @@ -506,7 +458,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def handle_incoming( %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data ) do - with actor <- get_actor(data), + with actor <- Containment.get_actor(data), %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"), @@ -532,7 +484,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def handle_incoming( %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data ) do - with actor <- get_actor(data), + with actor <- Containment.get_actor(data), %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"), @@ -556,9 +508,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def handle_incoming( %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data ) do - with actor <- get_actor(data), + with actor <- Containment.get_actor(data), %User{} = actor <- User.get_or_fetch_by_ap_id(actor), - {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id), + {:ok, object} <- get_obj_helper(object_id), {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do {:ok, activity} else @@ -569,9 +521,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def handle_incoming( %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data ) do - with actor <- get_actor(data), + with actor <- Containment.get_actor(data), %User{} = actor <- User.get_or_fetch_by_ap_id(actor), - {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id), + {:ok, object} <- get_obj_helper(object_id), public <- Visibility.is_public?(data), {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do {:ok, activity} @@ -585,7 +537,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"] @@ -624,10 +576,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do ) do object_id = Utils.get_ap_id(object_id) - with actor <- get_actor(data), + with actor <- Containment.get_actor(data), %User{} = actor <- User.get_or_fetch_by_ap_id(actor), - {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id), - :ok <- contain_origin(actor.ap_id, object.data), + {:ok, object} <- get_obj_helper(object_id), + :ok <- Containment.contain_origin(actor.ap_id, object.data), {:ok, activity} <- ActivityPub.delete(object, false) do {:ok, activity} else @@ -643,9 +595,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do "id" => id } = data ) do - with actor <- get_actor(data), + with actor <- Containment.get_actor(data), %User{} = actor <- User.get_or_fetch_by_ap_id(actor), - {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id), + {:ok, object} <- get_obj_helper(object_id), {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do {:ok, activity} else @@ -713,9 +665,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do "id" => id } = data ) do - with actor <- get_actor(data), + with actor <- Containment.get_actor(data), %User{} = actor <- User.get_or_fetch_by_ap_id(actor), - {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id), + {:ok, object} <- get_obj_helper(object_id), {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do {:ok, activity} else @@ -725,9 +677,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def handle_incoming(_), do: :error - def fetch_obj_helper(id) when is_bitstring(id), do: ActivityPub.fetch_object_from_id(id) - def fetch_obj_helper(obj) when is_map(obj), do: ActivityPub.fetch_object_from_id(obj["id"]) - def get_obj_helper(id) do if object = Object.normalize(id), do: {:ok, object}, else: nil end @@ -764,9 +713,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do # internal -> Mastodon # """ - def prepare_outgoing(%{"type" => "Create", "object" => object} = data) do + def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do object = - object + Object.normalize(object_id).data |> prepare_object data = @@ -827,7 +776,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def maybe_fix_object_url(data) do if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do - case fetch_obj_helper(data["object"]) do + case get_obj_helper(data["object"]) do {:ok, relative_object} -> if relative_object.data["external_url"] do _data = @@ -1015,7 +964,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/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index ccc9da7c6..581b9d1ab 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -234,14 +234,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do @doc """ Inserts a full object if it is contained in an activity. """ - def insert_full_object(%{"object" => %{"type" => type} = object_data}) + def insert_full_object(%{"object" => %{"type" => type} = object_data} = map) when is_map(object_data) and type in @supported_object_types do with {:ok, object} <- Object.create(object_data) do - {:ok, object} + map = + map + |> Map.put("object", object.data["id"]) + + {:ok, map, object} end end - def insert_full_object(_), do: {:ok, nil} + def insert_full_object(map), do: {:ok, map, nil} def update_object_in_activities(%{data: %{"id" => id}} = object) do # TODO diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index db52fe933..6dee61dd6 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -41,16 +41,21 @@ defmodule Pleroma.Web.ActivityPub.Visibility do # guard def entire_thread_visible_for_user?(nil, _user), do: false - # child + # XXX: Probably even more inefficient than the previous implementation intended to be a placeholder untill https://git.pleroma.social/pleroma/pleroma/merge_requests/971 is in develop + # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength + def entire_thread_visible_for_user?( - %Activity{data: %{"object" => %{"inReplyTo" => parent_id}}} = tail, + %Activity{} = tail, + # %Activity{data: %{"object" => %{"inReplyTo" => parent_id}}} = tail, user - ) - when is_binary(parent_id) do - parent = Activity.get_in_reply_to_activity(tail) - visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user) + ) do + case Object.normalize(tail) do + %{data: %{"inReplyTo" => parent_id}} when is_binary(parent_id) -> + parent = Activity.get_in_reply_to_activity(tail) + visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user) + + _ -> + visible_for_user?(tail, user) + end end - - # root - def entire_thread_visible_for_user?(tail, user), do: visible_for_user?(tail, user) end 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 74babdf14..cfbc5dc10 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -125,7 +125,10 @@ defmodule Pleroma.Web.CommonAPI do "public" in_reply_to -> - Pleroma.Web.MastodonAPI.StatusView.get_visibility(in_reply_to.data["object"]) + # XXX: these heuristics should be moved out of MastodonAPI. + with %Object{} = object <- Object.normalize(in_reply_to) do + Pleroma.Web.MastodonAPI.StatusView.get_visibility(object) + end end end @@ -214,8 +217,10 @@ defmodule Pleroma.Web.CommonAPI do with %Activity{ actor: ^user_ap_id, data: %{ - "type" => "Create", - "object" => %{ + "type" => "Create" + }, + object: %Object{ + data: %{ "to" => object_to, "type" => "Note" } @@ -279,7 +284,7 @@ defmodule Pleroma.Web.CommonAPI do 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 185292878..887f878c4 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -208,7 +208,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do context, content_html, attachments, - inReplyTo, + in_reply_to, tags, cw \\ nil, cc \\ [] @@ -225,9 +225,11 @@ defmodule Pleroma.Web.CommonAPI.Utils do "tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq() } - if inReplyTo do + if in_reply_to do + in_reply_to_object = Object.normalize(in_reply_to) + object - |> Map.put("inReplyTo", inReplyTo.data["object"]["id"]) + |> Map.put("inReplyTo", in_reply_to_object.data["id"]) else object end @@ -282,7 +284,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 c47328e13..29e178ba9 100644 --- a/lib/pleroma/web/federator/federator.ex +++ b/lib/pleroma/web/federator/federator.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.Federator do alias Pleroma.Activity + alias Pleroma.Object.Containment alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Relay @@ -136,7 +137,7 @@ defmodule Pleroma.Web.Federator do # actor shouldn't be acting on objects outside their own AP server. with {:ok, _user} <- ap_enabled_actor(params["actor"]), nil <- Activity.normalize(params["id"]), - :ok <- Transmogrifier.contain_origin_from_id(params["actor"], params), + :ok <- Containment.contain_origin_from_id(params["actor"], params), {:ok, activity} <- Transmogrifier.handle_incoming(params) do {:ok, activity} else @@ -185,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 63fadce38..0ba8d9eea 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -4,13 +4,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do use Pleroma.Web, :controller - alias Ecto.Changeset alias Pleroma.Activity alias Pleroma.Config alias Pleroma.Filter alias Pleroma.Notification alias Pleroma.Object + alias Pleroma.Object.Fetcher alias Pleroma.Pagination alias Pleroma.Repo alias Pleroma.ScheduledActivity @@ -190,7 +190,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do "static_url" => url, "visible_in_picker" => true, "url" => url, - "tags" => String.split(tags, ",") + "tags" => tags } end) end @@ -304,7 +304,7 @@ 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"]) do activities = ActivityPub.fetch_user_activities(user, reading_user, params) conn @@ -338,7 +338,7 @@ 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 conn |> put_view(StatusView) @@ -487,7 +487,8 @@ 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 conn |> put_view(StatusView) |> try_render("status.json", %{activity: announce, for: user, as: :activity}) @@ -496,7 +497,7 @@ 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 conn |> put_view(StatusView) |> try_render("status.json", %{activity: activity, for: user, as: :activity}) @@ -543,10 +544,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Activity{} = activity <- Activity.get_by_id(id), - %User{} = user <- User.get_by_nickname(user.nickname), + with %Activity{} = activity <- Activity.get_by_id_with_object(id), + %Object{} = object <- Object.normalize(activity), + %User{} = user <- User.get_cached_by_nickname(user.nickname), true <- Visibility.visible_for_user?(activity, user), - {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do + {:ok, user} <- User.bookmark(user, object.data["id"]) do conn |> put_view(StatusView) |> try_render("status.json", %{activity: activity, for: user, as: :activity}) @@ -554,10 +556,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Activity{} = activity <- Activity.get_by_id(id), - %User{} = user <- User.get_by_nickname(user.nickname), + with %Activity{} = activity <- Activity.get_by_id_with_object(id), + %Object{} = object <- Object.normalize(activity), + %User{} = user <- User.get_cached_by_nickname(user.nickname), true <- Visibility.visible_for_user?(activity, user), - {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do + {:ok, user} <- User.unbookmark(user, object.data["id"]) do conn |> put_view(StatusView) |> try_render("status.json", %{activity: activity, for: user, as: :activity}) @@ -681,7 +684,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end def favourited_by(conn, %{"id" => id}) do - with %Activity{data: %{"object" => %{"likes" => likes}}} <- Activity.get_by_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) users = Repo.all(q) @@ -694,7 +698,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end def reblogged_by(conn, %{"id" => id}) do - with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Activity.get_by_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) users = Repo.all(q) @@ -745,7 +750,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 @@ -762,7 +767,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 @@ -787,7 +792,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) @@ -801,7 +806,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) @@ -817,8 +822,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}) @@ -826,19 +830,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") @@ -881,7 +872,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) @@ -895,7 +886,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) @@ -916,7 +907,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 @@ -931,7 +922,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 @@ -997,7 +988,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do def status_search(user, query) do fetched = if Regex.match?(~r/https?:/, query) do - with {:ok, object} <- ActivityPub.fetch_object_from_id(query), + with {:ok, object} <- Fetcher.fetch_object_from_id(query), %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), true <- Visibility.visible_for_user?(activity, user) do [activity] @@ -1008,13 +999,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do q = from( - a in Activity, + [a, o] in Activity.with_preloaded_object(Activity), where: fragment("?->>'type' = 'Create'", a.data), where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients, where: fragment( - "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)", - a.data, + "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)", + o.data, ^query ), limit: 20, @@ -1096,8 +1087,45 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> render("index.json", %{activities: activities, for: user, as: :activity}) end + 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, _) do - user = User.get_by_id(user.id) + user = User.get_cached_by_id(user.id) activities = user.bookmarks @@ -1154,7 +1182,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) @@ -1166,7 +1194,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) @@ -1459,7 +1487,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..d87fdb15d 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 = diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index a9f607aa5..7dd80d708 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do alias Pleroma.Activity alias Pleroma.HTML + alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.CommonAPI @@ -19,8 +20,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do defp get_replied_to_activities(activities) do activities |> Enum.map(fn - %{data: %{"type" => "Create", "object" => %{"inReplyTo" => in_reply_to}}} -> - in_reply_to != "" && in_reply_to + %{data: %{"type" => "Create", "object" => object}} -> + object = Object.normalize(object) + object.data["inReplyTo"] != "" && object.data["inReplyTo"] _ -> nil @@ -29,7 +31,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do |> Activity.create_by_object_ap_id() |> Repo.all() |> Enum.reduce(%{}, fn activity, acc -> - Map.put(acc, activity.data["object"]["id"], activity) + object = Object.normalize(activity) + Map.put(acc, object.data["id"], activity) end) end @@ -55,8 +58,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do defp get_context_id(_), do: nil defp reblogged?(activity, user) do - object = activity.data["object"] || %{} - present?(user && user.ap_id in (object["announcements"] || [])) + object = Object.normalize(activity) || %{} + present?(user && user.ap_id in (object.data["announcements"] || [])) end def render("index.json", opts) do @@ -80,6 +83,10 @@ 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] && activity_object.data["id"] in opts[:for].bookmarks + mentions = activity.recipients |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end) @@ -100,8 +107,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, @@ -122,14 +129,16 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do } end - def render("status.json", %{activity: %{data: %{"object" => object}} = activity} = opts) do + def render("status.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do + object = Object.normalize(activity) + user = get_user(activity.data["actor"]) - like_count = object["like_count"] || 0 - announcement_count = object["announcement_count"] || 0 + like_count = object.data["like_count"] || 0 + announcement_count = object.data["announcement_count"] || 0 - tags = object["tag"] || [] - sensitive = object["sensitive"] || Enum.member?(tags, "nsfw") + tags = object.data["tag"] || [] + sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw") mentions = activity.recipients @@ -137,15 +146,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do |> Enum.filter(& &1) |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end) - favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || []) - bookmarked = opts[:for] && object["id"] in opts[:for].bookmarks + favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || []) - attachment_data = object["attachment"] || [] + bookmarked = opts[:for] && object.data["id"] in opts[:for].bookmarks + + attachment_data = object.data["attachment"] || [] attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment) - created_at = Utils.to_masto_date(object["published"]) + created_at = Utils.to_masto_date(object.data["published"]) reply_to = get_reply_to(activity, opts) + reply_to_user = reply_to && get_user(reply_to.data["actor"]) content = @@ -167,7 +178,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do "mastoapi:content" ) - summary = object["summary"] || "" + summary = object.data["summary"] || "" summary_html = summary @@ -190,12 +201,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do if user.local do Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity) else - object["external_url"] || object["id"] + object.data["external_url"] || object.data["id"] end %{ id: to_string(activity.id), - uri: object["id"], + uri: object.data["id"], url: url, account: AccountView.render("account.json", %{user: user}), in_reply_to_id: reply_to && to_string(reply_to.id), @@ -205,7 +216,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do content: content_html, created_at: created_at, reblogs_count: announcement_count, - replies_count: object["repliesCount"] || 0, + replies_count: object.data["repliesCount"] || 0, favourites_count: like_count, reblogged: reblogged?(activity, opts[:for]), favourited: present?(favorited), @@ -223,10 +234,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do website: nil }, language: nil, - emojis: build_emojis(activity.data["object"]["emoji"]), + emojis: build_emojis(object.data["emoji"]), 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} } @@ -305,15 +317,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do end def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do - with nil <- replied_to_activities[activity.data["object"]["inReplyTo"]] do + object = Object.normalize(activity) + + with nil <- replied_to_activities[object.data["inReplyTo"]] do # If user didn't participate in the thread Activity.get_in_reply_to_activity(activity) end end - def get_reply_to(%{data: %{"object" => object}}, _) do - if object["inReplyTo"] && object["inReplyTo"] != "" do - Activity.get_create_by_object_ap_id(object["inReplyTo"]) + def get_reply_to(%{data: %{"object" => _object}} = activity, _) do + object = Object.normalize(activity) + + if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do + Activity.get_create_by_object_ap_id(object.data["inReplyTo"]) else nil end @@ -321,8 +337,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do def get_visibility(object) do public = "https://www.w3.org/ns/activitystreams#Public" - to = object["to"] || [] - cc = object["cc"] || [] + to = object.data["to"] || [] + cc = object.data["cc"] || [] cond do public in to -> @@ -343,25 +359,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do end end - def render_content(%{"type" => "Video"} = object) do - with name when not is_nil(name) and name != "" <- object["name"] do - "<p><a href=\"#{object["id"]}\">#{name}</a></p>#{object["content"]}" + def render_content(%{data: %{"type" => "Video"}} = object) do + with name when not is_nil(name) and name != "" <- object.data["name"] do + "<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}" else - _ -> object["content"] || "" + _ -> object.data["content"] || "" end end - def render_content(%{"type" => object_type} = object) + def render_content(%{data: %{"type" => object_type}} = object) when object_type in ["Article", "Page"] do - with summary when not is_nil(summary) and summary != "" <- object["name"], - url when is_bitstring(url) <- object["url"] do - "<p><a href=\"#{url}\">#{summary}</a></p>#{object["content"]}" + with summary when not is_nil(summary) and summary != "" <- object.data["name"], + url when is_bitstring(url) <- object.data["url"] do + "<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}" else - _ -> object["content"] || "" + _ -> object.data["content"] || "" end end - def render_content(object), do: object["content"] || "" + def render_content(object), do: object.data["content"] || "" @doc """ Builds a dictionary tags. 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/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 1a1b74bb0..166691a09 100644 --- a/lib/pleroma/web/ostatus/activity_representer.ex +++ b/lib/pleroma/web/ostatus/activity_representer.ex @@ -54,23 +54,16 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do end) end - defp get_links(%{local: true, data: data}) do + defp get_links(%{local: true}, %{"id" => object_id}) do h = fn str -> [to_charlist(str)] end [ - {:link, [type: ['application/atom+xml'], href: h.(data["object"]["id"]), rel: 'self'], []}, - {:link, [type: ['text/html'], href: h.(data["object"]["id"]), rel: 'alternate'], []} + {:link, [type: ['application/atom+xml'], href: h.(object_id), rel: 'self'], []}, + {:link, [type: ['text/html'], href: h.(object_id), rel: 'alternate'], []} ] end - defp get_links(%{ - local: false, - data: %{ - "object" => %{ - "external_url" => external_url - } - } - }) do + defp get_links(%{local: false}, %{"external_url" => external_url}) do h = fn str -> [to_charlist(str)] end [ @@ -78,7 +71,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do ] end - defp get_links(_activity), do: [] + defp get_links(_activity, _object_data), do: [] defp get_emoji_links(emojis) do Enum.map(emojis, fn {emoji, file} -> @@ -88,14 +81,16 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do def to_simple_form(activity, user, with_author \\ false) - def to_simple_form(%{data: %{"object" => %{"type" => "Note"}}} = activity, user, with_author) do + def to_simple_form(%{data: %{"type" => "Create"}} = activity, user, with_author) do h = fn str -> [to_charlist(str)] end - updated_at = activity.data["object"]["published"] - inserted_at = activity.data["object"]["published"] + object = Object.normalize(activity) + + updated_at = object.data["published"] + inserted_at = object.data["published"] attachments = - Enum.map(activity.data["object"]["attachment"] || [], fn attachment -> + Enum.map(object.data["attachment"] || [], fn attachment -> url = hd(attachment["url"]) {:link, @@ -108,7 +103,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do mentions = activity.recipients |> get_mentions categories = - (activity.data["object"]["tag"] || []) + (object.data["tag"] || []) |> Enum.map(fn tag -> if is_binary(tag) do {:category, [term: to_charlist(tag)], []} @@ -118,11 +113,11 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do end) |> Enum.filter(& &1) - emoji_links = get_emoji_links(activity.data["object"]["emoji"] || %{}) + emoji_links = get_emoji_links(object.data["emoji"] || %{}) summary = - if activity.data["object"]["summary"] do - [{:summary, [], h.(activity.data["object"]["summary"])}] + if object.data["summary"] do + [{:summary, [], h.(object.data["summary"])}] else [] end @@ -131,10 +126,9 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']}, {:"activity:verb", ['http://activitystrea.ms/schema/1.0/post']}, # For notes, federate the object id. - {:id, h.(activity.data["object"]["id"])}, + {:id, h.(object.data["id"])}, {:title, ['New note by #{user.nickname}']}, - {:content, [type: 'html'], - h.(activity.data["object"]["content"] |> String.replace(~r/[\n\r]/, ""))}, + {:content, [type: 'html'], h.(object.data["content"] |> String.replace(~r/[\n\r]/, ""))}, {:published, h.(inserted_at)}, {:updated, h.(updated_at)}, {:"ostatus:conversation", [ref: h.(activity.data["context"])], @@ -142,7 +136,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do {:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []} ] ++ summary ++ - get_links(activity) ++ + get_links(activity, object.data) ++ categories ++ attachments ++ in_reply_to ++ author ++ mentions ++ emoji_links end diff --git a/lib/pleroma/web/ostatus/handlers/note_handler.ex b/lib/pleroma/web/ostatus/handlers/note_handler.ex index db995ec77..ec6e5cfaf 100644 --- a/lib/pleroma/web/ostatus/handlers/note_handler.ex +++ b/lib/pleroma/web/ostatus/handlers/note_handler.ex @@ -113,8 +113,9 @@ defmodule Pleroma.Web.OStatus.NoteHandler do cw <- OStatus.get_cw(entry), in_reply_to <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry), in_reply_to_activity <- fetch_replied_to_activity(entry, in_reply_to), - in_reply_to <- - (in_reply_to_activity && in_reply_to_activity.data["object"]["id"]) || in_reply_to, + in_reply_to_object <- + (in_reply_to_activity && Object.normalize(in_reply_to_activity)) || nil, + in_reply_to <- (in_reply_to_object && in_reply_to_object.data["id"]) || in_reply_to, attachments <- OStatus.get_attachments(entry), context <- get_context(entry, in_reply_to), tags <- OStatus.get_tags(entry), 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/router.ex b/lib/pleroma/web/router.ex index 8b665d61b..ff4f08af5 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 ed45ca735..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 @@ -75,7 +75,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do def remote_follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do if is_status?(acct) do - {:ok, object} = ActivityPub.fetch_object_from_id(acct) + {:ok, object} = Pleroma.Object.Fetcher.fetch_object_from_id(acct) %Activity{id: activity_id} = Activity.get_create_by_object_ap_id(object.data["id"]) redirect(conn, to: "/notice/#{activity_id}") else @@ -101,7 +101,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do end defp is_status?(acct) do - case ActivityPub.fetch_and_contain_remote_object_from_id(acct) do + case Pleroma.Object.Fetcher.fetch_and_contain_remote_object_from_id(acct) do {:ok, %{"type" => type}} when type in ["Article", "Note", "Video", "Page", "Question"] -> true @@ -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 d6ce0a7c6..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 @@ -266,6 +266,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do defp parse_int(_, default), do: default + # TODO: unify the search query with MastoAPI one and do only pagination here def search(_user, %{"q" => query} = params) do limit = parse_int(params["rpp"], 20) page = parse_int(params["page"], 1) @@ -273,13 +274,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do q = from( - a in Activity, + [a, o] in Activity.with_preloaded_object(Activity), where: fragment("?->>'type' = 'Create'", a.data), where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients, where: fragment( - "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)", - a.data, + "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)", + o.data, ^query ), limit: ^limit, 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/activity_view.ex b/lib/pleroma/web/twitter_api/views/activity_view.ex index ecb2b437b..c64152da8 100644 --- a/lib/pleroma/web/twitter_api/views/activity_view.ex +++ b/lib/pleroma/web/twitter_api/views/activity_view.ex @@ -224,15 +224,17 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do def render( "activity.json", - %{activity: %{data: %{"type" => "Create", "object" => object}} = activity} = opts + %{activity: %{data: %{"type" => "Create", "object" => object_id}} = activity} = opts ) do user = get_user(activity.data["actor"], opts) - created_at = object["published"] |> Utils.date_to_asctime() - like_count = object["like_count"] || 0 - announcement_count = object["announcement_count"] || 0 - favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || []) - repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || []) + object = Object.normalize(object_id) + + created_at = object.data["published"] |> Utils.date_to_asctime() + like_count = object.data["like_count"] || 0 + announcement_count = object.data["announcement_count"] || 0 + favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || []) + repeated = opts[:for] && opts[:for].ap_id in (object.data["announcements"] || []) pinned = activity.id in user.info.pinned_activities attentions = @@ -245,12 +247,12 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do conversation_id = get_context_id(activity, opts) - tags = activity.data["object"]["tag"] || [] - possibly_sensitive = activity.data["object"]["sensitive"] || Enum.member?(tags, "nsfw") + tags = object.data["tag"] || [] + possibly_sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw") tags = if possibly_sensitive, do: Enum.uniq(["nsfw" | tags]), else: tags - {summary, content} = render_content(object) + {summary, content} = render_content(object.data) html = content @@ -259,7 +261,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do activity, "twitterapi:content" ) - |> Formatter.emojify(object["emoji"]) + |> Formatter.emojify(object.data["emoji"]) text = if content do @@ -284,7 +286,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do %{ "id" => activity.id, - "uri" => activity.data["object"]["id"], + "uri" => object.data["id"], "user" => UserView.render("show.json", %{user: user, for: opts[:for]}), "statusnet_html" => html, "text" => text, @@ -297,20 +299,20 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do "in_reply_to_ostatus_uri" => reply_user && reply_user.ap_id, "in_reply_to_user_id" => reply_user && reply_user.id, "statusnet_conversation_id" => conversation_id, - "attachments" => (object["attachment"] || []) |> ObjectRepresenter.enum_to_list(opts), + "attachments" => (object.data["attachment"] || []) |> ObjectRepresenter.enum_to_list(opts), "attentions" => attentions, "fave_num" => like_count, "repeat_num" => announcement_count, "favorited" => !!favorited, "repeated" => !!repeated, "pinned" => pinned, - "external_url" => object["external_url"] || object["id"], + "external_url" => object.data["external_url"] || object.data["id"], "tags" => tags, "activity_type" => "post", "possibly_sensitive" => possibly_sensitive, "visibility" => StatusView.get_visibility(object), "summary" => summary, - "summary_html" => summary |> Formatter.emojify(object["emoji"]), + "summary_html" => summary |> Formatter.emojify(object.data["emoji"]), "card" => card, "muted" => CommonAPI.thread_muted?(user, activity) || User.mutes?(opts[:for], user) } 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 -> |