Merge branch 'develop' into feature/matstodon-statuses-by-name

This commit is contained in:
Mark Felder 2019-07-15 17:10:27 -05:00
commit ffb4eb9779
872 changed files with 19751 additions and 4409 deletions

View file

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Healthcheck do
@moduledoc """
Module collects metrics about app and assign healthy status.
@ -29,13 +33,13 @@ defmodule Pleroma.Healthcheck do
end
defp assign_db_info(healthcheck) do
database = Application.get_env(:pleroma, Repo)[:database]
database = Pleroma.Config.get([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]
pool_size = Pleroma.Config.get([Repo, :pool_size])
db_info =
Enum.reduce(result.rows, %{active: 0, idle: 0}, fn [state, cnt], states ->

View file

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
Postgrex.Types.define(
Pleroma.PostgresTypes,
[] ++ Ecto.Adapters.Postgres.extensions(),

67
lib/mix/pleroma.ex Normal file
View file

@ -0,0 +1,67 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Pleroma do
@doc "Common functions to be reused in mix tasks"
def start_pleroma do
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
{:ok, _} = Application.ensure_all_started(:pleroma)
end
def load_pleroma do
Application.load(:pleroma)
end
def get_option(options, opt, prompt, defval \\ nil, defname \\ nil) do
Keyword.get(options, opt) || shell_prompt(prompt, defval, defname)
end
def shell_prompt(prompt, defval \\ nil, defname \\ nil) do
prompt_message = "#{prompt} [#{defname || defval}] "
input =
if mix_shell?(),
do: Mix.shell().prompt(prompt_message),
else: :io.get_line(prompt_message)
case input do
"\n" ->
case defval do
nil ->
shell_prompt(prompt, defval, defname)
defval ->
defval
end
input ->
String.trim(input)
end
end
def shell_yes?(message) do
if mix_shell?(),
do: Mix.shell().yes?("Continue?"),
else: shell_prompt(message, "Continue?") in ~w(Yn Y y)
end
def shell_info(message) do
if mix_shell?(),
do: Mix.shell().info(message),
else: IO.puts(message)
end
def shell_error(message) do
if mix_shell?(),
do: Mix.shell().error(message),
else: IO.puts(:stderr, message)
end
@doc "Performs a safe check whether `Mix.shell/0` is available (does not raise if Mix is not loaded)"
def mix_shell?, do: :erlang.function_exported(Mix, :shell, 0)
def escape_sh_path(path) do
~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(')
end
end

View file

@ -1,19 +1,23 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Benchmark do
import Mix.Pleroma
use Mix.Task
alias Mix.Tasks.Pleroma.Common
def run(["search"]) do
Common.start_pleroma()
start_pleroma()
Benchee.run(%{
"search" => fn ->
Pleroma.Web.MastodonAPI.MastodonAPIController.status_search(nil, "cofe")
Pleroma.Activity.search(nil, "cofe")
end
})
end
def run(["tag"]) do
Common.start_pleroma()
start_pleroma()
Benchee.run(%{
"tag" => fn ->

View file

@ -1,28 +0,0 @@
# 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.Common do
@doc "Common functions to be reused in mix tasks"
def start_pleroma do
Mix.Task.run("app.start")
end
def get_option(options, opt, prompt, defval \\ nil, defname \\ nil) do
Keyword.get(options, opt) ||
case Mix.shell().prompt("#{prompt} [#{defname || defval}]") do
"\n" ->
case defval do
nil -> get_option(options, opt, prompt, defval)
defval -> defval
end
opt ->
opt |> String.trim()
end
end
def escape_sh_path(path) do
~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(')
end
end

View file

@ -0,0 +1,83 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Config do
use Mix.Task
import Mix.Pleroma
alias Pleroma.Repo
alias Pleroma.Web.AdminAPI.Config
@shortdoc "Manages the location of the config"
@moduledoc """
Manages the location of the config.
## Transfers config from file to DB.
mix pleroma.config migrate_to_db
## Transfers config from DB to file.
mix pleroma.config migrate_from_db ENV
"""
def run(["migrate_to_db"]) do
start_pleroma()
if Pleroma.Config.get([:instance, :dynamic_configuration]) do
Application.get_all_env(:pleroma)
|> Enum.reject(fn {k, _v} -> k in [Pleroma.Repo, :env] end)
|> Enum.each(fn {k, v} ->
key = to_string(k) |> String.replace("Elixir.", "")
key =
if String.starts_with?(key, "Pleroma.") do
key
else
":" <> key
end
{:ok, _} = Config.update_or_create(%{group: "pleroma", key: key, value: v})
Mix.shell().info("#{key} is migrated.")
end)
Mix.shell().info("Settings migrated.")
else
Mix.shell().info(
"Migration is not allowed by config. You can change this behavior in instance settings."
)
end
end
def run(["migrate_from_db", env, delete?]) do
start_pleroma()
delete? = if delete? == "true", do: true, else: false
if Pleroma.Config.get([:instance, :dynamic_configuration]) do
config_path = "config/#{env}.exported_from_db.secret.exs"
{:ok, file} = File.open(config_path, [:write])
IO.write(file, "use Mix.Config\r\n")
Repo.all(Config)
|> Enum.each(fn config ->
IO.write(
file,
"config :#{config.group}, #{config.key}, #{inspect(Config.from_binary(config.value))}\r\n\r\n"
)
if delete? do
{:ok, _} = Repo.delete(config)
Mix.shell().info("#{config.key} deleted from DB.")
end
end)
File.close(file)
System.cmd("mix", ["format", config_path])
else
Mix.shell().info(
"Migration is not allowed by config. You can change this behavior in instance settings."
)
end
end
end

View file

@ -3,12 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Database do
alias Mix.Tasks.Pleroma.Common
alias Pleroma.Conversation
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
require Logger
import Mix.Pleroma
use Mix.Task
@shortdoc "A collection of database related tasks"
@ -45,7 +45,7 @@ defmodule Mix.Tasks.Pleroma.Database do
]
)
Common.start_pleroma()
start_pleroma()
Logger.info("Removing embedded objects")
Repo.query!(
@ -66,12 +66,12 @@ defmodule Mix.Tasks.Pleroma.Database do
end
def run(["bump_all_conversations"]) do
Common.start_pleroma()
start_pleroma()
Conversation.bump_for_all_activities()
end
def run(["update_users_following_followers_counts"]) do
Common.start_pleroma()
start_pleroma()
users = Repo.all(User)
Enum.each(users, &User.remove_duplicated_following/1)
@ -89,7 +89,7 @@ defmodule Mix.Tasks.Pleroma.Database do
]
)
Common.start_pleroma()
start_pleroma()
deadline = Pleroma.Config.get([:instance, :remote_post_retention_days])

View file

@ -0,0 +1,50 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-onl
defmodule Mix.Tasks.Pleroma.Ecto do
@doc """
Ensures the given repository's migrations path exists on the file system.
"""
@spec ensure_migrations_path(Ecto.Repo.t(), Keyword.t()) :: String.t()
def ensure_migrations_path(repo, opts) do
path = opts[:migrations_path] || Path.join(source_repo_priv(repo), "migrations")
path =
case Path.type(path) do
:relative ->
Path.join(Application.app_dir(:pleroma), path)
:absolute ->
path
end
if not File.dir?(path) do
raise_missing_migrations(Path.relative_to_cwd(path), repo)
end
path
end
@doc """
Returns the private repository path relative to the source.
"""
def source_repo_priv(repo) do
config = repo.config()
priv = config[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}"
Path.join(Application.app_dir(:pleroma), priv)
end
defp raise_missing_migrations(path, repo) do
raise("""
Could not find migrations directory #{inspect(path)}
for repo #{inspect(repo)}.
This may be because you are in a new project and the
migration directory has not been created yet. Creating an
empty directory at the path above will fix this error.
If you expected existing migrations to be found, please
make sure your repository has been properly configured
and the configured path exists.
""")
end
end

View file

@ -0,0 +1,63 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-onl
defmodule Mix.Tasks.Pleroma.Ecto.Migrate do
use Mix.Task
import Mix.Pleroma
require Logger
@shortdoc "Wrapper on `ecto.migrate` task."
@aliases [
n: :step,
v: :to
]
@switches [
all: :boolean,
step: :integer,
to: :integer,
quiet: :boolean,
log_sql: :boolean,
strict_version_order: :boolean,
migrations_path: :string
]
@moduledoc """
Changes `Logger` level to `:info` before start migration.
Changes level back when migration ends.
## Start migration
mix pleroma.ecto.migrate [OPTIONS]
Options:
- see https://hexdocs.pm/ecto/2.0.0/Mix.Tasks.Ecto.Migrate.html
"""
@impl true
def run(args \\ []) do
load_pleroma()
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
opts =
if opts[:to] || opts[:step] || opts[:all],
do: opts,
else: Keyword.put(opts, :all, true)
opts =
if opts[:quiet],
do: Keyword.merge(opts, log: false, log_sql: false),
else: opts
path = Mix.Tasks.Pleroma.Ecto.ensure_migrations_path(Pleroma.Repo, opts)
level = Logger.level()
Logger.configure(level: :info)
{:ok, _, _} = Ecto.Migrator.with_repo(Pleroma.Repo, &Ecto.Migrator.run(&1, path, :up, opts))
Logger.configure(level: level)
end
end

View file

@ -0,0 +1,67 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-onl
defmodule Mix.Tasks.Pleroma.Ecto.Rollback do
use Mix.Task
import Mix.Pleroma
require Logger
@shortdoc "Wrapper on `ecto.rollback` task"
@aliases [
n: :step,
v: :to
]
@switches [
all: :boolean,
step: :integer,
to: :integer,
start: :boolean,
quiet: :boolean,
log_sql: :boolean,
migrations_path: :string
]
@moduledoc """
Changes `Logger` level to `:info` before start rollback.
Changes level back when rollback ends.
## Start rollback
mix pleroma.ecto.rollback
Options:
- see https://hexdocs.pm/ecto/2.0.0/Mix.Tasks.Ecto.Rollback.html
"""
@impl true
def run(args \\ []) do
load_pleroma()
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
opts =
if opts[:to] || opts[:step] || opts[:all],
do: opts,
else: Keyword.put(opts, :step, 1)
opts =
if opts[:quiet],
do: Keyword.merge(opts, log: false, log_sql: false),
else: opts
path = Mix.Tasks.Pleroma.Ecto.ensure_migrations_path(Pleroma.Repo, opts)
level = Logger.level()
Logger.configure(level: :info)
if Pleroma.Config.get(:env) == :test do
Logger.info("Rollback succesfully")
else
{:ok, _, _} =
Ecto.Migrator.with_repo(Pleroma.Repo, &Ecto.Migrator.run(&1, path, :down, opts))
end
Logger.configure(level: level)
end
end

View file

@ -55,15 +55,13 @@ defmodule Mix.Tasks.Pleroma.Emoji do
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)
fetch_manifest(if options[:manifest], do: options[:manifest], else: default_manifest())
Enum.each(manifest, fn {name, info} ->
to_print = [
@ -88,7 +86,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do
{options, pack_names, []} = parse_global_opts(args)
manifest_url = if options[:manifest], do: options[:manifest], else: @default_manifest
manifest_url = if options[:manifest], do: options[:manifest], else: default_manifest()
manifest = fetch_manifest(manifest_url)
@ -298,4 +296,6 @@ defmodule Mix.Tasks.Pleroma.Emoji do
Tesla.client(middleware)
end
defp default_manifest, do: Pleroma.Config.get!([:emoji, :default_manifest])
end

View file

@ -4,7 +4,7 @@
defmodule Mix.Tasks.Pleroma.Instance do
use Mix.Task
alias Mix.Tasks.Pleroma.Common
import Mix.Pleroma
@shortdoc "Manages Pleroma instance"
@moduledoc """
@ -29,7 +29,13 @@ defmodule Mix.Tasks.Pleroma.Instance do
- `--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
- `--rum Y/N` - Whether to enable RUM indexes
- `--indexable Y/N` - Allow/disallow indexing site by search engines
- `--db-configurable Y/N` - Allow/disallow configuring instance from admin part
- `--uploads-dir` - the directory uploads go in when using a local uploader
- `--static-dir` - the directory custom public files should be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)
- `--listen-ip` - the ip the app should listen to, defaults to 127.0.0.1
- `--listen-port` - the port the app should listen to, defaults to 4000
"""
def run(["gen" | rest]) do
@ -48,7 +54,13 @@ defmodule Mix.Tasks.Pleroma.Instance do
dbname: :string,
dbuser: :string,
dbpass: :string,
indexable: :string
rum: :string,
indexable: :string,
db_configurable: :string,
uploads_dir: :string,
static_dir: :string,
listen_ip: :string,
listen_port: :string
],
aliases: [
o: :output,
@ -68,7 +80,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
if proceed? do
[domain, port | _] =
String.split(
Common.get_option(
get_option(
options,
:domain,
"What domain will your instance use? (e.g pleroma.soykaf.com)"
@ -77,16 +89,16 @@ defmodule Mix.Tasks.Pleroma.Instance do
) ++ [443]
name =
Common.get_option(
get_option(
options,
:instance_name,
"What is the name of your instance? (e.g. Pleroma/Soykaf)"
)
email = Common.get_option(options, :admin_email, "What is your admin email address?")
email = get_option(options, :admin_email, "What is your admin email address?")
notify_email =
Common.get_option(
get_option(
options,
:notify_email,
"What email address do you want to use for sending email notifications?",
@ -94,21 +106,27 @@ defmodule Mix.Tasks.Pleroma.Instance do
)
indexable =
Common.get_option(
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")
db_configurable? =
get_option(
options,
:db_configurable,
"Do you want to store the configuration in the database (allows controlling it from admin-fe)? (y/n)",
"n"
) === "y"
dbname =
Common.get_option(options, :dbname, "What is the name of your database?", "pleroma_dev")
dbhost = get_option(options, :dbhost, "What is the hostname of your database?", "localhost")
dbname = get_option(options, :dbname, "What is the name of your database?", "pleroma")
dbuser =
Common.get_option(
get_option(
options,
:dbuser,
"What is the user used to connect to your database?",
@ -116,7 +134,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
)
dbpass =
Common.get_option(
get_option(
options,
:dbpass,
"What is the password used to connect to your database?",
@ -124,13 +142,54 @@ defmodule Mix.Tasks.Pleroma.Instance do
"autogenerated"
)
rum_enabled =
get_option(
options,
:rum,
"Would you like to use RUM indices?",
"n"
) === "y"
listen_port =
get_option(
options,
:listen_port,
"What port will the app listen to (leave it if you are using the default setup with nginx)?",
4000
)
listen_ip =
get_option(
options,
:listen_ip,
"What ip will the app listen to (leave it if you are using the default setup with nginx)?",
"127.0.0.1"
)
uploads_dir =
get_option(
options,
:uploads_dir,
"What directory should media uploads go in (when using the local uploader)?",
Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads])
)
static_dir =
get_option(
options,
:static_dir,
"What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?",
Pleroma.Config.get([:instance, :static_dir])
)
secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
{web_push_public_key, web_push_private_key} = :crypto.generate_key(:ecdh, :prime256v1)
template_dir = Application.app_dir(:pleroma, "priv") <> "/templates"
result_config =
EEx.eval_file(
"sample_config.eex" |> Path.expand(__DIR__),
template_dir <> "/sample_config.eex",
domain: domain,
port: port,
email: email,
@ -140,46 +199,40 @@ defmodule Mix.Tasks.Pleroma.Instance do
dbname: dbname,
dbuser: dbuser,
dbpass: dbpass,
version: Pleroma.Mixfile.project() |> Keyword.get(:version),
secret: secret,
signing_salt: signing_salt,
web_push_public_key: Base.url_encode64(web_push_public_key, padding: false),
web_push_private_key: Base.url_encode64(web_push_private_key, padding: false)
web_push_private_key: Base.url_encode64(web_push_private_key, padding: false),
db_configurable?: db_configurable?,
static_dir: static_dir,
uploads_dir: uploads_dir,
rum_enabled: rum_enabled,
listen_ip: listen_ip,
listen_port: listen_port
)
result_psql =
EEx.eval_file(
"sample_psql.eex" |> Path.expand(__DIR__),
template_dir <> "/sample_psql.eex",
dbname: dbname,
dbuser: dbuser,
dbpass: dbpass
dbpass: dbpass,
rum_enabled: rum_enabled
)
Mix.shell().info(
"Writing config to #{config_path}. You should rename it to config/prod.secret.exs or config/dev.secret.exs."
)
shell_info("Writing config to #{config_path}.")
File.write(config_path, result_config)
Mix.shell().info("Writing #{psql_path}.")
shell_info("Writing the postgres script to #{psql_path}.")
File.write(psql_path, result_psql)
write_robots_txt(indexable)
write_robots_txt(indexable, template_dir)
Mix.shell().info(
"\n" <>
"""
To get started:
1. Verify the contents of the generated files.
2. Run `sudo -u postgres psql -f #{Common.escape_sh_path(psql_path)}`.
""" <>
if config_path in ["config/dev.secret.exs", "config/prod.secret.exs"] do
""
else
"3. Run `mv #{Common.escape_sh_path(config_path)} 'config/prod.secret.exs'`."
end
shell_info(
"\n All files successfully written! Refer to the installation instructions for your platform for next steps"
)
else
Mix.shell().error(
shell_error(
"The task would have overwritten the following files:\n" <>
(Enum.map(paths, &"- #{&1}\n") |> Enum.join("")) <>
"Rerun with `--force` to overwrite them."
@ -187,10 +240,10 @@ defmodule Mix.Tasks.Pleroma.Instance do
end
end
defp write_robots_txt(indexable) do
defp write_robots_txt(indexable, template_dir) do
robots_txt =
EEx.eval_file(
Path.expand("robots_txt.eex", __DIR__),
template_dir <> "/robots_txt.eex",
indexable: indexable
)
@ -204,10 +257,10 @@ defmodule Mix.Tasks.Pleroma.Instance do
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")
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}.")
shell_info("Writing #{robots_txt_path}.")
end
end

View file

@ -4,7 +4,7 @@
defmodule Mix.Tasks.Pleroma.Relay do
use Mix.Task
alias Mix.Tasks.Pleroma.Common
import Mix.Pleroma
alias Pleroma.Web.ActivityPub.Relay
@shortdoc "Manages remote relays"
@ -24,24 +24,24 @@ defmodule Mix.Tasks.Pleroma.Relay do
Example: ``mix pleroma.relay unfollow https://example.org/relay``
"""
def run(["follow", target]) do
Common.start_pleroma()
start_pleroma()
with {:ok, _activity} <- Relay.follow(target) do
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
else
{:error, e} -> Mix.shell().error("Error while following #{target}: #{inspect(e)}")
{:error, e} -> shell_error("Error while following #{target}: #{inspect(e)}")
end
end
def run(["unfollow", target]) do
Common.start_pleroma()
start_pleroma()
with {:ok, _activity} <- Relay.unfollow(target) do
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
else
{:error, e} -> Mix.shell().error("Error while following #{target}: #{inspect(e)}")
{:error, e} -> shell_error("Error while following #{target}: #{inspect(e)}")
end
end
end

View file

@ -1,2 +0,0 @@
User-Agent: *
Disallow: <%= if indexable, do: "", else: "/" %>

View file

@ -1,78 +0,0 @@
# Pleroma instance configuration
# NOTE: This file should not be committed to a repo or otherwise made public
# without removing sensitive information.
use Mix.Config
config :pleroma, Pleroma.Web.Endpoint,
url: [host: "<%= domain %>", scheme: "https", port: <%= port %>],
secret_key_base: "<%= secret %>",
signing_salt: "<%= signing_salt %>"
config :pleroma, :instance,
name: "<%= name %>",
email: "<%= email %>",
notify_email: "<%= notify_email %>",
limit: 5000,
registrations_open: true,
dedupe_media: false
config :pleroma, :media_proxy,
enabled: false,
redirect_on_failure: true
#base_url: "https://cache.pleroma.social"
config :pleroma, Pleroma.Repo,
adapter: Ecto.Adapters.Postgres,
username: "<%= dbuser %>",
password: "<%= dbpass %>",
database: "<%= dbname %>",
hostname: "<%= dbhost %>",
pool_size: 10
# Configure web push notifications
config :web_push_encryption, :vapid_details,
subject: "mailto:<%= email %>",
public_key: "<%= web_push_public_key %>",
private_key: "<%= web_push_private_key %>"
# Enable Strict-Transport-Security once SSL is working:
# config :pleroma, :http_security,
# sts: true
# Configure S3 support if desired.
# The public S3 endpoint is different depending on region and provider,
# consult your S3 provider's documentation for details on what to use.
#
# config :pleroma, Pleroma.Uploaders.S3,
# bucket: "some-bucket",
# public_endpoint: "https://s3.amazonaws.com"
#
# Configure S3 credentials:
# config :ex_aws, :s3,
# access_key_id: "xxxxxxxxxxxxx",
# secret_access_key: "yyyyyyyyyyyy",
# region: "us-east-1",
# scheme: "https://"
#
# For using third-party S3 clones like wasabi, also do:
# config :ex_aws, :s3,
# host: "s3.wasabisys.com"
# Configure Openstack Swift support if desired.
#
# Many openstack deployments are different, so config is left very open with
# no assumptions made on which provider you're using. This should allow very
# wide support without needing separate handlers for OVH, Rackspace, etc.
#
# config :pleroma, Pleroma.Uploaders.Swift,
# container: "some-container",
# username: "api-username-yyyy",
# password: "api-key-xxxx",
# tenant_id: "<openstack-project/tenant-id>",
# auth_url: "https://keystone-endpoint.provider.com",
# storage_url: "https://swift-endpoint.prodider.com/v1/AUTH_<tenant>/<container>",
# object_url: "https://cdn-endpoint.provider.com/<container>"
#

View file

@ -1,7 +0,0 @@
CREATE USER <%= dbuser %> WITH ENCRYPTED PASSWORD '<%= dbpass %>';
CREATE DATABASE <%= dbname %> OWNER <%= dbuser %>;
\c <%= dbname %>;
--Extensions made by ecto.migrate that need superuser access
CREATE EXTENSION IF NOT EXISTS citext;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

View file

@ -4,7 +4,7 @@
defmodule Mix.Tasks.Pleroma.Uploads do
use Mix.Task
alias Mix.Tasks.Pleroma.Common
import Mix.Pleroma
alias Pleroma.Upload
alias Pleroma.Uploaders.Local
require Logger
@ -24,7 +24,7 @@ defmodule Mix.Tasks.Pleroma.Uploads do
"""
def run(["migrate_local", target_uploader | args]) do
delete? = Enum.member?(args, "--delete")
Common.start_pleroma()
start_pleroma()
local_path = Pleroma.Config.get!([Local, :uploads])
uploader = Module.concat(Pleroma.Uploaders, target_uploader)
@ -38,10 +38,10 @@ defmodule Mix.Tasks.Pleroma.Uploads do
Pleroma.Config.put([Upload, :uploader], uploader)
end
Mix.shell().info("Migrating files from local #{local_path} to #{to_string(uploader)}")
shell_info("Migrating files from local #{local_path} to #{to_string(uploader)}")
if delete? do
Mix.shell().info(
shell_info(
"Attention: uploaded files will be deleted, hope you have backups! (--delete ; cancel with ^C)"
)
@ -78,7 +78,7 @@ defmodule Mix.Tasks.Pleroma.Uploads do
|> Enum.filter(& &1)
total_count = length(uploads)
Mix.shell().info("Found #{total_count} uploads")
shell_info("Found #{total_count} uploads")
uploads
|> Task.async_stream(
@ -90,7 +90,7 @@ defmodule Mix.Tasks.Pleroma.Uploads do
:ok
error ->
Mix.shell().error("failed to upload #{inspect(upload.path)}: #{inspect(error)}")
shell_error("failed to upload #{inspect(upload.path)}: #{inspect(error)}")
end
end,
timeout: 150_000
@ -99,10 +99,10 @@ defmodule Mix.Tasks.Pleroma.Uploads do
# credo:disable-for-next-line Credo.Check.Warning.UnusedEnumOperation
|> Enum.reduce(0, fn done, count ->
count = count + length(done)
Mix.shell().info("Uploaded #{count}/#{total_count} files")
shell_info("Uploaded #{count}/#{total_count} files")
count
end)
Mix.shell().info("Done!")
shell_info("Done!")
end
end

View file

@ -5,9 +5,10 @@
defmodule Mix.Tasks.Pleroma.User do
use Mix.Task
import Ecto.Changeset
alias Mix.Tasks.Pleroma.Common
import Mix.Pleroma
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.OAuth
@shortdoc "Manages Pleroma users"
@moduledoc """
@ -49,6 +50,10 @@ defmodule Mix.Tasks.Pleroma.User do
mix pleroma.user delete_activities NICKNAME
## Sign user out from all applications (delete user's OAuth tokens and authorizations).
mix pleroma.user sign_out NICKNAME
## Deactivate or activate the user's account.
mix pleroma.user toggle_activated NICKNAME
@ -115,7 +120,7 @@ defmodule Mix.Tasks.Pleroma.User do
admin? = Keyword.get(options, :admin, false)
assume_yes? = Keyword.get(options, :assume_yes, false)
Mix.shell().info("""
shell_info("""
A user will be created with the following information:
- nickname: #{nickname}
- email: #{email}
@ -128,10 +133,10 @@ defmodule Mix.Tasks.Pleroma.User do
- admin: #{if(admin?, do: "true", else: "false")}
""")
proceed? = assume_yes? or Mix.shell().yes?("Continue?")
proceed? = assume_yes? or shell_yes?("Continue?")
if proceed? do
Common.start_pleroma()
start_pleroma()
params = %{
nickname: nickname,
@ -145,7 +150,7 @@ defmodule Mix.Tasks.Pleroma.User do
changeset = User.register_changeset(%User{}, params, need_confirmation: false)
{:ok, _user} = User.register(changeset)
Mix.shell().info("User #{nickname} created")
shell_info("User #{nickname} created")
if moderator? do
run(["set", nickname, "--moderator"])
@ -159,64 +164,64 @@ defmodule Mix.Tasks.Pleroma.User do
run(["reset_password", nickname])
end
else
Mix.shell().info("User will not be created.")
shell_info("User will not be created.")
end
end
def run(["rm", nickname]) do
Common.start_pleroma()
start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
User.perform(:delete, user)
Mix.shell().info("User #{nickname} deleted.")
shell_info("User #{nickname} deleted.")
else
_ ->
Mix.shell().error("No local user #{nickname}")
shell_error("No local user #{nickname}")
end
end
def run(["toggle_activated", nickname]) do
Common.start_pleroma()
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
{:ok, user} = User.deactivate(user, !user.info.deactivated)
Mix.shell().info(
shell_info(
"Activation status of #{nickname}: #{if(user.info.deactivated, do: "de", else: "")}activated"
)
else
_ ->
Mix.shell().error("No user #{nickname}")
shell_error("No user #{nickname}")
end
end
def run(["reset_password", nickname]) do
Common.start_pleroma()
start_pleroma()
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}")
shell_info("Generated password reset token for #{user.nickname}")
IO.puts(
"URL: #{
Pleroma.Web.Router.Helpers.util_url(
Pleroma.Web.Router.Helpers.reset_password_url(
Pleroma.Web.Endpoint,
:show_password_reset,
:reset,
token.token
)
}"
)
else
_ ->
Mix.shell().error("No local user #{nickname}")
shell_error("No local user #{nickname}")
end
end
def run(["unsubscribe", nickname]) do
Common.start_pleroma()
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
Mix.shell().info("Deactivating #{user.nickname}")
shell_info("Deactivating #{user.nickname}")
User.deactivate(user)
{:ok, friends} = User.get_friends(user)
@ -224,7 +229,7 @@ defmodule Mix.Tasks.Pleroma.User do
Enum.each(friends, fn friend ->
user = User.get_cached_by_id(user.id)
Mix.shell().info("Unsubscribing #{friend.nickname} from #{user.nickname}")
shell_info("Unsubscribing #{friend.nickname} from #{user.nickname}")
User.unfollow(user, friend)
end)
@ -233,16 +238,16 @@ defmodule Mix.Tasks.Pleroma.User do
user = User.get_cached_by_id(user.id)
if Enum.empty?(user.following) do
Mix.shell().info("Successfully unsubscribed all followers from #{user.nickname}")
shell_info("Successfully unsubscribed all followers from #{user.nickname}")
end
else
_ ->
Mix.shell().error("No user #{nickname}")
shell_error("No user #{nickname}")
end
end
def run(["set", nickname | rest]) do
Common.start_pleroma()
start_pleroma()
{options, [], []} =
OptionParser.parse(
@ -274,33 +279,33 @@ defmodule Mix.Tasks.Pleroma.User do
end
else
_ ->
Mix.shell().error("No local user #{nickname}")
shell_error("No local user #{nickname}")
end
end
def run(["tag", nickname | tags]) do
Common.start_pleroma()
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.tag(tags)
Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}")
shell_info("Tags of #{user.nickname}: #{inspect(tags)}")
else
_ ->
Mix.shell().error("Could not change user tags for #{nickname}")
shell_error("Could not change user tags for #{nickname}")
end
end
def run(["untag", nickname | tags]) do
Common.start_pleroma()
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.untag(tags)
Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}")
shell_info("Tags of #{user.nickname}: #{inspect(tags)}")
else
_ ->
Mix.shell().error("Could not change user tags for #{nickname}")
shell_error("Could not change user tags for #{nickname}")
end
end
@ -321,14 +326,12 @@ defmodule Mix.Tasks.Pleroma.User do
end)
|> Enum.into(%{})
Common.start_pleroma()
start_pleroma()
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, "_", " ")
)
shell_info("Generated user invite token " <> String.replace(invite.invite_type, "_", " "))
url =
Pleroma.Web.Router.Helpers.redirect_url(
@ -340,14 +343,14 @@ defmodule Mix.Tasks.Pleroma.User do
IO.puts(url)
else
error ->
Mix.shell().error("Could not create invite token: #{inspect(error)}")
shell_error("Could not create invite token: #{inspect(error)}")
end
end
def run(["invites"]) do
Common.start_pleroma()
start_pleroma()
Mix.shell().info("Invites list:")
shell_info("Invites list:")
UserInviteToken.list_invites()
|> Enum.each(fn invite ->
@ -361,7 +364,7 @@ defmodule Mix.Tasks.Pleroma.User do
" | Max use: #{max_use} Left use: #{max_use - invite.uses}"
end
Mix.shell().info(
shell_info(
"ID: #{invite.id} | Token: #{invite.token} | Token type: #{invite.invite_type} | Used: #{
invite.used
}#{expire_info}#{using_info}"
@ -370,40 +373,54 @@ defmodule Mix.Tasks.Pleroma.User do
end
def run(["revoke_invite", token]) do
Common.start_pleroma()
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.")
shell_info("Invite for token #{token} was revoked.")
else
_ -> Mix.shell().error("No invite found with token #{token}")
_ -> shell_error("No invite found with token #{token}")
end
end
def run(["delete_activities", nickname]) do
Common.start_pleroma()
start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
{:ok, _} = User.delete_user_activities(user)
Mix.shell().info("User #{nickname} statuses deleted.")
shell_info("User #{nickname} statuses deleted.")
else
_ ->
Mix.shell().error("No local user #{nickname}")
shell_error("No local user #{nickname}")
end
end
def run(["toggle_confirmed", nickname]) do
Common.start_pleroma()
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
{:ok, user} = User.toggle_confirmation(user)
message = if user.info.confirmation_pending, do: "needs", else: "doesn't need"
Mix.shell().info("#{nickname} #{message} confirmation.")
shell_info("#{nickname} #{message} confirmation.")
else
_ ->
Mix.shell().error("No local user #{nickname}")
shell_error("No local user #{nickname}")
end
end
def run(["sign_out", nickname]) do
start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
OAuth.Token.delete_user_tokens(user)
OAuth.Authorization.delete_user_authorizations(user)
shell_info("#{nickname} signed out from all apps.")
else
_ ->
shell_error("No local user #{nickname}")
end
end
@ -416,7 +433,7 @@ defmodule Mix.Tasks.Pleroma.User do
{:ok, user} = User.update_and_set_cache(user_cng)
Mix.shell().info("Moderator status of #{user.nickname}: #{user.info.is_moderator}")
shell_info("Moderator status of #{user.nickname}: #{user.info.is_moderator}")
user
end
@ -429,7 +446,7 @@ defmodule Mix.Tasks.Pleroma.User do
{:ok, user} = User.update_and_set_cache(user_cng)
Mix.shell().info("Admin status of #{user.nickname}: #{user.info.is_admin}")
shell_info("Admin status of #{user.nickname}: #{user.info.is_admin}")
user
end
@ -442,7 +459,7 @@ defmodule Mix.Tasks.Pleroma.User do
{:ok, user} = User.update_and_set_cache(user_cng)
Mix.shell().info("Locked status of #{user.nickname}: #{user.info.locked}")
shell_info("Locked status of #{user.nickname}: #{user.info.locked}")
user
end
end

View file

@ -343,4 +343,6 @@ defmodule Pleroma.Activity do
)
)
end
defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
end

View file

@ -0,0 +1,92 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Activity.Search do
alias Pleroma.Activity
alias Pleroma.Object.Fetcher
alias Pleroma.Pagination
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Visibility
import Ecto.Query
def search(user, search_query, options \\ []) do
index_type = if Pleroma.Config.get([:database, :rum_enabled]), do: :rum, else: :gin
limit = Enum.min([Keyword.get(options, :limit), 40])
offset = Keyword.get(options, :offset, 0)
author = Keyword.get(options, :author)
Activity
|> Activity.with_preloaded_object()
|> Activity.restrict_deactivated_users()
|> restrict_public()
|> query_with(index_type, search_query)
|> maybe_restrict_local(user)
|> maybe_restrict_author(author)
|> Pagination.fetch_paginated(%{"offset" => offset, "limit" => limit}, :offset)
|> maybe_fetch(user, search_query)
end
def maybe_restrict_author(query, %User{} = author) do
from([a, o] in query,
where: a.actor == ^author.ap_id
)
end
def maybe_restrict_author(query, _), do: query
defp restrict_public(q) do
from([a, o] in q,
where: fragment("?->>'type' = 'Create'", a.data),
where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients
)
end
defp query_with(q, :gin, search_query) do
from([a, o] in q,
where:
fragment(
"to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
o.data,
^search_query
)
)
end
defp query_with(q, :rum, search_query) do
from([a, o] in q,
where:
fragment(
"? @@ plainto_tsquery('english', ?)",
o.fts_content,
^search_query
),
order_by: [fragment("? <=> now()::date", o.inserted_at)]
)
end
defp maybe_restrict_local(q, user) do
limit = Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated)
case {limit, user} do
{:all, _} -> restrict_local(q)
{:unauthenticated, %User{}} -> q
{:unauthenticated, _} -> restrict_local(q)
{false, _} -> q
end
end
defp restrict_local(q), do: where(q, local: true)
defp maybe_fetch(activities, user, search_query) do
with true <- Regex.match?(~r/https?:/, search_query),
{:ok, object} <- Fetcher.fetch_object_from_id(search_query),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user) do
activities ++ [activity]
else
_ -> activities
end
end
end

View file

@ -4,7 +4,6 @@
defmodule Pleroma.Application do
use Application
import Supervisor.Spec
@name Mix.Project.config()[:name]
@version Mix.Project.config()[:version]
@ -31,96 +30,128 @@ defmodule Pleroma.Application do
children =
[
# Start the Ecto repository
supervisor(Pleroma.Repo, []),
worker(Pleroma.Emoji, []),
worker(Pleroma.Captcha, []),
worker(
Cachex,
[
:used_captcha_cache,
[
ttl_interval: :timer.seconds(Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid]))
]
],
id: :cachex_used_captcha_cache
),
worker(
Cachex,
[
:user_cache,
[
default_ttl: 25_000,
ttl_interval: 1000,
limit: 2500
]
],
id: :cachex_user
),
worker(
Cachex,
[
:object_cache,
[
default_ttl: 25_000,
ttl_interval: 1000,
limit: 2500
]
],
id: :cachex_object
),
worker(
Cachex,
[
:rich_media_cache,
[
default_ttl: :timer.minutes(120),
limit: 5000
]
],
id: :cachex_rich_media
),
worker(
Cachex,
[
:scrubber_cache,
[
limit: 2500
]
],
id: :cachex_scrubber
),
worker(
Cachex,
[
:idempotency_cache,
[
expiration:
expiration(
default: :timer.seconds(6 * 60 * 60),
interval: :timer.seconds(60)
),
limit: 2500
]
],
id: :cachex_idem
),
worker(Pleroma.FlakeId, []),
worker(Pleroma.ScheduledActivityWorker, [])
%{id: Pleroma.Repo, start: {Pleroma.Repo, :start_link, []}, type: :supervisor},
%{id: Pleroma.Config.TransferTask, start: {Pleroma.Config.TransferTask, :start_link, []}},
%{id: Pleroma.Emoji, start: {Pleroma.Emoji, :start_link, []}},
%{id: Pleroma.Captcha, start: {Pleroma.Captcha, :start_link, []}},
%{
id: :cachex_used_captcha_cache,
start:
{Cachex, :start_link,
[
:used_captcha_cache,
[
ttl_interval:
:timer.seconds(Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid]))
]
]}
},
%{
id: :cachex_user,
start:
{Cachex, :start_link,
[
:user_cache,
[
default_ttl: 25_000,
ttl_interval: 1000,
limit: 2500
]
]}
},
%{
id: :cachex_object,
start:
{Cachex, :start_link,
[
:object_cache,
[
default_ttl: 25_000,
ttl_interval: 1000,
limit: 2500
]
]}
},
%{
id: :cachex_rich_media,
start:
{Cachex, :start_link,
[
:rich_media_cache,
[
default_ttl: :timer.minutes(120),
limit: 5000
]
]}
},
%{
id: :cachex_scrubber,
start:
{Cachex, :start_link,
[
:scrubber_cache,
[
limit: 2500
]
]}
},
%{
id: :cachex_idem,
start:
{Cachex, :start_link,
[
:idempotency_cache,
[
expiration:
expiration(
default: :timer.seconds(6 * 60 * 60),
interval: :timer.seconds(60)
),
limit: 2500
]
]}
},
%{id: Pleroma.FlakeId, start: {Pleroma.FlakeId, :start_link, []}},
%{
id: Pleroma.ScheduledActivityWorker,
start: {Pleroma.ScheduledActivityWorker, :start_link, []}
}
] ++
hackney_pool_children() ++
[
worker(Pleroma.Web.Federator.RetryQueue, []),
worker(Pleroma.Web.OAuth.Token.CleanWorker, []),
worker(Pleroma.Stats, []),
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)
%{
id: Pleroma.Web.Federator.RetryQueue,
start: {Pleroma.Web.Federator.RetryQueue, :start_link, []}
},
%{
id: Pleroma.Web.OAuth.Token.CleanWorker,
start: {Pleroma.Web.OAuth.Token.CleanWorker, :start_link, []}
},
%{
id: Pleroma.Stats,
start: {Pleroma.Stats, :start_link, []}
},
%{
id: :web_push_init,
start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
restart: :temporary
},
%{
id: :federator_init,
start: {Task, :start_link, [&Pleroma.Web.Federator.init/0]},
restart: :temporary
}
] ++
streamer_child() ++
chat_child() ++
[
# Start the endpoint when the application starts
supervisor(Pleroma.Web.Endpoint, []),
worker(Pleroma.Gopher.Server, [])
%{
id: Pleroma.Web.Endpoint,
start: {Pleroma.Web.Endpoint, :start_link, []},
type: :supervisor
},
%{id: Pleroma.Gopher.Server, start: {Pleroma.Gopher.Server, :start_link, []}}
]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
@ -144,7 +175,6 @@ defmodule Pleroma.Application do
Pleroma.Repo.Instrumenter.setup()
end
Prometheus.Registry.register_collector(:prometheus_process_collector)
Pleroma.Web.Endpoint.MetricsExporter.setup()
Pleroma.Web.Endpoint.PipelineInstrumenter.setup()
Pleroma.Web.Endpoint.Instrumenter.setup()
@ -157,24 +187,29 @@ defmodule Pleroma.Application do
else
[]
end ++
if Pleroma.Config.get([Pleroma.Uploader, :proxy_remote]) do
if Pleroma.Config.get([Pleroma.Upload, :proxy_remote]) do
[:upload]
else
[]
end
end
if Mix.env() == :test do
if Pleroma.Config.get(:env) == :test do
defp streamer_child, do: []
defp chat_child, do: []
else
defp streamer_child do
[worker(Pleroma.Web.Streamer, [])]
[%{id: Pleroma.Web.Streamer, start: {Pleroma.Web.Streamer, :start_link, []}}]
end
defp chat_child do
if Pleroma.Config.get([:chat, :enabled]) do
[worker(Pleroma.Web.ChatChannel.ChatChannelState, [])]
[
%{
id: Pleroma.Web.ChatChannel.ChatChannelState,
start: {Pleroma.Web.ChatChannel.ChatChannelState, :start_link, []}
}
]
else
[]
end

View file

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.BBS.Authenticator do
use Sshd.PasswordAuthenticator
alias Comeonin.Pbkdf2

View file

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.BBS.Handler do
use Sshd.ShellHandler
alias Pleroma.Activity

View file

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Bookmark do
use Ecto.Schema

View file

@ -3,6 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha do
import Pleroma.Web.Gettext
alias Calendar.DateTime
alias Plug.Crypto.KeyGenerator
alias Plug.Crypto.MessageEncryptor
@ -83,10 +85,11 @@ defmodule Pleroma.Captcha do
with {:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret),
%{at: at, answer_data: answer_md5} <- :erlang.binary_to_term(data) do
try do
if DateTime.before?(at, valid_if_after), do: throw({:error, "CAPTCHA expired"})
if DateTime.before?(at, valid_if_after),
do: throw({:error, dgettext("errors", "CAPTCHA expired")})
if not is_nil(Cachex.get!(:used_captcha_cache, token)),
do: throw({:error, "CAPTCHA already used"})
do: throw({:error, dgettext("errors", "CAPTCHA already used")})
res = method().validate(token, captcha, answer_md5)
# Throw if an error occurs
@ -101,7 +104,7 @@ defmodule Pleroma.Captcha do
:throw, e -> e
end
else
_ -> {:error, "Invalid answer data"}
_ -> {:error, dgettext("errors", "Invalid answer data")}
end
{:reply, result, state}

View file

@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha.Kocaptcha do
import Pleroma.Web.Gettext
alias Pleroma.Captcha.Service
@behaviour Service
@ -12,7 +13,7 @@ defmodule Pleroma.Captcha.Kocaptcha do
case Tesla.get(endpoint <> "/new") do
{:error, _} ->
%{error: "Kocaptcha service unavailable"}
%{error: dgettext("errors", "Kocaptcha service unavailable")}
{:ok, res} ->
json_resp = Jason.decode!(res.body)
@ -32,6 +33,6 @@ defmodule Pleroma.Captcha.Kocaptcha do
if not is_nil(captcha) and
:crypto.hash(:md5, captcha) |> Base.encode16() == String.upcase(answer_data),
do: :ok,
else: {:error, "Invalid CAPTCHA"}
else: {:error, dgettext("errors", "Invalid CAPTCHA")}
end
end

View file

@ -38,7 +38,7 @@ defmodule Pleroma.Config do
def put([parent_key | keys], value) do
parent =
Application.get_env(:pleroma, parent_key)
Application.get_env(:pleroma, parent_key, [])
|> put_in(keys, value)
Application.put_env(:pleroma, parent_key, parent)

View file

@ -0,0 +1,59 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.TransferTask do
use Task
alias Pleroma.Web.AdminAPI.Config
def start_link do
load_and_update_env()
if Pleroma.Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Pleroma.Repo)
:ignore
end
def load_and_update_env do
if Pleroma.Config.get([:instance, :dynamic_configuration]) and
Ecto.Adapters.SQL.table_exists?(Pleroma.Repo, "config") do
for_restart =
Pleroma.Repo.all(Config)
|> Enum.map(&update_env(&1))
# We need to restart applications for loaded settings take effect
for_restart
|> Enum.reject(&(&1 in [:pleroma, :ok]))
|> Enum.each(fn app ->
Application.stop(app)
:ok = Application.start(app)
end)
end
end
defp update_env(setting) do
try do
key =
if String.starts_with?(setting.key, "Pleroma.") do
"Elixir." <> setting.key
else
String.trim_leading(setting.key, ":")
end
group = String.to_existing_atom(setting.group)
Application.put_env(
group,
String.to_existing_atom(key),
Config.from_binary(setting.value)
)
group
rescue
e ->
require Logger
Logger.warn(
"updating env causes error, key: #{inspect(setting.key)}, error: #{inspect(e)}"
)
end
end
end

View file

@ -49,7 +49,7 @@ defmodule Pleroma.Conversation do
with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity),
"Create" <- activity.data["type"],
object <- Pleroma.Object.normalize(activity),
"Note" <- object.data["type"],
true <- object.data["type"] in ["Note", "Question"],
ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"] do
{:ok, conversation} = create_for_ap_id(ap_id)

View file

@ -59,10 +59,10 @@ defmodule Pleroma.Conversation.Participation do
def for_user(user, params \\ %{}) do
from(p in __MODULE__,
where: p.user_id == ^user.id,
order_by: [desc: p.updated_at]
order_by: [desc: p.updated_at],
preload: [conversation: [:users]]
)
|> Pleroma.Pagination.fetch_paginated(params)
|> Repo.preload(conversation: [:users])
end
def for_user_with_last_activity_id(user, params \\ %{}) do
@ -79,5 +79,6 @@ defmodule Pleroma.Conversation.Participation do
| last_activity_id: activity_id
}
end)
|> Enum.filter(& &1.last_activity_id)
end
end

View file

@ -3,11 +3,58 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emails.Mailer do
use Swoosh.Mailer, otp_app: :pleroma
@moduledoc """
Defines the Pleroma mailer.
The module contains functions to delivery email using Swoosh.Mailer.
"""
alias Swoosh.DeliveryError
@otp_app :pleroma
@mailer_config [otp: :pleroma]
@spec enabled?() :: boolean()
def enabled?, do: Pleroma.Config.get([__MODULE__, :enabled])
@doc "add email to queue"
def deliver_async(email, config \\ []) do
PleromaJobQueue.enqueue(:mailer, __MODULE__, [:deliver_async, email, config])
end
@doc "callback to perform send email from queue"
def perform(:deliver_async, email, config), do: deliver(email, config)
@spec deliver(Swoosh.Email.t(), Keyword.t()) :: {:ok, term} | {:error, term}
def deliver(email, config \\ [])
def deliver(email, config) do
case enabled?() do
true -> Swoosh.Mailer.deliver(email, parse_config(config))
false -> {:error, :deliveries_disabled}
end
end
@spec deliver!(Swoosh.Email.t(), Keyword.t()) :: term | no_return
def deliver!(email, config \\ [])
def deliver!(email, config) do
case deliver(email, config) do
{:ok, result} -> result
{:error, reason} -> raise DeliveryError, reason: reason
end
end
@on_load :validate_dependency
@doc false
def validate_dependency do
parse_config([])
|> Keyword.get(:adapter)
|> Swoosh.Mailer.validate_dependency()
end
defp parse_config(config) do
Swoosh.Mailer.parse_config(@otp_app, __MODULE__, @mailer_config, config)
end
end

View file

@ -23,13 +23,8 @@ defmodule Pleroma.Emails.UserEmail do
defp recipient(email, name), do: {name, email}
defp recipient(%Pleroma.User{} = user), do: recipient(user.email, user.name)
def password_reset_email(user, password_reset_token) when is_binary(password_reset_token) do
password_reset_url =
Router.Helpers.util_url(
Endpoint,
:show_password_reset,
password_reset_token
)
def password_reset_email(user, token) when is_binary(token) do
password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)
html_body = """
<h3>Reset your password at #{instance_name()}</h3>

View file

@ -22,7 +22,6 @@ defmodule Pleroma.Emoji do
@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
@ -87,6 +86,8 @@ defmodule Pleroma.Emoji do
"emoji"
)
emoji_groups = Pleroma.Config.get([:emoji, :groups])
case File.ls(emoji_dir_path) do
{:error, :enoent} ->
# The custom emoji directory doesn't exist,
@ -97,14 +98,28 @@ defmodule Pleroma.Emoji do
# There was some other error
Logger.error("Could not access the custom emoji directory #{emoji_dir_path}: #{e}")
{:ok, packs} ->
{:ok, results} ->
grouped =
Enum.group_by(results, fn file -> File.dir?(Path.join(emoji_dir_path, file)) end)
packs = grouped[true] || []
files = grouped[false] || []
# Print the packs we've found
Logger.info("Found emoji packs: #{Enum.join(packs, ", ")}")
if not Enum.empty?(files) do
Logger.warn(
"Found files in the emoji folder. These will be ignored, please move them to a subdirectory\nFound files: #{
Enum.join(files, ", ")
}"
)
end
emojis =
Enum.flat_map(
packs,
fn pack -> load_pack(Path.join(emoji_dir_path, pack)) end
fn pack -> load_pack(Path.join(emoji_dir_path, pack), emoji_groups) end
)
true = :ets.insert(@ets, emojis)
@ -112,12 +127,12 @@ defmodule Pleroma.Emoji do
# 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] || []
shortcode_globs = Pleroma.Config.get([:emoji, :shortcode_globs], [])
emojis =
(load_from_file("config/emoji.txt") ++
load_from_file("config/custom_emoji.txt") ++
load_from_globs(shortcode_globs))
(load_from_file("config/emoji.txt", emoji_groups) ++
load_from_file("config/custom_emoji.txt", emoji_groups) ++
load_from_globs(shortcode_globs, emoji_groups))
|> Enum.reject(fn value -> value == nil end)
true = :ets.insert(@ets, emojis)
@ -125,23 +140,25 @@ defmodule Pleroma.Emoji do
:ok
end
defp load_pack(pack_dir) do
defp load_pack(pack_dir, emoji_groups) 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)
load_from_file(emoji_txt, emoji_groups)
else
extensions = Pleroma.Config.get([:emoji, :pack_extensions])
Logger.info(
"No emoji.txt found for pack \"#{pack_name}\", assuming all .png files are emoji"
"No emoji.txt found for pack \"#{pack_name}\", assuming all #{Enum.join(extensions, ", ")} files are emoji"
)
make_shortcode_to_file_map(pack_dir, [".png"])
make_shortcode_to_file_map(pack_dir, extensions)
|> Enum.map(fn {shortcode, rel_file} ->
filename = Path.join("/emoji/#{pack_name}", rel_file)
{shortcode, filename, [to_string(match_extra(@groups, filename))]}
{shortcode, filename, [to_string(match_extra(emoji_groups, filename))]}
end)
end
end
@ -170,21 +187,21 @@ defmodule Pleroma.Emoji do
|> Enum.filter(fn f -> Path.extname(f) in exts end)
end
defp load_from_file(file) do
defp load_from_file(file, emoji_groups) do
if File.exists?(file) do
load_from_file_stream(File.stream!(file))
load_from_file_stream(File.stream!(file), emoji_groups)
else
[]
end
end
defp load_from_file_stream(stream) do
defp load_from_file_stream(stream, emoji_groups) do
stream
|> Stream.map(&String.trim/1)
|> Stream.map(fn line ->
case String.split(line, ~r/,\s*/) do
[name, file] ->
{name, file, [to_string(match_extra(@groups, file))]}
{name, file, [to_string(match_extra(emoji_groups, file))]}
[name, file | tags] ->
{name, file, tags}
@ -196,7 +213,7 @@ defmodule Pleroma.Emoji do
|> Enum.to_list()
end
defp load_from_globs(globs) do
defp load_from_globs(globs, emoji_groups) do
static_path = Path.join(:code.priv_dir(:pleroma), "static")
paths =
@ -207,7 +224,7 @@ defmodule Pleroma.Emoji do
|> Enum.concat()
Enum.map(paths, fn path ->
tag = match_extra(@groups, Path.join("/", Path.relative_to(path, static_path)))
tag = match_extra(emoji_groups, Path.join("/", Path.relative_to(path, static_path)))
shortcode = Path.basename(path, Path.extname(path))
external_path = Path.join("/", Path.relative_to(path, static_path))
{shortcode, external_path, [to_string(tag)]}

View file

@ -0,0 +1,27 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Helpers.UriHelper do
def append_uri_params(uri, appended_params) do
uri = URI.parse(uri)
appended_params = for {k, v} <- appended_params, into: %{}, do: {to_string(k), v}
existing_params = URI.query_decoder(uri.query || "") |> Enum.into(%{})
updated_params_keys = Enum.uniq(Map.keys(existing_params) ++ Map.keys(appended_params))
updated_params =
for k <- updated_params_keys, do: {k, appended_params[k] || existing_params[k]}
uri
|> Map.put(:query, URI.encode_query(updated_params))
|> URI.to_string()
end
def append_param_if_present(%{} = params, param_name, param_value) do
if param_value do
Map.put(params, param_name, param_value)
else
params
end
end
end

View file

@ -89,7 +89,7 @@ defmodule Pleroma.HTML do
Cachex.fetch!(:scrubber_cache, key, fn _key ->
result =
content
|> Floki.filter_out("a.mention")
|> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"]")
|> Floki.attribute("a", "href")
|> Enum.at(0)
@ -104,7 +104,6 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
paragraphs, breaks and links are allowed through the filter.
"""
@markup Application.get_env(:pleroma, :markup)
@valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
require HtmlSanitizeEx.Scrubber.Meta
@ -142,9 +141,7 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
Meta.allow_tag_with_these_attributes("span", [])
# allow inline images for custom emoji
@allow_inline_images Keyword.get(@markup, :allow_inline_images)
if @allow_inline_images do
if Pleroma.Config.get([:markup, :allow_inline_images]) do
# restrict img tags to http/https only, because of MediaProxy.
Meta.allow_tag_with_uri_attributes("img", ["src"], ["http", "https"])
@ -168,7 +165,6 @@ defmodule Pleroma.HTML.Scrubber.Default do
# credo:disable-for-previous-line
# No idea how to fix this one…
@markup Application.get_env(:pleroma, :markup)
@valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
Meta.remove_cdata_sections_before_scrub()
@ -213,7 +209,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
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)
@allow_inline_images Pleroma.Config.get([:markup, :allow_inline_images])
if @allow_inline_images do
# restrict img tags to http/https only, because of MediaProxy.
@ -228,9 +224,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
])
end
@allow_tables Keyword.get(@markup, :allow_tables)
if @allow_tables do
if Pleroma.Config.get([:markup, :allow_tables]) do
Meta.allow_tag_with_these_attributes("table", [])
Meta.allow_tag_with_these_attributes("tbody", [])
Meta.allow_tag_with_these_attributes("td", [])
@ -239,9 +233,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes("tr", [])
end
@allow_headings Keyword.get(@markup, :allow_headings)
if @allow_headings do
if Pleroma.Config.get([:markup, :allow_headings]) do
Meta.allow_tag_with_these_attributes("h1", [])
Meta.allow_tag_with_these_attributes("h2", [])
Meta.allow_tag_with_these_attributes("h3", [])
@ -249,9 +241,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes("h5", [])
end
@allow_fonts Keyword.get(@markup, :allow_fonts)
if @allow_fonts do
if Pleroma.Config.get([:markup, :allow_fonts]) do
Meta.allow_tag_with_these_attributes("font", ["face"])
end

View file

@ -29,7 +29,7 @@ defmodule Pleroma.HTTP.Connection do
# fetch Hackney options
#
defp hackney_options(opts) do
def hackney_options(opts) do
options = Keyword.get(opts, :adapter, [])
adapter_options = Pleroma.Config.get([:http, :adapter], [])
proxy_url = Pleroma.Config.get([:http, :proxy_url], nil)

View file

@ -65,13 +65,7 @@ defmodule Pleroma.HTTP do
end
def process_request_options(options) do
config = Application.get_env(:pleroma, :http, [])
proxy = Keyword.get(config, :proxy_url, nil)
case proxy do
nil -> options
_ -> options ++ [proxy: proxy]
end
Keyword.merge(Pleroma.HTTP.Connection.hackney_options([]), options)
end
@doc """

View file

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Instances do
@moduledoc "Instances context."
@ -13,7 +17,7 @@ defmodule Pleroma.Instances do
def reachability_datetime_threshold do
federation_reachability_timeout_days =
Pleroma.Config.get(:instance)[:federation_reachability_timeout_days] || 0
Pleroma.Config.get([:instance, :federation_reachability_timeout_days], 0)
if federation_reachability_timeout_days > 0 do
NaiveDateTime.add(

View file

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Instances.Instance do
@moduledoc "Instance."

View file

@ -35,10 +35,12 @@ defmodule Pleroma.Keys do
end
def keys_from_pem(pem) do
[private_key_code] = :public_key.pem_decode(pem)
private_key = :public_key.pem_entry_decode(private_key_code)
{:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key
public_key = {:RSAPublicKey, modulus, exponent}
{:ok, private_key, public_key}
with [private_key_code] <- :public_key.pem_decode(pem),
private_key <- :public_key.pem_entry_decode(private_key_code),
{:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} <- private_key do
{:ok, private_key, {:RSAPublicKey, modulus, exponent}}
else
error -> {:error, error}
end
end
end

View file

@ -16,6 +16,7 @@ defmodule Pleroma.List do
belongs_to(:user, User, type: Pleroma.FlakeId)
field(:title, :string)
field(:following, {:array, :string}, default: [])
field(:ap_id, :string)
timestamps()
end
@ -55,6 +56,10 @@ defmodule Pleroma.List do
Repo.one(query)
end
def get_by_ap_id(ap_id) do
Repo.get_by(__MODULE__, ap_id: ap_id)
end
def get_following(%Pleroma.List{following: following} = _list) do
q =
from(
@ -105,7 +110,14 @@ defmodule Pleroma.List do
def create(title, %User{} = creator) do
list = %Pleroma.List{user_id: creator.id, title: title}
Repo.insert(list)
Repo.transaction(fn ->
list = Repo.insert!(list)
list
|> change(ap_id: "#{creator.ap_id}/lists/#{list.id}")
|> Repo.update!()
end)
end
def follow(%Pleroma.List{following: following} = list, %User{} = followed) do
@ -125,4 +137,19 @@ defmodule Pleroma.List do
|> follow_changeset(attrs)
|> Repo.update()
end
def memberships(%User{follower_address: follower_address}) do
Pleroma.List
|> where([l], ^follower_address in l.following)
|> select([l], l.ap_id)
|> Repo.all()
end
def memberships(_), do: []
def member?(%Pleroma.List{following: following}, %User{follower_address: follower_address}) do
Enum.member?(following, follower_address)
end
def member?(_, _), do: false
end

View file

@ -11,8 +11,9 @@ defmodule Pleroma.Notification do
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.Push
alias Pleroma.Web.Streamer
import Ecto.Query
import Ecto.Changeset
@ -30,31 +31,47 @@ defmodule Pleroma.Notification do
|> cast(attrs, [:seen])
end
def for_user_query(user) do
Notification
|> where(user_id: ^user.id)
|> where(
[n, a],
fragment(
"? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
a.actor
)
)
|> join(:inner, [n], activity in assoc(n, :activity))
|> join(:left, [n, a], object in Object,
on:
def for_user_query(user, opts) do
query =
Notification
|> where(user_id: ^user.id)
|> where(
[n, a],
fragment(
"(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
object.data,
a.data
"? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
a.actor
)
)
|> preload([n, a, o], activity: {a, object: o})
)
|> 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})
if opts[:with_muted] do
query
else
where(query, [n, a], a.actor not in ^user.info.muted_notifications)
|> where([n, a], a.actor not in ^user.info.blocks)
|> where(
[n, a],
fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.info.domain_blocks
)
|> join(:left, [n, a], tm in Pleroma.ThreadMute,
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
)
|> where([n, a, o, tm], is_nil(tm.user_id))
end
end
def for_user(user, opts \\ %{}) do
user
|> for_user_query()
|> for_user_query(opts)
|> Pagination.fetch_paginated(opts)
end
@ -125,10 +142,21 @@ defmodule Pleroma.Notification do
end
end
def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)
when type in ["Create", "Like", "Announce", "Follow"] do
users = get_notified_from_activity(activity)
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
object = Object.normalize(activity)
unless object && object.data["type"] == "Answer" do
users = get_notified_from_activity(activity)
notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
{:ok, notifications}
else
{:ok, []}
end
end
def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)
when type in ["Like", "Announce", "Follow"] do
users = get_notified_from_activity(activity)
notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
{:ok, notifications}
end
@ -140,8 +168,9 @@ defmodule Pleroma.Notification 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)
Pleroma.Web.Push.send(notification)
Streamer.stream("user", notification)
Streamer.stream("user:notification", notification)
Push.send(notification)
notification
end
end
@ -165,32 +194,24 @@ defmodule Pleroma.Notification do
def get_notified_from_activity(_, _local_only), do: []
@spec skip?(Activity.t(), User.t()) :: boolean()
def skip?(activity, user) do
[:self, :blocked, :local, :muted, :followers, :follows, :recently_followed]
[
:self,
:followers,
:follows,
:non_followers,
:non_follows,
:recently_followed
]
|> Enum.any?(&skip?(&1, activity, user))
end
@spec skip?(atom(), Activity.t(), User.t()) :: boolean()
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,
@ -201,12 +222,32 @@ defmodule Pleroma.Notification do
User.following?(follower, user)
end
def skip?(
:non_followers,
activity,
%{info: %{notification_settings: %{"non_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?(
:non_follows,
activity,
%{info: %{notification_settings: %{"non_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"]

View file

@ -35,50 +35,55 @@ defmodule Pleroma.Object do
|> unique_constraint(:ap_id, name: :objects_unique_apid_index)
end
def get_by_id(nil), do: nil
def get_by_id(id), do: Repo.get(Object, id)
def get_by_ap_id(nil), do: nil
def get_by_ap_id(ap_id) do
Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
end
def normalize(_, fetch_remote \\ true)
defp warn_on_no_object_preloaded(ap_id) do
"Object.normalize() called without preloaded object (#{ap_id}). Consider preloading the object"
|> Logger.debug()
Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
end
def normalize(_, fetch_remote \\ true, options \\ [])
# 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
def normalize(%Object{} = object, _, _), do: object
def normalize(%Activity{object: %Object{} = object}, _, _), do: object
# A hack for fake activities
def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _) do
def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _, _) do
%Object{id: "pleroma:fake_object_id", data: data}
end
# Catch and log Object.normalize() calls where the Activity's child object is not
# preloaded.
def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, 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))}")
# No preloaded object
def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, fetch_remote, _) do
warn_on_no_object_preloaded(ap_id)
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))}")
# No preloaded object
def normalize(%Activity{data: %{"object" => ap_id}}, fetch_remote, _) do
warn_on_no_object_preloaded(ap_id)
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
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, options) when is_binary(ap_id) do
Fetcher.fetch_object_from_id!(ap_id, options)
end
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}),
@ -195,4 +200,34 @@ defmodule Pleroma.Object do
_ -> {:error, "Not found"}
end
end
def increase_vote_count(ap_id, name) do
with %Object{} = object <- Object.normalize(ap_id),
"Question" <- object.data["type"] do
multiple = Map.has_key?(object.data, "anyOf")
options =
(object.data["anyOf"] || object.data["oneOf"] || [])
|> Enum.map(fn
%{"name" => ^name} = option ->
Kernel.update_in(option["replies"]["totalItems"], &(&1 + 1))
option ->
option
end)
data =
if multiple do
Map.put(object.data, "anyOf", options)
else
Map.put(object.data, "oneOf", options)
end
object
|> Object.change(%{data: data})
|> update_and_set_cache()
else
_ -> :noop
end
end
end

View file

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Object.Containment do
@moduledoc """
This module contains some useful functions for containing objects to specific
@ -44,6 +48,9 @@ defmodule Pleroma.Object.Containment do
end
end
def contain_origin(id, %{"attributedTo" => actor} = params),
do: contain_origin(id, Map.put(params, "actor", actor))
def contain_origin_from_id(_id, %{"id" => nil}), do: :error
def contain_origin_from_id(id, %{"id" => other_id} = _params) do
@ -56,4 +63,9 @@ defmodule Pleroma.Object.Containment do
:error
end
end
def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}),
do: contain_origin(id, object)
def contain_child(_), do: :ok
end

View file

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Object.Fetcher do
alias Pleroma.HTTP
alias Pleroma.Object
@ -22,39 +26,45 @@ defmodule Pleroma.Object.Fetcher do
# TODO:
# This will create a Create activity, which we need internally at the moment.
def fetch_object_from_id(id) do
def fetch_object_from_id(id, options \\ []) 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),
with {:fetch, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
{:normalize, nil} <- {:normalize, Object.normalize(data, false)},
params <- %{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
# Should we seriously keep this attributedTo thing?
"actor" => data["actor"] || data["attributedTo"],
"object" => data
},
:ok <- Containment.contain_origin(id, params),
{:ok, activity} <- Transmogrifier.handle_incoming(params),
{:containment, :ok} <- {:containment, Containment.contain_origin(id, params)},
{:ok, activity} <- Transmogrifier.handle_incoming(params, options),
{:object, _data, %Object{} = object} <-
{:object, data, Object.normalize(activity, false)} do
{:ok, object}
else
{:containment, _} ->
{:error, "Object containment failed."}
{:error, {:reject, nil}} ->
{:reject, nil}
{:object, data, nil} ->
reinject_object(data)
object = %Object{} ->
{:normalize, object = %Object{}} ->
{:ok, object}
_e ->
# Only fallback when receiving a fetch/normalization error with ActivityPub
Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
# FIXME: OStatus Object Containment?
case OStatus.fetch_activity_from_url(id) do
{:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)}
e -> e
@ -63,8 +73,8 @@ defmodule Pleroma.Object.Fetcher do
end
end
def fetch_object_from_id!(id) do
with {:ok, object} <- fetch_object_from_id(id) do
def fetch_object_from_id!(id, options \\ []) do
with {:ok, object} <- fetch_object_from_id(id, options) do
object
else
_e ->
@ -85,6 +95,9 @@ defmodule Pleroma.Object.Fetcher do
:ok <- Containment.contain_origin_from_id(id, data) do
{:ok, data}
else
{:ok, %{status: code}} when code in [404, 410] ->
{:error, "Object has been deleted"}
e ->
{:error, e}
end

View file

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ObjectTombstone do
@enforce_keys [:id, :formerType, :deleted]
defstruct [:id, :formerType, :deleted, type: "Tombstone"]

View file

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Pagination do
@moduledoc """
Implements Mastodon-compatible pagination.
@ -10,16 +14,28 @@ defmodule Pleroma.Pagination do
@default_limit 20
def fetch_paginated(query, params) do
def fetch_paginated(query, params, type \\ :keyset)
def fetch_paginated(query, params, :keyset) do
options = cast_params(params)
query
|> paginate(options)
|> paginate(options, :keyset)
|> Repo.all()
|> enforce_order(options)
end
def paginate(query, options) do
def fetch_paginated(query, params, :offset) do
options = cast_params(params)
query
|> paginate(options, :offset)
|> Repo.all()
end
def paginate(query, options, method \\ :keyset)
def paginate(query, options, :keyset) do
query
|> restrict(:min_id, options)
|> restrict(:since_id, options)
@ -28,11 +44,18 @@ defmodule Pleroma.Pagination do
|> restrict(:limit, options)
end
def paginate(query, options, :offset) do
query
|> restrict(:offset, options)
|> restrict(:limit, options)
end
defp cast_params(params) do
param_types = %{
min_id: :string,
since_id: :string,
max_id: :string,
offset: :integer,
limit: :integer
}
@ -66,6 +89,10 @@ defmodule Pleroma.Pagination do
order_by(query, [u], fragment("? desc nulls last", u.id))
end
defp restrict(query, :offset, %{offset: offset}) do
offset(query, ^offset)
end
defp restrict(query, :limit, options) do
limit = Map.get(options, :limit, @default_limit)

View file

@ -37,6 +37,7 @@ defmodule Pleroma.PasswordResetToken do
|> put_change(:used, true)
end
@spec reset_password(binary(), map()) :: {:ok, User.t()} | {:error, binary()}
def reset_password(token, data) do
with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
%User{} = user <- User.get_cached_by_id(token.user_id),

View file

@ -6,11 +6,26 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
alias Comeonin.Pbkdf2
import Plug.Conn
alias Pleroma.User
require Logger
def init(options) do
options
end
def checkpw(password, password_hash) do
cond do
String.starts_with?(password_hash, "$pbkdf2") ->
Pbkdf2.checkpw(password, password_hash)
String.starts_with?(password_hash, "$6") ->
:crypt.crypt(password, password_hash) == password_hash
true ->
Logger.error("Password hash not recognized")
false
end
end
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do
import Plug.Conn
import Pleroma.Web.TranslationHelpers
alias Pleroma.User
def init(options) do
@ -16,8 +17,7 @@ defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do
def call(conn, _) do
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{error: "Invalid credentials."}))
|> render_error(:forbidden, "Invalid credentials.")
|> halt
end
end

View file

@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do
import Pleroma.Web.TranslationHelpers
import Plug.Conn
alias Pleroma.Config
alias Pleroma.User
@ -23,8 +24,7 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do
{false, _} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{error: "This resource requires authentication."}))
|> render_error(:forbidden, "This resource requires authentication.")
|> halt
end
end

View file

@ -10,7 +10,7 @@ defmodule Pleroma.Web.FederatingPlug do
end
def call(conn, _opts) do
if Keyword.get(Application.get_env(:pleroma, :instance), :federating) do
if Pleroma.Config.get([:instance, :federating]) do
conn
else
conn

View file

@ -56,14 +56,14 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
connect_src = "connect-src 'self' #{static_url} #{websocket_url}"
connect_src =
if Mix.env() == :dev do
if Pleroma.Config.get(:env) == :dev do
connect_src <> " http://localhost:3035/"
else
connect_src
end
script_src =
if Mix.env() == :dev do
if Pleroma.Config.get(:env) == :dev do
"script-src 'self' 'unsafe-eval'"
else
"script-src 'self'"

View file

@ -0,0 +1,84 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.IdempotencyPlug do
import Phoenix.Controller, only: [json: 2]
import Plug.Conn
@behaviour Plug
@impl true
def init(opts), do: opts
# Sending idempotency keys in `GET` and `DELETE` requests has no effect
# and should be avoided, as these requests are idempotent by definition.
@impl true
def call(%{method: method} = conn, _) when method in ["POST", "PUT", "PATCH"] do
case get_req_header(conn, "idempotency-key") do
[key] -> process_request(conn, key)
_ -> conn
end
end
def call(conn, _), do: conn
def process_request(conn, key) do
case Cachex.get(:idempotency_cache, key) do
{:ok, nil} ->
cache_resposnse(conn, key)
{:ok, record} ->
send_cached(conn, key, record)
{atom, message} when atom in [:ignore, :error] ->
render_error(conn, message)
end
end
defp cache_resposnse(conn, key) do
register_before_send(conn, fn conn ->
[request_id] = get_resp_header(conn, "x-request-id")
content_type = get_content_type(conn)
record = {request_id, content_type, conn.status, conn.resp_body}
{:ok, _} = Cachex.put(:idempotency_cache, key, record)
conn
|> put_resp_header("idempotency-key", key)
|> put_resp_header("x-original-request-id", request_id)
end)
end
defp send_cached(conn, key, record) do
{request_id, content_type, status, body} = record
conn
|> put_resp_header("idempotency-key", key)
|> put_resp_header("idempotent-replayed", "true")
|> put_resp_header("x-original-request-id", request_id)
|> put_resp_content_type(content_type)
|> send_resp(status, body)
|> halt()
end
defp render_error(conn, message) do
conn
|> put_status(:unprocessable_entity)
|> json(%{error: message})
|> halt()
end
defp get_content_type(conn) do
[content_type] = get_resp_header(conn, "content-type")
if String.contains?(content_type, ";") do
content_type
|> String.split(";")
|> hd()
else
content_type
end
end
end

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Plugs.OAuthScopesPlug do
import Plug.Conn
import Pleroma.Web.Gettext
@behaviour Plug
@ -30,11 +31,14 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
true ->
missing_scopes = scopes -- token.scopes
error_message = "Insufficient permissions: #{Enum.join(missing_scopes, " #{op} ")}."
permissions = Enum.join(missing_scopes, " #{op} ")
error_message =
dgettext("errors", "Insufficient permissions: %{permissions}.", permissions: permissions)
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{error: error_message}))
|> send_resp(:forbidden, Jason.encode!(%{error: error_message}))
|> halt()
end
end

View file

@ -1,36 +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.Plugs.RateLimitPlug do
import Phoenix.Controller, only: [json: 2]
import Plug.Conn
def init(opts), do: opts
def call(conn, opts) do
enabled? = Pleroma.Config.get([:app_account_creation, :enabled])
case check_rate(conn, Map.put(opts, :enabled, enabled?)) do
{:ok, _count} -> conn
{:error, _count} -> render_error(conn)
%Plug.Conn{} = conn -> conn
end
end
defp check_rate(conn, %{enabled: true} = opts) do
max_requests = opts[:max_requests]
bucket_name = conn.remote_ip |> Tuple.to_list() |> Enum.join(".")
ExRated.check_rate(bucket_name, opts[:interval] * 1000, max_requests)
end
defp check_rate(conn, _), do: conn
defp render_error(conn) do
conn
|> put_status(:forbidden)
|> json(%{error: "Rate limit exceeded."})
|> halt()
end
end

View file

@ -0,0 +1,131 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.RateLimiter do
@moduledoc """
## Configuration
A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where:
* The first element: `scale` (Integer). The time scale in milliseconds.
* The second element: `limit` (Integer). How many requests to limit in the time scale provided.
It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated.
To disable a limiter set its value to `nil`.
### Example
config :pleroma, :rate_limit,
one: {1000, 10},
two: [{10_000, 10}, {10_000, 50}],
foobar: nil
Here we have three limiters:
* `one` which is not over 10req/1s
* `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users
* `foobar` which is disabled
## Usage
AllowedSyntax:
plug(Pleroma.Plugs.RateLimiter, :limiter_name)
plug(Pleroma.Plugs.RateLimiter, {:limiter_name, options})
Allowed options:
* `bucket_name` overrides bucket name (e.g. to have a separate limit for a set of actions)
* `params` appends values of specified request params (e.g. ["id"]) to bucket name
Inside a controller:
plug(Pleroma.Plugs.RateLimiter, :one when action == :one)
plug(Pleroma.Plugs.RateLimiter, :two when action in [:two, :three])
plug(
Pleroma.Plugs.RateLimiter,
{:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
when action in ~w(fav_status unfav_status)a
)
or inside a router pipeline:
pipeline :api do
...
plug(Pleroma.Plugs.RateLimiter, :one)
...
end
"""
import Pleroma.Web.TranslationHelpers
import Plug.Conn
alias Pleroma.User
def init(limiter_name) when is_atom(limiter_name) do
init({limiter_name, []})
end
def init({limiter_name, opts}) do
case Pleroma.Config.get([:rate_limit, limiter_name]) do
nil -> nil
config -> {limiter_name, config, opts}
end
end
# Do not limit if there is no limiter configuration
def call(conn, nil), do: conn
def call(conn, settings) do
case check_rate(conn, settings) do
{:ok, _count} ->
conn
{:error, _count} ->
render_throttled_error(conn)
end
end
defp bucket_name(conn, limiter_name, opts) do
bucket_name = opts[:bucket_name] || limiter_name
if params_names = opts[:params] do
params_values = for p <- Enum.sort(params_names), do: conn.params[p]
Enum.join([bucket_name] ++ params_values, ":")
else
bucket_name
end
end
defp check_rate(
%{assigns: %{user: %User{id: user_id}}} = conn,
{limiter_name, [_, {scale, limit}], opts}
) do
bucket_name = bucket_name(conn, limiter_name, opts)
ExRated.check_rate("#{bucket_name}:#{user_id}", scale, limit)
end
defp check_rate(conn, {limiter_name, [{scale, limit} | _], opts}) do
bucket_name = bucket_name(conn, limiter_name, opts)
ExRated.check_rate("#{bucket_name}:#{ip(conn)}", scale, limit)
end
defp check_rate(conn, {limiter_name, {scale, limit}, opts}) do
check_rate(conn, {limiter_name, [{scale, limit}, {scale, limit}], opts})
end
def ip(%{remote_ip: remote_ip}) do
remote_ip
|> Tuple.to_list()
|> Enum.join(".")
end
defp render_throttled_error(conn) do
conn
|> render_error(:too_many_requests, "Throttled")
|> halt()
end
end

View file

@ -0,0 +1,63 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# NOTE: this module is based on https://github.com/smeevil/set_locale
defmodule Pleroma.Plugs.SetLocalePlug do
import Plug.Conn, only: [get_req_header: 2, assign: 3]
def init(_), do: nil
def call(conn, _) do
locale = get_locale_from_header(conn) || Gettext.get_locale()
Gettext.put_locale(locale)
assign(conn, :locale, locale)
end
defp get_locale_from_header(conn) do
conn
|> extract_accept_language()
|> Enum.find(&supported_locale?/1)
end
defp extract_accept_language(conn) do
case get_req_header(conn, "accept-language") do
[value | _] ->
value
|> String.split(",")
|> Enum.map(&parse_language_option/1)
|> Enum.sort(&(&1.quality > &2.quality))
|> Enum.map(& &1.tag)
|> Enum.reject(&is_nil/1)
|> ensure_language_fallbacks()
_ ->
[]
end
end
defp supported_locale?(locale) do
Pleroma.Web.Gettext
|> Gettext.known_locales()
|> Enum.member?(locale)
end
defp parse_language_option(string) do
captures = Regex.named_captures(~r/^\s?(?<tag>[\w\-]+)(?:;q=(?<quality>[\d\.]+))?$/i, string)
quality =
case Float.parse(captures["quality"] || "1.0") do
{val, _} -> val
:error -> 1.0
end
%{tag: captures["tag"], quality: quality}
end
defp ensure_language_fallbacks(tags) do
Enum.flat_map(tags, fn tag ->
[language | _] = String.split(tag, "-")
if Enum.member?(tags, language), do: [tag], else: [tag, language]
end)
end
end

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Plugs.UploadedMedia do
"""
import Plug.Conn
import Pleroma.Web.Gettext
require Logger
@behaviour Plug
@ -36,7 +37,7 @@ defmodule Pleroma.Plugs.UploadedMedia do
conn
end
config = Pleroma.Config.get([Pleroma.Upload])
config = Pleroma.Config.get(Pleroma.Upload)
with uploader <- Keyword.fetch!(config, :uploader),
proxy_remote = Keyword.get(config, :proxy_remote, false),
@ -45,7 +46,7 @@ defmodule Pleroma.Plugs.UploadedMedia do
else
_ ->
conn
|> send_resp(500, "Failed")
|> send_resp(:internal_server_error, dgettext("errors", "Failed"))
|> halt()
end
end
@ -64,7 +65,7 @@ defmodule Pleroma.Plugs.UploadedMedia do
conn
else
conn
|> send_resp(404, "Not found")
|> send_resp(:not_found, dgettext("errors", "Not found"))
|> halt()
end
end
@ -84,7 +85,7 @@ defmodule Pleroma.Plugs.UploadedMedia do
Logger.error("#{__MODULE__}: Unknown get startegy: #{inspect(unknown)}")
conn
|> send_resp(500, "Internal Error")
|> send_resp(:internal_server_error, dgettext("errors", "Internal Error"))
|> halt()
end
end

View file

@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.UserIsAdminPlug do
import Pleroma.Web.TranslationHelpers
import Plug.Conn
alias Pleroma.User
@ -16,8 +17,7 @@ defmodule Pleroma.Plugs.UserIsAdminPlug do
def call(conn, _) do
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{error: "User is not admin."}))
|> render_error(:forbidden, "User is not admin.")
|> halt
end
end

View file

@ -0,0 +1,66 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReleaseTasks do
@repo Pleroma.Repo
def run(args) do
[task | args] = String.split(args)
case task do
"migrate" -> migrate(args)
"create" -> create()
"rollback" -> rollback(args)
task -> mix_task(task, args)
end
end
defp mix_task(task, args) do
Application.load(:pleroma)
{:ok, modules} = :application.get_key(:pleroma, :modules)
module =
Enum.find(modules, fn module ->
module = Module.split(module)
match?(["Mix", "Tasks", "Pleroma" | _], module) and
String.downcase(List.last(module)) == task
end)
if module do
module.run(args)
else
IO.puts("The task #{task} does not exist")
end
end
def migrate(args) do
Mix.Tasks.Pleroma.Ecto.Migrate.run(args)
end
def rollback(args) do
Mix.Tasks.Pleroma.Ecto.Rollback.run(args)
end
def create do
Application.load(:pleroma)
case @repo.__adapter__.storage_up(@repo.config) do
:ok ->
IO.puts("The database for #{inspect(@repo)} has been created")
{:error, :already_up} ->
IO.puts("The database for #{inspect(@repo)} has already been created")
{:error, term} when is_binary(term) ->
IO.puts(:stderr, "The database for #{inspect(@repo)} couldn't be created: #{term}")
{:error, term} ->
IO.puts(
:stderr,
"The database for #{inspect(@repo)} couldn't be created: #{inspect(term)}"
)
end
end
end

View file

@ -0,0 +1,34 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.RepoStreamer do
alias Pleroma.Repo
import Ecto.Query
def chunk_stream(query, chunk_size) do
Stream.unfold(0, fn
:halt ->
{[], :halt}
last_id ->
query
|> order_by(asc: :id)
|> where([r], r.id > ^last_id)
|> limit(^chunk_size)
|> Repo.all()
|> case do
[] ->
{[], :halt}
records ->
last_id = List.last(records).id
{records, last_id}
end
end)
|> Stream.take_while(fn
[] -> false
_ -> true
end)
end
end

View file

@ -0,0 +1,28 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReverseProxy.Client do
@callback request(atom(), String.t(), [tuple()], String.t(), list()) ::
{:ok, pos_integer(), [tuple()], reference() | map()}
| {:ok, pos_integer(), [tuple()]}
| {:ok, reference()}
| {:error, term()}
@callback stream_body(reference() | pid() | map()) ::
{:ok, binary()} | :done | {:error, String.t()}
@callback close(reference() | pid() | map()) :: :ok
def request(method, url, headers, "", opts \\ []) do
client().request(method, url, headers, "", opts)
end
def stream_body(ref), do: client().stream_body(ref)
def close(ref), do: client().close(ref)
defp client do
Pleroma.Config.get([Pleroma.ReverseProxy.Client], :hackney)
end
end

View file

@ -61,9 +61,7 @@ defmodule Pleroma.ReverseProxy do
* `http`: options for [hackney](https://github.com/benoitc/hackney).
"""
@hackney Application.get_env(:pleroma, :hackney, :hackney)
@default_hackney_options []
@default_hackney_options [pool: :media]
@inline_content_types [
"image/gif",
@ -96,7 +94,8 @@ defmodule Pleroma.ReverseProxy do
def call(conn = %{method: method}, url, opts) when method in @methods do
hackney_opts =
@default_hackney_options
Pleroma.HTTP.Connection.hackney_options([])
|> Keyword.merge(@default_hackney_options)
|> Keyword.merge(Keyword.get(opts, :http, []))
|> HTTP.process_request_options()
@ -148,7 +147,7 @@ defmodule Pleroma.ReverseProxy do
Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")
method = method |> String.downcase() |> String.to_existing_atom()
case @hackney.request(method, url, headers, "", hackney_opts) do
case client().request(method, url, headers, "", hackney_opts) do
{:ok, code, headers, client} when code in @valid_resp_codes ->
{:ok, code, downcase_headers(headers), client}
@ -175,7 +174,7 @@ defmodule Pleroma.ReverseProxy do
halt(conn)
{:error, :closed, conn} ->
:hackney.close(client)
client().close(client)
halt(conn)
{:error, error, conn} ->
@ -183,7 +182,7 @@ defmodule Pleroma.ReverseProxy do
"#{__MODULE__} request to #{url} failed while reading/chunking: #{inspect(error)}"
)
:hackney.close(client)
client().close(client)
halt(conn)
end
end
@ -198,7 +197,7 @@ defmodule Pleroma.ReverseProxy do
duration,
Keyword.get(opts, :max_read_duration, @max_read_duration)
),
{:ok, data} <- @hackney.stream_body(client),
{:ok, data} <- client().stream_body(client),
{:ok, duration} <- increase_read_duration(duration),
sent_so_far = sent_so_far + byte_size(data),
:ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)),
@ -379,4 +378,6 @@ defmodule Pleroma.ReverseProxy do
defp increase_read_duration(_) do
{:ok, :no_duration_limit, :no_duration_limit}
end
defp client, do: Pleroma.ReverseProxy.Client
end

View file

@ -10,10 +10,19 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilename do
"""
@behaviour Pleroma.Upload.Filter
def filter(upload) do
extension = List.last(String.split(upload.name, "."))
name = Pleroma.Config.get([__MODULE__, :text], random(extension))
{:ok, %Pleroma.Upload{upload | name: name}}
alias Pleroma.Config
alias Pleroma.Upload
def filter(%Upload{name: name} = upload) do
extension = List.last(String.split(name, "."))
name = predefined_name(extension) || random(extension)
{:ok, %Upload{upload | name: name}}
end
@spec predefined_name(String.t()) :: String.t() | nil
defp predefined_name(extension) do
with name when not is_nil(name) <- Config.get([__MODULE__, :text]),
do: String.replace(name, "{extension}", extension)
end
defp random(extension) do

View file

@ -1,51 +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.Uploaders.Swift.Keystone do
use HTTPoison.Base
def process_url(url) do
Enum.join(
[Pleroma.Config.get!([Pleroma.Uploaders.Swift, :auth_url]), url],
"/"
)
end
def process_response_body(body) do
body
|> Jason.decode!()
end
def get_token do
settings = Pleroma.Config.get(Pleroma.Uploaders.Swift)
username = Keyword.fetch!(settings, :username)
password = Keyword.fetch!(settings, :password)
tenant_id = Keyword.fetch!(settings, :tenant_id)
case post(
"/tokens",
make_auth_body(username, password, tenant_id),
["Content-Type": "application/json"],
hackney: [:insecure]
) do
{:ok, %Tesla.Env{status: 200, body: body}} ->
body["access"]["token"]["id"]
{:ok, %Tesla.Env{status: _}} ->
""
end
end
def make_auth_body(username, password, tenant) do
Jason.encode!(%{
:auth => %{
:passwordCredentials => %{
:username => username,
:password => password
},
:tenantId => tenant
}
})
end
end

View file

@ -1,29 +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.Uploaders.Swift.Client do
use HTTPoison.Base
def process_url(url) do
Enum.join(
[Pleroma.Config.get!([Pleroma.Uploaders.Swift, :storage_url]), url],
"/"
)
end
def upload_file(filename, body, content_type) do
token = Pleroma.Uploaders.Swift.Keystone.get_token()
case put("#{filename}", body, "X-Auth-Token": token, "Content-Type": content_type) do
{:ok, %Tesla.Env{status: 201}} ->
{:ok, {:file, filename}}
{:ok, %Tesla.Env{status: 401}} ->
{:error, "Unauthorized, Bad Token"}
{:error, _} ->
{:error, "Swift Upload Error"}
end
end
end

View file

@ -1,19 +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.Uploaders.Swift do
@behaviour Pleroma.Uploaders.Uploader
def get_file(name) do
{:ok, {:url, Path.join([Pleroma.Config.get!([__MODULE__, :object_url]), name])}}
end
def put_file(upload) do
Pleroma.Uploaders.Swift.Client.upload_file(
upload.path,
File.read!(upload.tmpfile),
upload.content_type
)
end
end

View file

@ -3,6 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Uploaders.Uploader do
import Pleroma.Web.Gettext
@moduledoc """
Defines the contract to put and get an uploaded file to any backend.
"""
@ -66,7 +68,7 @@ defmodule Pleroma.Uploaders.Uploader do
{:error, error}
end
after
30_000 -> {:error, "Uploader callback timeout"}
30_000 -> {:error, dgettext("errors", "Uploader callback timeout")}
end
end
end

View file

@ -9,12 +9,14 @@ defmodule Pleroma.User do
import Ecto.Query
alias Comeonin.Pbkdf2
alias Ecto.Multi
alias Pleroma.Activity
alias Pleroma.Keys
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.RepoStreamer
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
@ -50,6 +52,7 @@ defmodule Pleroma.User do
field(:avatar, :map)
field(:local, :boolean, default: true)
field(:follower_address, :string)
field(:following_address, :string)
field(:search_rank, :float, virtual: true)
field(:search_type, :integer, virtual: true)
field(:tags, {:array, :string}, default: [])
@ -105,17 +108,32 @@ defmodule Pleroma.User do
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
@spec ap_following(User.t()) :: Sring.t()
def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
def user_info(%User{} = user, args \\ %{}) do
following_count =
if args[:following_count], do: args[:following_count], else: following_count(user)
follower_count =
if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
%{
following_count: following_count(user),
note_count: user.info.note_count,
follower_count: user.info.follower_count,
locked: user.info.locked,
confirmation_pending: user.info.confirmation_pending,
default_scope: user.info.default_scope
}
|> Map.put(:following_count, following_count)
|> Map.put(:follower_count, follower_count)
end
def set_info_cache(user, args) do
Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
end
@spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
def restrict_deactivated(query) do
from(u in query,
where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
@ -150,9 +168,10 @@ defmodule Pleroma.User do
if changes.valid? do
case info_cng.changes[:source_data] do
%{"followers" => followers} ->
%{"followers" => followers, "following" => following} ->
changes
|> put_change(:follower_address, followers)
|> put_change(:following_address, following)
_ ->
followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
@ -184,7 +203,14 @@ defmodule Pleroma.User do
|> User.Info.user_upgrade(params[:info])
struct
|> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
|> cast(params, [
:bio,
:name,
:follower_address,
:following_address,
:avatar,
:last_refreshed_at
])
|> unique_constraint(:nickname)
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: 5000)
@ -193,27 +219,24 @@ defmodule Pleroma.User do
end
def password_update_changeset(struct, params) do
changeset =
struct
|> cast(params, [:password, :password_confirmation])
|> validate_required([:password, :password_confirmation])
|> validate_confirmation(:password)
OAuth.Token.delete_user_tokens(struct)
OAuth.Authorization.delete_user_authorizations(struct)
if changeset.valid? do
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
changeset
|> put_change(:password_hash, hashed)
else
changeset
end
struct
|> cast(params, [:password, :password_confirmation])
|> validate_required([:password, :password_confirmation])
|> validate_confirmation(:password)
|> put_password_hash
end
def reset_password(user, data) do
update_and_set_cache(password_update_changeset(user, data))
def reset_password(%User{id: user_id} = user, data) do
multi =
Multi.new()
|> Multi.update(:user, password_update_changeset(user, data))
|> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
|> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
case Repo.transaction(multi) do
{:ok, %{user: user} = _} -> set_cache(user)
{:error, _, changeset, _} -> {:error, changeset}
end
end
def register_changeset(struct, params \\ %{}, opts \\ []) do
@ -249,12 +272,11 @@ defmodule Pleroma.User do
end
if changeset.valid? do
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
changeset
|> put_change(:password_hash, hashed)
|> put_password_hash
|> put_change(:ap_id, ap_id)
|> unique_constraint(:ap_id)
|> put_change(:following, [followers])
@ -324,14 +346,6 @@ defmodule Pleroma.User do
end
end
def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
if not following?(follower, followed) do
follow(follower, followed)
else
{:ok, follower}
end
end
@doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
@spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
def follow_all(follower, followeds) do
@ -366,14 +380,12 @@ defmodule Pleroma.User do
end
def follow(%User{} = follower, %User{info: info} = followed) do
user_config = Application.get_env(:pleroma, :user)
deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
ap_followers = followed.follower_address
cond do
following?(follower, followed) or info.deactivated ->
{:error, "Could not follow user: #{followed.nickname} is already on your list."}
info.deactivated ->
{:error, "Could not follow user: You are deactivated."}
deny_follow_blocked and blocks?(followed, follower) ->
{:error, "Could not follow user: #{followed.nickname} blocked you."}
@ -737,126 +749,13 @@ defmodule Pleroma.User do
|> Repo.all()
end
def search(query, resolve \\ false, for_user \\ nil) do
# Strip the beginning @ off if there is a query
query = String.trim_leading(query, "@")
@spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
info = muter.info
if resolve, do: get_or_fetch(query)
{:ok, results} =
Repo.transaction(fn ->
Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
Repo.all(search_query(query, for_user))
end)
results
end
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)
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)
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
processed_query =
term
|> String.replace(~r/\W+/, " ")
|> String.trim()
|> String.split()
|> Enum.map(&(&1 <> ":*"))
|> Enum.join(" | ")
from(
u in query,
select_merge: %{
search_type: ^0,
search_rank:
fragment(
"""
ts_rank_cd(
setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
to_tsquery('simple', ?),
32
)
""",
u.nickname,
u.name,
^processed_query
)
},
where:
fragment(
"""
(setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
""",
u.nickname,
u.name,
^processed_query
)
)
|> restrict_deactivated()
end
defp trigram_search_subquery(term) 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(?, '')))",
^term,
u.nickname,
u.name
)
},
where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
)
|> restrict_deactivated()
end
def mute(muter, %User{ap_id: ap_id}) do
info_cng =
muter.info
|> User.Info.add_to_mutes(ap_id)
User.Info.add_to_mutes(info, ap_id)
|> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
cng =
change(muter)
@ -866,9 +765,11 @@ defmodule Pleroma.User do
end
def unmute(muter, %{ap_id: ap_id}) do
info = muter.info
info_cng =
muter.info
|> User.Info.remove_from_mutes(ap_id)
User.Info.remove_from_mutes(info, ap_id)
|> User.Info.remove_from_muted_notifications(info, ap_id)
cng =
change(muter)
@ -964,15 +865,18 @@ defmodule Pleroma.User do
def mutes?(nil, _), do: false
def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
def blocks?(user, %{ap_id: ap_id}) do
blocks = user.info.blocks
domain_blocks = user.info.domain_blocks
@spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
def muted_notifications?(nil, _), do: false
def muted_notifications?(user, %{ap_id: ap_id}),
do: Enum.member?(user.info.muted_notifications, ap_id)
def blocks?(%User{info: info} = _user, %{ap_id: ap_id}) do
blocks = info.blocks
domain_blocks = info.domain_blocks
%{host: host} = URI.parse(ap_id)
Enum.member?(blocks, ap_id) ||
Enum.any?(domain_blocks, fn domain ->
host == domain
end)
Enum.member?(blocks, ap_id) || Enum.any?(domain_blocks, &(&1 == host))
end
def subscribed_to?(user, %{ap_id: ap_id}) do
@ -1058,18 +962,26 @@ defmodule Pleroma.User do
@spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:delete, %User{} = user) do
{:ok, user} = User.deactivate(user)
{:ok, _user} = ActivityPub.delete(user)
# Remove all relationships
{:ok, followers} = User.get_followers(user)
Enum.each(followers, fn follower -> User.unfollow(follower, user) end)
Enum.each(followers, fn follower ->
ActivityPub.unfollow(follower, user)
User.unfollow(follower, user)
end)
{:ok, friends} = User.get_friends(user)
Enum.each(friends, fn followed -> User.unfollow(user, followed) end)
Enum.each(friends, fn followed ->
ActivityPub.unfollow(user, followed)
User.unfollow(user, followed)
end)
delete_user_activities(user)
invalidate_cache(user)
Repo.delete(user)
end
@spec perform(atom(), User.t()) :: {:ok, User.t()}
@ -1125,6 +1037,34 @@ defmodule Pleroma.User do
)
end
@spec external_users_query() :: Ecto.Query.t()
def external_users_query do
User.Query.build(%{
external: true,
active: true,
order_by: :id
})
end
@spec external_users(keyword()) :: [User.t()]
def external_users(opts \\ []) do
query =
external_users_query()
|> select([u], struct(u, [:id, :ap_id, :info]))
query =
if opts[:max_id],
do: where(query, [u], u.id > ^opts[:max_id]),
else: query
query =
if opts[:limit],
do: limit(query, ^opts[:limit]),
else: query
Repo.all(query)
end
def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
do:
PleromaJobQueue.enqueue(:background, __MODULE__, [
@ -1142,18 +1082,35 @@ defmodule Pleroma.User do
])
def delete_user_activities(%User{ap_id: ap_id} = user) do
stream =
ap_id
|> Activity.query_by_actor()
|> Repo.stream()
Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity)
ap_id
|> Activity.query_by_actor()
|> RepoStreamer.chunk_stream(50)
|> Stream.each(fn activities ->
Enum.each(activities, &delete_activity(&1))
end)
|> Stream.run()
{:ok, user}
end
defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
Object.normalize(activity) |> ActivityPub.delete()
activity
|> Object.normalize()
|> ActivityPub.delete()
end
defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
user = get_cached_by_ap_id(activity.actor)
object = Object.normalize(activity)
ActivityPub.unlike(user, object)
end
defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
user = get_cached_by_ap_id(activity.actor)
object = Object.normalize(activity)
ActivityPub.unannounce(user, object)
end
defp delete_activity(_activity), do: "Doing nothing"
@ -1162,9 +1119,7 @@ defmodule Pleroma.User do
Pleroma.HTML.Scrubber.TwitterText
end
@default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
def html_filter_policy(_), do: @default_scrubbers
def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
def fetch_by_ap_id(ap_id) do
ap_try = ActivityPub.make_user_from_ap_id(ap_id)
@ -1235,10 +1190,12 @@ defmodule Pleroma.User do
end
# OStatus Magic Key
def public_key_from_info(%{magic_key: magic_key}) do
def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
{:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
end
def public_key_from_info(_), do: {:error, "not found key"}
def get_public_key_for_ap_id(ap_id) do
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
{:ok, public_key} <- public_key_from_info(user.info) do
@ -1424,23 +1381,34 @@ defmodule Pleroma.User do
}
end
def ensure_keys_present(user) do
info = user.info
def ensure_keys_present(%User{info: info} = user) do
if info.keys do
{:ok, user}
else
{:ok, pem} = Keys.generate_rsa_pem()
info_cng =
info
|> User.Info.set_keys(pem)
cng =
Ecto.Changeset.change(user)
|> Ecto.Changeset.put_embed(:info, info_cng)
update_and_set_cache(cng)
user
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
|> update_and_set_cache()
end
end
def get_ap_ids_by_nicknames(nicknames) do
from(u in User,
where: u.nickname in ^nicknames,
select: u.ap_id
)
|> Repo.all()
end
defdelegate search(query, opts \\ []), to: User.Search
defp put_password_hash(
%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
) do
change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
end
defp put_password_hash(changeset), do: changeset
end

View file

@ -24,6 +24,7 @@ defmodule Pleroma.User.Info do
field(:domain_blocks, {:array, :string}, default: [])
field(:mutes, {:array, :string}, default: [])
field(:muted_reblogs, {:array, :string}, default: [])
field(:muted_notifications, {:array, :string}, default: [])
field(:subscribers, {:array, :string}, default: [])
field(:deactivated, :boolean, default: false)
field(:no_rich_text, :boolean, default: false)
@ -42,14 +43,21 @@ defmodule Pleroma.User.Info do
field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true)
field(:pinned_activities, {:array, :string}, default: [])
field(:flavour, :string, default: nil)
field(:mascot, :map, default: nil)
field(:emoji, {:array, :map}, default: [])
field(:pleroma_settings_store, :map, default: %{})
field(:notification_settings, :map,
default: %{"remote" => true, "local" => true, "followers" => true, "follows" => true}
default: %{
"followers" => true,
"follows" => true,
"non_follows" => true,
"non_followers" => true
}
)
field(:skip_thread_containment, :boolean, default: false)
# Found in the wild
# ap_id -> Where is this used?
# bio -> Where is this used?
@ -68,10 +76,15 @@ defmodule Pleroma.User.Info do
end
def update_notification_settings(info, settings) do
settings =
settings
|> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end)
|> Map.new()
notification_settings =
info.notification_settings
|> Map.merge(settings)
|> Map.take(["remote", "local", "followers", "follows"])
|> Map.take(["followers", "follows", "non_follows", "non_followers"])
params = %{notification_settings: notification_settings}
@ -108,6 +121,16 @@ defmodule Pleroma.User.Info do
|> validate_required([:mutes])
end
@spec set_notification_mutes(Changeset.t(), [String.t()], boolean()) :: Changeset.t()
def set_notification_mutes(changeset, muted_notifications, notifications?) do
if notifications? do
put_change(changeset, :muted_notifications, muted_notifications)
|> validate_required([:muted_notifications])
else
changeset
end
end
def set_blocks(info, blocks) do
params = %{blocks: blocks}
@ -124,14 +147,31 @@ defmodule Pleroma.User.Info do
|> validate_required([:subscribers])
end
@spec add_to_mutes(Info.t(), String.t()) :: Changeset.t()
def add_to_mutes(info, muted) do
set_mutes(info, Enum.uniq([muted | info.mutes]))
end
@spec add_to_muted_notifications(Changeset.t(), Info.t(), String.t(), boolean()) ::
Changeset.t()
def add_to_muted_notifications(changeset, info, muted, notifications?) do
set_notification_mutes(
changeset,
Enum.uniq([muted | info.muted_notifications]),
notifications?
)
end
@spec remove_from_mutes(Info.t(), String.t()) :: Changeset.t()
def remove_from_mutes(info, muted) do
set_mutes(info, List.delete(info.mutes, muted))
end
@spec remove_from_muted_notifications(Changeset.t(), Info.t(), String.t()) :: Changeset.t()
def remove_from_muted_notifications(changeset, info, muted) do
set_notification_mutes(changeset, List.delete(info.muted_notifications, muted), true)
end
def add_to_block(info, blocked) do
set_blocks(info, Enum.uniq([blocked | info.blocks]))
end
@ -209,7 +249,9 @@ defmodule Pleroma.User.Info do
:hide_followers,
:hide_favorites,
:background,
:show_role
:show_role,
:skip_thread_containment,
:pleroma_settings_store
])
end
@ -241,14 +283,6 @@ defmodule Pleroma.User.Info do
|> validate_required([:settings])
end
def mastodon_flavour_update(info, flavour) do
params = %{flavour: flavour}
info
|> cast(params, [:flavour])
|> validate_required([:flavour])
end
def mascot_update(info, url) do
params = %{mascot: url}

View file

@ -7,7 +7,7 @@ defmodule Pleroma.User.Query do
User query builder module. Builds query from new query or another user query.
## Example:
query = Pleroma.User.Query(%{nickname: "nickname"})
query = Pleroma.User.Query.build(%{nickname: "nickname"})
another_query = Pleroma.User.Query.build(query, %{email: "email@example.com"})
Pleroma.Repo.all(query)
Pleroma.Repo.all(another_query)
@ -47,7 +47,10 @@ defmodule Pleroma.User.Query do
friends: User.t(),
recipients_from_activity: [String.t()],
nickname: [String.t()],
ap_id: [String.t()]
ap_id: [String.t()],
order_by: term(),
select: term(),
limit: pos_integer()
}
| %{}
@ -141,6 +144,18 @@ defmodule Pleroma.User.Query do
where(query, [u], u.ap_id in ^to or fragment("? && ?", u.following, ^to))
end
defp compose_query({:order_by, key}, query) do
order_by(query, [u], field(u, ^key))
end
defp compose_query({:select, keys}, query) do
select(query, [u], ^keys)
end
defp compose_query({:limit, limit}, query) do
limit(query, ^limit)
end
defp compose_query(_unsupported_param, query), do: query
defp prepare_tag_criteria(tag, query) do

223
lib/pleroma/user/search.ex Normal file
View file

@ -0,0 +1,223 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.Search do
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.User
import Ecto.Query
@similarity_threshold 0.25
@limit 20
def search(query_string, opts \\ []) do
resolve = Keyword.get(opts, :resolve, false)
following = Keyword.get(opts, :following, false)
result_limit = Keyword.get(opts, :limit, @limit)
offset = Keyword.get(opts, :offset, 0)
for_user = Keyword.get(opts, :for_user)
query_string = format_query(query_string)
maybe_resolve(resolve, for_user, query_string)
{:ok, results} =
Repo.transaction(fn ->
Ecto.Adapters.SQL.query(
Repo,
"select set_limit(#{@similarity_threshold})",
[]
)
query_string
|> search_query(for_user, following)
|> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset)
end)
results
end
defp format_query(query_string) do
# Strip the beginning @ off if there is a query
query_string = String.trim_leading(query_string, "@")
with [name, domain] <- String.split(query_string, "@"),
formatted_domain <- String.replace(domain, ~r/[!-\-|@|[-`|{-~|\/|:]+/, "") do
name <> "@" <> to_string(:idna.encode(formatted_domain))
else
_ -> query_string
end
end
defp search_query(query_string, for_user, following) do
for_user
|> base_query(following)
|> filter_blocked_user(for_user)
|> filter_blocked_domains(for_user)
|> search_subqueries(query_string)
|> union_subqueries
|> distinct_query()
|> boost_search_rank_query(for_user)
|> subquery()
|> order_by(desc: :search_rank)
|> maybe_restrict_local(for_user)
end
defp base_query(_user, false), do: User
defp base_query(user, true), do: User.get_followers_query(user)
defp filter_blocked_user(query, %User{info: %{blocks: blocks}})
when length(blocks) > 0 do
from(q in query, where: not (q.ap_id in ^blocks))
end
defp filter_blocked_user(query, _), do: query
defp filter_blocked_domains(query, %User{info: %{domain_blocks: domain_blocks}})
when length(domain_blocks) > 0 do
domains = Enum.join(domain_blocks, ",")
from(
q in query,
where: fragment("substring(ap_id from '.*://([^/]*)') NOT IN (?)", ^domains)
)
end
defp filter_blocked_domains(query, _), do: query
defp union_subqueries({fts_subquery, trigram_subquery}) do
from(s in trigram_subquery, union_all: ^fts_subquery)
end
defp search_subqueries(base_query, query_string) do
{
fts_search_subquery(base_query, query_string),
trigram_search_subquery(base_query, query_string)
}
end
defp distinct_query(q) do
from(s in subquery(q), order_by: s.search_type, distinct: s.id)
end
defp maybe_resolve(true, user, query) do
case {limit(), user} do
{:all, _} -> :noop
{:unauthenticated, %User{}} -> User.get_or_fetch(query)
{:unauthenticated, _} -> :noop
{false, _} -> User.get_or_fetch(query)
end
end
defp maybe_resolve(_, _, _), do: :noop
defp maybe_restrict_local(q, user) do
case {limit(), user} do
{:all, _} -> restrict_local(q)
{:unauthenticated, %User{}} -> q
{:unauthenticated, _} -> restrict_local(q)
{false, _} -> q
end
end
defp limit, do: Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated)
defp restrict_local(q), do: where(q, [u], u.local == true)
defp boost_search_rank_query(query, nil), do: query
defp boost_search_rank_query(query, for_user) do
friends_ids = User.get_friends_ids(for_user)
followers_ids = User.get_followers_ids(for_user)
from(u in subquery(query),
select_merge: %{
search_rank:
fragment(
"""
CASE WHEN (?) THEN 0.5 + (?) * 1.3
WHEN (?) THEN 0.5 + (?) * 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
@spec fts_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t()
defp fts_search_subquery(query, term) do
processed_query =
String.trim_trailing(term, "@" <> local_domain())
|> String.replace(~r/[!-\/|@|[-`|{-~|:-?]+/, " ")
|> String.trim()
|> String.split()
|> Enum.map(&(&1 <> ":*"))
|> Enum.join(" | ")
from(
u in query,
select_merge: %{
search_type: ^0,
search_rank:
fragment(
"""
ts_rank_cd(
setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
to_tsquery('simple', ?),
32
)
""",
u.nickname,
u.name,
^processed_query
)
},
where:
fragment(
"""
(setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
""",
u.nickname,
u.name,
^processed_query
)
)
|> User.restrict_deactivated()
end
@spec trigram_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t()
defp trigram_search_subquery(query, term) do
term = String.trim_trailing(term, "@" <> local_domain())
from(
u in query,
select_merge: %{
# ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
search_type: fragment("?", 1),
search_rank:
fragment(
"similarity(?, trim(? || ' ' || coalesce(?, '')))",
^term,
u.nickname,
u.name
)
},
where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
)
|> User.restrict_deactivated()
end
defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
end

View file

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.WelcomeMessage do
alias Pleroma.User
alias Pleroma.Web.CommonAPI

View file

@ -4,9 +4,11 @@
defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Object.Fetcher
alias Pleroma.Pagination
alias Pleroma.Repo
@ -25,19 +27,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
# For Announce activities, we filter the recipients based on following status for any actors
# that match actual users. See issue #164 for more information about why this is necessary.
defp get_recipients(%{"type" => "Announce"} = data) do
to = data["to"] || []
cc = data["cc"] || []
to = Map.get(data, "to", [])
cc = Map.get(data, "cc", [])
bcc = Map.get(data, "bcc", [])
actor = User.get_cached_by_ap_id(data["actor"])
recipients =
(to ++ cc)
|> Enum.filter(fn recipient ->
Enum.filter(Enum.concat([to, cc, bcc]), fn recipient ->
case User.get_cached_by_ap_id(recipient) do
nil ->
true
user ->
User.following?(user, actor)
nil -> true
user -> User.following?(user, actor)
end
end)
@ -45,17 +44,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
defp get_recipients(%{"type" => "Create"} = data) do
to = data["to"] || []
cc = data["cc"] || []
actor = data["actor"] || []
recipients = (to ++ cc ++ [actor]) |> Enum.uniq()
to = Map.get(data, "to", [])
cc = Map.get(data, "cc", [])
bcc = Map.get(data, "bcc", [])
actor = Map.get(data, "actor", [])
recipients = [to, cc, bcc, [actor]] |> Enum.concat() |> Enum.uniq()
{recipients, to, cc}
end
defp get_recipients(data) do
to = data["to"] || []
cc = data["cc"] || []
recipients = to ++ cc
to = Map.get(data, "to", [])
cc = Map.get(data, "cc", [])
bcc = Map.get(data, "bcc", [])
recipients = Enum.concat([to, cc, bcc])
{recipients, to, cc}
end
@ -73,7 +74,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do
limit = Pleroma.Config.get([:instance, :remote_limit])
limit = Config.get([:instance, :remote_limit])
String.length(content) <= limit
end
@ -108,6 +109,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def decrease_replies_count_if_reply(_object), do: :noop
def increase_poll_votes_if_vote(%{
"object" => %{"inReplyTo" => reply_ap_id, "name" => name},
"type" => "Create"
}) do
Object.increase_vote_count(reply_ap_id, name)
end
def increase_poll_votes_if_vote(_create_data), 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, fake),
@ -116,6 +126,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{:ok, map} <- MRF.filter(map),
{recipients, _, _} = get_recipients(map),
{:fake, false, map, recipients} <- {:fake, fake, map, recipients},
:ok <- Containment.contain_child(map),
{:ok, map, object} <- insert_full_object(map) do
{:ok, activity} =
Repo.insert(%Activity{
@ -179,44 +190,62 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end)
end
def stream_out_participations(%Object{data: %{"context" => context}}, user) do
with %Conversation{} = conversation <- Conversation.get_for_ap_id(context),
conversation = Repo.preload(conversation, :participations),
last_activity_id =
fetch_latest_activity_id_for_context(conversation.ap_id, %{
"user" => user,
"blocking_user" => user
}) do
if last_activity_id do
stream_out_participations(conversation.participations)
end
end
end
def stream_out_participations(_, _), do: :noop
def stream_out(activity) do
public = "https://www.w3.org/ns/activitystreams#Public"
if activity.data["type"] in ["Create", "Announce", "Delete"] do
Pleroma.Web.Streamer.stream("user", activity)
Pleroma.Web.Streamer.stream("list", activity)
object = Object.normalize(activity)
# Do not stream out poll replies
unless object.data["type"] == "Answer" do
Pleroma.Web.Streamer.stream("user", activity)
Pleroma.Web.Streamer.stream("list", activity)
if Enum.member?(activity.data["to"], public) do
Pleroma.Web.Streamer.stream("public", activity)
if Enum.member?(activity.data["to"], public) do
Pleroma.Web.Streamer.stream("public", activity)
if activity.local do
Pleroma.Web.Streamer.stream("public:local", activity)
end
if activity.local do
Pleroma.Web.Streamer.stream("public:local", activity)
end
if activity.data["type"] in ["Create"] do
object = Object.normalize(activity)
if activity.data["type"] in ["Create"] do
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)
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 object.data["attachment"] != [] do
Pleroma.Web.Streamer.stream("public:media", activity)
if object.data["attachment"] != [] do
Pleroma.Web.Streamer.stream("public:media", activity)
if activity.local do
Pleroma.Web.Streamer.stream("public:local:media", activity)
if activity.local do
Pleroma.Web.Streamer.stream("public:local:media", activity)
end
end
end
else
# TODO: Write test, replace with visibility test
if !Enum.member?(activity.data["cc"] || [], public) &&
!Enum.member?(
activity.data["to"],
User.get_cached_by_ap_id(activity.data["actor"]).follower_address
),
do: Pleroma.Web.Streamer.stream("direct", activity)
end
else
# TODO: Write test, replace with visibility test
if !Enum.member?(activity.data["cc"] || [], public) &&
!Enum.member?(
activity.data["to"],
User.get_cached_by_ap_id(activity.data["actor"]).follower_address
),
do: Pleroma.Web.Streamer.stream("direct", activity)
end
end
end
@ -235,6 +264,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{:ok, activity} <- insert(create_data, local, fake),
{:fake, false, activity} <- {:fake, fake, activity},
_ <- increase_replies_count_if_reply(create_data),
_ <- increase_poll_votes_if_vote(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),
@ -376,6 +406,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
def delete(%User{ap_id: ap_id, follower_address: follower_address} = user) do
with data <- %{
"to" => [follower_address],
"type" => "Delete",
"actor" => ap_id,
"object" => %{"type" => "Person", "id" => ap_id}
},
{:ok, activity} <- insert(data, true, true),
:ok <- maybe_federate(activity) do
{:ok, user}
end
end
def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do
user = User.get_cached_by_ap_id(actor)
to = (object.data["to"] || []) ++ (object.data["cc"] || [])
@ -388,7 +431,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
"to" => to,
"deleted_activity_id" => activity && activity.id
},
{:ok, activity} <- insert(data, local),
{:ok, activity} <- insert(data, local, false),
stream_out_participations(object, user),
_ <- decrease_replies_count_if_reply(object),
# Changing note count prior to enqueuing federation task in order to avoid
# race conditions on updating user.info
@ -399,16 +443,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
def block(blocker, blocked, activity_id \\ nil, local \\ true) do
ap_config = Application.get_env(:pleroma, :activitypub)
unfollow_blocked = Keyword.get(ap_config, :unfollow_blocked)
outgoing_blocks = Keyword.get(ap_config, :outgoing_blocks)
outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
with true <- unfollow_blocked do
if unfollow_blocked do
follow_activity = fetch_latest_follow(blocker, blocked)
if follow_activity do
unfollow(blocker, blocked, nil, local)
end
if follow_activity, do: unfollow(blocker, blocked, nil, local)
end
with true <- outgoing_blocks,
@ -480,6 +520,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public
from(activity in Activity)
|> maybe_preload_objects(opts)
|> restrict_blocked(opts)
|> restrict_recipients(recipients, opts["user"])
|> where(
@ -492,6 +533,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
^context
)
)
|> exclude_poll_votes(opts)
|> order_by([activity], desc: activity.id)
end
@ -499,7 +541,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def fetch_activities_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(opts)
|> Activity.with_preloaded_object()
|> Repo.all()
end
@ -507,7 +548,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
Pleroma.FlakeId.t() | nil
def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(opts)
|> fetch_activities_for_context_query(Map.merge(%{"skip_preload" => true}, opts))
|> limit(1)
|> select([a], a.id)
|> Repo.one()
@ -548,14 +589,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_visibility(query, %{visibility: visibility})
when visibility in @valid_visibilities do
query =
from(
a in query,
where:
fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility)
)
query
from(
a in query,
where:
fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility)
)
end
defp restrict_visibility(_query, %{visibility: visibility})
@ -565,17 +603,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_visibility(query, _visibility), do: query
defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}) do
query =
from(
a in query,
where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data)
)
defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _),
do: query
query
defp restrict_thread_visibility(
query,
%{"user" => %User{info: %{skip_thread_containment: true}}},
_
),
do: query
defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}, _) do
from(
a in query,
where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data)
)
end
defp restrict_thread_visibility(query, _), do: query
defp restrict_thread_visibility(query, _, _), do: query
def fetch_user_activities(user, reading_user, params \\ %{}) do
params =
@ -653,20 +698,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_tag(query, _), do: query
defp restrict_to_cc(query, recipients_to, recipients_cc) do
from(
activity in query,
where:
fragment(
"(?->'to' \\?| ?) or (?->'cc' \\?| ?)",
activity.data,
^recipients_to,
activity.data,
^recipients_cc
)
)
end
defp restrict_recipients(query, [], _user), do: query
defp restrict_recipients(query, recipients, nil) do
@ -820,6 +851,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_muted_reblogs(query, _), do: query
defp exclude_poll_votes(query, %{"include_poll_votes" => "true"}), do: query
defp exclude_poll_votes(query, _) do
if has_named_binding?(query, :object) do
from([activity, object: o] in query,
where: fragment("not(?->>'type' = ?)", o.data, "Answer")
)
else
query
end
end
defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query
defp maybe_preload_objects(query, _) do
@ -854,9 +897,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp maybe_order(query, _), do: query
def fetch_activities_query(recipients, opts \\ %{}) do
base_query = from(activity in Activity)
config = %{
skip_thread_containment: Config.get([:instance, :skip_thread_containment])
}
base_query
Activity
|> maybe_preload_objects(opts)
|> maybe_preload_bookmarks(opts)
|> maybe_set_thread_muted_field(opts)
@ -875,23 +920,53 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_muted(opts)
|> restrict_media(opts)
|> restrict_visibility(opts)
|> restrict_thread_visibility(opts)
|> restrict_thread_visibility(opts, config)
|> restrict_replies(opts)
|> restrict_reblogs(opts)
|> restrict_pinned(opts)
|> restrict_muted_reblogs(opts)
|> Activity.restrict_deactivated_users()
|> exclude_poll_votes(opts)
end
def fetch_activities(recipients, opts \\ %{}) do
fetch_activities_query(recipients, opts)
list_memberships = Pleroma.List.memberships(opts["user"])
fetch_activities_query(recipients ++ list_memberships, opts)
|> Pagination.fetch_paginated(opts)
|> Enum.reverse()
|> maybe_update_cc(list_memberships, opts["user"])
end
def fetch_activities_bounded(recipients_to, recipients_cc, opts \\ %{}) do
defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id})
when is_list(list_memberships) and length(list_memberships) > 0 do
Enum.map(activities, fn
%{data: %{"bcc" => bcc}} = activity when is_list(bcc) and length(bcc) > 0 ->
if Enum.any?(bcc, &(&1 in list_memberships)) do
update_in(activity.data["cc"], &[user_ap_id | &1])
else
activity
end
activity ->
activity
end)
end
defp maybe_update_cc(activities, _, _), do: activities
def fetch_activities_bounded_query(query, recipients, recipients_with_public) do
from(activity in query,
where:
fragment("? && ?", activity.recipients, ^recipients) or
(fragment("? && ?", activity.recipients, ^recipients_with_public) and
"https://www.w3.org/ns/activitystreams#Public" in activity.recipients)
)
end
def fetch_activities_bounded(recipients, recipients_with_public, opts \\ %{}) do
fetch_activities_query([], opts)
|> restrict_to_cc(recipients_to, recipients_cc)
|> fetch_activities_bounded_query(recipients, recipients_with_public)
|> Pagination.fetch_paginated(opts)
|> Enum.reverse()
end
@ -938,6 +1013,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
avatar: avatar,
name: data["name"],
follower_address: data["followers"],
following_address: data["following"],
bio: data["summary"]
}

View file

@ -27,13 +27,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
plug(:relay_active? when action in [:relay])
def relay_active?(conn, _) do
if Keyword.get(Application.get_env(:pleroma, :instance), :allow_relay) do
if Pleroma.Config.get([:instance, :allow_relay]) do
conn
else
conn
|> put_status(404)
|> json(%{error: "not found"})
|> halt
|> render_error(:not_found, "not found")
|> halt()
end
end
@ -104,43 +103,57 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
end
def following(conn, %{"nickname" => nickname, "page" => page}) do
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
{:show_follows, true} <-
{:show_follows, (for_user && for_user == user) || !user.info.hide_follows} do
{page, _} = Integer.parse(page)
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("following.json", %{user: user, page: page}))
|> json(UserView.render("following.json", %{user: user, page: page, for: for_user}))
else
{:show_follows, _} ->
conn
|> put_resp_header("content-type", "application/activity+json")
|> send_resp(403, "")
end
end
def following(conn, %{"nickname" => nickname}) do
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("following.json", %{user: user}))
|> json(UserView.render("following.json", %{user: user, for: for_user}))
end
end
def followers(conn, %{"nickname" => nickname, "page" => page}) do
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
{:show_followers, true} <-
{:show_followers, (for_user && for_user == user) || !user.info.hide_followers} do
{page, _} = Integer.parse(page)
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("followers.json", %{user: user, page: page}))
|> json(UserView.render("followers.json", %{user: user, page: page, for: for_user}))
else
{:show_followers, _} ->
conn
|> put_resp_header("content-type", "application/activity+json")
|> send_resp(403, "")
end
end
def followers(conn, %{"nickname" => nickname}) do
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("followers.json", %{user: user}))
|> json(UserView.render("followers.json", %{user: user, for: for_user}))
end
end
@ -190,7 +203,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
Logger.info(inspect(conn.req_headers))
end
json(conn, "error")
json(conn, dgettext("errors", "error"))
end
def relay(conn, _params) do
@ -218,9 +231,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("inbox.json", %{user: user, max_id: params["max_id"]}))
else
err =
dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
nickname: nickname,
as_nickname: user.nickname
)
conn
|> put_status(:forbidden)
|> json("can't read inbox of #{nickname} as #{user.nickname}")
|> json(err)
end
end
@ -246,7 +265,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
{:ok, delete} <- ActivityPub.delete(object) do
{:ok, delete}
else
_ -> {:error, "Can't delete object"}
_ -> {:error, dgettext("errors", "Can't delete object")}
end
end
@ -255,12 +274,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
{:ok, activity, _object} <- ActivityPub.like(user, object) do
{:ok, activity}
else
_ -> {:error, "Can't like object"}
_ -> {:error, dgettext("errors", "Can't like object")}
end
end
def handle_user_activity(_, _) do
{:error, "Unhandled activity type"}
{:error, dgettext("errors", "Unhandled activity type")}
end
def update_outbox(
@ -288,22 +307,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> json(message)
end
else
err =
dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
nickname: nickname,
as_nickname: user.nickname
)
conn
|> put_status(:forbidden)
|> json("can't update outbox of #{nickname} as #{user.nickname}")
|> json(err)
end
end
def errors(conn, {:error, :not_found}) do
conn
|> put_status(404)
|> json("Not found")
|> put_status(:not_found)
|> json(dgettext("errors", "Not found"))
end
def errors(conn, _e) do
conn
|> put_status(500)
|> json("error")
|> put_status(:internal_server_error)
|> json(dgettext("errors", "error"))
end
defp set_requester_reachable(%Plug.Conn{} = conn, _) do
@ -314,4 +339,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
conn
end
defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
{:ok, new_user} = User.ensure_keys_present(user)
for_user =
if new_user != user and match?(%User{}, for_user) do
User.get_cached_by_nickname(for_user.nickname)
else
for_user
end
{new_user, for_user}
end
end

View file

@ -5,8 +5,8 @@
defmodule Pleroma.Web.ActivityPub.MRF do
@callback filter(Map.t()) :: {:ok | :reject, Map.t()}
def filter(object) do
get_policies()
def filter(policies, %{} = object) do
policies
|> Enum.reduce({:ok, object}, fn
policy, {:ok, object} ->
policy.filter(object)
@ -16,10 +16,10 @@ defmodule Pleroma.Web.ActivityPub.MRF do
end)
end
def filter(%{} = object), do: get_policies() |> filter(object)
def get_policies do
Application.get_env(:pleroma, :instance, [])
|> Keyword.get(:rewrite_policy, [])
|> get_policies()
Pleroma.Config.get([:instance, :rewrite_policy], []) |> get_policies()
end
defp get_policies(policy) when is_atom(policy), do: [policy]

View file

@ -0,0 +1,48 @@
# Pleroma: A lightweight social networking server
# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do
alias Pleroma.User
require Logger
# has the user successfully posted before?
defp old_user?(%User{} = u) do
u.info.note_count > 0 || u.info.follower_count > 0
end
# does the post contain links?
defp contains_links?(%{"content" => content} = _object) do
content
|> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"],a.zrl")
|> Floki.attribute("a", "href")
|> length() > 0
end
defp contains_links?(_), do: false
def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message) do
with {:ok, %User{} = u} <- User.get_or_fetch_by_ap_id(actor),
{:contains_links, true} <- {:contains_links, contains_links?(object)},
{:old_user, true} <- {:old_user, old_user?(u)} do
{:ok, message}
else
{:contains_links, false} ->
{:ok, message}
{:old_user, false} ->
{:reject, nil}
{:error, _} ->
{:reject, nil}
e ->
Logger.warn("[MRF anti-link-spam] WTF: unhandled error #{inspect(e)}")
{:reject, nil}
end
end
# in all other cases, pass through
def filter(message), do: {:ok, message}
end

View file

@ -9,8 +9,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do
@behaviour Pleroma.Web.ActivityPub.MRF
@reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless])
def filter_by_summary(
%{"summary" => parent_summary} = _parent,
%{data: %{"summary" => parent_summary}} = _in_reply_to,
%{"summary" => child_summary} = child
)
when not is_nil(child_summary) and byte_size(child_summary) > 0 and
@ -24,17 +25,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do
end
end
def filter_by_summary(_parent, child), do: child
def filter(%{"type" => activity_type} = object) when activity_type == "Create" do
child = object["object"]
in_reply_to = Object.normalize(child["inReplyTo"])
def filter_by_summary(_in_reply_to, child), do: child
def filter(%{"type" => "Create", "object" => child_object} = object) do
child =
if(in_reply_to,
do: filter_by_summary(in_reply_to.data, child),
else: child
)
child_object["inReplyTo"]
|> Object.normalize(child_object["inReplyTo"])
|> filter_by_summary(child_object)
object = Map.put(object, "object", child)

View file

@ -0,0 +1,56 @@
# Pleroma: A lightweight social networking server
# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
@moduledoc "Preloads any attachments in the MediaProxy cache by prefetching them"
@behaviour Pleroma.Web.ActivityPub.MRF
alias Pleroma.HTTP
alias Pleroma.Web.MediaProxy
require Logger
@hackney_options [
pool: :media,
recv_timeout: 10_000
]
def perform(:prefetch, url) do
Logger.info("Prefetching #{inspect(url)}")
url
|> MediaProxy.url()
|> HTTP.get([], adapter: @hackney_options)
end
def perform(:preload, %{"object" => %{"attachment" => attachments}} = _message) do
Enum.each(attachments, fn
%{"url" => url} when is_list(url) ->
url
|> Enum.each(fn
%{"href" => href} ->
PleromaJobQueue.enqueue(:background, __MODULE__, [:prefetch, href])
x ->
Logger.debug("Unhandled attachment URL object #{inspect(x)}")
end)
x ->
Logger.debug("Unhandled attachment #{inspect(x)}")
end)
end
@impl true
def filter(
%{"type" => "Create", "object" => %{"attachment" => attachments} = _object} = message
)
when is_list(attachments) and length(attachments) > 0 do
PleromaJobQueue.enqueue(:background, __MODULE__, [:preload, message])
{:ok, message}
end
@impl true
def filter(message), do: {:ok, message}
end

View file

@ -10,19 +10,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do
def filter(
%{
"type" => "Create",
"object" => %{"content" => content, "attachment" => _attachment} = child_object
"object" => %{"content" => content, "attachment" => _} = _child_object
} = object
)
when content in [".", "<p>.</p>"] do
child_object =
child_object
|> Map.put("content", "")
object =
object
|> Map.put("object", child_object)
{:ok, object}
{:ok, put_in(object, ["object", "content"], "")}
end
@impl true

View file

@ -8,18 +8,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do
@behaviour Pleroma.Web.ActivityPub.MRF
def filter(%{"type" => activity_type} = object) when activity_type == "Create" do
def filter(%{"type" => "Create", "object" => child_object} = object) do
scrub_policy = Pleroma.Config.get([:mrf_normalize_markup, :scrub_policy])
child = object["object"]
content =
child["content"]
child_object["content"]
|> HTML.filter_tags(scrub_policy)
child = Map.put(child, "content", content)
object = Map.put(object, "object", child)
object = put_in(object, ["object", "content"], content)
{:ok, object}
end

View file

@ -3,46 +3,42 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
alias Pleroma.User
@moduledoc "Rejects non-public (followers-only, direct) activities"
alias Pleroma.Config
alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF
@public "https://www.w3.org/ns/activitystreams#Public"
@impl true
def filter(%{"type" => "Create"} = object) do
user = User.get_cached_by_ap_id(object["actor"])
public = "https://www.w3.org/ns/activitystreams#Public"
# Determine visibility
visibility =
cond do
public in object["to"] -> "public"
public in object["cc"] -> "unlisted"
@public in object["to"] -> "public"
@public in object["cc"] -> "unlisted"
user.follower_address in object["to"] -> "followers"
true -> "direct"
end
policy = Pleroma.Config.get(:mrf_rejectnonpublic)
policy = Config.get(:mrf_rejectnonpublic)
case visibility do
"public" ->
cond do
visibility in ["public", "unlisted"] ->
{:ok, object}
"unlisted" ->
visibility == "followers" and Keyword.get(policy, :allow_followersonly) ->
{:ok, object}
"followers" ->
with true <- Keyword.get(policy, :allow_followersonly) do
{:ok, object}
else
_e -> {:reject, nil}
end
visibility == "direct" and Keyword.get(policy, :allow_direct) ->
{:ok, object}
"direct" ->
with true <- Keyword.get(policy, :allow_direct) do
{:ok, object}
else
_e -> {:reject, nil}
end
true ->
{:reject, nil}
end
end

View file

@ -0,0 +1,40 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicy do
alias Pleroma.Config
alias Pleroma.Web.ActivityPub.MRF
require Logger
@behaviour MRF
defp lookup_subchain(actor) do
with matches <- Config.get([:mrf_subchain, :match_actor]),
{match, subchain} <- Enum.find(matches, fn {k, _v} -> String.match?(actor, k) end) do
{:ok, match, subchain}
else
_e -> {:error, :notfound}
end
end
@impl true
def filter(%{"actor" => actor} = message) do
with {:ok, match, subchain} <- lookup_subchain(actor) do
Logger.debug(
"[SubchainPolicy] Matched #{actor} against #{inspect(match)} with subchain #{
inspect(subchain)
}"
)
subchain
|> MRF.filter(message)
else
_e -> {:ok, message}
end
end
@impl true
def filter(message), do: {:ok, message}
end

View file

@ -19,12 +19,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
- `mrf_tag:disable-any-subscription`: Reject any follow requests
"""
@public "https://www.w3.org/ns/activitystreams#Public"
defp get_tags(%User{tags: tags}) when is_list(tags), do: tags
defp get_tags(_), do: []
defp process_tag(
"mrf_tag:media-force-nsfw",
%{"type" => "Create", "object" => %{"attachment" => child_attachment} = object} = message
%{
"type" => "Create",
"object" => %{"attachment" => child_attachment} = object
} = message
)
when length(child_attachment) > 0 do
tags = (object["tag"] || []) ++ ["nsfw"]
@ -41,7 +46,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
defp process_tag(
"mrf_tag:media-strip",
%{"type" => "Create", "object" => %{"attachment" => child_attachment} = object} = message
%{
"type" => "Create",
"object" => %{"attachment" => child_attachment} = object
} = message
)
when length(child_attachment) > 0 do
object = Map.delete(object, "attachment")
@ -52,19 +60,22 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
defp process_tag(
"mrf_tag:force-unlisted",
%{"type" => "Create", "to" => to, "cc" => cc, "actor" => actor} = message
%{
"type" => "Create",
"to" => to,
"cc" => cc,
"actor" => actor,
"object" => object
} = message
) do
user = User.get_cached_by_ap_id(actor)
if Enum.member?(to, "https://www.w3.org/ns/activitystreams#Public") do
to =
List.delete(to, "https://www.w3.org/ns/activitystreams#Public") ++ [user.follower_address]
cc =
List.delete(cc, user.follower_address) ++ ["https://www.w3.org/ns/activitystreams#Public"]
if Enum.member?(to, @public) do
to = List.delete(to, @public) ++ [user.follower_address]
cc = List.delete(cc, user.follower_address) ++ [@public]
object =
message["object"]
object
|> Map.put("to", to)
|> Map.put("cc", cc)
@ -82,19 +93,22 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
defp process_tag(
"mrf_tag:sandbox",
%{"type" => "Create", "to" => to, "cc" => cc, "actor" => actor} = message
%{
"type" => "Create",
"to" => to,
"cc" => cc,
"actor" => actor,
"object" => object
} = message
) do
user = User.get_cached_by_ap_id(actor)
if Enum.member?(to, "https://www.w3.org/ns/activitystreams#Public") or
Enum.member?(cc, "https://www.w3.org/ns/activitystreams#Public") do
to =
List.delete(to, "https://www.w3.org/ns/activitystreams#Public") ++ [user.follower_address]
cc = List.delete(cc, "https://www.w3.org/ns/activitystreams#Public")
if Enum.member?(to, @public) or Enum.member?(cc, @public) do
to = List.delete(to, @public) ++ [user.follower_address]
cc = List.delete(cc, @public)
object =
message["object"]
object
|> Map.put("to", to)
|> Map.put("cc", cc)
@ -123,7 +137,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
end
end
defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow"}), do: {:reject, nil}
defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow"}),
do: {:reject, nil}
defp process_tag(_, message), do: {:ok, message}

View file

@ -21,7 +21,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do
@impl true
def filter(%{"actor" => actor} = object) do
actor_info = URI.parse(actor)
allow_list = Config.get([:mrf_user_allowlist, String.to_atom(actor_info.host)], [])
allow_list =
Config.get(
[:mrf_user_allowlist, String.to_atom(actor_info.host)],
[]
)
filter_by_list(object, allow_list)
end

View file

@ -88,22 +88,72 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
true
else
inbox_info = URI.parse(inbox)
!Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host)
!Enum.member?(Config.get([:instance, :quarantined_instances], []), inbox_info.host)
end
end
defp recipients(actor, activity) do
followers =
if actor.follower_address in activity.recipients do
{:ok, followers} = User.get_followers(actor)
Enum.filter(followers, &(!&1.local))
else
[]
end
Pleroma.Web.Salmon.remote_users(actor, activity) ++ followers
end
defp get_cc_ap_ids(ap_id, recipients) do
host = Map.get(URI.parse(ap_id), :host)
recipients
|> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end)
|> Enum.map(& &1.ap_id)
end
@doc """
Publishes an activity with BCC to all relevant peers.
"""
def publish(actor, %{data: %{"bcc" => bcc}} = activity) when is_list(bcc) and bcc != [] do
public = is_public?(activity)
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
recipients = recipients(actor, activity)
recipients
|> Enum.filter(&User.ap_enabled?/1)
|> Enum.map(fn %{info: %{source_data: data}} -> data["inbox"] end)
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|> Instances.filter_reachable()
|> Enum.each(fn {inbox, unreachable_since} ->
%User{ap_id: ap_id} =
Enum.find(recipients, fn %{info: %{source_data: data}} -> data["inbox"] == inbox end)
# Get all the recipients on the same host and add them to cc. Otherwise it a remote
# instance would only accept a first message for the first recipient and ignore the rest.
cc = get_cc_ap_ids(ap_id, recipients)
json =
data
|> Map.put("cc", cc)
|> Jason.encode!()
Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{
inbox: inbox,
json: json,
actor: actor,
id: activity.data["id"],
unreachable_since: unreachable_since
})
end)
end
@doc """
Publishes an activity to all relevant peers.
"""
def publish(%User{} = actor, %Activity{} = activity) do
remote_followers =
if actor.follower_address in activity.recipients do
{:ok, followers} = User.get_followers(actor)
followers |> Enum.filter(&(!&1.local))
else
[]
end
public = is_public?(activity)
if public && Config.get([:instance, :allow_relay]) do
@ -114,7 +164,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
json = Jason.encode!(data)
(Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers)
recipients(actor, activity)
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
|> Enum.map(fn %{info: %{source_data: data}} ->
(is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]

View file

@ -14,6 +14,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator
import Ecto.Query
@ -22,19 +23,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
@doc """
Modifies an incoming AP object (mastodon format) to our internal format.
"""
def fix_object(object) do
def fix_object(object, options \\ []) do
object
|> fix_actor
|> fix_url
|> fix_attachments
|> fix_context
|> fix_in_reply_to
|> fix_in_reply_to(options)
|> fix_emoji
|> fix_tag
|> fix_content_map
|> fix_likes
|> fix_addressing
|> fix_summary
|> fix_type(options)
end
def fix_summary(%{"summary" => nil} = object) do
@ -65,7 +67,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mentions) do
def fix_explicit_addressing(
%{"to" => to, "cc" => cc} = object,
explicit_mentions,
follower_collection
) do
explicit_to =
to
|> Enum.filter(fn x -> x in explicit_mentions end)
@ -76,6 +82,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
final_cc =
(cc ++ explicit_cc)
|> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
|> Enum.uniq()
object
@ -83,7 +90,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("cc", final_cc)
end
def fix_explicit_addressing(object, _explicit_mentions), do: object
def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
# if directMessage flag is set to true, leave the addressing alone
def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
@ -93,10 +100,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
object
|> Utils.determine_explicit_mentions()
explicit_mentions = explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public"]
follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address
object
|> fix_explicit_addressing(explicit_mentions)
explicit_mentions =
explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public", follower_collection]
fix_explicit_addressing(object, explicit_mentions, follower_collection)
end
# if as:Public is addressed, then make sure the followers collection is also addressed
@ -133,7 +142,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> fix_addressing_list("cc")
|> fix_addressing_list("bto")
|> fix_addressing_list("bcc")
|> fix_explicit_addressing
|> fix_explicit_addressing()
|> fix_implicit_addressing(followers_collection)
end
@ -156,7 +165,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
object
end
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object)
def fix_in_reply_to(object, options \\ [])
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
when not is_nil(in_reply_to) do
in_reply_to_id =
cond do
@ -174,28 +185,34 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
""
end
case get_obj_helper(in_reply_to_id) do
{:ok, replied_object} ->
with %Activity{} = _activity <-
Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
object
|> Map.put("inReplyTo", replied_object.data["id"])
|> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
|> Map.put("conversation", replied_object.data["context"] || object["conversation"])
|> Map.put("context", replied_object.data["context"] || object["conversation"])
else
e ->
Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
object
end
object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
e ->
Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
object
if Federator.allowed_incoming_reply_depth?(options[:depth]) do
case get_obj_helper(in_reply_to_id, options) do
{:ok, replied_object} ->
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("conversation", replied_object.data["context"] || object["conversation"])
|> Map.put("context", replied_object.data["context"] || object["conversation"])
else
e ->
Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
object
end
e ->
Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
object
end
else
object
end
end
def fix_in_reply_to(object), do: object
def fix_in_reply_to(object, _options), do: object
def fix_context(object) do
context = object["context"] || object["conversation"] || Utils.generate_context_id()
@ -328,6 +345,23 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_content_map(object), do: object
def fix_type(object, options \\ [])
def fix_type(%{"inReplyTo" => reply_id} = object, options) when is_binary(reply_id) do
reply =
if Federator.allowed_incoming_reply_depth?(options[:depth]) do
Object.normalize(reply_id, true)
end
if reply && (reply.data["type"] == "Question" and object["name"]) do
Map.put(object, "type", "Answer")
else
object
end
end
def fix_type(object, _), do: object
defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
with true <- id =~ "follows",
%User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
@ -354,9 +388,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
def handle_incoming(data, options \\ [])
# 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
def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
with context <- data["context"] || Utils.generate_context_id(),
content <- data["content"] || "",
%User{} = actor <- User.get_cached_by_ap_id(actor),
@ -389,16 +425,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
# disallow objects with bogus IDs
def handle_incoming(%{"id" => nil}), do: :error
def handle_incoming(%{"id" => ""}), do: :error
def handle_incoming(%{"id" => nil}, _options), do: :error
def handle_incoming(%{"id" => ""}, _options), do: :error
# length of https:// = 8, should validate better, but good enough for now.
def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8), do: :error
def handle_incoming(%{"id" => id}, _options) when not (is_binary(id) and length(id) > 8),
do: :error
# TODO: validate those with a Ecto scheme
# - tags
# - emoji
def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
when objtype in ["Article", "Note", "Video", "Page"] do
def handle_incoming(
%{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
options
)
when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do
actor = Containment.get_actor(data)
data =
@ -407,7 +447,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
with nil <- Activity.get_create_by_object_ap_id(object["id"]),
{:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
object = fix_object(data["object"])
options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
object = fix_object(data["object"], options)
params = %{
to: data["to"],
@ -432,16 +473,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
_options
) 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
with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
{:user_blocked, false} <-
{_, 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
{_, false} <- {:user_locked, User.locked?(followed)},
{_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
{_, {:ok, _}} <-
{:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed,
@ -450,7 +494,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
})
else
{:user_blocked, true} ->
{:ok, _} = Utils.update_follow_state(activity, "reject")
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
ActivityPub.reject(%{
to: [follower.ap_id],
@ -460,7 +504,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
})
{:follow, {:error, _}} ->
{:ok, _} = Utils.update_follow_state(activity, "reject")
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
ActivityPub.reject(%{
to: [follower.ap_id],
@ -481,38 +525,35 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
def handle_incoming(
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
_options
) do
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"),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, activity} <-
ActivityPub.accept(%{
to: follow_activity.data["to"],
type: "Accept",
actor: followed,
object: follow_activity.data["id"],
local: false
}) do
if not User.following?(follower, followed) do
{:ok, _follower} = User.follow(follower, followed)
end
{:ok, activity}
{:ok, _follower} = User.follow(follower, followed) do
ActivityPub.accept(%{
to: follow_activity.data["to"],
type: "Accept",
actor: followed,
object: follow_activity.data["id"],
local: false
})
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
%{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
_options
) do
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"),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, activity} <-
ActivityPub.reject(%{
@ -531,7 +572,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
def handle_incoming(
%{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data
%{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
@ -544,7 +586,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
def handle_incoming(
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
@ -559,7 +602,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def handle_incoming(
%{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
data
data,
_options
)
when object_type in ["Person", "Application", "Service", "Organization"] do
with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
@ -597,7 +641,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
# an error or a tombstone. This would allow us to verify that a deletion actually took
# place.
def handle_incoming(
%{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data
%{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = data,
_options
) do
object_id = Utils.get_ap_id(object_id)
@ -608,7 +653,30 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, activity} <- ActivityPub.delete(object, false) do
{:ok, activity}
else
_e -> :error
nil ->
case User.get_cached_by_ap_id(object_id) do
%User{ap_id: ^actor} = user ->
{:ok, followers} = User.get_followers(user)
Enum.each(followers, fn follower ->
User.unfollow(follower, user)
end)
{:ok, friends} = User.get_friends(user)
Enum.each(friends, fn followed ->
User.unfollow(user, followed)
end)
User.invalidate_cache(user)
Repo.delete(user)
nil ->
:error
end
_e ->
:error
end
end
@ -618,7 +686,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"object" => %{"type" => "Announce", "object" => object_id},
"actor" => _actor,
"id" => id
} = data
} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
@ -636,7 +705,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"object" => %{"type" => "Follow", "object" => followed},
"actor" => follower,
"id" => id
} = _data
} = _data,
_options
) do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
{:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
@ -654,7 +724,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"object" => %{"type" => "Block", "object" => blocked},
"actor" => blocker,
"id" => id
} = _data
} = _data,
_options
) do
with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
%User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
@ -668,7 +739,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
def handle_incoming(
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
_options
) do
with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
%User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
@ -688,7 +760,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"object" => %{"type" => "Like", "object" => object_id},
"actor" => _actor,
"id" => id
} = data
} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
@ -700,10 +773,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
def handle_incoming(_), do: :error
def handle_incoming(_, _), do: :error
def get_obj_helper(id) do
if object = Object.normalize(id), do: {:ok, object}, else: nil
def get_obj_helper(id, options \\ []) do
if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil
end
def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
@ -731,6 +804,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> set_reply_to_uri
|> strip_internal_fields
|> strip_internal_tags
|> set_type
end
# @doc
@ -740,13 +814,16 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
object =
Object.normalize(object_id).data
object_id
|> Object.normalize()
|> Map.get(:data)
|> prepare_object
data =
data
|> Map.put("object", object)
|> Map.merge(Utils.make_json_ld_header())
|> Map.delete("bcc")
{:ok, data}
end
@ -895,6 +972,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
Map.put(object, "sensitive", "nsfw" in tags)
end
def set_type(%{"type" => "Answer"} = object) do
Map.put(object, "type", "Note")
end
def set_type(object), do: object
def add_attributed_to(object) do
attributed_to = object["attributedTo"] || object["actor"]
@ -1007,6 +1090,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
end
if Pleroma.Config.get([:instance, :external_user_synchronization]) do
update_following_followers_counters(user)
end
{:ok, user}
else
%User{} = user -> {:ok, user}
@ -1039,4 +1126,27 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
data
|> maybe_fix_user_url
end
def update_following_followers_counters(user) do
info = %{}
following = fetch_counter(user.following_address)
info = if following, do: Map.put(info, :following_count, following), else: info
followers = fetch_counter(user.follower_address)
info = if followers, do: Map.put(info, :follower_count, followers), else: info
User.set_info_cache(user, info)
end
defp fetch_counter(url) do
with {:ok, %{body: body, status: code}} when code in 200..299 <-
Pleroma.HTTP.get(
url,
[{:Accept, "application/activity+json"}]
),
{:ok, data} <- Jason.decode(body) do
data["totalItems"]
end
end
end

View file

@ -19,18 +19,14 @@ defmodule Pleroma.Web.ActivityPub.Utils do
require Logger
@supported_object_types ["Article", "Note", "Video", "Page"]
@supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer"]
@supported_report_states ~w(open closed resolved)
@valid_visibilities ~w(public unlisted private direct)
# Some implementations send the actor URI as the actor field, others send the entire actor object,
# so figure out what the actor's URI is based on what we have.
def get_ap_id(object) do
case object do
%{"id" => id} -> id
id -> id
end
end
def get_ap_id(%{"id" => id} = _), do: id
def get_ap_id(id), do: id
def normalize_params(params) do
Map.put(params, "actor", get_ap_id(params["actor"]))
@ -151,16 +147,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do
def create_context(context) do
context = context || generate_id("contexts")
changeset = Object.context_mapping(context)
case Repo.insert(changeset) do
{:ok, object} ->
# Ecto has problems accessing the constraint inside the jsonb,
# so we explicitly check for the existed object before insert
object = Object.get_cached_by_ap_id(context)
with true <- is_nil(object),
changeset <- Object.context_mapping(context),
{:ok, inserted_object} <- Repo.insert(changeset) do
inserted_object
else
_ ->
object
# 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)
end
end
@ -168,14 +166,17 @@ defmodule Pleroma.Web.ActivityPub.Utils do
Enqueues an activity for federation if it's local
"""
def maybe_federate(%Activity{local: true} = activity) do
priority =
case activity.data["type"] do
"Delete" -> 10
"Create" -> 1
_ -> 5
end
if Pleroma.Config.get!([:instance, :federating]) do
priority =
case activity.data["type"] do
"Delete" -> 10
"Create" -> 1
_ -> 5
end
Pleroma.Web.Federator.publish(activity, priority)
end
Pleroma.Web.Federator.publish(activity, priority)
:ok
end
@ -376,8 +377,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
@doc """
Updates a follow activity's state (for locked accounts).
"""
def update_follow_state(
%Activity{data: %{"actor" => actor, "object" => object, "state" => "pending"}} = activity,
def update_follow_state_for_all(
%Activity{data: %{"actor" => actor, "object" => object}} = activity,
state
) do
try do
@ -789,4 +790,22 @@ defmodule Pleroma.Web.ActivityPub.Utils do
[to, cc, recipients]
end
end
def get_existing_votes(actor, %{data: %{"id" => id}}) do
query =
from(
[activity, object: object] in Activity.with_preloaded_object(Activity),
where: fragment("(?)->>'type' = 'Create'", activity.data),
where: fragment("(?)->>'actor' = ?", activity.data, ^actor),
where:
fragment(
"(?)->>'inReplyTo' = ?",
object.data,
^to_string(id)
),
where: fragment("(?)->>'type' = 'Answer'", object.data)
)
Repo.all(query)
end
end

View file

@ -98,29 +98,31 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|> Map.merge(Utils.make_json_ld_header())
end
def render("following.json", %{user: user, page: page}) do
def render("following.json", %{user: user, page: page} = opts) do
showing = (opts[:for] && opts[:for] == user) || !user.info.hide_follows
query = User.get_friends_query(user)
query = from(user in query, select: [:ap_id])
following = Repo.all(query)
total =
if !user.info.hide_follows do
if showing do
length(following)
else
0
end
collection(following, "#{user.ap_id}/following", page, !user.info.hide_follows, total)
collection(following, "#{user.ap_id}/following", page, showing, total)
|> Map.merge(Utils.make_json_ld_header())
end
def render("following.json", %{user: user}) do
def render("following.json", %{user: user} = opts) do
showing = (opts[:for] && opts[:for] == user) || !user.info.hide_follows
query = User.get_friends_query(user)
query = from(user in query, select: [:ap_id])
following = Repo.all(query)
total =
if !user.info.hide_follows do
if showing do
length(following)
else
0
@ -130,34 +132,43 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"id" => "#{user.ap_id}/following",
"type" => "OrderedCollection",
"totalItems" => total,
"first" => collection(following, "#{user.ap_id}/following", 1, !user.info.hide_follows)
"first" =>
if showing do
collection(following, "#{user.ap_id}/following", 1, !user.info.hide_follows)
else
"#{user.ap_id}/following?page=1"
end
}
|> Map.merge(Utils.make_json_ld_header())
end
def render("followers.json", %{user: user, page: page}) do
def render("followers.json", %{user: user, page: page} = opts) do
showing = (opts[:for] && opts[:for] == user) || !user.info.hide_followers
query = User.get_followers_query(user)
query = from(user in query, select: [:ap_id])
followers = Repo.all(query)
total =
if !user.info.hide_followers do
if showing do
length(followers)
else
0
end
collection(followers, "#{user.ap_id}/followers", page, !user.info.hide_followers, total)
collection(followers, "#{user.ap_id}/followers", page, showing, total)
|> Map.merge(Utils.make_json_ld_header())
end
def render("followers.json", %{user: user}) do
def render("followers.json", %{user: user} = opts) do
showing = (opts[:for] && opts[:for] == user) || !user.info.hide_followers
query = User.get_followers_query(user)
query = from(user in query, select: [:ap_id])
followers = Repo.all(query)
total =
if !user.info.hide_followers do
if showing do
length(followers)
else
0
@ -168,7 +179,11 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"type" => "OrderedCollection",
"totalItems" => total,
"first" =>
collection(followers, "#{user.ap_id}/followers", 1, !user.info.hide_followers, total)
if showing do
collection(followers, "#{user.ap_id}/followers", 1, showing, total)
else
"#{user.ap_id}/followers?page=1"
end
}
|> Map.merge(Utils.make_json_ld_header())
end

View file

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Visibility do
alias Pleroma.Activity
alias Pleroma.Object
@ -30,6 +34,20 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
!is_public?(activity) && !is_private?(activity)
end
def is_list?(%{data: %{"listMessage" => _}}), do: true
def is_list?(_), do: false
def visible_for_user?(%{actor: ap_id}, %User{ap_id: ap_id}), do: true
def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do
user.ap_id in activity.data["to"] ||
list_ap_id
|> Pleroma.List.get_by_ap_id()
|> Pleroma.List.member?(user)
end
def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false
def visible_for_user?(activity, nil) do
is_public?(activity)
end
@ -66,6 +84,12 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
Enum.any?(to, &String.contains?(&1, "/followers")) ->
"private"
object.data["directMessage"] == true ->
"direct"
is_binary(object.data["listMessage"]) ->
"list"
length(cc) > 0 ->
"private"

View file

@ -10,6 +10,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.Config
alias Pleroma.Web.AdminAPI.ConfigView
alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.CommonAPI
@ -72,7 +74,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
def user_show(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
conn
|> json(AccountView.render("show.json", %{user: user}))
else
@ -158,9 +160,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
def right_add(conn, _) do
conn
|> put_status(404)
|> json(%{error: "No such permission_group"})
render_error(conn, :not_found, "No such permission_group")
end
def right_get(conn, %{"nickname" => nickname}) do
@ -182,9 +182,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
)
when permission_group in ["moderator", "admin"] do
if admin_nickname == nickname do
conn
|> put_status(403)
|> json(%{error: "You can't revoke your own admin status."})
render_error(conn, :forbidden, "You can't revoke your own admin status.")
else
user = User.get_cached_by_nickname(nickname)
@ -205,9 +203,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
def right_delete(conn, _) do
conn
|> put_status(404)
|> json(%{error: "No such permission_group"})
render_error(conn, :not_found, "No such permission_group")
end
def set_activation_status(conn, %{"nickname" => nickname, "status" => status}) do
@ -362,28 +358,63 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
end
def config_show(conn, _params) do
configs = Pleroma.Repo.all(Config)
conn
|> put_view(ConfigView)
|> render("index.json", %{configs: configs})
end
def config_update(conn, %{"configs" => configs}) do
updated =
if Pleroma.Config.get([:instance, :dynamic_configuration]) do
updated =
Enum.map(configs, fn
%{"group" => group, "key" => key, "delete" => "true"} ->
{:ok, _} = Config.delete(%{group: group, key: key})
nil
%{"group" => group, "key" => key, "value" => value} ->
{:ok, config} = Config.update_or_create(%{group: group, key: key, value: value})
config
end)
|> Enum.reject(&is_nil(&1))
Pleroma.Config.TransferTask.load_and_update_env()
Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "false"])
updated
else
[]
end
conn
|> put_view(ConfigView)
|> render("index.json", %{configs: updated})
end
def errors(conn, {:error, :not_found}) do
conn
|> put_status(404)
|> json("Not found")
|> put_status(:not_found)
|> json(dgettext("errors", "Not found"))
end
def errors(conn, {:error, reason}) do
conn
|> put_status(400)
|> put_status(:bad_request)
|> json(reason)
end
def errors(conn, {:param_cast, _}) do
conn
|> put_status(400)
|> json("Invalid parameters")
|> put_status(:bad_request)
|> json(dgettext("errors", "Invalid parameters"))
end
def errors(conn, _) do
conn
|> put_status(500)
|> json("Something went wrong")
|> put_status(:internal_server_error)
|> json(dgettext("errors", "Something went wrong"))
end
defp page_params(params) do

View file

@ -0,0 +1,152 @@
# 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.Config do
use Ecto.Schema
import Ecto.Changeset
import Pleroma.Web.Gettext
alias __MODULE__
alias Pleroma.Repo
@type t :: %__MODULE__{}
schema "config" do
field(:key, :string)
field(:group, :string)
field(:value, :binary)
timestamps()
end
@spec get_by_params(map()) :: Config.t() | nil
def get_by_params(params), do: Repo.get_by(Config, params)
@spec changeset(Config.t(), map()) :: Changeset.t()
def changeset(config, params \\ %{}) do
config
|> cast(params, [:key, :group, :value])
|> validate_required([:key, :group, :value])
|> unique_constraint(:key, name: :config_group_key_index)
end
@spec create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()}
def create(params) do
%Config{}
|> changeset(Map.put(params, :value, transform(params[:value])))
|> Repo.insert()
end
@spec update(Config.t(), map()) :: {:ok, Config} | {:error, Changeset.t()}
def update(%Config{} = config, %{value: value}) do
config
|> change(value: transform(value))
|> Repo.update()
end
@spec update_or_create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()}
def update_or_create(params) do
with %Config{} = config <- Config.get_by_params(Map.take(params, [:group, :key])) do
Config.update(config, params)
else
nil -> Config.create(params)
end
end
@spec delete(map()) :: {:ok, Config.t()} | {:error, Changeset.t()}
def delete(params) do
with %Config{} = config <- Config.get_by_params(params) do
Repo.delete(config)
else
nil ->
err =
dgettext("errors", "Config with params %{params} not found", params: inspect(params))
{:error, err}
end
end
@spec from_binary(binary()) :: term()
def from_binary(binary), do: :erlang.binary_to_term(binary)
@spec from_binary_with_convert(binary()) :: any()
def from_binary_with_convert(binary) do
from_binary(binary)
|> do_convert()
end
defp do_convert(entity) when is_list(entity) do
for v <- entity, into: [], do: do_convert(v)
end
defp do_convert(entity) when is_map(entity) do
for {k, v} <- entity, into: %{}, do: {do_convert(k), do_convert(v)}
end
defp do_convert({:dispatch, [entity]}), do: %{"tuple" => [":dispatch", [inspect(entity)]]}
defp do_convert(entity) when is_tuple(entity),
do: %{"tuple" => do_convert(Tuple.to_list(entity))}
defp do_convert(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity),
do: entity
defp do_convert(entity) when is_atom(entity) do
string = to_string(entity)
if String.starts_with?(string, "Elixir."),
do: do_convert(string),
else: ":" <> string
end
defp do_convert("Elixir." <> module_name), do: module_name
defp do_convert(entity) when is_binary(entity), do: entity
@spec transform(any()) :: binary()
def transform(entity) when is_binary(entity) or is_map(entity) or is_list(entity) do
:erlang.term_to_binary(do_transform(entity))
end
def transform(entity), do: :erlang.term_to_binary(entity)
defp do_transform(%Regex{} = entity) when is_map(entity), do: entity
defp do_transform(%{"tuple" => [":dispatch", [entity]]}) do
cleaned_string = String.replace(entity, ~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "")
{dispatch_settings, []} = Code.eval_string(cleaned_string, [], requires: [], macros: [])
{:dispatch, [dispatch_settings]}
end
defp do_transform(%{"tuple" => entity}) do
Enum.reduce(entity, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end)
end
defp do_transform(entity) when is_map(entity) do
for {k, v} <- entity, into: %{}, do: {do_transform(k), do_transform(v)}
end
defp do_transform(entity) when is_list(entity) do
for v <- entity, into: [], do: do_transform(v)
end
defp do_transform(entity) when is_binary(entity) do
String.trim(entity)
|> do_transform_string()
end
defp do_transform(entity), do: entity
defp do_transform_string("~r/" <> pattern) do
pattern = String.trim_trailing(pattern, "/")
~r/#{pattern}/
end
defp do_transform_string(":" <> atom), do: String.to_atom(atom)
defp do_transform_string(value) do
if String.starts_with?(value, "Pleroma") or String.starts_with?(value, "Phoenix"),
do: String.to_existing_atom("Elixir." <> value),
else: value
end
end

View file

@ -5,8 +5,11 @@
defmodule Pleroma.Web.AdminAPI.AccountView do
use Pleroma.Web, :view
alias Pleroma.HTML
alias Pleroma.User
alias Pleroma.User.Info
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.MediaProxy
def render("index.json", %{users: users, count: count, page_size: page_size}) do
%{
@ -17,9 +20,14 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
end
def render("show.json", %{user: user}) do
avatar = User.avatar_url(user) |> MediaProxy.url()
display_name = HTML.strip_tags(user.name || user.nickname)
%{
"id" => user.id,
"avatar" => avatar,
"nickname" => user.nickname,
"display_name" => display_name,
"deactivated" => user.info.deactivated,
"local" => user.local,
"roles" => Info.roles(user.info),

View file

@ -0,0 +1,21 @@
# 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.ConfigView do
use Pleroma.Web, :view
def render("index.json", %{configs: configs}) do
%{
configs: render_many(configs, __MODULE__, "show.json", as: :config)
}
end
def render("show.json", %{config: config}) do
%{
key: config.key,
group: config.group,
value: Pleroma.Web.AdminAPI.Config.from_binary_with_convert(config.value)
}
end
end

View file

@ -5,9 +5,9 @@
defmodule Pleroma.Web.AdminAPI.ReportView do
use Pleroma.Web, :view
alias Pleroma.Activity
alias Pleroma.HTML
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", %{reports: reports}) do
@ -23,6 +23,13 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
[account_ap_id | status_ap_ids] = report.data["object"]
account = User.get_cached_by_ap_id(account_ap_id)
content =
unless is_nil(report.data["content"]) do
HTML.filter_tags(report.data["content"])
else
nil
end
statuses =
Enum.map(status_ap_ids, fn ap_id ->
Activity.get_by_ap_id_with_object(ap_id)
@ -30,12 +37,19 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
%{
id: report.id,
account: AccountView.render("account.json", %{user: account}),
actor: AccountView.render("account.json", %{user: user}),
content: report.data["content"],
account: merge_account_views(account),
actor: merge_account_views(user),
content: content,
created_at: created_at,
statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}),
state: report.data["state"]
}
end
defp merge_account_views(%User{} = user) do
Pleroma.Web.MastodonAPI.AccountView.render("account.json", %{user: user})
|> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user}))
end
defp merge_account_views(_), do: %{}
end

View file

@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.PleromaAuthenticator do
alias Comeonin.Pbkdf2
alias Pleroma.Plugs.AuthenticationPlug
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
@ -16,7 +16,7 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
def get_user(%Plug.Conn{} = conn) do
with {:ok, {name, password}} <- fetch_credentials(conn),
{_, %User{} = user} <- {:user, fetch_user(name)},
{_, true} <- {:checkpw, Pbkdf2.checkpw(password, user.password_hash)} do
{_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)} do
{:ok, user}
else
error ->
@ -24,6 +24,14 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
end
end
@doc """
Gets or creates Pleroma.Registration record from Ueberauth assigns.
Note: some strategies (like `keycloak`) might need extra configuration to fill `uid` from callback response
see [`docs/config.md`](docs/config.md).
"""
def get_registration(%Plug.Conn{assigns: %{ueberauth_auth: %{uid: nil}}}),
do: {:error, :missing_uid}
def get_registration(%Plug.Conn{
assigns: %{ueberauth_auth: %{provider: provider, uid: uid} = auth}
}) do
@ -51,9 +59,10 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
def get_registration(%Plug.Conn{} = _conn), do: {:error, :missing_credentials}
@doc "Creates Pleroma.User record basing on params and Pleroma.Registration record."
def create_from_registration(
%Plug.Conn{params: %{"authorization" => registration_attrs}},
registration
%Registration{} = registration
) do
nickname = value([registration_attrs["nickname"], Registration.nickname(registration)])
email = value([registration_attrs["email"], Registration.email(registration)])

View file

@ -4,14 +4,15 @@
defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.ThreadMute
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
import Pleroma.Web.Gettext
import Pleroma.Web.CommonAPI.Utils
def follow(follower, followed) do
@ -29,15 +30,16 @@ defmodule Pleroma.Web.CommonAPI do
def unfollow(follower, unfollowed) do
with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
{:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed) do
{:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
{:ok, _unfollowed} <- User.unsubscribe(follower, unfollowed) do
{:ok, follower}
end
end
def accept_follow_request(follower, followed) do
with {:ok, follower} <- User.maybe_follow(follower, followed),
with {:ok, follower} <- User.follow(follower, followed),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
{:ok, _activity} <-
ActivityPub.accept(%{
to: [follower.ap_id],
@ -51,7 +53,7 @@ defmodule Pleroma.Web.CommonAPI do
def reject_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
{:ok, _activity} <-
ActivityPub.reject(%{
to: [follower.ap_id],
@ -73,7 +75,7 @@ defmodule Pleroma.Web.CommonAPI do
{:ok, delete}
else
_ ->
{:error, "Could not delete"}
{:error, dgettext("errors", "Could not delete")}
end
end
@ -84,7 +86,7 @@ defmodule Pleroma.Web.CommonAPI do
ActivityPub.announce(user, object)
else
_ ->
{:error, "Could not repeat"}
{:error, dgettext("errors", "Could not repeat")}
end
end
@ -94,7 +96,7 @@ defmodule Pleroma.Web.CommonAPI do
ActivityPub.unannounce(user, object)
else
_ ->
{:error, "Could not unrepeat"}
{:error, dgettext("errors", "Could not unrepeat")}
end
end
@ -105,7 +107,7 @@ defmodule Pleroma.Web.CommonAPI do
ActivityPub.like(user, object)
else
_ ->
{:error, "Could not favorite"}
{:error, dgettext("errors", "Could not favorite")}
end
end
@ -115,14 +117,69 @@ defmodule Pleroma.Web.CommonAPI do
ActivityPub.unlike(user, object)
else
_ ->
{:error, "Could not unfavorite"}
{:error, dgettext("errors", "Could not unfavorite")}
end
end
def vote(user, object, choices) do
with "Question" <- object.data["type"],
{:author, false} <- {:author, object.data["actor"] == user.ap_id},
{:existing_votes, []} <- {:existing_votes, Utils.get_existing_votes(user.ap_id, object)},
{options, max_count} <- get_options_and_max_count(object),
option_count <- Enum.count(options),
{:choice_check, {choices, true}} <-
{:choice_check, normalize_and_validate_choice_indices(choices, option_count)},
{:count_check, true} <- {:count_check, Enum.count(choices) <= max_count} do
answer_activities =
Enum.map(choices, fn index ->
answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
{:ok, activity} =
ActivityPub.create(%{
to: answer_data["to"],
actor: user,
context: object.data["context"],
object: answer_data,
additional: %{"cc" => answer_data["cc"]}
})
activity
end)
object = Object.get_cached_by_ap_id(object.data["id"])
{:ok, answer_activities, object}
else
{:author, _} -> {:error, dgettext("errors", "Poll's author can't vote")}
{:existing_votes, _} -> {:error, dgettext("errors", "Already voted")}
{:choice_check, {_, false}} -> {:error, dgettext("errors", "Invalid indices")}
{:count_check, false} -> {:error, dgettext("errors", "Too many choices")}
end
end
defp get_options_and_max_count(object) do
if Map.has_key?(object.data, "anyOf") do
{object.data["anyOf"], Enum.count(object.data["anyOf"])}
else
{object.data["oneOf"], 1}
end
end
defp normalize_and_validate_choice_indices(choices, count) do
Enum.map_reduce(choices, true, fn index, valid ->
index = if is_binary(index), do: String.to_integer(index), else: index
{index, if(valid, do: index < count, else: valid)}
end)
end
def get_visibility(%{"visibility" => visibility}, in_reply_to)
when visibility in ~w{public unlisted private direct},
do: {visibility, get_replied_to_visibility(in_reply_to)}
def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to) do
visibility = {:list, String.to_integer(list_id)}
{visibility, get_replied_to_visibility(in_reply_to)}
end
def get_visibility(_, in_reply_to) when not is_nil(in_reply_to) do
visibility = get_replied_to_visibility(in_reply_to)
{visibility, visibility}
@ -154,12 +211,15 @@ defmodule Pleroma.Web.CommonAPI do
data,
visibility
),
{to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility),
mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id),
addressed_users <- get_addressed_users(mentioned_users, data["to"]),
{poll, poll_emoji} <- make_poll_data(data),
{to, cc} <- get_to_and_cc(user, addressed_users, in_reply_to, visibility),
context <- make_context(in_reply_to),
cw <- data["spoiler_text"] || "",
sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}),
full_payload <- String.trim(status <> cw),
length when length in 1..limit <- String.length(full_payload),
:ok <- validate_character_limit(full_payload, attachments, limit),
object <-
make_note_data(
user.ap_id,
@ -171,29 +231,36 @@ defmodule Pleroma.Web.CommonAPI do
tags,
cw,
cc,
sensitive
sensitive,
poll
),
object <-
Map.put(
object,
"emoji",
Formatter.get_emoji_map(full_payload)
Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji)
) do
res =
ActivityPub.create(
%{
to: to,
actor: user,
context: context,
object: object,
additional: %{"cc" => cc, "directMessage" => visibility == "direct"}
},
Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
)
preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
direct? = visibility == "direct"
res
%{
to: to,
actor: user,
context: context,
object: object,
additional: %{"cc" => cc, "directMessage" => direct?}
}
|> maybe_add_list_data(user, visibility)
|> ActivityPub.create(preview?)
else
e -> {:error, e}
{:private_to_public, true} ->
{:error, dgettext("errors", "The message visibility must be direct")}
{:error, _} = e ->
e
e ->
{:error, e}
end
end
@ -228,12 +295,11 @@ defmodule Pleroma.Web.CommonAPI do
},
object: %Object{
data: %{
"to" => object_to,
"type" => "Note"
}
}
} = activity <- get_by_id_or_ap_id(id_or_ap_id),
true <- Enum.member?(object_to, "https://www.w3.org/ns/activitystreams#Public"),
true <- Visibility.is_public?(activity),
%{valid?: true} = info_changeset <-
User.Info.add_pinnned_activity(user.info, activity),
changeset <-
@ -245,7 +311,7 @@ defmodule Pleroma.Web.CommonAPI do
{:error, err}
_ ->
{:error, "Could not pin"}
{:error, dgettext("errors", "Could not pin")}
end
end
@ -262,7 +328,7 @@ defmodule Pleroma.Web.CommonAPI do
{:error, err}
_ ->
{:error, "Could not unpin"}
{:error, dgettext("errors", "Could not unpin")}
end
end
@ -270,7 +336,7 @@ defmodule Pleroma.Web.CommonAPI do
with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
{:ok, activity}
else
{:error, _} -> {:error, "conversation is already muted"}
{:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
end
end
@ -289,15 +355,6 @@ 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_cached_by_id(account_id)},
@ -315,8 +372,8 @@ defmodule Pleroma.Web.CommonAPI do
{:ok, activity}
else
{:error, err} -> {:error, err}
{:account_id, %{}} -> {:error, "Valid `account_id` required"}
{:account, nil} -> {:error, "Account not found"}
{:account_id, %{}} -> {:error, dgettext("errors", "Valid `account_id` required")}
{:account, nil} -> {:error, dgettext("errors", "Account not found")}
end
end
@ -325,14 +382,9 @@ defmodule Pleroma.Web.CommonAPI do
{:ok, activity} <- Utils.update_report_state(activity, state) do
{:ok, activity}
else
nil ->
{:error, :not_found}
{:error, reason} ->
{:error, reason}
_ ->
{:error, "Could not update state"}
nil -> {:error, :not_found}
{:error, reason} -> {:error, reason}
_ -> {:error, dgettext("errors", "Could not update state")}
end
end
@ -342,11 +394,8 @@ defmodule Pleroma.Web.CommonAPI do
{:ok, activity} <- set_visibility(activity, opts) do
{:ok, activity}
else
nil ->
{:error, :not_found}
{:error, reason} ->
{:error, reason}
nil -> {:error, :not_found}
{:error, reason} -> {:error, reason}
end
end

View file

@ -3,12 +3,14 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI.Utils do
import Pleroma.Web.Gettext
alias Calendar.Strftime
alias Comeonin.Pbkdf2
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.Plugs.AuthenticationPlug
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
@ -61,9 +63,9 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end)
end
def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do
mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
@spec get_to_and_cc(User.t(), list(String.t()), Activity.t() | nil, String.t()) ::
{list(String.t()), list(String.t())}
def get_to_and_cc(user, mentioned_users, inReplyTo, "public") do
to = ["https://www.w3.org/ns/activitystreams#Public" | mentioned_users]
cc = [user.follower_address]
@ -74,9 +76,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
def to_for_user_and_mentions(user, mentions, inReplyTo, "unlisted") do
mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted") do
to = [user.follower_address | mentioned_users]
cc = ["https://www.w3.org/ns/activitystreams#Public"]
@ -87,14 +87,12 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
def to_for_user_and_mentions(user, mentions, inReplyTo, "private") do
{to, cc} = to_for_user_and_mentions(user, mentions, inReplyTo, "direct")
def get_to_and_cc(user, mentioned_users, inReplyTo, "private") do
{to, cc} = get_to_and_cc(user, mentioned_users, inReplyTo, "direct")
{[user.follower_address | to], cc}
end
def to_for_user_and_mentions(_user, mentions, inReplyTo, "direct") do
mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct") do
if inReplyTo do
{Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []}
else
@ -102,6 +100,95 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}), do: {mentions, []}
def get_addressed_users(_, to) when is_list(to) do
User.get_ap_ids_by_nicknames(to)
end
def get_addressed_users(mentioned_users, _), do: mentioned_users
def maybe_add_list_data(activity_params, user, {:list, list_id}) do
case Pleroma.List.get(list_id, user) do
%Pleroma.List{} = list ->
activity_params
|> put_in([:additional, "bcc"], [list.ap_id])
|> put_in([:additional, "listMessage"], list.ap_id)
|> put_in([:object, "listMessage"], list.ap_id)
_ ->
activity_params
end
end
def maybe_add_list_data(activity_params, _, _), do: activity_params
def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data)
when is_list(options) do
%{max_expiration: max_expiration, min_expiration: min_expiration} =
limits = Pleroma.Config.get([:instance, :poll_limits])
# XXX: There is probably a cleaner way of doing this
try do
# In some cases mastofe sends out strings instead of integers
expires_in = if is_binary(expires_in), do: String.to_integer(expires_in), else: expires_in
if Enum.count(options) > limits.max_options do
raise ArgumentError, message: "Poll can't contain more than #{limits.max_options} options"
end
{poll, emoji} =
Enum.map_reduce(options, %{}, fn option, emoji ->
if String.length(option) > limits.max_option_chars do
raise ArgumentError,
message:
"Poll options cannot be longer than #{limits.max_option_chars} characters each"
end
{%{
"name" => option,
"type" => "Note",
"replies" => %{"type" => "Collection", "totalItems" => 0}
}, Map.merge(emoji, Formatter.get_emoji_map(option))}
end)
case expires_in do
expires_in when expires_in > max_expiration ->
raise ArgumentError, message: "Expiration date is too far in the future"
expires_in when expires_in < min_expiration ->
raise ArgumentError, message: "Expiration date is too soon"
_ ->
:noop
end
end_time =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(expires_in)
|> NaiveDateTime.to_iso8601()
poll =
if Pleroma.Web.ControllerHelper.truthy_param?(data["poll"]["multiple"]) do
%{"type" => "Question", "anyOf" => poll, "closed" => end_time}
else
%{"type" => "Question", "oneOf" => poll, "closed" => end_time}
end
{poll, emoji}
rescue
e in ArgumentError -> e.message
end
end
def make_poll_data(%{"poll" => poll}) when is_map(poll) do
"Invalid poll"
end
def make_poll_data(_data) do
{%{}, %{}}
end
def make_content_html(
status,
attachments,
@ -224,7 +311,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do
tags,
cw \\ nil,
cc \\ [],
sensitive \\ false
sensitive \\ false,
merge \\ %{}
) do
object = %{
"type" => "Note",
@ -239,12 +327,15 @@ defmodule Pleroma.Web.CommonAPI.Utils do
"tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
}
with false <- is_nil(in_reply_to),
%Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do
Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
else
_ -> object
end
object =
with false <- is_nil(in_reply_to),
%Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do
Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
else
_ -> object
end
Map.merge(object, merge)
end
def format_naive_asctime(date) do
@ -297,10 +388,10 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def confirm_current_password(user, password) do
with %User{local: true} = db_user <- User.get_cached_by_id(user.id),
true <- Pbkdf2.checkpw(password, db_user.password_hash) do
true <- AuthenticationPlug.checkpw(password, db_user.password_hash) do
{:ok, db_user}
else
_ -> {:error, "Invalid password."}
_ -> {:error, dgettext("errors", "Invalid password.")}
end
end
@ -383,7 +474,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do
if String.length(comment) <= max_size do
{:ok, format_input(comment, "text/plain")}
else
{:error, "Comment must be up to #{max_size} characters"}
{:error,
dgettext("errors", "Comment must be up to %{max_size} characters", max_size: max_size)}
end
end
@ -418,7 +510,32 @@ defmodule Pleroma.Web.CommonAPI.Utils do
context
else
_e ->
{:error, "No such conversation"}
{:error, dgettext("errors", "No such conversation")}
end
end
def make_answer_data(%User{ap_id: ap_id}, object, name) do
%{
"type" => "Answer",
"actor" => ap_id,
"cc" => [object.data["actor"]],
"to" => [],
"name" => name,
"inReplyTo" => object.data["id"]
}
end
def validate_character_limit(full_payload, attachments, limit) do
length = String.length(full_payload)
if length < limit do
if length > 0 or Enum.count(attachments) > 0 do
:ok
else
{:error, dgettext("errors", "Cannot post an empty status without attachments")}
end
else
{:error, dgettext("errors", "The status is over the character limit")}
end
end
end

View file

@ -15,4 +15,22 @@ defmodule Pleroma.Web.ControllerHelper do
|> put_status(status)
|> json(json)
end
@spec fetch_integer_param(map(), String.t(), integer() | nil) :: integer() | nil
def fetch_integer_param(params, name, default \\ nil) do
params
|> Map.get(name, default)
|> param_to_integer(default)
end
defp param_to_integer(val, _) when is_integer(val), do: val
defp param_to_integer(val, default) when is_binary(val) do
case Integer.parse(val) do
{res, _} -> res
_ -> default
end
end
defp param_to_integer(_, default), do: default
end

Some files were not shown because too many files have changed in this diff Show more