diff options
author | Egor Kislitsyn <egor@kislitsyn.com> | 2019-04-22 13:08:30 +0700 |
---|---|---|
committer | Egor Kislitsyn <egor@kislitsyn.com> | 2019-04-22 13:08:30 +0700 |
commit | e8c2f9a73a37636a9a8ed5c2998617b841f482da (patch) | |
tree | 4b194e631d2bbb58b8522c3d74a0b0c2ea09fedd /lib | |
parent | 38926a063a39c4d4387049f5d364f72a9f8bd2de (diff) | |
parent | 7fcbda702e76b6390076c28832f5aea80086d15a (diff) | |
download | pleroma-e8c2f9a73a37636a9a8ed5c2998617b841f482da.tar.gz |
Merge remote-tracking branch 'pleroma/develop' into feature/disable-account
Diffstat (limited to 'lib')
51 files changed, 1160 insertions, 622 deletions
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..2754dd876 --- /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/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 8f8d86a11..6cee8d630 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -24,10 +24,12 @@ defmodule Mix.Tasks.Pleroma.Instance do - `--domain DOMAIN` - the domain of your instance - `--instance-name INSTANCE_NAME` - the name of your instance - `--admin-email ADMIN_EMAIL` - the email address of the instance admin + - `--notify-email NOTIFY_EMAIL` - email address for notifications - `--dbhost HOSTNAME` - the hostname of the PostgreSQL database to use - `--dbname DBNAME` - the name of the database to use - `--dbuser DBUSER` - the user (aka role) to use for the database connection - `--dbpass DBPASS` - the password to use for the database connection + - `--indexable Y/N` - Allow/disallow indexing site by search engines """ def run(["gen" | rest]) do @@ -41,10 +43,12 @@ defmodule Mix.Tasks.Pleroma.Instance do domain: :string, instance_name: :string, admin_email: :string, + notify_email: :string, dbhost: :string, dbname: :string, dbuser: :string, - dbpass: :string + dbpass: :string, + indexable: :string ], aliases: [ o: :output, @@ -61,7 +65,7 @@ defmodule Mix.Tasks.Pleroma.Instance do will_overwrite = Enum.filter(paths, &File.exists?/1) proceed? = Enum.empty?(will_overwrite) or Keyword.get(options, :force, false) - unless not proceed? do + if proceed? do [domain, port | _] = String.split( Common.get_option( @@ -81,6 +85,14 @@ defmodule Mix.Tasks.Pleroma.Instance do email = Common.get_option(options, :admin_email, "What is your admin email address?") + notify_email = + Common.get_option( + options, + :notify_email, + "What email address do you want to use for sending email notifications?", + email + ) + indexable = Common.get_option( options, @@ -122,6 +134,7 @@ defmodule Mix.Tasks.Pleroma.Instance do domain: domain, port: port, email: email, + notify_email: notify_email, name: name, dbhost: dbhost, dbname: dbname, diff --git a/lib/mix/tasks/pleroma/sample_config.eex b/lib/mix/tasks/pleroma/sample_config.eex index 1c935c0d8..52bd57cb7 100644 --- a/lib/mix/tasks/pleroma/sample_config.eex +++ b/lib/mix/tasks/pleroma/sample_config.eex @@ -13,6 +13,7 @@ config :pleroma, Pleroma.Web.Endpoint, config :pleroma, :instance, name: "<%= name %>", email: "<%= email %>", + notify_email: "<%= notify_email %>", limit: 5000, registrations_open: true, dedupe_media: false @@ -75,4 +76,3 @@ config :web_push_encryption, :vapid_details, # storage_url: "https://swift-endpoint.prodider.com/v1/AUTH_<tenant>/<container>", # object_url: "https://cdn-endpoint.provider.com/<container>" # - diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index d06fd917d..9c1c804e0 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( @@ -200,21 +208,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) @@ -222,6 +236,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) @@ -250,52 +265,6 @@ defmodule Pleroma.Activity do |> Repo.all() end - def increase_replies_count(id) do - Activity - |> where(id: ^id) - |> update([a], - 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(id) do - Activity - |> where(id: ^id) - |> update([a], - 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 - def restrict_deactivated_users(query) do from(activity in query, where: diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex index afefccec5..df0f72f96 100644 --- a/lib/pleroma/emails/admin_email.ex +++ b/lib/pleroma/emails/admin_email.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.AdminEmail do +defmodule Pleroma.Emails.AdminEmail do @moduledoc "Admin emails" import Swoosh.Email @@ -11,7 +11,10 @@ defmodule Pleroma.AdminEmail do defp instance_config, do: Pleroma.Config.get(:instance) defp instance_name, do: instance_config()[:name] - defp instance_email, do: instance_config()[:email] + + defp instance_notify_email do + Keyword.get(instance_config(), :notify_email, instance_config()[:email]) + end defp user_url(user) do Helpers.o_status_url(Pleroma.Web.Endpoint, :feed_redirect, user.nickname) @@ -59,7 +62,7 @@ defmodule Pleroma.AdminEmail do new() |> to({to.name, to.email}) - |> from({instance_name(), instance_email()}) + |> from({instance_name(), instance_notify_email()}) |> reply_to({reporter.name, reporter.email}) |> subject("#{instance_name()} Report") |> html_body(html_body) diff --git a/lib/pleroma/emails/mailer.ex b/lib/pleroma/emails/mailer.ex index b384e6fec..53f5a661c 100644 --- a/lib/pleroma/emails/mailer.ex +++ b/lib/pleroma/emails/mailer.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Mailer do +defmodule Pleroma.Emails.Mailer do use Swoosh.Mailer, otp_app: :pleroma def deliver_async(email, config \\ []) do diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex index a3a09e96c..8502a0d0c 100644 --- a/lib/pleroma/emails/user_email.ex +++ b/lib/pleroma/emails/user_email.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.UserEmail do +defmodule Pleroma.Emails.UserEmail do @moduledoc "User emails" import Swoosh.Email @@ -15,7 +15,8 @@ defmodule Pleroma.UserEmail do defp instance_name, do: instance_config()[:name] defp sender do - {instance_name(), instance_config()[:email]} + email = Keyword.get(instance_config(), :notify_email, instance_config()[:email]) + {instance_name(), email} end defp recipient(email, nil), do: email 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/formatter.ex b/lib/pleroma/formatter.ex index 8ea9dbd38..dab8910c1 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -9,20 +9,31 @@ defmodule Pleroma.Formatter do alias Pleroma.Web.MediaProxy @safe_mention_regex ~r/^(\s*(?<mentions>@.+?\s+)+)(?<rest>.*)/ + @link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui @markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/ - @link_regex ~r{((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+}ui - # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength @auto_linker_config hashtag: true, hashtag_handler: &Pleroma.Formatter.hashtag_handler/4, mention: true, mention_handler: &Pleroma.Formatter.mention_handler/4 + def escape_mention_handler("@" <> nickname = mention, buffer, _, _) do + case User.get_cached_by_nickname(nickname) do + %User{} -> + # escape markdown characters with `\\` + # (we don't want something like @user__name to be parsed by markdown) + String.replace(mention, @markdown_characters_regex, "\\\\\\1") + + _ -> + buffer + end + end + def mention_handler("@" <> nickname, buffer, opts, acc) do case User.get_cached_by_nickname(nickname) do %User{id: id} = user -> ap_id = get_ap_id(user) - nickname_text = get_nickname_text(nickname, opts) |> maybe_escape(opts) + nickname_text = get_nickname_text(nickname, opts) link = "<span class='h-card'><a data-user='#{id}' class='u-url mention' href='#{ap_id}'>@<span>#{ @@ -70,6 +81,25 @@ defmodule Pleroma.Formatter do end end + @doc """ + Escapes a special characters in mention names. + """ + def mentions_escape(text, options \\ []) do + options = + Keyword.merge(options, + mention: true, + url: false, + mention_handler: &Pleroma.Formatter.escape_mention_handler/4 + ) + + if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do + %{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text) + AutoLinker.link(mentions, options) <> AutoLinker.link(rest, options) + else + AutoLinker.link(text, options) + end + end + def emojify(text) do emojify(text, Emoji.get_all()) end @@ -140,10 +170,4 @@ defmodule Pleroma.Formatter do defp get_nickname_text(nickname, %{mentions_format: :full}), do: User.full_nickname(nickname) defp get_nickname_text(nickname, _), do: User.local_nickname(nickname) - - defp maybe_escape(str, %{mentions_escape: true}) do - String.replace(str, @markdown_characters_regex, "\\\\\\1") - end - - defp maybe_escape(str, _), do: str end diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex index 6a56a6f67..2ebc5d5f7 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.data["object"]) 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 7f1dbe28c..4b42d8c9b 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/notification.ex b/lib/pleroma/notification.ex index 941218eea..585157efe 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -105,6 +105,14 @@ defmodule Pleroma.Notification do |> Repo.delete_all() end + def destroy_multiple(%{id: user_id} = _user, ids) do + from(n in Notification, + where: n.id in ^ids, + where: n.user_id == ^user_id + ) + |> Repo.delete_all() + end + def dismiss(%{id: user_id} = _user, id) do notification = Repo.get(Notification, id) @@ -180,8 +188,7 @@ defmodule Pleroma.Notification do def skip?(:muted, activity, user) do actor = activity.data["actor"] - User.mutes?(user, %{ap_id: actor}) or - CommonAPI.thread_muted?(user, activity) + User.mutes?(user, %{ap_id: actor}) or CommonAPI.thread_muted?(user, activity) end def skip?( 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..138e7866f --- /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.data["object"], 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/pagination.ex b/lib/pleroma/pagination.ex index 7c864deef..f435e5c9c 100644 --- a/lib/pleroma/pagination.ex +++ b/lib/pleroma/pagination.ex @@ -36,6 +36,12 @@ defmodule Pleroma.Pagination do limit: :integer } + params = + Enum.reduce(params, %{}, fn + {key, _value}, acc when is_atom(key) -> Map.drop(acc, [key]) + {key, value}, acc -> Map.put(acc, key, value) + end) + changeset = cast({%{}, param_types}, params, Map.keys(param_types)) changeset.changes end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 95f5c6d21..6aaa3244f 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -294,8 +294,10 @@ defmodule Pleroma.User do if user.info.confirmation_pending && Pleroma.Config.get([:instance, :account_activation_required]) do user - |> Pleroma.UserEmail.account_confirmation_email() - |> Pleroma.Mailer.deliver_async() + |> Pleroma.Emails.UserEmail.account_confirmation_email() + |> Pleroma.Emails.Mailer.deliver_async() + + {:ok, :enqueued} else {:ok, :noop} end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index e749a80aa..a345372e2 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -7,13 +7,14 @@ 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 alias Pleroma.User 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 @@ -90,12 +91,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end def increase_replies_count_if_reply(%{ - "object" => - %{"inReplyTo" => reply_ap_id, "inReplyToStatusId" => reply_status_id} = object, + "object" => %{"inReplyTo" => reply_ap_id} = object, "type" => "Create" }) do if is_public?(object) do - Activity.increase_replies_count(reply_status_id) Object.increase_replies_count(reply_ap_id) end end @@ -103,10 +102,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do def increase_replies_count_if_reply(_create_data), do: :noop def decrease_replies_count_if_reply(%Object{ - data: %{"inReplyTo" => reply_ap_id, "inReplyToStatusId" => reply_status_id} = object + data: %{"inReplyTo" => reply_ap_id} = object }) do if is_public?(object) do - Activity.decrease_replies_count(reply_status_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.data["object"]) 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 @@ -449,8 +448,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do :ok <- maybe_federate(activity) do Enum.each(User.all_superusers(), fn superuser -> superuser - |> Pleroma.AdminEmail.report(actor, account, statuses, content) - |> Pleroma.Mailer.deliver_async() + |> Pleroma.Emails.AdminEmail.report(actor, account, statuses, content) + |> Pleroma.Emails.Mailer.deliver_async() end) {:ok, activity} @@ -493,7 +492,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do q |> restrict_unlisted() - |> Repo.all() + |> Pagination.fetch_paginated(opts) |> Enum.reverse() 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 @@ -636,26 +647,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do ) end - defp restrict_limit(query, %{"limit" => limit}) do - from(activity in query, limit: ^limit) - end - - defp restrict_limit(query, _), do: query - defp restrict_local(query, %{"local_only" => true}) do from(activity in query, where: activity.local == true) end defp restrict_local(query, _), do: query - defp restrict_max(query, %{"max_id" => ""}), do: query - - defp restrict_max(query, %{"max_id" => max_id}) do - from(activity in query, where: activity.id < ^max_id) - end - - defp restrict_max(query, _), do: query - defp restrict_actor(query, %{"actor_id" => actor_id}) do from(activity in query, where: activity.actor == ^actor_id) end @@ -681,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 @@ -726,7 +727,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do from( activity in query, where: fragment("not (? = ANY(?))", activity.actor, ^blocks), - where: fragment("not (?->'to' \\?| ?)", activity.data, ^blocks), + where: fragment("not (? && ?)", activity.recipients, ^blocks), + where: + fragment( + "not (?->>'type' = 'Announce' and ?->'to' \\?| ?)", + activity.data, + activity.data, + ^blocks + ), where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks) ) end @@ -776,12 +784,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end def fetch_activities_query(recipients, opts \\ %{}) do - base_query = - from( - activity in Activity, - limit: 20, - order_by: [fragment("? desc nulls last", activity.id)] - ) + base_query = from(activity in Activity) base_query |> maybe_preload_objects(opts) @@ -791,8 +794,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> restrict_tag_all(opts) |> restrict_since(opts) |> restrict_local(opts) - |> restrict_limit(opts) - |> restrict_max(opts) |> restrict_actor(opts) |> restrict_type(opts) |> restrict_favorited_by(opts) @@ -809,14 +810,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do def fetch_activities(recipients, opts \\ %{}) do fetch_activities_query(recipients, opts) - |> Repo.all() + |> Pagination.fetch_paginated(opts) |> Enum.reverse() end def fetch_activities_bounded(recipients_to, recipients_cc, opts \\ %{}) do fetch_activities_query([], opts) |> restrict_to_cc(recipients_to, recipients_cc) - |> Repo.all() + |> Pagination.fetch_paginated(opts) |> Enum.reverse() end @@ -881,7 +882,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)}") @@ -991,60 +992,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 7091d6927..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 @@ -153,9 +154,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do - with %User{} = user <- User.get_cached_by_nickname(nickname), - true <- Utils.recipient_in_message(user.ap_id, params), - params <- Utils.maybe_splice_recipient(user.ap_id, params) do + with %User{} = recipient <- User.get_cached_by_nickname(nickname), + %User{} = actor <- User.get_or_fetch_by_ap_id(params["actor"]), + true <- Utils.recipient_in_message(recipient, actor, params), + params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do Federator.incoming_ap_doc(params) json(conn, "ok") end @@ -172,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 49ea73204..a80aa52c6 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,14 +175,13 @@ 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 <- + with %Activity{} = _activity <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do object |> Map.put("inReplyTo", replied_object.data["id"]) |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id) - |> Map.put("inReplyToStatusId", activity.id) |> Map.put("conversation", replied_object.data["context"] || object["conversation"]) |> Map.put("context", replied_object.data["context"] || object["conversation"]) else @@ -449,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) @@ -507,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"), @@ -533,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"), @@ -557,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 @@ -570,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} @@ -625,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 @@ -644,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 @@ -714,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 @@ -726,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 @@ -765,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 = @@ -828,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 = diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 0b53f71c3..581b9d1ab 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -52,7 +52,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do defp recipient_in_collection(ap_id, coll) when is_list(coll), do: ap_id in coll defp recipient_in_collection(_, _), do: false - def recipient_in_message(ap_id, params) do + def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params) do cond do recipient_in_collection(ap_id, params["to"]) -> true @@ -71,6 +71,11 @@ defmodule Pleroma.Web.ActivityPub.Utils do !params["to"] && !params["cc"] && !params["bto"] && !params["bcc"] -> true + # if the message is sent from somebody the user is following, then assume it + # is addressed to the recipient + User.following?(recipient, actor) -> + true + true -> false end @@ -229,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 70a5b5c5d..c436715d5 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -238,8 +238,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do !Pleroma.Config.get([:instance, :registrations_open]), {:ok, invite_token} <- UserInviteToken.create_invite(), email <- - Pleroma.UserEmail.user_invitation_email(user, invite_token, email, params["name"]), - {:ok, _} <- Pleroma.Mailer.deliver(email) do + Pleroma.Emails.UserEmail.user_invitation_email( + user, + invite_token, + email, + params["name"] + ), + {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do json_response(conn, :no_content, "") end end diff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex index 89d88af32..b02f595dc 100644 --- a/lib/pleroma/web/auth/authenticator.ex +++ b/lib/pleroma/web/auth/authenticator.ex @@ -13,21 +13,21 @@ defmodule Pleroma.Web.Auth.Authenticator do ) end - @callback get_user(Plug.Conn.t(), Map.t()) :: {:ok, User.t()} | {:error, any()} - def get_user(plug, params), do: implementation().get_user(plug, params) + @callback get_user(Plug.Conn.t()) :: {:ok, User.t()} | {:error, any()} + def get_user(plug), do: implementation().get_user(plug) - @callback create_from_registration(Plug.Conn.t(), Map.t(), Registration.t()) :: + @callback create_from_registration(Plug.Conn.t(), Registration.t()) :: {:ok, User.t()} | {:error, any()} - def create_from_registration(plug, params, registration), - do: implementation().create_from_registration(plug, params, registration) + def create_from_registration(plug, registration), + do: implementation().create_from_registration(plug, registration) - @callback get_registration(Plug.Conn.t(), Map.t()) :: + @callback get_registration(Plug.Conn.t()) :: {:ok, Registration.t()} | {:error, any()} - def get_registration(plug, params), - do: implementation().get_registration(plug, params) + def get_registration(plug), do: implementation().get_registration(plug) @callback handle_error(Plug.Conn.t(), any()) :: any() - def handle_error(plug, error), do: implementation().handle_error(plug, error) + def handle_error(plug, error), + do: implementation().handle_error(plug, error) @callback auth_template() :: String.t() | nil def auth_template do diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex index 8b6d5a77f..363c99597 100644 --- a/lib/pleroma/web/auth/ldap_authenticator.ex +++ b/lib/pleroma/web/auth/ldap_authenticator.ex @@ -13,14 +13,16 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do @connection_timeout 10_000 @search_timeout 10_000 - defdelegate get_registration(conn, params), to: @base + defdelegate get_registration(conn), to: @base + defdelegate create_from_registration(conn, registration), to: @base + defdelegate handle_error(conn, error), to: @base + defdelegate auth_template, to: @base + defdelegate oauth_consumer_template, to: @base - defdelegate create_from_registration(conn, params, registration), to: @base - - def get_user(%Plug.Conn{} = conn, params) do + def get_user(%Plug.Conn{} = conn) do if Pleroma.Config.get([:ldap, :enabled]) do {name, password} = - case params do + case conn.params do %{"authorization" => %{"name" => name, "password" => password}} -> {name, password} @@ -34,25 +36,17 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do {:error, {:ldap_connection_error, _}} -> # When LDAP is unavailable, try default authenticator - @base.get_user(conn, params) + @base.get_user(conn) error -> error end else # Fall back to default authenticator - @base.get_user(conn, params) + @base.get_user(conn) end end - def handle_error(%Plug.Conn{} = _conn, error) do - error - end - - def auth_template, do: nil - - def oauth_consumer_template, do: nil - defp ldap_user(name, password) do ldap = Pleroma.Config.get(:ldap, []) host = Keyword.get(ldap, :host, "localhost") diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index c826adb4c..d647f1e05 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -10,9 +10,9 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do @behaviour Pleroma.Web.Auth.Authenticator - def get_user(%Plug.Conn{} = _conn, params) do + def get_user(%Plug.Conn{} = conn) do {name, password} = - case params do + case conn.params do %{"authorization" => %{"name" => name, "password" => password}} -> {name, password} @@ -29,10 +29,9 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do end end - def get_registration( - %Plug.Conn{assigns: %{ueberauth_auth: %{provider: provider, uid: uid} = auth}}, - _params - ) do + def get_registration(%Plug.Conn{ + assigns: %{ueberauth_auth: %{provider: provider, uid: uid} = auth} + }) do registration = Registration.get_by_provider_uid(provider, uid) if registration do @@ -40,7 +39,8 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do else info = auth.info - Registration.changeset(%Registration{}, %{ + %Registration{} + |> Registration.changeset(%{ provider: to_string(provider), uid: to_string(uid), info: %{ @@ -54,13 +54,16 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do end end - def get_registration(%Plug.Conn{} = _conn, _params), do: {:error, :missing_credentials} + def get_registration(%Plug.Conn{} = _conn), do: {:error, :missing_credentials} - def create_from_registration(_conn, params, registration) do - nickname = value([params["nickname"], Registration.nickname(registration)]) - email = value([params["email"], Registration.email(registration)]) - name = value([params["name"], Registration.name(registration)]) || nickname - bio = value([params["bio"], Registration.description(registration)]) + def create_from_registration( + %Plug.Conn{params: %{"authorization" => registration_attrs}}, + registration + ) do + nickname = value([registration_attrs["nickname"], Registration.nickname(registration)]) + email = value([registration_attrs["email"], Registration.email(registration)]) + name = value([registration_attrs["name"], Registration.name(registration)]) || nickname + bio = value([registration_attrs["bio"], Registration.description(registration)]) random_password = :crypto.strong_rand_bytes(64) |> Base.encode64() diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 74babdf14..6458a3449 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" } diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 7b9f0ea06..25f498fcb 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -195,11 +195,10 @@ defmodule Pleroma.Web.CommonAPI.Utils do Formatting text to markdown. """ def format_input(text, "text/markdown", options) do - options = Keyword.put(options, :mentions_escape, true) - text + |> Formatter.mentions_escape(options) + |> Earmark.as_html!() |> Formatter.linkify(options) - |> (fn {text, mentions, tags} -> {Earmark.as_html!(text), mentions, tags} end).() |> Formatter.html_escape("text/html") end @@ -209,7 +208,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do context, content_html, attachments, - inReplyTo, + in_reply_to, tags, cw \\ nil, cc \\ [] @@ -226,10 +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.data["object"]) + object - |> Map.put("inReplyTo", inReplyTo.data["object"]["id"]) - |> Map.put("inReplyToStatusId", inReplyTo.id) + |> Map.put("inReplyTo", in_reply_to_object.data["id"]) else object end diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 1633477c3..7f939991d 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -58,14 +58,9 @@ defmodule Pleroma.Web.Endpoint do do: "__Host-pleroma_key", else: "pleroma_key" - same_site = - if Pleroma.Config.oauth_consumer_enabled?() do - # Note: "SameSite=Strict" prevents sign in with external OAuth provider - # (there would be no cookies during callback request from OAuth provider) - "SameSite=Lax" - else - "SameSite=Strict" - end + extra = + Pleroma.Config.get([__MODULE__, :extra_cookie_attrs]) + |> Enum.join(";") # The session will be stored in the cookie and signed, # this means its contents can be read but not tampered with. @@ -77,7 +72,7 @@ defmodule Pleroma.Web.Endpoint do signing_salt: {Pleroma.Config, :get, [[__MODULE__, :signing_salt], "CqaoopA2"]}, http_only: true, secure: secure_cookies, - extra: same_site + extra: extra ) # Note: the plug and its configuration is compile-time this can't be upstreamed yet diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex index c47328e13..1b4deb6dc 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 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 ed082abdf..d271d3786 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -4,13 +4,14 @@ 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 alias Pleroma.Stats @@ -189,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 @@ -202,15 +203,29 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do params = conn.params - |> Map.drop(["since_id", "max_id"]) + |> Map.drop(["since_id", "max_id", "min_id"]) |> Map.merge(params) last = List.last(activities) - first = List.first(activities) if last do - min = last.id - max = first.id + max_id = last.id + + limit = + params + |> Map.get("limit", "20") + |> String.to_integer() + + min_id = + if length(activities) <= limit do + activities + |> List.first() + |> Map.get(:id) + else + activities + |> Enum.at(limit * -1) + |> Map.get(:id) + end {next_url, prev_url} = if param do @@ -219,13 +234,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do Pleroma.Web.Endpoint, method, param, - Map.merge(params, %{max_id: min}) + Map.merge(params, %{max_id: max_id}) ), mastodon_api_url( Pleroma.Web.Endpoint, method, param, - Map.merge(params, %{since_id: max}) + Map.merge(params, %{min_id: min_id}) ) } else @@ -233,12 +248,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do mastodon_api_url( Pleroma.Web.Endpoint, method, - Map.merge(params, %{max_id: min}) + Map.merge(params, %{max_id: max_id}) ), mastodon_api_url( Pleroma.Web.Endpoint, method, - Map.merge(params, %{since_id: max}) + Map.merge(params, %{min_id: min_id}) ) } end @@ -314,7 +329,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do activities = [user.ap_id] |> ActivityPub.fetch_activities_query(params) - |> Repo.all() + |> Pagination.fetch_paginated(params) conn |> add_link_headers(:dm_timeline, activities) @@ -323,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) @@ -472,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}) @@ -481,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}) @@ -528,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), + with %Activity{} = activity <- Activity.get_by_id_with_object(id), + %Object{} = object <- Object.normalize(activity), %User{} = user <- User.get_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}) @@ -539,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), + with %Activity{} = activity <- Activity.get_by_id_with_object(id), + %Object{} = object <- Object.normalize(activity), %User{} = user <- User.get_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}) @@ -612,6 +630,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end + def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do + Notification.destroy_multiple(user, ids) + json(conn, %{}) + end + def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do id = List.wrap(id) q = from(u in User, where: u.id in ^id) @@ -661,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) @@ -674,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) @@ -795,25 +820,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do - with %User{} = followed <- User.get_by_id(id), - false <- User.following?(follower, followed), - {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do + with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)}, + {_, true} <- {:followed, follower.id != followed.id}, + {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do conn |> put_view(AccountView) |> render("relationship.json", %{user: follower, target: followed}) else - 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}) + {:followed, _} -> + {:error, :not_found} {:error, message} -> conn @@ -823,12 +838,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do - with %User{} = followed <- User.get_by_nickname(uri), + with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)}, + {_, true} <- {:followed, follower.id != followed.id}, {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do conn |> put_view(AccountView) |> render("account.json", %{user: followed, for: follower}) else + {:followed, _} -> + {:error, :not_found} + {:error, message} -> conn |> put_resp_content_type("application/json") @@ -837,11 +856,18 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do - with %User{} = followed <- User.get_by_id(id), + with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)}, + {_, true} <- {:followed, follower.id != followed.id}, {:ok, follower} <- CommonAPI.unfollow(follower, followed) do conn |> put_view(AccountView) |> render("relationship.json", %{user: follower, target: followed}) + else + {:followed, _} -> + {:error, :not_found} + + error -> + error end end @@ -962,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] @@ -973,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, diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index d4a8e4fff..17c33080b 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.data["object"]) + Map.put(acc, object.data["id"], activity) end) end @@ -54,6 +57,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do defp get_context_id(_), do: nil + defp reblogged?(activity, user) do + object = Object.normalize(activity) || %{} + present?(user && user.ap_id in (object.data["announcements"] || [])) + end + def render("index.json", opts) do replied_to_activities = get_replied_to_activities(opts.activities) @@ -72,8 +80,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do user = get_user(activity.data["actor"]) created_at = Utils.to_masto_date(activity.data["published"]) - reblogged = Activity.get_create_by_object_ap_id(object) - reblogged = render("status.json", Map.put(opts, :activity, reblogged)) + 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 @@ -94,9 +106,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do reblogs_count: 0, replies_count: 0, favourites_count: 0, - reblogged: false, - favourited: false, - bookmarked: false, + reblogged: reblogged?(reblogged_activity, opts[:for]), + favourited: present?(favorited), + bookmarked: present?(bookmarked), muted: false, pinned: pinned?(activity, user), sensitive: false, @@ -117,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 @@ -132,16 +146,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do |> Enum.filter(& &1) |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end) - repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || []) - 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 = @@ -163,7 +178,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do "mastoapi:content" ) - summary = object["summary"] || "" + summary = object.data["summary"] || "" summary_html = summary @@ -186,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), @@ -201,9 +216,9 @@ 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: present?(repeated), + reblogged: reblogged?(activity, opts[:for]), favourited: present?(favorited), bookmarked: present?(bookmarked), muted: CommonAPI.thread_muted?(user, activity) || User.mutes?(opts[:for], user), @@ -219,7 +234,7 @@ 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), @@ -301,13 +316,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do end def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do - _id = activity.data["object"]["inReplyTo"] - replied_to_activities[activity.data["object"]["inReplyTo"]] + object = Object.normalize(activity.data["object"]) + + 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 @@ -315,8 +336,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 -> @@ -337,25 +358,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/metadata/rel_me.ex b/lib/pleroma/web/metadata/rel_me.ex new file mode 100644 index 000000000..03af899c4 --- /dev/null +++ b/lib/pleroma/web/metadata/rel_me.ex @@ -0,0 +1,13 @@ +defmodule Pleroma.Web.Metadata.Providers.RelMe do + alias Pleroma.Web.Metadata.Providers.Provider + @behaviour Provider + + @impl Provider + def build_tags(%{user: user}) do + (Floki.attribute(user.bio, "link[rel~=me]", "href") ++ + Floki.attribute(user.bio, "a[rel~=me]", "href")) + |> Enum.map(fn link -> + {:link, [rel: "me", href: link], []} + end) + end +end diff --git a/lib/pleroma/web/oauth/fallback_controller.ex b/lib/pleroma/web/oauth/fallback_controller.ex index afaa00242..e3984f009 100644 --- a/lib/pleroma/web/oauth/fallback_controller.ex +++ b/lib/pleroma/web/oauth/fallback_controller.ex @@ -24,6 +24,6 @@ defmodule Pleroma.Web.OAuth.FallbackController do conn |> put_status(:unauthorized) |> put_flash(:error, "Invalid Username/Password") - |> OAuthController.authorize(conn.params["authorization"]) + |> OAuthController.authorize(conn.params) end end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index bee7084ad..5ea04635d 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) @@ -49,6 +55,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do available_scopes = (app && app.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: params["response_type"], client_id: params["client_id"], @@ -62,18 +69,20 @@ defmodule Pleroma.Web.OAuth.OAuthController do def create_authorization( conn, - %{"authorization" => auth_params} = params, + %{"authorization" => _} = params, opts \\ [] ) do with {:ok, auth} <- do_create_authorization(conn, params, opts[:user]) do - after_create_authorization(conn, auth, auth_params) + after_create_authorization(conn, auth, params) else error -> - handle_create_authorization_error(conn, error, auth_params) + handle_create_authorization_error(conn, error, params) end end - def after_create_authorization(conn, auth, %{"redirect_uri" => redirect_uri} = auth_params) do + def after_create_authorization(conn, auth, %{ + "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs + }) do redirect_uri = redirect_uri(conn, redirect_uri) if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" do @@ -86,8 +95,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do url_params = %{:code => auth.token} url_params = - if auth_params["state"] do - Map.put(url_params, :state, auth_params["state"]) + if auth_attrs["state"] do + Map.put(url_params, :state, auth_attrs["state"]) else url_params end @@ -98,26 +107,34 @@ defmodule Pleroma.Web.OAuth.OAuthController do end end - defp handle_create_authorization_error(conn, {scopes_issue, _}, auth_params) + defp handle_create_authorization_error( + conn, + {scopes_issue, _}, + %{"authorization" => _} = params + ) when scopes_issue in [:unsupported_scopes, :missing_scopes] do # Per https://github.com/tootsuite/mastodon/blob/ # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39 conn |> put_flash(:error, "This action is outside the authorized scopes") |> put_status(:unauthorized) - |> authorize(auth_params) + |> authorize(params) end - defp handle_create_authorization_error(conn, {:auth_active, false}, auth_params) do + defp handle_create_authorization_error( + conn, + {:auth_active, false}, + %{"authorization" => _} = params + ) do # Per https://github.com/tootsuite/mastodon/blob/ # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76 conn |> put_flash(:error, "Your login is missing a confirmed e-mail address") |> put_status(:forbidden) - |> authorize(auth_params) + |> authorize(params) end - defp handle_create_authorization_error(conn, error, _auth_params) do + defp handle_create_authorization_error(conn, error, %{"authorization" => _}) do Authenticator.handle_error(conn, error) end @@ -151,7 +168,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do conn, %{"grant_type" => "password"} = params ) do - with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn, params)}, + with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn)}, %App{} = app <- get_app_from_request(conn, params), {:auth_active, true} <- {:auth_active, User.auth_active?(user)}, {:user_active, true} <- {:user_active, !user.info.deactivated}, @@ -214,19 +231,19 @@ defmodule Pleroma.Web.OAuth.OAuthController do end @doc "Prepares OAuth request to provider for Ueberauth" - def prepare_request(conn, %{"provider" => provider} = params) do + def prepare_request(conn, %{"provider" => provider, "authorization" => auth_attrs}) do scope = - oauth_scopes(params, []) + oauth_scopes(auth_attrs, []) |> Enum.join(" ") state = - params + auth_attrs |> Map.delete("scopes") |> Map.put("scope", scope) |> Poison.encode!() params = - params + auth_attrs |> Map.drop(~w(scope scopes client_id redirect_uri)) |> Map.put("state", state) @@ -260,26 +277,26 @@ defmodule Pleroma.Web.OAuth.OAuthController do def callback(conn, params) do params = callback_params(params) - with {:ok, registration} <- Authenticator.get_registration(conn, params) do + with {:ok, registration} <- Authenticator.get_registration(conn) do user = Repo.preload(registration, :user).user - auth_params = Map.take(params, ~w(client_id redirect_uri scope scopes state)) + auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state)) if user do create_authorization( conn, - %{"authorization" => auth_params}, + %{"authorization" => auth_attrs}, user: user ) else registration_params = - Map.merge(auth_params, %{ + Map.merge(auth_attrs, %{ "nickname" => Registration.nickname(registration), "email" => Registration.email(registration) }) conn |> put_session(:registration_id, registration.id) - |> registration_details(registration_params) + |> registration_details(%{"authorization" => registration_params}) end else _ -> @@ -293,53 +310,44 @@ defmodule Pleroma.Web.OAuth.OAuthController do Map.merge(params, Poison.decode!(state)) end - def registration_details(conn, params) do + def registration_details(conn, %{"authorization" => auth_attrs}) do render(conn, "register.html", %{ - client_id: params["client_id"], - redirect_uri: params["redirect_uri"], - state: params["state"], - scopes: oauth_scopes(params, []), - nickname: params["nickname"], - email: params["email"] + client_id: auth_attrs["client_id"], + redirect_uri: auth_attrs["redirect_uri"], + state: auth_attrs["state"], + scopes: oauth_scopes(auth_attrs, []), + nickname: auth_attrs["nickname"], + email: auth_attrs["email"] }) end - def register(conn, %{"op" => "connect"} = params) do - authorization_params = Map.put(params, "name", params["auth_name"]) - create_authorization_params = %{"authorization" => authorization_params} - + def register(conn, %{"authorization" => _, "op" => "connect"} = params) do with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), %Registration{} = registration <- Repo.get(Registration, registration_id), {_, {:ok, auth}} <- - {:create_authorization, do_create_authorization(conn, create_authorization_params)}, + {:create_authorization, do_create_authorization(conn, params)}, %User{} = user <- Repo.preload(auth, :user).user, {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do conn |> put_session_registration_id(nil) - |> after_create_authorization(auth, authorization_params) + |> after_create_authorization(auth, params) else {:create_authorization, error} -> - {:register, handle_create_authorization_error(conn, error, create_authorization_params)} + {:register, handle_create_authorization_error(conn, error, params)} _ -> {:register, :generic_error} end end - def register(conn, %{"op" => "register"} = params) do + def register(conn, %{"authorization" => _, "op" => "register"} = params) do with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), %Registration{} = registration <- Repo.get(Registration, registration_id), - {:ok, user} <- Authenticator.create_from_registration(conn, params, registration) do + {:ok, user} <- Authenticator.create_from_registration(conn, registration) do conn |> put_session_registration_id(nil) |> create_authorization( - %{ - "authorization" => %{ - "client_id" => params["client_id"], - "redirect_uri" => params["redirect_uri"], - "scopes" => oauth_scopes(params, nil) - } - }, + params, user: user ) else @@ -374,15 +382,15 @@ defmodule Pleroma.Web.OAuth.OAuthController do %{ "client_id" => client_id, "redirect_uri" => redirect_uri - } = auth_params - } = params, + } = auth_attrs + }, user \\ nil ) do with {_, {:ok, %User{} = user}} <- - {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn, params)}, + {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)}, %App{} = app <- Repo.get_by(App, client_id: client_id), true <- redirect_uri in String.split(app.redirect_uris), - scopes <- oauth_scopes(auth_params, []), + scopes <- oauth_scopes(auth_attrs, []), {:unsupported_scopes, []} <- {:unsupported_scopes, scopes -- app.scopes}, # Note: `scope` param is intentionally not optional in this context {:missing_scopes, false} <- {:missing_scopes, scopes == []}, diff --git a/lib/pleroma/web/ostatus/activity_representer.ex b/lib/pleroma/web/ostatus/activity_representer.ex index 1a1b74bb0..b11a2b5ce 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.data["object"]) + + 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/rel_me.ex b/lib/pleroma/web/rel_me.ex index eaca41132..26eb614a6 100644 --- a/lib/pleroma/web/rel_me.ex +++ b/lib/pleroma/web/rel_me.ex @@ -6,7 +6,8 @@ defmodule Pleroma.Web.RelMe do @hackney_options [ pool: :media, recv_timeout: 2_000, - max_body: 2_000_000 + max_body: 2_000_000, + with_body: true ] if Mix.env() == :test do diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index 4bd271d8e..62e8fa610 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -12,7 +12,8 @@ defmodule Pleroma.Web.RichMedia.Parser do @hackney_options [ pool: :media, recv_timeout: 2_000, - max_body: 2_000_000 + max_body: 2_000_000, + with_body: true ] def parse(nil), do: {:error, "No URL provided"} diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index c331098b4..f475de639 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -243,7 +243,6 @@ defmodule Pleroma.Web.Router do get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials) get("/accounts/relationships", MastodonAPIController, :relationships) - get("/accounts/search", MastodonAPIController, :account_search) get("/accounts/:id/lists", MastodonAPIController, :account_lists) get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array) @@ -262,6 +261,7 @@ defmodule Pleroma.Web.Router do post("/notifications/dismiss", MastodonAPIController, :dismiss_notification) get("/notifications", MastodonAPIController, :notifications) get("/notifications/:id", MastodonAPIController, :get_notification) + delete("/notifications/destroy_multiple", MastodonAPIController, :destroy_multiple) get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses) get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status) @@ -377,6 +377,8 @@ defmodule Pleroma.Web.Router do get("/trends", MastodonAPIController, :empty_array) + get("/accounts/search", MastodonAPIController, :account_search) + scope [] do pipe_through(:oauth_read_or_unauthenticated) diff --git a/lib/pleroma/web/templates/layout/app.html.eex b/lib/pleroma/web/templates/layout/app.html.eex index 8333bc921..3389c91cc 100644 --- a/lib/pleroma/web/templates/layout/app.html.eex +++ b/lib/pleroma/web/templates/layout/app.html.eex @@ -179,6 +179,17 @@ flex-basis: 50%; } } + .form-row { + display: flex; + } + .form-row > label { + text-align: left; + line-height: 47px; + flex: 1; + } + .form-row > input { + flex: 2; + } </style> </head> <body> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex index 4b8fb5dae..e6cfe108b 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex @@ -5,7 +5,7 @@ <%= for scope <- @available_scopes do %> <%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %> <div class="scope"> - <%= checkbox @form, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: assigns[:scope_param] || "scope[]" %> + <%= checkbox @form, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %> <%= label @form, :"scope_#{scope}", String.capitalize(scope) %> </div> <% end %> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex index 85f62ca64..4bcda7300 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex @@ -1,6 +1,6 @@ <h2>Sign in with external provider</h2> -<%= form_for @conn, o_auth_path(@conn, :prepare_request), [method: "get"], fn f -> %> +<%= form_for @conn, o_auth_path(@conn, :prepare_request), [as: "authorization", method: "get"], fn f -> %> <%= render @view_module, "_scopes.html", Map.put(assigns, :form, f) %> <%= hidden_input f, :client_id, value: @client_id %> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex index 126390391..facedc8db 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex @@ -8,8 +8,7 @@ <h2>Registration Details</h2> <p>If you'd like to register a new account, please provide the details below.</p> - -<%= form_for @conn, o_auth_path(@conn, :register), [], fn f -> %> +<%= form_for @conn, o_auth_path(@conn, :register), [as: "authorization"], fn f -> %> <div class="input"> <%= label f, :nickname, "Nickname" %> @@ -25,8 +24,8 @@ <p>Alternatively, sign in to connect to existing account.</p> <div class="input"> - <%= label f, :auth_name, "Name or email" %> - <%= text_input f, :auth_name %> + <%= label f, :name, "Name or email" %> + <%= text_input f, :name %> </div> <div class="input"> <%= label f, :password, "Password" %> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex index 87278e636..3e360a52c 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex @@ -17,7 +17,7 @@ <%= password_input f, :password %> </div> -<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f, scope_param: "authorization[scope][]"}) %> +<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %> <%= hidden_input f, :client_id, value: @client_id %> <%= hidden_input f, :response_type, value: @response_type %> diff --git a/lib/pleroma/web/templates/twitter_api/util/password_reset.html.eex b/lib/pleroma/web/templates/twitter_api/util/password_reset.html.eex index 3c7960998..a3facf017 100644 --- a/lib/pleroma/web/templates/twitter_api/util/password_reset.html.eex +++ b/lib/pleroma/web/templates/twitter_api/util/password_reset.html.eex @@ -1,12 +1,13 @@ <h2>Password Reset for <%= @user.nickname %></h2> <%= form_for @conn, util_path(@conn, :password_reset), [as: "data"], fn f -> %> -<%= label f, :password, "Password" %> -<%= password_input f, :password %> -<br> - -<%= label f, :password_confirmation, "Confirmation" %> -<%= password_input f, :password_confirmation %> -<br> -<%= hidden_input f, :token, value: @token.token %> -<%= submit "Reset" %> + <div class="form-row"> + <%= label f, :password, "Password" %> + <%= password_input f, :password %> + </div> + <div class="form-row"> + <%= label f, :password_confirmation, "Confirmation" %> + <%= password_input f, :password_confirmation %> + </div> + <%= hidden_input f, :token, value: @token.token %> + <%= submit "Reset" %> <% end %> diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 44f4b183b..9b0cf2b07 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -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 @@ -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(%{}) @@ -304,7 +304,12 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do end def follow_import(%{assigns: %{user: follower}} = conn, %{"list" => list}) do - with followed_identifiers <- String.split(list), + with lines <- String.split(list, "\n"), + followed_identifiers <- + Enum.map(lines, fn line -> + String.split(line, ",") |> List.first() + end) + |> List.delete("Account address"), {:ok, _} = Task.start(fn -> User.follow_import(follower, followed_identifiers) end) do json(conn, "job started") end diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index bf1051afd..c3f769c00 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -4,10 +4,10 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do alias Pleroma.Activity - alias Pleroma.Mailer + alias Pleroma.Emails.Mailer + alias Pleroma.Emails.UserEmail alias Pleroma.Repo alias Pleroma.User - alias Pleroma.UserEmail alias Pleroma.UserInviteToken alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI @@ -269,6 +269,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) @@ -276,13 +277,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/views/activity_view.ex b/lib/pleroma/web/twitter_api/views/activity_view.ex index 433322eb8..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,33 +286,33 @@ 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, "is_local" => activity.local, "is_post_verb" => true, "created_at" => created_at, - "in_reply_to_status_id" => object["inReplyToStatusId"], + "in_reply_to_status_id" => reply_parent && reply_parent.id, "in_reply_to_screen_name" => reply_user && reply_user.nickname, "in_reply_to_profileurl" => User.profile_url(reply_user), "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) } |