aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorAlex S <alex.strizhakov@gmail.com>2019-05-01 12:29:33 +0700
committerAlex S <alex.strizhakov@gmail.com>2019-05-01 12:29:33 +0700
commitbdfa3a6fa87f95e93aa853ec4961645215f31221 (patch)
tree467b9dd762c9878541e34f2b38a1c58e387c8f2c /lib
parent97b35e00b049c8f908484163b5ffdbcb55db7867 (diff)
parent77690b9d03facf74483e3379f72b5b51c9f1bd4e (diff)
downloadpleroma-bdfa3a6fa87f95e93aa853ec4961645215f31221.tar.gz
Merging with develop
Conflicts: lib/pleroma/web/activity_pub/transmogrifier.ex lib/pleroma/user.ex
Diffstat (limited to 'lib')
-rw-r--r--lib/healthcheck.ex60
-rw-r--r--lib/mix/tasks/pleroma/database.ex51
-rw-r--r--lib/mix/tasks/pleroma/emoji.ex293
-rw-r--r--lib/mix/tasks/pleroma/instance.ex51
-rw-r--r--lib/mix/tasks/pleroma/robots_txt.eex2
-rw-r--r--lib/mix/tasks/pleroma/robotstxt.ex32
-rw-r--r--lib/mix/tasks/pleroma/sample_config.eex2
-rw-r--r--lib/mix/tasks/pleroma/user.ex118
-rw-r--r--lib/pleroma/PasswordResetToken.ex2
-rw-r--r--lib/pleroma/activity.ex143
-rw-r--r--lib/pleroma/application.ex27
-rw-r--r--lib/pleroma/bookmark.ex60
-rw-r--r--lib/pleroma/config.ex4
-rw-r--r--lib/pleroma/emails/admin_email.ex19
-rw-r--r--lib/pleroma/emails/mailer.ex4
-rw-r--r--lib/pleroma/emails/user_email.ex5
-rw-r--r--lib/pleroma/emoji.ex208
-rw-r--r--lib/pleroma/flake_id.ex2
-rw-r--r--lib/pleroma/formatter.ex70
-rw-r--r--lib/pleroma/gopher/server.ex11
-rw-r--r--lib/pleroma/html.ex72
-rw-r--r--lib/pleroma/instances/instance.ex2
-rw-r--r--lib/pleroma/jobs.ex152
-rw-r--r--lib/pleroma/list.ex2
-rw-r--r--lib/pleroma/notification.ex126
-rw-r--r--lib/pleroma/object.ex90
-rw-r--r--lib/pleroma/object/containment.ex61
-rw-r--r--lib/pleroma/object/fetcher.ex75
-rw-r--r--lib/pleroma/pagination.ex84
-rw-r--r--lib/pleroma/plugs/instance_static.ex3
-rw-r--r--lib/pleroma/plugs/uploaded_media.ex12
-rw-r--r--lib/pleroma/plugs/user_fetcher_plug.ex22
-rw-r--r--lib/pleroma/registration.ex57
-rw-r--r--lib/pleroma/repo.ex9
-rw-r--r--lib/pleroma/reverse_proxy.ex20
-rw-r--r--lib/pleroma/scheduled_activity.ex161
-rw-r--r--lib/pleroma/scheduled_activity_worker.ex58
-rw-r--r--lib/pleroma/upload.ex14
-rw-r--r--lib/pleroma/uploaders/s3.ex13
-rw-r--r--lib/pleroma/user.ex404
-rw-r--r--lib/pleroma/user/info.ex57
-rw-r--r--lib/pleroma/user_invite_token.ex113
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex266
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub_controller.ex10
-rw-r--r--lib/pleroma/web/activity_pub/mrf/keyword_policy.ex23
-rw-r--r--lib/pleroma/web/activity_pub/relay.ex2
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex256
-rw-r--r--lib/pleroma/web/activity_pub/utils.ex89
-rw-r--r--lib/pleroma/web/activity_pub/views/object_view.ex4
-rw-r--r--lib/pleroma/web/activity_pub/views/user_view.ex23
-rw-r--r--lib/pleroma/web/activity_pub/visibility.ex23
-rw-r--r--lib/pleroma/web/admin_api/admin_api_controller.ex121
-rw-r--r--lib/pleroma/web/admin_api/search.ex54
-rw-r--r--lib/pleroma/web/admin_api/views/account_view.ex18
-rw-r--r--lib/pleroma/web/auth/authenticator.ex24
-rw-r--r--lib/pleroma/web/auth/ldap_authenticator.ex144
-rw-r--r--lib/pleroma/web/auth/pleroma_authenticator.ex74
-rw-r--r--lib/pleroma/web/channels/user_socket.ex2
-rw-r--r--lib/pleroma/web/common_api/common_api.ex88
-rw-r--r--lib/pleroma/web/common_api/utils.ex113
-rw-r--r--lib/pleroma/web/controller_helper.ex5
-rw-r--r--lib/pleroma/web/endpoint.ex35
-rw-r--r--lib/pleroma/web/federator/federator.ex24
-rw-r--r--lib/pleroma/web/mastodon_api/mastodon_api.ex79
-rw-r--r--lib/pleroma/web/mastodon_api/mastodon_api_controller.ex459
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex51
-rw-r--r--lib/pleroma/web/mastodon_api/views/app_view.ex41
-rw-r--r--lib/pleroma/web/mastodon_api/views/notification_view.ex64
-rw-r--r--lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex57
-rw-r--r--lib/pleroma/web/mastodon_api/views/status_view.ex154
-rw-r--r--lib/pleroma/web/mastodon_api/websocket_handler.ex2
-rw-r--r--lib/pleroma/web/media_proxy/media_proxy.ex50
-rw-r--r--lib/pleroma/web/metadata/rel_me.ex13
-rw-r--r--lib/pleroma/web/metadata/utils.ex2
-rw-r--r--lib/pleroma/web/nodeinfo/nodeinfo_controller.ex3
-rw-r--r--lib/pleroma/web/oauth/authorization.ex2
-rw-r--r--lib/pleroma/web/oauth/fallback_controller.ex19
-rw-r--r--lib/pleroma/web/oauth/oauth_controller.ex353
-rw-r--r--lib/pleroma/web/oauth/token.ex4
-rw-r--r--lib/pleroma/web/ostatus/activity_representer.ex42
-rw-r--r--lib/pleroma/web/ostatus/handlers/note_handler.ex7
-rw-r--r--lib/pleroma/web/ostatus/ostatus.ex12
-rw-r--r--lib/pleroma/web/ostatus/ostatus_controller.ex13
-rw-r--r--lib/pleroma/web/push/impl.ex31
-rw-r--r--lib/pleroma/web/push/push.ex48
-rw-r--r--lib/pleroma/web/rel_me.ex3
-rw-r--r--lib/pleroma/web/rich_media/helpers.ex6
-rw-r--r--lib/pleroma/web/rich_media/parser.ex3
-rw-r--r--lib/pleroma/web/router.ex62
-rw-r--r--lib/pleroma/web/streamer.ex20
-rw-r--r--lib/pleroma/web/templates/layout/app.html.eex11
-rw-r--r--lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex13
-rw-r--r--lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex13
-rw-r--r--lib/pleroma/web/templates/o_auth/o_auth/register.html.eex42
-rw-r--r--lib/pleroma/web/templates/o_auth/o_auth/show.html.eex22
-rw-r--r--lib/pleroma/web/templates/twitter_api/util/password_reset.html.eex19
-rw-r--r--lib/pleroma/web/twitter_api/controllers/util_controller.ex113
-rw-r--r--lib/pleroma/web/twitter_api/representers/activity_representer.ex15
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api.ex113
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api_controller.ex15
-rw-r--r--lib/pleroma/web/twitter_api/views/activity_view.ex43
-rw-r--r--lib/pleroma/web/twitter_api/views/user_view.ex117
-rw-r--r--lib/pleroma/web/web_finger/web_finger.ex2
-rw-r--r--lib/pleroma/web/websub/websub_client_subscription.ex2
104 files changed, 4754 insertions, 1555 deletions
diff --git a/lib/healthcheck.ex b/lib/healthcheck.ex
new file mode 100644
index 000000000..646fb3b9d
--- /dev/null
+++ b/lib/healthcheck.ex
@@ -0,0 +1,60 @@
+defmodule Pleroma.Healthcheck do
+ @moduledoc """
+ Module collects metrics about app and assign healthy status.
+ """
+ alias Pleroma.Healthcheck
+ alias Pleroma.Repo
+
+ defstruct pool_size: 0,
+ active: 0,
+ idle: 0,
+ memory_used: 0,
+ healthy: true
+
+ @type t :: %__MODULE__{
+ pool_size: non_neg_integer(),
+ active: non_neg_integer(),
+ idle: non_neg_integer(),
+ memory_used: number(),
+ healthy: boolean()
+ }
+
+ @spec system_info() :: t()
+ def system_info do
+ %Healthcheck{
+ memory_used: Float.round(:erlang.memory(:total) / 1024 / 1024, 2)
+ }
+ |> assign_db_info()
+ |> check_health()
+ end
+
+ defp assign_db_info(healthcheck) do
+ database = Application.get_env(:pleroma, Repo)[:database]
+
+ query =
+ "select state, count(pid) from pg_stat_activity where datname = '#{database}' group by state;"
+
+ result = Repo.query!(query)
+ pool_size = Application.get_env(:pleroma, Repo)[:pool_size]
+
+ db_info =
+ Enum.reduce(result.rows, %{active: 0, idle: 0}, fn [state, cnt], states ->
+ if state == "active" do
+ Map.put(states, :active, states.active + cnt)
+ else
+ Map.put(states, :idle, states.idle + cnt)
+ end
+ end)
+ |> Map.put(:pool_size, pool_size)
+
+ Map.merge(healthcheck, db_info)
+ end
+
+ @spec check_health(Healthcheck.t()) :: Healthcheck.t()
+ def check_health(%{pool_size: pool_size, active: active} = check)
+ when active >= pool_size do
+ %{check | healthy: false}
+ end
+
+ def check_health(check), do: check
+end
diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex
new file mode 100644
index 000000000..ab9a3a7ff
--- /dev/null
+++ b/lib/mix/tasks/pleroma/database.ex
@@ -0,0 +1,51 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Mix.Tasks.Pleroma.Database do
+ alias Mix.Tasks.Pleroma.Common
+ require Logger
+ use Mix.Task
+
+ @shortdoc "A collection of database related tasks"
+ @moduledoc """
+ A collection of database related tasks
+
+ ## Replace embedded objects with their references
+
+ Replaces embedded objects with references to them in the `objects` table. Only needs to be ran once. The reason why this is not a migration is because it could significantly increase the database size after being ran, however after this `VACUUM FULL` will be able to reclaim about 20% (really depends on what is in the database, your mileage may vary) of the db size before the migration.
+
+ mix pleroma.database remove_embedded_objects
+
+ Options:
+ - `--vacuum` - run `VACUUM FULL` after the embedded objects are replaced with their references
+ """
+ def run(["remove_embedded_objects" | args]) do
+ {options, [], []} =
+ OptionParser.parse(
+ args,
+ strict: [
+ vacuum: :boolean
+ ]
+ )
+
+ Common.start_pleroma()
+ Logger.info("Removing embedded objects")
+
+ Pleroma.Repo.query!(
+ "update activities set data = jsonb_set(data, '{object}'::text[], data->'object'->'id') where data->'object'->>'id' is not null;",
+ [],
+ timeout: :infinity
+ )
+
+ if Keyword.get(options, :vacuum) do
+ Logger.info("Runnning VACUUM FULL")
+
+ Pleroma.Repo.query!(
+ "vacuum full;",
+ [],
+ timeout: :infinity
+ )
+ end
+ end
+end
diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex
new file mode 100644
index 000000000..cced73226
--- /dev/null
+++ b/lib/mix/tasks/pleroma/emoji.ex
@@ -0,0 +1,293 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Mix.Tasks.Pleroma.Emoji do
+ use Mix.Task
+
+ @shortdoc "Manages emoji packs"
+ @moduledoc """
+ Manages emoji packs
+
+ ## ls-packs
+
+ mix pleroma.emoji ls-packs [OPTION...]
+
+ Lists the emoji packs and metadata specified in the manifest.
+
+ ### Options
+
+ - `-m, --manifest PATH/URL` - path to a custom manifest, it can
+ either be an URL starting with `http`, in that case the
+ manifest will be fetched from that address, or a local path
+
+ ## get-packs
+
+ mix pleroma.emoji get-packs [OPTION...] PACKS
+
+ Fetches, verifies and installs the specified PACKS from the
+ manifest into the `STATIC-DIR/emoji/PACK-NAME`
+
+ ### Options
+
+ - `-m, --manifest PATH/URL` - same as ls-packs
+
+ ## gen-pack
+
+ mix pleroma.emoji gen-pack PACK-URL
+
+ Creates a new manifest entry and a file list from the specified
+ remote pack file. Currently, only .zip archives are recognized
+ as remote pack files and packs are therefore assumed to be zip
+ archives. This command is intended to run interactively and will
+ first ask you some basic questions about the pack, then download
+ the remote file and generate an SHA256 checksum for it, then
+ generate an emoji file list for you.
+
+ The manifest entry will either be written to a newly created
+ `index.json` file or appended to the existing one, *replacing*
+ the old pack with the same name if it was in the file previously.
+
+ The file list will be written to the file specified previously,
+ *replacing* that file. You _should_ check that the file list doesn't
+ contain anything you don't need in the pack, that is, anything that is
+ not an emoji (the whole pack is downloaded, but only emoji files
+ are extracted).
+ """
+
+ @default_manifest Pleroma.Config.get!([:emoji, :default_manifest])
+
+ def run(["ls-packs" | args]) do
+ Application.ensure_all_started(:hackney)
+
+ {options, [], []} = parse_global_opts(args)
+
+ manifest =
+ fetch_manifest(if options[:manifest], do: options[:manifest], else: @default_manifest)
+
+ Enum.each(manifest, fn {name, info} ->
+ to_print = [
+ {"Name", name},
+ {"Homepage", info["homepage"]},
+ {"Description", info["description"]},
+ {"License", info["license"]},
+ {"Source", info["src"]}
+ ]
+
+ for {param, value} <- to_print do
+ IO.puts(IO.ANSI.format([:bright, param, :normal, ": ", value]))
+ end
+
+ # A newline
+ IO.puts("")
+ end)
+ end
+
+ def run(["get-packs" | args]) do
+ Application.ensure_all_started(:hackney)
+
+ {options, pack_names, []} = parse_global_opts(args)
+
+ manifest_url = if options[:manifest], do: options[:manifest], else: @default_manifest
+
+ manifest = fetch_manifest(manifest_url)
+
+ for pack_name <- pack_names do
+ if Map.has_key?(manifest, pack_name) do
+ pack = manifest[pack_name]
+ src_url = pack["src"]
+
+ IO.puts(
+ IO.ANSI.format([
+ "Downloading ",
+ :bright,
+ pack_name,
+ :normal,
+ " from ",
+ :underline,
+ src_url
+ ])
+ )
+
+ binary_archive = Tesla.get!(src_url).body
+ archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
+
+ sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright]
+
+ if archive_sha == String.upcase(pack["src_sha256"]) do
+ IO.puts(IO.ANSI.format(sha_status_text ++ [:green, "OK"]))
+ else
+ IO.puts(IO.ANSI.format(sha_status_text ++ [:red, "BAD"]))
+
+ raise "Bad SHA256 for #{pack_name}"
+ end
+
+ # The url specified in files should be in the same directory
+ files_url = Path.join(Path.dirname(manifest_url), pack["files"])
+
+ IO.puts(
+ IO.ANSI.format([
+ "Fetching the file list for ",
+ :bright,
+ pack_name,
+ :normal,
+ " from ",
+ :underline,
+ files_url
+ ])
+ )
+
+ files = Tesla.get!(files_url).body |> Poison.decode!()
+
+ IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name]))
+
+ pack_path =
+ Path.join([
+ Pleroma.Config.get!([:instance, :static_dir]),
+ "emoji",
+ pack_name
+ ])
+
+ files_to_unzip =
+ Enum.map(
+ files,
+ fn {_, f} -> to_charlist(f) end
+ )
+
+ {:ok, _} =
+ :zip.unzip(binary_archive,
+ cwd: pack_path,
+ file_list: files_to_unzip
+ )
+
+ IO.puts(IO.ANSI.format(["Writing emoji.txt for ", :bright, pack_name]))
+
+ emoji_txt_str =
+ Enum.map(
+ files,
+ fn {shortcode, path} ->
+ emojo_path = Path.join("/emoji/#{pack_name}", path)
+ "#{shortcode}, #{emojo_path}"
+ end
+ )
+ |> Enum.join("\n")
+
+ File.write!(Path.join(pack_path, "emoji.txt"), emoji_txt_str)
+ else
+ IO.puts(IO.ANSI.format([:bright, :red, "No pack named \"#{pack_name}\" found"]))
+ end
+ end
+ end
+
+ def run(["gen-pack", src]) do
+ Application.ensure_all_started(:hackney)
+
+ proposed_name = Path.basename(src) |> Path.rootname()
+ name = String.trim(IO.gets("Pack name [#{proposed_name}]: "))
+ # If there's no name, use the default one
+ name = if String.length(name) > 0, do: name, else: proposed_name
+
+ license = String.trim(IO.gets("License: "))
+ homepage = String.trim(IO.gets("Homepage: "))
+ description = String.trim(IO.gets("Description: "))
+
+ proposed_files_name = "#{name}.json"
+ files_name = String.trim(IO.gets("Save file list to [#{proposed_files_name}]: "))
+ files_name = if String.length(files_name) > 0, do: files_name, else: proposed_files_name
+
+ default_exts = [".png", ".gif"]
+ default_exts_str = Enum.join(default_exts, " ")
+
+ exts =
+ String.trim(
+ IO.gets("Emoji file extensions (separated with spaces) [#{default_exts_str}]: ")
+ )
+
+ exts =
+ if String.length(exts) > 0 do
+ String.split(exts, " ")
+ |> Enum.filter(fn e -> e |> String.trim() |> String.length() > 0 end)
+ else
+ default_exts
+ end
+
+ IO.puts("Downloading the pack and generating SHA256")
+
+ binary_archive = Tesla.get!(src).body
+ archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
+
+ IO.puts("SHA256 is #{archive_sha}")
+
+ pack_json = %{
+ name => %{
+ license: license,
+ homepage: homepage,
+ description: description,
+ src: src,
+ src_sha256: archive_sha,
+ files: files_name
+ }
+ }
+
+ tmp_pack_dir = Path.join(System.tmp_dir!(), "emoji-pack-#{name}")
+
+ {:ok, _} =
+ :zip.unzip(
+ binary_archive,
+ cwd: tmp_pack_dir
+ )
+
+ emoji_map = Pleroma.Emoji.make_shortcode_to_file_map(tmp_pack_dir, exts)
+
+ File.write!(files_name, Poison.encode!(emoji_map, pretty: true))
+
+ IO.puts("""
+
+ #{files_name} has been created and contains the list of all found emojis in the pack.
+ Please review the files in the remove those not needed.
+ """)
+
+ if File.exists?("index.json") do
+ existing_data = File.read!("index.json") |> Poison.decode!()
+
+ File.write!(
+ "index.json",
+ Poison.encode!(
+ Map.merge(
+ existing_data,
+ pack_json
+ ),
+ pretty: true
+ )
+ )
+
+ IO.puts("index.json file has been update with the #{name} pack")
+ else
+ File.write!("index.json", Poison.encode!(pack_json, pretty: true))
+
+ IO.puts("index.json has been created with the #{name} pack")
+ end
+ end
+
+ defp fetch_manifest(from) do
+ Poison.decode!(
+ if String.starts_with?(from, "http") do
+ Tesla.get!(from).body
+ else
+ File.read!(from)
+ end
+ )
+ end
+
+ defp parse_global_opts(args) do
+ OptionParser.parse(
+ args,
+ strict: [
+ manifest: :string
+ ],
+ aliases: [
+ m: :manifest
+ ]
+ )
+ end
+end
diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex
index 1ba452275..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,22 @@ 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,
+ :indexable,
+ "Do you want search engines to index your site? (y/n)",
+ "y"
+ ) === "y"
+
dbhost =
Common.get_option(options, :dbhost, "What is the hostname of your database?", "localhost")
@@ -114,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,
@@ -142,6 +163,8 @@ defmodule Mix.Tasks.Pleroma.Instance do
Mix.shell().info("Writing #{psql_path}.")
File.write(psql_path, result_psql)
+ write_robots_txt(indexable)
+
Mix.shell().info(
"\n" <>
"""
@@ -163,4 +186,28 @@ defmodule Mix.Tasks.Pleroma.Instance do
)
end
end
+
+ defp write_robots_txt(indexable) do
+ robots_txt =
+ EEx.eval_file(
+ Path.expand("robots_txt.eex", __DIR__),
+ indexable: indexable
+ )
+
+ static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/")
+
+ unless File.exists?(static_dir) do
+ File.mkdir_p!(static_dir)
+ end
+
+ robots_txt_path = Path.join(static_dir, "robots.txt")
+
+ if File.exists?(robots_txt_path) do
+ File.cp!(robots_txt_path, "#{robots_txt_path}.bak")
+ Mix.shell().info("Backing up existing robots.txt to #{robots_txt_path}.bak")
+ end
+
+ File.write(robots_txt_path, robots_txt)
+ Mix.shell().info("Writing #{robots_txt_path}.")
+ end
end
diff --git a/lib/mix/tasks/pleroma/robots_txt.eex b/lib/mix/tasks/pleroma/robots_txt.eex
new file mode 100644
index 000000000..1af3c47ee
--- /dev/null
+++ b/lib/mix/tasks/pleroma/robots_txt.eex
@@ -0,0 +1,2 @@
+User-Agent: *
+Disallow: <%= if indexable, do: "", else: "/" %>
diff --git a/lib/mix/tasks/pleroma/robotstxt.ex b/lib/mix/tasks/pleroma/robotstxt.ex
new file mode 100644
index 000000000..2128e1cd6
--- /dev/null
+++ b/lib/mix/tasks/pleroma/robotstxt.ex
@@ -0,0 +1,32 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Mix.Tasks.Pleroma.RobotsTxt do
+ use Mix.Task
+
+ @shortdoc "Generate robots.txt"
+ @moduledoc """
+ Generates robots.txt
+
+ ## Overwrite robots.txt to disallow all
+
+ mix pleroma.robots_txt disallow_all
+
+ This will write a robots.txt that will hide all paths on your instance
+ from search engines and other robots that obey robots.txt
+
+ """
+ def run(["disallow_all"]) do
+ static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/")
+
+ if !File.exists?(static_dir) do
+ File.mkdir_p!(static_dir)
+ end
+
+ robots_txt_path = Path.join(static_dir, "robots.txt")
+ robots_txt_content = "User-Agent: *\nDisallow: /\n"
+
+ File.write!(robots_txt_path, robots_txt_content, [:write])
+ end
+end
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/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex
index f6cca0d06..b396ff0de 100644
--- a/lib/mix/tasks/pleroma/user.ex
+++ b/lib/mix/tasks/pleroma/user.ex
@@ -6,8 +6,8 @@ defmodule Mix.Tasks.Pleroma.User do
use Mix.Task
import Ecto.Changeset
alias Mix.Tasks.Pleroma.Common
- alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.UserInviteToken
@shortdoc "Manages Pleroma users"
@moduledoc """
@@ -23,16 +23,32 @@ defmodule Mix.Tasks.Pleroma.User do
- `--password PASSWORD` - the user's password
- `--moderator`/`--no-moderator` - whether the user is a moderator
- `--admin`/`--no-admin` - whether the user is an admin
- - `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions
+ - `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions
## Generate an invite link.
- mix pleroma.user invite
+ mix pleroma.user invite [OPTION...]
+
+ Options:
+ - `--expires_at DATE` - last day on which token is active (e.g. "2019-04-05")
+ - `--max_use NUMBER` - maximum numbers of token uses
+
+ ## List generated invites
+
+ mix pleroma.user invites
+
+ ## Revoke invite
+
+ mix pleroma.user revoke_invite TOKEN OR TOKEN_ID
## Delete the user's account.
mix pleroma.user rm NICKNAME
+ ## Delete the user's activities.
+
+ mix pleroma.user delete_activities NICKNAME
+
## Deactivate or activate the user's account.
mix pleroma.user toggle_activated NICKNAME
@@ -146,7 +162,7 @@ defmodule Mix.Tasks.Pleroma.User do
def run(["rm", nickname]) do
Common.start_pleroma()
- with %User{local: true} = user <- User.get_by_nickname(nickname) do
+ with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
User.delete(user)
Mix.shell().info("User #{nickname} deleted.")
else
@@ -158,7 +174,7 @@ defmodule Mix.Tasks.Pleroma.User do
def run(["toggle_activated", nickname]) do
Common.start_pleroma()
- with %User{} = user <- User.get_by_nickname(nickname) do
+ with %User{} = user <- User.get_cached_by_nickname(nickname) do
{:ok, user} = User.deactivate(user, !user.info.deactivated)
Mix.shell().info(
@@ -173,7 +189,7 @@ defmodule Mix.Tasks.Pleroma.User do
def run(["reset_password", nickname]) do
Common.start_pleroma()
- with %User{local: true} = user <- User.get_by_nickname(nickname),
+ with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
{:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do
Mix.shell().info("Generated password reset token for #{user.nickname}")
@@ -195,14 +211,14 @@ defmodule Mix.Tasks.Pleroma.User do
def run(["unsubscribe", nickname]) do
Common.start_pleroma()
- with %User{} = user <- User.get_by_nickname(nickname) do
+ with %User{} = user <- User.get_cached_by_nickname(nickname) do
Mix.shell().info("Deactivating #{user.nickname}")
User.deactivate(user)
{:ok, friends} = User.get_friends(user)
Enum.each(friends, fn friend ->
- user = Repo.get(User, user.id)
+ user = User.get_cached_by_id(user.id)
Mix.shell().info("Unsubscribing #{friend.nickname} from #{user.nickname}")
User.unfollow(user, friend)
@@ -210,7 +226,7 @@ defmodule Mix.Tasks.Pleroma.User do
:timer.sleep(500)
- user = Repo.get(User, user.id)
+ user = User.get_cached_by_id(user.id)
if Enum.empty?(user.following) do
Mix.shell().info("Successfully unsubscribed all followers from #{user.nickname}")
@@ -234,7 +250,7 @@ defmodule Mix.Tasks.Pleroma.User do
]
)
- with %User{local: true} = user <- User.get_by_nickname(nickname) do
+ with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
user =
case Keyword.get(options, :moderator) do
nil -> user
@@ -261,7 +277,7 @@ defmodule Mix.Tasks.Pleroma.User do
def run(["tag", nickname | tags]) do
Common.start_pleroma()
- with %User{} = user <- User.get_by_nickname(nickname) do
+ with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.tag(tags)
Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}")
@@ -274,7 +290,7 @@ defmodule Mix.Tasks.Pleroma.User do
def run(["untag", nickname | tags]) do
Common.start_pleroma()
- with %User{} = user <- User.get_by_nickname(nickname) do
+ with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.untag(tags)
Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}")
@@ -284,23 +300,91 @@ defmodule Mix.Tasks.Pleroma.User do
end
end
- def run(["invite"]) do
+ def run(["invite" | rest]) do
+ {options, [], []} =
+ OptionParser.parse(rest,
+ strict: [
+ expires_at: :string,
+ max_use: :integer
+ ]
+ )
+
+ options =
+ options
+ |> Keyword.update(:expires_at, {:ok, nil}, fn
+ nil -> {:ok, nil}
+ val -> Date.from_iso8601(val)
+ end)
+ |> Enum.into(%{})
+
Common.start_pleroma()
- with {:ok, token} <- Pleroma.UserInviteToken.create_token() do
- Mix.shell().info("Generated user invite token")
+ with {:ok, val} <- options[:expires_at],
+ options = Map.put(options, :expires_at, val),
+ {:ok, invite} <- UserInviteToken.create_invite(options) do
+ Mix.shell().info(
+ "Generated user invite token " <> String.replace(invite.invite_type, "_", " ")
+ )
url =
Pleroma.Web.Router.Helpers.redirect_url(
Pleroma.Web.Endpoint,
:registration_page,
- token.token
+ invite.token
)
IO.puts(url)
else
+ error ->
+ Mix.shell().error("Could not create invite token: #{inspect(error)}")
+ end
+ end
+
+ def run(["invites"]) do
+ Common.start_pleroma()
+
+ Mix.shell().info("Invites list:")
+
+ UserInviteToken.list_invites()
+ |> Enum.each(fn invite ->
+ expire_info =
+ with expires_at when not is_nil(expires_at) <- invite.expires_at do
+ " | Expires at: #{Date.to_string(expires_at)}"
+ end
+
+ using_info =
+ with max_use when not is_nil(max_use) <- invite.max_use do
+ " | Max use: #{max_use} Left use: #{max_use - invite.uses}"
+ end
+
+ Mix.shell().info(
+ "ID: #{invite.id} | Token: #{invite.token} | Token type: #{invite.invite_type} | Used: #{
+ invite.used
+ }#{expire_info}#{using_info}"
+ )
+ end)
+ end
+
+ def run(["revoke_invite", token]) do
+ Common.start_pleroma()
+
+ with {:ok, invite} <- UserInviteToken.find_by_token(token),
+ {:ok, _} <- UserInviteToken.update_invite(invite, %{used: true}) do
+ Mix.shell().info("Invite for token #{token} was revoked.")
+ else
+ _ -> Mix.shell().error("No invite found with token #{token}")
+ end
+ end
+
+ def run(["delete_activities", nickname]) do
+ Common.start_pleroma()
+
+ with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
+ User.delete_user_activities(user)
+ Mix.shell().info("User #{nickname} statuses deleted.")
+ else
_ ->
- Mix.shell().error("Could not create invite token.")
+ Mix.shell().error("No local user #{nickname}")
end
end
diff --git a/lib/pleroma/PasswordResetToken.ex b/lib/pleroma/PasswordResetToken.ex
index 772c239a1..f31ea5bc5 100644
--- a/lib/pleroma/PasswordResetToken.ex
+++ b/lib/pleroma/PasswordResetToken.ex
@@ -39,7 +39,7 @@ defmodule Pleroma.PasswordResetToken do
def reset_password(token, data) do
with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
- %User{} = user <- Repo.get(User, token.user_id),
+ %User{} = user <- User.get_cached_by_id(token.user_id),
{:ok, _user} <- User.reset_password(user, data),
{:ok, token} <- Repo.update(used_changeset(token)) do
{:ok, token}
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index 79dc26b01..4a2ded518 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -7,8 +7,10 @@ defmodule Pleroma.Activity do
alias Pleroma.Activity
alias Pleroma.Notification
+ alias Pleroma.Object
alias Pleroma.Repo
+ import Ecto.Changeset
import Ecto.Query
@type t :: %__MODULE__{}
@@ -22,16 +24,53 @@ defmodule Pleroma.Activity do
"Like" => "favourite"
}
+ @mastodon_to_ap_notification_types for {k, v} <- @mastodon_notification_types,
+ into: %{},
+ do: {v, k}
+
schema "activities" do
field(:data, :map)
field(:local, :boolean, default: true)
field(:actor, :string)
- field(:recipients, {:array, :string})
+ field(:recipients, {:array, :string}, default: [])
has_many(:notifications, Notification, on_delete: :delete_all)
+ # Attention: this is a fake relation, don't try to preload it blindly and expect it to work!
+ # The foreign key is embedded in a jsonb field.
+ #
+ # To use it, you probably want to do an inner join and a preload:
+ #
+ # ```
+ # |> join(:inner, [activity], o in Object,
+ # on: fragment("(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')",
+ # o.data, activity.data, activity.data))
+ # |> preload([activity, object], [object: object])
+ # ```
+ #
+ # As a convenience, Activity.with_preloaded_object() sets up an inner join and preload for the
+ # typical case.
+ has_one(:object, Object, on_delete: :nothing, foreign_key: :id)
+
timestamps()
end
+ def with_preloaded_object(query) do
+ query
+ |> join(
+ :inner,
+ [activity],
+ o in Object,
+ on:
+ fragment(
+ "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
+ o.data,
+ activity.data,
+ activity.data
+ )
+ )
+ |> preload([activity, object], object: object)
+ end
+
def get_by_ap_id(ap_id) do
Repo.one(
from(
@@ -41,10 +80,51 @@ 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(
+ activity in Activity,
+ where: fragment("(?)->>'id' = ?", activity.data, ^to_string(ap_id)),
+ left_join: o in Object,
+ on:
+ fragment(
+ "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
+ o.data,
+ activity.data,
+ activity.data
+ ),
+ preload: [object: o]
+ )
+ )
+ end
+
def get_by_id(id) do
Repo.get(Activity, id)
end
+ def get_by_id_with_object(id) do
+ from(activity in Activity,
+ where: activity.id == ^id,
+ inner_join: o in Object,
+ on:
+ fragment(
+ "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
+ o.data,
+ activity.data,
+ activity.data
+ ),
+ preload: [object: o]
+ )
+ |> Repo.one()
+ end
+
def by_object_ap_id(ap_id) do
from(
activity in Activity,
@@ -72,7 +152,7 @@ defmodule Pleroma.Activity do
)
end
- def create_by_object_ap_id(ap_id) do
+ def create_by_object_ap_id(ap_id) when is_binary(ap_id) do
from(
activity in Activity,
where:
@@ -86,6 +166,8 @@ defmodule Pleroma.Activity do
)
end
+ def create_by_object_ap_id(_), do: nil
+
def get_all_create_by_object_ap_id(ap_id) do
Repo.all(create_by_object_ap_id(ap_id))
end
@@ -97,21 +179,60 @@ defmodule Pleroma.Activity do
def get_create_by_object_ap_id(_), do: nil
- def normalize(obj) when is_map(obj), do: Activity.get_by_ap_id(obj["id"])
- def normalize(ap_id) when is_binary(ap_id), do: Activity.get_by_ap_id(ap_id)
- def normalize(_), do: nil
+ def create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
+ from(
+ activity in Activity,
+ where:
+ fragment(
+ "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
+ activity.data,
+ activity.data,
+ ^to_string(ap_id)
+ ),
+ where: fragment("(?)->>'type' = 'Create'", activity.data),
+ inner_join: o in Object,
+ on:
+ fragment(
+ "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
+ o.data,
+ activity.data,
+ activity.data
+ ),
+ preload: [object: o]
+ )
+ end
+
+ def create_by_object_ap_id_with_object(_), do: nil
+
+ 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 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)
- |> Repo.delete_all(returning: true)
+ |> select([u], u)
+ |> 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)
@@ -126,6 +247,10 @@ defmodule Pleroma.Activity do
def mastodon_notification_type(%Activity{}), do: nil
+ def from_mastodon_notification_type(type) do
+ Map.get(@mastodon_to_ap_notification_types, type)
+ end
+
def all_by_actor_and_id(actor, status_ids \\ [])
def all_by_actor_and_id(_actor, []), do: []
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index cc81e1805..eeb415084 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -25,6 +25,7 @@ defmodule Pleroma.Application do
import Cachex.Spec
Pleroma.Config.DeprecationWarnings.warn()
+ setup_instrumenters()
# Define workers and child supervisors to be supervised
children =
@@ -103,15 +104,15 @@ defmodule Pleroma.Application do
],
id: :cachex_idem
),
- worker(Pleroma.FlakeId, [])
+ worker(Pleroma.FlakeId, []),
+ worker(Pleroma.ScheduledActivityWorker, [])
] ++
hackney_pool_children() ++
[
worker(Pleroma.Web.Federator.RetryQueue, []),
worker(Pleroma.Stats, []),
- worker(Pleroma.Web.Push, []),
- worker(Pleroma.Jobs, []),
- worker(Task, [&Pleroma.Web.Federator.init/0], restart: :temporary)
+ worker(Task, [&Pleroma.Web.Push.init/0], restart: :temporary, id: :web_push_init),
+ worker(Task, [&Pleroma.Web.Federator.init/0], restart: :temporary, id: :federator_init)
] ++
streamer_child() ++
chat_child() ++
@@ -127,6 +128,24 @@ defmodule Pleroma.Application do
Supervisor.start_link(children, opts)
end
+ defp setup_instrumenters do
+ require Prometheus.Registry
+
+ :ok =
+ :telemetry.attach(
+ "prometheus-ecto",
+ [:pleroma, :repo, :query],
+ &Pleroma.Repo.Instrumenter.handle_event/4,
+ %{}
+ )
+
+ Prometheus.Registry.register_collector(:prometheus_process_collector)
+ Pleroma.Web.Endpoint.MetricsExporter.setup()
+ Pleroma.Web.Endpoint.PipelineInstrumenter.setup()
+ Pleroma.Web.Endpoint.Instrumenter.setup()
+ Pleroma.Repo.Instrumenter.setup()
+ end
+
def enabled_hackney_pools do
[:media] ++
if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do
diff --git a/lib/pleroma/bookmark.ex b/lib/pleroma/bookmark.ex
new file mode 100644
index 000000000..7f8fd43b6
--- /dev/null
+++ b/lib/pleroma/bookmark.ex
@@ -0,0 +1,60 @@
+defmodule Pleroma.Bookmark do
+ use Ecto.Schema
+
+ import Ecto.Changeset
+ import Ecto.Query
+
+ alias Pleroma.Activity
+ alias Pleroma.Bookmark
+ alias Pleroma.FlakeId
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ @type t :: %__MODULE__{}
+
+ schema "bookmarks" do
+ belongs_to(:user, User, type: FlakeId)
+ belongs_to(:activity, Activity, type: FlakeId)
+
+ timestamps()
+ end
+
+ @spec create(FlakeId.t(), FlakeId.t()) :: {:ok, Bookmark.t()} | {:error, Changeset.t()}
+ def create(user_id, activity_id) do
+ attrs = %{
+ user_id: user_id,
+ activity_id: activity_id
+ }
+
+ %Bookmark{}
+ |> cast(attrs, [:user_id, :activity_id])
+ |> validate_required([:user_id, :activity_id])
+ |> unique_constraint(:activity_id, name: :bookmarks_user_id_activity_id_index)
+ |> Repo.insert()
+ end
+
+ @spec for_user_query(FlakeId.t()) :: Ecto.Query.t()
+ def for_user_query(user_id) do
+ Bookmark
+ |> where(user_id: ^user_id)
+ |> join(:inner, [b], activity in assoc(b, :activity))
+ |> preload([b, a], activity: a)
+ end
+
+ def get(user_id, activity_id) do
+ Bookmark
+ |> where(user_id: ^user_id)
+ |> where(activity_id: ^activity_id)
+ |> Repo.one()
+ end
+
+ @spec destroy(FlakeId.t(), FlakeId.t()) :: {:ok, Bookmark.t()} | {:error, Changeset.t()}
+ def destroy(user_id, activity_id) do
+ from(b in Bookmark,
+ where: b.user_id == ^user_id,
+ where: b.activity_id == ^activity_id
+ )
+ |> Repo.one()
+ |> Repo.delete()
+ end
+end
diff --git a/lib/pleroma/config.ex b/lib/pleroma/config.ex
index 21507cd38..189faa15f 100644
--- a/lib/pleroma/config.ex
+++ b/lib/pleroma/config.ex
@@ -57,4 +57,8 @@ defmodule Pleroma.Config do
def delete(key) do
Application.delete_env(:pleroma, key)
end
+
+ def oauth_consumer_strategies, do: get([:auth, :oauth_consumer_strategies], [])
+
+ def oauth_consumer_enabled?, do: oauth_consumer_strategies() != []
end
diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex
index 9b20c7e08..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)
@@ -29,9 +32,13 @@ defmodule Pleroma.AdminEmail do
if length(statuses) > 0 do
statuses_list_html =
statuses
- |> Enum.map(fn %{id: id} ->
- status_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, id)
- "<li><a href=\"#{status_url}\">#{status_url}</li>"
+ |> Enum.map(fn
+ %{id: id} ->
+ status_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, id)
+ "<li><a href=\"#{status_url}\">#{status_url}</li>"
+
+ id when is_binary(id) ->
+ "<li><a href=\"#{id}\">#{id}</li>"
end)
|> Enum.join("\n")
@@ -55,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 f7e3aa78b..53f5a661c 100644
--- a/lib/pleroma/emails/mailer.ex
+++ b/lib/pleroma/emails/mailer.ex
@@ -2,11 +2,11 @@
# 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
- Pleroma.Jobs.enqueue(:mailer, __MODULE__, [:deliver_async, email, config])
+ PleromaJobQueue.enqueue(:mailer, __MODULE__, [:deliver_async, email, config])
end
def perform(:deliver_async, email, config), do: deliver(email, config)
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 f3f08cd9d..6390cce4c 100644
--- a/lib/pleroma/emoji.ex
+++ b/lib/pleroma/emoji.ex
@@ -6,15 +6,23 @@ 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
+ * glob paths, nested folder is used as tag name for grouping e.g. priv/static/emoji/custom/nested_folder
This GenServer stores in an ETS table the list of the loaded emojis, and also allows to reload the list at runtime.
"""
use GenServer
+
+ require Logger
+
+ @type pattern :: Regex.t() | module() | String.t()
+ @type patterns :: pattern() | [pattern()]
+ @type group_patterns :: keyword(patterns())
+
@ets __MODULE__.Ets
@ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}]
+ @groups Application.get_env(:pleroma, :emoji)[:groups]
@doc false
def start_link do
@@ -73,91 +81,94 @@ defmodule Pleroma.Emoji do
end
defp load do
+ 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(Keyword.get(Application.get_env(:pleroma, :instance), :finmoji_enabled)) ++
- load_from_file("config/emoji.txt") ++
+ (load_from_file("config/emoji.txt") ++
load_from_file("config/custom_emoji.txt") ++
- load_from_globs(
- Keyword.get(Application.get_env(:pleroma, :emoji, []), :shortcode_globs, [])
- ))
+ 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 ->
- {finmoji, "/finmoji/128px/#{finmoji}-128.png"}
- 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
- defp load_finmoji(_), do: []
+ 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
+
+ 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
@@ -172,8 +183,14 @@ defmodule Pleroma.Emoji do
|> Stream.map(&String.trim/1)
|> Stream.map(fn line ->
case String.split(line, ~r/,\s*/) do
- [name, file] -> {name, file}
- _ -> nil
+ [name, file] ->
+ {name, file, [to_string(match_extra(@groups, file))]}
+
+ [name, file | tags] ->
+ {name, file, tags}
+
+ _ ->
+ nil
end
end)
|> Enum.to_list()
@@ -190,9 +207,40 @@ defmodule Pleroma.Emoji do
|> Enum.concat()
Enum.map(paths, fn path ->
+ 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}
+ {shortcode, external_path, [to_string(tag)]}
+ end)
+ end
+
+ @doc """
+ Finds a matching group for the given emoji filename
+ """
+ @spec match_extra(group_patterns(), String.t()) :: atom() | nil
+ def match_extra(group_patterns, filename) do
+ match_group_patterns(group_patterns, fn pattern ->
+ case pattern do
+ %Regex{} = regex -> Regex.match?(regex, filename)
+ string when is_binary(string) -> filename == string
+ end
+ end)
+ end
+
+ defp match_group_patterns(group_patterns, matcher) do
+ Enum.find_value(group_patterns, fn {group, patterns} ->
+ patterns =
+ patterns
+ |> List.wrap()
+ |> Enum.map(fn pattern ->
+ if String.contains?(pattern, "*") do
+ ~r(#{String.replace(pattern, "*", ".*")})
+ else
+ pattern
+ end
+ end)
+
+ Enum.any?(patterns, matcher) && group
end)
end
end
diff --git a/lib/pleroma/flake_id.ex b/lib/pleroma/flake_id.ex
index 4259d5718..58ab3650d 100644
--- a/lib/pleroma/flake_id.ex
+++ b/lib/pleroma/flake_id.ex
@@ -46,7 +46,7 @@ defmodule Pleroma.FlakeId do
def from_string(string) when is_binary(string) and byte_size(string) < 18 do
case Integer.parse(string) do
- {id, _} -> <<0::integer-size(64), id::integer-size(64)>>
+ {id, ""} -> <<0::integer-size(64), id::integer-size(64)>>
_ -> nil
end
end
diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex
index 1e4ede3f2..dab8910c1 100644
--- a/lib/pleroma/formatter.ex
+++ b/lib/pleroma/formatter.ex
@@ -8,20 +8,32 @@ defmodule Pleroma.Formatter do
alias Pleroma.User
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>#{
@@ -45,15 +57,47 @@ defmodule Pleroma.Formatter do
@doc """
Parses a text and replace plain text links with HTML. Returns a tuple with a result text, mentions, and hashtags.
+
+ If the 'safe_mention' option is given, only consecutive mentions at the start the post are actually mentioned.
"""
@spec linkify(String.t(), keyword()) ::
{String.t(), [{String.t(), User.t()}], [{String.t(), String.t()}]}
def linkify(text, options \\ []) do
options = options ++ @auto_linker_config
- acc = %{mentions: MapSet.new(), tags: MapSet.new()}
- {text, %{mentions: mentions, tags: tags}} = AutoLinker.link_map(text, acc, options)
- {text, MapSet.to_list(mentions), MapSet.to_list(tags)}
+ if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
+ %{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
+ acc = %{mentions: MapSet.new(), tags: MapSet.new()}
+
+ {text_mentions, %{mentions: mentions}} = AutoLinker.link_map(mentions, acc, options)
+ {text_rest, %{tags: tags}} = AutoLinker.link_map(rest, acc, options)
+
+ {text_mentions <> text_rest, MapSet.to_list(mentions), MapSet.to_list(tags)}
+ else
+ acc = %{mentions: MapSet.new(), tags: MapSet.new()}
+ {text, %{mentions: mentions, tags: tags}} = AutoLinker.link_map(text, acc, options)
+
+ {text, MapSet.to_list(mentions), MapSet.to_list(tags)}
+ 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
@@ -63,9 +107,9 @@ defmodule Pleroma.Formatter do
def emojify(text, nil), do: text
def emojify(text, emoji, strip \\ false) do
- Enum.reduce(emoji, text, fn {emoji, file}, text ->
- emoji = HTML.strip_tags(emoji)
- file = HTML.strip_tags(file)
+ Enum.reduce(emoji, text, fn emoji_data, text ->
+ emoji = HTML.strip_tags(elem(emoji_data, 0))
+ file = HTML.strip_tags(elem(emoji_data, 1))
html =
if not strip do
@@ -87,7 +131,7 @@ defmodule Pleroma.Formatter do
def demojify(text, nil), do: text
def get_emoji(text) when is_binary(text) do
- Enum.filter(Emoji.get_all(), fn {emoji, _} -> String.contains?(text, ":#{emoji}:") end)
+ Enum.filter(Emoji.get_all(), fn {emoji, _, _} -> String.contains?(text, ":#{emoji}:") end)
end
def get_emoji(_), do: []
@@ -126,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 6baacc566..1d2e0785c 100644
--- a/lib/pleroma/gopher/server.ex
+++ b/lib/pleroma/gopher/server.ex
@@ -38,7 +38,7 @@ end
defmodule Pleroma.Gopher.Server.ProtocolHandler do
alias Pleroma.Activity
alias Pleroma.HTML
- alias Pleroma.Repo
+ alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
@@ -66,7 +66,8 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
def link(name, selector, type \\ 1) do
address = Pleroma.Web.Endpoint.host()
port = Pleroma.Config.get([:gopher, :port], 1234)
- "#{type}#{name}\t#{selector}\t#{address}\t#{port}\r\n"
+ dstport = Pleroma.Config.get([:gopher, :dstport], port)
+ "#{type}#{name}\t#{selector}\t#{address}\t#{dstport}\r\n"
end
def render_activities(activities) do
@@ -75,14 +76,14 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
|> Enum.map(fn activity ->
user = User.get_cached_by_ap_id(activity.data["actor"])
- object = activity.data["object"]
+ object = Object.normalize(activity)
like_count = object["like_count"] || 0
announcement_count = object["announcement_count"] || 0
link("Post ##{activity.id} by #{user.nickname}", "/notices/#{activity.id}") <>
info("#{like_count} likes, #{announcement_count} repeats") <>
"i\tfake\t(NULL)\t0\r\n" <>
- info(HTML.strip_tags(String.replace(activity.data["object"]["content"], "<br>", "\r")))
+ info(HTML.strip_tags(String.replace(object["content"], "<br>", "\r")))
end)
|> Enum.join("i\tfake\t(NULL)\t0\r\n")
end
@@ -110,7 +111,7 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
end
def response("/notices/" <> id) do
- with %Activity{} = activity <- Repo.get(Activity, id),
+ with %Activity{} = activity <- Activity.get_by_id(id),
true <- Visibility.is_public?(activity) do
activities =
ActivityPub.fetch_activities_for_context(activity.data["context"])
diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex
index 05253157e..cf6c0ee0a 100644
--- a/lib/pleroma/html.ex
+++ b/lib/pleroma/html.ex
@@ -28,27 +28,40 @@ defmodule Pleroma.HTML do
def filter_tags(html), do: filter_tags(html, nil)
def strip_tags(html), do: Scrubber.scrub(html, Scrubber.StripTags)
- def get_cached_scrubbed_html_for_object(content, scrubbers, object, module) do
- key = "#{module}#{generate_scrubber_signature(scrubbers)}|#{object.id}"
- Cachex.fetch!(:scrubber_cache, key, fn _key -> ensure_scrubbed_html(content, scrubbers) end)
+ def get_cached_scrubbed_html_for_activity(content, scrubbers, activity, key \\ "") do
+ key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}"
+
+ Cachex.fetch!(:scrubber_cache, key, fn _key ->
+ object = Pleroma.Object.normalize(activity)
+ ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false)
+ end)
end
- def get_cached_stripped_html_for_object(content, object, module) do
- get_cached_scrubbed_html_for_object(
+ def get_cached_stripped_html_for_activity(content, activity, key) do
+ get_cached_scrubbed_html_for_activity(
content,
HtmlSanitizeEx.Scrubber.StripTags,
- object,
- module
+ activity,
+ key
)
end
def ensure_scrubbed_html(
content,
- scrubbers
+ scrubbers,
+ false = _fake
) do
{:commit, filter_tags(content, scrubbers)}
end
+ def ensure_scrubbed_html(
+ content,
+ scrubbers,
+ true = _fake
+ ) do
+ {:ignore, filter_tags(content, scrubbers)}
+ end
+
defp generate_scrubber_signature(scrubber) when is_atom(scrubber) do
generate_scrubber_signature([scrubber])
end
@@ -93,14 +106,31 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
# links
Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
- Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"])
+
+ Meta.allow_tag_with_this_attribute_values("a", "class", [
+ "hashtag",
+ "u-url",
+ "mention",
+ "u-url mention",
+ "mention u-url"
+ ])
+
+ Meta.allow_tag_with_this_attribute_values("a", "rel", [
+ "tag",
+ "nofollow",
+ "noopener",
+ "noreferrer"
+ ])
+
+ Meta.allow_tag_with_these_attributes("a", ["name", "title"])
# paragraphs and linebreaks
Meta.allow_tag_with_these_attributes("br", [])
Meta.allow_tag_with_these_attributes("p", [])
# microformats
- Meta.allow_tag_with_these_attributes("span", ["class"])
+ Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card"])
+ Meta.allow_tag_with_these_attributes("span", [])
# allow inline images for custom emoji
@allow_inline_images Keyword.get(@markup, :allow_inline_images)
@@ -135,7 +165,23 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.strip_comments()
Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
- Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"])
+
+ Meta.allow_tag_with_this_attribute_values("a", "class", [
+ "hashtag",
+ "u-url",
+ "mention",
+ "u-url mention",
+ "mention u-url"
+ ])
+
+ Meta.allow_tag_with_this_attribute_values("a", "rel", [
+ "tag",
+ "nofollow",
+ "noopener",
+ "noreferrer"
+ ])
+
+ Meta.allow_tag_with_these_attributes("a", ["name", "title"])
Meta.allow_tag_with_these_attributes("abbr", ["title"])
@@ -150,11 +196,13 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes("ol", [])
Meta.allow_tag_with_these_attributes("p", [])
Meta.allow_tag_with_these_attributes("pre", [])
- Meta.allow_tag_with_these_attributes("span", ["class"])
Meta.allow_tag_with_these_attributes("strong", [])
Meta.allow_tag_with_these_attributes("u", [])
Meta.allow_tag_with_these_attributes("ul", [])
+ Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card"])
+ Meta.allow_tag_with_these_attributes("span", [])
+
@allow_inline_images Keyword.get(@markup, :allow_inline_images)
if @allow_inline_images do
diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex
index e92006151..420803a8f 100644
--- a/lib/pleroma/instances/instance.ex
+++ b/lib/pleroma/instances/instance.ex
@@ -12,7 +12,7 @@ defmodule Pleroma.Instances.Instance do
schema "instances" do
field(:host, :string)
- field(:unreachable_since, :naive_datetime)
+ field(:unreachable_since, :naive_datetime_usec)
timestamps()
end
diff --git a/lib/pleroma/jobs.ex b/lib/pleroma/jobs.ex
deleted file mode 100644
index 24b7e5e46..000000000
--- a/lib/pleroma/jobs.ex
+++ /dev/null
@@ -1,152 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Jobs do
- @moduledoc """
- A basic job queue
- """
- use GenServer
-
- require Logger
-
- def init(args) do
- {:ok, args}
- end
-
- def start_link do
- queues =
- Pleroma.Config.get(Pleroma.Jobs)
- |> Enum.map(fn {name, _} -> create_queue(name) end)
- |> Enum.into(%{})
-
- state = %{
- queues: queues,
- refs: %{}
- }
-
- GenServer.start_link(__MODULE__, state, name: __MODULE__)
- end
-
- def create_queue(name) do
- {name, {:sets.new(), []}}
- end
-
- @doc """
- Enqueues a job.
-
- Returns `:ok`.
-
- ## Arguments
-
- - `queue_name` - a queue name(must be specified in the config).
- - `mod` - a worker module (must have `perform` function).
- - `args` - a list of arguments for the `perform` function of the worker module.
- - `priority` - a job priority (`0` by default).
-
- ## Examples
-
- Enqueue `Module.perform/0` with `priority=1`:
-
- iex> Pleroma.Jobs.enqueue(:example_queue, Module, [])
- :ok
-
- Enqueue `Module.perform(:job_name)` with `priority=5`:
-
- iex> Pleroma.Jobs.enqueue(:example_queue, Module, [:job_name], 5)
- :ok
-
- Enqueue `Module.perform(:another_job, data)` with `priority=1`:
-
- iex> data = "foobar"
- iex> Pleroma.Jobs.enqueue(:example_queue, Module, [:another_job, data])
- :ok
-
- Enqueue `Module.perform(:foobar_job, :foo, :bar, 42)` with `priority=1`:
-
- iex> Pleroma.Jobs.enqueue(:example_queue, Module, [:foobar_job, :foo, :bar, 42])
- :ok
-
- """
-
- def enqueue(queue_name, mod, args, priority \\ 1)
-
- if Mix.env() == :test do
- def enqueue(_queue_name, mod, args, _priority) do
- apply(mod, :perform, args)
- end
- else
- @spec enqueue(atom(), atom(), [any()], integer()) :: :ok
- def enqueue(queue_name, mod, args, priority) do
- GenServer.cast(__MODULE__, {:enqueue, queue_name, mod, args, priority})
- end
- end
-
- def handle_cast({:enqueue, queue_name, mod, args, priority}, state) do
- {running_jobs, queue} = state[:queues][queue_name]
-
- queue = enqueue_sorted(queue, {mod, args}, priority)
-
- state =
- state
- |> update_queue(queue_name, {running_jobs, queue})
- |> maybe_start_job(queue_name, running_jobs, queue)
-
- {:noreply, state}
- end
-
- def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do
- queue_name = state.refs[ref]
-
- {running_jobs, queue} = state[:queues][queue_name]
-
- running_jobs = :sets.del_element(ref, running_jobs)
-
- state =
- state
- |> remove_ref(ref)
- |> update_queue(queue_name, {running_jobs, queue})
- |> maybe_start_job(queue_name, running_jobs, queue)
-
- {:noreply, state}
- end
-
- def maybe_start_job(state, queue_name, running_jobs, queue) do
- if :sets.size(running_jobs) < Pleroma.Config.get([__MODULE__, queue_name, :max_jobs]) &&
- queue != [] do
- {{mod, args}, queue} = queue_pop(queue)
- {:ok, pid} = Task.start(fn -> apply(mod, :perform, args) end)
- mref = Process.monitor(pid)
-
- state
- |> add_ref(queue_name, mref)
- |> update_queue(queue_name, {:sets.add_element(mref, running_jobs), queue})
- else
- state
- end
- end
-
- def enqueue_sorted(queue, element, priority) do
- [%{item: element, priority: priority} | queue]
- |> Enum.sort_by(fn %{priority: priority} -> priority end)
- end
-
- def queue_pop([%{item: element} | queue]) do
- {element, queue}
- end
-
- defp add_ref(state, queue_name, ref) do
- refs = Map.put(state[:refs], ref, queue_name)
- Map.put(state, :refs, refs)
- end
-
- defp remove_ref(state, ref) do
- refs = Map.delete(state[:refs], ref)
- Map.put(state, :refs, refs)
- end
-
- defp update_queue(state, queue_name, data) do
- queues = Map.put(state[:queues], queue_name, data)
- Map.put(state, :queues, queues)
- end
-end
diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex
index 55c4cf6df..a5b1cad68 100644
--- a/lib/pleroma/list.ex
+++ b/lib/pleroma/list.ex
@@ -80,7 +80,7 @@ defmodule Pleroma.List do
# Get lists to which the account belongs.
def get_lists_account_belongs(%User{} = owner, account_id) do
- user = Repo.get(User, account_id)
+ user = User.get_cached_by_id(account_id)
query =
from(
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index fe8181d8b..dd274cf6b 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -7,12 +7,15 @@ defmodule Pleroma.Notification do
alias Pleroma.Activity
alias Pleroma.Notification
+ alias Pleroma.Object
+ alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
import Ecto.Query
+ import Ecto.Changeset
schema "notifications" do
field(:seen, :boolean, default: false)
@@ -22,36 +25,30 @@ defmodule Pleroma.Notification do
timestamps()
end
- # TODO: Make generic and unify (see activity_pub.ex)
- defp restrict_max(query, %{"max_id" => max_id}) do
- from(activity in query, where: activity.id < ^max_id)
+ def changeset(%Notification{} = notification, attrs) do
+ notification
+ |> cast(attrs, [:seen])
end
- defp restrict_max(query, _), do: query
-
- defp restrict_since(query, %{"since_id" => since_id}) do
- from(activity in query, where: activity.id > ^since_id)
+ def for_user_query(user) do
+ Notification
+ |> where(user_id: ^user.id)
+ |> join(:inner, [n], activity in assoc(n, :activity))
+ |> join(:left, [n, a], object in Object,
+ on:
+ fragment(
+ "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
+ object.data,
+ a.data
+ )
+ )
+ |> preload([n, a, o], activity: {a, object: o})
end
- defp restrict_since(query, _), do: query
-
def for_user(user, opts \\ %{}) do
- query =
- from(
- n in Notification,
- where: n.user_id == ^user.id,
- order_by: [desc: n.id],
- join: activity in assoc(n, :activity),
- preload: [activity: activity],
- limit: 20
- )
-
- query =
- query
- |> restrict_since(opts)
- |> restrict_max(opts)
-
- Repo.all(query)
+ user
+ |> for_user_query()
+ |> Pagination.fetch_paginated(opts)
end
def set_read_up_to(%{id: user_id} = _user, id) do
@@ -68,6 +65,14 @@ defmodule Pleroma.Notification do
Repo.update_all(query, [])
end
+ def read_one(%User{} = user, notification_id) do
+ with {:ok, %Notification{} = notification} <- get(user, notification_id) do
+ notification
+ |> changeset(%{seen: true})
+ |> Repo.update()
+ end
+ end
+
def get(%{id: user_id} = _user, id) do
query =
from(
@@ -93,6 +98,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)
@@ -117,13 +130,7 @@ defmodule Pleroma.Notification do
# TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user) do
- unless User.blocks?(user, %{ap_id: activity.data["actor"]}) or
- CommonAPI.thread_muted?(user, activity) or user.ap_id == activity.data["actor"] or
- (activity.data["type"] == "Follow" and
- Enum.any?(Notification.for_user(user), fn notif ->
- notif.activity.data["type"] == "Follow" and
- notif.activity.data["actor"] == activity.data["actor"]
- end)) do
+ unless skip?(activity, user) do
notification = %Notification{user_id: user.id, activity: activity}
{:ok, notification} = Repo.insert(notification)
Pleroma.Web.Streamer.stream("user", notification)
@@ -143,10 +150,65 @@ defmodule Pleroma.Notification do
[]
|> Utils.maybe_notify_to_recipients(activity)
|> Utils.maybe_notify_mentioned_recipients(activity)
+ |> Utils.maybe_notify_subscribers(activity)
|> Enum.uniq()
User.get_users_from_set(recipients, local_only)
end
def get_notified_from_activity(_, _local_only), do: []
+
+ def skip?(activity, user) do
+ [:self, :blocked, :local, :muted, :followers, :follows, :recently_followed]
+ |> Enum.any?(&skip?(&1, activity, user))
+ end
+
+ def skip?(:self, activity, user) do
+ activity.data["actor"] == user.ap_id
+ end
+
+ def skip?(:blocked, activity, user) do
+ actor = activity.data["actor"]
+ User.blocks?(user, %{ap_id: actor})
+ end
+
+ def skip?(:local, %{local: true}, %{info: %{notification_settings: %{"local" => false}}}),
+ do: true
+
+ def skip?(:local, %{local: false}, %{info: %{notification_settings: %{"remote" => false}}}),
+ do: true
+
+ def skip?(:muted, activity, user) do
+ actor = activity.data["actor"]
+
+ User.mutes?(user, %{ap_id: actor}) or CommonAPI.thread_muted?(user, activity)
+ end
+
+ def skip?(
+ :followers,
+ activity,
+ %{info: %{notification_settings: %{"followers" => false}}} = user
+ ) do
+ actor = activity.data["actor"]
+ follower = User.get_cached_by_ap_id(actor)
+ User.following?(follower, user)
+ end
+
+ def skip?(:follows, activity, %{info: %{notification_settings: %{"follows" => false}}} = user) do
+ actor = activity.data["actor"]
+ followed = User.get_cached_by_ap_id(actor)
+ User.following?(user, followed)
+ end
+
+ def skip?(:recently_followed, %{data: %{"type" => "Follow"}} = activity, user) do
+ actor = activity.data["actor"]
+
+ Notification.for_user(user)
+ |> Enum.any?(fn
+ %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
+ _ -> false
+ end)
+ end
+
+ def skip?(_, _, _), do: false
end
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index 58e46ef1d..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
@@ -14,6 +15,8 @@ defmodule Pleroma.Object do
import Ecto.Query
import Ecto.Changeset
+ require Logger
+
schema "objects" do
field(:data, :map)
@@ -38,9 +41,44 @@ defmodule Pleroma.Object do
Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
end
- 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(_, 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(%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
+ %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}}}, 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, fetch_remote)
+ end
+
+ 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, fetch_remote)
+ end
+
+ # Old way, try fetching the object through cache.
+ 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}),
@@ -104,4 +142,50 @@ defmodule Pleroma.Object do
e -> e
end
end
+
+ def increase_replies_count(ap_id) do
+ Object
+ |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
+ |> update([o],
+ set: [
+ data:
+ fragment(
+ """
+ jsonb_set(?, '{repliesCount}',
+ (coalesce((?->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
+ """,
+ o.data,
+ o.data
+ )
+ ]
+ )
+ |> Repo.update_all([])
+ |> case do
+ {1, [object]} -> set_cache(object)
+ _ -> {:error, "Not found"}
+ end
+ end
+
+ def decrease_replies_count(ap_id) do
+ Object
+ |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
+ |> update([o],
+ set: [
+ data:
+ fragment(
+ """
+ jsonb_set(?, '{repliesCount}',
+ (greatest(0, (?->>'repliesCount')::int - 1))::varchar::jsonb, true)
+ """,
+ o.data,
+ o.data
+ )
+ ]
+ )
+ |> Repo.update_all([])
+ |> case do
+ {1, [object]} -> set_cache(object)
+ _ -> {:error, "Not found"}
+ end
+ end
end
diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex
new file mode 100644
index 000000000..25bd911fb
--- /dev/null
+++ b/lib/pleroma/object/containment.ex
@@ -0,0 +1,61 @@
+defmodule Pleroma.Object.Containment do
+ @moduledoc """
+ # Object Containment
+
+ This module contains some useful functions for containing objects to specific
+ origins and determining those origins. They previously lived in the
+ ActivityPub `Transmogrifier` module.
+
+ Object containment is an important step in validating remote objects to prevent
+ spoofing, therefore removal of object containment functions is NOT recommended.
+ """
+ def get_actor(%{"actor" => actor}) when is_binary(actor) do
+ actor
+ end
+
+ def get_actor(%{"actor" => actor}) when is_list(actor) do
+ if is_binary(Enum.at(actor, 0)) do
+ Enum.at(actor, 0)
+ else
+ Enum.find(actor, fn %{"type" => type} -> type in ["Person", "Service", "Application"] end)
+ |> Map.get("id")
+ end
+ end
+
+ def get_actor(%{"actor" => %{"id" => id}}) when is_bitstring(id) do
+ id
+ end
+
+ def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do
+ get_actor(%{"actor" => actor})
+ end
+
+ @doc """
+ Checks that an imported AP object's actor matches the domain it came from.
+ """
+ def contain_origin(_id, %{"actor" => nil}), do: :error
+
+ def contain_origin(id, %{"actor" => _actor} = params) do
+ id_uri = URI.parse(id)
+ actor_uri = URI.parse(get_actor(params))
+
+ if id_uri.host == actor_uri.host do
+ :ok
+ else
+ :error
+ end
+ end
+
+ def contain_origin_from_id(_id, %{"id" => nil}), do: :error
+
+ def contain_origin_from_id(id, %{"id" => other_id} = _params) do
+ id_uri = URI.parse(id)
+ other_uri = URI.parse(other_id)
+
+ if id_uri.host == other_uri.host do
+ :ok
+ else
+ :error
+ end
+ end
+end
diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
new file mode 100644
index 000000000..8d4bcc95e
--- /dev/null
+++ b/lib/pleroma/object/fetcher.ex
@@ -0,0 +1,75 @@
+defmodule Pleroma.Object.Fetcher do
+ alias Pleroma.Object
+ alias Pleroma.Object.Containment
+ alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Web.OStatus
+
+ require Logger
+
+ @httpoison Application.get_env(:pleroma, :httpoison)
+
+ # TODO:
+ # This will create a Create activity, which we need internally at the moment.
+ def fetch_object_from_id(id) do
+ if object = Object.get_cached_by_ap_id(id) do
+ {:ok, object}
+ else
+ Logger.info("Fetching #{id} via AP")
+
+ with {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
+ nil <- Object.normalize(data, false),
+ params <- %{
+ "type" => "Create",
+ "to" => data["to"],
+ "cc" => data["cc"],
+ "actor" => data["actor"] || data["attributedTo"],
+ "object" => data
+ },
+ :ok <- Containment.contain_origin(id, params),
+ {:ok, activity} <- Transmogrifier.handle_incoming(params) do
+ {:ok, Object.normalize(activity, false)}
+ else
+ {:error, {:reject, nil}} ->
+ {:reject, nil}
+
+ object = %Object{} ->
+ {:ok, object}
+
+ _e ->
+ Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
+
+ case OStatus.fetch_activity_from_url(id) do
+ {:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)}
+ e -> e
+ end
+ end
+ end
+ end
+
+ def fetch_object_from_id!(id) do
+ with {:ok, object} <- fetch_object_from_id(id) do
+ object
+ else
+ _e ->
+ nil
+ end
+ end
+
+ def fetch_and_contain_remote_object_from_id(id) do
+ Logger.info("Fetching object #{id} via AP")
+
+ with true <- String.starts_with?(id, "http"),
+ {:ok, %{body: body, status: code}} when code in 200..299 <-
+ @httpoison.get(
+ id,
+ [{:Accept, "application/activity+json"}]
+ ),
+ {:ok, data} <- Jason.decode(body),
+ :ok <- Containment.contain_origin_from_id(id, data) do
+ {:ok, data}
+ else
+ e ->
+ {:error, e}
+ end
+ end
+end
diff --git a/lib/pleroma/pagination.ex b/lib/pleroma/pagination.ex
new file mode 100644
index 000000000..f435e5c9c
--- /dev/null
+++ b/lib/pleroma/pagination.ex
@@ -0,0 +1,84 @@
+defmodule Pleroma.Pagination do
+ @moduledoc """
+ Implements Mastodon-compatible pagination.
+ """
+
+ import Ecto.Query
+ import Ecto.Changeset
+
+ alias Pleroma.Repo
+
+ @default_limit 20
+
+ def fetch_paginated(query, params) do
+ options = cast_params(params)
+
+ query
+ |> paginate(options)
+ |> Repo.all()
+ |> enforce_order(options)
+ end
+
+ def paginate(query, options) do
+ query
+ |> restrict(:min_id, options)
+ |> restrict(:since_id, options)
+ |> restrict(:max_id, options)
+ |> restrict(:order, options)
+ |> restrict(:limit, options)
+ end
+
+ defp cast_params(params) do
+ param_types = %{
+ min_id: :string,
+ since_id: :string,
+ max_id: :string,
+ 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
+
+ defp restrict(query, :min_id, %{min_id: min_id}) do
+ where(query, [q], q.id > ^min_id)
+ end
+
+ defp restrict(query, :since_id, %{since_id: since_id}) do
+ where(query, [q], q.id > ^since_id)
+ end
+
+ defp restrict(query, :max_id, %{max_id: max_id}) do
+ where(query, [q], q.id < ^max_id)
+ end
+
+ defp restrict(query, :order, %{min_id: _}) do
+ order_by(query, [u], fragment("? asc nulls last", u.id))
+ end
+
+ defp restrict(query, :order, _options) do
+ order_by(query, [u], fragment("? desc nulls last", u.id))
+ end
+
+ defp restrict(query, :limit, options) do
+ limit = Map.get(options, :limit, @default_limit)
+
+ query
+ |> limit(^limit)
+ end
+
+ defp restrict(query, _, _), do: query
+
+ defp enforce_order(result, %{min_id: _}) do
+ result
+ |> Enum.reverse()
+ end
+
+ defp enforce_order(result, _), do: result
+end
diff --git a/lib/pleroma/plugs/instance_static.ex b/lib/pleroma/plugs/instance_static.ex
index 41125921a..a64f1ea80 100644
--- a/lib/pleroma/plugs/instance_static.ex
+++ b/lib/pleroma/plugs/instance_static.ex
@@ -21,7 +21,8 @@ defmodule Pleroma.Plugs.InstanceStatic do
end
end
- @only ~w(index.html static emoji packs sounds images instance favicon.png sw.js sw-pleroma.js)
+ @only ~w(index.html robots.txt static emoji packs sounds images instance favicon.png sw.js
+ sw-pleroma.js)
def init(opts) do
opts
diff --git a/lib/pleroma/plugs/uploaded_media.ex b/lib/pleroma/plugs/uploaded_media.ex
index 13aa8641a..fd77b8d8f 100644
--- a/lib/pleroma/plugs/uploaded_media.ex
+++ b/lib/pleroma/plugs/uploaded_media.ex
@@ -24,6 +24,18 @@ defmodule Pleroma.Plugs.UploadedMedia do
end
def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
+ conn =
+ case fetch_query_params(conn) do
+ %{query_params: %{"name" => name}} = conn ->
+ name = String.replace(name, "\"", "\\\"")
+
+ conn
+ |> put_resp_header("content-disposition", "filename=\"#{name}\"")
+
+ conn ->
+ conn
+ end
+
config = Pleroma.Config.get([Pleroma.Upload])
with uploader <- Keyword.fetch!(config, :uploader),
diff --git a/lib/pleroma/plugs/user_fetcher_plug.ex b/lib/pleroma/plugs/user_fetcher_plug.ex
index 5a77f6833..4089aa958 100644
--- a/lib/pleroma/plugs/user_fetcher_plug.ex
+++ b/lib/pleroma/plugs/user_fetcher_plug.ex
@@ -3,9 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.UserFetcherPlug do
- alias Pleroma.Repo
alias Pleroma.User
-
import Plug.Conn
def init(options) do
@@ -14,26 +12,10 @@ defmodule Pleroma.Plugs.UserFetcherPlug do
def call(conn, _options) do
with %{auth_credentials: %{username: username}} <- conn.assigns,
- {:ok, %User{} = user} <- user_fetcher(username) do
- conn
- |> assign(:auth_user, user)
+ %User{} = user <- User.get_by_nickname_or_email(username) do
+ assign(conn, :auth_user, user)
else
_ -> conn
end
end
-
- defp user_fetcher(username_or_email) do
- {
- :ok,
- cond do
- # First, try logging in as if it was a name
- user = Repo.get_by(User, %{nickname: username_or_email}) ->
- user
-
- # If we get nil, we try using it as an email
- user = Repo.get_by(User, %{email: username_or_email}) ->
- user
- end
- }
- end
end
diff --git a/lib/pleroma/registration.ex b/lib/pleroma/registration.ex
new file mode 100644
index 000000000..21fd1fc3f
--- /dev/null
+++ b/lib/pleroma/registration.ex
@@ -0,0 +1,57 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Registration do
+ use Ecto.Schema
+
+ import Ecto.Changeset
+
+ alias Pleroma.Registration
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
+
+ schema "registrations" do
+ belongs_to(:user, User, type: Pleroma.FlakeId)
+ field(:provider, :string)
+ field(:uid, :string)
+ field(:info, :map, default: %{})
+
+ timestamps()
+ end
+
+ def nickname(registration, default \\ nil),
+ do: Map.get(registration.info, "nickname", default)
+
+ def email(registration, default \\ nil),
+ do: Map.get(registration.info, "email", default)
+
+ def name(registration, default \\ nil),
+ do: Map.get(registration.info, "name", default)
+
+ def description(registration, default \\ nil),
+ do: Map.get(registration.info, "description", default)
+
+ def changeset(registration, params \\ %{}) do
+ registration
+ |> cast(params, [:user_id, :provider, :uid, :info])
+ |> validate_required([:provider, :uid])
+ |> foreign_key_constraint(:user_id)
+ |> unique_constraint(:uid, name: :registrations_provider_uid_index)
+ end
+
+ def bind_to_user(registration, user) do
+ registration
+ |> changeset(%{user_id: (user && user.id) || nil})
+ |> Repo.update()
+ end
+
+ def get_by_provider_uid(provider, uid) do
+ Repo.get_by(Registration,
+ provider: to_string(provider),
+ uid: to_string(uid)
+ )
+ end
+end
diff --git a/lib/pleroma/repo.ex b/lib/pleroma/repo.ex
index e6a51b19e..aa5d427ae 100644
--- a/lib/pleroma/repo.ex
+++ b/lib/pleroma/repo.ex
@@ -3,7 +3,14 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Repo do
- use Ecto.Repo, otp_app: :pleroma
+ use Ecto.Repo,
+ otp_app: :pleroma,
+ adapter: Ecto.Adapters.Postgres,
+ migration_timestamps: [type: :naive_datetime_usec]
+
+ defmodule Instrumenter do
+ use Prometheus.EctoInstrumenter
+ end
@doc """
Dynamically loads the repository url from the
diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex
index 6298b92f4..a3f177fec 100644
--- a/lib/pleroma/reverse_proxy.ex
+++ b/lib/pleroma/reverse_proxy.ex
@@ -311,7 +311,25 @@ defmodule Pleroma.ReverseProxy do
end
if attachment? do
- disposition = "attachment; filename=" <> Keyword.get(opts, :attachment_name, "attachment")
+ name =
+ try do
+ {{"content-disposition", content_disposition_string}, _} =
+ List.keytake(headers, "content-disposition", 0)
+
+ [name | _] =
+ Regex.run(
+ ~r/filename="((?:[^"\\]|\\.)*)"/u,
+ content_disposition_string || "",
+ capture: :all_but_first
+ )
+
+ name
+ rescue
+ MatchError -> Keyword.get(opts, :attachment_name, "attachment")
+ end
+
+ disposition = "attachment; filename=\"#{name}\""
+
List.keystore(headers, "content-disposition", 0, {"content-disposition", disposition})
else
headers
diff --git a/lib/pleroma/scheduled_activity.ex b/lib/pleroma/scheduled_activity.ex
new file mode 100644
index 000000000..de0e54699
--- /dev/null
+++ b/lib/pleroma/scheduled_activity.ex
@@ -0,0 +1,161 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ScheduledActivity do
+ use Ecto.Schema
+
+ alias Pleroma.Config
+ alias Pleroma.Repo
+ alias Pleroma.ScheduledActivity
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI.Utils
+
+ import Ecto.Query
+ import Ecto.Changeset
+
+ @min_offset :timer.minutes(5)
+
+ schema "scheduled_activities" do
+ belongs_to(:user, User, type: Pleroma.FlakeId)
+ field(:scheduled_at, :naive_datetime)
+ field(:params, :map)
+
+ timestamps()
+ end
+
+ def changeset(%ScheduledActivity{} = scheduled_activity, attrs) do
+ scheduled_activity
+ |> cast(attrs, [:scheduled_at, :params])
+ |> validate_required([:scheduled_at, :params])
+ |> validate_scheduled_at()
+ |> with_media_attachments()
+ end
+
+ defp with_media_attachments(
+ %{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset
+ )
+ when is_list(media_ids) do
+ media_attachments = Utils.attachments_from_ids(%{"media_ids" => media_ids})
+
+ params =
+ params
+ |> Map.put("media_attachments", media_attachments)
+ |> Map.put("media_ids", media_ids)
+
+ put_change(changeset, :params, params)
+ end
+
+ defp with_media_attachments(changeset), do: changeset
+
+ def update_changeset(%ScheduledActivity{} = scheduled_activity, attrs) do
+ scheduled_activity
+ |> cast(attrs, [:scheduled_at])
+ |> validate_required([:scheduled_at])
+ |> validate_scheduled_at()
+ end
+
+ def validate_scheduled_at(changeset) do
+ validate_change(changeset, :scheduled_at, fn _, scheduled_at ->
+ cond do
+ not far_enough?(scheduled_at) ->
+ [scheduled_at: "must be at least 5 minutes from now"]
+
+ exceeds_daily_user_limit?(changeset.data.user_id, scheduled_at) ->
+ [scheduled_at: "daily limit exceeded"]
+
+ exceeds_total_user_limit?(changeset.data.user_id) ->
+ [scheduled_at: "total limit exceeded"]
+
+ true ->
+ []
+ end
+ end)
+ end
+
+ def exceeds_daily_user_limit?(user_id, scheduled_at) do
+ ScheduledActivity
+ |> where(user_id: ^user_id)
+ |> where([sa], type(sa.scheduled_at, :date) == type(^scheduled_at, :date))
+ |> select([sa], count(sa.id))
+ |> Repo.one()
+ |> Kernel.>=(Config.get([ScheduledActivity, :daily_user_limit]))
+ end
+
+ def exceeds_total_user_limit?(user_id) do
+ ScheduledActivity
+ |> where(user_id: ^user_id)
+ |> select([sa], count(sa.id))
+ |> Repo.one()
+ |> Kernel.>=(Config.get([ScheduledActivity, :total_user_limit]))
+ end
+
+ def far_enough?(scheduled_at) when is_binary(scheduled_at) do
+ with {:ok, scheduled_at} <- Ecto.Type.cast(:naive_datetime, scheduled_at) do
+ far_enough?(scheduled_at)
+ else
+ _ -> false
+ end
+ end
+
+ def far_enough?(scheduled_at) do
+ now = NaiveDateTime.utc_now()
+ diff = NaiveDateTime.diff(scheduled_at, now, :millisecond)
+ diff > @min_offset
+ end
+
+ def new(%User{} = user, attrs) do
+ %ScheduledActivity{user_id: user.id}
+ |> changeset(attrs)
+ end
+
+ def create(%User{} = user, attrs) do
+ user
+ |> new(attrs)
+ |> Repo.insert()
+ end
+
+ def get(%User{} = user, scheduled_activity_id) do
+ ScheduledActivity
+ |> where(user_id: ^user.id)
+ |> where(id: ^scheduled_activity_id)
+ |> Repo.one()
+ end
+
+ def update(%ScheduledActivity{} = scheduled_activity, attrs) do
+ scheduled_activity
+ |> update_changeset(attrs)
+ |> Repo.update()
+ end
+
+ def delete(%ScheduledActivity{} = scheduled_activity) do
+ scheduled_activity
+ |> Repo.delete()
+ end
+
+ def delete(id) when is_binary(id) or is_integer(id) do
+ ScheduledActivity
+ |> where(id: ^id)
+ |> select([sa], sa)
+ |> Repo.delete_all()
+ |> case do
+ {1, [scheduled_activity]} -> {:ok, scheduled_activity}
+ _ -> :error
+ end
+ end
+
+ def for_user_query(%User{} = user) do
+ ScheduledActivity
+ |> where(user_id: ^user.id)
+ end
+
+ def due_activities(offset \\ 0) do
+ naive_datetime =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.add(offset, :millisecond)
+
+ ScheduledActivity
+ |> where([sa], sa.scheduled_at < ^naive_datetime)
+ |> Repo.all()
+ end
+end
diff --git a/lib/pleroma/scheduled_activity_worker.ex b/lib/pleroma/scheduled_activity_worker.ex
new file mode 100644
index 000000000..65b38622f
--- /dev/null
+++ b/lib/pleroma/scheduled_activity_worker.ex
@@ -0,0 +1,58 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ScheduledActivityWorker do
+ @moduledoc """
+ Sends scheduled activities to the job queue.
+ """
+
+ alias Pleroma.Config
+ alias Pleroma.ScheduledActivity
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+ use GenServer
+ require Logger
+
+ @schedule_interval :timer.minutes(1)
+
+ def start_link do
+ GenServer.start_link(__MODULE__, nil)
+ end
+
+ def init(_) do
+ if Config.get([ScheduledActivity, :enabled]) do
+ schedule_next()
+ {:ok, nil}
+ else
+ :ignore
+ end
+ end
+
+ def perform(:execute, scheduled_activity_id) do
+ try do
+ {:ok, scheduled_activity} = ScheduledActivity.delete(scheduled_activity_id)
+ %User{} = user = User.get_cached_by_id(scheduled_activity.user_id)
+ {:ok, _result} = CommonAPI.post(user, scheduled_activity.params)
+ rescue
+ error ->
+ Logger.error(
+ "#{__MODULE__} Couldn't create a status from the scheduled activity: #{inspect(error)}"
+ )
+ end
+ end
+
+ def handle_info(:perform, state) do
+ ScheduledActivity.due_activities(@schedule_interval)
+ |> Enum.each(fn scheduled_activity ->
+ PleromaJobQueue.enqueue(:scheduled_activities, __MODULE__, [:execute, scheduled_activity.id])
+ end)
+
+ schedule_next()
+ {:noreply, state}
+ end
+
+ defp schedule_next do
+ Process.send_after(self(), :perform, @schedule_interval)
+ end
+end
diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex
index 1a97e9fde..f72334930 100644
--- a/lib/pleroma/upload.ex
+++ b/lib/pleroma/upload.ex
@@ -70,7 +70,7 @@ defmodule Pleroma.Upload do
%{
"type" => "Link",
"mediaType" => upload.content_type,
- "href" => url_from_spec(opts.base_url, url_spec)
+ "href" => url_from_spec(upload, opts.base_url, url_spec)
}
],
"name" => Map.get(opts, :description) || upload.name
@@ -219,14 +219,18 @@ defmodule Pleroma.Upload do
tmp_path
end
- defp url_from_spec(base_url, {:file, path}) do
+ defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do
path =
- path
- |> URI.encode(&char_unescaped?/1)
+ URI.encode(path, &char_unescaped?/1) <>
+ if Pleroma.Config.get([__MODULE__, :link_name], false) do
+ "?name=#{URI.encode(name, &char_unescaped?/1)}"
+ else
+ ""
+ end
[base_url, "media", path]
|> Path.join()
end
- defp url_from_spec(_base_url, {:url, url}), do: url
+ defp url_from_spec(_upload, _base_url, {:url, url}), do: url
end
diff --git a/lib/pleroma/uploaders/s3.ex b/lib/pleroma/uploaders/s3.ex
index e7de3f3e0..521daa93b 100644
--- a/lib/pleroma/uploaders/s3.ex
+++ b/lib/pleroma/uploaders/s3.ex
@@ -13,10 +13,15 @@ defmodule Pleroma.Uploaders.S3 do
bucket = Keyword.fetch!(config, :bucket)
bucket_with_namespace =
- if namespace = Keyword.get(config, :bucket_namespace) do
- namespace <> ":" <> bucket
- else
- bucket
+ cond do
+ truncated_namespace = Keyword.get(config, :truncated_namespace) ->
+ truncated_namespace
+
+ namespace = Keyword.get(config, :bucket_namespace) ->
+ namespace <> ":" <> bucket
+
+ true ->
+ bucket
end
{:ok,
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 5e8dfc669..755d8f773 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -10,9 +10,11 @@ defmodule Pleroma.User do
alias Comeonin.Pbkdf2
alias Pleroma.Activity
+ alias Pleroma.Bookmark
alias Pleroma.Formatter
alias Pleroma.Notification
alias Pleroma.Object
+ alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web
@@ -50,23 +52,21 @@ defmodule Pleroma.User do
field(:local, :boolean, default: true)
field(:follower_address, :string)
field(:search_rank, :float, virtual: true)
+ field(:search_type, :integer, virtual: true)
field(:tags, {:array, :string}, default: [])
- field(:bookmarks, {:array, :string}, default: [])
- field(:last_refreshed_at, :naive_datetime)
+ field(:last_refreshed_at, :naive_datetime_usec)
+ has_many(:bookmarks, Bookmark)
has_many(:notifications, Notification)
+ has_many(:registrations, Registration)
embeds_one(:info, Pleroma.User.Info)
timestamps()
end
- def auth_active?(%User{local: false}), do: true
-
- def auth_active?(%User{info: %User.Info{confirmation_pending: false}}), do: true
-
def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
do: !Pleroma.Config.get([:instance, :account_activation_required])
- def auth_active?(_), do: false
+ def auth_active?(%User{}), do: true
def visible_for?(user, for_user \\ nil)
@@ -82,17 +82,17 @@ defmodule Pleroma.User do
def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
def superuser?(_), do: false
- def avatar_url(user) do
+ def avatar_url(user, options \\ []) do
case user.avatar do
%{"url" => [%{"href" => href} | _]} -> href
- _ -> "#{Web.base_url()}/images/avi.png"
+ _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
end
end
- def banner_url(user) do
+ def banner_url(user, options \\ []) do
case user.info.banner do
%{"url" => [%{"href" => href} | _]} -> href
- _ -> "#{Web.base_url()}/images/banner.png"
+ _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
end
end
@@ -104,9 +104,8 @@ defmodule Pleroma.User do
"#{Web.base_url()}/users/#{nickname}"
end
- def ap_followers(%User{} = user) do
- "#{ap_id(user)}/followers"
- end
+ def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
+ def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
def user_info(%User{} = user) do
oneself = if user.local, do: 1, else: 0
@@ -220,7 +219,7 @@ defmodule Pleroma.User do
changeset =
struct
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
- |> validate_required([:email, :name, :nickname, :password, :password_confirmation])
+ |> validate_required([:name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password)
|> unique_constraint(:email)
|> unique_constraint(:nickname)
@@ -231,6 +230,13 @@ defmodule Pleroma.User do
|> validate_length(:name, min: 1, max: 100)
|> put_change(:info, info_change)
+ changeset =
+ if opts[:external] do
+ changeset
+ else
+ validate_required(changeset, [:email])
+ end
+
if changeset.valid? do
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
@@ -264,6 +270,7 @@ defmodule Pleroma.User do
def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset),
{:ok, user} <- autofollow_users(user),
+ {:ok, user} <- set_cache(user),
{:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
{:ok, _} <- try_send_confirmation_email(user) do
{:ok, user}
@@ -274,8 +281,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
@@ -335,10 +344,11 @@ defmodule Pleroma.User do
^followed_addresses
)
]
- ]
+ ],
+ select: u
)
- {1, [follower]} = Repo.update_all(q, [], returning: true)
+ {1, [follower]} = Repo.update_all(q, [])
Enum.each(followeds, fn followed ->
update_follower_count(followed)
@@ -368,10 +378,11 @@ defmodule Pleroma.User do
q =
from(u in User,
where: u.id == ^follower.id,
- update: [push: [following: ^ap_followers]]
+ update: [push: [following: ^ap_followers]],
+ select: u
)
- {1, [follower]} = Repo.update_all(q, [], returning: true)
+ {1, [follower]} = Repo.update_all(q, [])
{:ok, _} = update_follower_count(followed)
@@ -386,10 +397,11 @@ defmodule Pleroma.User do
q =
from(u in User,
where: u.id == ^follower.id,
- update: [pull: [following: ^ap_followers]]
+ update: [pull: [following: ^ap_followers]],
+ select: u
)
- {1, [follower]} = Repo.update_all(q, [], returning: true)
+ {1, [follower]} = Repo.update_all(q, [])
{:ok, followed} = update_follower_count(followed)
@@ -443,10 +455,13 @@ defmodule Pleroma.User do
name = List.last(String.split(ap_id, "/"))
nickname = "#{name}@#{domain}"
- get_by_nickname(nickname)
+ get_cached_by_nickname(nickname)
end
- def set_cache(user) do
+ def set_cache({:ok, user}), do: set_cache(user)
+ def set_cache({:error, err}), do: {:error, err}
+
+ def set_cache(%User{} = user) do
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
@@ -514,11 +529,10 @@ defmodule Pleroma.User do
end
end
+ def get_by_email(email), do: Repo.get_by(User, email: email)
+
def get_by_nickname_or_email(nickname_or_email) do
- case user = Repo.get_by(User, nickname: nickname_or_email) do
- %User{} -> user
- nil -> Repo.get_by(User, email: nickname_or_email)
- end
+ get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
end
def get_cached_user_info(user) do
@@ -543,6 +557,7 @@ defmodule Pleroma.User do
with [_nick, _domain] <- String.split(nickname, "@"),
{:ok, user} <- fetch_by_nickname(nickname) do
if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
+ # TODO turn into job
{:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
end
@@ -645,7 +660,7 @@ defmodule Pleroma.User do
users =
user
|> User.get_follow_requests_query()
- |> join(:inner, [a], u in User, a.actor == u.ap_id)
+ |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
|> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
|> group_by([a, u], u.id)
|> select([a, u], u)
@@ -667,7 +682,8 @@ defmodule Pleroma.User do
)
]
)
- |> Repo.update_all([], returning: true)
+ |> select([u], u)
+ |> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
@@ -687,7 +703,8 @@ defmodule Pleroma.User do
)
]
)
- |> Repo.update_all([], returning: true)
+ |> select([u], u)
+ |> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
@@ -733,7 +750,8 @@ defmodule Pleroma.User do
)
]
)
- |> Repo.update_all([], returning: true)
+ |> select([u], u)
+ |> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
@@ -774,90 +792,59 @@ defmodule Pleroma.User do
Repo.all(query)
end
- @spec search_for_admin(%{
- local: boolean(),
- page: number(),
- page_size: number()
- }) :: {:ok, [Pleroma.User.t()], number()}
- def search_for_admin(%{query: nil, local: local, page: page, page_size: page_size}) do
- query =
- from(u in User, order_by: u.id)
- |> maybe_local_user_query(local)
-
- paginated_query =
- query
- |> paginate(page, page_size)
-
- count =
- query
- |> Repo.aggregate(:count, :id)
-
- {:ok, Repo.all(paginated_query), count}
- end
-
- @spec search_for_admin(%{
- query: binary(),
- admin: Pleroma.User.t(),
- local: boolean(),
- page: number(),
- page_size: number()
- }) :: {:ok, [Pleroma.User.t()], number()}
- def search_for_admin(%{
- query: term,
- admin: admin,
- local: local,
- page: page,
- page_size: page_size
- }) do
- term = String.trim_leading(term, "@")
-
- local_paginated_query =
- User
- |> maybe_local_user_query(local)
- |> paginate(page, page_size)
-
- search_query = fts_search_subquery(term, local_paginated_query)
-
- count =
- term
- |> fts_search_subquery()
- |> maybe_local_user_query(local)
- |> Repo.aggregate(:count, :id)
-
- {:ok, do_search(search_query, admin), count}
- end
-
def search(query, resolve \\ false, for_user \\ nil) do
# Strip the beginning @ off if there is a query
query = String.trim_leading(query, "@")
if resolve, do: get_or_fetch(query)
- fts_results = do_search(fts_search_subquery(query), for_user)
-
- {:ok, trigram_results} =
+ {:ok, results} =
Repo.transaction(fn ->
Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
- do_search(trigram_search_subquery(query), for_user)
+ Repo.all(search_query(query, for_user))
end)
- Enum.uniq_by(fts_results ++ trigram_results, & &1.id)
+ results
end
- defp do_search(subquery, for_user, options \\ []) do
- q =
- from(
- s in subquery(subquery),
- order_by: [desc: s.search_rank],
- limit: ^(options[:limit] || 20)
- )
+ def search_query(query, for_user) do
+ fts_subquery = fts_search_subquery(query)
+ trigram_subquery = trigram_search_subquery(query)
+ union_query = from(s in trigram_subquery, union_all: ^fts_subquery)
+ distinct_query = from(s in subquery(union_query), order_by: s.search_type, distinct: s.id)
- results =
- q
- |> Repo.all()
- |> Enum.filter(&(&1.search_rank > 0))
+ from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
+ order_by: [desc: s.search_rank],
+ limit: 20
+ )
+ end
+
+ defp boost_search_rank_query(query, nil), do: query
+
+ defp boost_search_rank_query(query, for_user) do
+ friends_ids = get_friends_ids(for_user)
+ followers_ids = get_followers_ids(for_user)
- boost_search_results(results, for_user)
+ from(u in subquery(query),
+ select_merge: %{
+ search_rank:
+ fragment(
+ """
+ CASE WHEN (?) THEN (?) * 1.3
+ WHEN (?) THEN (?) * 1.2
+ WHEN (?) THEN (?) * 1.1
+ ELSE (?) END
+ """,
+ u.id in ^friends_ids and u.id in ^followers_ids,
+ u.search_rank,
+ u.id in ^friends_ids,
+ u.search_rank,
+ u.id in ^followers_ids,
+ u.search_rank,
+ u.search_rank
+ )
+ }
+ )
end
defp fts_search_subquery(term, query \\ User) do
@@ -872,6 +859,7 @@ defmodule Pleroma.User do
from(
u in query,
select_merge: %{
+ search_type: ^0,
search_rank:
fragment(
"""
@@ -904,6 +892,8 @@ defmodule Pleroma.User do
from(
u in User,
select_merge: %{
+ # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
+ search_type: fragment("?", 1),
search_rank:
fragment(
"similarity(?, trim(? || ' ' || coalesce(?, '')))",
@@ -916,33 +906,6 @@ defmodule Pleroma.User do
)
end
- defp boost_search_results(results, nil), do: results
-
- defp boost_search_results(results, for_user) do
- friends_ids = get_friends_ids(for_user)
- followers_ids = get_followers_ids(for_user)
-
- Enum.map(
- results,
- fn u ->
- search_rank_coef =
- cond do
- u.id in friends_ids ->
- 1.2
-
- u.id in followers_ids ->
- 1.1
-
- true ->
- 1
- end
-
- Map.put(u, :search_rank, u.search_rank * search_rank_coef)
- end
- )
- |> Enum.sort_by(&(-&1.search_rank))
- end
-
def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
Enum.map(
blocked_identifiers,
@@ -984,6 +947,38 @@ defmodule Pleroma.User do
update_and_set_cache(cng)
end
+ def subscribe(subscriber, %{ap_id: ap_id}) do
+ deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
+
+ with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
+ blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
+
+ if blocked do
+ {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
+ else
+ info_cng =
+ subscribed.info
+ |> User.Info.add_to_subscribers(subscriber.ap_id)
+
+ change(subscribed)
+ |> put_embed(:info, info_cng)
+ |> update_and_set_cache()
+ end
+ end
+ end
+
+ def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
+ with %User{} = user <- get_cached_by_ap_id(ap_id) do
+ info_cng =
+ user.info
+ |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
+
+ change(user)
+ |> put_embed(:info, info_cng)
+ |> update_and_set_cache()
+ end
+ end
+
def block(blocker, %User{ap_id: ap_id} = blocked) do
# sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
blocker =
@@ -994,10 +989,20 @@ defmodule Pleroma.User do
blocker
end
+ blocker =
+ if subscribed_to?(blocked, blocker) do
+ {:ok, blocker} = unsubscribe(blocked, blocker)
+ blocker
+ else
+ blocker
+ end
+
if following?(blocked, blocker) do
unfollow(blocked, blocker)
end
+ {:ok, blocker} = update_follower_count(blocker)
+
info_cng =
blocker.info
|> User.Info.add_to_block(ap_id)
@@ -1011,7 +1016,7 @@ defmodule Pleroma.User do
# helper to handle the block given only an actor's AP id
def block(blocker, %{ap_id: ap_id}) do
- block(blocker, User.get_by_ap_id(ap_id))
+ block(blocker, get_cached_by_ap_id(ap_id))
end
def unblock(blocker, %{ap_id: ap_id}) do
@@ -1040,12 +1045,21 @@ defmodule Pleroma.User do
end)
end
+ def subscribed_to?(user, %{ap_id: ap_id}) do
+ with %User{} = target <- get_cached_by_ap_id(ap_id) do
+ Enum.member?(target.info.subscribers, user.ap_id)
+ end
+ end
+
def muted_users(user),
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes))
def blocked_users(user),
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
+ def subscribers(user),
+ do: Repo.all(from(u in User, where: u.ap_id in ^user.info.subscribers))
+
def block_domain(user, domain) do
info_cng =
user.info
@@ -1082,6 +1096,42 @@ defmodule Pleroma.User do
)
end
+ def maybe_external_user_query(query, external) do
+ if external, do: external_user_query(query), else: query
+ end
+
+ def external_user_query(query \\ User) do
+ from(
+ u in query,
+ where: u.local == false,
+ where: not is_nil(u.nickname)
+ )
+ end
+
+ def maybe_active_user_query(query, active) do
+ if active, do: active_user_query(query), else: query
+ end
+
+ def active_user_query(query \\ User) do
+ from(
+ u in query,
+ where: fragment("not (?->'deactivated' @> 'true')", u.info),
+ where: not is_nil(u.nickname)
+ )
+ end
+
+ def maybe_deactivated_user_query(query, deactivated) do
+ if deactivated, do: deactivated_user_query(query), else: query
+ end
+
+ def deactivated_user_query(query \\ User) do
+ from(
+ u in query,
+ where: fragment("(?->'deactivated' @> 'true')", u.info),
+ where: not is_nil(u.nickname)
+ )
+ end
+
def active_local_user_query do
from(
u in local_user_query(),
@@ -1107,32 +1157,41 @@ defmodule Pleroma.User do
update_and_set_cache(cng)
end
+ def update_notification_settings(%User{} = user, settings \\ %{}) do
+ info_changeset = User.Info.update_notification_settings(user.info, settings)
+
+ change(user)
+ |> put_embed(:info, info_changeset)
+ |> update_and_set_cache()
+ end
+
def delete(%User{} = user) do
{:ok, user} = User.deactivate(user)
# Remove all relationships
{:ok, followers} = User.get_followers(user)
- followers
- |> Enum.each(fn follower -> User.unfollow(follower, user) end)
+ Enum.each(followers, fn follower -> User.unfollow(follower, user) end)
{:ok, friends} = User.get_friends(user)
- friends
- |> Enum.each(fn followed -> User.unfollow(user, followed) end)
+ Enum.each(friends, fn followed -> User.unfollow(user, followed) end)
- query = from(a in Activity, where: a.actor == ^user.ap_id)
+ delete_user_activities(user)
+ end
- Repo.all(query)
- |> Enum.each(fn activity ->
- case activity.data["type"] do
- "Create" ->
- ActivityPub.delete(Object.normalize(activity.data["object"]))
+ def delete_user_activities(%User{ap_id: ap_id} = user) do
+ Activity
+ |> where(actor: ^ap_id)
+ |> Activity.with_preloaded_object()
+ |> Repo.all()
+ |> Enum.each(fn
+ %{data: %{"type" => "Create"}} = activity ->
+ activity |> Object.normalize() |> ActivityPub.delete()
- # TODO: Do something with likes, follows, repeats.
- _ ->
- "Doing nothing"
- end
+ # TODO: Do something with likes, follows, repeats.
+ _ ->
+ "Doing nothing"
end)
{:ok, user}
@@ -1162,13 +1221,16 @@ defmodule Pleroma.User do
end
def get_or_fetch_by_ap_id(ap_id) do
- user = get_by_ap_id(ap_id)
+ user = get_cached_by_ap_id(ap_id)
if !is_nil(user) and !User.needs_update?(user) do
{:ok, user}
else
- with %User{} = user <- fetch_by_ap_id(ap_id) do
- if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
+ # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
+ should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
+
+ if should_fetch_initial do
+ with {:ok, %User{} = user} = fetch_by_ap_id(ap_id) do
{:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
end
@@ -1182,7 +1244,7 @@ defmodule Pleroma.User do
def get_or_create_instance_user do
relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
- if user = get_by_ap_id(relay_uri) do
+ if user = get_cached_by_ap_id(relay_uri) do
user
else
changes =
@@ -1229,13 +1291,11 @@ defmodule Pleroma.User do
defp blank?(n), do: n
def insert_or_update_user(data) do
- data =
- data
- |> Map.put(:name, blank?(data[:name]) || data[:nickname])
-
- cs = User.remote_user_creation(data)
-
- Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
+ data
+ |> Map.put(:name, blank?(data[:name]) || data[:nickname])
+ |> remote_user_creation()
+ |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
+ |> set_cache()
end
def ap_enabled?(%User{local: true}), do: true
@@ -1251,8 +1311,8 @@ defmodule Pleroma.User do
# this is because we have synchronous follow APIs and need to simulate them
# with an async handshake
def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
- with %User{} = a <- Repo.get(User, a.id),
- %User{} = b <- Repo.get(User, b.id) do
+ with %User{} = a <- User.get_cached_by_id(a.id),
+ %User{} = b <- User.get_cached_by_id(b.id) do
{:ok, a, b}
else
_e ->
@@ -1262,8 +1322,8 @@ defmodule Pleroma.User do
def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
with :ok <- :timer.sleep(timeout),
- %User{} = a <- Repo.get(User, a.id),
- %User{} = b <- Repo.get(User, b.id) do
+ %User{} = a <- User.get_cached_by_id(a.id),
+ %User{} = b <- User.get_cached_by_id(b.id) do
{:ok, a, b}
else
_e ->
@@ -1302,7 +1362,7 @@ defmodule Pleroma.User do
end
def tag(nickname, tags) when is_binary(nickname),
- do: tag(User.get_by_nickname(nickname), tags)
+ do: tag(get_by_nickname(nickname), tags)
def tag(%User{} = user, tags),
do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
@@ -1314,7 +1374,7 @@ defmodule Pleroma.User do
end
def untag(nickname, tags) when is_binary(nickname),
- do: untag(User.get_by_nickname(nickname), tags)
+ do: untag(get_by_nickname(nickname), tags)
def untag(%User{} = user, tags),
do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
@@ -1328,22 +1388,6 @@ defmodule Pleroma.User do
updated_user
end
- def bookmark(%User{} = user, status_id) do
- bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
- update_bookmarks(user, bookmarks)
- end
-
- def unbookmark(%User{} = user, status_id) do
- bookmarks = Enum.uniq(user.bookmarks -- [status_id])
- update_bookmarks(user, bookmarks)
- end
-
- def update_bookmarks(%User{} = user, bookmarks) do
- user
- |> change(%{bookmarks: bookmarks})
- |> update_and_set_cache
- end
-
defp normalize_tags(tags) do
[tags]
|> List.flatten()
@@ -1393,4 +1437,8 @@ defmodule Pleroma.User do
offset: ^((page - 1) * page_size)
)
end
+
+ def showing_reblogs?(%User{} = user, %User{} = target) do
+ target.ap_id not in user.info.muted_reblogs
+ end
end
diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex
index e3fd65a6e..a3658d57f 100644
--- a/lib/pleroma/user/info.ex
+++ b/lib/pleroma/user/info.ex
@@ -21,6 +21,8 @@ defmodule Pleroma.User.Info do
field(:blocks, {:array, :string}, default: [])
field(:domain_blocks, {:array, :string}, default: [])
field(:mutes, {:array, :string}, default: [])
+ field(:muted_reblogs, {:array, :string}, default: [])
+ field(:subscribers, {:array, :string}, default: [])
field(:deactivated, :boolean, default: false)
field(:no_rich_text, :boolean, default: false)
field(:ap_enabled, :boolean, default: false)
@@ -36,9 +38,14 @@ defmodule Pleroma.User.Info do
field(:salmon, :string, default: nil)
field(:hide_followers, :boolean, default: false)
field(:hide_follows, :boolean, default: false)
+ field(:hide_favorites, :boolean, default: true)
field(:pinned_activities, {:array, :string}, default: [])
field(:flavour, :string, default: nil)
+ field(:notification_settings, :map,
+ default: %{"remote" => true, "local" => true, "followers" => true, "follows" => true}
+ )
+
# Found in the wild
# ap_id -> Where is this used?
# bio -> Where is this used?
@@ -56,6 +63,19 @@ defmodule Pleroma.User.Info do
|> validate_required([:deactivated])
end
+ def update_notification_settings(info, settings) do
+ notification_settings =
+ info.notification_settings
+ |> Map.merge(settings)
+ |> Map.take(["remote", "local", "followers", "follows"])
+
+ params = %{notification_settings: notification_settings}
+
+ info
+ |> cast(params, [:notification_settings])
+ |> validate_required([:notification_settings])
+ end
+
def add_to_note_count(info, number) do
set_note_count(info, info.note_count + number)
end
@@ -92,6 +112,14 @@ defmodule Pleroma.User.Info do
|> validate_required([:blocks])
end
+ def set_subscribers(info, subscribers) do
+ params = %{subscribers: subscribers}
+
+ info
+ |> cast(params, [:subscribers])
+ |> validate_required([:subscribers])
+ end
+
def add_to_mutes(info, muted) do
set_mutes(info, Enum.uniq([muted | info.mutes]))
end
@@ -108,6 +136,14 @@ defmodule Pleroma.User.Info do
set_blocks(info, List.delete(info.blocks, blocked))
end
+ def add_to_subscribers(info, subscribed) do
+ set_subscribers(info, Enum.uniq([subscribed | info.subscribers]))
+ end
+
+ def remove_from_subscribers(info, subscribed) do
+ set_subscribers(info, List.delete(info.subscribers, subscribed))
+ end
+
def set_domain_blocks(info, domain_blocks) do
params = %{domain_blocks: domain_blocks}
@@ -167,6 +203,7 @@ defmodule Pleroma.User.Info do
:banner,
:hide_follows,
:hide_followers,
+ :hide_favorites,
:background,
:show_role
])
@@ -190,14 +227,6 @@ defmodule Pleroma.User.Info do
cast(info, params, [:confirmation_pending, :confirmation_token])
end
- def mastodon_profile_update(info, params) do
- info
- |> cast(params, [
- :locked,
- :banner
- ])
- end
-
def mastodon_settings_update(info, settings) do
params = %{settings: settings}
@@ -259,4 +288,16 @@ defmodule Pleroma.User.Info do
moderator: is_moderator
}
end
+
+ def add_reblog_mute(info, ap_id) do
+ params = %{muted_reblogs: info.muted_reblogs ++ [ap_id]}
+
+ cast(info, params, [:muted_reblogs])
+ end
+
+ def remove_reblog_mute(info, ap_id) do
+ params = %{muted_reblogs: List.delete(info.muted_reblogs, ap_id)}
+
+ cast(info, params, [:muted_reblogs])
+ end
end
diff --git a/lib/pleroma/user_invite_token.ex b/lib/pleroma/user_invite_token.ex
index 9c5579934..86f0a5486 100644
--- a/lib/pleroma/user_invite_token.ex
+++ b/lib/pleroma/user_invite_token.ex
@@ -6,40 +6,119 @@ defmodule Pleroma.UserInviteToken do
use Ecto.Schema
import Ecto.Changeset
-
+ import Ecto.Query
alias Pleroma.Repo
alias Pleroma.UserInviteToken
+ @type t :: %__MODULE__{}
+ @type token :: String.t()
+
schema "user_invite_tokens" do
field(:token, :string)
field(:used, :boolean, default: false)
+ field(:max_use, :integer)
+ field(:expires_at, :date)
+ field(:uses, :integer, default: 0)
+ field(:invite_type, :string)
timestamps()
end
- def create_token do
+ @spec create_invite(map()) :: UserInviteToken.t()
+ def create_invite(params \\ %{}) do
+ %UserInviteToken{}
+ |> cast(params, [:max_use, :expires_at])
+ |> add_token()
+ |> assign_type()
+ |> Repo.insert()
+ end
+
+ defp add_token(changeset) do
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
+ put_change(changeset, :token, token)
+ end
- token = %UserInviteToken{
- used: false,
- token: token
- }
+ defp assign_type(%{changes: %{max_use: _max_use, expires_at: _expires_at}} = changeset) do
+ put_change(changeset, :invite_type, "reusable_date_limited")
+ end
+
+ defp assign_type(%{changes: %{expires_at: _expires_at}} = changeset) do
+ put_change(changeset, :invite_type, "date_limited")
+ end
+
+ defp assign_type(%{changes: %{max_use: _max_use}} = changeset) do
+ put_change(changeset, :invite_type, "reusable")
+ end
+
+ defp assign_type(changeset), do: put_change(changeset, :invite_type, "one_time")
- Repo.insert(token)
+ @spec list_invites() :: [UserInviteToken.t()]
+ def list_invites do
+ query = from(u in UserInviteToken, order_by: u.id)
+ Repo.all(query)
end
- def used_changeset(struct) do
- struct
- |> cast(%{}, [])
- |> put_change(:used, true)
+ @spec update_invite!(UserInviteToken.t(), map()) :: UserInviteToken.t() | no_return()
+ def update_invite!(invite, changes) do
+ change(invite, changes) |> Repo.update!()
end
- def mark_as_used(token) do
- with %{used: false} = token <- Repo.get_by(UserInviteToken, %{token: token}),
- {:ok, token} <- Repo.update(used_changeset(token)) do
- {:ok, token}
- else
- _e -> {:error, token}
+ @spec update_invite(UserInviteToken.t(), map()) ::
+ {:ok, UserInviteToken.t()} | {:error, Changeset.t()}
+ def update_invite(invite, changes) do
+ change(invite, changes) |> Repo.update()
+ end
+
+ @spec find_by_token!(token()) :: UserInviteToken.t() | no_return()
+ def find_by_token!(token), do: Repo.get_by!(UserInviteToken, token: token)
+
+ @spec find_by_token(token()) :: {:ok, UserInviteToken.t()} | nil
+ def find_by_token(token) do
+ with invite <- Repo.get_by(UserInviteToken, token: token) do
+ {:ok, invite}
end
end
+
+ @spec valid_invite?(UserInviteToken.t()) :: boolean()
+ def valid_invite?(%{invite_type: "one_time"} = invite) do
+ not invite.used
+ end
+
+ def valid_invite?(%{invite_type: "date_limited"} = invite) do
+ not_overdue_date?(invite) and not invite.used
+ end
+
+ def valid_invite?(%{invite_type: "reusable"} = invite) do
+ invite.uses < invite.max_use and not invite.used
+ end
+
+ def valid_invite?(%{invite_type: "reusable_date_limited"} = invite) do
+ not_overdue_date?(invite) and invite.uses < invite.max_use and not invite.used
+ end
+
+ defp not_overdue_date?(%{expires_at: expires_at}) do
+ Date.compare(Date.utc_today(), expires_at) in [:lt, :eq]
+ end
+
+ @spec update_usage!(UserInviteToken.t()) :: nil | UserInviteToken.t() | no_return()
+ def update_usage!(%{invite_type: "date_limited"}), do: nil
+
+ def update_usage!(%{invite_type: "one_time"} = invite),
+ do: update_invite!(invite, %{used: true})
+
+ def update_usage!(%{invite_type: invite_type} = invite)
+ when invite_type == "reusable" or invite_type == "reusable_date_limited" do
+ changes = %{
+ uses: invite.uses + 1
+ }
+
+ changes =
+ if changes.uses >= invite.max_use do
+ Map.put(changes, :used, true)
+ else
+ changes
+ end
+
+ update_invite!(invite, changes)
+ end
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 70db419ca..483a2153f 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
@@ -89,15 +90,36 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
end
- def insert(map, local \\ true) when is_map(map) do
+ def increase_replies_count_if_reply(%{
+ "object" => %{"inReplyTo" => reply_ap_id} = object,
+ "type" => "Create"
+ }) do
+ if is_public?(object) do
+ Object.increase_replies_count(reply_ap_id)
+ end
+ end
+
+ def increase_replies_count_if_reply(_create_data), do: :noop
+
+ def decrease_replies_count_if_reply(%Object{
+ data: %{"inReplyTo" => reply_ap_id} = object
+ }) do
+ if is_public?(object) do
+ Object.decrease_replies_count(reply_ap_id)
+ end
+ end
+
+ def decrease_replies_count_if_reply(_object), do: :noop
+
+ def insert(map, local \\ true, fake \\ false) when is_map(map) do
with nil <- Activity.normalize(map),
- map <- lazy_put_activity_defaults(map),
+ map <- lazy_put_activity_defaults(map, fake),
:ok <- check_actor_is_active(map["actor"]),
{_, true} <- {:remote_limit_error, check_remote_limit(map)},
{:ok, map} <- MRF.filter(map),
- :ok <- insert_full_object(map) do
- {recipients, _, _} = get_recipients(map)
-
+ {recipients, _, _} = get_recipients(map),
+ {:fake, false, map, recipients} <- {:fake, fake, map, recipients},
+ {:ok, map, object} <- insert_full_object(map) do
{:ok, activity} =
Repo.insert(%Activity{
data: map,
@@ -106,6 +128,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
recipients: recipients
})
+ # Splice in the child object if we have one.
+ activity =
+ if !is_nil(object) do
+ Map.put(activity, :object, object)
+ else
+ activity
+ end
+
Task.start(fn ->
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
end)
@@ -114,8 +144,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
stream_out(activity)
{:ok, activity}
else
- %Activity{} = activity -> {:ok, activity}
- error -> {:error, error}
+ %Activity{} = activity ->
+ {:ok, activity}
+
+ {:fake, true, map, recipients} ->
+ activity = %Activity{
+ data: map,
+ local: local,
+ actor: map["actor"],
+ recipients: recipients,
+ id: "pleroma:fakeid"
+ }
+
+ Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
+ {:ok, activity}
+
+ error ->
+ {:error, error}
end
end
@@ -134,12 +179,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
if activity.data["type"] in ["Create"] do
- activity.data["object"]
+ object = Object.normalize(activity)
+
+ 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
@@ -151,14 +198,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
if !Enum.member?(activity.data["cc"] || [], public) &&
!Enum.member?(
activity.data["to"],
- User.get_by_ap_id(activity.data["actor"]).follower_address
+ User.get_cached_by_ap_id(activity.data["actor"]).follower_address
),
do: Pleroma.Web.Streamer.stream("direct", activity)
end
end
end
- def create(%{to: to, actor: actor, context: context, object: object} = params) do
+ def create(%{to: to, actor: actor, context: context, object: object} = params, fake \\ false) do
additional = params[:additional] || %{}
# only accept false as false value
local = !(params[:local] == false)
@@ -169,12 +216,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
%{to: to, actor: actor, published: published, context: context, object: object},
additional
),
- {:ok, activity} <- insert(create_data, local),
+ {:ok, activity} <- insert(create_data, local, fake),
+ {:fake, false, activity} <- {:fake, fake, activity},
+ _ <- increase_replies_count_if_reply(create_data),
# Changing note count prior to enqueuing federation task in order to avoid
# race conditions on updating user.info
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
+ else
+ {:fake, true, activity} ->
+ {:ok, activity}
end
end
@@ -321,6 +373,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
"deleted_activity_id" => activity && activity.id
},
{:ok, activity} <- insert(data, local),
+ _ <- decrease_replies_count_if_reply(object),
# Changing note count prior to enqueuing federation task in order to avoid
# race conditions on updating user.info
{:ok, _actor} <- decrease_note_count_if_public(user, object),
@@ -370,20 +423,38 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
content: content
} = params
) do
- additional = params[:additional] || %{}
-
# only accept false as false value
local = !(params[:local] == false)
+ forward = !(params[:forward] == false)
+
+ additional = params[:additional] || %{}
- %{
+ params = %{
actor: actor,
context: context,
account: account,
statuses: statuses,
content: content
}
- |> make_flag_data(additional)
- |> insert(local)
+
+ additional =
+ if forward do
+ Map.merge(additional, %{"to" => [], "cc" => [account.ap_id]})
+ else
+ Map.merge(additional, %{"to" => [], "cc" => []})
+ end
+
+ with flag_data <- make_flag_data(params, additional),
+ {:ok, activity} <- insert(flag_data, local),
+ :ok <- maybe_federate(activity) do
+ Enum.each(User.all_superusers(), fn superuser ->
+ superuser
+ |> Pleroma.Emails.AdminEmail.report(actor, account, statuses, content)
+ |> Pleroma.Emails.Mailer.deliver_async()
+ end)
+
+ {:ok, activity}
+ end
end
def fetch_activities_for_context(context, opts \\ %{}) do
@@ -412,6 +483,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
),
order_by: [desc: :id]
)
+ |> Activity.with_preloaded_object()
Repo.all(query)
end
@@ -421,7 +493,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
q
|> restrict_unlisted()
- |> Repo.all()
+ |> Pagination.fetch_paginated(opts)
|> Enum.reverse()
end
@@ -500,37 +572,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
@@ -564,26 +648,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
@@ -609,10 +679,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
@@ -654,7 +728,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
@@ -679,23 +760,41 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_pinned(query, _), do: query
+ defp restrict_muted_reblogs(query, %{"muting_user" => %User{info: info}}) do
+ muted_reblogs = info.muted_reblogs || []
+
+ from(
+ activity in query,
+ where:
+ fragment(
+ "not ( ?->>'type' = 'Announce' and ? = ANY(?))",
+ activity.data,
+ activity.actor,
+ ^muted_reblogs
+ )
+ )
+ end
+
+ defp restrict_muted_reblogs(query, _), do: query
+
+ defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query
+
+ defp maybe_preload_objects(query, _) do
+ query
+ |> Activity.with_preloaded_object()
+ 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)
|> restrict_recipients(recipients, opts["user"])
|> restrict_tag(opts)
|> restrict_tag_reject(opts)
|> 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)
@@ -706,18 +805,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_replies(opts)
|> restrict_reblogs(opts)
|> restrict_pinned(opts)
+ |> restrict_muted_reblogs(opts)
end
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
@@ -782,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)}")
@@ -790,7 +890,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
def make_user_from_ap_id(ap_id) do
- if _user = User.get_by_ap_id(ap_id) do
+ if _user = User.get_cached_by_ap_id(ap_id) do
Transmogrifier.upgrade_user_from_ap_id(ap_id)
else
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do
@@ -892,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.data["object"])}
- 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"])}
- 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/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
index 25d5f9cd3..e8dfba672 100644
--- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
@@ -4,6 +4,10 @@
defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF
+ defp string_matches?(string, _) when not is_binary(string) do
+ false
+ end
+
defp string_matches?(string, pattern) when is_binary(pattern) do
String.contains?(string, pattern)
end
@@ -44,6 +48,20 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
end
defp check_replace(%{"object" => %{"content" => content, "summary" => summary}} = message) do
+ content =
+ if is_binary(content) do
+ content
+ else
+ ""
+ end
+
+ summary =
+ if is_binary(summary) do
+ summary
+ else
+ ""
+ end
+
{content, summary} =
Enum.reduce(
Pleroma.Config.get([:mrf_keyword, :replace]),
@@ -61,11 +79,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
end
@impl true
- def filter(%{"object" => %{"content" => nil}} = message) do
- {:ok, message}
- end
-
- @impl true
def filter(%{"type" => "Create", "object" => %{"content" => _content}} = message) do
with {:ok, message} <- check_reject(message),
{:ok, message} <- check_ftl_removal(message),
diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex
index 2bab9b03c..93808517b 100644
--- a/lib/pleroma/web/activity_pub/relay.ex
+++ b/lib/pleroma/web/activity_pub/relay.ex
@@ -41,7 +41,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do
def publish(%Activity{data: %{"type" => "Create"}} = activity) do
with %User{} = user <- get_actor(),
- %Object{} = object <- Object.normalize(activity.data["object"]["id"]) do
+ %Object{} = object <- Object.normalize(activity) do
ActivityPub.announce(user, object, nil, true, false)
else
e -> Logger.error("error: #{inspect(e)}")
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 546134f46..80317171d 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.
"""
@@ -83,14 +35,34 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> fix_content_map
|> fix_likes
|> fix_addressing
+ |> fix_summary
+ end
+
+ def fix_summary(%{"summary" => nil} = object) do
+ object
+ |> Map.put("summary", "")
+ end
+
+ def fix_summary(%{"summary" => _} = object) do
+ # summary is present, nothing to do
+ object
+ end
+
+ def fix_summary(object) do
+ object
+ |> Map.put("summary", "")
end
def fix_addressing_list(map, field) do
- if is_binary(map[field]) do
- map
- |> Map.put(field, [map[field]])
- else
- map
+ cond do
+ is_binary(map[field]) ->
+ Map.put(map, field, [map[field]])
+
+ is_nil(map[field]) ->
+ Map.put(map, field, [])
+
+ true ->
+ map
end
end
@@ -128,18 +100,47 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> fix_explicit_addressing(explicit_mentions)
end
+ # if as:Public is addressed, then make sure the followers collection is also addressed
+ # so that the activities will be delivered to local users.
+ def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
+ recipients = to ++ cc
+
+ if followers_collection not in recipients do
+ cond do
+ "https://www.w3.org/ns/activitystreams#Public" in cc ->
+ to = to ++ [followers_collection]
+ Map.put(object, "to", to)
+
+ "https://www.w3.org/ns/activitystreams#Public" in to ->
+ cc = cc ++ [followers_collection]
+ Map.put(object, "cc", cc)
+
+ true ->
+ object
+ end
+ else
+ object
+ end
+ end
+
+ def fix_implicit_addressing(object, _), do: object
+
def fix_addressing(object) do
+ %User{} = user = User.get_or_fetch_by_ap_id(object["actor"])
+ followers_collection = User.ap_followers(user)
+
object
|> fix_addressing_list("to")
|> fix_addressing_list("cc")
|> fix_addressing_list("bto")
|> fix_addressing_list("bcc")
|> fix_explicit_addressing
+ |> fix_implicit_addressing(followers_collection)
end
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
@@ -174,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
@@ -355,6 +355,40 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
+ # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
+ # with nil ID.
+ def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data) do
+ with context <- data["context"] || Utils.generate_context_id(),
+ content <- data["content"] || "",
+ %User{} = actor <- User.get_cached_by_ap_id(actor),
+
+ # Reduce the object list to find the reported user.
+ %User{} = account <-
+ Enum.reduce_while(objects, nil, fn ap_id, _ ->
+ with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
+ {:halt, user}
+ else
+ _ -> {:cont, nil}
+ end
+ end),
+
+ # Remove the reported user from the object list.
+ statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
+ params = %{
+ actor: actor,
+ context: context,
+ account: account,
+ statuses: statuses,
+ content: content,
+ additional: %{
+ "cc" => [account.ap_id]
+ }
+ }
+
+ ActivityPub.flag(params)
+ end
+ end
+
# disallow objects with bogus IDs
def handle_incoming(%{"id" => nil}), do: :error
def handle_incoming(%{"id" => ""}), do: :error
@@ -366,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)
@@ -404,27 +438,53 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
{:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
- if not User.locked?(followed) do
+ with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
+ {:user_blocked, false} <-
+ {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
+ {:user_locked, false} <- {:user_locked, User.locked?(followed)},
+ {:follow, {:ok, follower}} <- {:follow, User.follow(follower, followed)} do
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed,
object: data,
local: true
})
-
- User.follow(follower, followed)
+ else
+ {:user_blocked, true} ->
+ {:ok, _} = Utils.update_follow_state(activity, "reject")
+
+ ActivityPub.reject(%{
+ to: [follower.ap_id],
+ actor: followed,
+ object: data,
+ local: true
+ })
+
+ {:follow, {:error, _}} ->
+ {:ok, _} = Utils.update_follow_state(activity, "reject")
+
+ ActivityPub.reject(%{
+ to: [follower.ap_id],
+ actor: followed,
+ object: data,
+ local: true
+ })
+
+ {:user_locked, true} ->
+ :noop
end
{:ok, activity}
else
- _e -> :error
+ _e ->
+ :error
end
end
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),
{:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
@@ -450,7 +510,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),
{:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
@@ -474,9 +534,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),
{:ok, %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
@@ -487,9 +547,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),
{:ok, %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}
@@ -503,7 +563,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
data
)
when object_type in ["Person", "Application", "Service", "Organization"] do
- with %User{ap_id: ^actor_id} = actor <- User.get_by_ap_id(object["id"]) do
+ with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
banner = new_user_data[:info]["banner"]
@@ -542,10 +602,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),
{:ok, %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
@@ -561,9 +621,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"id" => id
} = data
) do
- with actor <- get_actor(data),
+ with actor <- Containment.get_actor(data),
{:ok, %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
@@ -631,9 +691,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"id" => id
} = data
) do
- with actor <- get_actor(data),
+ with actor <- Containment.get_actor(data),
{:ok, %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
@@ -643,9 +703,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
@@ -682,9 +739,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 =
@@ -745,7 +802,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 =
@@ -887,8 +944,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
defp strip_internal_tags(object), do: object
- defp user_upgrade_task(user) do
- old_follower_address = User.ap_followers(user)
+ def perform(:user_upgrade, user) do
+ # we pass a fake user so that the followers collection is stripped away
+ old_follower_address = User.ap_followers(%User{nickname: user.nickname})
q =
from(
@@ -931,28 +989,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
Repo.update_all(q, [])
end
- def upgrade_user_from_ap_id(ap_id, async \\ true) do
- with %User{local: false} = user <- User.get_by_ap_id(ap_id),
- {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do
- already_ap = User.ap_enabled?(user)
-
- {:ok, user} =
- User.upgrade_changeset(user, data)
- |> Repo.update()
-
- if !already_ap do
- # This could potentially take a long time, do it in the background
- if async do
- Task.start(fn ->
- user_upgrade_task(user)
- end)
- else
- user_upgrade_task(user)
- end
+ def upgrade_user_from_ap_id(ap_id) do
+ with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
+ {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
+ already_ap <- User.ap_enabled?(user),
+ {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
+ unless already_ap do
+ PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
end
{:ok, user}
else
+ %User{} = user -> {:ok, user}
e -> e
end
end
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 182f9cacb..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
@@ -99,7 +104,10 @@ defmodule Pleroma.Web.ActivityPub.Utils do
%{
"@context" => [
"https://www.w3.org/ns/activitystreams",
- "#{Web.base_url()}/schemas/litepub-0.1.jsonld"
+ "#{Web.base_url()}/schemas/litepub-0.1.jsonld",
+ %{
+ "@language" => "und"
+ }
]
}
end
@@ -175,18 +183,26 @@ defmodule Pleroma.Web.ActivityPub.Utils do
Adds an id and a published data if they aren't there,
also adds it to an included object
"""
- def lazy_put_activity_defaults(map) do
- %{data: %{"id" => context}, id: context_id} = create_context(map["context"])
-
+ def lazy_put_activity_defaults(map, fake \\ false) do
map =
- map
- |> Map.put_new_lazy("id", &generate_activity_id/0)
- |> Map.put_new_lazy("published", &make_date/0)
- |> Map.put_new("context", context)
- |> Map.put_new("context_id", context_id)
+ unless fake do
+ %{data: %{"id" => context}, id: context_id} = create_context(map["context"])
+
+ map
+ |> Map.put_new_lazy("id", &generate_activity_id/0)
+ |> Map.put_new_lazy("published", &make_date/0)
+ |> Map.put_new("context", context)
+ |> Map.put_new("context_id", context_id)
+ else
+ map
+ |> Map.put_new("id", "pleroma:fakeid")
+ |> Map.put_new_lazy("published", &make_date/0)
+ |> Map.put_new("context", "pleroma:fakecontext")
+ |> Map.put_new("context_id", -1)
+ end
if is_map(map["object"]) do
- object = lazy_put_object_defaults(map["object"], map)
+ object = lazy_put_object_defaults(map["object"], map, fake)
%{map | "object" => object}
else
map
@@ -196,7 +212,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do
@doc """
Adds an id and published date if they aren't there.
"""
- def lazy_put_object_defaults(map, activity \\ %{}) do
+ def lazy_put_object_defaults(map, activity \\ %{}, fake)
+
+ def lazy_put_object_defaults(map, activity, true = _fake) do
+ map
+ |> Map.put_new_lazy("published", &make_date/0)
+ |> Map.put_new("id", "pleroma:fake_object_id")
+ |> Map.put_new("context", activity["context"])
+ |> Map.put_new("fake", true)
+ |> Map.put_new("context_id", activity["context_id"])
+ end
+
+ def lazy_put_object_defaults(map, activity, _fake) do
map
|> Map.put_new_lazy("id", &generate_object_id/0)
|> Map.put_new_lazy("published", &make_date/0)
@@ -207,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.create(object_data) do
- :ok
+ with {:ok, object} <- Object.create(object_data) do
+ map =
+ map
+ |> Map.put("object", object.data["id"])
+
+ {:ok, map, object}
end
end
- def insert_full_object(_), do: :ok
+ def insert_full_object(map), do: {:ok, map, nil}
def update_object_in_activities(%{data: %{"id" => id}} = object) do
# TODO
@@ -354,7 +385,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
[state, actor, object]
)
- activity = Repo.get(Activity, activity.id)
+ activity = Activity.get_by_id(activity.id)
{:ok, activity}
rescue
e ->
@@ -404,13 +435,15 @@ defmodule Pleroma.Web.ActivityPub.Utils do
activity.data
),
where: activity.actor == ^follower_id,
+ # this is to use the index
where:
fragment(
- "? @> ?",
+ "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
- ^%{object: followed_id}
+ activity.data,
+ ^followed_id
),
- order_by: [desc: :id],
+ order_by: [fragment("? desc nulls last", activity.id)],
limit: 1
)
@@ -567,13 +600,15 @@ defmodule Pleroma.Web.ActivityPub.Utils do
activity.data
),
where: activity.actor == ^blocker_id,
+ # this is to use the index
where:
fragment(
- "? @> ?",
+ "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
- ^%{object: blocked_id}
+ activity.data,
+ ^blocked_id
),
- order_by: [desc: :id],
+ order_by: [fragment("? desc nulls last", activity.id)],
limit: 1
)
@@ -621,7 +656,13 @@ defmodule Pleroma.Web.ActivityPub.Utils do
#### Flag-related helpers
def make_flag_data(params, additional) do
- status_ap_ids = Enum.map(params.statuses || [], & &1.data["id"])
+ status_ap_ids =
+ Enum.map(params.statuses || [], fn
+ %Activity{} = act -> act.data["id"]
+ act when is_map(act) -> act["id"]
+ act when is_binary(act) -> act
+ end)
+
object = [params.account.ap_id] ++ status_ap_ids
%{
diff --git a/lib/pleroma/web/activity_pub/views/object_view.ex b/lib/pleroma/web/activity_pub/views/object_view.ex
index 84fa94e32..6028b773c 100644
--- a/lib/pleroma/web/activity_pub/views/object_view.ex
+++ b/lib/pleroma/web/activity_pub/views/object_view.ex
@@ -17,7 +17,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
def render("object.json", %{object: %Activity{data: %{"type" => "Create"}} = activity}) do
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
- object = Object.normalize(activity.data["object"])
+ object = Object.normalize(activity)
additional =
Transmogrifier.prepare_object(activity.data)
@@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
def render("object.json", %{object: %Activity{} = activity}) do
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
- object = Object.normalize(activity.data["object"])
+ object = Object.normalize(activity)
additional =
Transmogrifier.prepare_object(activity.data)
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index 3d00dcbf2..5926a3294 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -87,16 +87,10 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"publicKeyPem" => public_key
},
"endpoints" => endpoints,
- "icon" => %{
- "type" => "Image",
- "url" => User.avatar_url(user)
- },
- "image" => %{
- "type" => "Image",
- "url" => User.banner_url(user)
- },
"tag" => user.info.source_data["tag"] || []
}
+ |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
+ |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
|> Map.merge(Utils.make_json_ld_header())
end
@@ -294,4 +288,17 @@ defmodule Pleroma.Web.ActivityPub.UserView do
map
end
end
+
+ defp maybe_make_image(func, key, user) do
+ if image = func.(user, no_default: true) do
+ %{
+ key => %{
+ "type" => "Image",
+ "url" => image
+ }
+ }
+ else
+ %{}
+ end
+ end
end
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 6d9bf2895..711f233a6 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -3,27 +3,49 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.AdminAPIController do
- @users_page_size 50
-
use Pleroma.Web, :controller
alias Pleroma.User
+ alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.AdminAPI.AccountView
+ alias Pleroma.Web.AdminAPI.Search
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
require Logger
+ @users_page_size 50
+
action_fallback(:errors)
def user_delete(conn, %{"nickname" => nickname}) do
- User.get_by_nickname(nickname)
+ User.get_cached_by_nickname(nickname)
|> User.delete()
conn
|> json(nickname)
end
+ def user_follow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do
+ with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
+ %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
+ User.follow(follower, followed)
+ end
+
+ conn
+ |> json("ok")
+ end
+
+ def user_unfollow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do
+ with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
+ %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
+ User.unfollow(follower, followed)
+ end
+
+ conn
+ |> json("ok")
+ end
+
def user_create(
conn,
%{"nickname" => nickname, "email" => email, "password" => password}
@@ -44,8 +66,17 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|> json(user.nickname)
end
+ def user_show(conn, %{"nickname" => nickname}) do
+ with %User{} = user <- User.get_cached_by_nickname(nickname) do
+ conn
+ |> json(AccountView.render("show.json", %{user: user}))
+ else
+ _ -> {:error, :not_found}
+ end
+ end
+
def user_toggle_activation(conn, %{"nickname" => nickname}) do
- user = User.get_by_nickname(nickname)
+ user = User.get_cached_by_nickname(nickname)
{:ok, updated_user} = User.deactivate(user, !user.info.deactivated)
@@ -63,17 +94,17 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
do: json_response(conn, :no_content, "")
end
- def list_users(%{assigns: %{user: admin}} = conn, params) do
+ def list_users(conn, params) do
{page, page_size} = page_params(params)
+ filters = maybe_parse_filters(params["filters"])
- with {:ok, users, count} <-
- User.search_for_admin(%{
- query: params["query"],
- admin: admin,
- local: params["local_only"] == "true",
- page: page,
- page_size: page_size
- }),
+ search_params = %{
+ query: params["query"],
+ page: page,
+ page_size: page_size
+ }
+
+ with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
do:
conn
|> json(
@@ -85,9 +116,22 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
)
end
+ @filters ~w(local external active deactivated)
+
+ defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
+
+ @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
+ defp maybe_parse_filters(filters) do
+ filters
+ |> String.split(",")
+ |> Enum.filter(&Enum.member?(@filters, &1))
+ |> Enum.map(&String.to_atom(&1))
+ |> Enum.into(%{}, &{&1, true})
+ end
+
def right_add(conn, %{"permission_group" => permission_group, "nickname" => nickname})
when permission_group in ["moderator", "admin"] do
- user = User.get_by_nickname(nickname)
+ user = User.get_cached_by_nickname(nickname)
info =
%{}
@@ -112,7 +156,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
def right_get(conn, %{"nickname" => nickname}) do
- user = User.get_by_nickname(nickname)
+ user = User.get_cached_by_nickname(nickname)
conn
|> json(%{
@@ -134,7 +178,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|> put_status(403)
|> json(%{error: "You can't revoke your own admin status."})
else
- user = User.get_by_nickname(nickname)
+ user = User.get_cached_by_nickname(nickname)
info =
%{}
@@ -160,7 +204,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
def set_activation_status(conn, %{"nickname" => nickname, "status" => status}) do
with {:ok, status} <- Ecto.Type.cast(:boolean, status),
- %User{} = user <- User.get_by_nickname(nickname),
+ %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, _} <- User.deactivate(user, !status),
do: json_response(conn, :no_content, "")
end
@@ -192,31 +236,60 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
with true <-
Pleroma.Config.get([:instance, :invites_enabled]) &&
!Pleroma.Config.get([:instance, :registrations_open]),
- {:ok, invite_token} <- Pleroma.UserInviteToken.create_token(),
+ {: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
@doc "Get a account registeration invite token (base64 string)"
- def get_invite_token(conn, _params) do
- {:ok, token} = Pleroma.UserInviteToken.create_token()
+ def get_invite_token(conn, params) do
+ options = params["invite"] || %{}
+ {:ok, invite} = UserInviteToken.create_invite(options)
conn
- |> json(token.token)
+ |> json(invite.token)
+ end
+
+ @doc "Get list of created invites"
+ def invites(conn, _params) do
+ invites = UserInviteToken.list_invites()
+
+ conn
+ |> json(AccountView.render("invites.json", %{invites: invites}))
+ end
+
+ @doc "Revokes invite by token"
+ def revoke_invite(conn, %{"token" => token}) do
+ invite = UserInviteToken.find_by_token!(token)
+ {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true})
+
+ conn
+ |> json(AccountView.render("invite.json", %{invite: updated_invite}))
end
@doc "Get a password reset token (base64 string) for given nickname"
def get_password_reset(conn, %{"nickname" => nickname}) do
- (%User{local: true} = user) = User.get_by_nickname(nickname)
+ (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
{:ok, token} = Pleroma.PasswordResetToken.create_token(user)
conn
|> json(token.token)
end
+ def errors(conn, {:error, :not_found}) do
+ conn
+ |> put_status(404)
+ |> json("Not found")
+ end
+
def errors(conn, {:param_cast, _}) do
conn
|> put_status(400)
diff --git a/lib/pleroma/web/admin_api/search.ex b/lib/pleroma/web/admin_api/search.ex
new file mode 100644
index 000000000..9a8e41c2a
--- /dev/null
+++ b/lib/pleroma/web/admin_api/search.ex
@@ -0,0 +1,54 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.Search do
+ import Ecto.Query
+
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ @page_size 50
+
+ def user(%{query: term} = params) when is_nil(term) or term == "" do
+ query = maybe_filtered_query(params)
+
+ paginated_query =
+ maybe_filtered_query(params)
+ |> paginate(params[:page] || 1, params[:page_size] || @page_size)
+
+ count = query |> Repo.aggregate(:count, :id)
+
+ results = Repo.all(paginated_query)
+
+ {:ok, results, count}
+ end
+
+ def user(%{query: term} = params) when is_binary(term) do
+ search_query = from(u in maybe_filtered_query(params), where: ilike(u.nickname, ^"%#{term}%"))
+
+ count = search_query |> Repo.aggregate(:count, :id)
+
+ results =
+ search_query
+ |> paginate(params[:page] || 1, params[:page_size] || @page_size)
+ |> Repo.all()
+
+ {:ok, results, count}
+ end
+
+ defp maybe_filtered_query(params) do
+ from(u in User, order_by: u.nickname)
+ |> User.maybe_local_user_query(params[:local])
+ |> User.maybe_external_user_query(params[:external])
+ |> User.maybe_active_user_query(params[:active])
+ |> User.maybe_deactivated_user_query(params[:deactivated])
+ end
+
+ defp paginate(query, page, page_size) do
+ from(u in query,
+ limit: ^page_size,
+ offset: ^((page - 1) * page_size)
+ )
+ end
+end
diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex
index 4d6f921ef..28bb667d8 100644
--- a/lib/pleroma/web/admin_api/views/account_view.ex
+++ b/lib/pleroma/web/admin_api/views/account_view.ex
@@ -26,4 +26,22 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
"tags" => user.tags || []
}
end
+
+ def render("invite.json", %{invite: invite}) do
+ %{
+ "id" => invite.id,
+ "token" => invite.token,
+ "used" => invite.used,
+ "expires_at" => invite.expires_at,
+ "uses" => invite.uses,
+ "max_use" => invite.max_use,
+ "invite_type" => invite.invite_type
+ }
+ end
+
+ def render("invites.json", %{invites: invites}) do
+ %{
+ invites: render_many(invites, AccountView, "invite.json", as: :invite)
+ }
+ end
end
diff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex
index 82267c595..b02f595dc 100644
--- a/lib/pleroma/web/auth/authenticator.ex
+++ b/lib/pleroma/web/auth/authenticator.ex
@@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.Authenticator do
+ alias Pleroma.Registration
alias Pleroma.User
def implementation do
@@ -15,11 +16,30 @@ defmodule Pleroma.Web.Auth.Authenticator do
@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(), Registration.t()) ::
+ {:ok, User.t()} | {:error, any()}
+ def create_from_registration(plug, registration),
+ do: implementation().create_from_registration(plug, registration)
+
+ @callback get_registration(Plug.Conn.t()) ::
+ {:ok, Registration.t()} | {:error, any()}
+ 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
- implementation().auth_template() || Pleroma.Config.get(:auth_template, "show.html")
+ # Note: `config :pleroma, :auth_template, "..."` support is deprecated
+ implementation().auth_template() ||
+ Pleroma.Config.get([:auth, :auth_template], Pleroma.Config.get(:auth_template)) ||
+ "show.html"
+ end
+
+ @callback oauth_consumer_template() :: String.t() | nil
+ def oauth_consumer_template do
+ implementation().oauth_consumer_template() ||
+ Pleroma.Config.get([:auth, :oauth_consumer_template], "consumer.html")
end
end
diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex
new file mode 100644
index 000000000..363c99597
--- /dev/null
+++ b/lib/pleroma/web/auth/ldap_authenticator.ex
@@ -0,0 +1,144 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Auth.LDAPAuthenticator do
+ alias Pleroma.User
+
+ require Logger
+
+ @behaviour Pleroma.Web.Auth.Authenticator
+ @base Pleroma.Web.Auth.PleromaAuthenticator
+
+ @connection_timeout 10_000
+ @search_timeout 10_000
+
+ 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
+
+ def get_user(%Plug.Conn{} = conn) do
+ if Pleroma.Config.get([:ldap, :enabled]) do
+ {name, password} =
+ case conn.params do
+ %{"authorization" => %{"name" => name, "password" => password}} ->
+ {name, password}
+
+ %{"grant_type" => "password", "username" => name, "password" => password} ->
+ {name, password}
+ end
+
+ case ldap_user(name, password) do
+ %User{} = user ->
+ {:ok, user}
+
+ {:error, {:ldap_connection_error, _}} ->
+ # When LDAP is unavailable, try default authenticator
+ @base.get_user(conn)
+
+ error ->
+ error
+ end
+ else
+ # Fall back to default authenticator
+ @base.get_user(conn)
+ end
+ end
+
+ defp ldap_user(name, password) do
+ ldap = Pleroma.Config.get(:ldap, [])
+ host = Keyword.get(ldap, :host, "localhost")
+ port = Keyword.get(ldap, :port, 389)
+ ssl = Keyword.get(ldap, :ssl, false)
+ sslopts = Keyword.get(ldap, :sslopts, [])
+
+ options =
+ [{:port, port}, {:ssl, ssl}, {:timeout, @connection_timeout}] ++
+ if sslopts != [], do: [{:sslopts, sslopts}], else: []
+
+ case :eldap.open([to_charlist(host)], options) do
+ {:ok, connection} ->
+ try do
+ if Keyword.get(ldap, :tls, false) do
+ :application.ensure_all_started(:ssl)
+
+ case :eldap.start_tls(
+ connection,
+ Keyword.get(ldap, :tlsopts, []),
+ @connection_timeout
+ ) do
+ :ok ->
+ :ok
+
+ error ->
+ Logger.error("Could not start TLS: #{inspect(error)}")
+ end
+ end
+
+ bind_user(connection, ldap, name, password)
+ after
+ :eldap.close(connection)
+ end
+
+ {:error, error} ->
+ Logger.error("Could not open LDAP connection: #{inspect(error)}")
+ {:error, {:ldap_connection_error, error}}
+ end
+ end
+
+ defp bind_user(connection, ldap, name, password) do
+ uid = Keyword.get(ldap, :uid, "cn")
+ base = Keyword.get(ldap, :base)
+
+ case :eldap.simple_bind(connection, "#{uid}=#{name},#{base}", password) do
+ :ok ->
+ case User.get_by_nickname_or_email(name) do
+ %User{} = user ->
+ user
+
+ _ ->
+ register_user(connection, base, uid, name, password)
+ end
+
+ error ->
+ error
+ end
+ end
+
+ defp register_user(connection, base, uid, name, password) do
+ case :eldap.search(connection, [
+ {:base, to_charlist(base)},
+ {:filter, :eldap.equalityMatch(to_charlist(uid), to_charlist(name))},
+ {:scope, :eldap.wholeSubtree()},
+ {:attributes, ['mail', 'email']},
+ {:timeout, @search_timeout}
+ ]) do
+ {:ok, {:eldap_search_result, [{:eldap_entry, _, attributes}], _}} ->
+ with {_, [mail]} <- List.keyfind(attributes, 'mail', 0) do
+ params = %{
+ email: :erlang.list_to_binary(mail),
+ name: name,
+ nickname: name,
+ password: password,
+ password_confirmation: password
+ }
+
+ changeset = User.register_changeset(%User{}, params)
+
+ case User.register(changeset) do
+ {:ok, user} -> user
+ error -> error
+ end
+ else
+ _ ->
+ Logger.error("Could not find LDAP attribute mail: #{inspect(attributes)}")
+ {:error, :ldap_registration_missing_attributes}
+ end
+
+ error ->
+ error
+ end
+ end
+end
diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex
index 333446bef..d647f1e05 100644
--- a/lib/pleroma/web/auth/pleroma_authenticator.ex
+++ b/lib/pleroma/web/auth/pleroma_authenticator.ex
@@ -4,12 +4,21 @@
defmodule Pleroma.Web.Auth.PleromaAuthenticator do
alias Comeonin.Pbkdf2
+ alias Pleroma.Registration
+ alias Pleroma.Repo
alias Pleroma.User
@behaviour Pleroma.Web.Auth.Authenticator
def get_user(%Plug.Conn{} = conn) do
- %{"authorization" => %{"name" => name, "password" => password}} = conn.params
+ {name, password} =
+ case conn.params do
+ %{"authorization" => %{"name" => name, "password" => password}} ->
+ {name, password}
+
+ %{"grant_type" => "password", "username" => name, "password" => password} ->
+ {name, password}
+ end
with {_, %User{} = user} <- {:user, User.get_by_nickname_or_email(name)},
{_, true} <- {:checkpw, Pbkdf2.checkpw(password, user.password_hash)} do
@@ -20,9 +29,72 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
end
end
+ 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
+ {:ok, registration}
+ else
+ info = auth.info
+
+ %Registration{}
+ |> Registration.changeset(%{
+ provider: to_string(provider),
+ uid: to_string(uid),
+ info: %{
+ "nickname" => info.nickname,
+ "email" => info.email,
+ "name" => info.name,
+ "description" => info.description
+ }
+ })
+ |> Repo.insert()
+ end
+ end
+
+ def get_registration(%Plug.Conn{} = _conn), do: {:error, :missing_credentials}
+
+ 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()
+
+ with {:ok, new_user} <-
+ User.register_changeset(
+ %User{},
+ %{
+ email: email,
+ nickname: nickname,
+ name: name,
+ bio: bio,
+ password: random_password,
+ password_confirmation: random_password
+ },
+ external: true,
+ confirmed: true
+ )
+ |> Repo.insert(),
+ {:ok, _} <-
+ Registration.changeset(registration, %{user_id: new_user.id}) |> Repo.update() do
+ {:ok, new_user}
+ end
+ end
+
+ defp value(list), do: Enum.find(list, &(to_string(&1) != ""))
+
def handle_error(%Plug.Conn{} = _conn, error) do
error
end
def auth_template, do: nil
+
+ def oauth_consumer_template, do: nil
end
diff --git a/lib/pleroma/web/channels/user_socket.ex b/lib/pleroma/web/channels/user_socket.ex
index 3a700fa3b..8e2759e3b 100644
--- a/lib/pleroma/web/channels/user_socket.ex
+++ b/lib/pleroma/web/channels/user_socket.ex
@@ -24,7 +24,7 @@ defmodule Pleroma.Web.UserSocket do
def connect(%{"token" => token}, socket) do
with true <- Pleroma.Config.get([:chat, :enabled]),
{:ok, user_id} <- Phoenix.Token.verify(socket, "user socket", token, max_age: 84_600),
- %User{} = user <- Pleroma.Repo.get(User, user_id) do
+ %User{} = user <- Pleroma.User.get_cached_by_id(user_id) do
{:ok, assign(socket, :user_name, user.nickname)}
else
_e -> :error
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index de0759fb0..ecd183110 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -4,9 +4,9 @@
defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity
+ alias Pleroma.Bookmark
alias Pleroma.Formatter
alias Pleroma.Object
- alias Pleroma.Repo
alias Pleroma.ThreadMute
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
@@ -64,8 +64,9 @@ defmodule Pleroma.Web.CommonAPI do
end
def delete(activity_id, user) do
- with %Activity{data: %{"object" => %{"id" => object_id}}} <- Repo.get(Activity, activity_id),
- %Object{} = object <- Object.normalize(object_id),
+ with %Activity{data: %{"object" => _}} = activity <-
+ Activity.get_by_id_with_object(activity_id),
+ %Object{} = object <- Object.normalize(activity),
true <- User.superuser?(user) || user.ap_id == object.data["actor"],
{:ok, _} <- unpin(activity_id, user),
{:ok, delete} <- ActivityPub.delete(object) do
@@ -75,7 +76,7 @@ defmodule Pleroma.Web.CommonAPI do
def repeat(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
- object <- Object.normalize(activity.data["object"]["id"]),
+ object <- Object.normalize(activity),
nil <- Utils.get_existing_announce(user.ap_id, object) do
ActivityPub.announce(user, object)
else
@@ -86,7 +87,7 @@ defmodule Pleroma.Web.CommonAPI do
def unrepeat(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
- object <- Object.normalize(activity.data["object"]["id"]) do
+ object <- Object.normalize(activity) do
ActivityPub.unannounce(user, object)
else
_ ->
@@ -96,7 +97,7 @@ defmodule Pleroma.Web.CommonAPI do
def favorite(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
- object <- Object.normalize(activity.data["object"]["id"]),
+ object <- Object.normalize(activity),
nil <- Utils.get_existing_like(user.ap_id, object) do
ActivityPub.like(user, object)
else
@@ -107,7 +108,7 @@ defmodule Pleroma.Web.CommonAPI do
def unfavorite(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
- object <- Object.normalize(activity.data["object"]["id"]) do
+ object <- Object.normalize(activity) do
ActivityPub.unlike(user, object)
else
_ ->
@@ -125,7 +126,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
@@ -142,7 +146,8 @@ defmodule Pleroma.Web.CommonAPI do
make_content_html(
status,
attachments,
- data
+ data,
+ visibility
),
{to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility),
context <- make_context(in_reply_to),
@@ -166,18 +171,21 @@ defmodule Pleroma.Web.CommonAPI do
object,
"emoji",
(Formatter.get_emoji(status) ++ Formatter.get_emoji(data["spoiler_text"]))
- |> Enum.reduce(%{}, fn {name, file}, acc ->
+ |> Enum.reduce(%{}, fn {name, file, _}, acc ->
Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
end)
) do
res =
- ActivityPub.create(%{
- to: to,
- actor: user,
- context: context,
- object: object,
- additional: %{"cc" => cc, "directMessage" => visibility == "direct"}
- })
+ ActivityPub.create(
+ %{
+ to: to,
+ actor: user,
+ context: context,
+ object: object,
+ additional: %{"cc" => cc, "directMessage" => visibility == "direct"}
+ },
+ Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
+ )
res
end
@@ -210,8 +218,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"
}
@@ -273,9 +283,18 @@ defmodule Pleroma.Web.CommonAPI do
end
end
+ def bookmarked?(user, activity) do
+ with %Bookmark{} <- Bookmark.get(user.id, activity.id) do
+ true
+ else
+ _ ->
+ false
+ end
+ end
+
def report(user, data) do
with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
- {:account, %User{} = account} <- {:account, User.get_by_id(account_id)},
+ {:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)},
{:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
{:ok, statuses} <- get_report_statuses(account, data),
{:ok, activity} <-
@@ -284,14 +303,9 @@ defmodule Pleroma.Web.CommonAPI do
actor: user,
account: account,
statuses: statuses,
- content: content_html
+ content: content_html,
+ forward: data["forward"] || false
}) do
- Enum.each(User.all_superusers(), fn superuser ->
- superuser
- |> Pleroma.AdminEmail.report(user, account, statuses, content_html)
- |> Pleroma.Mailer.deliver_async()
- end)
-
{:ok, activity}
else
{:error, err} -> {:error, err}
@@ -299,4 +313,24 @@ defmodule Pleroma.Web.CommonAPI do
{:account, nil} -> {:error, "Account not found"}
end
end
+
+ def hide_reblogs(user, muted) do
+ ap_id = muted.ap_id
+
+ if ap_id not in user.info.muted_reblogs do
+ info_changeset = User.Info.add_reblog_mute(user.info, ap_id)
+ changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
+ User.update_and_set_cache(changeset)
+ end
+ end
+
+ def show_reblogs(user, muted) do
+ ap_id = muted.ap_id
+
+ if ap_id in user.info.muted_reblogs do
+ info_changeset = User.Info.remove_reblog_mute(user.info, ap_id)
+ changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
+ User.update_and_set_cache(changeset)
+ end
+ end
end
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index b7513ef28..1dfe50b40 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -12,25 +12,29 @@ defmodule Pleroma.Web.CommonAPI.Utils do
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Endpoint
alias Pleroma.Web.MediaProxy
+ require Logger
+
# This is a hack for twidere.
def get_by_id_or_ap_id(id) do
- activity = Repo.get(Activity, id) || Activity.get_create_by_object_ap_id(id)
+ activity =
+ Activity.get_by_id_with_object(id) || Activity.get_create_by_object_ap_id_with_object(id)
activity &&
if activity.data["type"] == "Create" do
activity
else
- Activity.get_create_by_object_ap_id(activity.data["object"])
+ Activity.get_create_by_object_ap_id_with_object(activity.data["object"])
end
end
def get_replied_to_activity(""), do: nil
def get_replied_to_activity(id) when not is_nil(id) do
- Repo.get(Activity, id)
+ Activity.get_by_id(id)
end
def get_replied_to_activity(_), do: nil
@@ -101,7 +105,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def make_content_html(
status,
attachments,
- data
+ data,
+ visibility
) do
no_attachment_links =
data
@@ -110,8 +115,15 @@ defmodule Pleroma.Web.CommonAPI.Utils do
content_type = get_content_type(data["content_type"])
+ options =
+ if visibility == "direct" && Config.get([:instance, :safe_dm_mentions]) do
+ [safe_mention: true]
+ else
+ []
+ end
+
status
- |> format_input(content_type)
+ |> format_input(content_type, options)
|> maybe_add_attachments(attachments, no_attachment_links)
|> maybe_add_nsfw_tag(data)
end
@@ -171,6 +183,18 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
@doc """
+ Formatting text as BBCode.
+ """
+ def format_input(text, "text/bbcode", options) do
+ text
+ |> String.replace(~r/\r/, "")
+ |> Formatter.html_escape("text/plain")
+ |> BBCode.to_html()
+ |> (fn {:ok, html} -> html end).()
+ |> Formatter.linkify(options)
+ end
+
+ @doc """
Formatting text to html.
"""
def format_input(text, "text/html", options) do
@@ -183,11 +207,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
@@ -197,7 +220,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
context,
content_html,
attachments,
- inReplyTo,
+ in_reply_to,
tags,
cw \\ nil,
cc \\ []
@@ -214,10 +237,11 @@ defmodule Pleroma.Web.CommonAPI.Utils do
"tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
}
- if inReplyTo do
+ if in_reply_to do
+ in_reply_to_object = Object.normalize(in_reply_to)
+
object
- |> Map.put("inReplyTo", inReplyTo.data["object"]["id"])
- |> Map.put("inReplyToStatusId", inReplyTo.id)
+ |> Map.put("inReplyTo", in_reply_to_object.data["id"])
else
object
end
@@ -231,15 +255,21 @@ defmodule Pleroma.Web.CommonAPI.Utils do
Strftime.strftime!(date, "%a %b %d %H:%M:%S %z %Y")
end
- def date_to_asctime(date) do
- with {:ok, date, _offset} <- date |> DateTime.from_iso8601() do
+ def date_to_asctime(date) when is_binary(date) do
+ with {:ok, date, _offset} <- DateTime.from_iso8601(date) do
format_asctime(date)
else
_e ->
+ Logger.warn("Date #{date} in wrong format, must be ISO 8601")
""
end
end
+ def date_to_asctime(date) do
+ Logger.warn("Date #{date} in wrong format, must be ISO 8601")
+ ""
+ end
+
def to_masto_date(%NaiveDateTime{} = date) do
date
|> NaiveDateTime.to_iso8601()
@@ -266,7 +296,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
def confirm_current_password(user, password) do
- with %User{local: true} = db_user <- Repo.get(User, user.id),
+ with %User{local: true} = db_user <- User.get_cached_by_id(user.id),
true <- Pbkdf2.checkpw(password, db_user.password_hash) do
{:ok, db_user}
else
@@ -276,7 +306,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def emoji_from_profile(%{info: _info} = user) do
(Formatter.get_emoji(user.bio) ++ Formatter.get_emoji(user.name))
- |> Enum.map(fn {shortcode, url} ->
+ |> Enum.map(fn {shortcode, url, _} ->
%{
"type" => "Emoji",
"icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{url}"},
@@ -294,10 +324,10 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def maybe_notify_mentioned_recipients(
recipients,
- %Activity{data: %{"to" => _to, "type" => type} = data} = _activity
+ %Activity{data: %{"to" => _to, "type" => type} = data} = activity
)
when type == "Create" do
- object = Object.normalize(data["object"])
+ object = Object.normalize(activity)
object_data =
cond do
@@ -318,6 +348,24 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def maybe_notify_mentioned_recipients(recipients, _), do: recipients
+ def maybe_notify_subscribers(
+ recipients,
+ %Activity{data: %{"actor" => actor, "type" => type}} = activity
+ )
+ when type == "Create" do
+ with %User{} = user <- User.get_cached_by_ap_id(actor) do
+ subscriber_ids =
+ user
+ |> User.subscribers()
+ |> Enum.filter(&Visibility.visible_for_user?(activity, &1))
+ |> Enum.map(& &1.ap_id)
+
+ recipients ++ subscriber_ids
+ end
+ end
+
+ def maybe_notify_subscribers(recipients, _), do: recipients
+
def maybe_extract_mentions(%{"tag" => tag}) do
tag
|> Enum.filter(fn x -> is_map(x) end)
@@ -344,4 +392,33 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
def get_report_statuses(_, _), do: {:ok, nil}
+
+ # DEPRECATED mostly, context objects are now created at insertion time.
+ def context_to_conversation_id(context) do
+ with %Object{id: id} <- Object.get_cached_by_ap_id(context) do
+ id
+ else
+ _e ->
+ changeset = Object.context_mapping(context)
+
+ case Repo.insert(changeset) do
+ {:ok, %{id: id}} ->
+ id
+
+ # This should be solved by an upsert, but it seems ecto
+ # has problems accessing the constraint inside the jsonb.
+ {:error, _} ->
+ Object.get_cached_by_ap_id(context).id
+ end
+ end
+ end
+
+ def conversation_id_to_context(id) do
+ with %Object{data: %{"id" => context}} <- Repo.get(Object, id) do
+ context
+ else
+ _e ->
+ {:error, "No such conversation"}
+ end
+ end
end
diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex
index 4d6192db0..181483664 100644
--- a/lib/pleroma/web/controller_helper.ex
+++ b/lib/pleroma/web/controller_helper.ex
@@ -5,6 +5,11 @@
defmodule Pleroma.Web.ControllerHelper do
use Pleroma.Web, :controller
+ # As in MastoAPI, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html
+ @falsy_param_values [false, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"]
+ def truthy_param?(blank_value) when blank_value in [nil, ""], do: nil
+ def truthy_param?(value), do: value not in @falsy_param_values
+
def oauth_scopes(params, default) do
# Note: `scopes` is used by Mastodon — supporting it but sticking to
# OAuth's standard `scope` wherever we control it
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index 697b1bc3a..7f939991d 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -25,7 +25,7 @@ defmodule Pleroma.Web.Endpoint do
at: "/",
from: :pleroma,
only:
- ~w(index.html static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc)
+ ~w(index.html robots.txt static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc)
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
)
@@ -51,11 +51,17 @@ defmodule Pleroma.Web.Endpoint do
plug(Plug.MethodOverride)
plug(Plug.Head)
+ secure_cookies = Pleroma.Config.get([__MODULE__, :secure_cookie_flag])
+
cookie_name =
- if Application.get_env(:pleroma, Pleroma.Web.Endpoint) |> Keyword.get(:secure_cookie_flag),
+ if secure_cookies,
do: "__Host-pleroma_key",
else: "pleroma_key"
+ 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.
# Set :encryption_salt if you would also like to encrypt it.
@@ -65,11 +71,30 @@ defmodule Pleroma.Web.Endpoint do
key: cookie_name,
signing_salt: {Pleroma.Config, :get, [[__MODULE__, :signing_salt], "CqaoopA2"]},
http_only: true,
- secure:
- Application.get_env(:pleroma, Pleroma.Web.Endpoint) |> Keyword.get(:secure_cookie_flag),
- extra: "SameSite=Strict"
+ secure: secure_cookies,
+ extra: extra
)
+ # Note: the plug and its configuration is compile-time this can't be upstreamed yet
+ if proxies = Pleroma.Config.get([__MODULE__, :reverse_proxies]) do
+ plug(RemoteIp, proxies: proxies)
+ end
+
+ defmodule Instrumenter do
+ use Prometheus.PhoenixInstrumenter
+ end
+
+ defmodule PipelineInstrumenter do
+ use Prometheus.PlugPipelineInstrumenter
+ end
+
+ defmodule MetricsExporter do
+ use Prometheus.PlugExporter
+ end
+
+ plug(PipelineInstrumenter)
+ plug(MetricsExporter)
+
plug(Pleroma.Web.Router)
@doc """
diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex
index 5e690ddb8..29e178ba9 100644
--- a/lib/pleroma/web/federator/federator.ex
+++ b/lib/pleroma/web/federator/federator.ex
@@ -4,7 +4,7 @@
defmodule Pleroma.Web.Federator do
alias Pleroma.Activity
- alias Pleroma.Jobs
+ alias Pleroma.Object.Containment
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
@@ -31,39 +31,39 @@ defmodule Pleroma.Web.Federator do
# Client API
def incoming_doc(doc) do
- Jobs.enqueue(:federator_incoming, __MODULE__, [:incoming_doc, doc])
+ PleromaJobQueue.enqueue(:federator_incoming, __MODULE__, [:incoming_doc, doc])
end
def incoming_ap_doc(params) do
- Jobs.enqueue(:federator_incoming, __MODULE__, [:incoming_ap_doc, params])
+ PleromaJobQueue.enqueue(:federator_incoming, __MODULE__, [:incoming_ap_doc, params])
end
def publish(activity, priority \\ 1) do
- Jobs.enqueue(:federator_outgoing, __MODULE__, [:publish, activity], priority)
+ PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish, activity], priority)
end
def publish_single_ap(params) do
- Jobs.enqueue(:federator_outgoing, __MODULE__, [:publish_single_ap, params])
+ PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_ap, params])
end
def publish_single_websub(websub) do
- Jobs.enqueue(:federator_outgoing, __MODULE__, [:publish_single_websub, websub])
+ PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_websub, websub])
end
def verify_websub(websub) do
- Jobs.enqueue(:federator_outgoing, __MODULE__, [:verify_websub, websub])
+ PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:verify_websub, websub])
end
def request_subscription(sub) do
- Jobs.enqueue(:federator_outgoing, __MODULE__, [:request_subscription, sub])
+ PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:request_subscription, sub])
end
def refresh_subscriptions do
- Jobs.enqueue(:federator_outgoing, __MODULE__, [:refresh_subscriptions])
+ PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:refresh_subscriptions])
end
def publish_single_salmon(params) do
- Jobs.enqueue(:federator_outgoing, __MODULE__, [:publish_single_salmon, params])
+ PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_salmon, params])
end
# Job Worker Callbacks
@@ -137,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
@@ -186,7 +186,7 @@ defmodule Pleroma.Web.Federator do
end
def ap_enabled_actor(id) do
- user = User.get_by_ap_id(id)
+ user = User.get_cached_by_ap_id(id)
if User.ap_enabled?(user) do
{:ok, user}
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex
index 54cb6c97a..3a3ec7c2a 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex
@@ -2,61 +2,82 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
import Ecto.Query
import Ecto.Changeset
- alias Pleroma.Repo
+ alias Pleroma.Activity
+ alias Pleroma.Notification
+ alias Pleroma.Pagination
+ alias Pleroma.ScheduledActivity
alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
- @default_limit 20
+ 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
|> User.get_followers_query()
- |> paginate(params)
- |> Repo.all()
+ |> Pagination.fetch_paginated(params)
end
def get_friends(user, params \\ %{}) do
user
|> User.get_friends_query()
- |> paginate(params)
- |> Repo.all()
+ |> Pagination.fetch_paginated(params)
end
- def paginate(query, params \\ %{}) do
+ def get_notifications(user, params \\ %{}) do
options = cast_params(params)
- query
- |> restrict(:max_id, options)
- |> restrict(:since_id, options)
- |> restrict(:limit, options)
- |> order_by([u], fragment("? desc nulls last", u.id))
+ user
+ |> Notification.for_user_query()
+ |> restrict(:exclude_types, options)
+ |> Pagination.fetch_paginated(params)
end
- def cast_params(params) do
+ def get_scheduled_activities(user, params \\ %{}) do
+ user
+ |> ScheduledActivity.for_user_query()
+ |> Pagination.fetch_paginated(params)
+ end
+
+ defp cast_params(params) do
param_types = %{
- max_id: :string,
- since_id: :string,
- limit: :integer
+ exclude_types: {:array, :string},
+ reblogs: :boolean
}
changeset = cast({%{}, param_types}, params, Map.keys(param_types))
changeset.changes
end
- defp restrict(query, :max_id, %{max_id: max_id}) do
- query
- |> where([q], q.id < ^max_id)
- end
-
- defp restrict(query, :since_id, %{since_id: since_id}) do
- query
- |> where([q], q.id > ^since_id)
- end
-
- defp restrict(query, :limit, options) do
- limit = Map.get(options, :limit, @default_limit)
+ defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do
+ ap_types =
+ mastodon_types
+ |> Enum.map(&Activity.from_mastodon_notification_type/1)
+ |> Enum.filter(& &1)
query
- |> limit(^limit)
+ |> where([q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
end
defp restrict(query, _, _), do: query
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index cbf2a7ce1..727e1c310 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -4,13 +4,17 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller
-
+ alias Ecto.Changeset
alias Pleroma.Activity
+ alias Pleroma.Bookmark
alias Pleroma.Config
alias Pleroma.Filter
alias Pleroma.Notification
alias Pleroma.Object
+ alias Pleroma.Object.Fetcher
+ alias Pleroma.Pagination
alias Pleroma.Repo
+ alias Pleroma.ScheduledActivity
alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.Web
@@ -18,18 +22,21 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
+ alias Pleroma.Web.MastodonAPI.AppView
alias Pleroma.Web.MastodonAPI.FilterView
alias Pleroma.Web.MastodonAPI.ListView
alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.MastodonView
+ alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.ReportView
+ alias Pleroma.Web.MastodonAPI.ScheduledActivityView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
- import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
+ alias Pleroma.Web.ControllerHelper
import Ecto.Query
require Logger
@@ -40,7 +47,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
action_fallback(:errors)
def create_app(conn, params) do
- scopes = oauth_scopes(params, ["read"])
+ scopes = ControllerHelper.oauth_scopes(params, ["read"])
app_attrs =
params
@@ -50,16 +57,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
with cs <- App.register_changeset(%App{}, app_attrs),
false <- cs.changes[:client_name] == @local_mastodon_name,
{:ok, app} <- Repo.insert(cs) do
- res = %{
- id: app.id |> to_string,
- name: app.client_name,
- client_id: app.client_id,
- client_secret: app.client_secret,
- redirect_uri: app.redirect_uris,
- website: app.website
- }
-
- json(conn, res)
+ conn
+ |> put_view(AppView)
+ |> render("show.json", %{app: app})
end
end
@@ -97,8 +97,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end)
info_params =
- %{}
- |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
+ [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
+ |> Enum.reduce(%{}, fn key, acc ->
+ add_if_present(acc, params, to_string(key), key, fn value ->
+ {:ok, ControllerHelper.truthy_param?(value)}
+ end)
+ end)
+ |> add_if_present(params, "default_scope", :default_scope)
|> add_if_present(params, "header", :banner, fn value ->
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: :banner) do
@@ -108,7 +113,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end)
- info_cng = User.Info.mastodon_profile_update(user.info, info_params)
+ info_cng = User.Info.profile_update(user.info, info_params)
with changeset <- User.update_changeset(user, user_params),
changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
@@ -131,6 +136,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
json(conn, account)
end
+ def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
+ with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
+ conn
+ |> put_view(AppView)
+ |> render("short.json", %{app: app})
+ end
+ end
+
def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
@@ -160,6 +173,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
},
stats: Stats.get_stats(),
thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
+ languages: ["en"],
+ registrations: Pleroma.Config.get([:instance, :registrations_open]),
+ # Extra (not present in Mastodon):
max_toot_chars: Keyword.get(instance, :limit)
}
@@ -172,14 +188,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
defp mastodonized_emoji do
Pleroma.Emoji.get_all()
- |> Enum.map(fn {shortcode, relative_url} ->
+ |> Enum.map(fn {shortcode, relative_url, tags} ->
url = to_string(URI.merge(Web.base_url(), relative_url))
%{
"shortcode" => shortcode,
"static_url" => url,
"visible_in_picker" => true,
- "url" => url
+ "url" => url,
+ "tags" => tags
}
end)
end
@@ -192,15 +209,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
@@ -209,13 +240,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
@@ -223,12 +254,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
@@ -254,6 +285,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> ActivityPub.contain_timeline(user)
|> Enum.reverse()
+ user = Repo.preload(user, bookmarks: :activity)
+
conn
|> add_link_headers(:home_timeline, activities)
|> put_view(StatusView)
@@ -272,6 +305,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> ActivityPub.fetch_public_activities()
|> Enum.reverse()
+ user = Repo.preload(user, bookmarks: :activity)
+
conn
|> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
|> put_view(StatusView)
@@ -279,7 +314,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
- with %User{} = user <- Repo.get(User, params["id"]) do
+ with %User{} = user <- User.get_cached_by_id(params["id"]),
+ reading_user <- Repo.preload(reading_user, :bookmarks) do
activities = ActivityPub.fetch_user_activities(user, reading_user, params)
conn
@@ -304,7 +340,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
activities =
[user.ap_id]
|> ActivityPub.fetch_activities_query(params)
- |> Repo.all()
+ |> Pagination.fetch_paginated(params)
+
+ user = Repo.preload(user, bookmarks: :activity)
conn
|> add_link_headers(:dm_timeline, activities)
@@ -313,8 +351,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Repo.get(Activity, id),
+ with %Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
+ user = Repo.preload(user, bookmarks: :activity)
+
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user})
@@ -322,7 +362,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Repo.get(Activity, id),
+ with %Activity{} = activity <- Activity.get_by_id(id),
activities <-
ActivityPub.fetch_activities_for_context(activity.data["context"], %{
"blocking_user" => user,
@@ -358,6 +398,55 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
+ def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
+ with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
+ conn
+ |> add_link_headers(:scheduled_statuses, scheduled_activities)
+ |> put_view(ScheduledActivityView)
+ |> render("index.json", %{scheduled_activities: scheduled_activities})
+ end
+ end
+
+ def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
+ with %ScheduledActivity{} = scheduled_activity <-
+ ScheduledActivity.get(user, scheduled_activity_id) do
+ conn
+ |> put_view(ScheduledActivityView)
+ |> render("show.json", %{scheduled_activity: scheduled_activity})
+ else
+ _ -> {:error, :not_found}
+ end
+ end
+
+ def update_scheduled_status(
+ %{assigns: %{user: user}} = conn,
+ %{"id" => scheduled_activity_id} = params
+ ) do
+ with %ScheduledActivity{} = scheduled_activity <-
+ ScheduledActivity.get(user, scheduled_activity_id),
+ {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
+ conn
+ |> put_view(ScheduledActivityView)
+ |> render("show.json", %{scheduled_activity: scheduled_activity})
+ else
+ nil -> {:error, :not_found}
+ error -> error
+ end
+ end
+
+ def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
+ with %ScheduledActivity{} = scheduled_activity <-
+ ScheduledActivity.get(user, scheduled_activity_id),
+ {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
+ conn
+ |> put_view(ScheduledActivityView)
+ |> render("show.json", %{scheduled_activity: scheduled_activity})
+ else
+ nil -> {:error, :not_found}
+ error -> error
+ end
+ end
+
def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
when length(media_ids) > 0 do
params =
@@ -378,12 +467,27 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
_ -> Ecto.UUID.generate()
end
- {:ok, activity} =
- Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
+ scheduled_at = params["scheduled_at"]
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+ if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
+ with {:ok, scheduled_activity} <-
+ ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
+ conn
+ |> put_view(ScheduledActivityView)
+ |> render("show.json", %{scheduled_activity: scheduled_activity})
+ end
+ else
+ params = Map.drop(params, ["scheduled_at"])
+
+ {:ok, activity} =
+ Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
+ CommonAPI.post(user, params)
+ end)
+
+ conn
+ |> put_view(StatusView)
+ |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+ end
end
def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
@@ -398,7 +502,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
+ with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
+ %Activity{} = announce <- Activity.normalize(announce.data) do
+ user = Repo.preload(user, bookmarks: :activity)
+
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: announce, for: user, as: :activity})
@@ -407,7 +514,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
- %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
+ %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
+ user = Repo.preload(user, bookmarks: :activity)
+
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
@@ -454,10 +563,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Repo.get(Activity, id),
- %User{} = user <- User.get_by_nickname(user.nickname),
+ with %Activity{} = activity <- Activity.get_by_id_with_object(id),
+ %User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
- {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
+ {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
+ user = Repo.preload(user, bookmarks: :activity)
+
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
@@ -465,10 +576,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Repo.get(Activity, id),
- %User{} = user <- User.get_by_nickname(user.nickname),
+ with %Activity{} = activity <- Activity.get_by_id_with_object(id),
+ %User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
- {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
+ {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
+ user = Repo.preload(user, bookmarks: :activity)
+
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
@@ -501,21 +614,19 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def notifications(%{assigns: %{user: user}} = conn, params) do
- notifications = Notification.for_user(user, params)
-
- result =
- notifications
- |> Enum.map(fn x -> render_notification(user, x) end)
- |> Enum.filter(& &1)
+ notifications = MastodonAPI.get_notifications(user, params)
conn
|> add_link_headers(:notifications, notifications)
- |> json(result)
+ |> put_view(NotificationView)
+ |> render("index.json", %{notifications: notifications, for: user})
end
def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, notification} <- Notification.get(user, id) do
- json(conn, render_notification(user, notification))
+ conn
+ |> put_view(NotificationView)
+ |> render("show.json", %{notification: notification, for: user})
else
{:error, reason} ->
conn
@@ -540,6 +651,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)
@@ -589,7 +705,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def favourited_by(conn, %{"id" => id}) do
- with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, 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)
@@ -602,7 +719,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def reblogged_by(conn, %{"id" => id}) do
- with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, 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)
@@ -653,7 +771,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
- with %User{} = user <- Repo.get(User, id),
+ with %User{} = user <- User.get_cached_by_id(id),
followers <- MastodonAPI.get_followers(user, params) do
followers =
cond do
@@ -670,7 +788,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
- with %User{} = user <- Repo.get(User, id),
+ with %User{} = user <- User.get_cached_by_id(id),
followers <- MastodonAPI.get_friends(user, params) do
followers =
cond do
@@ -695,7 +813,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
- with %User{} = follower <- Repo.get(User, id),
+ with %User{} = follower <- User.get_cached_by_id(id),
{:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
conn
|> put_view(AccountView)
@@ -709,7 +827,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
- with %User{} = follower <- Repo.get(User, id),
+ with %User{} = follower <- User.get_cached_by_id(id),
{:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
conn
|> put_view(AccountView)
@@ -723,12 +841,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
- with %User{} = followed <- Repo.get(User, id),
- {: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
+ {:followed, _} ->
+ {:error, :not_found}
+
{:error, message} ->
conn
|> put_resp_content_type("application/json")
@@ -737,12 +859,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
- with %User{} = followed <- Repo.get_by(User, 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")
@@ -751,16 +877,23 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
- with %User{} = followed <- Repo.get(User, 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
def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
- with %User{} = muted <- Repo.get(User, id),
+ with %User{} = muted <- User.get_cached_by_id(id),
{:ok, muter} <- User.mute(muter, muted) do
conn
|> put_view(AccountView)
@@ -774,7 +907,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
- with %User{} = muted <- Repo.get(User, id),
+ with %User{} = muted <- User.get_cached_by_id(id),
{:ok, muter} <- User.unmute(muter, muted) do
conn
|> put_view(AccountView)
@@ -795,7 +928,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
- with %User{} = blocked <- Repo.get(User, id),
+ with %User{} = blocked <- User.get_cached_by_id(id),
{:ok, blocker} <- User.block(blocker, blocked),
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
conn
@@ -810,7 +943,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
- with %User{} = blocked <- Repo.get(User, id),
+ with %User{} = blocked <- User.get_cached_by_id(id),
{:ok, blocker} <- User.unblock(blocker, blocked),
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
conn
@@ -845,10 +978,38 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
json(conn, %{})
end
+ def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with %User{} = subscription_target <- User.get_cached_by_id(id),
+ {:ok, subscription_target} = User.subscribe(user, subscription_target) do
+ conn
+ |> put_view(AccountView)
+ |> render("relationship.json", %{user: user, target: subscription_target})
+ else
+ {:error, message} ->
+ conn
+ |> put_resp_content_type("application/json")
+ |> send_resp(403, Jason.encode!(%{"error" => message}))
+ end
+ end
+
+ def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with %User{} = subscription_target <- User.get_cached_by_id(id),
+ {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
+ conn
+ |> put_view(AccountView)
+ |> render("relationship.json", %{user: user, target: subscription_target})
+ else
+ {:error, message} ->
+ conn
+ |> put_resp_content_type("application/json")
+ |> send_resp(403, Jason.encode!(%{"error" => message}))
+ end
+ end
+
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]
@@ -859,13 +1020,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,
@@ -931,29 +1092,75 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def favourites(%{assigns: %{user: user}} = conn, params) do
- activities =
+ params =
params
|> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", user)
- |> ActivityPub.fetch_public_activities()
+
+ activities =
+ ActivityPub.fetch_activities([], params)
|> Enum.reverse()
+ user = Repo.preload(user, bookmarks: :activity)
+
conn
|> add_link_headers(:favourites, activities)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
- def bookmarks(%{assigns: %{user: user}} = conn, _) do
- user = Repo.get(User, user.id)
+ def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
+ with %User{} = user <- User.get_by_id(id),
+ false <- user.info.hide_favorites do
+ params =
+ params
+ |> Map.put("type", "Create")
+ |> Map.put("favorited_by", user.ap_id)
+ |> Map.put("blocking_user", for_user)
+
+ recipients =
+ if for_user do
+ ["https://www.w3.org/ns/activitystreams#Public"] ++
+ [for_user.ap_id | for_user.following]
+ else
+ ["https://www.w3.org/ns/activitystreams#Public"]
+ end
+
+ activities =
+ recipients
+ |> ActivityPub.fetch_activities(params)
+ |> Enum.reverse()
+
+ conn
+ |> add_link_headers(:favourites, activities)
+ |> put_view(StatusView)
+ |> render("index.json", %{activities: activities, for: for_user, as: :activity})
+ else
+ nil ->
+ {:error, :not_found}
+
+ true ->
+ conn
+ |> put_status(403)
+ |> json(%{error: "Can't get favorites"})
+ end
+ end
+
+ def bookmarks(%{assigns: %{user: user}} = conn, params) do
+ user = User.get_cached_by_id(user.id)
+ user = Repo.preload(user, bookmarks: :activity)
+
+ bookmarks =
+ Bookmark.for_user_query(user.id)
+ |> Pagination.fetch_paginated(params)
activities =
- user.bookmarks
- |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
- |> Enum.reverse()
+ bookmarks
+ |> Enum.map(fn b -> b.activity end)
conn
+ |> add_link_headers(:bookmarks, bookmarks)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
@@ -1003,7 +1210,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
accounts
|> Enum.each(fn account_id ->
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
- %User{} = followed <- Repo.get(User, account_id) do
+ %User{} = followed <- User.get_cached_by_id(account_id) do
Pleroma.List.follow(list, followed)
end
end)
@@ -1015,7 +1222,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
accounts
|> Enum.each(fn account_id ->
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
- %User{} = followed <- Repo.get(Pleroma.User, account_id) do
+ %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
Pleroma.List.unfollow(list, followed)
end
end)
@@ -1059,6 +1266,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> ActivityPub.fetch_activities_bounded(following, params)
|> Enum.reverse()
+ user = Repo.preload(user, bookmarks: :activity)
+
conn
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
@@ -1071,9 +1280,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def index(%{assigns: %{user: user}} = conn, _params) do
- token =
- conn
- |> get_session(:oauth_token)
+ token = get_session(conn, :oauth_token)
if user && token do
mastodon_emoji = mastodonized_emoji()
@@ -1101,7 +1308,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
auto_play_gif: false,
display_sensitive_media: false,
reduce_motion: false,
- max_toot_chars: limit
+ max_toot_chars: limit,
+ mascot: "/images/pleroma-fox-tan-smol.png"
},
rights: %{
delete_others_notice: present?(user.info.is_moderator),
@@ -1173,6 +1381,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> render("index.html", %{initial_state: initial_state, flavour: flavour})
else
conn
+ |> put_session(:return_to, conn.request_path)
|> redirect(to: "/web/login")
end
end
@@ -1229,16 +1438,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
"glitch"
end
- def login(conn, %{"code" => code}) do
+ def login(%{assigns: %{user: %User{}}} = conn, _params) do
+ redirect(conn, to: local_mastodon_root_path(conn))
+ end
+
+ @doc "Local Mastodon FE login init action"
+ def login(conn, %{"code" => auth_token}) do
with {:ok, app} <- get_or_make_app(),
- %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
+ %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
{:ok, token} <- Token.exchange_token(app, auth) do
conn
|> put_session(:oauth_token, token.token)
- |> redirect(to: "/web/getting-started")
+ |> redirect(to: local_mastodon_root_path(conn))
end
end
+ @doc "Local Mastodon FE callback action"
def login(conn, _) do
with {:ok, app} <- get_or_make_app() do
path =
@@ -1251,8 +1466,18 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
scope: Enum.join(app.scopes, " ")
)
- conn
- |> redirect(to: path)
+ redirect(conn, to: path)
+ end
+ end
+
+ defp local_mastodon_root_path(conn) do
+ case get_session(conn, :return_to) do
+ nil ->
+ mastodon_api_path(conn, :index, ["getting-started"])
+
+ return_to ->
+ delete_session(conn, :return_to)
+ return_to
end
end
@@ -1292,7 +1517,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
Logger.debug("Unimplemented, returning unmodified relationship")
- with %User{} = target <- Repo.get(User, id) do
+ with %User{} = target <- User.get_cached_by_id(id) do
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: user, target: target})
@@ -1309,45 +1534,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
json(conn, %{})
end
- def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
- actor = User.get_cached_by_ap_id(activity.data["actor"])
- parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
- mastodon_type = Activity.mastodon_notification_type(activity)
-
- response = %{
- id: to_string(id),
- type: mastodon_type,
- created_at: CommonAPI.Utils.to_masto_date(created_at),
- account: AccountView.render("account.json", %{user: actor, for: user})
- }
-
- case mastodon_type do
- "mention" ->
- response
- |> Map.merge(%{
- status: StatusView.render("status.json", %{activity: activity, for: user})
- })
-
- "favourite" ->
- response
- |> Map.merge(%{
- status: StatusView.render("status.json", %{activity: parent_activity, for: user})
- })
-
- "reblog" ->
- response
- |> Map.merge(%{
- status: StatusView.render("status.json", %{activity: parent_activity, for: user})
- })
-
- "follow" ->
- response
-
- _ ->
- nil
- end
- end
-
def get_filters(%{assigns: %{user: user}} = conn, _) do
filters = Filter.get_filters(user)
res = FilterView.render("filters.json", filters: filters)
@@ -1409,6 +1595,23 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
# fallback action
#
+ def errors(conn, {:error, %Changeset{} = changeset}) do
+ error_message =
+ changeset
+ |> Changeset.traverse_errors(fn {message, _opt} -> message end)
+ |> Enum.map_join(", ", fn {_k, v} -> v end)
+
+ conn
+ |> put_status(422)
+ |> json(%{error: error_message})
+ end
+
+ def errors(conn, {:error, :not_found}) do
+ conn
+ |> put_status(404)
+ |> json(%{error: "Record not found"})
+ end
+
def errors(conn, _) do
conn
|> put_status(500)
@@ -1473,7 +1676,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
- with %Activity{} = activity <- Repo.get(Activity, status_id),
+ with %Activity{} = activity <- Activity.get_by_id(status_id),
true <- Visibility.visible_for_user?(activity, user) do
data =
StatusView.render(
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index c32f27be2..779b9a382 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -53,9 +53,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
blocking: User.blocks?(user, target),
muting: User.mutes?(user, target),
muting_notifications: false,
+ subscribing: User.subscribed_to?(user, target),
requested: requested,
domain_blocking: false,
- showing_reblogs: false,
+ showing_reblogs: User.showing_reblogs?(user, target),
endorsed: false
}
end
@@ -67,7 +68,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp do_render("account.json", %{user: user} = opts) do
image = User.avatar_url(user) |> MediaProxy.url()
header = User.banner_url(user) |> MediaProxy.url()
- user_info = User.user_info(user)
+ user_info = User.get_cached_user_info(user)
bot = (user.info.source_data["type"] || "Person") in ["Application", "Service"]
emojis =
@@ -112,19 +113,23 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
bot: bot,
source: %{
note: "",
- privacy: user_info.default_scope,
- sensitive: false
+ sensitive: false,
+ pleroma: %{}
},
# Pleroma extension
pleroma: %{
confirmation_pending: user_info.confirmation_pending,
tags: user.tags,
- is_moderator: user.info.is_moderator,
- is_admin: user.info.is_admin,
+ hide_followers: user.info.hide_followers,
+ hide_follows: user.info.hide_follows,
+ hide_favorites: user.info.hide_favorites,
relationship: relationship
}
}
+ |> maybe_put_role(user, opts[:for])
+ |> maybe_put_settings(user, opts[:for], user_info)
+ |> maybe_put_notification_settings(user, opts[:for])
end
defp username_from_nickname(string) when is_binary(string) do
@@ -132,4 +137,38 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
end
defp username_from_nickname(_), do: nil
+
+ defp maybe_put_settings(
+ data,
+ %User{id: user_id} = user,
+ %User{id: user_id},
+ user_info
+ ) do
+ data
+ |> Kernel.put_in([:source, :privacy], user_info.default_scope)
+ |> Kernel.put_in([:source, :pleroma, :show_role], user.info.show_role)
+ |> Kernel.put_in([:source, :pleroma, :no_rich_text], user.info.no_rich_text)
+ end
+
+ defp maybe_put_settings(data, _, _, _), do: data
+
+ defp maybe_put_role(data, %User{info: %{show_role: true}} = user, _) do
+ data
+ |> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin)
+ |> Kernel.put_in([:pleroma, :is_moderator], user.info.is_moderator)
+ end
+
+ defp maybe_put_role(data, %User{id: user_id} = user, %User{id: user_id}) do
+ data
+ |> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin)
+ |> Kernel.put_in([:pleroma, :is_moderator], user.info.is_moderator)
+ end
+
+ defp maybe_put_role(data, _, _), do: data
+
+ defp maybe_put_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do
+ Kernel.put_in(data, [:pleroma, :notification_settings], user.info.notification_settings)
+ end
+
+ defp maybe_put_notification_settings(data, _, _), do: data
end
diff --git a/lib/pleroma/web/mastodon_api/views/app_view.ex b/lib/pleroma/web/mastodon_api/views/app_view.ex
new file mode 100644
index 000000000..f52b693a6
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/views/app_view.ex
@@ -0,0 +1,41 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.AppView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.Web.OAuth.App
+
+ @vapid_key :web_push_encryption
+ |> Application.get_env(:vapid_details, [])
+ |> Keyword.get(:public_key)
+
+ def render("show.json", %{app: %App{} = app}) do
+ %{
+ id: app.id |> to_string,
+ name: app.client_name,
+ client_id: app.client_id,
+ client_secret: app.client_secret,
+ redirect_uri: app.redirect_uris,
+ website: app.website
+ }
+ |> with_vapid_key()
+ end
+
+ def render("short.json", %{app: %App{website: webiste, client_name: name}}) do
+ %{
+ name: name,
+ website: webiste
+ }
+ |> with_vapid_key()
+ end
+
+ defp with_vapid_key(data) do
+ if @vapid_key do
+ Map.put(data, "vapid_key", @vapid_key)
+ else
+ data
+ end
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex
new file mode 100644
index 000000000..27e9cab06
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex
@@ -0,0 +1,64 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.NotificationView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.Activity
+ alias Pleroma.Notification
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.MastodonAPI.AccountView
+ alias Pleroma.Web.MastodonAPI.NotificationView
+ alias Pleroma.Web.MastodonAPI.StatusView
+
+ def render("index.json", %{notifications: notifications, for: user}) do
+ render_many(notifications, NotificationView, "show.json", %{for: user})
+ end
+
+ def render("show.json", %{
+ notification: %Notification{activity: activity} = notification,
+ for: user
+ }) do
+ actor = User.get_cached_by_ap_id(activity.data["actor"])
+ parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
+ mastodon_type = Activity.mastodon_notification_type(activity)
+
+ response = %{
+ id: to_string(notification.id),
+ type: mastodon_type,
+ created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
+ account: AccountView.render("account.json", %{user: actor, for: user}),
+ pleroma: %{
+ is_seen: notification.seen
+ }
+ }
+
+ case mastodon_type do
+ "mention" ->
+ response
+ |> Map.merge(%{
+ status: StatusView.render("status.json", %{activity: activity, for: user})
+ })
+
+ "favourite" ->
+ response
+ |> Map.merge(%{
+ status: StatusView.render("status.json", %{activity: parent_activity, for: user})
+ })
+
+ "reblog" ->
+ response
+ |> Map.merge(%{
+ status: StatusView.render("status.json", %{activity: parent_activity, for: user})
+ })
+
+ "follow" ->
+ response
+
+ _ ->
+ nil
+ end
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
new file mode 100644
index 000000000..0aae15ab9
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
@@ -0,0 +1,57 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.ScheduledActivity
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.MastodonAPI.ScheduledActivityView
+ alias Pleroma.Web.MastodonAPI.StatusView
+
+ def render("index.json", %{scheduled_activities: scheduled_activities}) do
+ render_many(scheduled_activities, ScheduledActivityView, "show.json")
+ end
+
+ def render("show.json", %{scheduled_activity: %ScheduledActivity{} = scheduled_activity}) do
+ %{
+ id: to_string(scheduled_activity.id),
+ scheduled_at: CommonAPI.Utils.to_masto_date(scheduled_activity.scheduled_at),
+ params: status_params(scheduled_activity.params)
+ }
+ |> with_media_attachments(scheduled_activity)
+ end
+
+ defp with_media_attachments(data, %{params: %{"media_attachments" => media_attachments}}) do
+ try do
+ attachments = render_many(media_attachments, StatusView, "attachment.json", as: :attachment)
+ Map.put(data, :media_attachments, attachments)
+ rescue
+ _ -> data
+ end
+ end
+
+ defp with_media_attachments(data, _), do: data
+
+ defp status_params(params) do
+ data = %{
+ text: params["status"],
+ sensitive: params["sensitive"],
+ spoiler_text: params["spoiler_text"],
+ visibility: params["visibility"],
+ scheduled_at: params["scheduled_at"],
+ poll: params["poll"],
+ in_reply_to_id: params["in_reply_to_id"]
+ }
+
+ data =
+ if media_ids = params["media_ids"] do
+ Map.put(data, :media_ids, media_ids)
+ else
+ data
+ end
+
+ data
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index bf3aaf025..62d064d71 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.Activity
alias Pleroma.HTML
+ alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
@@ -19,8 +20,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
defp get_replied_to_activities(activities) do
activities
|> Enum.map(fn
- %{data: %{"type" => "Create", "object" => %{"inReplyTo" => in_reply_to}}} ->
- in_reply_to != "" && in_reply_to
+ %{data: %{"type" => "Create", "object" => object}} ->
+ object = Object.normalize(object)
+ object.data["inReplyTo"] != "" && object.data["inReplyTo"]
_ ->
nil
@@ -29,7 +31,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|> Activity.create_by_object_ap_id()
|> Repo.all()
|> Enum.reduce(%{}, fn activity, acc ->
- Map.put(acc, activity.data["object"]["id"], activity)
+ object = Object.normalize(activity)
+ Map.put(acc, object.data["id"], activity)
end)
end
@@ -46,6 +49,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
end
+ defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id),
+ do: context_id
+
+ defp get_context_id(%{data: %{"context" => context}}) when is_binary(context),
+ do: Utils.context_to_conversation_id(context)
+
+ 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)
@@ -64,8 +80,13 @@ 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] && CommonAPI.bookmarked?(opts[:for], reblogged_activity)
mentions =
activity.recipients
@@ -86,9 +107,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,
@@ -109,14 +130,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
@@ -124,25 +147,53 @@ 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] && CommonAPI.bookmarked?(opts[:for], activity)
+
+ 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 =
object
|> render_content()
- |> HTML.get_cached_scrubbed_html_for_object(
+
+ content_html =
+ content
+ |> HTML.get_cached_scrubbed_html_for_activity(
User.html_filter_policy(opts[:for]),
activity,
- __MODULE__
+ "mastoapi:content"
+ )
+
+ content_plaintext =
+ content
+ |> HTML.get_cached_stripped_html_for_activity(
+ activity,
+ "mastoapi:content"
+ )
+
+ summary = object.data["summary"] || ""
+
+ summary_html =
+ summary
+ |> HTML.get_cached_scrubbed_html_for_activity(
+ User.html_filter_policy(opts[:for]),
+ activity,
+ "mastoapi:summary"
+ )
+
+ summary_plaintext =
+ summary
+ |> HTML.get_cached_stripped_html_for_activity(
+ activity,
+ "mastoapi:summary"
)
card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
@@ -151,30 +202,30 @@ 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),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
reblog: nil,
card: card,
- content: content,
+ content: content_html,
created_at: created_at,
reblogs_count: announcement_count,
- replies_count: 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),
pinned: pinned?(activity, user),
sensitive: sensitive,
- spoiler_text: object["summary"] || "",
+ spoiler_text: summary_html,
visibility: get_visibility(object),
media_attachments: attachments,
mentions: mentions,
@@ -184,9 +235,13 @@ 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
+ local: activity.local,
+ conversation_id: get_context_id(activity),
+ in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
+ content: %{"text/plain" => content_plaintext},
+ spoiler_text: %{"text/plain" => summary_plaintext}
}
}
end
@@ -257,18 +312,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
preview_url: href,
text_url: href,
type: type,
- description: attachment["name"]
+ description: attachment["name"],
+ pleroma: %{mime_type: media_type}
}
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)
+
+ 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
@@ -276,8 +338,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 ->
@@ -298,25 +360,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
end
- def render_content(%{"type" => "Video"} = object) do
- with name when not is_nil(name) and name != "" <- object["name"] do
- "<p><a href=\"#{object["id"]}\">#{name}</a></p>#{object["content"]}"
+ def render_content(%{data: %{"type" => "Video"}} = object) do
+ with name when not is_nil(name) and name != "" <- object.data["name"] do
+ "<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
else
- _ -> object["content"] || ""
+ _ -> object.data["content"] || ""
end
end
- def render_content(%{"type" => object_type} = object)
+ def render_content(%{data: %{"type" => object_type}} = object)
when object_type in ["Article", "Page"] do
- with summary when not is_nil(summary) and summary != "" <- object["name"],
- url when is_bitstring(url) <- object["url"] do
- "<p><a href=\"#{url}\">#{summary}</a></p>#{object["content"]}"
+ with summary when not is_nil(summary) and summary != "" <- object.data["name"],
+ url when is_bitstring(url) <- object.data["url"] do
+ "<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
else
- _ -> object["content"] || ""
+ _ -> object.data["content"] || ""
end
end
- def render_content(object), do: object["content"] || ""
+ def render_content(object), do: object.data["content"] || ""
@doc """
Builds a dictionary tags.
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index 9b262f461..abfa26754 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -90,7 +90,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
# Authenticated streams.
defp allow_request(stream, {"access_token", access_token}) when stream in @streams do
with %Token{user_id: user_id} <- Repo.get_by(Token, token: access_token),
- user = %User{} <- Repo.get(User, user_id) do
+ user = %User{} <- User.get_cached_by_id(user_id) do
{:ok, user}
else
_ -> {:error, 403}
diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex
index 3bd2affe9..5762e767b 100644
--- a/lib/pleroma/web/media_proxy/media_proxy.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy.ex
@@ -13,32 +13,44 @@ defmodule Pleroma.Web.MediaProxy do
def url(url) do
config = Application.get_env(:pleroma, :media_proxy, [])
+ domain = URI.parse(url).host
- if !Keyword.get(config, :enabled, false) or String.starts_with?(url, Pleroma.Web.base_url()) do
- url
- else
- secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
-
- # Must preserve `%2F` for compatibility with S3
- # https://git.pleroma.social/pleroma/pleroma/issues/580
- replacement = get_replacement(url, ":2F:")
-
- # The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice.
- base64 =
+ cond do
+ !Keyword.get(config, :enabled, false) or String.starts_with?(url, Pleroma.Web.base_url()) ->
url
- |> String.replace("%2F", replacement)
- |> URI.decode()
- |> URI.encode()
- |> String.replace(replacement, "%2F")
- |> Base.url_encode64(@base64_opts)
- sig = :crypto.hmac(:sha, secret, base64)
- sig64 = sig |> Base.url_encode64(@base64_opts)
+ Enum.any?(Pleroma.Config.get([:media_proxy, :whitelist]), fn pattern ->
+ String.equivalent?(domain, pattern)
+ end) ->
+ url
- build_url(sig64, base64, filename(url))
+ true ->
+ encode_url(url)
end
end
+ def encode_url(url) do
+ secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
+
+ # Must preserve `%2F` for compatibility with S3
+ # https://git.pleroma.social/pleroma/pleroma/issues/580
+ replacement = get_replacement(url, ":2F:")
+
+ # The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice.
+ base64 =
+ url
+ |> String.replace("%2F", replacement)
+ |> URI.decode()
+ |> URI.encode()
+ |> String.replace(replacement, "%2F")
+ |> Base.url_encode64(@base64_opts)
+
+ sig = :crypto.hmac(:sha, secret, base64)
+ sig64 = sig |> Base.url_encode64(@base64_opts)
+
+ build_url(sig64, base64, filename(url))
+ end
+
def decode_url(sig, url) do
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
sig = Base.url_decode64!(sig, @base64_opts)
diff --git a/lib/pleroma/web/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/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex
index 23bbde1a6..58385a3d1 100644
--- a/lib/pleroma/web/metadata/utils.ex
+++ b/lib/pleroma/web/metadata/utils.ex
@@ -12,7 +12,7 @@ defmodule Pleroma.Web.Metadata.Utils do
# html content comes from DB already encoded, decode first and scrub after
|> HtmlEntities.decode()
|> String.replace(~r/<br\s?\/?>/, " ")
- |> HTML.get_cached_stripped_html_for_object(object, __MODULE__)
+ |> HTML.get_cached_stripped_html_for_activity(object, "metadata")
|> Formatter.demojify()
|> Formatter.truncate()
end
diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
index 8c775ce24..216a962bd 100644
--- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
+++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
@@ -124,6 +124,9 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
end,
if Keyword.get(instance, :allow_relay) do
"relay"
+ end,
+ if Keyword.get(instance, :safe_dm_mentions) do
+ "safe_dm_mentions"
end
]
|> Enum.filter(& &1)
diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex
index a80543adf..3461f9983 100644
--- a/lib/pleroma/web/oauth/authorization.ex
+++ b/lib/pleroma/web/oauth/authorization.ex
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
schema "oauth_authorizations" do
field(:token, :string)
field(:scopes, {:array, :string}, default: [])
- field(:valid_until, :naive_datetime)
+ field(:valid_until, :naive_datetime_usec)
field(:used, :boolean, default: false)
belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId)
belongs_to(:app, App)
diff --git a/lib/pleroma/web/oauth/fallback_controller.ex b/lib/pleroma/web/oauth/fallback_controller.ex
index f0fe3b578..e3984f009 100644
--- a/lib/pleroma/web/oauth/fallback_controller.ex
+++ b/lib/pleroma/web/oauth/fallback_controller.ex
@@ -6,11 +6,24 @@ defmodule Pleroma.Web.OAuth.FallbackController do
use Pleroma.Web, :controller
alias Pleroma.Web.OAuth.OAuthController
- # No user/password
- def call(conn, _) do
+ def call(conn, {:register, :generic_error}) do
+ conn
+ |> put_status(:internal_server_error)
+ |> put_flash(:error, "Unknown error, please check the details and try again.")
+ |> OAuthController.registration_details(conn.params)
+ end
+
+ def call(conn, {:register, _error}) do
+ conn
+ |> put_status(:unauthorized)
+ |> put_flash(:error, "Invalid Username/Password")
+ |> OAuthController.registration_details(conn.params)
+ end
+
+ def call(conn, _error) 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 ec70b7ccc..688eaca11 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -5,26 +5,57 @@
defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller
- alias Comeonin.Pbkdf2
+ alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.Auth.Authenticator
+ alias Pleroma.Web.ControllerHelper
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
+ if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)
+
plug(:fetch_session)
plug(:fetch_flash)
action_fallback(Pleroma.Web.OAuth.FallbackController)
- def authorize(conn, params) do
+ # 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)
+ else
+ redirect_uri =
+ if is_binary(params["redirect_uri"]) do
+ params["redirect_uri"]
+ else
+ app = Repo.preload(token, :app).app
+
+ app.redirect_uris
+ |> String.split()
+ |> Enum.at(0)
+ end
+
+ redirect(conn, external: redirect_uri(conn, redirect_uri))
+ end
+ end
+
+ def authorize(conn, params), do: do_authorize(conn, params)
+
+ defp do_authorize(conn, params) do
app = Repo.get_by(App, client_id: params["client_id"])
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"],
@@ -36,68 +67,75 @@ defmodule Pleroma.Web.OAuth.OAuthController do
})
end
- def create_authorization(conn, %{
- "authorization" =>
- %{
- "client_id" => client_id,
- "redirect_uri" => redirect_uri
- } = auth_params
+ def create_authorization(
+ conn,
+ %{"authorization" => _} = params,
+ opts \\ []
+ ) do
+ with {:ok, auth} <- do_create_authorization(conn, params, opts[:user]) do
+ after_create_authorization(conn, auth, params)
+ else
+ error ->
+ handle_create_authorization_error(conn, error, params)
+ end
+ end
+
+ def after_create_authorization(conn, auth, %{
+ "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs
}) do
- with {_, {:ok, %User{} = user}} <- {:get_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, []),
- {:unsupported_scopes, []} <- {:unsupported_scopes, scopes -- app.scopes},
- # Note: `scope` param is intentionally not optional in this context
- {:missing_scopes, false} <- {:missing_scopes, scopes == []},
- {:auth_active, true} <- {:auth_active, User.auth_active?(user)},
- {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
- redirect_uri =
- if redirect_uri == "." do
- # Special case: Local MastodonFE
- mastodon_api_url(conn, :login)
- else
- redirect_uri
- end
+ redirect_uri = redirect_uri(conn, redirect_uri)
- cond do
- redirect_uri == "urn:ietf:wg:oauth:2.0:oob" ->
- render(conn, "results.html", %{
- auth: auth
- })
+ if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" do
+ render(conn, "results.html", %{
+ auth: auth
+ })
+ else
+ connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?"
+ url = "#{redirect_uri}#{connector}"
+ url_params = %{:code => auth.token}
- true ->
- connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?"
- url = "#{redirect_uri}#{connector}"
- url_params = %{:code => auth.token}
+ url_params =
+ if auth_attrs["state"] do
+ Map.put(url_params, :state, auth_attrs["state"])
+ else
+ url_params
+ end
- url_params =
- if auth_params["state"] do
- Map.put(url_params, :state, auth_params["state"])
- else
- url_params
- end
+ url = "#{url}#{Plug.Conn.Query.encode(url_params)}"
- url = "#{url}#{Plug.Conn.Query.encode(url_params)}"
+ redirect(conn, external: url)
+ end
+ end
- redirect(conn, external: url)
- end
- else
- {scopes_issue, _} when scopes_issue in [:unsupported_scopes, :missing_scopes] ->
- conn
- |> put_flash(:error, "Permissions not specified.")
- |> put_status(:unauthorized)
- |> authorize(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(params)
+ end
- {:auth_active, false} ->
- conn
- |> put_flash(:error, "Account confirmation pending.")
- |> put_status(:forbidden)
- |> authorize(auth_params)
+ 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(params)
+ end
- error ->
- Authenticator.handle_error(conn, error)
- end
+ defp handle_create_authorization_error(conn, error, %{"authorization" => _}) do
+ Authenticator.handle_error(conn, error)
end
def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
@@ -105,6 +143,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
fixed_token = fix_padding(params["code"]),
%Authorization{} = auth <-
Repo.get_by(Authorization, token: fixed_token, app_id: app.id),
+ %User{} = user <- User.get_cached_by_id(auth.user_id),
{:ok, token} <- Token.exchange_token(app, auth),
{:ok, inserted_at} <- DateTime.from_naive(token.inserted_at, "Etc/UTC") do
response = %{
@@ -113,7 +152,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
refresh_token: token.refresh_token,
created_at: DateTime.to_unix(inserted_at),
expires_in: 60 * 10,
- scope: Enum.join(token.scopes, " ")
+ scope: Enum.join(token.scopes, " "),
+ me: user.ap_id
}
json(conn, response)
@@ -126,12 +166,12 @@ defmodule Pleroma.Web.OAuth.OAuthController do
def token_exchange(
conn,
- %{"grant_type" => "password", "username" => name, "password" => password} = params
+ %{"grant_type" => "password"} = params
) do
- with %App{} = app <- get_app_from_request(conn, params),
- %User{} = user <- User.get_by_nickname_or_email(name),
- true <- Pbkdf2.checkpw(password, user.password_hash),
+ 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},
scopes <- oauth_scopes(params, app.scopes),
[] <- scopes -- app.scopes,
true <- Enum.any?(scopes),
@@ -142,15 +182,23 @@ defmodule Pleroma.Web.OAuth.OAuthController do
access_token: token.token,
refresh_token: token.refresh_token,
expires_in: 60 * 10,
- scope: Enum.join(token.scopes, " ")
+ scope: Enum.join(token.scopes, " "),
+ me: user.ap_id
}
json(conn, response)
else
{:auth_active, false} ->
+ # Per https://github.com/tootsuite/mastodon/blob/
+ # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76
conn
|> put_status(:forbidden)
- |> json(%{error: "Account confirmation pending"})
+ |> json(%{error: "Your login is missing a confirmed e-mail address"})
+
+ {:user_active, false} ->
+ conn
+ |> put_status(:forbidden)
+ |> json(%{error: "Your account is currently disabled"})
_error ->
put_status(conn, 400)
@@ -182,6 +230,175 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
end
+ @doc "Prepares OAuth request to provider for Ueberauth"
+ def prepare_request(conn, %{"provider" => provider, "authorization" => auth_attrs}) do
+ scope =
+ oauth_scopes(auth_attrs, [])
+ |> Enum.join(" ")
+
+ state =
+ auth_attrs
+ |> Map.delete("scopes")
+ |> Map.put("scope", scope)
+ |> Poison.encode!()
+
+ params =
+ auth_attrs
+ |> Map.drop(~w(scope scopes client_id redirect_uri))
+ |> Map.put("state", state)
+
+ # Handing the request to Ueberauth
+ redirect(conn, to: o_auth_path(conn, :request, provider, params))
+ end
+
+ def request(conn, params) do
+ message =
+ if params["provider"] do
+ "Unsupported OAuth provider: #{params["provider"]}."
+ else
+ "Bad OAuth request."
+ end
+
+ conn
+ |> put_flash(:error, message)
+ |> redirect(to: "/")
+ end
+
+ def callback(%{assigns: %{ueberauth_failure: failure}} = conn, params) do
+ params = callback_params(params)
+ messages = for e <- Map.get(failure, :errors, []), do: e.message
+ message = Enum.join(messages, "; ")
+
+ conn
+ |> put_flash(:error, "Failed to authenticate: #{message}.")
+ |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
+ end
+
+ def callback(conn, params) do
+ params = callback_params(params)
+
+ with {:ok, registration} <- Authenticator.get_registration(conn) do
+ user = Repo.preload(registration, :user).user
+ auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
+
+ if user do
+ create_authorization(
+ conn,
+ %{"authorization" => auth_attrs},
+ user: user
+ )
+ else
+ registration_params =
+ Map.merge(auth_attrs, %{
+ "nickname" => Registration.nickname(registration),
+ "email" => Registration.email(registration)
+ })
+
+ conn
+ |> put_session(:registration_id, registration.id)
+ |> registration_details(%{"authorization" => registration_params})
+ end
+ else
+ _ ->
+ conn
+ |> put_flash(:error, "Failed to set up user account.")
+ |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
+ end
+ end
+
+ defp callback_params(%{"state" => state} = params) do
+ Map.merge(params, Poison.decode!(state))
+ end
+
+ def registration_details(conn, %{"authorization" => auth_attrs}) do
+ render(conn, "register.html", %{
+ 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, %{"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, 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, params)
+ else
+ {:create_authorization, error} ->
+ {:register, handle_create_authorization_error(conn, error, params)}
+
+ _ ->
+ {:register, :generic_error}
+ end
+ end
+
+ 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, registration) do
+ conn
+ |> put_session_registration_id(nil)
+ |> create_authorization(
+ params,
+ user: user
+ )
+ else
+ {:error, changeset} ->
+ message =
+ Enum.map(changeset.errors, fn {field, {error, _}} ->
+ "#{field} #{error}"
+ end)
+ |> Enum.join("; ")
+
+ message =
+ String.replace(
+ message,
+ "ap_id has already been taken",
+ "nickname has already been taken"
+ )
+
+ conn
+ |> put_status(:forbidden)
+ |> put_flash(:error, "Error: #{message}.")
+ |> registration_details(params)
+
+ _ ->
+ {:register, :generic_error}
+ end
+ end
+
+ defp do_create_authorization(
+ conn,
+ %{
+ "authorization" =>
+ %{
+ "client_id" => client_id,
+ "redirect_uri" => redirect_uri
+ } = auth_attrs
+ },
+ user \\ nil
+ ) do
+ with {_, {:ok, %User{} = user}} <-
+ {: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_attrs, []),
+ {:unsupported_scopes, []} <- {:unsupported_scopes, scopes -- app.scopes},
+ # Note: `scope` param is intentionally not optional in this context
+ {:missing_scopes, false} <- {:missing_scopes, scopes == []},
+ {:auth_active, true} <- {:auth_active, User.auth_active?(user)} do
+ Authorization.create_authorization(app, user, scopes)
+ end
+ end
+
# XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be
# decoding it. Investigate sometime.
defp fix_padding(token) do
@@ -214,4 +431,14 @@ defmodule Pleroma.Web.OAuth.OAuthController do
nil
end
end
+
+ # Special case: Local MastodonFE
+ defp redirect_uri(conn, "."), do: mastodon_api_url(conn, :login)
+
+ defp redirect_uri(_conn, redirect_uri), do: redirect_uri
+
+ defp get_session_registration_id(conn), do: get_session(conn, :registration_id)
+
+ defp put_session_registration_id(conn, registration_id),
+ do: put_session(conn, :registration_id, registration_id)
end
diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex
index 2b074b470..399140003 100644
--- a/lib/pleroma/web/oauth/token.ex
+++ b/lib/pleroma/web/oauth/token.ex
@@ -17,7 +17,7 @@ defmodule Pleroma.Web.OAuth.Token do
field(:token, :string)
field(:refresh_token, :string)
field(:scopes, {:array, :string}, default: [])
- field(:valid_until, :naive_datetime)
+ field(:valid_until, :naive_datetime_usec)
belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId)
belongs_to(:app, App)
@@ -27,7 +27,7 @@ defmodule Pleroma.Web.OAuth.Token do
def exchange_token(app, auth) do
with {:ok, auth} <- Authorization.use_token(auth),
true <- auth.app_id == app.id do
- create_token(app, Repo.get(User, auth.user_id), auth.scopes)
+ create_token(app, User.get_cached_by_id(auth.user_id), auth.scopes)
end
end
diff --git a/lib/pleroma/web/ostatus/activity_representer.ex b/lib/pleroma/web/ostatus/activity_representer.ex
index 1a1b74bb0..166691a09 100644
--- a/lib/pleroma/web/ostatus/activity_representer.ex
+++ b/lib/pleroma/web/ostatus/activity_representer.ex
@@ -54,23 +54,16 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
end)
end
- defp get_links(%{local: true, data: data}) do
+ defp get_links(%{local: true}, %{"id" => object_id}) do
h = fn str -> [to_charlist(str)] end
[
- {:link, [type: ['application/atom+xml'], href: h.(data["object"]["id"]), rel: 'self'], []},
- {:link, [type: ['text/html'], href: h.(data["object"]["id"]), rel: 'alternate'], []}
+ {:link, [type: ['application/atom+xml'], href: h.(object_id), rel: 'self'], []},
+ {:link, [type: ['text/html'], href: h.(object_id), rel: 'alternate'], []}
]
end
- defp get_links(%{
- local: false,
- data: %{
- "object" => %{
- "external_url" => external_url
- }
- }
- }) do
+ defp get_links(%{local: false}, %{"external_url" => external_url}) do
h = fn str -> [to_charlist(str)] end
[
@@ -78,7 +71,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
]
end
- defp get_links(_activity), do: []
+ defp get_links(_activity, _object_data), do: []
defp get_emoji_links(emojis) do
Enum.map(emojis, fn {emoji, file} ->
@@ -88,14 +81,16 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
def to_simple_form(activity, user, with_author \\ false)
- def to_simple_form(%{data: %{"object" => %{"type" => "Note"}}} = activity, user, with_author) do
+ def to_simple_form(%{data: %{"type" => "Create"}} = activity, user, with_author) do
h = fn str -> [to_charlist(str)] end
- updated_at = activity.data["object"]["published"]
- inserted_at = activity.data["object"]["published"]
+ object = Object.normalize(activity)
+
+ updated_at = object.data["published"]
+ inserted_at = object.data["published"]
attachments =
- Enum.map(activity.data["object"]["attachment"] || [], fn attachment ->
+ Enum.map(object.data["attachment"] || [], fn attachment ->
url = hd(attachment["url"])
{:link,
@@ -108,7 +103,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
mentions = activity.recipients |> get_mentions
categories =
- (activity.data["object"]["tag"] || [])
+ (object.data["tag"] || [])
|> Enum.map(fn tag ->
if is_binary(tag) do
{:category, [term: to_charlist(tag)], []}
@@ -118,11 +113,11 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
end)
|> Enum.filter(& &1)
- emoji_links = get_emoji_links(activity.data["object"]["emoji"] || %{})
+ emoji_links = get_emoji_links(object.data["emoji"] || %{})
summary =
- if activity.data["object"]["summary"] do
- [{:summary, [], h.(activity.data["object"]["summary"])}]
+ if object.data["summary"] do
+ [{:summary, [], h.(object.data["summary"])}]
else
[]
end
@@ -131,10 +126,9 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']},
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/post']},
# For notes, federate the object id.
- {:id, h.(activity.data["object"]["id"])},
+ {:id, h.(object.data["id"])},
{:title, ['New note by #{user.nickname}']},
- {:content, [type: 'html'],
- h.(activity.data["object"]["content"] |> String.replace(~r/[\n\r]/, ""))},
+ {:content, [type: 'html'], h.(object.data["content"] |> String.replace(~r/[\n\r]/, ""))},
{:published, h.(inserted_at)},
{:updated, h.(updated_at)},
{:"ostatus:conversation", [ref: h.(activity.data["context"])],
@@ -142,7 +136,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
{:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []}
] ++
summary ++
- get_links(activity) ++
+ get_links(activity, object.data) ++
categories ++ attachments ++ in_reply_to ++ author ++ mentions ++ emoji_links
end
diff --git a/lib/pleroma/web/ostatus/handlers/note_handler.ex b/lib/pleroma/web/ostatus/handlers/note_handler.ex
index 770a71a0a..ec6e5cfaf 100644
--- a/lib/pleroma/web/ostatus/handlers/note_handler.ex
+++ b/lib/pleroma/web/ostatus/handlers/note_handler.ex
@@ -106,15 +106,16 @@ defmodule Pleroma.Web.OStatus.NoteHandler do
# TODO: Clean this up a bit.
def handle_note(entry, doc \\ nil) do
with id <- XML.string_from_xpath("//id", entry),
- activity when is_nil(activity) <- Activity.get_create_by_object_ap_id(id),
+ activity when is_nil(activity) <- Activity.get_create_by_object_ap_id_with_object(id),
[author] <- :xmerl_xpath.string('//author[1]', doc),
{:ok, actor} <- OStatus.find_make_or_update_user(author),
content_html <- OStatus.get_content(entry),
cw <- OStatus.get_cw(entry),
in_reply_to <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry),
in_reply_to_activity <- fetch_replied_to_activity(entry, in_reply_to),
- in_reply_to <-
- (in_reply_to_activity && in_reply_to_activity.data["object"]["id"]) || in_reply_to,
+ in_reply_to_object <-
+ (in_reply_to_activity && Object.normalize(in_reply_to_activity)) || nil,
+ in_reply_to <- (in_reply_to_object && in_reply_to_object.data["id"]) || in_reply_to,
attachments <- OStatus.get_attachments(entry),
context <- get_context(entry, in_reply_to),
tags <- OStatus.get_tags(entry),
diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex
index 266f86bf4..4744c6d83 100644
--- a/lib/pleroma/web/ostatus/ostatus.ex
+++ b/lib/pleroma/web/ostatus/ostatus.ex
@@ -23,8 +23,8 @@ defmodule Pleroma.Web.OStatus do
alias Pleroma.Web.WebFinger
alias Pleroma.Web.Websub
- def is_representable?(%Activity{data: data}) do
- object = Object.normalize(data["object"])
+ def is_representable?(%Activity{} = activity) do
+ object = Object.normalize(activity)
cond do
is_nil(object) ->
@@ -119,7 +119,7 @@ defmodule Pleroma.Web.OStatus do
def make_share(entry, doc, retweeted_activity) do
with {:ok, actor} <- find_make_or_update_user(doc),
- %Object{} = object <- Object.normalize(retweeted_activity.data["object"]),
+ %Object{} = object <- Object.normalize(retweeted_activity),
id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
{:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do
{:ok, activity}
@@ -137,7 +137,7 @@ defmodule Pleroma.Web.OStatus do
def make_favorite(entry, doc, favorited_activity) do
with {:ok, actor} <- find_make_or_update_user(doc),
- %Object{} = object <- Object.normalize(favorited_activity.data["object"]),
+ %Object{} = object <- Object.normalize(favorited_activity),
id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
{:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do
{:ok, activity}
@@ -159,7 +159,7 @@ defmodule Pleroma.Web.OStatus do
Logger.debug("Trying to get entry from db")
with id when not is_nil(id) <- string_from_xpath("//activity:object[1]/id", entry),
- %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
+ %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
{:ok, activity}
else
_ ->
@@ -294,7 +294,7 @@ defmodule Pleroma.Web.OStatus do
}
with false <- update,
- %User{} = user <- User.get_by_ap_id(data.ap_id) do
+ %User{} = user <- User.get_cached_by_ap_id(data.ap_id) do
{:ok, user}
else
_e -> User.insert_or_update_user(data)
diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex
index 0579a5f3d..2fb6ce41b 100644
--- a/lib/pleroma/web/ostatus/ostatus_controller.ex
+++ b/lib/pleroma/web/ostatus/ostatus_controller.ex
@@ -102,7 +102,8 @@ defmodule Pleroma.Web.OStatus.OStatusController do
ActivityPubController.call(conn, :object)
else
with id <- o_status_url(conn, :object, uuid),
- {_, %Activity{} = activity} <- {:activity, Activity.get_create_by_object_ap_id(id)},
+ {_, %Activity{} = activity} <-
+ {:activity, Activity.get_create_by_object_ap_id_with_object(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case get_format(conn) do
@@ -148,13 +149,13 @@ defmodule Pleroma.Web.OStatus.OStatusController do
end
def notice(conn, %{"id" => id}) do
- with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id(id)},
+ with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id_with_object(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case format = get_format(conn) do
"html" ->
if activity.data["type"] == "Create" do
- %Object{} = object = Object.normalize(activity.data["object"])
+ %Object{} = object = Object.normalize(activity)
Fallback.RedirectController.redirector_with_meta(conn, %{
activity_id: activity.id,
@@ -191,9 +192,9 @@ defmodule Pleroma.Web.OStatus.OStatusController do
# Returns an HTML embedded <audio> or <video> player suitable for embed iframes.
def notice_player(conn, %{"id" => id}) do
- with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
+ with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.is_public?(activity),
- %Object{} = object <- Object.normalize(activity.data["object"]),
+ %Object{} = object <- Object.normalize(activity),
%{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object,
true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do
conn
@@ -219,7 +220,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
%Activity{data: %{"type" => "Create"}} = activity,
_user
) do
- object = Object.normalize(activity.data["object"])
+ object = Object.normalize(activity)
conn
|> put_resp_header("content-type", "application/activity+json")
diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex
index 863573185..35d3ff07c 100644
--- a/lib/pleroma/web/push/impl.ex
+++ b/lib/pleroma/web/push/impl.ex
@@ -19,10 +19,12 @@ defmodule Pleroma.Web.Push.Impl do
@types ["Create", "Follow", "Announce", "Like"]
@doc "Performs sending notifications for user subscriptions"
- @spec perform_send(Notification.t()) :: list(any)
- def perform_send(
- %{activity: %{data: %{"type" => activity_type}, id: activity_id}, user_id: user_id} =
- notif
+ @spec perform(Notification.t()) :: list(any) | :error
+ def perform(
+ %{
+ activity: %{data: %{"type" => activity_type}, id: activity_id} = activity,
+ user_id: user_id
+ } = notif
)
when activity_type in @types do
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
@@ -30,13 +32,14 @@ defmodule Pleroma.Web.Push.Impl do
type = Activity.mastodon_notification_type(notif.activity)
gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
avatar_url = User.avatar_url(actor)
+ object = Object.normalize(activity)
for subscription <- fetch_subsriptions(user_id),
get_in(subscription.data, ["alerts", type]) do
%{
title: format_title(notif),
access_token: subscription.token.token,
- body: format_body(notif, actor),
+ body: format_body(notif, actor, object),
notification_id: notif.id,
notification_type: type,
icon: avatar_url,
@@ -50,7 +53,7 @@ defmodule Pleroma.Web.Push.Impl do
end
end
- def perform_send(_) do
+ def perform(_) do
Logger.warn("Unknown notification type")
:error
end
@@ -95,25 +98,25 @@ defmodule Pleroma.Web.Push.Impl do
end
def format_body(
- %{activity: %{data: %{"type" => "Create", "object" => %{"content" => content}}}},
- actor
+ %{activity: %{data: %{"type" => "Create"}}},
+ actor,
+ %{data: %{"content" => content}}
) do
"@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
end
def format_body(
- %{activity: %{data: %{"type" => "Announce", "object" => activity_id}}},
- actor
+ %{activity: %{data: %{"type" => "Announce"}}},
+ actor,
+ %{data: %{"content" => content}}
) do
- %Activity{data: %{"object" => %{"id" => object_id}}} = Activity.get_by_ap_id(activity_id)
- %Object{data: %{"content" => content}} = Object.get_by_ap_id(object_id)
-
"@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}"
end
def format_body(
%{activity: %{data: %{"type" => type}}},
- actor
+ actor,
+ _object
)
when type in ["Follow", "Like"] do
case type do
diff --git a/lib/pleroma/web/push/push.ex b/lib/pleroma/web/push/push.ex
index 5259e8e33..729dad02a 100644
--- a/lib/pleroma/web/push/push.ex
+++ b/lib/pleroma/web/push/push.ex
@@ -3,18 +3,20 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Push do
- use GenServer
-
alias Pleroma.Web.Push.Impl
require Logger
- ##############
- # Client API #
- ##############
+ def init do
+ unless enabled() do
+ Logger.warn("""
+ VAPID key pair is not found. If you wish to enabled web push, please run
+
+ mix web_push.gen.keypair
- def start_link do
- GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
+ and add the resulting output to your configuration file.
+ """)
+ end
end
def vapid_config do
@@ -30,35 +32,5 @@ defmodule Pleroma.Web.Push do
end
def send(notification),
- do: GenServer.cast(__MODULE__, {:send, notification})
-
- ####################
- # Server Callbacks #
- ####################
-
- @impl true
- def init(:ok) do
- if enabled() do
- {:ok, nil}
- else
- Logger.warn("""
- VAPID key pair is not found. If you wish to enabled web push, please run
-
- mix web_push.gen.keypair
-
- and add the resulting output to your configuration file.
- """)
-
- :ignore
- end
- end
-
- @impl true
- def handle_cast({:send, notification}, state) do
- if enabled() do
- Impl.perform_send(notification)
- end
-
- {:noreply, state}
- end
+ do: PleromaJobQueue.enqueue(:web_push, Impl, [notification])
end
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/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex
index 92c61ff51..f67aaf58b 100644
--- a/lib/pleroma/web/rich_media/helpers.ex
+++ b/lib/pleroma/web/rich_media/helpers.ex
@@ -21,9 +21,9 @@ defmodule Pleroma.Web.RichMedia.Helpers do
defp validate_page_url(%URI{}), do: :ok
defp validate_page_url(_), do: :error
- def fetch_data_for_activity(%Activity{} = activity) do
+ def fetch_data_for_activity(%Activity{data: %{"type" => "Create"}} = activity) do
with true <- Pleroma.Config.get([:rich_media, :enabled]),
- %Object{} = object <- Object.normalize(activity.data["object"]),
+ %Object{} = object <- Object.normalize(activity),
{:ok, page_url} <- HTML.extract_first_external_url(object, object.data["content"]),
:ok <- validate_page_url(page_url),
{:ok, rich_media} <- Parser.parse(page_url) do
@@ -32,4 +32,6 @@ defmodule Pleroma.Web.RichMedia.Helpers do
_ -> %{}
end
end
+
+ def fetch_data_for_activity(_), do: %{}
end
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 c56e4a421..ff4f08af5 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -5,6 +5,16 @@
defmodule Pleroma.Web.Router do
use Pleroma.Web, :router
+ pipeline :browser do
+ plug(:accepts, ["html"])
+ plug(:fetch_session)
+ end
+
+ pipeline :oauth do
+ plug(:fetch_session)
+ plug(Pleroma.Plugs.OAuthPlug)
+ end
+
pipeline :api do
plug(:accepts, ["json"])
plug(:fetch_session)
@@ -105,10 +115,6 @@ defmodule Pleroma.Web.Router do
plug(:accepts, ["json", "xml"])
end
- pipeline :oauth do
- plug(:accepts, ["html", "json"])
- end
-
pipeline :pleroma_api do
plug(:accepts, ["html", "json"])
end
@@ -129,6 +135,7 @@ defmodule Pleroma.Web.Router do
post("/password_reset", UtilController, :password_reset)
get("/emoji", UtilController, :emoji)
get("/captcha", UtilController, :captcha)
+ get("/healthcheck", UtilController, :healthcheck)
end
scope "/api/pleroma", Pleroma.Web do
@@ -139,7 +146,12 @@ defmodule Pleroma.Web.Router do
scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through([:admin_api, :oauth_write])
+ post("/user/follow", AdminAPIController, :user_follow)
+ post("/user/unfollow", AdminAPIController, :user_unfollow)
+
get("/users", AdminAPIController, :list_users)
+ get("/users/:nickname", AdminAPIController, :user_show)
+
delete("/user", AdminAPIController, :user_delete)
patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
post("/user", AdminAPIController, :user_create)
@@ -157,6 +169,8 @@ defmodule Pleroma.Web.Router do
delete("/relay", AdminAPIController, :relay_unfollow)
get("/invite_token", AdminAPIController, :get_invite_token)
+ get("/invites", AdminAPIController, :invites)
+ post("/revoke_invite", AdminAPIController, :revoke_invite)
post("/email_invite", AdminAPIController, :email_invite)
get("/password_reset", AdminAPIController, :get_password_reset)
@@ -182,6 +196,7 @@ defmodule Pleroma.Web.Router do
post("/change_password", UtilController, :change_password)
post("/delete_account", UtilController, :delete_account)
+ put("/notification_settings", UtilController, :update_notificaton_settings)
end
scope [] do
@@ -190,13 +205,33 @@ defmodule Pleroma.Web.Router do
post("/blocks_import", UtilController, :blocks_import)
post("/follow_import", UtilController, :follow_import)
end
+
+ scope [] do
+ pipe_through(:oauth_read)
+
+ post("/notifications/read", UtilController, :notifications_read)
+ end
end
scope "/oauth", Pleroma.Web.OAuth do
- get("/authorize", OAuthController, :authorize)
+ scope [] do
+ pipe_through(:oauth)
+ get("/authorize", OAuthController, :authorize)
+ end
+
post("/authorize", OAuthController, :create_authorization)
post("/token", OAuthController, :token_exchange)
post("/revoke", OAuthController, :token_revoke)
+ get("/registration_details", OAuthController, :registration_details)
+
+ scope [] do
+ pipe_through(:browser)
+
+ get("/prepare_request", OAuthController, :prepare_request)
+ get("/:provider", OAuthController, :request)
+ get("/:provider/callback", OAuthController, :callback)
+ post("/register", OAuthController, :register)
+ end
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
@@ -208,9 +243,9 @@ 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)
get("/follow_requests", MastodonAPIController, :follow_requests)
get("/blocks", MastodonAPIController, :blocks)
@@ -226,6 +261,10 @@ 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)
get("/lists", MastodonAPIController, :get_lists)
get("/lists/:id", MastodonAPIController, :get_list)
@@ -261,6 +300,9 @@ defmodule Pleroma.Web.Router do
post("/statuses/:id/mute", MastodonAPIController, :mute_conversation)
post("/statuses/:id/unmute", MastodonAPIController, :unmute_conversation)
+ put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status)
+ delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status)
+
post("/media", MastodonAPIController, :upload)
put("/media/:id", MastodonAPIController, :update_media)
@@ -298,6 +340,9 @@ defmodule Pleroma.Web.Router do
post("/domain_blocks", MastodonAPIController, :block_domain)
delete("/domain_blocks", MastodonAPIController, :unblock_domain)
+
+ post("/pleroma/accounts/:id/subscribe", MastodonAPIController, :subscribe)
+ post("/pleroma/accounts/:id/unsubscribe", MastodonAPIController, :unsubscribe)
end
scope [] do
@@ -322,6 +367,7 @@ defmodule Pleroma.Web.Router do
get("/instance", MastodonAPIController, :masto_instance)
get("/instance/peers", MastodonAPIController, :peers)
post("/apps", MastodonAPIController, :create_app)
+ get("/apps/verify_credentials", MastodonAPIController, :verify_app_credentials)
get("/custom_emojis", MastodonAPIController, :custom_emojis)
get("/statuses/:id/card", MastodonAPIController, :status_card)
@@ -331,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)
@@ -347,6 +395,8 @@ defmodule Pleroma.Web.Router do
get("/accounts/:id", MastodonAPIController, :user)
get("/search", MastodonAPIController, :search)
+
+ get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites)
end
end
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index aec11a79f..72eaf2084 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -8,9 +8,10 @@ defmodule Pleroma.Web.Streamer do
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Object
- alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.MastodonAPI.NotificationView
@keepalive_interval :timer.seconds(30)
@@ -80,7 +81,7 @@ defmodule Pleroma.Web.Streamer do
_ ->
Pleroma.List.get_lists_from_activity(item)
|> Enum.filter(fn list ->
- owner = Repo.get(User, list.user_id)
+ owner = User.get_cached_by_id(list.user_id)
Visibility.visible_for_user?(item, owner)
end)
@@ -106,10 +107,10 @@ defmodule Pleroma.Web.Streamer do
%{
event: "notification",
payload:
- Pleroma.Web.MastodonAPI.MastodonAPIController.render_notification(
- socket.assigns["user"],
- item
- )
+ NotificationView.render("show.json", %{
+ notification: item,
+ for: socket.assigns["user"]
+ })
|> Jason.encode!()
}
|> Jason.encode!()
@@ -198,10 +199,12 @@ defmodule Pleroma.Web.Streamer do
user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
blocks = user.info.blocks || []
mutes = user.info.mutes || []
+ reblog_mutes = user.info.muted_reblogs || []
- parent = Object.normalize(item.data["object"])
+ parent = Object.normalize(item)
unless is_nil(parent) or item.actor in blocks or item.actor in mutes or
+ item.actor in reblog_mutes or not ActivityPub.contain_activity(item, user) or
parent.data["actor"] in blocks or parent.data["actor"] in mutes do
send(socket.transport_pid, {:text, represent_update(item, user)})
end
@@ -232,7 +235,8 @@ defmodule Pleroma.Web.Streamer do
blocks = user.info.blocks || []
mutes = user.info.mutes || []
- unless item.actor in blocks or item.actor in mutes do
+ unless item.actor in blocks or item.actor in mutes or
+ not ActivityPub.contain_activity(item, user) do
send(socket.transport_pid, {:text, represent_update(item, user)})
end
else
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
new file mode 100644
index 000000000..e6cfe108b
--- /dev/null
+++ b/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex
@@ -0,0 +1,13 @@
+<div class="scopes-input">
+ <%= label @form, :scope, "Permissions" %>
+
+ <div class="scopes">
+ <%= 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: "authorization[scope][]" %>
+ <%= label @form, :"scope_#{scope}", String.capitalize(scope) %>
+ </div>
+ <% end %>
+ </div>
+</div>
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
new file mode 100644
index 000000000..4bcda7300
--- /dev/null
+++ b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex
@@ -0,0 +1,13 @@
+<h2>Sign in with external provider</h2>
+
+<%= 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 %>
+ <%= hidden_input f, :redirect_uri, value: @redirect_uri %>
+ <%= hidden_input f, :state, value: @state %>
+
+ <%= for strategy <- Pleroma.Config.oauth_consumer_strategies() do %>
+ <%= submit "Sign in with #{String.capitalize(strategy)}", name: "provider", value: strategy %>
+ <% end %>
+<% end %>
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
new file mode 100644
index 000000000..facedc8db
--- /dev/null
+++ b/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex
@@ -0,0 +1,42 @@
+<%= if get_flash(@conn, :info) do %>
+ <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
+<% end %>
+<%= if get_flash(@conn, :error) do %>
+ <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
+<% end %>
+
+<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), [as: "authorization"], fn f -> %>
+
+<div class="input">
+ <%= label f, :nickname, "Nickname" %>
+ <%= text_input f, :nickname, value: @nickname %>
+</div>
+<div class="input">
+ <%= label f, :email, "Email" %>
+ <%= text_input f, :email, value: @email %>
+</div>
+
+<%= submit "Proceed as new user", name: "op", value: "register" %>
+
+<p>Alternatively, sign in to connect to existing account.</p>
+
+<div class="input">
+ <%= label f, :name, "Name or email" %>
+ <%= text_input f, :name %>
+</div>
+<div class="input">
+ <%= label f, :password, "Password" %>
+ <%= password_input f, :password %>
+</div>
+
+<%= submit "Proceed as existing user", name: "op", value: "connect" %>
+
+<%= hidden_input f, :client_id, value: @client_id %>
+<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
+<%= hidden_input f, :scope, value: Enum.join(@scopes, " ") %>
+<%= hidden_input f, :state, value: @state %>
+
+<% end %>
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 161333847..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
@@ -4,7 +4,9 @@
<%= if get_flash(@conn, :error) do %>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<% end %>
+
<h2>OAuth Authorization</h2>
+
<%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %>
<div class="input">
<%= label f, :name, "Name or email" %>
@@ -14,22 +16,16 @@
<%= label f, :password, "Password" %>
<%= password_input f, :password %>
</div>
-<div class="scopes-input">
-<%= label f, :scope, "Permissions" %>
- <div class="scopes">
- <%= 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 f, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %>
- <%= label f, :"scope_#{scope}", String.capitalize(scope) %>
- </div>
- <% end %>
- </div>
-</div>
+
+<%= 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 %>
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
-<%= hidden_input f, :state, value: @state%>
+<%= hidden_input f, :state, value: @state %>
<%= submit "Authorize" %>
<% end %>
+
+<%= if Pleroma.Config.oauth_consumer_enabled?() do %>
+ <%= render @view_module, Pleroma.Web.Auth.Authenticator.oauth_consumer_template(), assigns %>
+<% end %>
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 8ed02a93f..1122e6c5d 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -8,7 +8,9 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
require Logger
alias Comeonin.Pbkdf2
+ alias Pleroma.Activity
alias Pleroma.Emoji
+ alias Pleroma.Notification
alias Pleroma.PasswordResetToken
alias Pleroma.Repo
alias Pleroma.User
@@ -20,7 +22,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
def show_password_reset(conn, %{"token" => token}) do
with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
- %User{} = user <- Repo.get(User, token.user_id) do
+ %User{} = user <- User.get_cached_by_id(token.user_id) do
render(conn, "password_reset.html", %{
token: token,
user: user
@@ -72,36 +74,52 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
def remote_follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do
- {err, followee} = OStatus.find_or_make_user(acct)
- avatar = User.avatar_url(followee)
- name = followee.nickname
- id = followee.id
-
- if !!user do
- conn
- |> render("follow.html", %{error: err, acct: acct, avatar: avatar, name: name, id: id})
+ if is_status?(acct) do
+ {: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
- conn
- |> render("follow_login.html", %{
- error: false,
- acct: acct,
- avatar: avatar,
- name: name,
- id: id
- })
+ {err, followee} = OStatus.find_or_make_user(acct)
+ avatar = User.avatar_url(followee)
+ name = followee.nickname
+ id = followee.id
+
+ if !!user do
+ conn
+ |> render("follow.html", %{error: err, acct: acct, avatar: avatar, name: name, id: id})
+ else
+ conn
+ |> render("follow_login.html", %{
+ error: false,
+ acct: acct,
+ avatar: avatar,
+ name: name,
+ id: id
+ })
+ end
+ end
+ end
+
+ defp is_status?(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
+
+ _ ->
+ false
end
end
def do_remote_follow(conn, %{
"authorization" => %{"name" => username, "password" => password, "id" => id}
}) do
- followee = Repo.get(User, id)
+ followee = User.get_cached_by_id(id)
avatar = User.avatar_url(followee)
name = followee.nickname
with %User{} = user <- User.get_cached_by_nickname(username),
true <- Pbkdf2.checkpw(password, user.password_hash),
- %User{} = _followed <- Repo.get(User, id),
+ %User{} = _followed <- User.get_cached_by_id(id),
{:ok, follower} <- User.follow(user, followee),
{:ok, _activity} <- ActivityPub.follow(follower, followee) do
conn
@@ -123,7 +141,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}}) do
- with %User{} = followee <- Repo.get(User, id),
+ with %User{} = followee <- User.get_cached_by_id(id),
{:ok, follower} <- User.follow(user, followee),
{:ok, _activity} <- ActivityPub.follow(follower, followee) do
conn
@@ -142,6 +160,17 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
+ def notifications_read(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
+ with {:ok, _} <- Notification.read_one(user, notification_id) do
+ json(conn, %{status: "success"})
+ else
+ {:error, message} ->
+ conn
+ |> put_resp_content_type("application/json")
+ |> send_resp(403, Jason.encode!(%{"error" => message}))
+ end
+ end
+
def config(conn, _params) do
instance = Pleroma.Config.get(:instance)
instance_fe = Pleroma.Config.get(:fe)
@@ -185,7 +214,9 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
vapidPublicKey: vapid_public_key,
accountActivationRequired:
if(Keyword.get(instance, :account_activation_required, false), do: "1", else: "0"),
- invitesEnabled: if(Keyword.get(instance, :invites_enabled, false), do: "1", else: "0")
+ invitesEnabled: if(Keyword.get(instance, :invites_enabled, false), do: "1", else: "0"),
+ safeDMMentionsEnabled:
+ if(Pleroma.Config.get([:instance, :safe_dm_mentions]), do: "1", else: "0")
}
pleroma_fe =
@@ -252,7 +283,20 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
def emoji(conn, _params) do
- json(conn, Enum.into(Emoji.get_all(), %{}))
+ emoji =
+ Emoji.get_all()
+ |> Enum.map(fn {short_code, path, tags} ->
+ {short_code, %{image_url: path, tags: tags}}
+ end)
+ |> Enum.into(%{})
+
+ json(conn, emoji)
+ end
+
+ def update_notificaton_settings(%{assigns: %{user: user}} = conn, params) do
+ with {:ok, _} <- User.update_notification_settings(user, params) do
+ json(conn, %{status: "success"})
+ end
end
def follow_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
@@ -260,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
@@ -314,4 +363,22 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
def captcha(conn, _params) do
json(conn, Pleroma.Captcha.new())
end
+
+ def healthcheck(conn, _params) do
+ info =
+ if Pleroma.Config.get([:instance, :healthcheck]) do
+ Pleroma.Healthcheck.system_info()
+ else
+ %{}
+ end
+
+ conn =
+ if info[:healthy] do
+ conn
+ else
+ Plug.Conn.put_status(conn, :service_unavailable)
+ end
+
+ json(conn, info)
+ end
end
diff --git a/lib/pleroma/web/twitter_api/representers/activity_representer.ex b/lib/pleroma/web/twitter_api/representers/activity_representer.ex
deleted file mode 100644
index 55c612ddd..000000000
--- a/lib/pleroma/web/twitter_api/representers/activity_representer.ex
+++ /dev/null
@@ -1,15 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-# FIXME: Remove this module?
-# THIS MODULE IS DEPRECATED! DON'T USE IT!
-# USE THE Pleroma.Web.TwitterAPI.Views.ActivityView MODULE!
-defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
- def to_map(activity, opts) do
- Pleroma.Web.TwitterAPI.ActivityView.render(
- "activity.json",
- Map.put(opts, :activity, activity)
- )
- end
-end
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index feadf8670..3a7774647 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -4,11 +4,10 @@
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
alias Pleroma.Activity
- alias Pleroma.Mailer
- alias Pleroma.Object
+ 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
@@ -21,7 +20,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
end
def delete(%User{} = user, id) do
- with %Activity{data: %{"type" => _type}} <- Repo.get(Activity, id),
+ with %Activity{data: %{"type" => _type}} <- Activity.get_by_id(id),
{:ok, activity} <- CommonAPI.delete(id, user) do
{:ok, activity}
end
@@ -130,7 +129,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
end
def register_user(params) do
- token_string = params["token"]
+ token = params["token"]
params = %{
nickname: params["nickname"],
@@ -164,36 +163,49 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
{:error, %{error: Jason.encode!(%{captcha: [error]})}}
else
registrations_open = Pleroma.Config.get([:instance, :registrations_open])
+ registration_process(registrations_open, params, token)
+ end
+ end
- # no need to query DB if registration is open
- token =
- unless registrations_open || is_nil(token_string) do
- Repo.get_by(UserInviteToken, %{token: token_string})
- end
+ defp registration_process(registration_open, params, token)
+ when registration_open == false or is_nil(registration_open) do
+ invite =
+ unless is_nil(token) do
+ Repo.get_by(UserInviteToken, %{token: token})
+ end
- cond do
- registrations_open || (!is_nil(token) && !token.used) ->
- changeset = User.register_changeset(%User{}, params)
+ valid_invite? = invite && UserInviteToken.valid_invite?(invite)
- with {:ok, user} <- User.register(changeset) do
- !registrations_open && UserInviteToken.mark_as_used(token.token)
+ case invite do
+ nil ->
+ {:error, "Invalid token"}
- {:ok, user}
- else
- {:error, changeset} ->
- errors =
- Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
- |> Jason.encode!()
+ invite when valid_invite? ->
+ UserInviteToken.update_usage!(invite)
+ create_user(params)
- {:error, %{error: errors}}
- end
+ _ ->
+ {:error, "Expired token"}
+ end
+ end
- !registrations_open && is_nil(token) ->
- {:error, "Invalid token"}
+ defp registration_process(true, params, _token) do
+ create_user(params)
+ end
- !registrations_open && token.used ->
- {:error, "Expired token"}
- end
+ defp create_user(params) do
+ changeset = User.register_changeset(%User{}, params)
+
+ case User.register(changeset) do
+ {:ok, user} ->
+ {:ok, user}
+
+ {:error, changeset} ->
+ errors =
+ Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
+ |> Jason.encode!()
+
+ {:error, %{error: errors}}
end
end
@@ -228,12 +240,9 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
end
%{"screen_name" => nickname} ->
- case target = Repo.get_by(User, nickname: nickname) do
- nil ->
- {:error, "No user with such screen_name"}
-
- _ ->
- {:ok, target}
+ case User.get_cached_by_nickname(nickname) do
+ nil -> {:error, "No user with such screen_name"}
+ target -> {:ok, target}
end
_ ->
@@ -257,6 +266,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
defp parse_int(_, default), do: default
+ # TODO: unify the search query with MastoAPI one and do only pagination here
def search(_user, %{"q" => query} = params) do
limit = parse_int(params["rpp"], 20)
page = parse_int(params["page"], 1)
@@ -264,13 +274,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
q =
from(
- a in Activity,
+ [a, o] in Activity.with_preloaded_object(Activity),
where: fragment("?->>'type' = 'Create'", a.data),
where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
where:
fragment(
- "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
- a.data,
+ "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
+ o.data,
^query
),
limit: ^limit,
@@ -282,35 +292,6 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
_activities = Repo.all(q)
end
- # DEPRECATED mostly, context objects are now created at insertion time.
- def context_to_conversation_id(context) do
- with %Object{id: id} <- Object.get_cached_by_ap_id(context) do
- id
- else
- _e ->
- changeset = Object.context_mapping(context)
-
- case Repo.insert(changeset) do
- {:ok, %{id: id}} ->
- id
-
- # This should be solved by an upsert, but it seems ecto
- # has problems accessing the constraint inside the jsonb.
- {:error, _} ->
- Object.get_cached_by_ap_id(context).id
- end
- end
- end
-
- def conversation_id_to_context(id) do
- with %Object{data: %{"id" => context}} <- Repo.get(Object, id) do
- context
- else
- _e ->
- {:error, "No such conversation"}
- end
- end
-
def get_external_profile(for_user, uri) do
with {:ok, %User{} = user} <- User.get_or_fetch(uri) do
{:ok, UserView.render("show.json", %{user: user, for: for_user})}
diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
index 6ea0b110b..79ed9dad2 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -16,6 +16,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.ActivityView
alias Pleroma.Web.TwitterAPI.NotificationView
@@ -269,7 +270,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Repo.get(Activity, id),
+ with %Activity{} = activity <- Activity.get_by_id(id),
true <- Visibility.visible_for_user?(activity, user) do
conn
|> put_view(ActivityView)
@@ -278,7 +279,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with context when is_binary(context) <- TwitterAPI.conversation_id_to_context(id),
+ with context when is_binary(context) <- Utils.conversation_id_to_context(id),
activities <-
ActivityPub.fetch_activities_for_context(context, %{
"blocking_user" => user,
@@ -341,7 +342,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def get_by_id_or_ap_id(id) do
- activity = Repo.get(Activity, id) || Activity.get_create_by_object_ap_id(id)
+ activity = Activity.get_by_id(id) || Activity.get_create_by_object_ap_id(id)
if activity.data["type"] == "Create" do
activity
@@ -433,7 +434,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
- with %User{} = user <- Repo.get(User, uid),
+ with %User{} = user <- User.get_cached_by_id(uid),
true <- user.local,
true <- user.info.confirmation_pending,
true <- user.info.confirmation_token == token,
@@ -586,7 +587,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
def approve_friend_request(conn, %{"user_id" => uid} = _params) do
with followed <- conn.assigns[:user],
- %User{} = follower <- Repo.get(User, uid),
+ %User{} = follower <- User.get_cached_by_id(uid),
{:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
conn
|> put_view(UserView)
@@ -598,7 +599,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
def deny_friend_request(conn, %{"user_id" => uid} = _params) do
with followed <- conn.assigns[:user],
- %User{} = follower <- Repo.get(User, uid),
+ %User{} = follower <- User.get_cached_by_id(uid),
{:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
conn
|> put_view(UserView)
@@ -631,7 +632,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
defp build_info_cng(user, params) do
info_params =
- ["no_rich_text", "locked", "hide_followers", "hide_follows", "show_role"]
+ ["no_rich_text", "locked", "hide_followers", "hide_follows", "hide_favorites", "show_role"]
|> Enum.reduce(%{}, fn key, res ->
if value = params[key] do
Map.put(res, key, value == "true")
diff --git a/lib/pleroma/web/twitter_api/views/activity_view.ex b/lib/pleroma/web/twitter_api/views/activity_view.ex
index 4926f007e..c64152da8 100644
--- a/lib/pleroma/web/twitter_api/views/activity_view.ex
+++ b/lib/pleroma/web/twitter_api/views/activity_view.ex
@@ -15,7 +15,6 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.TwitterAPI.ActivityView
alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter
- alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Pleroma.Web.TwitterAPI.UserView
import Ecto.Query
@@ -78,7 +77,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
defp get_context_id(%{data: %{"context" => context}}, options) do
cond do
id = options[:context_ids][context] -> id
- true -> TwitterAPI.context_to_conversation_id(context)
+ true -> Utils.context_to_conversation_id(context)
end
end
@@ -225,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 =
@@ -246,27 +247,29 @@ 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
- |> HTML.get_cached_scrubbed_html_for_object(
+ |> HTML.get_cached_scrubbed_html_for_activity(
User.html_filter_policy(opts[:for]),
activity,
- __MODULE__
+ "twitterapi:content"
)
- |> Formatter.emojify(object["emoji"])
+ |> Formatter.emojify(object.data["emoji"])
text =
if content do
content
|> String.replace(~r/<br\s?\/?>/, "\n")
- |> HTML.get_cached_stripped_html_for_object(activity, __MODULE__)
+ |> HTML.get_cached_stripped_html_for_activity(activity, "twitterapi:content")
+ else
+ ""
end
reply_parent = Activity.get_in_reply_to_activity(activity)
@@ -283,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)
}
diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex
index 0791ed760..ea015b8f0 100644
--- a/lib/pleroma/web/twitter_api/views/user_view.ex
+++ b/lib/pleroma/web/twitter_api/views/user_view.ex
@@ -74,58 +74,49 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
- data = %{
- "created_at" => user.inserted_at |> Utils.format_naive_asctime(),
- "description" => HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")),
- "description_html" => HTML.filter_tags(user.bio, User.html_filter_policy(for_user)),
- "favourites_count" => 0,
- "followers_count" => user_info[:follower_count],
- "following" => following,
- "follows_you" => follows_you,
- "statusnet_blocking" => statusnet_blocking,
- "friends_count" => user_info[:following_count],
- "id" => user.id,
- "name" => user.name || user.nickname,
- "name_html" =>
- if(user.name,
- do: HTML.strip_tags(user.name) |> Formatter.emojify(emoji),
- else: user.nickname
- ),
- "profile_image_url" => image,
- "profile_image_url_https" => image,
- "profile_image_url_profile_size" => image,
- "profile_image_url_original" => image,
- "rights" => %{
- "delete_others_notice" => !!user.info.is_moderator,
- "admin" => !!user.info.is_admin
- },
- "screen_name" => user.nickname,
- "statuses_count" => user_info[:note_count],
- "statusnet_profile_url" => user.ap_id,
- "cover_photo" => User.banner_url(user) |> MediaProxy.url(),
- "background_image" => image_url(user.info.background) |> MediaProxy.url(),
- "is_local" => user.local,
- "locked" => user.info.locked,
- "default_scope" => user.info.default_scope,
- "no_rich_text" => user.info.no_rich_text,
- "hide_followers" => user.info.hide_followers,
- "hide_follows" => user.info.hide_follows,
- "fields" => fields,
-
- # Pleroma extension
- "pleroma" =>
- %{
- "confirmation_pending" => user_info.confirmation_pending,
- "tags" => user.tags
- }
- |> maybe_with_activation_status(user, for_user)
- }
-
data =
- if(user.info.is_admin || user.info.is_moderator,
- do: maybe_with_role(data, user, for_user),
- else: data
- )
+ %{
+ "created_at" => user.inserted_at |> Utils.format_naive_asctime(),
+ "description" => HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")),
+ "description_html" => HTML.filter_tags(user.bio, User.html_filter_policy(for_user)),
+ "favourites_count" => 0,
+ "followers_count" => user_info[:follower_count],
+ "following" => following,
+ "follows_you" => follows_you,
+ "statusnet_blocking" => statusnet_blocking,
+ "friends_count" => user_info[:following_count],
+ "id" => user.id,
+ "name" => user.name || user.nickname,
+ "name_html" =>
+ if(user.name,
+ do: HTML.strip_tags(user.name) |> Formatter.emojify(emoji),
+ else: user.nickname
+ ),
+ "profile_image_url" => image,
+ "profile_image_url_https" => image,
+ "profile_image_url_profile_size" => image,
+ "profile_image_url_original" => image,
+ "screen_name" => user.nickname,
+ "statuses_count" => user_info[:note_count],
+ "statusnet_profile_url" => user.ap_id,
+ "cover_photo" => User.banner_url(user) |> MediaProxy.url(),
+ "background_image" => image_url(user.info.background) |> MediaProxy.url(),
+ "is_local" => user.local,
+ "locked" => user.info.locked,
+ "hide_followers" => user.info.hide_followers,
+ "hide_follows" => user.info.hide_follows,
+ "fields" => fields,
+
+ # Pleroma extension
+ "pleroma" =>
+ %{
+ "confirmation_pending" => user_info.confirmation_pending,
+ "tags" => user.tags
+ }
+ |> maybe_with_activation_status(user, for_user)
+ }
+ |> maybe_with_user_settings(user, for_user)
+ |> maybe_with_role(user, for_user)
if assigns[:token] do
Map.put(data, "token", token_string(assigns[:token]))
@@ -141,15 +132,35 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
defp maybe_with_activation_status(data, _, _), do: data
defp maybe_with_role(data, %User{id: id} = user, %User{id: id}) do
- Map.merge(data, %{"role" => role(user), "show_role" => user.info.show_role})
+ Map.merge(data, %{
+ "role" => role(user),
+ "show_role" => user.info.show_role,
+ "rights" => %{
+ "delete_others_notice" => !!user.info.is_moderator,
+ "admin" => !!user.info.is_admin
+ }
+ })
end
defp maybe_with_role(data, %User{info: %{show_role: true}} = user, _user) do
- Map.merge(data, %{"role" => role(user)})
+ Map.merge(data, %{
+ "role" => role(user),
+ "rights" => %{
+ "delete_others_notice" => !!user.info.is_moderator,
+ "admin" => !!user.info.is_admin
+ }
+ })
end
defp maybe_with_role(data, _, _), do: data
+ defp maybe_with_user_settings(data, %User{info: info, id: id} = _user, %User{id: id}) do
+ data
+ |> Kernel.put_in(["default_scope"], info.default_scope)
+ |> Kernel.put_in(["no_rich_text"], info.no_rich_text)
+ end
+
+ defp maybe_with_user_settings(data, _, _), do: data
defp role(%User{info: %{:is_admin => true}}), do: "admin"
defp role(%User{info: %{:is_moderator => true}}), do: "moderator"
defp role(_), do: "member"
diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex
index 32c3455f5..a3b0bf999 100644
--- a/lib/pleroma/web/web_finger/web_finger.ex
+++ b/lib/pleroma/web/web_finger/web_finger.ex
@@ -37,7 +37,7 @@ defmodule Pleroma.Web.WebFinger do
regex = ~r/(acct:)?(?<username>\w+)@#{host}/
with %{"username" => username} <- Regex.named_captures(regex, resource),
- %User{} = user <- User.get_by_nickname(username) do
+ %User{} = user <- User.get_cached_by_nickname(username) do
{:ok, represent_user(user, fmt)}
else
_e ->
diff --git a/lib/pleroma/web/websub/websub_client_subscription.ex b/lib/pleroma/web/websub/websub_client_subscription.ex
index 969ee0684..77703c496 100644
--- a/lib/pleroma/web/websub/websub_client_subscription.ex
+++ b/lib/pleroma/web/websub/websub_client_subscription.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.Websub.WebsubClientSubscription do
schema "websub_client_subscriptions" do
field(:topic, :string)
field(:secret, :string)
- field(:valid_until, :naive_datetime)
+ field(:valid_until, :naive_datetime_usec)
field(:state, :string)
field(:subscribers, {:array, :string}, default: [])
field(:hub, :string)