Merge remote-tracking branch 'origin/develop' into shigusegubu
* origin/develop: (295 commits) update frontend Fix uploads test. fix text field don't re-use build Fix tagging problems for existing instances. Alias Kocaptcha in the test Use :ets.match_delete to delete old captchas Switch to phoenixframework/phoenix as the needed change is merged. Integration tests for mastodon websocket Cowboy handler for Mastodon WebSocket Support both OAuth token record and token string in UserView minutes->seconds_retained in config.md Clean captchas up periodically, not schedule it after theyre created Fix captcha tests Change minutes_retained config to seconds_retained Make the hosted kocaptcha the default value Add a configurable auto-cleanup for captchas More put_view. Use bindings dbuser and dbname in sample_psql.eex Upgrade to Phoenix 1.4 ...
This commit is contained in:
commit
4349a83b9d
233 changed files with 8692 additions and 3097 deletions
|
|
@ -1,19 +0,0 @@
|
|||
defmodule Mix.Tasks.DeactivateUser do
|
||||
use Mix.Task
|
||||
alias Pleroma.User
|
||||
|
||||
@moduledoc """
|
||||
Deactivates a user (local or remote)
|
||||
|
||||
Usage: ``mix deactivate_user <nickname>``
|
||||
|
||||
Example: ``mix deactivate_user lain``
|
||||
"""
|
||||
def run([nickname]) do
|
||||
Mix.Task.run("app.start")
|
||||
|
||||
with user <- User.get_by_nickname(nickname) do
|
||||
User.deactivate(user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
defmodule Mix.Tasks.GenerateConfig do
|
||||
use Mix.Task
|
||||
|
||||
@moduledoc """
|
||||
Generate a new config
|
||||
|
||||
## Usage
|
||||
``mix generate_config``
|
||||
|
||||
This mix task is interactive, and will overwrite the config present at ``config/generated_config.exs``.
|
||||
"""
|
||||
|
||||
def run(_) do
|
||||
IO.puts("Answer a few questions to generate a new config\n")
|
||||
IO.puts("--- THIS WILL OVERWRITE YOUR config/generated_config.exs! ---\n")
|
||||
domain = IO.gets("What is your domain name? (e.g. pleroma.soykaf.com): ") |> String.trim()
|
||||
name = IO.gets("What is the name of your instance? (e.g. Pleroma/Soykaf): ") |> String.trim()
|
||||
email = IO.gets("What's your admin email address: ") |> String.trim()
|
||||
|
||||
secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
|
||||
dbpass = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
|
||||
|
||||
resultSql = EEx.eval_file("lib/mix/tasks/sample_psql.eex", dbpass: dbpass)
|
||||
|
||||
result =
|
||||
EEx.eval_file(
|
||||
"lib/mix/tasks/sample_config.eex",
|
||||
domain: domain,
|
||||
email: email,
|
||||
name: name,
|
||||
secret: secret,
|
||||
dbpass: dbpass
|
||||
)
|
||||
|
||||
IO.puts(
|
||||
"\nWriting config to config/generated_config.exs.\n\nCheck it and configure your database, then copy it to either config/dev.secret.exs or config/prod.secret.exs"
|
||||
)
|
||||
|
||||
File.write("config/generated_config.exs", result)
|
||||
|
||||
IO.puts(
|
||||
"\nWriting setup_db.psql, please run it as postgre superuser, i.e.: sudo su postgres -c 'psql -f config/setup_db.psql'"
|
||||
)
|
||||
|
||||
File.write("config/setup_db.psql", resultSql)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
defmodule Mix.Tasks.GenerateInviteToken do
|
||||
use Mix.Task
|
||||
|
||||
@moduledoc """
|
||||
Generates invite token
|
||||
|
||||
This is in the form of a URL to be used by the Invited user to register themselves.
|
||||
|
||||
## Usage
|
||||
``mix generate_invite_token``
|
||||
"""
|
||||
def run([]) do
|
||||
Mix.Task.run("app.start")
|
||||
|
||||
with {:ok, token} <- Pleroma.UserInviteToken.create_token() do
|
||||
IO.puts("Generated user invite token")
|
||||
|
||||
IO.puts(
|
||||
"Url: #{
|
||||
Pleroma.Web.Router.Helpers.redirect_url(
|
||||
Pleroma.Web.Endpoint,
|
||||
:registration_page,
|
||||
token.token
|
||||
)
|
||||
}"
|
||||
)
|
||||
else
|
||||
_ ->
|
||||
IO.puts("Error creating token")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
defmodule Mix.Tasks.GeneratePasswordReset do
|
||||
use Mix.Task
|
||||
alias Pleroma.User
|
||||
|
||||
@moduledoc """
|
||||
Generate password reset link for user
|
||||
|
||||
Usage: ``mix generate_password_reset <nickname>``
|
||||
|
||||
Example: ``mix generate_password_reset lain``
|
||||
"""
|
||||
def run([nickname]) do
|
||||
Mix.Task.run("app.start")
|
||||
|
||||
with %User{local: true} = user <- User.get_by_nickname(nickname),
|
||||
{:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do
|
||||
IO.puts("Generated password reset token for #{user.nickname}")
|
||||
|
||||
IO.puts(
|
||||
"Url: #{
|
||||
Pleroma.Web.Router.Helpers.util_url(
|
||||
Pleroma.Web.Endpoint,
|
||||
:show_password_reset,
|
||||
token.token
|
||||
)
|
||||
}"
|
||||
)
|
||||
else
|
||||
_ ->
|
||||
IO.puts("No local user #{nickname}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
defmodule Mix.Tasks.SetModerator do
|
||||
@moduledoc """
|
||||
Set moderator to a local user
|
||||
|
||||
Usage: ``mix set_moderator <nickname>``
|
||||
|
||||
Example: ``mix set_moderator lain``
|
||||
"""
|
||||
|
||||
use Mix.Task
|
||||
import Mix.Ecto
|
||||
alias Pleroma.{Repo, User}
|
||||
|
||||
def run([nickname | rest]) do
|
||||
Application.ensure_all_started(:pleroma)
|
||||
|
||||
moderator =
|
||||
case rest do
|
||||
[moderator] -> moderator == "true"
|
||||
_ -> true
|
||||
end
|
||||
|
||||
with %User{local: true} = user <- User.get_by_nickname(nickname) do
|
||||
info =
|
||||
user.info
|
||||
|> Map.put("is_moderator", !!moderator)
|
||||
|
||||
cng = User.info_changeset(user, %{info: info})
|
||||
{:ok, user} = User.update_and_set_cache(cng)
|
||||
|
||||
IO.puts("Moderator status of #{nickname}: #{user.info["is_moderator"]}")
|
||||
else
|
||||
_ ->
|
||||
IO.puts("No local user #{nickname}")
|
||||
end
|
||||
end
|
||||
end
|
||||
24
lib/mix/tasks/pleroma/common.ex
Normal file
24
lib/mix/tasks/pleroma/common.ex
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
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
|
||||
160
lib/mix/tasks/pleroma/instance.ex
Normal file
160
lib/mix/tasks/pleroma/instance.ex
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
defmodule Mix.Tasks.Pleroma.Instance do
|
||||
use Mix.Task
|
||||
alias Mix.Tasks.Pleroma.Common
|
||||
|
||||
@shortdoc "Manages Pleroma instance"
|
||||
@moduledoc """
|
||||
Manages Pleroma instance.
|
||||
|
||||
## Generate a new instance config.
|
||||
|
||||
mix pleroma.instance gen [OPTION...]
|
||||
|
||||
If any options are left unspecified, you will be prompted interactively
|
||||
|
||||
## Options
|
||||
|
||||
- `-f`, `--force` - overwrite any output files
|
||||
- `-o PATH`, `--output PATH` - the output file for the generated configuration
|
||||
- `--output-psql PATH` - the output file for the generated PostgreSQL setup
|
||||
- `--domain DOMAIN` - the domain of your instance
|
||||
- `--instance-name INSTANCE_NAME` - the name of your instance
|
||||
- `--admin-email ADMIN_EMAIL` - the email address of the instance admin
|
||||
- `--dbhost HOSTNAME` - the hostname of the PostgreSQL database to use
|
||||
- `--dbname DBNAME` - the name of the database to use
|
||||
- `--dbuser DBUSER` - the user (aka role) to use for the database connection
|
||||
- `--dbpass DBPASS` - the password to use for the database connection
|
||||
"""
|
||||
|
||||
def run(["gen" | rest]) do
|
||||
{options, [], []} =
|
||||
OptionParser.parse(
|
||||
rest,
|
||||
strict: [
|
||||
force: :boolean,
|
||||
output: :string,
|
||||
output_psql: :string,
|
||||
domain: :string,
|
||||
instance_name: :string,
|
||||
admin_email: :string,
|
||||
dbhost: :string,
|
||||
dbname: :string,
|
||||
dbuser: :string,
|
||||
dbpass: :string
|
||||
],
|
||||
aliases: [
|
||||
o: :output,
|
||||
f: :force
|
||||
]
|
||||
)
|
||||
|
||||
paths =
|
||||
[config_path, psql_path] = [
|
||||
Keyword.get(options, :output, "config/generated_config.exs"),
|
||||
Keyword.get(options, :output_psql, "config/setup_db.psql")
|
||||
]
|
||||
|
||||
will_overwrite = Enum.filter(paths, &File.exists?/1)
|
||||
proceed? = Enum.empty?(will_overwrite) or Keyword.get(options, :force, false)
|
||||
|
||||
unless not proceed? do
|
||||
[domain, port | _] =
|
||||
String.split(
|
||||
Common.get_option(
|
||||
options,
|
||||
:domain,
|
||||
"What domain will your instance use? (e.g pleroma.soykaf.com)"
|
||||
),
|
||||
":"
|
||||
) ++ [443]
|
||||
|
||||
name =
|
||||
Common.get_option(
|
||||
options,
|
||||
: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?")
|
||||
|
||||
dbhost =
|
||||
Common.get_option(options, :dbhost, "What is the hostname of your database?", "localhost")
|
||||
|
||||
dbname =
|
||||
Common.get_option(options, :dbname, "What is the name of your database?", "pleroma_dev")
|
||||
|
||||
dbuser =
|
||||
Common.get_option(
|
||||
options,
|
||||
:dbuser,
|
||||
"What is the user used to connect to your database?",
|
||||
"pleroma"
|
||||
)
|
||||
|
||||
dbpass =
|
||||
Common.get_option(
|
||||
options,
|
||||
:dbpass,
|
||||
"What is the password used to connect to your database?",
|
||||
:crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64),
|
||||
"autogenerated"
|
||||
)
|
||||
|
||||
secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
|
||||
{web_push_public_key, web_push_private_key} = :crypto.generate_key(:ecdh, :prime256v1)
|
||||
|
||||
result_config =
|
||||
EEx.eval_file(
|
||||
"sample_config.eex" |> Path.expand(__DIR__),
|
||||
domain: domain,
|
||||
port: port,
|
||||
email: email,
|
||||
name: name,
|
||||
dbhost: dbhost,
|
||||
dbname: dbname,
|
||||
dbuser: dbuser,
|
||||
dbpass: dbpass,
|
||||
version: Pleroma.Mixfile.project() |> Keyword.get(:version),
|
||||
secret: secret,
|
||||
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)
|
||||
)
|
||||
|
||||
result_psql =
|
||||
EEx.eval_file(
|
||||
"sample_psql.eex" |> Path.expand(__DIR__),
|
||||
dbname: dbname,
|
||||
dbuser: dbuser,
|
||||
dbpass: dbpass
|
||||
)
|
||||
|
||||
Mix.shell().info(
|
||||
"Writing config to #{config_path}. You should rename it to config/prod.secret.exs or config/dev.secret.exs."
|
||||
)
|
||||
|
||||
File.write(config_path, result_config)
|
||||
Mix.shell().info("Writing #{psql_path}.")
|
||||
File.write(psql_path, result_psql)
|
||||
|
||||
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
|
||||
)
|
||||
else
|
||||
Mix.shell().error(
|
||||
"The task would have overwritten the following files:\n" <>
|
||||
(Enum.map(paths, &"- #{&1}\n") |> Enum.join("")) <>
|
||||
"Rerun with `--force` to overwrite them."
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
43
lib/mix/tasks/pleroma/relay.ex
Normal file
43
lib/mix/tasks/pleroma/relay.ex
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
defmodule Mix.Tasks.Pleroma.Relay do
|
||||
use Mix.Task
|
||||
alias Pleroma.Web.ActivityPub.Relay
|
||||
alias Mix.Tasks.Pleroma.Common
|
||||
|
||||
@shortdoc "Manages remote relays"
|
||||
@moduledoc """
|
||||
Manages remote relays
|
||||
|
||||
## Follow a remote relay
|
||||
|
||||
``mix pleroma.relay follow <relay_url>``
|
||||
|
||||
Example: ``mix pleroma.relay follow https://example.org/relay``
|
||||
|
||||
## Unfollow a remote relay
|
||||
|
||||
``mix pleroma.relay unfollow <relay_url>``
|
||||
|
||||
Example: ``mix pleroma.relay unfollow https://example.org/relay``
|
||||
"""
|
||||
def run(["follow", target]) do
|
||||
Common.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)}")
|
||||
end
|
||||
end
|
||||
|
||||
def run(["unfollow", target]) do
|
||||
Common.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)}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,7 +1,12 @@
|
|||
# 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: 443],
|
||||
url: [host: "<%= domain %>", scheme: "https", port: <%= port %>],
|
||||
secret_key_base: "<%= secret %>"
|
||||
|
||||
config :pleroma, :instance,
|
||||
|
|
@ -16,15 +21,20 @@ config :pleroma, :media_proxy,
|
|||
redirect_on_failure: true
|
||||
#base_url: "https://cache.pleroma.social"
|
||||
|
||||
# Configure your database
|
||||
config :pleroma, Pleroma.Repo,
|
||||
adapter: Ecto.Adapters.Postgres,
|
||||
username: "pleroma",
|
||||
username: "<%= dbuser %>",
|
||||
password: "<%= dbpass %>",
|
||||
database: "pleroma_dev",
|
||||
hostname: "localhost",
|
||||
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
|
||||
|
|
@ -50,9 +60,9 @@ config :pleroma, Pleroma.Repo,
|
|||
|
||||
|
||||
# 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
|
||||
#
|
||||
# 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,
|
||||
7
lib/mix/tasks/pleroma/sample_psql.eex
Normal file
7
lib/mix/tasks/pleroma/sample_psql.eex
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
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";
|
||||
103
lib/mix/tasks/pleroma/uploads.ex
Normal file
103
lib/mix/tasks/pleroma/uploads.ex
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
defmodule Mix.Tasks.Pleroma.Uploads do
|
||||
use Mix.Task
|
||||
alias Pleroma.{Upload, Uploaders.Local}
|
||||
alias Mix.Tasks.Pleroma.Common
|
||||
require Logger
|
||||
|
||||
@log_every 50
|
||||
|
||||
@shortdoc "Migrates uploads from local to remote storage"
|
||||
@moduledoc """
|
||||
Manages uploads
|
||||
|
||||
## Migrate uploads from local to remote storage
|
||||
mix pleroma.uploads migrate_local TARGET_UPLOADER [OPTIONS...]
|
||||
Options:
|
||||
- `--delete` - delete local uploads after migrating them to the target uploader
|
||||
|
||||
|
||||
A list of avalible uploaders can be seen in config.exs
|
||||
"""
|
||||
def run(["migrate_local", target_uploader | args]) do
|
||||
delete? = Enum.member?(args, "--delete")
|
||||
Common.start_pleroma()
|
||||
local_path = Pleroma.Config.get!([Local, :uploads])
|
||||
uploader = Module.concat(Pleroma.Uploaders, target_uploader)
|
||||
|
||||
unless Code.ensure_loaded?(uploader) do
|
||||
raise("The uploader #{inspect(uploader)} is not an existing/loaded module.")
|
||||
end
|
||||
|
||||
target_enabled? = Pleroma.Config.get([Upload, :uploader]) == uploader
|
||||
|
||||
unless target_enabled? do
|
||||
Pleroma.Config.put([Upload, :uploader], uploader)
|
||||
end
|
||||
|
||||
Mix.shell().info("Migrating files from local #{local_path} to #{to_string(uploader)}")
|
||||
|
||||
if delete? do
|
||||
Mix.shell().info(
|
||||
"Attention: uploaded files will be deleted, hope you have backups! (--delete ; cancel with ^C)"
|
||||
)
|
||||
|
||||
:timer.sleep(:timer.seconds(5))
|
||||
end
|
||||
|
||||
uploads =
|
||||
File.ls!(local_path)
|
||||
|> Enum.map(fn id ->
|
||||
root_path = Path.join(local_path, id)
|
||||
|
||||
cond do
|
||||
File.dir?(root_path) ->
|
||||
files = for file <- File.ls!(root_path), do: {id, file, Path.join([root_path, file])}
|
||||
|
||||
case List.first(files) do
|
||||
{id, file, path} ->
|
||||
{%Pleroma.Upload{id: id, name: file, path: id <> "/" <> file, tempfile: path},
|
||||
root_path}
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
||||
File.exists?(root_path) ->
|
||||
file = Path.basename(id)
|
||||
hash = Path.rootname(id)
|
||||
{%Pleroma.Upload{id: hash, name: file, path: file, tempfile: root_path}, root_path}
|
||||
|
||||
true ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
total_count = length(uploads)
|
||||
Mix.shell().info("Found #{total_count} uploads")
|
||||
|
||||
uploads
|
||||
|> Task.async_stream(
|
||||
fn {upload, root_path} ->
|
||||
case Upload.store(upload, uploader: uploader, filters: [], size_limit: nil) do
|
||||
{:ok, _} ->
|
||||
if delete?, do: File.rm_rf!(root_path)
|
||||
Logger.debug("uploaded: #{inspect(upload.path)} #{inspect(upload)}")
|
||||
:ok
|
||||
|
||||
error ->
|
||||
Mix.shell().error("failed to upload #{inspect(upload.path)}: #{inspect(error)}")
|
||||
end
|
||||
end,
|
||||
timeout: 150_000
|
||||
)
|
||||
|> Stream.chunk_every(@log_every)
|
||||
|> Enum.reduce(0, fn done, count ->
|
||||
count = count + length(done)
|
||||
Mix.shell().info("Uploaded #{count}/#{total_count} files")
|
||||
count
|
||||
end)
|
||||
|
||||
Mix.shell().info("Done!")
|
||||
end
|
||||
end
|
||||
300
lib/mix/tasks/pleroma/user.ex
Normal file
300
lib/mix/tasks/pleroma/user.ex
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
defmodule Mix.Tasks.Pleroma.User do
|
||||
use Mix.Task
|
||||
import Ecto.Changeset
|
||||
alias Pleroma.{Repo, User}
|
||||
alias Mix.Tasks.Pleroma.Common
|
||||
|
||||
@shortdoc "Manages Pleroma users"
|
||||
@moduledoc """
|
||||
Manages Pleroma users.
|
||||
|
||||
## Create a new user.
|
||||
|
||||
mix pleroma.user new NICKNAME EMAIL [OPTION...]
|
||||
|
||||
Options:
|
||||
- `--name NAME` - the user's name (i.e., "Lain Iwakura")
|
||||
- `--bio BIO` - the user's bio
|
||||
- `--password PASSWORD` - the user's password
|
||||
- `--moderator`/`--no-moderator` - whether the user is a moderator
|
||||
- `--admin`/`--no-admin` - whether the user is an admin
|
||||
|
||||
## Generate an invite link.
|
||||
|
||||
mix pleroma.user invite
|
||||
|
||||
## Delete the user's account.
|
||||
|
||||
mix pleroma.user rm NICKNAME
|
||||
|
||||
## Deactivate or activate the user's account.
|
||||
|
||||
mix pleroma.user toggle_activated NICKNAME
|
||||
|
||||
## Unsubscribe local users from user's account and deactivate it
|
||||
|
||||
mix pleroma.user unsubscribe NICKNAME
|
||||
|
||||
## Create a password reset link.
|
||||
|
||||
mix pleroma.user reset_password NICKNAME
|
||||
|
||||
## Set the value of the given user's settings.
|
||||
|
||||
mix pleroma.user set NICKNAME [OPTION...]
|
||||
|
||||
Options:
|
||||
- `--locked`/`--no-locked` - whether the user's account is locked
|
||||
- `--moderator`/`--no-moderator` - whether the user is a moderator
|
||||
- `--admin`/`--no-admin` - whether the user is an admin
|
||||
"""
|
||||
def run(["new", nickname, email | rest]) do
|
||||
{options, [], []} =
|
||||
OptionParser.parse(
|
||||
rest,
|
||||
strict: [
|
||||
name: :string,
|
||||
bio: :string,
|
||||
password: :string,
|
||||
moderator: :boolean,
|
||||
admin: :boolean
|
||||
]
|
||||
)
|
||||
|
||||
name = Keyword.get(options, :name, nickname)
|
||||
bio = Keyword.get(options, :bio, "")
|
||||
|
||||
{password, generated_password?} =
|
||||
case Keyword.get(options, :password) do
|
||||
nil ->
|
||||
{:crypto.strong_rand_bytes(16) |> Base.encode64(), true}
|
||||
|
||||
password ->
|
||||
{password, false}
|
||||
end
|
||||
|
||||
moderator? = Keyword.get(options, :moderator, false)
|
||||
admin? = Keyword.get(options, :admin, false)
|
||||
|
||||
Mix.shell().info("""
|
||||
A user will be created with the following information:
|
||||
- nickname: #{nickname}
|
||||
- email: #{email}
|
||||
- password: #{
|
||||
if(generated_password?, do: "[generated; a reset link will be created]", else: password)
|
||||
}
|
||||
- name: #{name}
|
||||
- bio: #{bio}
|
||||
- moderator: #{if(moderator?, do: "true", else: "false")}
|
||||
- admin: #{if(admin?, do: "true", else: "false")}
|
||||
""")
|
||||
|
||||
proceed? = Mix.shell().yes?("Continue?")
|
||||
|
||||
unless not proceed? do
|
||||
Common.start_pleroma()
|
||||
|
||||
params = %{
|
||||
nickname: nickname,
|
||||
email: email,
|
||||
password: password,
|
||||
password_confirmation: password,
|
||||
name: name,
|
||||
bio: bio
|
||||
}
|
||||
|
||||
user = User.register_changeset(%User{}, params)
|
||||
Repo.insert!(user)
|
||||
|
||||
Mix.shell().info("User #{nickname} created")
|
||||
|
||||
if moderator? do
|
||||
run(["set", nickname, "--moderator"])
|
||||
end
|
||||
|
||||
if admin? do
|
||||
run(["set", nickname, "--admin"])
|
||||
end
|
||||
|
||||
if generated_password? do
|
||||
run(["reset_password", nickname])
|
||||
end
|
||||
else
|
||||
Mix.shell().info("User will not be created.")
|
||||
end
|
||||
end
|
||||
|
||||
def run(["rm", nickname]) do
|
||||
Common.start_pleroma()
|
||||
|
||||
with %User{local: true} = user <- User.get_by_nickname(nickname) do
|
||||
User.delete(user)
|
||||
Mix.shell().info("User #{nickname} deleted.")
|
||||
else
|
||||
_ ->
|
||||
Mix.shell().error("No local user #{nickname}")
|
||||
end
|
||||
end
|
||||
|
||||
def run(["toggle_activated", nickname]) do
|
||||
Common.start_pleroma()
|
||||
|
||||
with %User{} = user <- User.get_by_nickname(nickname) do
|
||||
{:ok, user} = User.deactivate(user, !user.info.deactivated)
|
||||
|
||||
Mix.shell().info(
|
||||
"Activation status of #{nickname}: #{if(user.info.deactivated, do: "de", else: "")}activated"
|
||||
)
|
||||
else
|
||||
_ ->
|
||||
Mix.shell().error("No user #{nickname}")
|
||||
end
|
||||
end
|
||||
|
||||
def run(["reset_password", nickname]) do
|
||||
Common.start_pleroma()
|
||||
|
||||
with %User{local: true} = user <- User.get_by_nickname(nickname),
|
||||
{:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do
|
||||
Mix.shell().info("Generated password reset token for #{user.nickname}")
|
||||
|
||||
IO.puts(
|
||||
"URL: #{
|
||||
Pleroma.Web.Router.Helpers.util_url(
|
||||
Pleroma.Web.Endpoint,
|
||||
:show_password_reset,
|
||||
token.token
|
||||
)
|
||||
}"
|
||||
)
|
||||
else
|
||||
_ ->
|
||||
Mix.shell().error("No local user #{nickname}")
|
||||
end
|
||||
end
|
||||
|
||||
def run(["unsubscribe", nickname]) do
|
||||
Common.start_pleroma()
|
||||
|
||||
with %User{} = user <- User.get_by_nickname(nickname) do
|
||||
Mix.shell().info("Deactivating #{user.nickname}")
|
||||
User.deactivate(user)
|
||||
|
||||
{:ok, friends} = User.get_friends(user)
|
||||
|
||||
Enum.each(friends, fn friend ->
|
||||
user = Repo.get(User, user.id)
|
||||
|
||||
Mix.shell().info("Unsubscribing #{friend.nickname} from #{user.nickname}")
|
||||
User.unfollow(user, friend)
|
||||
end)
|
||||
|
||||
:timer.sleep(500)
|
||||
|
||||
user = Repo.get(User, user.id)
|
||||
|
||||
if length(user.following) == 0 do
|
||||
Mix.shell().info("Successfully unsubscribed all followers from #{user.nickname}")
|
||||
end
|
||||
else
|
||||
_ ->
|
||||
Mix.shell().error("No user #{nickname}")
|
||||
end
|
||||
end
|
||||
|
||||
def run(["set", nickname | rest]) do
|
||||
Common.start_pleroma()
|
||||
|
||||
{options, [], []} =
|
||||
OptionParser.parse(
|
||||
rest,
|
||||
strict: [
|
||||
moderator: :boolean,
|
||||
admin: :boolean,
|
||||
locked: :boolean
|
||||
]
|
||||
)
|
||||
|
||||
with %User{local: true} = user <- User.get_by_nickname(nickname) do
|
||||
user =
|
||||
case Keyword.get(options, :moderator) do
|
||||
nil -> user
|
||||
value -> set_moderator(user, value)
|
||||
end
|
||||
|
||||
user =
|
||||
case Keyword.get(options, :locked) do
|
||||
nil -> user
|
||||
value -> set_locked(user, value)
|
||||
end
|
||||
|
||||
_user =
|
||||
case Keyword.get(options, :admin) do
|
||||
nil -> user
|
||||
value -> set_admin(user, value)
|
||||
end
|
||||
else
|
||||
_ ->
|
||||
Mix.shell().error("No local user #{nickname}")
|
||||
end
|
||||
end
|
||||
|
||||
def run(["invite"]) do
|
||||
Common.start_pleroma()
|
||||
|
||||
with {:ok, token} <- Pleroma.UserInviteToken.create_token() do
|
||||
Mix.shell().info("Generated user invite token")
|
||||
|
||||
url =
|
||||
Pleroma.Web.Router.Helpers.redirect_url(
|
||||
Pleroma.Web.Endpoint,
|
||||
:registration_page,
|
||||
token.token
|
||||
)
|
||||
|
||||
IO.puts(url)
|
||||
else
|
||||
_ ->
|
||||
Mix.shell().error("Could not create invite token.")
|
||||
end
|
||||
end
|
||||
|
||||
defp set_moderator(user, value) do
|
||||
info_cng = User.Info.admin_api_update(user.info, %{is_moderator: value})
|
||||
|
||||
user_cng =
|
||||
Ecto.Changeset.change(user)
|
||||
|> put_embed(:info, info_cng)
|
||||
|
||||
{:ok, user} = User.update_and_set_cache(user_cng)
|
||||
|
||||
Mix.shell().info("Moderator status of #{user.nickname}: #{user.info.is_moderator}")
|
||||
user
|
||||
end
|
||||
|
||||
defp set_admin(user, value) do
|
||||
info_cng = User.Info.admin_api_update(user.info, %{is_admin: value})
|
||||
|
||||
user_cng =
|
||||
Ecto.Changeset.change(user)
|
||||
|> put_embed(:info, info_cng)
|
||||
|
||||
{:ok, user} = User.update_and_set_cache(user_cng)
|
||||
|
||||
Mix.shell().info("Admin status of #{user.nickname}: #{user.info.is_admin}")
|
||||
user
|
||||
end
|
||||
|
||||
defp set_locked(user, value) do
|
||||
info_cng = User.Info.user_upgrade(user.info, %{locked: value})
|
||||
|
||||
user_cng =
|
||||
Ecto.Changeset.change(user)
|
||||
|> put_embed(:info, info_cng)
|
||||
|
||||
{:ok, user} = User.update_and_set_cache(user_cng)
|
||||
|
||||
Mix.shell().info("Locked status of #{user.nickname}: #{user.info.locked}")
|
||||
user
|
||||
end
|
||||
end
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
defmodule Mix.Tasks.ReactivateUser do
|
||||
use Mix.Task
|
||||
alias Pleroma.User
|
||||
|
||||
@moduledoc """
|
||||
Reactivate a user
|
||||
|
||||
Usage: ``mix reactivate_user <nickname>``
|
||||
|
||||
Example: ``mix reactivate_user lain``
|
||||
"""
|
||||
def run([nickname]) do
|
||||
Mix.Task.run("app.start")
|
||||
|
||||
with user <- User.get_by_nickname(nickname) do
|
||||
User.deactivate(user, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
defmodule Mix.Tasks.RegisterUser do
|
||||
@moduledoc """
|
||||
Manually register a local user
|
||||
|
||||
Usage: ``mix register_user <name> <nickname> <email> <bio> <password>``
|
||||
|
||||
Example: ``mix register_user 仮面の告白 lain lain@example.org "blushy-crushy fediverse idol + pleroma dev" pleaseDontHeckLain``
|
||||
"""
|
||||
|
||||
use Mix.Task
|
||||
alias Pleroma.{Repo, User}
|
||||
|
||||
@shortdoc "Register user"
|
||||
def run([name, nickname, email, bio, password]) do
|
||||
Mix.Task.run("app.start")
|
||||
|
||||
params = %{
|
||||
name: name,
|
||||
nickname: nickname,
|
||||
email: email,
|
||||
password: password,
|
||||
password_confirmation: password,
|
||||
bio: bio
|
||||
}
|
||||
|
||||
user = User.register_changeset(%User{}, params)
|
||||
|
||||
Repo.insert!(user)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
defmodule Mix.Tasks.RelayFollow do
|
||||
use Mix.Task
|
||||
require Logger
|
||||
alias Pleroma.Web.ActivityPub.Relay
|
||||
|
||||
@shortdoc "Follows a remote relay"
|
||||
@moduledoc """
|
||||
Follows a remote relay
|
||||
|
||||
Usage: ``mix relay_follow <relay_url>``
|
||||
|
||||
Example: ``mix relay_follow https://example.org/relay``
|
||||
"""
|
||||
def run([target]) do
|
||||
Mix.Task.run("app.start")
|
||||
|
||||
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)}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
defmodule Mix.Tasks.RelayUnfollow do
|
||||
use Mix.Task
|
||||
require Logger
|
||||
alias Pleroma.Web.ActivityPub.Relay
|
||||
|
||||
@moduledoc """
|
||||
Unfollows a remote relay
|
||||
|
||||
Usage: ``mix relay_follow <relay_url>``
|
||||
|
||||
Example: ``mix relay_follow https://example.org/relay``
|
||||
"""
|
||||
def run([target]) do
|
||||
Mix.Task.run("app.start")
|
||||
|
||||
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)}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
defmodule Mix.Tasks.RmUser do
|
||||
use Mix.Task
|
||||
alias Pleroma.User
|
||||
|
||||
@moduledoc """
|
||||
Permanently deletes a user
|
||||
|
||||
Usage: ``mix rm_user [nickname]``
|
||||
|
||||
Example: ``mix rm_user lain``
|
||||
"""
|
||||
def run([nickname]) do
|
||||
Mix.Task.run("app.start")
|
||||
|
||||
with %User{local: true} = user <- User.get_by_nickname(nickname) do
|
||||
{:ok, _} = User.delete(user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
CREATE USER pleroma WITH ENCRYPTED PASSWORD '<%= dbpass %>';
|
||||
CREATE DATABASE pleroma_dev OWNER pleroma;
|
||||
\c pleroma_dev;
|
||||
--Extensions made by ecto.migrate that need superuser access
|
||||
CREATE EXTENSION IF NOT EXISTS citext;
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
defmodule Mix.Tasks.SetAdmin do
|
||||
use Mix.Task
|
||||
alias Pleroma.User
|
||||
|
||||
@doc """
|
||||
Sets admin status
|
||||
Usage: set_admin nickname [true|false]
|
||||
"""
|
||||
def run([nickname | rest]) do
|
||||
Application.ensure_all_started(:pleroma)
|
||||
|
||||
status =
|
||||
case rest do
|
||||
[status] -> status == "true"
|
||||
_ -> true
|
||||
end
|
||||
|
||||
with %User{local: true} = user <- User.get_by_nickname(nickname) do
|
||||
info =
|
||||
user.info
|
||||
|> Map.put("is_admin", !!status)
|
||||
|
||||
cng = User.info_changeset(user, %{info: info})
|
||||
{:ok, user} = User.update_and_set_cache(cng)
|
||||
|
||||
IO.puts("Admin status of #{nickname}: #{user.info["is_admin"]}")
|
||||
else
|
||||
_ ->
|
||||
IO.puts("No local user #{nickname}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
defmodule Mix.Tasks.SetLocked do
|
||||
@moduledoc """
|
||||
Lock a local user
|
||||
|
||||
The local user will then have to manually accept/reject followers. This can also be done by the user into their settings.
|
||||
|
||||
Usage: ``mix set_locked <username>``
|
||||
|
||||
Example: ``mix set_locked lain``
|
||||
"""
|
||||
|
||||
use Mix.Task
|
||||
import Mix.Ecto
|
||||
alias Pleroma.{Repo, User}
|
||||
|
||||
def run([nickname | rest]) do
|
||||
ensure_started(Repo, [])
|
||||
|
||||
locked =
|
||||
case rest do
|
||||
[locked] -> locked == "true"
|
||||
_ -> true
|
||||
end
|
||||
|
||||
with %User{local: true} = user <- User.get_by_nickname(nickname) do
|
||||
info =
|
||||
user.info
|
||||
|> Map.put("locked", !!locked)
|
||||
|
||||
cng = User.info_changeset(user, %{info: info})
|
||||
user = Repo.update!(cng)
|
||||
|
||||
IO.puts("locked status of #{nickname}: #{user.info["locked"]}")
|
||||
else
|
||||
_ ->
|
||||
IO.puts("No local user #{nickname}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
defmodule Mix.Tasks.UnsubscribeUser do
|
||||
use Mix.Task
|
||||
alias Pleroma.{User, Repo}
|
||||
require Logger
|
||||
|
||||
@moduledoc """
|
||||
Deactivate and Unsubscribe local users from a user
|
||||
|
||||
Usage: ``mix unsubscribe_user <nickname>``
|
||||
|
||||
Example: ``mix unsubscribe_user lain``
|
||||
"""
|
||||
def run([nickname]) do
|
||||
Mix.Task.run("app.start")
|
||||
|
||||
with %User{} = user <- User.get_by_nickname(nickname) do
|
||||
Logger.info("Deactivating #{user.nickname}")
|
||||
User.deactivate(user)
|
||||
|
||||
{:ok, friends} = User.get_friends(user)
|
||||
|
||||
Enum.each(friends, fn friend ->
|
||||
user = Repo.get(User, user.id)
|
||||
|
||||
Logger.info("Unsubscribing #{friend.nickname} from #{user.nickname}")
|
||||
User.unfollow(user, friend)
|
||||
end)
|
||||
|
||||
:timer.sleep(500)
|
||||
|
||||
user = Repo.get(User, user.id)
|
||||
|
||||
if length(user.following) == 0 do
|
||||
Logger.info("Successfully unsubscribed all followers from #{user.nickname}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,6 +3,14 @@ defmodule Pleroma.Activity do
|
|||
alias Pleroma.{Repo, Activity, Notification}
|
||||
import Ecto.Query
|
||||
|
||||
# https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
|
||||
@mastodon_notification_types %{
|
||||
"Create" => "mention",
|
||||
"Follow" => "follow",
|
||||
"Announce" => "reblog",
|
||||
"Like" => "favourite"
|
||||
}
|
||||
|
||||
schema "activities" do
|
||||
field(:data, :map)
|
||||
field(:local, :boolean, default: true)
|
||||
|
|
@ -88,4 +96,11 @@ defmodule Pleroma.Activity do
|
|||
end
|
||||
|
||||
def get_in_reply_to_activity(_), do: nil
|
||||
|
||||
for {ap_type, type} <- @mastodon_notification_types do
|
||||
def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
|
||||
do: unquote(type)
|
||||
end
|
||||
|
||||
def mastodon_notification_type(%Activity{}), do: nil
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
defmodule Pleroma.Application do
|
||||
use Application
|
||||
import Supervisor.Spec
|
||||
|
||||
@name "Pleroma"
|
||||
@version Mix.Project.config()[:version]
|
||||
|
|
@ -7,11 +8,14 @@ defmodule Pleroma.Application do
|
|||
def version, do: @version
|
||||
def named_version(), do: @name <> " " <> @version
|
||||
|
||||
def user_agent() do
|
||||
info = "#{Pleroma.Web.base_url()} <#{Pleroma.Config.get([:instance, :email], "")}>"
|
||||
named_version() <> "; " <> info
|
||||
end
|
||||
|
||||
# See http://elixir-lang.org/docs/stable/elixir/Application.html
|
||||
# for more information on OTP Applications
|
||||
@env Mix.env()
|
||||
def start(_type, _args) do
|
||||
import Supervisor.Spec
|
||||
import Cachex.Spec
|
||||
|
||||
# Define workers and child supervisors to be supervised
|
||||
|
|
@ -20,10 +24,7 @@ defmodule Pleroma.Application do
|
|||
# Start the Ecto repository
|
||||
supervisor(Pleroma.Repo, []),
|
||||
worker(Pleroma.Emoji, []),
|
||||
# Start the endpoint when the application starts
|
||||
supervisor(Pleroma.Web.Endpoint, []),
|
||||
# Start your own worker by calling: Pleroma.Worker.start_link(arg1, arg2, arg3)
|
||||
# worker(Pleroma.Worker, [arg1, arg2, arg3]),
|
||||
worker(Pleroma.Captcha, []),
|
||||
worker(
|
||||
Cachex,
|
||||
[
|
||||
|
|
@ -63,20 +64,18 @@ defmodule Pleroma.Application do
|
|||
],
|
||||
id: :cachex_idem
|
||||
),
|
||||
worker(Pleroma.Web.Federator, []),
|
||||
worker(Pleroma.Web.Federator.RetryQueue, []),
|
||||
worker(Pleroma.Gopher.Server, []),
|
||||
worker(Pleroma.Stats, [])
|
||||
worker(Pleroma.Web.Federator, []),
|
||||
worker(Pleroma.Stats, []),
|
||||
worker(Pleroma.Web.Push, [])
|
||||
] ++
|
||||
if @env == :test,
|
||||
do: [],
|
||||
else:
|
||||
[worker(Pleroma.Web.Streamer, [])] ++
|
||||
if(
|
||||
!chat_enabled(),
|
||||
do: [],
|
||||
else: [worker(Pleroma.Web.ChatChannel.ChatChannelState, [])]
|
||||
)
|
||||
streamer_child() ++
|
||||
chat_child() ++
|
||||
[
|
||||
# Start the endpoint when the application starts
|
||||
supervisor(Pleroma.Web.Endpoint, []),
|
||||
worker(Pleroma.Gopher.Server, [])
|
||||
]
|
||||
|
||||
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
|
||||
# for other strategies and supported options
|
||||
|
|
@ -84,7 +83,20 @@ defmodule Pleroma.Application do
|
|||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
|
||||
defp chat_enabled do
|
||||
Application.get_env(:pleroma, :chat, []) |> Keyword.get(:enabled)
|
||||
if Mix.env() == :test do
|
||||
defp streamer_child(), do: []
|
||||
defp chat_child(), do: []
|
||||
else
|
||||
defp streamer_child() do
|
||||
[worker(Pleroma.Web.Streamer, [])]
|
||||
end
|
||||
|
||||
defp chat_child() do
|
||||
if Pleroma.Config.get([:chat, :enabled]) do
|
||||
[worker(Pleroma.Web.ChatChannel.ChatChannelState, [])]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
66
lib/pleroma/captcha/captcha.ex
Normal file
66
lib/pleroma/captcha/captcha.ex
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
defmodule Pleroma.Captcha do
|
||||
use GenServer
|
||||
|
||||
@ets_options [:ordered_set, :private, :named_table, {:read_concurrency, true}]
|
||||
|
||||
@doc false
|
||||
def start_link() do
|
||||
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def init(_) do
|
||||
# Create a ETS table to store captchas
|
||||
ets_name = Module.concat(method(), Ets)
|
||||
^ets_name = :ets.new(Module.concat(method(), Ets), @ets_options)
|
||||
|
||||
# Clean up old captchas every few minutes
|
||||
seconds_retained = Pleroma.Config.get!([__MODULE__, :seconds_retained])
|
||||
Process.send_after(self(), :cleanup, 1000 * seconds_retained)
|
||||
|
||||
{:ok, nil}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ask the configured captcha service for a new captcha
|
||||
"""
|
||||
def new() do
|
||||
GenServer.call(__MODULE__, :new)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ask the configured captcha service to validate the captcha
|
||||
"""
|
||||
def validate(token, captcha) do
|
||||
GenServer.call(__MODULE__, {:validate, token, captcha})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_call(:new, _from, state) do
|
||||
enabled = Pleroma.Config.get([__MODULE__, :enabled])
|
||||
|
||||
if !enabled do
|
||||
{:reply, %{type: :none}, state}
|
||||
else
|
||||
{:reply, method().new(), state}
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_call({:validate, token, captcha}, _from, state) do
|
||||
{:reply, method().validate(token, captcha), state}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_info(:cleanup, state) do
|
||||
:ok = method().cleanup()
|
||||
|
||||
seconds_retained = Pleroma.Config.get!([__MODULE__, :seconds_retained])
|
||||
# Schedule the next clenup
|
||||
Process.send_after(self(), :cleanup, 1000 * seconds_retained)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
defp method, do: Pleroma.Config.get!([__MODULE__, :method])
|
||||
end
|
||||
28
lib/pleroma/captcha/captcha_service.ex
Normal file
28
lib/pleroma/captcha/captcha_service.ex
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
defmodule Pleroma.Captcha.Service do
|
||||
@doc """
|
||||
Request new captcha from a captcha service.
|
||||
|
||||
Returns:
|
||||
|
||||
Service-specific data for using the newly created captcha
|
||||
"""
|
||||
@callback new() :: map
|
||||
|
||||
@doc """
|
||||
Validated the provided captcha solution.
|
||||
|
||||
Arguments:
|
||||
* `token` the captcha is associated with
|
||||
* `captcha` solution of the captcha to validate
|
||||
|
||||
Returns:
|
||||
|
||||
`true` if captcha is valid, `false` if not
|
||||
"""
|
||||
@callback validate(token :: String.t(), captcha :: String.t()) :: boolean
|
||||
|
||||
@doc """
|
||||
This function is called periodically to clean up old captchas
|
||||
"""
|
||||
@callback cleanup() :: :ok
|
||||
end
|
||||
67
lib/pleroma/captcha/kocaptcha.ex
Normal file
67
lib/pleroma/captcha/kocaptcha.ex
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
defmodule Pleroma.Captcha.Kocaptcha do
|
||||
alias Calendar.DateTime
|
||||
|
||||
alias Pleroma.Captcha.Service
|
||||
@behaviour Service
|
||||
|
||||
@ets __MODULE__.Ets
|
||||
|
||||
@impl Service
|
||||
def new() do
|
||||
endpoint = Pleroma.Config.get!([__MODULE__, :endpoint])
|
||||
|
||||
case Tesla.get(endpoint <> "/new") do
|
||||
{:error, _} ->
|
||||
%{error: "Kocaptcha service unavailable"}
|
||||
|
||||
{:ok, res} ->
|
||||
json_resp = Poison.decode!(res.body)
|
||||
|
||||
token = json_resp["token"]
|
||||
|
||||
true =
|
||||
:ets.insert(
|
||||
@ets,
|
||||
{token, json_resp["md5"], DateTime.now_utc() |> DateTime.Format.unix()}
|
||||
)
|
||||
|
||||
%{type: :kocaptcha, token: token, url: endpoint <> json_resp["url"]}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Service
|
||||
def validate(token, captcha) do
|
||||
with false <- is_nil(captcha),
|
||||
[{^token, saved_md5, _}] <- :ets.lookup(@ets, token),
|
||||
true <- :crypto.hash(:md5, captcha) |> Base.encode16() == String.upcase(saved_md5) do
|
||||
# Clear the saved value
|
||||
:ets.delete(@ets, token)
|
||||
|
||||
true
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
@impl Service
|
||||
def cleanup() do
|
||||
seconds_retained = Pleroma.Config.get!([Pleroma.Captcha, :seconds_retained])
|
||||
# If the time in ETS is less than current_time - seconds_retained, then the time has
|
||||
# already passed
|
||||
delete_after =
|
||||
DateTime.subtract!(DateTime.now_utc(), seconds_retained) |> DateTime.Format.unix()
|
||||
|
||||
:ets.select_delete(
|
||||
@ets,
|
||||
[
|
||||
{
|
||||
{:_, :_, :"$1"},
|
||||
[{:<, :"$1", {:const, delete_after}}],
|
||||
[true]
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
|
@ -39,4 +39,18 @@ defmodule Pleroma.Config do
|
|||
def put(key, value) do
|
||||
Application.put_env(:pleroma, key, value)
|
||||
end
|
||||
|
||||
def delete([key]), do: delete(key)
|
||||
|
||||
def delete([parent_key | keys]) do
|
||||
{_, parent} =
|
||||
Application.get_env(:pleroma, parent_key)
|
||||
|> get_and_update_in(keys, fn _ -> :pop end)
|
||||
|
||||
Application.put_env(:pleroma, parent_key, parent)
|
||||
end
|
||||
|
||||
def delete(key) do
|
||||
Application.delete_env(:pleroma, key)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
3
lib/pleroma/emails/mailer.ex
Normal file
3
lib/pleroma/emails/mailer.ex
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
defmodule Pleroma.Mailer do
|
||||
use Swoosh.Mailer, otp_app: :pleroma
|
||||
end
|
||||
66
lib/pleroma/emails/user_email.ex
Normal file
66
lib/pleroma/emails/user_email.ex
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
defmodule Pleroma.UserEmail do
|
||||
@moduledoc "User emails"
|
||||
|
||||
import Swoosh.Email
|
||||
|
||||
alias Pleroma.Web.{Endpoint, Router}
|
||||
|
||||
defp instance_config, do: Pleroma.Config.get(:instance)
|
||||
|
||||
defp instance_name, do: instance_config()[:name]
|
||||
|
||||
defp sender do
|
||||
{instance_name(), instance_config()[:email]}
|
||||
end
|
||||
|
||||
defp recipient(email, nil), do: email
|
||||
defp recipient(email, name), do: {name, email}
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
html_body = """
|
||||
<h3>Reset your password at #{instance_name()}</h3>
|
||||
<p>Someone has requested password change for your account at #{instance_name()}.</p>
|
||||
<p>If it was you, visit the following link to proceed: <a href="#{password_reset_url}">reset password</a>.</p>
|
||||
<p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>
|
||||
"""
|
||||
|
||||
new()
|
||||
|> to(recipient(user.email, user.name))
|
||||
|> from(sender())
|
||||
|> subject("Password reset")
|
||||
|> html_body(html_body)
|
||||
end
|
||||
|
||||
def user_invitation_email(
|
||||
user,
|
||||
%Pleroma.UserInviteToken{} = user_invite_token,
|
||||
to_email,
|
||||
to_name \\ nil
|
||||
) do
|
||||
registration_url =
|
||||
Router.Helpers.redirect_url(
|
||||
Endpoint,
|
||||
:registration_page,
|
||||
user_invite_token.token
|
||||
)
|
||||
|
||||
html_body = """
|
||||
<h3>You are invited to #{instance_name()}</h3>
|
||||
<p>#{user.name} invites you to join #{instance_name()}, an instance of Pleroma federated social networking platform.</p>
|
||||
<p>Click the following link to register: <a href="#{registration_url}">accept invitation</a>.</p>
|
||||
"""
|
||||
|
||||
new()
|
||||
|> to(recipient(to_email, to_name))
|
||||
|> from(sender())
|
||||
|> subject("Invitation to #{instance_name()}")
|
||||
|> html_body(html_body)
|
||||
end
|
||||
end
|
||||
|
|
@ -10,7 +10,7 @@ defmodule Pleroma.Emoji do
|
|||
"""
|
||||
use GenServer
|
||||
@ets __MODULE__.Ets
|
||||
@ets_options [:set, :protected, :named_table, {:read_concurrency, true}]
|
||||
@ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}]
|
||||
|
||||
@doc false
|
||||
def start_link() do
|
||||
|
|
@ -165,7 +165,7 @@ defmodule Pleroma.Emoji do
|
|||
|
||||
defp load_from_file_stream(stream) do
|
||||
stream
|
||||
|> Stream.map(&String.strip/1)
|
||||
|> Stream.map(&String.trim/1)
|
||||
|> Stream.map(fn line ->
|
||||
case String.split(line, ~r/,\s*/) do
|
||||
[name, file] -> {name, file}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
defmodule Pleroma.Filter do
|
||||
use Ecto.Schema
|
||||
import Ecto.{Changeset, Query}
|
||||
alias Pleroma.{User, Repo, Activity}
|
||||
alias Pleroma.{User, Repo}
|
||||
|
||||
schema "filters" do
|
||||
belongs_to(:user, Pleroma.User)
|
||||
belongs_to(:user, User)
|
||||
field(:filter_id, :integer)
|
||||
field(:hide, :boolean, default: false)
|
||||
field(:whole_word, :boolean, default: true)
|
||||
|
|
@ -26,7 +26,7 @@ defmodule Pleroma.Filter do
|
|||
Repo.one(query)
|
||||
end
|
||||
|
||||
def get_filters(%Pleroma.User{id: user_id} = user) do
|
||||
def get_filters(%User{id: user_id} = _user) do
|
||||
query =
|
||||
from(
|
||||
f in Pleroma.Filter,
|
||||
|
|
@ -38,9 +38,9 @@ defmodule Pleroma.Filter do
|
|||
|
||||
def create(%Pleroma.Filter{user_id: user_id, filter_id: nil} = filter) do
|
||||
# If filter_id wasn't given, use the max filter_id for this user plus 1.
|
||||
# XXX This could result in a race condition if a user tries to add two
|
||||
# different filters for their account from two different clients at the
|
||||
# same time, but that should be unlikely.
|
||||
# XXX This could result in a race condition if a user tries to add two
|
||||
# different filters for their account from two different clients at the
|
||||
# same time, but that should be unlikely.
|
||||
|
||||
max_id_query =
|
||||
from(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ defmodule Pleroma.Formatter do
|
|||
alias Pleroma.Emoji
|
||||
|
||||
@tag_regex ~r/\#\w+/u
|
||||
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
|
||||
|
||||
def parse_tags(text, data \\ %{}) do
|
||||
Regex.scan(@tag_regex, text)
|
||||
|> Enum.map(fn ["#" <> tag = full_tag] -> {full_tag, String.downcase(tag)} end)
|
||||
|
|
@ -18,7 +20,7 @@ defmodule Pleroma.Formatter do
|
|||
def parse_mentions(text) do
|
||||
# Modified from https://www.w3.org/TR/html5/forms.html#valid-e-mail-address
|
||||
regex =
|
||||
~r/@[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]*@?[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*/u
|
||||
~r/@[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]*@?[a-zA-Z0-9_-](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*/u
|
||||
|
||||
Regex.scan(regex, text)
|
||||
|> List.flatten()
|
||||
|
|
@ -76,6 +78,18 @@ defmodule Pleroma.Formatter do
|
|||
|> Enum.join("")
|
||||
end
|
||||
|
||||
@doc """
|
||||
Escapes a special characters in mention names.
|
||||
"""
|
||||
@spec mentions_escape(String.t(), list({String.t(), any()})) :: String.t()
|
||||
def mentions_escape(text, mentions) do
|
||||
mentions
|
||||
|> Enum.reduce(text, fn {name, _}, acc ->
|
||||
escape_name = String.replace(name, @markdown_characters_regex, "\\\\\\1")
|
||||
String.replace(acc, name, escape_name)
|
||||
end)
|
||||
end
|
||||
|
||||
@doc "changes scheme:... urls to html links"
|
||||
def add_links({subs, text}) do
|
||||
links =
|
||||
|
|
@ -114,10 +128,10 @@ defmodule Pleroma.Formatter do
|
|||
|
||||
subs =
|
||||
subs ++
|
||||
Enum.map(mentions, fn {match, %User{ap_id: ap_id, info: info}, uuid} ->
|
||||
Enum.map(mentions, fn {match, %User{id: id, ap_id: ap_id, info: info}, uuid} ->
|
||||
ap_id =
|
||||
if is_binary(info["source_data"]["url"]) do
|
||||
info["source_data"]["url"]
|
||||
if is_binary(info.source_data["url"]) do
|
||||
info.source_data["url"]
|
||||
else
|
||||
ap_id
|
||||
end
|
||||
|
|
@ -125,7 +139,7 @@ defmodule Pleroma.Formatter do
|
|||
short_match = String.split(match, "@") |> tl() |> hd()
|
||||
|
||||
{uuid,
|
||||
"<span><a class='mention' href='#{ap_id}'>@<span>#{short_match}</span></a></span>"}
|
||||
"<span><a data-user='#{id}' class='mention' href='#{ap_id}'>@<span>#{short_match}</span></a></span>"}
|
||||
end)
|
||||
|
||||
{subs, uuid_text}
|
||||
|
|
@ -147,7 +161,11 @@ defmodule Pleroma.Formatter do
|
|||
subs =
|
||||
subs ++
|
||||
Enum.map(tags, fn {tag_text, tag, uuid} ->
|
||||
url = "<a href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{tag_text}</a>"
|
||||
url =
|
||||
"<a data-tag='#{tag}' href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{
|
||||
tag_text
|
||||
}</a>"
|
||||
|
||||
{uuid, url}
|
||||
end)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,27 +6,28 @@ defmodule Pleroma.Gopher.Server do
|
|||
config = Pleroma.Config.get(:gopher, [])
|
||||
ip = Keyword.get(config, :ip, {0, 0, 0, 0})
|
||||
port = Keyword.get(config, :port, 1234)
|
||||
GenServer.start_link(__MODULE__, [ip, port], [])
|
||||
|
||||
if Keyword.get(config, :enabled, false) do
|
||||
GenServer.start_link(__MODULE__, [ip, port], [])
|
||||
else
|
||||
Logger.info("Gopher server disabled")
|
||||
:ignore
|
||||
end
|
||||
end
|
||||
|
||||
def init([ip, port]) do
|
||||
if Pleroma.Config.get([:gopher, :enabled], false) do
|
||||
Logger.info("Starting gopher server on #{port}")
|
||||
Logger.info("Starting gopher server on #{port}")
|
||||
|
||||
:ranch.start_listener(
|
||||
:gopher,
|
||||
100,
|
||||
:ranch_tcp,
|
||||
[port: port],
|
||||
__MODULE__.ProtocolHandler,
|
||||
[]
|
||||
)
|
||||
:ranch.start_listener(
|
||||
:gopher,
|
||||
100,
|
||||
:ranch_tcp,
|
||||
[ip: ip, port: port],
|
||||
__MODULE__.ProtocolHandler,
|
||||
[]
|
||||
)
|
||||
|
||||
{:ok, %{ip: ip, port: port}}
|
||||
else
|
||||
Logger.info("Gopher server disabled")
|
||||
{:ok, nil}
|
||||
end
|
||||
{:ok, %{ip: ip, port: port}}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -17,15 +17,9 @@ defmodule Pleroma.HTML do
|
|||
end)
|
||||
end
|
||||
|
||||
def filter_tags(html, scrubber) do
|
||||
html |> Scrubber.scrub(scrubber)
|
||||
end
|
||||
|
||||
def filter_tags(html, scrubber), do: Scrubber.scrub(html, scrubber)
|
||||
def filter_tags(html), do: filter_tags(html, nil)
|
||||
|
||||
def strip_tags(html) do
|
||||
html |> Scrubber.scrub(Scrubber.StripTags)
|
||||
end
|
||||
def strip_tags(html), do: Scrubber.scrub(html, Scrubber.StripTags)
|
||||
end
|
||||
|
||||
defmodule Pleroma.HTML.Scrubber.TwitterText do
|
||||
|
|
@ -45,7 +39,7 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
|
|||
Meta.strip_comments()
|
||||
|
||||
# links
|
||||
Meta.allow_tag_with_uri_attributes("a", ["href"], @valid_schemes)
|
||||
Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
|
||||
Meta.allow_tag_with_these_attributes("a", ["name", "title"])
|
||||
|
||||
# paragraphs and linebreaks
|
||||
|
|
@ -86,7 +80,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
|
|||
Meta.remove_cdata_sections_before_scrub()
|
||||
Meta.strip_comments()
|
||||
|
||||
Meta.allow_tag_with_uri_attributes("a", ["href"], @valid_schemes)
|
||||
Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
|
||||
Meta.allow_tag_with_these_attributes("a", ["name", "title"])
|
||||
|
||||
Meta.allow_tag_with_these_attributes("abbr", ["title"])
|
||||
|
|
@ -166,7 +160,7 @@ defmodule Pleroma.HTML.Transform.MediaProxy do
|
|||
{"src", media_url}
|
||||
end
|
||||
|
||||
def scrub_attribute(tag, attribute), do: attribute
|
||||
def scrub_attribute(_tag, attribute), do: attribute
|
||||
|
||||
def scrub({"img", attributes, children}) do
|
||||
attributes =
|
||||
|
|
@ -177,9 +171,9 @@ defmodule Pleroma.HTML.Transform.MediaProxy do
|
|||
{"img", attributes, children}
|
||||
end
|
||||
|
||||
def scrub({:comment, children}), do: ""
|
||||
def scrub({:comment, _children}), do: ""
|
||||
|
||||
def scrub({tag, attributes, children}), do: {tag, attributes, children}
|
||||
def scrub({tag, children}), do: children
|
||||
def scrub({_tag, children}), do: children
|
||||
def scrub(text), do: text
|
||||
end
|
||||
|
|
|
|||
32
lib/pleroma/http/connection.ex
Normal file
32
lib/pleroma/http/connection.ex
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
defmodule Pleroma.HTTP.Connection do
|
||||
@moduledoc """
|
||||
Connection for http-requests.
|
||||
"""
|
||||
|
||||
@hackney_options [
|
||||
pool: :default,
|
||||
timeout: 10000,
|
||||
recv_timeout: 20000,
|
||||
follow_redirect: true
|
||||
]
|
||||
@adapter Application.get_env(:tesla, :adapter)
|
||||
|
||||
@doc """
|
||||
Configure a client connection
|
||||
|
||||
# Returns
|
||||
|
||||
Tesla.Env.client
|
||||
"""
|
||||
@spec new(Keyword.t()) :: Tesla.Env.client()
|
||||
def new(opts \\ []) do
|
||||
Tesla.client([], {@adapter, hackney_options(opts)})
|
||||
end
|
||||
|
||||
# fetch Hackney options
|
||||
#
|
||||
defp hackney_options(opts) do
|
||||
options = Keyword.get(opts, :adapter, [])
|
||||
@hackney_options ++ options
|
||||
end
|
||||
end
|
||||
|
|
@ -1,14 +1,42 @@
|
|||
defmodule Pleroma.HTTP do
|
||||
require HTTPoison
|
||||
@moduledoc """
|
||||
|
||||
"""
|
||||
|
||||
alias Pleroma.HTTP.Connection
|
||||
alias Pleroma.HTTP.RequestBuilder, as: Builder
|
||||
|
||||
@doc """
|
||||
Builds and perform http request.
|
||||
|
||||
# Arguments:
|
||||
`method` - :get, :post, :put, :delete
|
||||
`url`
|
||||
`body`
|
||||
`headers` - a keyworld list of headers, e.g. `[{"content-type", "text/plain"}]`
|
||||
`options` - custom, per-request middleware or adapter options
|
||||
|
||||
# Returns:
|
||||
`{:ok, %Tesla.Env{}}` or `{:error, error}`
|
||||
|
||||
"""
|
||||
def request(method, url, body \\ "", headers \\ [], options \\ []) do
|
||||
options =
|
||||
process_request_options(options)
|
||||
|> process_sni_options(url)
|
||||
|
||||
HTTPoison.request(method, url, body, headers, options)
|
||||
%{}
|
||||
|> Builder.method(method)
|
||||
|> Builder.headers(headers)
|
||||
|> Builder.opts(options)
|
||||
|> Builder.url(url)
|
||||
|> Builder.add_param(:body, :body, body)
|
||||
|> Enum.into([])
|
||||
|> (&Tesla.request(Connection.new(), &1)).()
|
||||
end
|
||||
|
||||
defp process_sni_options(options, nil), do: options
|
||||
|
||||
defp process_sni_options(options, url) do
|
||||
uri = URI.parse(url)
|
||||
host = uri.host |> to_charlist()
|
||||
|
|
@ -22,7 +50,7 @@ defmodule Pleroma.HTTP do
|
|||
def process_request_options(options) do
|
||||
config = Application.get_env(:pleroma, :http, [])
|
||||
proxy = Keyword.get(config, :proxy_url, nil)
|
||||
options = options ++ [hackney: [pool: :default]]
|
||||
options = options ++ [adapter: [pool: :default]]
|
||||
|
||||
case proxy do
|
||||
nil -> options
|
||||
|
|
@ -30,8 +58,19 @@ defmodule Pleroma.HTTP do
|
|||
end
|
||||
end
|
||||
|
||||
def get(url, headers \\ [], options \\ []), do: request(:get, url, "", headers, options)
|
||||
@doc """
|
||||
Performs GET request.
|
||||
|
||||
See `Pleroma.HTTP.request/5`
|
||||
"""
|
||||
def get(url, headers \\ [], options \\ []),
|
||||
do: request(:get, url, "", headers, options)
|
||||
|
||||
@doc """
|
||||
Performs POST request.
|
||||
|
||||
See `Pleroma.HTTP.request/5`
|
||||
"""
|
||||
def post(url, body, headers \\ [], options \\ []),
|
||||
do: request(:post, url, body, headers, options)
|
||||
end
|
||||
|
|
|
|||
126
lib/pleroma/http/request_builder.ex
Normal file
126
lib/pleroma/http/request_builder.ex
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
defmodule Pleroma.HTTP.RequestBuilder do
|
||||
@moduledoc """
|
||||
Helper functions for building Tesla requests
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Specify the request method when building a request
|
||||
|
||||
## Parameters
|
||||
|
||||
- request (Map) - Collected request options
|
||||
- m (atom) - Request method
|
||||
|
||||
## Returns
|
||||
|
||||
Map
|
||||
"""
|
||||
@spec method(map(), atom) :: map()
|
||||
def method(request, m) do
|
||||
Map.put_new(request, :method, m)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Specify the request method when building a request
|
||||
|
||||
## Parameters
|
||||
|
||||
- request (Map) - Collected request options
|
||||
- u (String) - Request URL
|
||||
|
||||
## Returns
|
||||
|
||||
Map
|
||||
"""
|
||||
@spec url(map(), String.t()) :: map()
|
||||
def url(request, u) do
|
||||
Map.put_new(request, :url, u)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Add headers to the request
|
||||
"""
|
||||
@spec headers(map(), list(tuple)) :: map()
|
||||
def headers(request, h) do
|
||||
Map.put_new(request, :headers, h)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Add custom, per-request middleware or adapter options to the request
|
||||
"""
|
||||
@spec opts(map(), Keyword.t()) :: map()
|
||||
def opts(request, options) do
|
||||
Map.put_new(request, :opts, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Add optional parameters to the request
|
||||
|
||||
## Parameters
|
||||
|
||||
- request (Map) - Collected request options
|
||||
- definitions (Map) - Map of parameter name to parameter location.
|
||||
- options (KeywordList) - The provided optional parameters
|
||||
|
||||
## Returns
|
||||
|
||||
Map
|
||||
"""
|
||||
@spec add_optional_params(map(), %{optional(atom) => atom}, keyword()) :: map()
|
||||
def add_optional_params(request, _, []), do: request
|
||||
|
||||
def add_optional_params(request, definitions, [{key, value} | tail]) do
|
||||
case definitions do
|
||||
%{^key => location} ->
|
||||
request
|
||||
|> add_param(location, key, value)
|
||||
|> add_optional_params(definitions, tail)
|
||||
|
||||
_ ->
|
||||
add_optional_params(request, definitions, tail)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Add optional parameters to the request
|
||||
|
||||
## Parameters
|
||||
|
||||
- request (Map) - Collected request options
|
||||
- location (atom) - Where to put the parameter
|
||||
- key (atom) - The name of the parameter
|
||||
- value (any) - The value of the parameter
|
||||
|
||||
## Returns
|
||||
|
||||
Map
|
||||
"""
|
||||
@spec add_param(map(), atom, atom, any()) :: map()
|
||||
def add_param(request, :body, :body, value), do: Map.put(request, :body, value)
|
||||
|
||||
def add_param(request, :body, key, value) do
|
||||
request
|
||||
|> Map.put_new_lazy(:body, &Tesla.Multipart.new/0)
|
||||
|> Map.update!(
|
||||
:body,
|
||||
&Tesla.Multipart.add_field(&1, key, Poison.encode!(value),
|
||||
headers: [{:"Content-Type", "application/json"}]
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def add_param(request, :file, name, path) do
|
||||
request
|
||||
|> Map.put_new_lazy(:body, &Tesla.Multipart.new/0)
|
||||
|> Map.update!(:body, &Tesla.Multipart.add_file(&1, path, name: name))
|
||||
end
|
||||
|
||||
def add_param(request, :form, name, value) do
|
||||
request
|
||||
|> Map.update(:body, %{name => value}, &Map.put(&1, name, value))
|
||||
end
|
||||
|
||||
def add_param(request, location, key, value) do
|
||||
Map.update(request, location, [{key, value}], &(&1 ++ [{key, value}]))
|
||||
end
|
||||
end
|
||||
|
|
@ -23,7 +23,7 @@ defmodule Pleroma.List do
|
|||
|> validate_required([:following])
|
||||
end
|
||||
|
||||
def for_user(user, opts) do
|
||||
def for_user(user, _opts) do
|
||||
query =
|
||||
from(
|
||||
l in Pleroma.List,
|
||||
|
|
@ -46,7 +46,7 @@ defmodule Pleroma.List do
|
|||
Repo.one(query)
|
||||
end
|
||||
|
||||
def get_following(%Pleroma.List{following: following} = list) do
|
||||
def get_following(%Pleroma.List{following: following} = _list) do
|
||||
q =
|
||||
from(
|
||||
u in User,
|
||||
|
|
|
|||
108
lib/pleroma/mime.ex
Normal file
108
lib/pleroma/mime.ex
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
defmodule Pleroma.MIME do
|
||||
@moduledoc """
|
||||
Returns the mime-type of a binary and optionally a normalized file-name.
|
||||
"""
|
||||
@default "application/octet-stream"
|
||||
@read_bytes 35
|
||||
|
||||
@spec file_mime_type(String.t()) ::
|
||||
{:ok, content_type :: String.t(), filename :: String.t()} | {:error, any()} | :error
|
||||
def file_mime_type(path, filename) do
|
||||
with {:ok, content_type} <- file_mime_type(path),
|
||||
filename <- fix_extension(filename, content_type) do
|
||||
{:ok, content_type, filename}
|
||||
end
|
||||
end
|
||||
|
||||
@spec file_mime_type(String.t()) :: {:ok, String.t()} | {:error, any()} | :error
|
||||
def file_mime_type(filename) do
|
||||
File.open(filename, [:read], fn f ->
|
||||
check_mime_type(IO.binread(f, @read_bytes))
|
||||
end)
|
||||
end
|
||||
|
||||
def bin_mime_type(binary, filename) do
|
||||
with {:ok, content_type} <- bin_mime_type(binary),
|
||||
filename <- fix_extension(filename, content_type) do
|
||||
{:ok, content_type, filename}
|
||||
end
|
||||
end
|
||||
|
||||
@spec bin_mime_type(binary()) :: {:ok, String.t()} | :error
|
||||
def bin_mime_type(<<head::binary-size(@read_bytes), _::binary>>) do
|
||||
{:ok, check_mime_type(head)}
|
||||
end
|
||||
|
||||
def bin_mime_type(_), do: :error
|
||||
|
||||
def mime_type(<<_::binary>>), do: {:ok, @default}
|
||||
|
||||
defp fix_extension(filename, content_type) do
|
||||
parts = String.split(filename, ".")
|
||||
|
||||
new_filename =
|
||||
if length(parts) > 1 do
|
||||
Enum.drop(parts, -1) |> Enum.join(".")
|
||||
else
|
||||
Enum.join(parts)
|
||||
end
|
||||
|
||||
cond do
|
||||
content_type == "application/octet-stream" ->
|
||||
filename
|
||||
|
||||
ext = List.first(MIME.extensions(content_type)) ->
|
||||
new_filename <> "." <> ext
|
||||
|
||||
true ->
|
||||
Enum.join([new_filename, String.split(content_type, "/") |> List.last()], ".")
|
||||
end
|
||||
end
|
||||
|
||||
defp check_mime_type(<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, _::binary>>) do
|
||||
"image/png"
|
||||
end
|
||||
|
||||
defp check_mime_type(<<0x47, 0x49, 0x46, 0x38, _, 0x61, _::binary>>) do
|
||||
"image/gif"
|
||||
end
|
||||
|
||||
defp check_mime_type(<<0xFF, 0xD8, 0xFF, _::binary>>) do
|
||||
"image/jpeg"
|
||||
end
|
||||
|
||||
defp check_mime_type(<<0x1A, 0x45, 0xDF, 0xA3, _::binary>>) do
|
||||
"video/webm"
|
||||
end
|
||||
|
||||
defp check_mime_type(<<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70, _::binary>>) do
|
||||
"video/mp4"
|
||||
end
|
||||
|
||||
defp check_mime_type(<<0x49, 0x44, 0x33, _::binary>>) do
|
||||
"audio/mpeg"
|
||||
end
|
||||
|
||||
defp check_mime_type(<<255, 251, _, 68, 0, 0, 0, 0, _::binary>>) do
|
||||
"audio/mpeg"
|
||||
end
|
||||
|
||||
defp check_mime_type(
|
||||
<<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, _::size(160), 0x80, 0x74, 0x68, 0x65,
|
||||
0x6F, 0x72, 0x61, _::binary>>
|
||||
) do
|
||||
"video/ogg"
|
||||
end
|
||||
|
||||
defp check_mime_type(<<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, _::binary>>) do
|
||||
"audio/ogg"
|
||||
end
|
||||
|
||||
defp check_mime_type(<<0x52, 0x49, 0x46, 0x46, _::binary>>) do
|
||||
"audio/wav"
|
||||
end
|
||||
|
||||
defp check_mime_type(_) do
|
||||
@default
|
||||
end
|
||||
end
|
||||
|
|
@ -110,6 +110,7 @@ defmodule Pleroma.Notification 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)
|
||||
notification
|
||||
end
|
||||
end
|
||||
|
|
@ -117,7 +118,7 @@ defmodule Pleroma.Notification do
|
|||
def get_notified_from_activity(activity, local_only \\ true)
|
||||
|
||||
def get_notified_from_activity(
|
||||
%Activity{data: %{"to" => _, "type" => type} = data} = activity,
|
||||
%Activity{data: %{"to" => _, "type" => type} = _data} = activity,
|
||||
local_only
|
||||
)
|
||||
when type in ["Create", "Like", "Announce", "Follow"] do
|
||||
|
|
@ -130,18 +131,18 @@ defmodule Pleroma.Notification do
|
|||
User.get_users_from_set(recipients, local_only)
|
||||
end
|
||||
|
||||
def get_notified_from_activity(_, local_only), do: []
|
||||
def get_notified_from_activity(_, _local_only), do: []
|
||||
|
||||
defp maybe_notify_to_recipients(
|
||||
recipients,
|
||||
%Activity{data: %{"to" => to, "type" => type}} = activity
|
||||
%Activity{data: %{"to" => to, "type" => _type}} = _activity
|
||||
) do
|
||||
recipients ++ to
|
||||
end
|
||||
|
||||
defp maybe_notify_mentioned_recipients(
|
||||
recipients,
|
||||
%Activity{data: %{"to" => to, "type" => type} = data} = activity
|
||||
%Activity{data: %{"to" => _to, "type" => type} = data} = _activity
|
||||
)
|
||||
when type == "Create" do
|
||||
object = Object.normalize(data["object"])
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
defmodule Pleroma.Object do
|
||||
use Ecto.Schema
|
||||
alias Pleroma.{Repo, Object, Activity}
|
||||
alias Pleroma.{Repo, Object, User, Activity}
|
||||
import Ecto.{Query, Changeset}
|
||||
|
||||
schema "objects" do
|
||||
|
|
@ -31,6 +31,13 @@ defmodule Pleroma.Object do
|
|||
def normalize(ap_id) when is_binary(ap_id), do: Object.get_by_ap_id(ap_id)
|
||||
def normalize(_), do: nil
|
||||
|
||||
# Owned objects can only be mutated by their owner
|
||||
def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),
|
||||
do: actor == ap_id
|
||||
|
||||
# Legacy objects can be mutated by anybody
|
||||
def authorize_mutation(%Object{}, %User{}), do: true
|
||||
|
||||
if Mix.env() == :test do
|
||||
def get_cached_by_ap_id(ap_id) do
|
||||
get_by_ap_id(ap_id)
|
||||
|
|
|
|||
|
|
@ -26,14 +26,7 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
|
|||
end
|
||||
end
|
||||
|
||||
def call(
|
||||
%{
|
||||
assigns: %{
|
||||
auth_credentials: %{password: password}
|
||||
}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
def call(%{assigns: %{auth_credentials: %{password: _}}} = conn, _) do
|
||||
Pbkdf2.dummy_checkpw()
|
||||
conn
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ defmodule Pleroma.Plugs.BasicAuthDecoderPlug do
|
|||
options
|
||||
end
|
||||
|
||||
def call(conn, opts) do
|
||||
def call(conn, _opts) do
|
||||
with ["Basic " <> header] <- get_req_header(conn, "authorization"),
|
||||
{:ok, userinfo} <- Base.decode64(header),
|
||||
[username, password] <- String.split(userinfo, ":", parts: 2) do
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ defmodule Pleroma.Web.FederatingPlug do
|
|||
options
|
||||
end
|
||||
|
||||
def call(conn, opts) do
|
||||
def call(conn, _opts) do
|
||||
if Keyword.get(Application.get_env(:pleroma, :instance), :federating) do
|
||||
conn
|
||||
else
|
||||
conn
|
||||
|> put_status(404)
|
||||
|> Phoenix.Controller.render(Pleroma.Web.ErrorView, "404.json")
|
||||
|> Phoenix.Controller.put_view(Pleroma.Web.ErrorView)
|
||||
|> Phoenix.Controller.render("404.json")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
|
|||
|
||||
def init(opts), do: opts
|
||||
|
||||
def call(conn, options) do
|
||||
def call(conn, _options) do
|
||||
if Config.get([:http_security, :enabled]) do
|
||||
conn =
|
||||
merge_resp_headers(conn, headers())
|
||||
|> maybe_send_sts_header(Config.get([:http_security, :sts]))
|
||||
conn
|
||||
|> merge_resp_headers(headers())
|
||||
|> maybe_send_sts_header(Config.get([:http_security, :sts]))
|
||||
else
|
||||
conn
|
||||
end
|
||||
|
|
@ -29,6 +29,8 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
|
|||
end
|
||||
|
||||
defp csp_string do
|
||||
protocol = Config.get([Pleroma.Web.Endpoint, :protocol])
|
||||
|
||||
[
|
||||
"default-src 'none'",
|
||||
"base-uri 'self'",
|
||||
|
|
@ -39,7 +41,10 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
|
|||
"font-src 'self'",
|
||||
"script-src 'self'",
|
||||
"connect-src 'self' " <> String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
|
||||
"upgrade-insecure-requests"
|
||||
"manifest-src 'self'",
|
||||
if protocol == "https" do
|
||||
"upgrade-insecure-requests"
|
||||
end
|
||||
]
|
||||
|> Enum.join("; ")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,30 +1,70 @@
|
|||
defmodule Pleroma.Plugs.OAuthPlug do
|
||||
import Plug.Conn
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
import Ecto.Query
|
||||
|
||||
def init(options) do
|
||||
options
|
||||
end
|
||||
alias Pleroma.{
|
||||
User,
|
||||
Repo,
|
||||
Web.OAuth.Token
|
||||
}
|
||||
|
||||
@realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i")
|
||||
|
||||
def init(options), do: options
|
||||
|
||||
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
|
||||
|
||||
def call(conn, _) do
|
||||
token =
|
||||
case get_req_header(conn, "authorization") do
|
||||
["Bearer " <> header] -> header
|
||||
_ -> get_session(conn, :oauth_token)
|
||||
end
|
||||
|
||||
with token when not is_nil(token) <- token,
|
||||
%Token{user_id: user_id} <- Repo.get_by(Token, token: token),
|
||||
%User{} = user <- Repo.get(User, user_id),
|
||||
false <- !!user.info["deactivated"] do
|
||||
with {:ok, token_str} <- fetch_token_str(conn),
|
||||
{:ok, user, token_record} <- fetch_user_and_token(token_str) do
|
||||
conn
|
||||
|> assign(:token, token_record)
|
||||
|> assign(:user, user)
|
||||
else
|
||||
_ -> conn
|
||||
end
|
||||
end
|
||||
|
||||
# Gets user by token
|
||||
#
|
||||
@spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil
|
||||
defp fetch_user_and_token(token) do
|
||||
query = from(q in Token, where: q.token == ^token, preload: [:user])
|
||||
|
||||
with %Token{user: %{info: %{deactivated: false} = _} = user} = token_record <- Repo.one(query) do
|
||||
{:ok, user, token_record}
|
||||
end
|
||||
end
|
||||
|
||||
# Gets token from session by :oauth_token key
|
||||
#
|
||||
@spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
|
||||
defp fetch_token_from_session(conn) do
|
||||
case get_session(conn, :oauth_token) do
|
||||
nil -> :no_token_found
|
||||
token -> {:ok, token}
|
||||
end
|
||||
end
|
||||
|
||||
# Gets token from headers
|
||||
#
|
||||
@spec fetch_token_str(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
|
||||
defp fetch_token_str(%Plug.Conn{} = conn) do
|
||||
headers = get_req_header(conn, "authorization")
|
||||
|
||||
with :no_token_found <- fetch_token_str(headers),
|
||||
do: fetch_token_from_session(conn)
|
||||
end
|
||||
|
||||
@spec fetch_token_str(Keyword.t()) :: :no_token_found | {:ok, String.t()}
|
||||
defp fetch_token_str([]), do: :no_token_found
|
||||
|
||||
defp fetch_token_str([token | tail]) do
|
||||
trimmed_token = String.trim(token)
|
||||
|
||||
case Regex.run(@realm_reg, trimmed_token) do
|
||||
[_, match] -> {:ok, String.trim(match)}
|
||||
_ -> fetch_token_str(tail)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
defmodule Pleroma.Plugs.SessionAuthenticationPlug do
|
||||
import Plug.Conn
|
||||
alias Pleroma.User
|
||||
|
||||
def init(options) do
|
||||
options
|
||||
|
|
|
|||
74
lib/pleroma/plugs/uploaded_media.ex
Normal file
74
lib/pleroma/plugs/uploaded_media.ex
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
defmodule Pleroma.Plugs.UploadedMedia do
|
||||
@moduledoc """
|
||||
"""
|
||||
|
||||
import Plug.Conn
|
||||
require Logger
|
||||
|
||||
@behaviour Plug
|
||||
# no slashes
|
||||
@path "media"
|
||||
|
||||
def init(_opts) do
|
||||
static_plug_opts =
|
||||
[]
|
||||
|> Keyword.put(:from, "__unconfigured_media_plug")
|
||||
|> Keyword.put(:at, "/__unconfigured_media_plug")
|
||||
|> Plug.Static.init()
|
||||
|
||||
%{static_plug_opts: static_plug_opts}
|
||||
end
|
||||
|
||||
def call(conn = %{request_path: <<"/", @path, "/", file::binary>>}, opts) do
|
||||
config = Pleroma.Config.get([Pleroma.Upload])
|
||||
|
||||
with uploader <- Keyword.fetch!(config, :uploader),
|
||||
proxy_remote = Keyword.get(config, :proxy_remote, false),
|
||||
{:ok, get_method} <- uploader.get_file(file) do
|
||||
get_media(conn, get_method, proxy_remote, opts)
|
||||
else
|
||||
_ ->
|
||||
conn
|
||||
|> send_resp(500, "Failed")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
||||
def call(conn, _opts), do: conn
|
||||
|
||||
defp get_media(conn, {:static_dir, directory}, _, opts) do
|
||||
static_opts =
|
||||
Map.get(opts, :static_plug_opts)
|
||||
|> Map.put(:at, [@path])
|
||||
|> Map.put(:from, directory)
|
||||
|
||||
conn = Plug.Static.call(conn, static_opts)
|
||||
|
||||
if conn.halted do
|
||||
conn
|
||||
else
|
||||
conn
|
||||
|> send_resp(404, "Not found")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
||||
defp get_media(conn, {:url, url}, true, _) do
|
||||
conn
|
||||
|> Pleroma.ReverseProxy.call(url, Pleroma.Config.get([Pleroma.Upload, :proxy_opts], []))
|
||||
end
|
||||
|
||||
defp get_media(conn, {:url, url}, _, _) do
|
||||
conn
|
||||
|> Phoenix.Controller.redirect(external: url)
|
||||
|> halt()
|
||||
end
|
||||
|
||||
defp get_media(conn, unknown, _, _) do
|
||||
Logger.error("#{__MODULE__}: Unknown get startegy: #{inspect(unknown)}")
|
||||
|
||||
conn
|
||||
|> send_resp(500, "Internal Error")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
|
@ -6,7 +6,7 @@ defmodule Pleroma.Plugs.UserEnabledPlug do
|
|||
options
|
||||
end
|
||||
|
||||
def call(%{assigns: %{user: %User{info: %{"deactivated" => true}}}} = conn, _) do
|
||||
def call(%{assigns: %{user: %User{info: %{deactivated: true}}}} = conn, _) do
|
||||
conn
|
||||
|> assign(:user, nil)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ defmodule Pleroma.Plugs.UserFetcherPlug do
|
|||
options
|
||||
end
|
||||
|
||||
def call(conn, options) do
|
||||
def call(conn, _options) do
|
||||
with %{auth_credentials: %{username: username}} <- conn.assigns,
|
||||
{:ok, %User{} = user} <- user_fetcher(username) do
|
||||
conn
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ defmodule Pleroma.Plugs.UserIsAdminPlug do
|
|||
options
|
||||
end
|
||||
|
||||
def call(%{assigns: %{user: %User{info: %{"is_admin" => true}}}} = conn, _) do
|
||||
def call(%{assigns: %{user: %User{info: %{is_admin: true}}}} = conn, _) do
|
||||
conn
|
||||
end
|
||||
|
||||
|
|
|
|||
344
lib/pleroma/reverse_proxy.ex
Normal file
344
lib/pleroma/reverse_proxy.ex
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
defmodule Pleroma.ReverseProxy do
|
||||
@keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since if-unmodified-since if-none-match if-range range)
|
||||
@resp_cache_headers ~w(etag date last-modified cache-control)
|
||||
@keep_resp_headers @resp_cache_headers ++
|
||||
~w(content-type content-disposition content-encoding content-range accept-ranges vary)
|
||||
@default_cache_control_header "public, max-age=1209600"
|
||||
@valid_resp_codes [200, 206, 304]
|
||||
@max_read_duration :timer.seconds(30)
|
||||
@max_body_length :infinity
|
||||
@methods ~w(GET HEAD)
|
||||
|
||||
@moduledoc """
|
||||
A reverse proxy.
|
||||
|
||||
Pleroma.ReverseProxy.call(conn, url, options)
|
||||
|
||||
It is not meant to be added into a plug pipeline, but to be called from another plug or controller.
|
||||
|
||||
Supports `#{inspect(@methods)}` HTTP methods, and only allows `#{inspect(@valid_resp_codes)}` status codes.
|
||||
|
||||
Responses are chunked to the client while downloading from the upstream.
|
||||
|
||||
Some request / responses headers are preserved:
|
||||
|
||||
* request: `#{inspect(@keep_req_headers)}`
|
||||
* response: `#{inspect(@keep_resp_headers)}`
|
||||
|
||||
If no caching headers (`#{inspect(@resp_cache_headers)}`) are returned by upstream, `cache-control` will be
|
||||
set to `#{inspect(@default_cache_control_header)}`.
|
||||
|
||||
Options:
|
||||
|
||||
* `redirect_on_failure` (default `false`). Redirects the client to the real remote URL if there's any HTTP
|
||||
errors. Any error during body processing will not be redirected as the response is chunked. This may expose
|
||||
remote URL, clients IPs, ….
|
||||
|
||||
* `max_body_length` (default `#{inspect(@max_body_length)}`): limits the content length to be approximately the
|
||||
specified length. It is validated with the `content-length` header and also verified when proxying.
|
||||
|
||||
* `max_read_duration` (default `#{inspect(@max_read_duration)}` ms): the total time the connection is allowed to
|
||||
read from the remote upstream.
|
||||
|
||||
* `inline_content_types`:
|
||||
* `true` will not alter `content-disposition` (up to the upstream),
|
||||
* `false` will add `content-disposition: attachment` to any request,
|
||||
* a list of whitelisted content types
|
||||
|
||||
* `keep_user_agent` will forward the client's user-agent to the upstream. This may be useful if the upstream is
|
||||
doing content transformation (encoding, …) depending on the request.
|
||||
|
||||
* `req_headers`, `resp_headers` additional headers.
|
||||
|
||||
* `http`: options for [hackney](https://github.com/benoitc/hackney).
|
||||
|
||||
"""
|
||||
@hackney Application.get_env(:pleroma, :hackney, :hackney)
|
||||
@httpoison Application.get_env(:pleroma, :httpoison, HTTPoison)
|
||||
|
||||
@default_hackney_options []
|
||||
|
||||
@inline_content_types [
|
||||
"image/gif",
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/png",
|
||||
"image/svg+xml",
|
||||
"audio/mpeg",
|
||||
"audio/mp3",
|
||||
"video/webm",
|
||||
"video/mp4",
|
||||
"video/quicktime"
|
||||
]
|
||||
|
||||
require Logger
|
||||
import Plug.Conn
|
||||
|
||||
@type option() ::
|
||||
{:keep_user_agent, boolean}
|
||||
| {:max_read_duration, :timer.time() | :infinity}
|
||||
| {:max_body_length, non_neg_integer() | :infinity}
|
||||
| {:http, []}
|
||||
| {:req_headers, [{String.t(), String.t()}]}
|
||||
| {:resp_headers, [{String.t(), String.t()}]}
|
||||
| {:inline_content_types, boolean() | [String.t()]}
|
||||
| {:redirect_on_failure, boolean()}
|
||||
|
||||
@spec call(Plug.Conn.t(), url :: String.t(), [option()]) :: Plug.Conn.t()
|
||||
def call(_conn, _url, _opts \\ [])
|
||||
|
||||
def call(conn = %{method: method}, url, opts) when method in @methods do
|
||||
hackney_opts =
|
||||
@default_hackney_options
|
||||
|> Keyword.merge(Keyword.get(opts, :http, []))
|
||||
|> @httpoison.process_request_options()
|
||||
|
||||
req_headers = build_req_headers(conn.req_headers, opts)
|
||||
|
||||
opts =
|
||||
if filename = Pleroma.Web.MediaProxy.filename(url) do
|
||||
Keyword.put_new(opts, :attachment_name, filename)
|
||||
else
|
||||
opts
|
||||
end
|
||||
|
||||
with {:ok, code, headers, client} <- request(method, url, req_headers, hackney_opts),
|
||||
:ok <- header_length_constraint(headers, Keyword.get(opts, :max_body_length)) do
|
||||
response(conn, client, url, code, headers, opts)
|
||||
else
|
||||
{:ok, code, headers} ->
|
||||
head_response(conn, url, code, headers, opts)
|
||||
|> halt()
|
||||
|
||||
{:error, {:invalid_http_response, code}} ->
|
||||
Logger.error("#{__MODULE__}: request to #{inspect(url)} failed with HTTP status #{code}")
|
||||
|
||||
conn
|
||||
|> error_or_redirect(
|
||||
url,
|
||||
code,
|
||||
"Request failed: " <> Plug.Conn.Status.reason_phrase(code),
|
||||
opts
|
||||
)
|
||||
|> halt()
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("#{__MODULE__}: request to #{inspect(url)} failed: #{inspect(error)}")
|
||||
|
||||
conn
|
||||
|> error_or_redirect(url, 500, "Request failed", opts)
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
||||
def call(conn, _, _) do
|
||||
conn
|
||||
|> send_resp(400, Plug.Conn.Status.reason_phrase(400))
|
||||
|> halt()
|
||||
end
|
||||
|
||||
defp request(method, url, headers, hackney_opts) 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
|
||||
{:ok, code, headers, client} when code in @valid_resp_codes ->
|
||||
{:ok, code, downcase_headers(headers), client}
|
||||
|
||||
{:ok, code, headers} when code in @valid_resp_codes ->
|
||||
{:ok, code, downcase_headers(headers)}
|
||||
|
||||
{:ok, code, _, _} ->
|
||||
{:error, {:invalid_http_response, code}}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
defp response(conn, client, url, status, headers, opts) do
|
||||
result =
|
||||
conn
|
||||
|> put_resp_headers(build_resp_headers(headers, opts))
|
||||
|> send_chunked(status)
|
||||
|> chunk_reply(client, opts)
|
||||
|
||||
case result do
|
||||
{:ok, conn} ->
|
||||
halt(conn)
|
||||
|
||||
{:error, :closed, conn} ->
|
||||
:hackney.close(client)
|
||||
halt(conn)
|
||||
|
||||
{:error, error, conn} ->
|
||||
Logger.warn(
|
||||
"#{__MODULE__} request to #{url} failed while reading/chunking: #{inspect(error)}"
|
||||
)
|
||||
|
||||
:hackney.close(client)
|
||||
halt(conn)
|
||||
end
|
||||
end
|
||||
|
||||
defp chunk_reply(conn, client, opts) do
|
||||
chunk_reply(conn, client, opts, 0, 0)
|
||||
end
|
||||
|
||||
defp chunk_reply(conn, client, opts, sent_so_far, duration) do
|
||||
with {:ok, duration} <-
|
||||
check_read_duration(
|
||||
duration,
|
||||
Keyword.get(opts, :max_read_duration, @max_read_duration)
|
||||
),
|
||||
{:ok, data} <- @hackney.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)),
|
||||
{:ok, conn} <- chunk(conn, data) do
|
||||
chunk_reply(conn, client, opts, sent_so_far, duration)
|
||||
else
|
||||
:done -> {:ok, conn}
|
||||
{:error, error} -> {:error, error, conn}
|
||||
end
|
||||
end
|
||||
|
||||
defp head_response(conn, _url, code, headers, opts) do
|
||||
conn
|
||||
|> put_resp_headers(build_resp_headers(headers, opts))
|
||||
|> send_resp(code, "")
|
||||
end
|
||||
|
||||
defp error_or_redirect(conn, url, code, body, opts) do
|
||||
if Keyword.get(opts, :redirect_on_failure, false) do
|
||||
conn
|
||||
|> Phoenix.Controller.redirect(external: url)
|
||||
|> halt()
|
||||
else
|
||||
conn
|
||||
|> send_resp(code, body)
|
||||
|> halt
|
||||
end
|
||||
end
|
||||
|
||||
defp downcase_headers(headers) do
|
||||
Enum.map(headers, fn {k, v} ->
|
||||
{String.downcase(k), v}
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_content_type(headers) do
|
||||
{_, content_type} =
|
||||
List.keyfind(headers, "content-type", 0, {"content-type", "application/octet-stream"})
|
||||
|
||||
[content_type | _] = String.split(content_type, ";")
|
||||
content_type
|
||||
end
|
||||
|
||||
defp put_resp_headers(conn, headers) do
|
||||
Enum.reduce(headers, conn, fn {k, v}, conn ->
|
||||
put_resp_header(conn, k, v)
|
||||
end)
|
||||
end
|
||||
|
||||
defp build_req_headers(headers, opts) do
|
||||
headers
|
||||
|> downcase_headers()
|
||||
|> Enum.filter(fn {k, _} -> k in @keep_req_headers end)
|
||||
|> (fn headers ->
|
||||
headers = headers ++ Keyword.get(opts, :req_headers, [])
|
||||
|
||||
if Keyword.get(opts, :keep_user_agent, false) do
|
||||
List.keystore(
|
||||
headers,
|
||||
"user-agent",
|
||||
0,
|
||||
{"user-agent", Pleroma.Application.user_agent()}
|
||||
)
|
||||
else
|
||||
headers
|
||||
end
|
||||
end).()
|
||||
end
|
||||
|
||||
defp build_resp_headers(headers, opts) do
|
||||
headers
|
||||
|> Enum.filter(fn {k, _} -> k in @keep_resp_headers end)
|
||||
|> build_resp_cache_headers(opts)
|
||||
|> build_resp_content_disposition_header(opts)
|
||||
|> (fn headers -> headers ++ Keyword.get(opts, :resp_headers, []) end).()
|
||||
end
|
||||
|
||||
defp build_resp_cache_headers(headers, _opts) do
|
||||
has_cache? = Enum.any?(headers, fn {k, _} -> k in @resp_cache_headers end)
|
||||
|
||||
if has_cache? do
|
||||
headers
|
||||
else
|
||||
List.keystore(headers, "cache-control", 0, {"cache-control", @default_cache_control_header})
|
||||
end
|
||||
end
|
||||
|
||||
defp build_resp_content_disposition_header(headers, opts) do
|
||||
opt = Keyword.get(opts, :inline_content_types, @inline_content_types)
|
||||
|
||||
content_type = get_content_type(headers)
|
||||
|
||||
attachment? =
|
||||
cond do
|
||||
is_list(opt) && !Enum.member?(opt, content_type) -> true
|
||||
opt == false -> true
|
||||
true -> false
|
||||
end
|
||||
|
||||
if attachment? do
|
||||
disposition = "attachment; filename=" <> Keyword.get(opts, :attachment_name, "attachment")
|
||||
List.keystore(headers, "content-disposition", 0, {"content-disposition", disposition})
|
||||
else
|
||||
headers
|
||||
end
|
||||
end
|
||||
|
||||
defp header_length_constraint(headers, limit) when is_integer(limit) and limit > 0 do
|
||||
with {_, size} <- List.keyfind(headers, "content-length", 0),
|
||||
{size, _} <- Integer.parse(size),
|
||||
true <- size <= limit do
|
||||
:ok
|
||||
else
|
||||
false ->
|
||||
{:error, :body_too_large}
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp header_length_constraint(_, _), do: :ok
|
||||
|
||||
defp body_size_constraint(size, limit) when is_integer(limit) and limit > 0 and size >= limit do
|
||||
{:error, :body_too_large}
|
||||
end
|
||||
|
||||
defp body_size_constraint(_, _), do: :ok
|
||||
|
||||
defp check_read_duration(duration, max)
|
||||
when is_integer(duration) and is_integer(max) and max > 0 do
|
||||
if duration > max do
|
||||
{:error, :read_duration_exceeded}
|
||||
else
|
||||
{:ok, {duration, :erlang.system_time(:millisecond)}}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_read_duration(_, _), do: {:ok, :no_duration_limit, :no_duration_limit}
|
||||
|
||||
defp increase_read_duration({previous_duration, started})
|
||||
when is_integer(previous_duration) and is_integer(started) do
|
||||
duration = :erlang.system_time(:millisecond) - started
|
||||
{:ok, previous_duration + duration}
|
||||
end
|
||||
|
||||
defp increase_read_duration(_) do
|
||||
{:ok, :no_duration_limit, :no_duration_limit}
|
||||
end
|
||||
end
|
||||
|
|
@ -1,81 +1,208 @@
|
|||
defmodule Pleroma.Upload do
|
||||
@moduledoc """
|
||||
# Upload
|
||||
|
||||
Options:
|
||||
* `:type`: presets for activity type (defaults to Document) and size limits from app configuration
|
||||
* `:description`: upload alternative text
|
||||
* `:base_url`: override base url
|
||||
* `:uploader`: override uploader
|
||||
* `:filters`: override filters
|
||||
* `:size_limit`: override size limit
|
||||
* `:activity_type`: override activity type
|
||||
|
||||
The `%Pleroma.Upload{}` struct: all documented fields are meant to be overwritten in filters:
|
||||
|
||||
* `:id` - the upload id.
|
||||
* `:name` - the upload file name.
|
||||
* `:path` - the upload path: set at first to `id/name` but can be changed. Keep in mind that the path
|
||||
is once created permanent and changing it (especially in uploaders) is probably a bad idea!
|
||||
* `:tempfile` - path to the temporary file. Prefer in-place changes on the file rather than changing the
|
||||
path as the temporary file is also tracked by `Plug.Upload{}` and automatically deleted once the request is over.
|
||||
|
||||
Related behaviors:
|
||||
|
||||
* `Pleroma.Uploaders.Uploader`
|
||||
* `Pleroma.Upload.Filter`
|
||||
|
||||
"""
|
||||
alias Ecto.UUID
|
||||
require Logger
|
||||
|
||||
def check_file_size(path, nil), do: true
|
||||
@type source ::
|
||||
Plug.Upload.t() | data_uri_string ::
|
||||
String.t() | {:from_local, name :: String.t(), id :: String.t(), path :: String.t()}
|
||||
|
||||
def check_file_size(path, size_limit) do
|
||||
{:ok, %{size: size}} = File.stat(path)
|
||||
size <= size_limit
|
||||
end
|
||||
@type option ::
|
||||
{:type, :avatar | :banner | :background}
|
||||
| {:description, String.t()}
|
||||
| {:activity_type, String.t()}
|
||||
| {:size_limit, nil | non_neg_integer()}
|
||||
| {:uploader, module()}
|
||||
| {:filters, [module()]}
|
||||
|
||||
def store(file, should_dedupe, size_limit \\ nil)
|
||||
@type t :: %__MODULE__{
|
||||
id: String.t(),
|
||||
name: String.t(),
|
||||
tempfile: String.t(),
|
||||
content_type: String.t(),
|
||||
path: String.t()
|
||||
}
|
||||
defstruct [:id, :name, :tempfile, :content_type, :path]
|
||||
|
||||
def store(%Plug.Upload{} = file, should_dedupe, size_limit) do
|
||||
content_type = get_content_type(file.path)
|
||||
@spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()}
|
||||
def store(upload, opts \\ []) do
|
||||
opts = get_opts(opts)
|
||||
|
||||
with uuid <- get_uuid(file, should_dedupe),
|
||||
name <- get_name(file, uuid, content_type, should_dedupe),
|
||||
true <- check_file_size(file.path, size_limit) do
|
||||
strip_exif_data(content_type, file.path)
|
||||
|
||||
{:ok, url_path} = uploader().put_file(name, uuid, file.path, content_type, should_dedupe)
|
||||
|
||||
%{
|
||||
"type" => "Document",
|
||||
"url" => [
|
||||
%{
|
||||
"type" => "Link",
|
||||
"mediaType" => content_type,
|
||||
"href" => url_path
|
||||
}
|
||||
],
|
||||
"name" => name
|
||||
}
|
||||
with {:ok, upload} <- prepare_upload(upload, opts),
|
||||
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
|
||||
{:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
|
||||
{:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do
|
||||
{:ok,
|
||||
%{
|
||||
"type" => opts.activity_type,
|
||||
"url" => [
|
||||
%{
|
||||
"type" => "Link",
|
||||
"mediaType" => upload.content_type,
|
||||
"href" => url_from_spec(opts.base_url, url_spec)
|
||||
}
|
||||
],
|
||||
"name" => Map.get(opts, :description) || upload.name
|
||||
}}
|
||||
else
|
||||
_e -> nil
|
||||
end
|
||||
end
|
||||
|
||||
def store(%{"img" => "data:image/" <> image_data}, should_dedupe, size_limit) do
|
||||
parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
|
||||
data = Base.decode64!(parsed["data"], ignore: :whitespace)
|
||||
|
||||
with tmp_path <- tempfile_for_image(data),
|
||||
uuid <- UUID.generate(),
|
||||
true <- check_file_size(tmp_path, size_limit) do
|
||||
content_type = get_content_type(tmp_path)
|
||||
strip_exif_data(content_type, tmp_path)
|
||||
|
||||
name =
|
||||
create_name(
|
||||
String.downcase(Base.encode16(:crypto.hash(:sha256, data))),
|
||||
parsed["filetype"],
|
||||
content_type
|
||||
{:error, error} ->
|
||||
Logger.error(
|
||||
"#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}"
|
||||
)
|
||||
|
||||
{:ok, url_path} = uploader().put_file(name, uuid, tmp_path, content_type, should_dedupe)
|
||||
|
||||
%{
|
||||
"type" => "Image",
|
||||
"url" => [
|
||||
%{
|
||||
"type" => "Link",
|
||||
"mediaType" => content_type,
|
||||
"href" => url_path
|
||||
}
|
||||
],
|
||||
"name" => name
|
||||
}
|
||||
else
|
||||
_e -> nil
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a tempfile using the Plug.Upload Genserver which cleans them up
|
||||
automatically.
|
||||
"""
|
||||
def tempfile_for_image(data) do
|
||||
defp get_opts(opts) do
|
||||
{size_limit, activity_type} =
|
||||
case Keyword.get(opts, :type) do
|
||||
:banner ->
|
||||
{Pleroma.Config.get!([:instance, :banner_upload_limit]), "Image"}
|
||||
|
||||
:avatar ->
|
||||
{Pleroma.Config.get!([:instance, :avatar_upload_limit]), "Image"}
|
||||
|
||||
:background ->
|
||||
{Pleroma.Config.get!([:instance, :background_upload_limit]), "Image"}
|
||||
|
||||
_ ->
|
||||
{Pleroma.Config.get!([:instance, :upload_limit]), "Document"}
|
||||
end
|
||||
|
||||
opts = %{
|
||||
activity_type: Keyword.get(opts, :activity_type, activity_type),
|
||||
size_limit: Keyword.get(opts, :size_limit, size_limit),
|
||||
uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader])),
|
||||
filters: Keyword.get(opts, :filters, Pleroma.Config.get([__MODULE__, :filters])),
|
||||
description: Keyword.get(opts, :description),
|
||||
base_url:
|
||||
Keyword.get(
|
||||
opts,
|
||||
:base_url,
|
||||
Pleroma.Config.get([__MODULE__, :base_url], Pleroma.Web.base_url())
|
||||
)
|
||||
}
|
||||
|
||||
# TODO: 1.0+ : remove old config compatibility
|
||||
opts =
|
||||
if Pleroma.Config.get([__MODULE__, :strip_exif]) == true &&
|
||||
!Enum.member?(opts.filters, Pleroma.Upload.Filter.Mogrify) do
|
||||
Logger.warn("""
|
||||
Pleroma: configuration `:instance, :strip_exif` is deprecated, please instead set:
|
||||
|
||||
:pleroma, Pleroma.Upload, [filters: [Pleroma.Upload.Filter.Mogrify]]
|
||||
|
||||
:pleroma, Pleroma.Upload.Filter.Mogrify, args: "strip"
|
||||
""")
|
||||
|
||||
Pleroma.Config.put([Pleroma.Upload.Filter.Mogrify], args: "strip")
|
||||
Map.put(opts, :filters, opts.filters ++ [Pleroma.Upload.Filter.Mogrify])
|
||||
else
|
||||
opts
|
||||
end
|
||||
|
||||
if Pleroma.Config.get([:instance, :dedupe_media]) == true &&
|
||||
!Enum.member?(opts.filters, Pleroma.Upload.Filter.Dedupe) do
|
||||
Logger.warn("""
|
||||
Pleroma: configuration `:instance, :dedupe_media` is deprecated, please instead set:
|
||||
|
||||
:pleroma, Pleroma.Upload, [filters: [Pleroma.Upload.Filter.Dedupe]]
|
||||
""")
|
||||
|
||||
Map.put(opts, :filters, opts.filters ++ [Pleroma.Upload.Filter.Dedupe])
|
||||
else
|
||||
opts
|
||||
end
|
||||
end
|
||||
|
||||
defp prepare_upload(%Plug.Upload{} = file, opts) do
|
||||
with :ok <- check_file_size(file.path, opts.size_limit),
|
||||
{:ok, content_type, name} <- Pleroma.MIME.file_mime_type(file.path, file.filename) do
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
id: UUID.generate(),
|
||||
name: name,
|
||||
tempfile: file.path,
|
||||
content_type: content_type
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
defp prepare_upload(%{"img" => "data:image/" <> image_data}, opts) do
|
||||
parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
|
||||
data = Base.decode64!(parsed["data"], ignore: :whitespace)
|
||||
hash = String.downcase(Base.encode16(:crypto.hash(:sha256, data)))
|
||||
|
||||
with :ok <- check_binary_size(data, opts.size_limit),
|
||||
tmp_path <- tempfile_for_image(data),
|
||||
{:ok, content_type, name} <-
|
||||
Pleroma.MIME.bin_mime_type(data, hash <> "." <> parsed["filetype"]) do
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
id: UUID.generate(),
|
||||
name: name,
|
||||
tempfile: tmp_path,
|
||||
content_type: content_type
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
# For Mix.Tasks.MigrateLocalUploads
|
||||
defp prepare_upload(upload = %__MODULE__{tempfile: path}, _opts) do
|
||||
with {:ok, content_type} <- Pleroma.MIME.file_mime_type(path) do
|
||||
{:ok, %__MODULE__{upload | content_type: content_type}}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_binary_size(binary, size_limit)
|
||||
when is_integer(size_limit) and size_limit > 0 and byte_size(binary) >= size_limit do
|
||||
{:error, :file_too_large}
|
||||
end
|
||||
|
||||
defp check_binary_size(_, _), do: :ok
|
||||
|
||||
defp check_file_size(path, size_limit) when is_integer(size_limit) and size_limit > 0 do
|
||||
with {:ok, %{size: size}} <- File.stat(path),
|
||||
true <- size <= size_limit do
|
||||
:ok
|
||||
else
|
||||
false -> {:error, :file_too_large}
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
|
||||
defp check_file_size(_, _), do: :ok
|
||||
|
||||
# Creates a tempfile using the Plug.Upload Genserver which cleans them up
|
||||
# automatically.
|
||||
defp tempfile_for_image(data) do
|
||||
{:ok, tmp_path} = Plug.Upload.random_file("profile_pics")
|
||||
{:ok, tmp_file} = File.open(tmp_path, [:write, :raw, :binary])
|
||||
IO.binwrite(tmp_file, data)
|
||||
|
|
@ -83,102 +210,10 @@ defmodule Pleroma.Upload do
|
|||
tmp_path
|
||||
end
|
||||
|
||||
def strip_exif_data(content_type, file) do
|
||||
settings = Application.get_env(:pleroma, Pleroma.Upload)
|
||||
do_strip = Keyword.fetch!(settings, :strip_exif)
|
||||
[filetype, _ext] = String.split(content_type, "/")
|
||||
|
||||
if filetype == "image" and do_strip == true do
|
||||
Mogrify.open(file) |> Mogrify.custom("strip") |> Mogrify.save(in_place: true)
|
||||
end
|
||||
defp url_from_spec(base_url, {:file, path}) do
|
||||
[base_url, "media", path]
|
||||
|> Path.join()
|
||||
end
|
||||
|
||||
defp create_name(uuid, ext, type) do
|
||||
case type do
|
||||
"application/octet-stream" ->
|
||||
String.downcase(Enum.join([uuid, ext], "."))
|
||||
|
||||
"audio/mpeg" ->
|
||||
String.downcase(Enum.join([uuid, "mp3"], "."))
|
||||
|
||||
_ ->
|
||||
String.downcase(Enum.join([uuid, List.last(String.split(type, "/"))], "."))
|
||||
end
|
||||
end
|
||||
|
||||
defp get_uuid(file, should_dedupe) do
|
||||
if should_dedupe do
|
||||
Base.encode16(:crypto.hash(:sha256, File.read!(file.path)))
|
||||
else
|
||||
UUID.generate()
|
||||
end
|
||||
end
|
||||
|
||||
defp get_name(file, uuid, type, should_dedupe) do
|
||||
if should_dedupe do
|
||||
create_name(uuid, List.last(String.split(file.filename, ".")), type)
|
||||
else
|
||||
parts = String.split(file.filename, ".")
|
||||
|
||||
new_filename =
|
||||
if length(parts) > 1 do
|
||||
Enum.drop(parts, -1) |> Enum.join(".")
|
||||
else
|
||||
Enum.join(parts)
|
||||
end
|
||||
|
||||
case type do
|
||||
"application/octet-stream" -> file.filename
|
||||
"audio/mpeg" -> new_filename <> ".mp3"
|
||||
"image/jpeg" -> new_filename <> ".jpg"
|
||||
_ -> Enum.join([new_filename, String.split(type, "/") |> List.last()], ".")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_content_type(file) do
|
||||
match =
|
||||
File.open(file, [:read], fn f ->
|
||||
case IO.binread(f, 8) do
|
||||
<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>> ->
|
||||
"image/png"
|
||||
|
||||
<<0x47, 0x49, 0x46, 0x38, _, 0x61, _, _>> ->
|
||||
"image/gif"
|
||||
|
||||
<<0xFF, 0xD8, 0xFF, _, _, _, _, _>> ->
|
||||
"image/jpeg"
|
||||
|
||||
<<0x1A, 0x45, 0xDF, 0xA3, _, _, _, _>> ->
|
||||
"video/webm"
|
||||
|
||||
<<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70>> ->
|
||||
"video/mp4"
|
||||
|
||||
<<0x49, 0x44, 0x33, _, _, _, _, _>> ->
|
||||
"audio/mpeg"
|
||||
|
||||
<<255, 251, _, 68, 0, 0, 0, 0>> ->
|
||||
"audio/mpeg"
|
||||
|
||||
<<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00>> ->
|
||||
"audio/ogg"
|
||||
|
||||
<<0x52, 0x49, 0x46, 0x46, _, _, _, _>> ->
|
||||
"audio/wav"
|
||||
|
||||
_ ->
|
||||
"application/octet-stream"
|
||||
end
|
||||
end)
|
||||
|
||||
case match do
|
||||
{:ok, type} -> type
|
||||
_e -> "application/octet-stream"
|
||||
end
|
||||
end
|
||||
|
||||
defp uploader() do
|
||||
Pleroma.Config.get!([Pleroma.Upload, :uploader])
|
||||
end
|
||||
defp url_from_spec(_base_url, {:url, url}), do: url
|
||||
end
|
||||
|
|
|
|||
35
lib/pleroma/upload/filter.ex
Normal file
35
lib/pleroma/upload/filter.ex
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
defmodule Pleroma.Upload.Filter do
|
||||
@moduledoc """
|
||||
Upload Filter behaviour
|
||||
|
||||
This behaviour allows to run filtering actions just before a file is uploaded. This allows to:
|
||||
|
||||
* morph in place the temporary file
|
||||
* change any field of a `Pleroma.Upload` struct
|
||||
* cancel/stop the upload
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
@callback filter(Pleroma.Upload.t()) :: :ok | {:ok, Pleroma.Upload.t()} | {:error, any()}
|
||||
|
||||
@spec filter([module()], Pleroma.Upload.t()) :: {:ok, Pleroma.Upload.t()} | {:error, any()}
|
||||
|
||||
def filter([], upload) do
|
||||
{:ok, upload}
|
||||
end
|
||||
|
||||
def filter([filter | rest], upload) do
|
||||
case filter.filter(upload) do
|
||||
:ok ->
|
||||
filter(rest, upload)
|
||||
|
||||
{:ok, upload} ->
|
||||
filter(rest, upload)
|
||||
|
||||
error ->
|
||||
Logger.error("#{__MODULE__}: Filter #{filter} failed: #{inspect(error)}")
|
||||
error
|
||||
end
|
||||
end
|
||||
end
|
||||
23
lib/pleroma/upload/filter/anonymize_filename.ex
Normal file
23
lib/pleroma/upload/filter/anonymize_filename.ex
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
defmodule Pleroma.Upload.Filter.AnonymizeFilename do
|
||||
@moduledoc """
|
||||
Replaces the original filename with a pre-defined text or randomly generated string.
|
||||
|
||||
Should be used after `Pleroma.Upload.Filter.Dedupe`.
|
||||
"""
|
||||
@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}}
|
||||
end
|
||||
|
||||
defp random(extension) do
|
||||
string =
|
||||
10
|
||||
|> :crypto.strong_rand_bytes()
|
||||
|> Base.url_encode64(padding: false)
|
||||
|
||||
string <> "." <> extension
|
||||
end
|
||||
end
|
||||
11
lib/pleroma/upload/filter/dedupe.ex
Normal file
11
lib/pleroma/upload/filter/dedupe.ex
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
defmodule Pleroma.Upload.Filter.Dedupe do
|
||||
@behaviour Pleroma.Upload.Filter
|
||||
alias Pleroma.Upload
|
||||
|
||||
def filter(upload = %Upload{name: name}) do
|
||||
extension = String.split(name, ".") |> List.last()
|
||||
shasum = :crypto.hash(:sha256, File.read!(upload.tempfile)) |> Base.encode16(case: :lower)
|
||||
filename = shasum <> "." <> extension
|
||||
{:ok, %Upload{upload | id: shasum, path: filename}}
|
||||
end
|
||||
end
|
||||
60
lib/pleroma/upload/filter/mogrifun.ex
Normal file
60
lib/pleroma/upload/filter/mogrifun.ex
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
defmodule Pleroma.Upload.Filter.Mogrifun do
|
||||
@behaviour Pleroma.Upload.Filter
|
||||
|
||||
@filters [
|
||||
{"implode", "1"},
|
||||
{"-raise", "20"},
|
||||
{"+raise", "20"},
|
||||
[{"-interpolate", "nearest"}, {"-virtual-pixel", "mirror"}, {"-spread", "5"}],
|
||||
"+polaroid",
|
||||
{"-statistic", "Mode 10"},
|
||||
{"-emboss", "0x1.1"},
|
||||
{"-emboss", "0x2"},
|
||||
{"-colorspace", "Gray"},
|
||||
"-negate",
|
||||
[{"-channel", "green"}, "-negate"],
|
||||
[{"-channel", "red"}, "-negate"],
|
||||
[{"-channel", "blue"}, "-negate"],
|
||||
{"+level-colors", "green,gold"},
|
||||
{"+level-colors", ",DodgerBlue"},
|
||||
{"+level-colors", ",Gold"},
|
||||
{"+level-colors", ",Lime"},
|
||||
{"+level-colors", ",Red"},
|
||||
{"+level-colors", ",DarkGreen"},
|
||||
{"+level-colors", "firebrick,yellow"},
|
||||
{"+level-colors", "'rgb(102,75,25)',lemonchiffon"},
|
||||
[{"fill", "red"}, {"tint", "40"}],
|
||||
[{"fill", "green"}, {"tint", "40"}],
|
||||
[{"fill", "blue"}, {"tint", "40"}],
|
||||
[{"fill", "yellow"}, {"tint", "40"}]
|
||||
]
|
||||
|
||||
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
|
||||
filter = Enum.random(@filters)
|
||||
|
||||
file
|
||||
|> Mogrify.open()
|
||||
|> mogrify_filter(filter)
|
||||
|> Mogrify.save(in_place: true)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def filter(_), do: :ok
|
||||
|
||||
defp mogrify_filter(mogrify, [filter | rest]) do
|
||||
mogrify
|
||||
|> mogrify_filter(filter)
|
||||
|> mogrify_filter(rest)
|
||||
end
|
||||
|
||||
defp mogrify_filter(mogrify, []), do: mogrify
|
||||
|
||||
defp mogrify_filter(mogrify, {action, options}) do
|
||||
Mogrify.custom(mogrify, action, options)
|
||||
end
|
||||
|
||||
defp mogrify_filter(mogrify, string) when is_binary(string) do
|
||||
Mogrify.custom(mogrify, string)
|
||||
end
|
||||
end
|
||||
37
lib/pleroma/upload/filter/mogrify.ex
Normal file
37
lib/pleroma/upload/filter/mogrify.ex
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
defmodule Pleroma.Upload.Filter.Mogrify do
|
||||
@behaviour Pleroma.Upload.Filter
|
||||
|
||||
@type conversion :: action :: String.t() | {action :: String.t(), opts :: String.t()}
|
||||
@type conversions :: conversion() | [conversion()]
|
||||
|
||||
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
|
||||
filters = Pleroma.Config.get!([__MODULE__, :args])
|
||||
|
||||
file
|
||||
|> Mogrify.open()
|
||||
|> mogrify_filter(filters)
|
||||
|> Mogrify.save(in_place: true)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def filter(_), do: :ok
|
||||
|
||||
defp mogrify_filter(mogrify, nil), do: mogrify
|
||||
|
||||
defp mogrify_filter(mogrify, [filter | rest]) do
|
||||
mogrify
|
||||
|> mogrify_filter(filter)
|
||||
|> mogrify_filter(rest)
|
||||
end
|
||||
|
||||
defp mogrify_filter(mogrify, []), do: mogrify
|
||||
|
||||
defp mogrify_filter(mogrify, {action, options}) do
|
||||
Mogrify.custom(mogrify, action, options)
|
||||
end
|
||||
|
||||
defp mogrify_filter(mogrify, action) when is_binary(action) do
|
||||
Mogrify.custom(mogrify, action)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,51 +1,32 @@
|
|||
defmodule Pleroma.Uploaders.Local do
|
||||
@behaviour Pleroma.Uploaders.Uploader
|
||||
|
||||
alias Pleroma.Web
|
||||
def get_file(_) do
|
||||
{:ok, {:static_dir, upload_path()}}
|
||||
end
|
||||
|
||||
def put_file(name, uuid, tmpfile, _content_type, should_dedupe) do
|
||||
upload_folder = get_upload_path(uuid, should_dedupe)
|
||||
url_path = get_url(name, uuid, should_dedupe)
|
||||
def put_file(upload) do
|
||||
{local_path, file} =
|
||||
case Enum.reverse(String.split(upload.path, "/", trim: true)) do
|
||||
[file] ->
|
||||
{upload_path(), file}
|
||||
|
||||
File.mkdir_p!(upload_folder)
|
||||
[file | folders] ->
|
||||
path = Path.join([upload_path()] ++ Enum.reverse(folders))
|
||||
File.mkdir_p!(path)
|
||||
{path, file}
|
||||
end
|
||||
|
||||
result_file = Path.join(upload_folder, name)
|
||||
result_file = Path.join(local_path, file)
|
||||
|
||||
if File.exists?(result_file) do
|
||||
File.rm!(tmpfile)
|
||||
else
|
||||
File.cp!(tmpfile, result_file)
|
||||
unless File.exists?(result_file) do
|
||||
File.cp!(upload.tempfile, result_file)
|
||||
end
|
||||
|
||||
{:ok, url_path}
|
||||
:ok
|
||||
end
|
||||
|
||||
def upload_path do
|
||||
settings = Application.get_env(:pleroma, Pleroma.Uploaders.Local)
|
||||
Keyword.fetch!(settings, :uploads)
|
||||
end
|
||||
|
||||
defp get_upload_path(uuid, should_dedupe) do
|
||||
if should_dedupe do
|
||||
upload_path()
|
||||
else
|
||||
Path.join(upload_path(), uuid)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_url(name, uuid, should_dedupe) do
|
||||
if should_dedupe do
|
||||
url_for(:cow_uri.urlencode(name))
|
||||
else
|
||||
url_for(Path.join(uuid, :cow_uri.urlencode(name)))
|
||||
end
|
||||
end
|
||||
|
||||
defp url_for(file) do
|
||||
settings = Application.get_env(:pleroma, Pleroma.Uploaders.Local)
|
||||
|
||||
Keyword.get(settings, :uploads_url)
|
||||
|> String.replace("{{file}}", file)
|
||||
|> String.replace("{{base_url}}", Web.base_url())
|
||||
Pleroma.Config.get!([__MODULE__, :uploads])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,22 +5,27 @@ defmodule Pleroma.Uploaders.MDII do
|
|||
|
||||
@httpoison Application.get_env(:pleroma, :httpoison)
|
||||
|
||||
def put_file(name, uuid, path, content_type, should_dedupe) do
|
||||
cgi = Pleroma.Config.get([Pleroma.Uploaders.MDII, :cgi])
|
||||
files = Pleroma.Config.get([Pleroma.Uploaders.MDII, :files])
|
||||
# MDII-hosted images are never passed through the MediaPlug; only local media.
|
||||
# Delegate to Pleroma.Uploaders.Local
|
||||
def get_file(file) do
|
||||
Pleroma.Uploaders.Local.get_file(file)
|
||||
end
|
||||
|
||||
{:ok, file_data} = File.read(path)
|
||||
def put_file(upload) do
|
||||
cgi = Config.get([Pleroma.Uploaders.MDII, :cgi])
|
||||
files = Config.get([Pleroma.Uploaders.MDII, :files])
|
||||
|
||||
extension = String.split(name, ".") |> List.last()
|
||||
{:ok, file_data} = File.read(upload.tempfile)
|
||||
|
||||
extension = String.split(upload.name, ".") |> List.last()
|
||||
query = "#{cgi}?#{extension}"
|
||||
|
||||
with {:ok, %{status_code: 200, body: body}} <- @httpoison.post(query, file_data) do
|
||||
File.rm!(path)
|
||||
with {:ok, %{status: 200, body: body}} <- @httpoison.post(query, file_data) do
|
||||
remote_file_name = String.split(body) |> List.first()
|
||||
public_url = "#{files}/#{remote_file_name}.#{extension}"
|
||||
{:ok, public_url}
|
||||
{:ok, {:url, public_url}}
|
||||
else
|
||||
_ -> Pleroma.Uploaders.Local.put_file(name, uuid, path, content_type, should_dedupe)
|
||||
_ -> Pleroma.Uploaders.Local.put_file(upload)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,40 +1,46 @@
|
|||
defmodule Pleroma.Uploaders.S3 do
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
||||
@behaviour Pleroma.Uploaders.Uploader
|
||||
require Logger
|
||||
|
||||
def put_file(name, uuid, path, content_type, _should_dedupe) do
|
||||
settings = Application.get_env(:pleroma, Pleroma.Uploaders.S3)
|
||||
bucket = Keyword.fetch!(settings, :bucket)
|
||||
public_endpoint = Keyword.fetch!(settings, :public_endpoint)
|
||||
force_media_proxy = Keyword.fetch!(settings, :force_media_proxy)
|
||||
# The file name is re-encoded with S3's constraints here to comply with previous links with less strict filenames
|
||||
def get_file(file) do
|
||||
config = Pleroma.Config.get([__MODULE__])
|
||||
|
||||
{:ok, file_data} = File.read(path)
|
||||
{:ok,
|
||||
{:url,
|
||||
Path.join([
|
||||
Keyword.fetch!(config, :public_endpoint),
|
||||
Keyword.fetch!(config, :bucket),
|
||||
strict_encode(URI.decode(file))
|
||||
])}}
|
||||
end
|
||||
|
||||
File.rm!(path)
|
||||
def put_file(upload = %Pleroma.Upload{}) do
|
||||
config = Pleroma.Config.get([__MODULE__])
|
||||
bucket = Keyword.get(config, :bucket)
|
||||
|
||||
s3_name = "#{uuid}/#{encode(name)}"
|
||||
{:ok, file_data} = File.read(upload.tempfile)
|
||||
|
||||
{:ok, _} =
|
||||
s3_name = strict_encode(upload.path)
|
||||
|
||||
op =
|
||||
ExAws.S3.put_object(bucket, s3_name, file_data, [
|
||||
{:acl, :public_read},
|
||||
{:content_type, content_type}
|
||||
{:content_type, upload.content_type}
|
||||
])
|
||||
|> ExAws.request()
|
||||
|
||||
url_base = "#{public_endpoint}/#{bucket}/#{s3_name}"
|
||||
case ExAws.request(op) do
|
||||
{:ok, _} ->
|
||||
{:ok, {:file, s3_name}}
|
||||
|
||||
public_url =
|
||||
if force_media_proxy do
|
||||
MediaProxy.url(url_base)
|
||||
else
|
||||
url_base
|
||||
end
|
||||
|
||||
{:ok, public_url}
|
||||
error ->
|
||||
Logger.error("#{__MODULE__}: #{inspect(error)}")
|
||||
{:error, "S3 Upload failed"}
|
||||
end
|
||||
end
|
||||
|
||||
defp encode(name) do
|
||||
String.replace(name, ~r/[^0-9a-zA-Z!.*'()_-]/, "-")
|
||||
@regex Regex.compile!("[^0-9a-zA-Z!.*/'()_-]")
|
||||
def strict_encode(name) do
|
||||
String.replace(name, @regex, "-")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ defmodule Pleroma.Uploaders.Swift.Keystone do
|
|||
["Content-Type": "application/json"],
|
||||
hackney: [:insecure]
|
||||
) do
|
||||
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
|
||||
{:ok, %Tesla.Env{status: 200, body: body}} ->
|
||||
body["access"]["token"]["id"]
|
||||
|
||||
{:ok, %HTTPoison.Response{status_code: _}} ->
|
||||
{:ok, %Tesla.Env{status: _}} ->
|
||||
""
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,14 +9,13 @@ defmodule Pleroma.Uploaders.Swift.Client do
|
|||
end
|
||||
|
||||
def upload_file(filename, body, content_type) do
|
||||
object_url = Pleroma.Config.get!([Pleroma.Uploaders.Swift, :object_url])
|
||||
token = Pleroma.Uploaders.Swift.Keystone.get_token()
|
||||
|
||||
case put("#{filename}", body, "X-Auth-Token": token, "Content-Type": content_type) do
|
||||
{:ok, %HTTPoison.Response{status_code: 201}} ->
|
||||
{:ok, "#{object_url}/#{filename}"}
|
||||
{:ok, %Tesla.Env{status: 201}} ->
|
||||
{:ok, {:file, filename}}
|
||||
|
||||
{:ok, %HTTPoison.Response{status_code: 401}} ->
|
||||
{:ok, %Tesla.Env{status: 401}} ->
|
||||
{:error, "Unauthorized, Bad Token"}
|
||||
|
||||
{:error, _} ->
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
defmodule Pleroma.Uploaders.Swift do
|
||||
@behaviour Pleroma.Uploaders.Uploader
|
||||
|
||||
def put_file(name, uuid, tmp_path, content_type, _should_dedupe) do
|
||||
{:ok, file_data} = File.read(tmp_path)
|
||||
remote_name = "#{uuid}/#{name}"
|
||||
def get_file(name) do
|
||||
{:ok, {:url, Path.join([Pleroma.Config.get!([__MODULE__, :object_url]), name])}}
|
||||
end
|
||||
|
||||
Pleroma.Uploaders.Swift.Client.upload_file(remote_name, file_data, content_type)
|
||||
def put_file(upload) do
|
||||
Pleroma.Uploaders.Swift.Client.upload_file(
|
||||
upload.path,
|
||||
File.read!(upload.tmpfile),
|
||||
upload.content_type
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,20 +1,40 @@
|
|||
defmodule Pleroma.Uploaders.Uploader do
|
||||
@moduledoc """
|
||||
Defines the contract to put an uploaded file to any backend.
|
||||
Defines the contract to put and get an uploaded file to any backend.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Instructs how to get the file from the backend.
|
||||
|
||||
Used by `Pleroma.Plugs.UploadedMedia`.
|
||||
"""
|
||||
@type get_method :: {:static_dir, directory :: String.t()} | {:url, url :: String.t()}
|
||||
@callback get_file(file :: String.t()) :: {:ok, get_method()}
|
||||
|
||||
@doc """
|
||||
Put a file to the backend.
|
||||
|
||||
Returns `{:ok, String.t } | {:error, String.t} containing the path of the
|
||||
uploaded file, or error information if the file failed to be saved to the
|
||||
respective backend.
|
||||
Returns:
|
||||
|
||||
* `:ok` which assumes `{:ok, upload.path}`
|
||||
* `{:ok, spec}` where spec is:
|
||||
* `{:file, filename :: String.t}` to handle reads with `get_file/1` (recommended)
|
||||
|
||||
This allows to correctly proxy or redirect requests to the backend, while allowing to migrate backends without breaking any URL.
|
||||
* `{url, url :: String.t}` to bypass `get_file/2` and use the `url` directly in the activity.
|
||||
* `{:error, String.t}` error information if the file failed to be saved to the backend.
|
||||
|
||||
|
||||
"""
|
||||
@callback put_file(
|
||||
name :: String.t(),
|
||||
uuid :: String.t(),
|
||||
file :: File.t(),
|
||||
content_type :: String.t(),
|
||||
should_dedupe :: Boolean.t()
|
||||
) :: {:ok, String.t()} | {:error, String.t()}
|
||||
@callback put_file(Pleroma.Upload.t()) ::
|
||||
:ok | {:ok, {:file | :url, String.t()}} | {:error, String.t()}
|
||||
|
||||
@spec put_file(module(), Pleroma.Upload.t()) ::
|
||||
{:ok, {:file | :url, String.t()}} | {:error, String.t()}
|
||||
def put_file(uploader, upload) do
|
||||
case uploader.put_file(upload) do
|
||||
:ok -> {:ok, {:file, upload.path}}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,9 +4,18 @@ defmodule Pleroma.User do
|
|||
import Ecto.{Changeset, Query}
|
||||
alias Pleroma.{Repo, User, Object, Web, Activity, Notification}
|
||||
alias Comeonin.Pbkdf2
|
||||
alias Pleroma.Formatter
|
||||
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
|
||||
alias Pleroma.Web.{OStatus, Websub, OAuth}
|
||||
alias Pleroma.Web.ActivityPub.{Utils, ActivityPub}
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
@email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
|
||||
|
||||
@strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
|
||||
@extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
|
||||
|
||||
schema "users" do
|
||||
field(:bio, :string)
|
||||
field(:email, :string)
|
||||
|
|
@ -19,11 +28,12 @@ defmodule Pleroma.User do
|
|||
field(:ap_id, :string)
|
||||
field(:avatar, :map)
|
||||
field(:local, :boolean, default: true)
|
||||
field(:info, :map, default: %{})
|
||||
field(:follower_address, :string)
|
||||
field(:search_distance, :float, virtual: true)
|
||||
field(:tags, {:array, :string}, default: [])
|
||||
field(:last_refreshed_at, :naive_datetime)
|
||||
has_many(:notifications, Notification)
|
||||
embeds_one(:info, Pleroma.User.Info)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
|
@ -36,13 +46,13 @@ defmodule Pleroma.User do
|
|||
end
|
||||
|
||||
def banner_url(user) do
|
||||
case user.info["banner"] do
|
||||
case user.info.banner do
|
||||
%{"url" => [%{"href" => href} | _]} -> href
|
||||
_ -> "#{Web.base_url()}/images/banner.png"
|
||||
end
|
||||
end
|
||||
|
||||
def profile_url(%User{info: %{"source_data" => %{"url" => url}}}), do: url
|
||||
def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
|
||||
def profile_url(%User{ap_id: ap_id}), do: ap_id
|
||||
def profile_url(_), do: nil
|
||||
|
||||
|
|
@ -60,38 +70,38 @@ defmodule Pleroma.User do
|
|||
|> validate_required([:following])
|
||||
end
|
||||
|
||||
def info_changeset(struct, params \\ %{}) do
|
||||
struct
|
||||
|> cast(params, [:info])
|
||||
|> validate_required([:info])
|
||||
end
|
||||
|
||||
def user_info(%User{} = user) do
|
||||
oneself = if user.local, do: 1, else: 0
|
||||
|
||||
%{
|
||||
following_count: length(user.following) - oneself,
|
||||
note_count: user.info["note_count"] || 0,
|
||||
follower_count: user.info["follower_count"] || 0,
|
||||
locked: user.info["locked"] || false,
|
||||
default_scope: user.info["default_scope"] || "public"
|
||||
note_count: user.info.note_count,
|
||||
follower_count: user.info.follower_count,
|
||||
locked: user.info.locked,
|
||||
default_scope: user.info.default_scope
|
||||
}
|
||||
end
|
||||
|
||||
@email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
|
||||
def remote_user_creation(params) do
|
||||
params =
|
||||
params
|
||||
|> Map.put(:info, params[:info] || %{})
|
||||
|
||||
info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
|
||||
|
||||
changes =
|
||||
%User{}
|
||||
|> cast(params, [:bio, :name, :ap_id, :nickname, :info, :avatar])
|
||||
|> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
|
||||
|> validate_required([:name, :ap_id])
|
||||
|> unique_constraint(:nickname)
|
||||
|> validate_format(:nickname, @email_regex)
|
||||
|> validate_length(:bio, max: 5000)
|
||||
|> validate_length(:name, max: 100)
|
||||
|> put_change(:local, false)
|
||||
|> put_embed(:info, info_cng)
|
||||
|
||||
if changes.valid? do
|
||||
case changes.changes[:info]["source_data"] do
|
||||
case info_cng.changes[:source_data] do
|
||||
%{"followers" => followers} ->
|
||||
changes
|
||||
|> put_change(:follower_address, followers)
|
||||
|
|
@ -109,9 +119,9 @@ defmodule Pleroma.User do
|
|||
|
||||
def update_changeset(struct, params \\ %{}) do
|
||||
struct
|
||||
|> cast(params, [:bio, :name])
|
||||
|> cast(params, [:bio, :name, :avatar])
|
||||
|> unique_constraint(:nickname)
|
||||
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|
||||
|> validate_format(:nickname, local_nickname_regex())
|
||||
|> validate_length(:bio, max: 5000)
|
||||
|> validate_length(:name, min: 1, max: 100)
|
||||
end
|
||||
|
|
@ -121,12 +131,17 @@ defmodule Pleroma.User do
|
|||
params
|
||||
|> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
|
||||
|
||||
info_cng =
|
||||
struct.info
|
||||
|> User.Info.user_upgrade(params[:info])
|
||||
|
||||
struct
|
||||
|> cast(params, [:bio, :name, :info, :follower_address, :avatar, :last_refreshed_at])
|
||||
|> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
|
||||
|> unique_constraint(:nickname)
|
||||
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|
||||
|> validate_format(:nickname, local_nickname_regex())
|
||||
|> validate_length(:bio, max: 5000)
|
||||
|> validate_length(:name, max: 100)
|
||||
|> put_embed(:info, info_cng)
|
||||
end
|
||||
|
||||
def password_update_changeset(struct, params) do
|
||||
|
|
@ -161,10 +176,11 @@ defmodule Pleroma.User do
|
|||
|> validate_confirmation(:password)
|
||||
|> unique_constraint(:email)
|
||||
|> unique_constraint(:nickname)
|
||||
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|
||||
|> validate_format(:nickname, local_nickname_regex())
|
||||
|> validate_format(:email, @email_regex)
|
||||
|> validate_length(:bio, max: 1000)
|
||||
|> validate_length(:name, min: 1, max: 100)
|
||||
|> put_change(:info, %Pleroma.User.Info{})
|
||||
|
||||
if changeset.valid? do
|
||||
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
|
||||
|
|
@ -191,7 +207,7 @@ defmodule Pleroma.User do
|
|||
|
||||
def needs_update?(_), do: true
|
||||
|
||||
def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{"locked" => true}}) do
|
||||
def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
|
||||
{:ok, follower}
|
||||
end
|
||||
|
||||
|
|
@ -200,14 +216,14 @@ defmodule Pleroma.User do
|
|||
end
|
||||
|
||||
def maybe_direct_follow(%User{} = follower, %User{} = followed) do
|
||||
if !User.ap_enabled?(followed) do
|
||||
if not User.ap_enabled?(followed) do
|
||||
follow(follower, followed)
|
||||
else
|
||||
{:ok, follower}
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_follow(%User{} = follower, %User{info: info} = followed) do
|
||||
def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
|
||||
if not following?(follower, followed) do
|
||||
follow(follower, followed)
|
||||
else
|
||||
|
|
@ -222,7 +238,7 @@ defmodule Pleroma.User do
|
|||
ap_followers = followed.follower_address
|
||||
|
||||
cond do
|
||||
following?(follower, followed) or info["deactivated"] ->
|
||||
following?(follower, followed) or info.deactivated ->
|
||||
{:error, "Could not follow user: #{followed.nickname} is already on your list."}
|
||||
|
||||
deny_follow_blocked and blocks?(followed, follower) ->
|
||||
|
|
@ -269,12 +285,13 @@ defmodule Pleroma.User do
|
|||
end
|
||||
end
|
||||
|
||||
@spec following?(User.t(), User.t()) :: boolean
|
||||
def following?(%User{} = follower, %User{} = followed) do
|
||||
Enum.member?(follower.following, followed.follower_address)
|
||||
end
|
||||
|
||||
def locked?(%User{} = user) do
|
||||
user.info["locked"] || false
|
||||
user.info.locked || false
|
||||
end
|
||||
|
||||
def get_by_ap_id(ap_id) do
|
||||
|
|
@ -411,22 +428,23 @@ defmodule Pleroma.User do
|
|||
end
|
||||
|
||||
def increase_note_count(%User{} = user) do
|
||||
note_count = (user.info["note_count"] || 0) + 1
|
||||
new_info = Map.put(user.info, "note_count", note_count)
|
||||
info_cng = User.Info.add_to_note_count(user.info, 1)
|
||||
|
||||
cs = info_changeset(user, %{info: new_info})
|
||||
cng =
|
||||
change(user)
|
||||
|> put_embed(:info, info_cng)
|
||||
|
||||
update_and_set_cache(cs)
|
||||
update_and_set_cache(cng)
|
||||
end
|
||||
|
||||
def decrease_note_count(%User{} = user) do
|
||||
note_count = user.info["note_count"] || 0
|
||||
note_count = if note_count <= 0, do: 0, else: note_count - 1
|
||||
new_info = Map.put(user.info, "note_count", note_count)
|
||||
info_cng = User.Info.add_to_note_count(user.info, -1)
|
||||
|
||||
cs = info_changeset(user, %{info: new_info})
|
||||
cng =
|
||||
change(user)
|
||||
|> put_embed(:info, info_cng)
|
||||
|
||||
update_and_set_cache(cs)
|
||||
update_and_set_cache(cng)
|
||||
end
|
||||
|
||||
def update_note_count(%User{} = user) do
|
||||
|
|
@ -439,11 +457,13 @@ defmodule Pleroma.User do
|
|||
|
||||
note_count = Repo.one(note_count_query)
|
||||
|
||||
new_info = Map.put(user.info, "note_count", note_count)
|
||||
info_cng = User.Info.set_note_count(user.info, note_count)
|
||||
|
||||
cs = info_changeset(user, %{info: new_info})
|
||||
cng =
|
||||
change(user)
|
||||
|> put_embed(:info, info_cng)
|
||||
|
||||
update_and_set_cache(cs)
|
||||
update_and_set_cache(cng)
|
||||
end
|
||||
|
||||
def update_follower_count(%User{} = user) do
|
||||
|
|
@ -457,11 +477,15 @@ defmodule Pleroma.User do
|
|||
|
||||
follower_count = Repo.one(follower_count_query)
|
||||
|
||||
new_info = Map.put(user.info, "follower_count", follower_count)
|
||||
info_cng =
|
||||
user.info
|
||||
|> User.Info.set_follower_count(follower_count)
|
||||
|
||||
cs = info_changeset(user, %{info: new_info})
|
||||
cng =
|
||||
change(user)
|
||||
|> put_embed(:info, info_cng)
|
||||
|
||||
update_and_set_cache(cs)
|
||||
update_and_set_cache(cng)
|
||||
end
|
||||
|
||||
def get_users_from_set_query(ap_ids, false) do
|
||||
|
|
@ -545,12 +569,15 @@ defmodule Pleroma.User do
|
|||
unfollow(blocked, blocker)
|
||||
end
|
||||
|
||||
blocks = blocker.info["blocks"] || []
|
||||
new_blocks = Enum.uniq([ap_id | blocks])
|
||||
new_info = Map.put(blocker.info, "blocks", new_blocks)
|
||||
info_cng =
|
||||
blocker.info
|
||||
|> User.Info.add_to_block(ap_id)
|
||||
|
||||
cs = User.info_changeset(blocker, %{info: new_info})
|
||||
update_and_set_cache(cs)
|
||||
cng =
|
||||
change(blocker)
|
||||
|> put_embed(:info, info_cng)
|
||||
|
||||
update_and_set_cache(cng)
|
||||
end
|
||||
|
||||
# helper to handle the block given only an actor's AP id
|
||||
|
|
@ -558,18 +585,21 @@ defmodule Pleroma.User do
|
|||
block(blocker, User.get_by_ap_id(ap_id))
|
||||
end
|
||||
|
||||
def unblock(user, %{ap_id: ap_id}) do
|
||||
blocks = user.info["blocks"] || []
|
||||
new_blocks = List.delete(blocks, ap_id)
|
||||
new_info = Map.put(user.info, "blocks", new_blocks)
|
||||
def unblock(blocker, %{ap_id: ap_id}) do
|
||||
info_cng =
|
||||
blocker.info
|
||||
|> User.Info.remove_from_block(ap_id)
|
||||
|
||||
cs = User.info_changeset(user, %{info: new_info})
|
||||
update_and_set_cache(cs)
|
||||
cng =
|
||||
change(blocker)
|
||||
|> put_embed(:info, info_cng)
|
||||
|
||||
update_and_set_cache(cng)
|
||||
end
|
||||
|
||||
def blocks?(user, %{ap_id: ap_id}) do
|
||||
blocks = user.info["blocks"] || []
|
||||
domain_blocks = user.info["domain_blocks"] || []
|
||||
blocks = user.info.blocks
|
||||
domain_blocks = user.info.domain_blocks
|
||||
%{host: host} = URI.parse(ap_id)
|
||||
|
||||
Enum.member?(blocks, ap_id) ||
|
||||
|
|
@ -579,21 +609,27 @@ defmodule Pleroma.User do
|
|||
end
|
||||
|
||||
def block_domain(user, domain) do
|
||||
domain_blocks = user.info["domain_blocks"] || []
|
||||
new_blocks = Enum.uniq([domain | domain_blocks])
|
||||
new_info = Map.put(user.info, "domain_blocks", new_blocks)
|
||||
info_cng =
|
||||
user.info
|
||||
|> User.Info.add_to_domain_block(domain)
|
||||
|
||||
cs = User.info_changeset(user, %{info: new_info})
|
||||
update_and_set_cache(cs)
|
||||
cng =
|
||||
change(user)
|
||||
|> put_embed(:info, info_cng)
|
||||
|
||||
update_and_set_cache(cng)
|
||||
end
|
||||
|
||||
def unblock_domain(user, domain) do
|
||||
blocks = user.info["domain_blocks"] || []
|
||||
new_blocks = List.delete(blocks, domain)
|
||||
new_info = Map.put(user.info, "domain_blocks", new_blocks)
|
||||
info_cng =
|
||||
user.info
|
||||
|> User.Info.remove_from_domain_block(domain)
|
||||
|
||||
cs = User.info_changeset(user, %{info: new_info})
|
||||
update_and_set_cache(cs)
|
||||
cng =
|
||||
change(user)
|
||||
|> put_embed(:info, info_cng)
|
||||
|
||||
update_and_set_cache(cng)
|
||||
end
|
||||
|
||||
def local_user_query() do
|
||||
|
|
@ -613,9 +649,13 @@ defmodule Pleroma.User do
|
|||
end
|
||||
|
||||
def deactivate(%User{} = user, status \\ true) do
|
||||
new_info = Map.put(user.info, "deactivated", status)
|
||||
cs = User.info_changeset(user, %{info: new_info})
|
||||
update_and_set_cache(cs)
|
||||
info_cng = User.Info.set_activation_status(user.info, status)
|
||||
|
||||
cng =
|
||||
change(user)
|
||||
|> put_embed(:info, info_cng)
|
||||
|
||||
update_and_set_cache(cng)
|
||||
end
|
||||
|
||||
def delete(%User{} = user) do
|
||||
|
|
@ -649,7 +689,7 @@ defmodule Pleroma.User do
|
|||
{:ok, user}
|
||||
end
|
||||
|
||||
def html_filter_policy(%User{info: %{"no_rich_text" => true}}) do
|
||||
def html_filter_policy(%User{info: %{no_rich_text: true}}) do
|
||||
Pleroma.HTML.Scrubber.TwitterText
|
||||
end
|
||||
|
||||
|
|
@ -683,7 +723,7 @@ defmodule Pleroma.User do
|
|||
user
|
||||
else
|
||||
changes =
|
||||
%User{}
|
||||
%User{info: %User.Info{}}
|
||||
|> cast(%{}, [:ap_id, :nickname, :local])
|
||||
|> put_change(:ap_id, relay_uri)
|
||||
|> put_change(:nickname, nil)
|
||||
|
|
@ -697,10 +737,11 @@ defmodule Pleroma.User do
|
|||
|
||||
# AP style
|
||||
def public_key_from_info(%{
|
||||
"source_data" => %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
|
||||
source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
|
||||
}) do
|
||||
key =
|
||||
:public_key.pem_decode(public_key_pem)
|
||||
public_key_pem
|
||||
|> :public_key.pem_decode()
|
||||
|> hd()
|
||||
|> :public_key.pem_entry_decode()
|
||||
|
||||
|
|
@ -708,7 +749,7 @@ 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}) do
|
||||
{:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
|
||||
end
|
||||
|
||||
|
|
@ -730,20 +771,18 @@ defmodule Pleroma.User do
|
|||
|> Map.put(:name, blank?(data[:name]) || data[:nickname])
|
||||
|
||||
cs = User.remote_user_creation(data)
|
||||
|
||||
Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
|
||||
end
|
||||
|
||||
def ap_enabled?(%User{local: true}), do: true
|
||||
def ap_enabled?(%User{info: info}), do: info["ap_enabled"]
|
||||
def ap_enabled?(%User{info: info}), do: info.ap_enabled
|
||||
def ap_enabled?(_), do: false
|
||||
|
||||
def get_or_fetch(uri_or_nickname) do
|
||||
if String.starts_with?(uri_or_nickname, "http") do
|
||||
get_or_fetch_by_ap_id(uri_or_nickname)
|
||||
else
|
||||
get_or_fetch_by_nickname(uri_or_nickname)
|
||||
end
|
||||
end
|
||||
@doc "Gets or fetch a user by uri or nickname."
|
||||
@spec get_or_fetch(String.t()) :: User.t()
|
||||
def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
|
||||
def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
|
||||
|
||||
# wait a period of time and return newest version of the User structs
|
||||
# this is because we have synchronous follow APIs and need to simulate them
|
||||
|
|
@ -768,4 +807,71 @@ defmodule Pleroma.User do
|
|||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
|
||||
def parse_bio(nil, _user), do: ""
|
||||
def parse_bio(bio, _user) when bio == "", do: bio
|
||||
|
||||
def parse_bio(bio, user) do
|
||||
mentions = Formatter.parse_mentions(bio)
|
||||
tags = Formatter.parse_tags(bio)
|
||||
|
||||
emoji =
|
||||
(user.info.source_data["tag"] || [])
|
||||
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|
||||
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
|
||||
{String.trim(name, ":"), url}
|
||||
end)
|
||||
|
||||
bio
|
||||
|> CommonUtils.format_input(mentions, tags, "text/plain")
|
||||
|> Formatter.emojify(emoji)
|
||||
end
|
||||
|
||||
def tag(user_identifiers, tags) when is_list(user_identifiers) do
|
||||
Repo.transaction(fn ->
|
||||
for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
|
||||
end)
|
||||
end
|
||||
|
||||
def tag(nickname, tags) when is_binary(nickname),
|
||||
do: tag(User.get_by_nickname(nickname), tags)
|
||||
|
||||
def tag(%User{} = user, tags),
|
||||
do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
|
||||
|
||||
def untag(user_identifiers, tags) when is_list(user_identifiers) do
|
||||
Repo.transaction(fn ->
|
||||
for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
|
||||
end)
|
||||
end
|
||||
|
||||
def untag(nickname, tags) when is_binary(nickname),
|
||||
do: untag(User.get_by_nickname(nickname), tags)
|
||||
|
||||
def untag(%User{} = user, tags),
|
||||
do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
|
||||
|
||||
defp update_tags(%User{} = user, new_tags) do
|
||||
{:ok, updated_user} =
|
||||
user
|
||||
|> change(%{tags: new_tags})
|
||||
|> Repo.update()
|
||||
|
||||
updated_user
|
||||
end
|
||||
|
||||
defp normalize_tags(tags) do
|
||||
[tags]
|
||||
|> List.flatten()
|
||||
|> Enum.map(&String.downcase(&1))
|
||||
end
|
||||
|
||||
defp local_nickname_regex() do
|
||||
if Pleroma.Config.get([:instance, :extended_nickname_format]) do
|
||||
@extended_local_nickname_regex
|
||||
else
|
||||
@strict_local_nickname_regex
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
175
lib/pleroma/user/info.ex
Normal file
175
lib/pleroma/user/info.ex
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
defmodule Pleroma.User.Info do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
embedded_schema do
|
||||
field(:banner, :map, default: %{})
|
||||
field(:background, :map, default: %{})
|
||||
field(:source_data, :map, default: %{})
|
||||
field(:note_count, :integer, default: 0)
|
||||
field(:follower_count, :integer, default: 0)
|
||||
field(:locked, :boolean, default: false)
|
||||
field(:default_scope, :string, default: "public")
|
||||
field(:blocks, {:array, :string}, default: [])
|
||||
field(:domain_blocks, {:array, :string}, default: [])
|
||||
field(:deactivated, :boolean, default: false)
|
||||
field(:no_rich_text, :boolean, default: false)
|
||||
field(:ap_enabled, :boolean, default: false)
|
||||
field(:is_moderator, :boolean, default: false)
|
||||
field(:is_admin, :boolean, default: false)
|
||||
field(:keys, :string, default: nil)
|
||||
field(:settings, :map, default: nil)
|
||||
field(:magic_key, :string, default: nil)
|
||||
field(:uri, :string, default: nil)
|
||||
field(:topic, :string, default: nil)
|
||||
field(:hub, :string, default: nil)
|
||||
field(:salmon, :string, default: nil)
|
||||
field(:hide_network, :boolean, default: false)
|
||||
|
||||
# Found in the wild
|
||||
# ap_id -> Where is this used?
|
||||
# bio -> Where is this used?
|
||||
# avatar -> Where is this used?
|
||||
# fqn -> Where is this used?
|
||||
# host -> Where is this used?
|
||||
# subject _> Where is this used?
|
||||
end
|
||||
|
||||
def set_activation_status(info, deactivated) do
|
||||
params = %{deactivated: deactivated}
|
||||
|
||||
info
|
||||
|> cast(params, [:deactivated])
|
||||
|> validate_required([:deactivated])
|
||||
end
|
||||
|
||||
def add_to_note_count(info, number) do
|
||||
set_note_count(info, info.note_count + number)
|
||||
end
|
||||
|
||||
def set_note_count(info, number) do
|
||||
params = %{note_count: Enum.max([0, number])}
|
||||
|
||||
info
|
||||
|> cast(params, [:note_count])
|
||||
|> validate_required([:note_count])
|
||||
end
|
||||
|
||||
def set_follower_count(info, number) do
|
||||
params = %{follower_count: Enum.max([0, number])}
|
||||
|
||||
info
|
||||
|> cast(params, [:follower_count])
|
||||
|> validate_required([:follower_count])
|
||||
end
|
||||
|
||||
def set_blocks(info, blocks) do
|
||||
params = %{blocks: blocks}
|
||||
|
||||
info
|
||||
|> cast(params, [:blocks])
|
||||
|> validate_required([:blocks])
|
||||
end
|
||||
|
||||
def add_to_block(info, blocked) do
|
||||
set_blocks(info, Enum.uniq([blocked | info.blocks]))
|
||||
end
|
||||
|
||||
def remove_from_block(info, blocked) do
|
||||
set_blocks(info, List.delete(info.blocks, blocked))
|
||||
end
|
||||
|
||||
def set_domain_blocks(info, domain_blocks) do
|
||||
params = %{domain_blocks: domain_blocks}
|
||||
|
||||
info
|
||||
|> cast(params, [:domain_blocks])
|
||||
|> validate_required([:domain_blocks])
|
||||
end
|
||||
|
||||
def add_to_domain_block(info, domain_blocked) do
|
||||
set_domain_blocks(info, Enum.uniq([domain_blocked | info.domain_blocks]))
|
||||
end
|
||||
|
||||
def remove_from_domain_block(info, domain_blocked) do
|
||||
set_domain_blocks(info, List.delete(info.domain_blocks, domain_blocked))
|
||||
end
|
||||
|
||||
def set_keys(info, keys) do
|
||||
params = %{keys: keys}
|
||||
|
||||
info
|
||||
|> cast(params, [:keys])
|
||||
|> validate_required([:keys])
|
||||
end
|
||||
|
||||
def remote_user_creation(info, params) do
|
||||
info
|
||||
|> cast(params, [
|
||||
:ap_enabled,
|
||||
:source_data,
|
||||
:banner,
|
||||
:locked,
|
||||
:magic_key,
|
||||
:uri,
|
||||
:hub,
|
||||
:topic,
|
||||
:salmon
|
||||
])
|
||||
end
|
||||
|
||||
def user_upgrade(info, params) do
|
||||
info
|
||||
|> cast(params, [
|
||||
:ap_enabled,
|
||||
:source_data,
|
||||
:banner,
|
||||
:locked,
|
||||
:magic_key
|
||||
])
|
||||
end
|
||||
|
||||
def profile_update(info, params) do
|
||||
info
|
||||
|> cast(params, [
|
||||
:locked,
|
||||
:no_rich_text,
|
||||
:default_scope,
|
||||
:banner,
|
||||
:hide_network,
|
||||
:background
|
||||
])
|
||||
end
|
||||
|
||||
def mastodon_profile_update(info, params) do
|
||||
info
|
||||
|> cast(params, [
|
||||
:locked,
|
||||
:banner
|
||||
])
|
||||
end
|
||||
|
||||
def mastodon_settings_update(info, settings) do
|
||||
params = %{settings: settings}
|
||||
|
||||
info
|
||||
|> cast(params, [:settings])
|
||||
|> validate_required([:settings])
|
||||
end
|
||||
|
||||
def set_source_data(info, source_data) do
|
||||
params = %{source_data: source_data}
|
||||
|
||||
info
|
||||
|> cast(params, [:source_data])
|
||||
|> validate_required([:source_data])
|
||||
end
|
||||
|
||||
def admin_api_update(info, params) do
|
||||
info
|
||||
|> cast(params, [
|
||||
:is_moderator,
|
||||
:is_admin
|
||||
])
|
||||
end
|
||||
end
|
||||
|
|
@ -3,7 +3,8 @@ defmodule Pleroma.UserInviteToken do
|
|||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Pleroma.{User, UserInviteToken, Repo}
|
||||
alias Pleroma.UserInviteToken
|
||||
alias Pleroma.Repo
|
||||
|
||||
schema "user_invite_tokens" do
|
||||
field(:token, :string)
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
defp check_actor_is_active(actor) do
|
||||
if not is_nil(actor) do
|
||||
with user <- User.get_cached_by_ap_id(actor),
|
||||
false <- !!user.info["deactivated"] do
|
||||
false <- user.info.deactivated do
|
||||
:ok
|
||||
else
|
||||
_e -> :reject
|
||||
|
|
@ -509,8 +509,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
|
||||
defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
|
||||
blocks = info["blocks"] || []
|
||||
domain_blocks = info["domain_blocks"] || []
|
||||
blocks = info.blocks || []
|
||||
domain_blocks = info.domain_blocks || []
|
||||
|
||||
from(
|
||||
activity in query,
|
||||
|
|
@ -572,11 +572,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> Enum.reverse()
|
||||
end
|
||||
|
||||
def upload(file, size_limit \\ nil) do
|
||||
with data <-
|
||||
Upload.store(file, Application.get_env(:pleroma, :instance)[:dedupe_media], size_limit),
|
||||
false <- is_nil(data) do
|
||||
Repo.insert(%Object{data: data})
|
||||
def upload(file, opts \\ []) do
|
||||
with {:ok, data} <- Upload.store(file, opts) do
|
||||
obj_data =
|
||||
if opts[:actor] do
|
||||
Map.put(data, "actor", opts[:actor])
|
||||
else
|
||||
data
|
||||
end
|
||||
|
||||
Repo.insert(%Object{data: obj_data})
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -678,7 +683,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
remote_inboxes =
|
||||
(Pleroma.Web.Salmon.remote_users(activity) ++ followers)
|
||||
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
|
||||
|> Enum.map(fn %{info: %{"source_data" => data}} ->
|
||||
|> Enum.map(fn %{info: %{source_data: data}} ->
|
||||
(is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
|
||||
end)
|
||||
|> Enum.uniq()
|
||||
|
|
@ -764,13 +769,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
Logger.info("Fetching #{id} via AP")
|
||||
|
||||
with true <- String.starts_with?(id, "http"),
|
||||
{:ok, %{body: body, status_code: code}} when code in 200..299 <-
|
||||
{:ok, %{body: body, status: code}} when code in 200..299 <-
|
||||
@httpoison.get(
|
||||
id,
|
||||
[Accept: "application/activity+json"],
|
||||
follow_redirect: true,
|
||||
timeout: 10000,
|
||||
recv_timeout: 20000
|
||||
[{:Accept, "application/activity+json"}]
|
||||
),
|
||||
{:ok, data} <- Jason.decode(body),
|
||||
:ok <- Transmogrifier.contain_origin_from_id(id, data) do
|
||||
|
|
@ -797,7 +799,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
|
||||
# guard
|
||||
def entire_thread_visible_for_user?(nil, user), do: false
|
||||
def entire_thread_visible_for_user?(nil, _user), do: false
|
||||
|
||||
# child
|
||||
def entire_thread_visible_for_user?(
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
json(conn, "error")
|
||||
end
|
||||
|
||||
def relay(conn, params) do
|
||||
def relay(conn, _params) do
|
||||
with %User{} = user <- Relay.get_actor(),
|
||||
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
|
||||
conn
|
||||
|
|
|
|||
40
lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
Normal file
40
lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do
|
||||
alias Pleroma.Object
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF
|
||||
|
||||
@reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless])
|
||||
def filter_by_summary(
|
||||
%{"summary" => parent_summary} = _parent,
|
||||
%{"summary" => child_summary} = child
|
||||
)
|
||||
when not is_nil(child_summary) and byte_size(child_summary) > 0 and
|
||||
not is_nil(parent_summary) and byte_size(parent_summary) > 0 do
|
||||
if (child_summary == parent_summary and not Regex.match?(@reply_prefix, child_summary)) or
|
||||
(Regex.match?(@reply_prefix, parent_summary) &&
|
||||
Regex.replace(@reply_prefix, parent_summary, "") == child_summary) do
|
||||
Map.put(child, "summary", "re: " <> child_summary)
|
||||
else
|
||||
child
|
||||
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"])
|
||||
|
||||
child =
|
||||
if(in_reply_to,
|
||||
do: filter_by_summary(in_reply_to.data, child),
|
||||
else: child
|
||||
)
|
||||
|
||||
object = Map.put(object, "object", child)
|
||||
|
||||
{:ok, object}
|
||||
end
|
||||
|
||||
def filter(object), do: {:ok, object}
|
||||
end
|
||||
|
|
@ -23,7 +23,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
|
||||
defp check_media_removal(
|
||||
%{host: actor_host} = _actor_info,
|
||||
%{"type" => "Create", "object" => %{"attachement" => child_attachment}} = object
|
||||
%{"type" => "Create", "object" => %{"attachment" => child_attachment}} = object
|
||||
)
|
||||
when length(child_attachment) > 0 do
|
||||
object =
|
||||
|
|
|
|||
23
lib/pleroma/web/activity_pub/mrf/user_allowlist.ex
Normal file
23
lib/pleroma/web/activity_pub/mrf/user_allowlist.ex
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do
|
||||
alias Pleroma.Config
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF
|
||||
|
||||
defp filter_by_list(object, []), do: {:ok, object}
|
||||
|
||||
defp filter_by_list(%{"actor" => actor} = object, allow_list) do
|
||||
if actor in allow_list do
|
||||
{:ok, object}
|
||||
else
|
||||
{:reject, nil}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(object) do
|
||||
actor_info = URI.parse(object["actor"])
|
||||
allow_list = Config.get([:mrf_user_allowlist, String.to_atom(actor_info.host)], [])
|
||||
|
||||
filter_by_list(object, allow_list)
|
||||
end
|
||||
end
|
||||
|
|
@ -37,9 +37,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
@doc """
|
||||
Checks that an imported AP object's actor matches the domain it came from.
|
||||
"""
|
||||
def contain_origin(id, %{"actor" => nil}), do: :error
|
||||
def contain_origin(_id, %{"actor" => nil}), do: :error
|
||||
|
||||
def contain_origin(id, %{"actor" => actor} = params) do
|
||||
def contain_origin(id, %{"actor" => _actor} = params) do
|
||||
id_uri = URI.parse(id)
|
||||
actor_uri = URI.parse(get_actor(params))
|
||||
|
||||
|
|
@ -50,9 +50,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
end
|
||||
|
||||
def contain_origin_from_id(id, %{"id" => nil}), do: :error
|
||||
def contain_origin_from_id(_id, %{"id" => nil}), do: :error
|
||||
|
||||
def contain_origin_from_id(id, %{"id" => other_id} = params) do
|
||||
def contain_origin_from_id(id, %{"id" => other_id} = _params) do
|
||||
id_uri = URI.parse(id)
|
||||
other_uri = URI.parse(other_id)
|
||||
|
||||
|
|
@ -266,6 +266,32 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
def fix_content_map(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),
|
||||
%Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
|
||||
{:ok, activity}
|
||||
else
|
||||
_ -> {:error, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp mastodon_follow_hack(_, _), do: {:error, nil}
|
||||
|
||||
defp get_follow_activity(follow_object, followed) do
|
||||
with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
|
||||
{_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
|
||||
{:ok, activity}
|
||||
else
|
||||
# Can't find the activity. This might a Mastodon 2.3 "Accept"
|
||||
{:activity, nil} ->
|
||||
mastodon_follow_hack(follow_object, followed)
|
||||
|
||||
_ ->
|
||||
{:error, nil}
|
||||
end
|
||||
end
|
||||
|
||||
# disallow objects with bogus IDs
|
||||
def handle_incoming(%{"id" => nil}), do: :error
|
||||
def handle_incoming(%{"id" => ""}), do: :error
|
||||
|
|
@ -331,34 +357,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
end
|
||||
|
||||
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),
|
||||
%Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
|
||||
{:ok, activity}
|
||||
else
|
||||
_ -> {:error, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp mastodon_follow_hack(_), do: {:error, nil}
|
||||
|
||||
defp get_follow_activity(follow_object, followed) do
|
||||
with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
|
||||
{_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
|
||||
{:ok, activity}
|
||||
else
|
||||
# Can't find the activity. This might a Mastodon 2.3 "Accept"
|
||||
{:activity, nil} ->
|
||||
mastodon_follow_hack(follow_object, followed)
|
||||
|
||||
_ ->
|
||||
{:error, nil}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Accept", "object" => follow_object, "actor" => actor, "id" => id} = data
|
||||
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
|
||||
) do
|
||||
with actor <- get_actor(data),
|
||||
%User{} = followed <- User.get_or_fetch_by_ap_id(actor),
|
||||
|
|
@ -374,7 +374,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
local: false
|
||||
}) do
|
||||
if not User.following?(follower, followed) do
|
||||
{:ok, follower} = User.follow(follower, followed)
|
||||
{:ok, _follower} = User.follow(follower, followed)
|
||||
end
|
||||
|
||||
{:ok, activity}
|
||||
|
|
@ -384,7 +384,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Reject", "object" => follow_object, "actor" => actor, "id" => id} = data
|
||||
%{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
|
||||
) do
|
||||
with actor <- get_actor(data),
|
||||
%User{} = followed <- User.get_or_fetch_by_ap_id(actor),
|
||||
|
|
@ -408,7 +408,7 @@ 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
|
||||
) do
|
||||
with actor <- get_actor(data),
|
||||
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
|
||||
|
|
@ -421,7 +421,7 @@ 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
|
||||
) do
|
||||
with actor <- get_actor(data),
|
||||
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
|
||||
|
|
@ -447,7 +447,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
update_data =
|
||||
new_user_data
|
||||
|> Map.take([:name, :bio, :avatar])
|
||||
|> Map.put(:info, Map.merge(actor.info, %{"banner" => banner, "locked" => locked}))
|
||||
|> Map.put(:info, %{"banner" => banner, "locked" => locked})
|
||||
|
||||
actor
|
||||
|> User.upgrade_changeset(update_data)
|
||||
|
|
@ -492,7 +492,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
%{
|
||||
"type" => "Undo",
|
||||
"object" => %{"type" => "Announce", "object" => object_id},
|
||||
"actor" => actor,
|
||||
"actor" => _actor,
|
||||
"id" => id
|
||||
} = data
|
||||
) do
|
||||
|
|
@ -520,7 +520,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
User.unfollow(follower, followed)
|
||||
{:ok, activity}
|
||||
else
|
||||
e -> :error
|
||||
_e -> :error
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -539,12 +539,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
User.unblock(blocker, blocked)
|
||||
{:ok, activity}
|
||||
else
|
||||
e -> :error
|
||||
_e -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = data
|
||||
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data
|
||||
) do
|
||||
with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
|
||||
%User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
|
||||
|
|
@ -554,7 +554,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
User.block(blocker, blocked)
|
||||
{:ok, activity}
|
||||
else
|
||||
e -> :error
|
||||
_e -> :error
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -562,7 +562,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
%{
|
||||
"type" => "Undo",
|
||||
"object" => %{"type" => "Like", "object" => object_id},
|
||||
"actor" => actor,
|
||||
"actor" => _actor,
|
||||
"id" => id
|
||||
} = data
|
||||
) do
|
||||
|
|
@ -850,10 +850,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
def upgrade_user_from_ap_id(ap_id, async \\ true) do
|
||||
with %User{local: false} = user <- User.get_by_ap_id(ap_id),
|
||||
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do
|
||||
data =
|
||||
data
|
||||
|> Map.put(:info, Map.merge(user.info, data[:info]))
|
||||
|
||||
already_ap = User.ap_enabled?(user)
|
||||
|
||||
{:ok, user} =
|
||||
|
|
|
|||
|
|
@ -292,7 +292,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
"""
|
||||
def make_follow_data(
|
||||
%User{ap_id: follower_id},
|
||||
%User{ap_id: followed_id} = followed,
|
||||
%User{ap_id: followed_id} = _followed,
|
||||
activity_id
|
||||
) do
|
||||
data = %{
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
# the instance itself is not a Person, but instead an Application
|
||||
def render("user.json", %{user: %{nickname: nil} = user}) do
|
||||
{:ok, user} = WebFinger.ensure_keys_present(user)
|
||||
{:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"])
|
||||
{:ok, _, public_key} = Salmon.keys_from_pem(user.info.keys)
|
||||
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
|
||||
public_key = :public_key.pem_encode([public_key])
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
|
||||
def render("user.json", %{user: user}) do
|
||||
{:ok, user} = WebFinger.ensure_keys_present(user)
|
||||
{:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"])
|
||||
{:ok, _, public_key} = Salmon.keys_from_pem(user.info.keys)
|
||||
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
|
||||
public_key = :public_key.pem_encode([public_key])
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
"name" => user.name,
|
||||
"summary" => user.bio,
|
||||
"url" => user.ap_id,
|
||||
"manuallyApprovesFollowers" => user.info["locked"] || false,
|
||||
"manuallyApprovesFollowers" => user.info.locked,
|
||||
"publicKey" => %{
|
||||
"id" => "#{user.ap_id}#main-key",
|
||||
"owner" => user.ap_id,
|
||||
|
|
@ -72,7 +72,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
"type" => "Image",
|
||||
"url" => User.banner_url(user)
|
||||
},
|
||||
"tag" => user.info["source_data"]["tag"] || []
|
||||
"tag" => user.info.source_data["tag"] || []
|
||||
}
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
|
@ -82,7 +82,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
query = from(user in query, select: [:ap_id])
|
||||
following = Repo.all(query)
|
||||
|
||||
collection(following, "#{user.ap_id}/following", page)
|
||||
collection(following, "#{user.ap_id}/following", page, !user.info.hide_network)
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
"id" => "#{user.ap_id}/following",
|
||||
"type" => "OrderedCollection",
|
||||
"totalItems" => length(following),
|
||||
"first" => collection(following, "#{user.ap_id}/following", 1)
|
||||
"first" => collection(following, "#{user.ap_id}/following", 1, !user.info.hide_network)
|
||||
}
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
|
@ -105,7 +105,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
query = from(user in query, select: [:ap_id])
|
||||
followers = Repo.all(query)
|
||||
|
||||
collection(followers, "#{user.ap_id}/followers", page)
|
||||
collection(followers, "#{user.ap_id}/followers", page, !user.info.hide_network)
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
"id" => "#{user.ap_id}/followers",
|
||||
"type" => "OrderedCollection",
|
||||
"totalItems" => length(followers),
|
||||
"first" => collection(followers, "#{user.ap_id}/followers", 1)
|
||||
"first" => collection(followers, "#{user.ap_id}/followers", 1, !user.info.hide_network)
|
||||
}
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
|
@ -172,7 +172,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
end
|
||||
end
|
||||
|
||||
def collection(collection, iri, page, total \\ nil) do
|
||||
def collection(collection, iri, page, show_items \\ true, total \\ nil) do
|
||||
offset = (page - 1) * 10
|
||||
items = Enum.slice(collection, offset, 10)
|
||||
items = Enum.map(items, fn user -> user.ap_id end)
|
||||
|
|
@ -183,7 +183,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
"type" => "OrderedCollectionPage",
|
||||
"partOf" => iri,
|
||||
"totalItems" => total,
|
||||
"orderedItems" => items
|
||||
"orderedItems" => if(show_items, do: items, else: [])
|
||||
}
|
||||
|
||||
if offset < total do
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
alias Pleroma.{User, Repo}
|
||||
alias Pleroma.Web.ActivityPub.Relay
|
||||
|
||||
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
|
||||
|
||||
require Logger
|
||||
|
||||
action_fallback(:errors)
|
||||
|
|
@ -40,26 +42,34 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
|> json(new_user.nickname)
|
||||
end
|
||||
|
||||
def tag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do
|
||||
with {:ok, _} <- User.tag(nicknames, tags),
|
||||
do: json_response(conn, :no_content, "")
|
||||
end
|
||||
|
||||
def untag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do
|
||||
with {:ok, _} <- User.untag(nicknames, tags),
|
||||
do: json_response(conn, :no_content, "")
|
||||
end
|
||||
|
||||
def right_add(conn, %{"permission_group" => permission_group, "nickname" => nickname})
|
||||
when permission_group in ["moderator", "admin"] do
|
||||
user = User.get_by_nickname(nickname)
|
||||
|
||||
info =
|
||||
user.info
|
||||
%{}
|
||||
|> Map.put("is_" <> permission_group, true)
|
||||
|
||||
cng = User.info_changeset(user, %{info: info})
|
||||
{:ok, user} = User.update_and_set_cache(cng)
|
||||
info_cng = User.Info.admin_api_update(user.info, info)
|
||||
|
||||
conn
|
||||
|> json(user.info)
|
||||
end
|
||||
cng =
|
||||
user
|
||||
|> Ecto.Changeset.change()
|
||||
|> Ecto.Changeset.put_embed(:info, info_cng)
|
||||
|
||||
def right_get(conn, %{"nickname" => nickname}) do
|
||||
user = User.get_by_nickname(nickname)
|
||||
{:ok, _user} = User.update_and_set_cache(cng)
|
||||
|
||||
conn
|
||||
|> json(user.info)
|
||||
json(conn, info)
|
||||
end
|
||||
|
||||
def right_add(conn, _) do
|
||||
|
|
@ -68,6 +78,16 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
|> json(%{error: "No such permission_group"})
|
||||
end
|
||||
|
||||
def right_get(conn, %{"nickname" => nickname}) do
|
||||
user = User.get_by_nickname(nickname)
|
||||
|
||||
conn
|
||||
|> json(%{
|
||||
is_moderator: user.info.is_moderator,
|
||||
is_admin: user.info.is_admin
|
||||
})
|
||||
end
|
||||
|
||||
def right_delete(
|
||||
%{assigns: %{user: %User{:nickname => admin_nickname}}} = conn,
|
||||
%{
|
||||
|
|
@ -84,14 +104,18 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
user = User.get_by_nickname(nickname)
|
||||
|
||||
info =
|
||||
user.info
|
||||
%{}
|
||||
|> Map.put("is_" <> permission_group, false)
|
||||
|
||||
cng = User.info_changeset(user, %{info: info})
|
||||
{:ok, user} = User.update_and_set_cache(cng)
|
||||
info_cng = User.Info.admin_api_update(user.info, info)
|
||||
|
||||
conn
|
||||
|> json(user.info)
|
||||
cng =
|
||||
Ecto.Changeset.change(user)
|
||||
|> Ecto.Changeset.put_embed(:info, info_cng)
|
||||
|
||||
{:ok, _user} = User.update_and_set_cache(cng)
|
||||
|
||||
json(conn, info)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -102,32 +126,41 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
end
|
||||
|
||||
def relay_follow(conn, %{"relay_url" => target}) do
|
||||
{status, message} = Relay.follow(target)
|
||||
|
||||
if status == :ok do
|
||||
conn
|
||||
|> json(target)
|
||||
with {:ok, _message} <- Relay.follow(target) do
|
||||
json(conn, target)
|
||||
else
|
||||
conn
|
||||
|> put_status(500)
|
||||
|> json(target)
|
||||
_ ->
|
||||
conn
|
||||
|> put_status(500)
|
||||
|> json(target)
|
||||
end
|
||||
end
|
||||
|
||||
def relay_unfollow(conn, %{"relay_url" => target}) do
|
||||
{status, message} = Relay.unfollow(target)
|
||||
|
||||
if status == :ok do
|
||||
conn
|
||||
|> json(target)
|
||||
with {:ok, _message} <- Relay.unfollow(target) do
|
||||
json(conn, target)
|
||||
else
|
||||
conn
|
||||
|> put_status(500)
|
||||
|> json(target)
|
||||
_ ->
|
||||
conn
|
||||
|> put_status(500)
|
||||
|> json(target)
|
||||
end
|
||||
end
|
||||
|
||||
@shortdoc "Get a account registeration invite token (base64 string)"
|
||||
@doc "Sends registration invite via email"
|
||||
def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
|
||||
with true <-
|
||||
Pleroma.Config.get([:instance, :invites_enabled]) &&
|
||||
!Pleroma.Config.get([:instance, :registrations_open]),
|
||||
{:ok, invite_token} <- Pleroma.UserInviteToken.create_token(),
|
||||
email <-
|
||||
Pleroma.UserEmail.user_invitation_email(user, invite_token, email, params["name"]),
|
||||
{:ok, _} <- Pleroma.Mailer.deliver(email) do
|
||||
json_response(conn, :no_content, "")
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Get a account registeration invite token (base64 string)"
|
||||
def get_invite_token(conn, _params) do
|
||||
{:ok, token} = Pleroma.UserInviteToken.create_token()
|
||||
|
||||
|
|
@ -135,7 +168,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
|> json(token.token)
|
||||
end
|
||||
|
||||
@shortdoc "Get a password reset token (base64 string) for given nickname"
|
||||
@doc "Get a password reset token (base64 string) for given nickname"
|
||||
def get_password_reset(conn, %{"nickname" => nickname}) do
|
||||
(%User{local: true} = user) = User.get_by_nickname(nickname)
|
||||
{:ok, token} = Pleroma.PasswordResetToken.create_token(user)
|
||||
|
|
|
|||
|
|
@ -6,10 +6,6 @@ defmodule Pleroma.Web.UserSocket do
|
|||
# channel "room:*", Pleroma.Web.RoomChannel
|
||||
channel("chat:*", Pleroma.Web.ChatChannel)
|
||||
|
||||
## Transports
|
||||
transport(:websocket, Phoenix.Transports.WebSocket)
|
||||
# transport :longpoll, Phoenix.Transports.LongPoll
|
||||
|
||||
# Socket params are passed from the client and can
|
||||
# be used to verify and authenticate a user. After
|
||||
# verification, you can put default assigns into
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
defmodule Pleroma.Web.CommonAPI do
|
||||
alias Pleroma.{User, Repo, Activity, Object}
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Formatter
|
||||
|
||||
import Pleroma.Web.CommonAPI.Utils
|
||||
|
|
@ -8,7 +9,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
def delete(activity_id, user) do
|
||||
with %Activity{data: %{"object" => %{"id" => object_id}}} <- Repo.get(Activity, activity_id),
|
||||
%Object{} = object <- Object.normalize(object_id),
|
||||
true <- user.info["is_moderator"] || user.ap_id == object.data["actor"],
|
||||
true <- user.info.is_moderator || user.ap_id == object.data["actor"],
|
||||
{:ok, delete} <- ActivityPub.delete(object) do
|
||||
{:ok, delete}
|
||||
end
|
||||
|
|
@ -16,7 +17,8 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
|
||||
def repeat(id_or_ap_id, user) do
|
||||
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
|
||||
object <- Object.normalize(activity.data["object"]["id"]) do
|
||||
object <- Object.normalize(activity.data["object"]["id"]),
|
||||
nil <- Utils.get_existing_announce(user.ap_id, object) do
|
||||
ActivityPub.announce(user, object)
|
||||
else
|
||||
_ ->
|
||||
|
|
@ -36,7 +38,8 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
|
||||
def favorite(id_or_ap_id, user) do
|
||||
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
|
||||
object <- Object.normalize(activity.data["object"]["id"]) do
|
||||
object <- Object.normalize(activity.data["object"]["id"]),
|
||||
nil <- Utils.get_existing_like(user.ap_id, object) do
|
||||
ActivityPub.like(user, object)
|
||||
else
|
||||
_ ->
|
||||
|
|
@ -135,12 +138,13 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
# Updates the emojis for a user based on their profile
|
||||
def update(user) do
|
||||
user =
|
||||
with emoji <- emoji_from_profile(user),
|
||||
source_data <- (user.info["source_data"] || %{}) |> Map.put("tag", emoji),
|
||||
new_info <- Map.put(user.info, "source_data", source_data),
|
||||
change <- User.info_changeset(user, %{info: new_info}),
|
||||
source_data <- (user.info.source_data || %{}) |> Map.put("tag", emoji),
|
||||
info_cng <- Pleroma.User.Info.set_source_data(user.info, source_data),
|
||||
change <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
|
||||
{:ok, user} <- User.update_and_set_cache(change) do
|
||||
user
|
||||
else
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
defmodule Pleroma.Web.CommonAPI.Utils do
|
||||
alias Pleroma.{Repo, Object, Formatter, Activity}
|
||||
alias Calendar.Strftime
|
||||
alias Comeonin.Pbkdf2
|
||||
alias Pleroma.{Activity, Formatter, Object, Repo}
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.MediaProxy
|
||||
alias Pleroma.User
|
||||
alias Calendar.Strftime
|
||||
alias Comeonin.Pbkdf2
|
||||
|
||||
# This is a hack for twidere.
|
||||
def get_by_id_or_ap_id(id) do
|
||||
|
|
@ -111,6 +112,9 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
Enum.join([text | attachment_text], "<br>")
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formatting text to plain text.
|
||||
"""
|
||||
def format_input(text, mentions, tags, "text/plain") do
|
||||
text
|
||||
|> Formatter.html_escape("text/plain")
|
||||
|
|
@ -122,7 +126,10 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
|> Formatter.finalize()
|
||||
end
|
||||
|
||||
def format_input(text, mentions, tags, "text/html") do
|
||||
@doc """
|
||||
Formatting text to html.
|
||||
"""
|
||||
def format_input(text, mentions, _tags, "text/html") do
|
||||
text
|
||||
|> Formatter.html_escape("text/html")
|
||||
|> String.replace(~r/\r?\n/, "<br>")
|
||||
|
|
@ -131,8 +138,12 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
|> Formatter.finalize()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formatting text to markdown.
|
||||
"""
|
||||
def format_input(text, mentions, tags, "text/markdown") do
|
||||
text
|
||||
|> Formatter.mentions_escape(mentions)
|
||||
|> Earmark.as_html!()
|
||||
|> Formatter.html_escape("text/html")
|
||||
|> String.replace(~r/\r?\n/, "")
|
||||
|
|
@ -148,7 +159,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
|> Enum.sort_by(fn {tag, _} -> -String.length(tag) end)
|
||||
|
||||
Enum.reduce(tags, text, fn {full, tag}, text ->
|
||||
url = "<a href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>##{tag}</a>"
|
||||
url = "<a href='#{Web.base_url()}/tag/#{tag}' rel='tag'>##{tag}</a>"
|
||||
String.replace(text, full, url)
|
||||
end)
|
||||
end
|
||||
|
|
@ -236,7 +247,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
end
|
||||
end
|
||||
|
||||
def emoji_from_profile(%{info: info} = user) do
|
||||
def emoji_from_profile(%{info: _info} = user) do
|
||||
(Formatter.get_emoji(user.bio) ++ Formatter.get_emoji(user.name))
|
||||
|> Enum.map(fn {shortcode, url} ->
|
||||
%{
|
||||
|
|
|
|||
9
lib/pleroma/web/controller_helper.ex
Normal file
9
lib/pleroma/web/controller_helper.ex
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
defmodule Pleroma.Web.ControllerHelper do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
def json_response(conn, status, json) do
|
||||
conn
|
||||
|> put_status(status)
|
||||
|> json(json)
|
||||
end
|
||||
end
|
||||
|
|
@ -3,8 +3,6 @@ defmodule Pleroma.Web.Endpoint do
|
|||
|
||||
socket("/socket", Pleroma.Web.UserSocket)
|
||||
|
||||
socket("/api/v1", Pleroma.Web.MastodonAPI.MastodonSocket)
|
||||
|
||||
# Serve at "/" the static files from "priv/static" directory.
|
||||
#
|
||||
# You should set gzip to true if you are running phoenix.digest
|
||||
|
|
@ -12,7 +10,7 @@ defmodule Pleroma.Web.Endpoint do
|
|||
plug(CORSPlug)
|
||||
plug(Pleroma.Plugs.HTTPSecurityPlug)
|
||||
|
||||
plug(Plug.Static, at: "/media", from: Pleroma.Uploaders.Local.upload_path(), gzip: false)
|
||||
plug(Pleroma.Plugs.UploadedMedia)
|
||||
|
||||
plug(
|
||||
Plug.Static,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ defmodule Pleroma.Web.Federator do
|
|||
|
||||
@websub Application.get_env(:pleroma, :websub)
|
||||
@ostatus Application.get_env(:pleroma, :ostatus)
|
||||
@httpoison Application.get_env(:pleroma, :httpoison)
|
||||
@max_jobs 20
|
||||
|
||||
def init(args) do
|
||||
|
|
@ -134,7 +133,7 @@ defmodule Pleroma.Web.Federator do
|
|||
|
||||
def handle(
|
||||
:publish_single_websub,
|
||||
%{xml: xml, topic: topic, callback: callback, secret: secret} = params
|
||||
%{xml: _xml, topic: _topic, callback: _callback, secret: _secret} = params
|
||||
) do
|
||||
case Websub.publish_one(params) do
|
||||
{:ok, _} ->
|
||||
|
|
@ -151,7 +150,7 @@ defmodule Pleroma.Web.Federator do
|
|||
end
|
||||
|
||||
if Mix.env() == :test do
|
||||
def enqueue(type, payload, priority \\ 1) do
|
||||
def enqueue(type, payload, _priority \\ 1) do
|
||||
if Pleroma.Config.get([:instance, :federating]) do
|
||||
handle(type, payload)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
defmodule Pleroma.Web.Federator.RetryQueue do
|
||||
use GenServer
|
||||
alias Pleroma.Web.{WebFinger, Websub}
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
|
||||
require Logger
|
||||
|
||||
@websub Application.get_env(:pleroma, :websub)
|
||||
@ostatus Application.get_env(:pleroma, :websub)
|
||||
@httpoison Application.get_env(:pleroma, :websub)
|
||||
@instance Application.get_env(:pleroma, :websub)
|
||||
# initial timeout, 5 min
|
||||
@initial_timeout 30_000
|
||||
@max_retries 5
|
||||
|
|
@ -17,7 +12,15 @@ defmodule Pleroma.Web.Federator.RetryQueue do
|
|||
end
|
||||
|
||||
def start_link() do
|
||||
GenServer.start_link(__MODULE__, %{delivered: 0, dropped: 0}, name: __MODULE__)
|
||||
enabled = Pleroma.Config.get([:retry_queue, :enabled], false)
|
||||
|
||||
if enabled do
|
||||
Logger.info("Starting retry queue")
|
||||
GenServer.start_link(__MODULE__, %{delivered: 0, dropped: 0}, name: __MODULE__)
|
||||
else
|
||||
Logger.info("Retry queue disabled")
|
||||
:ignore
|
||||
end
|
||||
end
|
||||
|
||||
def enqueue(data, transport, retries \\ 0) do
|
||||
|
|
@ -38,7 +41,7 @@ defmodule Pleroma.Web.Federator.RetryQueue do
|
|||
Process.send_after(
|
||||
__MODULE__,
|
||||
{:send, data, transport, retries},
|
||||
growth_function(retries)
|
||||
timeout
|
||||
)
|
||||
|
||||
{:noreply, state}
|
||||
|
|
@ -54,7 +57,7 @@ defmodule Pleroma.Web.Federator.RetryQueue do
|
|||
{:ok, _} ->
|
||||
{:noreply, %{state | delivered: delivery_count + 1}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, _reason} ->
|
||||
enqueue(data, transport, retries)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ defmodule Pleroma.Web.HTTPSignatures do
|
|||
end
|
||||
|
||||
def sign(user, headers) do
|
||||
with {:ok, %{info: %{"keys" => keys}}} <- Pleroma.Web.WebFinger.ensure_keys_present(user),
|
||||
with {:ok, %{info: %{keys: keys}}} <- Pleroma.Web.WebFinger.ensure_keys_present(user),
|
||||
{:ok, private_key, _} = Pleroma.Web.Salmon.keys_from_pem(keys) do
|
||||
sigstring = build_signing_string(headers, Map.keys(headers))
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
use Pleroma.Web, :controller
|
||||
alias Pleroma.{Repo, Object, Activity, User, Notification, Stats}
|
||||
alias Pleroma.Web
|
||||
alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView, FilterView}
|
||||
|
||||
alias Pleroma.Web.MastodonAPI.{
|
||||
StatusView,
|
||||
AccountView,
|
||||
MastodonView,
|
||||
ListView,
|
||||
FilterView,
|
||||
PushSubscriptionView
|
||||
}
|
||||
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.OAuth.{Authorization, Token, App}
|
||||
alias Pleroma.Web.MediaProxy
|
||||
alias Comeonin.Pbkdf2
|
||||
|
||||
import Ecto.Query
|
||||
require Logger
|
||||
|
||||
|
|
@ -32,75 +41,55 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
end
|
||||
end
|
||||
|
||||
defp add_if_present(
|
||||
map,
|
||||
params,
|
||||
params_field,
|
||||
map_field,
|
||||
value_function \\ fn x -> {:ok, x} end
|
||||
) do
|
||||
if Map.has_key?(params, params_field) do
|
||||
case value_function.(params[params_field]) do
|
||||
{:ok, new_value} -> Map.put(map, map_field, new_value)
|
||||
:error -> map
|
||||
end
|
||||
else
|
||||
map
|
||||
end
|
||||
end
|
||||
|
||||
def update_credentials(%{assigns: %{user: user}} = conn, params) do
|
||||
original_user = user
|
||||
|
||||
avatar_upload_limit =
|
||||
Application.get_env(:pleroma, :instance)
|
||||
|> Keyword.fetch(:avatar_upload_limit)
|
||||
|
||||
banner_upload_limit =
|
||||
Application.get_env(:pleroma, :instance)
|
||||
|> Keyword.fetch(:banner_upload_limit)
|
||||
|
||||
params =
|
||||
if bio = params["note"] do
|
||||
Map.put(params, "bio", bio)
|
||||
else
|
||||
params
|
||||
end
|
||||
|
||||
params =
|
||||
if name = params["display_name"] do
|
||||
Map.put(params, "name", name)
|
||||
else
|
||||
params
|
||||
end
|
||||
|
||||
user =
|
||||
if avatar = params["avatar"] do
|
||||
with %Plug.Upload{} <- avatar,
|
||||
{:ok, object} <- ActivityPub.upload(avatar, avatar_upload_limit),
|
||||
change = Ecto.Changeset.change(user, %{avatar: object.data}),
|
||||
{:ok, user} = User.update_and_set_cache(change) do
|
||||
user
|
||||
user_params =
|
||||
%{}
|
||||
|> add_if_present(params, "display_name", :name)
|
||||
|> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
|
||||
|> add_if_present(params, "avatar", :avatar, fn value ->
|
||||
with %Plug.Upload{} <- value,
|
||||
{:ok, object} <- ActivityPub.upload(value, type: :avatar) do
|
||||
{:ok, object.data}
|
||||
else
|
||||
_e -> user
|
||||
_ -> :error
|
||||
end
|
||||
else
|
||||
user
|
||||
end
|
||||
end)
|
||||
|
||||
user =
|
||||
if banner = params["header"] do
|
||||
with %Plug.Upload{} <- banner,
|
||||
{:ok, object} <- ActivityPub.upload(banner, banner_upload_limit),
|
||||
new_info <- Map.put(user.info, "banner", object.data),
|
||||
change <- User.info_changeset(user, %{info: new_info}),
|
||||
{:ok, user} <- User.update_and_set_cache(change) do
|
||||
user
|
||||
info_params =
|
||||
%{}
|
||||
|> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
|
||||
|> add_if_present(params, "header", :banner, fn value ->
|
||||
with %Plug.Upload{} <- value,
|
||||
{:ok, object} <- ActivityPub.upload(value, type: :banner) do
|
||||
{:ok, object.data}
|
||||
else
|
||||
_e -> user
|
||||
_ -> :error
|
||||
end
|
||||
else
|
||||
user
|
||||
end
|
||||
end)
|
||||
|
||||
user =
|
||||
if locked = params["locked"] do
|
||||
with locked <- locked == "true",
|
||||
new_info <- Map.put(user.info, "locked", locked),
|
||||
change <- User.info_changeset(user, %{info: new_info}),
|
||||
{:ok, user} <- User.update_and_set_cache(change) do
|
||||
user
|
||||
else
|
||||
_e -> user
|
||||
end
|
||||
else
|
||||
user
|
||||
end
|
||||
info_cng = User.Info.mastodon_profile_update(user.info, info_params)
|
||||
|
||||
with changeset <- User.update_changeset(user, params),
|
||||
with changeset <- User.update_changeset(user, user_params),
|
||||
changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
|
||||
{:ok, user} <- User.update_and_set_cache(changeset) do
|
||||
if original_user != user do
|
||||
CommonAPI.update(user)
|
||||
|
|
@ -237,7 +226,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
|
||||
conn
|
||||
|> add_link_headers(:home_timeline, activities)
|
||||
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
|
||||
|> put_view(StatusView)
|
||||
|> render("index.json", %{activities: activities, for: user, as: :activity})
|
||||
end
|
||||
|
||||
def public_timeline(%{assigns: %{user: user}} = conn, params) do
|
||||
|
|
@ -255,7 +245,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
|
||||
conn
|
||||
|> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
|
||||
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
|
||||
|> put_view(StatusView)
|
||||
|> render("index.json", %{activities: activities, for: user, as: :activity})
|
||||
end
|
||||
|
||||
def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
|
||||
|
|
@ -270,7 +261,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
|
||||
conn
|
||||
|> add_link_headers(:user_statuses, activities, params["id"])
|
||||
|> render(StatusView, "index.json", %{
|
||||
|> put_view(StatusView)
|
||||
|> render("index.json", %{
|
||||
activities: activities,
|
||||
for: reading_user,
|
||||
as: :activity
|
||||
|
|
@ -289,13 +281,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
|
||||
conn
|
||||
|> add_link_headers(:dm_timeline, activities)
|
||||
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
|
||||
|> put_view(StatusView)
|
||||
|> render("index.json", %{activities: activities, for: user, as: :activity})
|
||||
end
|
||||
|
||||
def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||
with %Activity{} = activity <- Repo.get(Activity, id),
|
||||
true <- ActivityPub.visible_for_user?(activity, user) do
|
||||
try_render(conn, StatusView, "status.json", %{activity: activity, for: user})
|
||||
conn
|
||||
|> put_view(StatusView)
|
||||
|> try_render("status.json", %{activity: activity, for: user})
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -358,7 +353,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
{:ok, activity} =
|
||||
Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
|
||||
|
||||
try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
|
||||
conn
|
||||
|> put_view(StatusView)
|
||||
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
|
||||
end
|
||||
|
||||
def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||
|
|
@ -374,28 +371,36 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
|
||||
def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
|
||||
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
|
||||
try_render(conn, StatusView, "status.json", %{activity: announce, for: user, as: :activity})
|
||||
conn
|
||||
|> put_view(StatusView)
|
||||
|> try_render("status.json", %{activity: announce, for: user, as: :activity})
|
||||
end
|
||||
end
|
||||
|
||||
def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
|
||||
with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
|
||||
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
|
||||
try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
|
||||
conn
|
||||
|> put_view(StatusView)
|
||||
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
|
||||
end
|
||||
end
|
||||
|
||||
def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
|
||||
with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
|
||||
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
|
||||
try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
|
||||
conn
|
||||
|> put_view(StatusView)
|
||||
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
|
||||
end
|
||||
end
|
||||
|
||||
def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
|
||||
with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
|
||||
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
|
||||
try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
|
||||
conn
|
||||
|> put_view(StatusView)
|
||||
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -444,49 +449,46 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
id = List.wrap(id)
|
||||
q = from(u in User, where: u.id in ^id)
|
||||
targets = Repo.all(q)
|
||||
render(conn, AccountView, "relationships.json", %{user: user, targets: targets})
|
||||
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("relationships.json", %{user: user, targets: targets})
|
||||
end
|
||||
|
||||
# Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
|
||||
def relationships(%{assigns: %{user: user}} = conn, _) do
|
||||
conn
|
||||
|> json([])
|
||||
end
|
||||
def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
|
||||
|
||||
def update_media(%{assigns: %{user: _}} = conn, data) do
|
||||
def update_media(%{assigns: %{user: user}} = conn, data) do
|
||||
with %Object{} = object <- Repo.get(Object, data["id"]),
|
||||
true <- Object.authorize_mutation(object, user),
|
||||
true <- is_binary(data["description"]),
|
||||
description <- data["description"] do
|
||||
new_data = %{object.data | "name" => description}
|
||||
|
||||
change = Object.change(object, %{data: new_data})
|
||||
{:ok, _} = Repo.update(change)
|
||||
{:ok, _} =
|
||||
object
|
||||
|> Object.change(%{data: new_data})
|
||||
|> Repo.update()
|
||||
|
||||
data =
|
||||
new_data
|
||||
|> Map.put("id", object.id)
|
||||
attachment_data = Map.put(new_data, "id", object.id)
|
||||
|
||||
render(conn, StatusView, "attachment.json", %{attachment: data})
|
||||
conn
|
||||
|> put_view(StatusView)
|
||||
|> render("attachment.json", %{attachment: attachment_data})
|
||||
end
|
||||
end
|
||||
|
||||
def upload(%{assigns: %{user: _}} = conn, %{"file" => file} = data) do
|
||||
with {:ok, object} <- ActivityPub.upload(file) do
|
||||
objdata =
|
||||
if Map.has_key?(data, "description") do
|
||||
Map.put(object.data, "name", data["description"])
|
||||
else
|
||||
object.data
|
||||
end
|
||||
def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
|
||||
with {:ok, object} <-
|
||||
ActivityPub.upload(file,
|
||||
actor: User.ap_id(user),
|
||||
description: Map.get(data, "description")
|
||||
) do
|
||||
attachment_data = Map.put(object.data, "id", object.id)
|
||||
|
||||
change = Object.change(object, %{data: objdata})
|
||||
{:ok, object} = Repo.update(change)
|
||||
|
||||
objdata =
|
||||
objdata
|
||||
|> Map.put("id", object.id)
|
||||
|
||||
render(conn, StatusView, "attachment.json", %{attachment: objdata})
|
||||
conn
|
||||
|> put_view(StatusView)
|
||||
|> render("attachment.json", %{attachment: attachment_data})
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -494,7 +496,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
|
||||
q = from(u in User, where: u.ap_id in ^likes)
|
||||
users = Repo.all(q)
|
||||
render(conn, AccountView, "accounts.json", %{users: users, as: :user})
|
||||
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render(AccountView, "accounts.json", %{users: users, as: :user})
|
||||
else
|
||||
_ -> json(conn, [])
|
||||
end
|
||||
|
|
@ -504,7 +509,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
|
||||
q = from(u in User, where: u.ap_id in ^announces)
|
||||
users = Repo.all(q)
|
||||
render(conn, AccountView, "accounts.json", %{users: users, as: :user})
|
||||
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("accounts.json", %{users: users, as: :user})
|
||||
else
|
||||
_ -> json(conn, [])
|
||||
end
|
||||
|
|
@ -526,27 +534,47 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
|
||||
conn
|
||||
|> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
|
||||
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
|
||||
|> put_view(StatusView)
|
||||
|> render("index.json", %{activities: activities, for: user, as: :activity})
|
||||
end
|
||||
|
||||
# TODO: Pagination
|
||||
def followers(conn, %{"id" => id}) do
|
||||
def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
|
||||
with %User{} = user <- Repo.get(User, id),
|
||||
{:ok, followers} <- User.get_followers(user) do
|
||||
render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
|
||||
followers =
|
||||
cond do
|
||||
for_user && user.id == for_user.id -> followers
|
||||
user.info.hide_network -> []
|
||||
true -> followers
|
||||
end
|
||||
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("accounts.json", %{users: followers, as: :user})
|
||||
end
|
||||
end
|
||||
|
||||
def following(conn, %{"id" => id}) do
|
||||
def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
|
||||
with %User{} = user <- Repo.get(User, id),
|
||||
{:ok, followers} <- User.get_friends(user) do
|
||||
render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
|
||||
followers =
|
||||
cond do
|
||||
for_user && user.id == for_user.id -> followers
|
||||
user.info.hide_network -> []
|
||||
true -> followers
|
||||
end
|
||||
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("accounts.json", %{users: followers, as: :user})
|
||||
end
|
||||
end
|
||||
|
||||
def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
|
||||
with {:ok, follow_requests} <- User.get_follow_requests(followed) do
|
||||
render(conn, AccountView, "accounts.json", %{users: follow_requests, as: :user})
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("accounts.json", %{users: follow_requests, as: :user})
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -562,7 +590,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
object: follow_activity.data["id"],
|
||||
type: "Accept"
|
||||
}) do
|
||||
render(conn, AccountView, "relationship.json", %{user: followed, target: follower})
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("relationship.json", %{user: followed, target: follower})
|
||||
else
|
||||
{:error, message} ->
|
||||
conn
|
||||
|
|
@ -582,7 +612,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
object: follow_activity.data["id"],
|
||||
type: "Reject"
|
||||
}) do
|
||||
render(conn, AccountView, "relationship.json", %{user: followed, target: follower})
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("relationship.json", %{user: followed, target: follower})
|
||||
else
|
||||
{:error, message} ->
|
||||
conn
|
||||
|
|
@ -601,7 +633,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
follower,
|
||||
followed
|
||||
) do
|
||||
render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("relationship.json", %{user: follower, target: followed})
|
||||
else
|
||||
{:error, message} ->
|
||||
conn
|
||||
|
|
@ -614,7 +648,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
with %User{} = followed <- Repo.get_by(User, nickname: uri),
|
||||
{:ok, follower} <- User.maybe_direct_follow(follower, followed),
|
||||
{:ok, _activity} <- ActivityPub.follow(follower, followed) do
|
||||
render(conn, AccountView, "account.json", %{user: followed, for: follower})
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("account.json", %{user: followed, for: follower})
|
||||
else
|
||||
{:error, message} ->
|
||||
conn
|
||||
|
|
@ -627,7 +663,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
with %User{} = followed <- Repo.get(User, id),
|
||||
{:ok, _activity} <- ActivityPub.unfollow(follower, followed),
|
||||
{:ok, follower, _} <- User.unfollow(follower, followed) do
|
||||
render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("relationship.json", %{user: follower, target: followed})
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -635,7 +673,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
with %User{} = blocked <- Repo.get(User, id),
|
||||
{:ok, blocker} <- User.block(blocker, blocked),
|
||||
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
|
||||
render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("relationship.json", %{user: blocker, target: blocked})
|
||||
else
|
||||
{:error, message} ->
|
||||
conn
|
||||
|
|
@ -648,7 +688,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
with %User{} = blocked <- Repo.get(User, id),
|
||||
{:ok, blocker} <- User.unblock(blocker, blocked),
|
||||
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
|
||||
render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("relationship.json", %{user: blocker, target: blocked})
|
||||
else
|
||||
{:error, message} ->
|
||||
conn
|
||||
|
|
@ -659,7 +701,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
|
||||
# TODO: Use proper query
|
||||
def blocks(%{assigns: %{user: user}} = conn, _) do
|
||||
with blocked_users <- user.info["blocks"] || [],
|
||||
with blocked_users <- user.info.blocks || [],
|
||||
accounts <- Enum.map(blocked_users, fn ap_id -> User.get_cached_by_ap_id(ap_id) end) do
|
||||
res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
|
||||
json(conn, res)
|
||||
|
|
@ -667,7 +709,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
end
|
||||
|
||||
def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
|
||||
json(conn, info["domain_blocks"] || [])
|
||||
json(conn, info.domain_blocks || [])
|
||||
end
|
||||
|
||||
def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
|
||||
|
|
@ -773,7 +815,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
|> Enum.reverse()
|
||||
|
||||
conn
|
||||
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
|
||||
|> put_view(StatusView)
|
||||
|> render("index.json", %{activities: activities, for: user, as: :activity})
|
||||
end
|
||||
|
||||
def get_lists(%{assigns: %{user: user}} = conn, opts) do
|
||||
|
|
@ -841,7 +884,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
|
||||
{:ok, users} = Pleroma.List.get_following(list) do
|
||||
render(conn, AccountView, "accounts.json", %{users: users, as: :user})
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("accounts.json", %{users: users, as: :user})
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -857,7 +902,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
end
|
||||
|
||||
def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
|
||||
with %Pleroma.List{title: title, following: following} <- Pleroma.List.get(id, user) do
|
||||
with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
|
||||
params =
|
||||
params
|
||||
|> Map.put("type", "Create")
|
||||
|
|
@ -874,7 +919,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
|> Enum.reverse()
|
||||
|
||||
conn
|
||||
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
|
||||
|> put_view(StatusView)
|
||||
|> render("index.json", %{activities: activities, for: user, as: :activity})
|
||||
else
|
||||
_e ->
|
||||
conn
|
||||
|
|
@ -915,11 +961,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
max_toot_chars: limit
|
||||
},
|
||||
rights: %{
|
||||
delete_others_notice: !!user.info["is_moderator"]
|
||||
delete_others_notice: !!user.info.is_moderator
|
||||
},
|
||||
compose: %{
|
||||
me: "#{user.id}",
|
||||
default_privacy: user.info["default_scope"] || "public",
|
||||
default_privacy: user.info.default_scope,
|
||||
default_sensitive: false
|
||||
},
|
||||
media_attachments: %{
|
||||
|
|
@ -939,7 +985,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
]
|
||||
},
|
||||
settings:
|
||||
Map.get(user.info, "settings") ||
|
||||
user.info.settings ||
|
||||
%{
|
||||
onboarded: true,
|
||||
home: %{
|
||||
|
|
@ -978,7 +1024,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
|
||||
conn
|
||||
|> put_layout(false)
|
||||
|> render(MastodonView, "index.html", %{initial_state: initial_state})
|
||||
|> put_view(MastodonView)
|
||||
|> render("index.html", %{initial_state: initial_state})
|
||||
else
|
||||
conn
|
||||
|> redirect(to: "/web/login")
|
||||
|
|
@ -986,15 +1033,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
end
|
||||
|
||||
def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
|
||||
with new_info <- Map.put(user.info, "settings", settings),
|
||||
change <- User.info_changeset(user, %{info: new_info}),
|
||||
{:ok, _user} <- User.update_and_set_cache(change) do
|
||||
conn
|
||||
|> json(%{})
|
||||
info_cng = User.Info.mastodon_settings_update(user.info, settings)
|
||||
|
||||
with changeset <- Ecto.Changeset.change(user),
|
||||
changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
|
||||
{:ok, _user} <- User.update_and_set_cache(changeset) do
|
||||
json(conn, %{})
|
||||
else
|
||||
e ->
|
||||
conn
|
||||
|> json(%{error: inspect(e)})
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -1049,7 +1098,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
Logger.debug("Unimplemented, returning unmodified relationship")
|
||||
|
||||
with %User{} = target <- Repo.get(User, id) do
|
||||
render(conn, AccountView, "relationship.json", %{user: user, target: target})
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("relationship.json", %{user: user, target: target})
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -1065,52 +1116,37 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
|
||||
def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
|
||||
actor = User.get_cached_by_ap_id(activity.data["actor"])
|
||||
parent_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
|
||||
mastodon_type = Activity.mastodon_notification_type(activity)
|
||||
|
||||
created_at =
|
||||
NaiveDateTime.to_iso8601(created_at)
|
||||
|> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)
|
||||
response = %{
|
||||
id: to_string(id),
|
||||
type: mastodon_type,
|
||||
created_at: CommonAPI.Utils.to_masto_date(created_at),
|
||||
account: AccountView.render("account.json", %{user: actor, for: user})
|
||||
}
|
||||
|
||||
id = id |> to_string
|
||||
|
||||
case activity.data["type"] do
|
||||
"Create" ->
|
||||
%{
|
||||
id: id,
|
||||
type: "mention",
|
||||
created_at: created_at,
|
||||
account: AccountView.render("account.json", %{user: actor, for: user}),
|
||||
case mastodon_type do
|
||||
"mention" ->
|
||||
response
|
||||
|> Map.merge(%{
|
||||
status: StatusView.render("status.json", %{activity: activity, for: user})
|
||||
}
|
||||
})
|
||||
|
||||
"Like" ->
|
||||
liked_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
|
||||
"favourite" ->
|
||||
response
|
||||
|> Map.merge(%{
|
||||
status: StatusView.render("status.json", %{activity: parent_activity, for: user})
|
||||
})
|
||||
|
||||
%{
|
||||
id: id,
|
||||
type: "favourite",
|
||||
created_at: created_at,
|
||||
account: AccountView.render("account.json", %{user: actor, for: user}),
|
||||
status: StatusView.render("status.json", %{activity: liked_activity, for: user})
|
||||
}
|
||||
"reblog" ->
|
||||
response
|
||||
|> Map.merge(%{
|
||||
status: StatusView.render("status.json", %{activity: parent_activity, for: user})
|
||||
})
|
||||
|
||||
"Announce" ->
|
||||
announced_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
|
||||
|
||||
%{
|
||||
id: id,
|
||||
type: "reblog",
|
||||
created_at: created_at,
|
||||
account: AccountView.render("account.json", %{user: actor, for: user}),
|
||||
status: StatusView.render("status.json", %{activity: announced_activity, for: user})
|
||||
}
|
||||
|
||||
"Follow" ->
|
||||
%{
|
||||
id: id,
|
||||
type: "follow",
|
||||
created_at: created_at,
|
||||
account: AccountView.render("account.json", %{user: actor, for: user})
|
||||
}
|
||||
"follow" ->
|
||||
response
|
||||
|
||||
_ ->
|
||||
nil
|
||||
|
|
@ -1176,6 +1212,37 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
json(conn, %{})
|
||||
end
|
||||
|
||||
def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
|
||||
true = Pleroma.Web.Push.enabled()
|
||||
Pleroma.Web.Push.Subscription.delete_if_exists(user, token)
|
||||
{:ok, subscription} = Pleroma.Web.Push.Subscription.create(user, token, params)
|
||||
view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
|
||||
json(conn, view)
|
||||
end
|
||||
|
||||
def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
|
||||
true = Pleroma.Web.Push.enabled()
|
||||
subscription = Pleroma.Web.Push.Subscription.get(user, token)
|
||||
view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
|
||||
json(conn, view)
|
||||
end
|
||||
|
||||
def update_push_subscription(
|
||||
%{assigns: %{user: user, token: token}} = conn,
|
||||
params
|
||||
) do
|
||||
true = Pleroma.Web.Push.enabled()
|
||||
{:ok, subscription} = Pleroma.Web.Push.Subscription.update(user, token, params)
|
||||
view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
|
||||
json(conn, view)
|
||||
end
|
||||
|
||||
def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
|
||||
true = Pleroma.Web.Push.enabled()
|
||||
{:ok, _response} = Pleroma.Web.Push.Subscription.delete(user, token)
|
||||
json(conn, %{})
|
||||
end
|
||||
|
||||
def errors(conn, _) do
|
||||
conn
|
||||
|> put_status(500)
|
||||
|
|
@ -1195,8 +1262,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
user = user.nickname
|
||||
url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user)
|
||||
|
||||
with {:ok, %{status_code: 200, body: body}} <-
|
||||
@httpoison.get(url, [], timeout: timeout, recv_timeout: timeout),
|
||||
with {:ok, %{status: 200, body: body}} <-
|
||||
@httpoison.get(
|
||||
url,
|
||||
[],
|
||||
adapter: [
|
||||
timeout: timeout,
|
||||
recv_timeout: timeout
|
||||
]
|
||||
),
|
||||
{:ok, data} <- Jason.decode(body) do
|
||||
data2 =
|
||||
Enum.slice(data, 0, limit)
|
||||
|
|
@ -1227,9 +1301,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
end
|
||||
end
|
||||
|
||||
def try_render(conn, renderer, target, params)
|
||||
def try_render(conn, target, params)
|
||||
when is_binary(target) do
|
||||
res = render(conn, renderer, target, params)
|
||||
res = render(conn, target, params)
|
||||
|
||||
if res == nil do
|
||||
conn
|
||||
|
|
@ -1240,7 +1314,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
end
|
||||
end
|
||||
|
||||
def try_render(conn, _, _, _) do
|
||||
def try_render(conn, _, _) do
|
||||
conn
|
||||
|> put_status(501)
|
||||
|> json(%{error: "Can't display this activity"})
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.MastodonSocket do
|
||||
use Phoenix.Socket
|
||||
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
alias Pleroma.{User, Repo}
|
||||
|
||||
transport(
|
||||
:streaming,
|
||||
Phoenix.Transports.WebSocket.Raw,
|
||||
# We never receive data.
|
||||
timeout: :infinity
|
||||
)
|
||||
|
||||
def connect(%{"access_token" => token} = params, socket) do
|
||||
with %Token{user_id: user_id} <- Repo.get_by(Token, token: token),
|
||||
%User{} = user <- Repo.get(User, user_id),
|
||||
stream
|
||||
when stream in [
|
||||
"public",
|
||||
"public:local",
|
||||
"public:media",
|
||||
"public:local:media",
|
||||
"user",
|
||||
"direct",
|
||||
"list",
|
||||
"hashtag"
|
||||
] <- params["stream"] do
|
||||
topic =
|
||||
case stream do
|
||||
"hashtag" -> "hashtag:#{params["tag"]}"
|
||||
"list" -> "list:#{params["list"]}"
|
||||
_ -> stream
|
||||
end
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:topic, topic)
|
||||
|> assign(:user, user)
|
||||
|
||||
Pleroma.Web.Streamer.add_socket(topic, socket)
|
||||
{:ok, socket}
|
||||
else
|
||||
_e -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def connect(%{"stream" => stream} = params, socket)
|
||||
when stream in ["public", "public:local", "hashtag"] do
|
||||
topic =
|
||||
case stream do
|
||||
"hashtag" -> "hashtag:#{params["tag"]}"
|
||||
_ -> stream
|
||||
end
|
||||
|
||||
with socket =
|
||||
socket
|
||||
|> assign(:topic, topic) do
|
||||
Pleroma.Web.Streamer.add_socket(topic, socket)
|
||||
{:ok, socket}
|
||||
else
|
||||
_e -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def id(_), do: nil
|
||||
|
||||
def handle(:text, message, _state) do
|
||||
# | :ok
|
||||
# | state
|
||||
# | {:text, message}
|
||||
# | {:text, message, state}
|
||||
# | {:close, "Goodbye!"}
|
||||
{:text, message}
|
||||
end
|
||||
|
||||
def handle(:closed, _, %{socket: socket}) do
|
||||
topic = socket.assigns[:topic]
|
||||
Pleroma.Web.Streamer.remove_socket(topic, socket)
|
||||
end
|
||||
end
|
||||
|
|
@ -14,10 +14,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
image = User.avatar_url(user) |> MediaProxy.url()
|
||||
header = User.banner_url(user) |> MediaProxy.url()
|
||||
user_info = User.user_info(user)
|
||||
bot = (user.info["source_data"]["type"] || "Person") in ["Application", "Service"]
|
||||
bot = (user.info.source_data["type"] || "Person") in ["Application", "Service"]
|
||||
|
||||
emojis =
|
||||
(user.info["source_data"]["tag"] || [])
|
||||
(user.info.source_data["tag"] || [])
|
||||
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|
||||
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
|
||||
%{
|
||||
|
|
@ -29,7 +29,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
end)
|
||||
|
||||
fields =
|
||||
(user.info["source_data"]["attachment"] || [])
|
||||
(user.info.source_data["attachment"] || [])
|
||||
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|
||||
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
|
||||
|
||||
|
|
@ -58,6 +58,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
note: "",
|
||||
privacy: user_info.default_scope,
|
||||
sensitive: false
|
||||
},
|
||||
|
||||
# Pleroma extension
|
||||
pleroma: %{
|
||||
tags: user.tags
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.MastodonView do
|
||||
use Pleroma.Web, :view
|
||||
import Phoenix.HTML
|
||||
import Phoenix.HTML.Form
|
||||
end
|
||||
|
|
|
|||
16
lib/pleroma/web/mastodon_api/views/push_subscription_view.ex
Normal file
16
lib/pleroma/web/mastodon_api/views/push_subscription_view.ex
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.PushSubscriptionView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
def render("push_subscription.json", %{subscription: subscription}) do
|
||||
%{
|
||||
id: to_string(subscription.id),
|
||||
endpoint: subscription.endpoint,
|
||||
alerts: Map.get(subscription.data, "alerts"),
|
||||
server_key: server_key()
|
||||
}
|
||||
end
|
||||
|
||||
defp server_key do
|
||||
Keyword.get(Application.get_env(:web_push_encryption, :vapid_details), :public_key)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,18 +1,21 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.StatusView do
|
||||
use Pleroma.Web, :view
|
||||
alias Pleroma.Web.MastodonAPI.{AccountView, StatusView}
|
||||
alias Pleroma.{User, Activity}
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.MediaProxy
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
|
||||
# TODO: Add cached version.
|
||||
defp get_replied_to_activities(activities) do
|
||||
activities
|
||||
|> Enum.map(fn
|
||||
%{data: %{"type" => "Create", "object" => %{"inReplyTo" => inReplyTo}}} ->
|
||||
inReplyTo != "" && inReplyTo
|
||||
%{data: %{"type" => "Create", "object" => %{"inReplyTo" => in_reply_to}}} ->
|
||||
in_reply_to != "" && in_reply_to
|
||||
|
||||
_ ->
|
||||
nil
|
||||
|
|
@ -28,8 +31,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
def render("index.json", opts) do
|
||||
replied_to_activities = get_replied_to_activities(opts.activities)
|
||||
|
||||
render_many(
|
||||
opts.activities,
|
||||
opts.activities
|
||||
|> render_many(
|
||||
StatusView,
|
||||
"status.json",
|
||||
Map.put(opts, :replied_to_activities, replied_to_activities)
|
||||
|
|
@ -72,9 +75,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
sensitive: false,
|
||||
spoiler_text: "",
|
||||
visibility: "public",
|
||||
media_attachments: [],
|
||||
media_attachments: reblogged[:media_attachments] || [],
|
||||
mentions: mentions,
|
||||
tags: [],
|
||||
tags: reblogged[:tags] || [],
|
||||
application: %{
|
||||
name: "Web",
|
||||
website: nil
|
||||
|
|
@ -111,20 +114,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
reply_to = get_reply_to(activity, opts)
|
||||
reply_to_user = reply_to && User.get_cached_by_ap_id(reply_to.data["actor"])
|
||||
|
||||
emojis =
|
||||
(activity.data["object"]["emoji"] || [])
|
||||
|> Enum.map(fn {name, url} ->
|
||||
name = HTML.strip_tags(name)
|
||||
|
||||
url =
|
||||
HTML.strip_tags(url)
|
||||
|> MediaProxy.url()
|
||||
|
||||
%{shortcode: name, url: url, static_url: url, visible_in_picker: false}
|
||||
end)
|
||||
|
||||
content =
|
||||
render_content(object)
|
||||
object
|
||||
|> render_content()
|
||||
|> HTML.filter_tags(User.html_filter_policy(opts[:for]))
|
||||
|
||||
%{
|
||||
|
|
@ -140,22 +132,21 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
reblogs_count: announcement_count,
|
||||
replies_count: 0,
|
||||
favourites_count: like_count,
|
||||
reblogged: !!repeated,
|
||||
favourited: !!favorited,
|
||||
reblogged: present?(repeated),
|
||||
favourited: present?(favorited),
|
||||
muted: false,
|
||||
sensitive: sensitive,
|
||||
spoiler_text: object["summary"] || "",
|
||||
visibility: get_visibility(object),
|
||||
media_attachments: attachments |> Enum.take(4),
|
||||
mentions: mentions,
|
||||
# fix,
|
||||
tags: [],
|
||||
tags: build_tags(tags),
|
||||
application: %{
|
||||
name: "Web",
|
||||
website: nil
|
||||
},
|
||||
language: nil,
|
||||
emojis: emojis
|
||||
emojis: build_emojis(activity.data["object"]["emoji"])
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -224,30 +215,77 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
end
|
||||
|
||||
def render_content(%{"type" => "Video"} = object) do
|
||||
name = object["name"]
|
||||
|
||||
content =
|
||||
if !!name and name != "" do
|
||||
"<p><a href=\"#{object["id"]}\">#{name}</a></p>#{object["content"]}"
|
||||
else
|
||||
object["content"] || ""
|
||||
end
|
||||
|
||||
content
|
||||
with name when not is_nil(name) and name != "" <- object["name"] do
|
||||
"<p><a href=\"#{object["id"]}\">#{name}</a></p>#{object["content"]}"
|
||||
else
|
||||
_ -> object["content"] || ""
|
||||
end
|
||||
end
|
||||
|
||||
def render_content(%{"type" => object_type} = object) when object_type in ["Article", "Page"] do
|
||||
summary = object["name"]
|
||||
|
||||
content =
|
||||
if !!summary and summary != "" and is_bitstring(object["url"]) do
|
||||
"<p><a href=\"#{object["url"]}\">#{summary}</a></p>#{object["content"]}"
|
||||
else
|
||||
object["content"] || ""
|
||||
end
|
||||
|
||||
content
|
||||
def render_content(%{"type" => object_type} = object)
|
||||
when object_type in ["Article", "Page"] do
|
||||
with summary when not is_nil(summary) and summary != "" <- object["name"],
|
||||
url when is_bitstring(url) <- object["url"] do
|
||||
"<p><a href=\"#{url}\">#{summary}</a></p>#{object["content"]}"
|
||||
else
|
||||
_ -> object["content"] || ""
|
||||
end
|
||||
end
|
||||
|
||||
def render_content(object), do: object["content"] || ""
|
||||
|
||||
@doc """
|
||||
Builds a dictionary tags.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
|
||||
[{"name": "fediverse", "url": "/tag/fediverse"},
|
||||
{"name": "nextcloud", "url": "/tag/nextcloud"}]
|
||||
|
||||
"""
|
||||
@spec build_tags(list(any())) :: list(map())
|
||||
def build_tags(object_tags) when is_list(object_tags) do
|
||||
object_tags = for tag when is_binary(tag) <- object_tags, do: tag
|
||||
|
||||
Enum.reduce(object_tags, [], fn tag, tags ->
|
||||
tags ++ [%{name: tag, url: "/tag/#{tag}"}]
|
||||
end)
|
||||
end
|
||||
|
||||
def build_tags(_), do: []
|
||||
|
||||
@doc """
|
||||
Builds list emojis.
|
||||
|
||||
Arguments: `nil` or list tuple of name and url.
|
||||
|
||||
Returns list emojis.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
|
||||
[%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
|
||||
|
||||
"""
|
||||
@spec build_emojis(nil | list(tuple())) :: list(map())
|
||||
def build_emojis(nil), do: []
|
||||
|
||||
def build_emojis(emojis) do
|
||||
emojis
|
||||
|> Enum.map(fn {name, url} ->
|
||||
name = HTML.strip_tags(name)
|
||||
|
||||
url =
|
||||
url
|
||||
|> HTML.strip_tags()
|
||||
|> MediaProxy.url()
|
||||
|
||||
%{shortcode: name, url: url, static_url: url, visible_in_picker: false}
|
||||
end)
|
||||
end
|
||||
|
||||
defp present?(nil), do: false
|
||||
defp present?(false), do: false
|
||||
defp present?(_), do: true
|
||||
end
|
||||
|
|
|
|||
120
lib/pleroma/web/mastodon_api/websocket_handler.ex
Normal file
120
lib/pleroma/web/mastodon_api/websocket_handler.ex
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
|
||||
require Logger
|
||||
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
alias Pleroma.{User, Repo}
|
||||
|
||||
@behaviour :cowboy_websocket_handler
|
||||
|
||||
@streams [
|
||||
"public",
|
||||
"public:local",
|
||||
"public:media",
|
||||
"public:local:media",
|
||||
"user",
|
||||
"direct",
|
||||
"list",
|
||||
"hashtag"
|
||||
]
|
||||
@anonymous_streams ["public", "public:local", "hashtag"]
|
||||
|
||||
# Handled by periodic keepalive in Pleroma.Web.Streamer.
|
||||
@timeout :infinity
|
||||
|
||||
def init(_type, _req, _opts) do
|
||||
{:upgrade, :protocol, :cowboy_websocket}
|
||||
end
|
||||
|
||||
def websocket_init(_type, req, _opts) do
|
||||
with {qs, req} <- :cowboy_req.qs(req),
|
||||
params <- :cow_qs.parse_qs(qs),
|
||||
access_token <- List.keyfind(params, "access_token", 0),
|
||||
{_, stream} <- List.keyfind(params, "stream", 0),
|
||||
{:ok, user} <- allow_request(stream, access_token),
|
||||
topic when is_binary(topic) <- expand_topic(stream, params) do
|
||||
send(self(), :subscribe)
|
||||
{:ok, req, %{user: user, topic: topic}, @timeout}
|
||||
else
|
||||
{:error, code} ->
|
||||
Logger.debug("#{__MODULE__} denied connection: #{inspect(code)} - #{inspect(req)}")
|
||||
{:ok, req} = :cowboy_req.reply(code, req)
|
||||
{:shutdown, req}
|
||||
|
||||
error ->
|
||||
Logger.debug("#{__MODULE__} denied connection: #{inspect(error)} - #{inspect(req)}")
|
||||
{:shutdown, req}
|
||||
end
|
||||
end
|
||||
|
||||
# We never receive messages.
|
||||
def websocket_handle(_frame, req, state) do
|
||||
{:ok, req, state}
|
||||
end
|
||||
|
||||
def websocket_info(:subscribe, req, state) do
|
||||
Logger.debug(
|
||||
"#{__MODULE__} accepted websocket connection for user #{
|
||||
(state.user || %{id: "anonymous"}).id
|
||||
}, topic #{state.topic}"
|
||||
)
|
||||
|
||||
Pleroma.Web.Streamer.add_socket(state.topic, streamer_socket(state))
|
||||
{:ok, req, state}
|
||||
end
|
||||
|
||||
def websocket_info({:text, message}, req, state) do
|
||||
{:reply, {:text, message}, req, state}
|
||||
end
|
||||
|
||||
def websocket_terminate(reason, _req, state) do
|
||||
Logger.debug(
|
||||
"#{__MODULE__} terminating websocket connection for user #{
|
||||
(state.user || %{id: "anonymous"}).id
|
||||
}, topic #{state.topic || "?"}: #{inspect(reason)}"
|
||||
)
|
||||
|
||||
Pleroma.Web.Streamer.remove_socket(state.topic, streamer_socket(state))
|
||||
:ok
|
||||
end
|
||||
|
||||
# Public streams without authentication.
|
||||
defp allow_request(stream, nil) when stream in @anonymous_streams do
|
||||
{:ok, nil}
|
||||
end
|
||||
|
||||
# Authenticated streams.
|
||||
defp allow_request(stream, {"access_token", access_token}) when stream in @streams do
|
||||
with %Token{user_id: user_id} <- Repo.get_by(Token, token: access_token),
|
||||
user = %User{} <- Repo.get(User, user_id) do
|
||||
{:ok, user}
|
||||
else
|
||||
_ -> {:error, 403}
|
||||
end
|
||||
end
|
||||
|
||||
# Not authenticated.
|
||||
defp allow_request(stream, _) when stream in @streams, do: {:error, 403}
|
||||
|
||||
# No matching stream.
|
||||
defp allow_request(_, _), do: {:error, 404}
|
||||
|
||||
defp expand_topic("hashtag", params) do
|
||||
case List.keyfind(params, "tag", 0) do
|
||||
{_, tag} -> "hashtag:#{tag}"
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp expand_topic("list", params) do
|
||||
case List.keyfind(params, "list", 0) do
|
||||
{_, list} -> "list:#{list}"
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp expand_topic(topic, _), do: topic
|
||||
|
||||
defp streamer_socket(state) do
|
||||
%{transport_pid: self(), assigns: state}
|
||||
end
|
||||
end
|
||||
|
|
@ -1,135 +1,39 @@
|
|||
defmodule Pleroma.Web.MediaProxy.MediaProxyController do
|
||||
use Pleroma.Web, :controller
|
||||
require Logger
|
||||
alias Pleroma.{Web.MediaProxy, ReverseProxy}
|
||||
|
||||
@httpoison Application.get_env(:pleroma, :httpoison)
|
||||
@default_proxy_opts [max_body_length: 25 * 1_048_576, http: [follow_redirect: true]]
|
||||
|
||||
@max_body_length 25 * 1_048_576
|
||||
|
||||
@cache_control %{
|
||||
default: "public, max-age=1209600",
|
||||
error: "public, must-revalidate, max-age=160"
|
||||
}
|
||||
|
||||
# Content-types that will not be returned as content-disposition attachments
|
||||
# Override with :media_proxy, :safe_content_types in the configuration
|
||||
@safe_content_types [
|
||||
"image/gif",
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/png",
|
||||
"image/svg+xml",
|
||||
"audio/mpeg",
|
||||
"audio/mp3",
|
||||
"video/webm",
|
||||
"video/mp4"
|
||||
]
|
||||
|
||||
def remote(conn, params = %{"sig" => sig, "url" => url}) do
|
||||
config = Application.get_env(:pleroma, :media_proxy, [])
|
||||
|
||||
with true <- Keyword.get(config, :enabled, false),
|
||||
{:ok, url} <- Pleroma.Web.MediaProxy.decode_url(sig, url),
|
||||
filename <- Path.basename(URI.parse(url).path),
|
||||
true <-
|
||||
if(Map.get(params, "filename"),
|
||||
do: filename == Path.basename(conn.request_path),
|
||||
else: true
|
||||
),
|
||||
{:ok, content_type, body} <- proxy_request(url),
|
||||
safe_content_type <-
|
||||
Enum.member?(
|
||||
Keyword.get(config, :safe_content_types, @safe_content_types),
|
||||
content_type
|
||||
) do
|
||||
conn
|
||||
|> put_resp_content_type(content_type)
|
||||
|> set_cache_header(:default)
|
||||
|> put_resp_header(
|
||||
"content-security-policy",
|
||||
"default-src 'none'; style-src 'unsafe-inline'; media-src data:; img-src 'self' data:"
|
||||
)
|
||||
|> put_resp_header("x-xss-protection", "1; mode=block")
|
||||
|> put_resp_header("x-content-type-options", "nosniff")
|
||||
|> put_attachement_header(safe_content_type, filename)
|
||||
|> send_resp(200, body)
|
||||
def remote(conn, params = %{"sig" => sig64, "url" => url64}) do
|
||||
with config <- Pleroma.Config.get([:media_proxy], []),
|
||||
true <- Keyword.get(config, :enabled, false),
|
||||
{:ok, url} <- MediaProxy.decode_url(sig64, url64),
|
||||
:ok <- filename_matches(Map.has_key?(params, "filename"), conn.request_path, url) do
|
||||
ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts))
|
||||
else
|
||||
false ->
|
||||
send_error(conn, 404)
|
||||
send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404))
|
||||
|
||||
{:error, :invalid_signature} ->
|
||||
send_error(conn, 403)
|
||||
send_resp(conn, 403, Plug.Conn.Status.reason_phrase(403))
|
||||
|
||||
{:error, {:http, _, url}} ->
|
||||
redirect_or_error(conn, url, Keyword.get(config, :redirect_on_failure, true))
|
||||
{:wrong_filename, filename} ->
|
||||
redirect(conn, external: MediaProxy.build_url(sig64, url64, filename))
|
||||
end
|
||||
end
|
||||
|
||||
defp proxy_request(link) do
|
||||
headers = [
|
||||
{"user-agent",
|
||||
"Pleroma/MediaProxy; #{Pleroma.Web.base_url()} <#{
|
||||
Application.get_env(:pleroma, :instance)[:email]
|
||||
}>"}
|
||||
]
|
||||
def filename_matches(has_filename, path, url) do
|
||||
filename =
|
||||
url
|
||||
|> MediaProxy.filename()
|
||||
|> URI.decode()
|
||||
|
||||
options =
|
||||
@httpoison.process_request_options([:insecure, {:follow_redirect, true}]) ++
|
||||
[{:pool, :default}]
|
||||
path = URI.decode(path)
|
||||
|
||||
with {:ok, 200, headers, client} <- :hackney.request(:get, link, headers, "", options),
|
||||
headers = Enum.into(headers, Map.new()),
|
||||
{:ok, body} <- proxy_request_body(client),
|
||||
content_type <- proxy_request_content_type(headers, body) do
|
||||
{:ok, content_type, body}
|
||||
if has_filename && filename && Path.basename(path) != filename do
|
||||
{:wrong_filename, filename}
|
||||
else
|
||||
{:ok, status, _, _} ->
|
||||
Logger.warn("MediaProxy: request failed, status #{status}, link: #{link}")
|
||||
{:error, {:http, :bad_status, link}}
|
||||
|
||||
{:error, error} ->
|
||||
Logger.warn("MediaProxy: request failed, error #{inspect(error)}, link: #{link}")
|
||||
{:error, {:http, error, link}}
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp set_cache_header(conn, key) do
|
||||
Plug.Conn.put_resp_header(conn, "cache-control", @cache_control[key])
|
||||
end
|
||||
|
||||
defp redirect_or_error(conn, url, true), do: redirect(conn, external: url)
|
||||
defp redirect_or_error(conn, url, _), do: send_error(conn, 502, "Media proxy error: " <> url)
|
||||
|
||||
defp send_error(conn, code, body \\ "") do
|
||||
conn
|
||||
|> set_cache_header(:error)
|
||||
|> send_resp(code, body)
|
||||
end
|
||||
|
||||
defp proxy_request_body(client), do: proxy_request_body(client, <<>>)
|
||||
|
||||
defp proxy_request_body(client, body) when byte_size(body) < @max_body_length do
|
||||
case :hackney.stream_body(client) do
|
||||
{:ok, data} -> proxy_request_body(client, <<body::binary, data::binary>>)
|
||||
:done -> {:ok, body}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp proxy_request_body(client, _) do
|
||||
:hackney.close(client)
|
||||
{:error, :body_too_large}
|
||||
end
|
||||
|
||||
# TODO: the body is passed here as well because some hosts do not provide a content-type.
|
||||
# At some point we may want to use magic numbers to discover the content-type and reply a proper one.
|
||||
defp proxy_request_content_type(headers, _body) do
|
||||
headers["Content-Type"] || headers["content-type"] || "application/octet-stream"
|
||||
end
|
||||
|
||||
defp put_attachement_header(conn, true, _), do: conn
|
||||
|
||||
defp put_attachement_header(conn, false, filename) do
|
||||
put_resp_header(conn, "content-disposition", "attachment; filename='#{filename}'")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,13 +14,18 @@ defmodule Pleroma.Web.MediaProxy do
|
|||
url
|
||||
else
|
||||
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
|
||||
base64 = Base.url_encode64(url, @base64_opts)
|
||||
|
||||
# The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice.
|
||||
base64 =
|
||||
url
|
||||
|> URI.decode()
|
||||
|> URI.encode()
|
||||
|> Base.url_encode64(@base64_opts)
|
||||
|
||||
sig = :crypto.hmac(:sha, secret, base64)
|
||||
sig64 = sig |> Base.url_encode64(@base64_opts)
|
||||
filename = if path = URI.parse(url).path, do: "/" <> Path.basename(path), else: ""
|
||||
|
||||
Keyword.get(config, :base_url, Pleroma.Web.base_url()) <>
|
||||
"/proxy/#{sig64}/#{base64}#{filename}"
|
||||
build_url(sig64, base64, filename(url))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -35,4 +40,20 @@ defmodule Pleroma.Web.MediaProxy do
|
|||
{:error, :invalid_signature}
|
||||
end
|
||||
end
|
||||
|
||||
def filename(url_or_path) do
|
||||
if path = URI.parse(url_or_path).path, do: Path.basename(path)
|
||||
end
|
||||
|
||||
def build_url(sig_base64, url_base64, filename \\ nil) do
|
||||
[
|
||||
Pleroma.Config.get([:media_proxy, :base_url], Pleroma.Web.base_url()),
|
||||
"proxy",
|
||||
sig_base64,
|
||||
url_base64,
|
||||
filename
|
||||
]
|
||||
|> Enum.filter(fn value -> value end)
|
||||
|> Path.join()
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
|
|||
alias Pleroma.Stats
|
||||
alias Pleroma.Web
|
||||
alias Pleroma.{User, Repo}
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Web.ActivityPub.MRF
|
||||
|
||||
plug(Pleroma.Web.FederatingPlug)
|
||||
|
|
@ -52,6 +53,10 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
|
|||
|> Repo.all()
|
||||
|> Enum.map(fn u -> u.ap_id end)
|
||||
|
||||
mrf_user_allowlist =
|
||||
Config.get([:mrf_user_allowlist], [])
|
||||
|> Enum.into(%{}, fn {k, v} -> {k, length(v)} end)
|
||||
|
||||
mrf_transparency = Keyword.get(instance, :mrf_transparency)
|
||||
|
||||
federation_response =
|
||||
|
|
@ -59,29 +64,35 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
|
|||
%{
|
||||
mrf_policies: mrf_policies,
|
||||
mrf_simple: mrf_simple,
|
||||
mrf_user_allowlist: mrf_user_allowlist,
|
||||
quarantined_instances: quarantined
|
||||
}
|
||||
else
|
||||
%{}
|
||||
end
|
||||
|
||||
features = [
|
||||
"pleroma_api",
|
||||
"mastodon_api",
|
||||
"mastodon_api_streaming",
|
||||
if Keyword.get(media_proxy, :enabled) do
|
||||
"media_proxy"
|
||||
end,
|
||||
if Keyword.get(gopher, :enabled) do
|
||||
"gopher"
|
||||
end,
|
||||
if Keyword.get(chat, :enabled) do
|
||||
"chat"
|
||||
end,
|
||||
if Keyword.get(suggestions, :enabled) do
|
||||
"suggestions"
|
||||
end
|
||||
]
|
||||
features =
|
||||
[
|
||||
"pleroma_api",
|
||||
"mastodon_api",
|
||||
"mastodon_api_streaming",
|
||||
if Keyword.get(media_proxy, :enabled) do
|
||||
"media_proxy"
|
||||
end,
|
||||
if Keyword.get(gopher, :enabled) do
|
||||
"gopher"
|
||||
end,
|
||||
if Keyword.get(chat, :enabled) do
|
||||
"chat"
|
||||
end,
|
||||
if Keyword.get(suggestions, :enabled) do
|
||||
"suggestions"
|
||||
end,
|
||||
if Keyword.get(instance, :allow_relay) do
|
||||
"relay"
|
||||
end
|
||||
]
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
response = %{
|
||||
version: "2.0",
|
||||
|
|
@ -121,6 +132,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
|
|||
banner: Keyword.get(instance, :banner_upload_limit),
|
||||
background: Keyword.get(instance, :background_upload_limit)
|
||||
},
|
||||
invitesEnabled: Keyword.get(instance, :invites_enabled, false),
|
||||
features: features
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|
|||
|
||||
def token_exchange(
|
||||
conn,
|
||||
%{"grant_type" => "password", "name" => name, "password" => password} = params
|
||||
%{"grant_type" => "password", "name" => name, "password" => _password} = params
|
||||
) do
|
||||
params =
|
||||
params
|
||||
|
|
|
|||
|
|
@ -226,25 +226,21 @@ defmodule Pleroma.Web.OStatus do
|
|||
old_data = %{
|
||||
avatar: user.avatar,
|
||||
bio: user.bio,
|
||||
name: user.name,
|
||||
info: user.info
|
||||
name: user.name
|
||||
}
|
||||
|
||||
with false <- user.local,
|
||||
avatar <- make_avatar_object(doc),
|
||||
bio <- string_from_xpath("//author[1]/summary", doc),
|
||||
name <- string_from_xpath("//author[1]/poco:displayName", doc),
|
||||
info <-
|
||||
Map.put(user.info, "banner", make_avatar_object(doc, "header") || user.info["banner"]),
|
||||
new_data <- %{
|
||||
avatar: avatar || old_data.avatar,
|
||||
name: name || old_data.name,
|
||||
bio: bio || old_data.bio,
|
||||
info: info || old_data.info
|
||||
bio: bio || old_data.bio
|
||||
},
|
||||
false <- new_data == old_data do
|
||||
change = Ecto.Changeset.change(user, new_data)
|
||||
Repo.update(change)
|
||||
User.update_and_set_cache(change)
|
||||
else
|
||||
_ ->
|
||||
{:ok, user}
|
||||
|
|
@ -350,13 +346,10 @@ defmodule Pleroma.Web.OStatus do
|
|||
|
||||
def fetch_activity_from_atom_url(url) do
|
||||
with true <- String.starts_with?(url, "http"),
|
||||
{:ok, %{body: body, status_code: code}} when code in 200..299 <-
|
||||
{:ok, %{body: body, status: code}} when code in 200..299 <-
|
||||
@httpoison.get(
|
||||
url,
|
||||
[Accept: "application/atom+xml"],
|
||||
follow_redirect: true,
|
||||
timeout: 10000,
|
||||
recv_timeout: 20000
|
||||
[{:Accept, "application/atom+xml"}]
|
||||
) do
|
||||
Logger.debug("Got document from #{url}, handling...")
|
||||
handle_incoming(body)
|
||||
|
|
@ -371,8 +364,7 @@ defmodule Pleroma.Web.OStatus do
|
|||
Logger.debug("Trying to fetch #{url}")
|
||||
|
||||
with true <- String.starts_with?(url, "http"),
|
||||
{:ok, %{body: body}} <-
|
||||
@httpoison.get(url, [], follow_redirect: true, timeout: 10000, recv_timeout: 20000),
|
||||
{:ok, %{body: body}} <- @httpoison.get(url, []),
|
||||
{:ok, atom_url} <- get_atom_url(body) do
|
||||
fetch_activity_from_atom_url(atom_url)
|
||||
else
|
||||
|
|
@ -383,19 +375,14 @@ defmodule Pleroma.Web.OStatus do
|
|||
end
|
||||
|
||||
def fetch_activity_from_url(url) do
|
||||
try do
|
||||
with {:ok, activities} when length(activities) > 0 <- fetch_activity_from_atom_url(url) do
|
||||
{:ok, activities}
|
||||
else
|
||||
_e ->
|
||||
with {:ok, activities} <- fetch_activity_from_html_url(url) do
|
||||
{:ok, activities}
|
||||
end
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Logger.debug("Couldn't get #{url}: #{inspect(e)}")
|
||||
{:error, "Couldn't get #{url}: #{inspect(e)}"}
|
||||
with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url) do
|
||||
{:ok, activities}
|
||||
else
|
||||
_e -> fetch_activity_from_html_url(url)
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Logger.debug("Couldn't get #{url}: #{inspect(e)}")
|
||||
{:error, "Couldn't get #{url}: #{inspect(e)}"}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
|
|||
conn,
|
||||
"activity+json",
|
||||
%Activity{data: %{"type" => "Create"}} = activity,
|
||||
user
|
||||
_user
|
||||
) do
|
||||
object = Object.normalize(activity.data["object"])
|
||||
|
||||
|
|
@ -166,7 +166,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
|
|||
|> json(ObjectView.render("object.json", %{object: object}))
|
||||
end
|
||||
|
||||
defp represent_activity(conn, "activity+json", _, _) do
|
||||
defp represent_activity(_conn, "activity+json", _, _) do
|
||||
{:error, :not_found}
|
||||
end
|
||||
|
||||
|
|
|
|||
134
lib/pleroma/web/push/push.ex
Normal file
134
lib/pleroma/web/push/push.ex
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
defmodule Pleroma.Web.Push do
|
||||
use GenServer
|
||||
|
||||
alias Pleroma.{Repo, User}
|
||||
alias Pleroma.Web.Push.Subscription
|
||||
|
||||
require Logger
|
||||
import Ecto.Query
|
||||
|
||||
@types ["Create", "Follow", "Announce", "Like"]
|
||||
|
||||
def start_link() do
|
||||
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
|
||||
end
|
||||
|
||||
def vapid_config() do
|
||||
Application.get_env(:web_push_encryption, :vapid_details, [])
|
||||
end
|
||||
|
||||
def enabled() do
|
||||
case vapid_config() do
|
||||
[] -> false
|
||||
list when is_list(list) -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
def send(notification) do
|
||||
if enabled() do
|
||||
GenServer.cast(Pleroma.Web.Push, {:send, notification})
|
||||
end
|
||||
end
|
||||
|
||||
def init(:ok) do
|
||||
if !enabled() do
|
||||
Logger.warn("""
|
||||
VAPID key pair is not found. If you wish to enabled web push, please run
|
||||
|
||||
mix web_push.gen.keypair
|
||||
|
||||
and add the resulting output to your configuration file.
|
||||
""")
|
||||
|
||||
:ignore
|
||||
else
|
||||
{:ok, nil}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_cast(
|
||||
{:send, %{activity: %{data: %{"type" => type}}, user_id: user_id} = notification},
|
||||
state
|
||||
)
|
||||
when type in @types do
|
||||
actor = User.get_cached_by_ap_id(notification.activity.data["actor"])
|
||||
|
||||
type = Pleroma.Activity.mastodon_notification_type(notification.activity)
|
||||
|
||||
Subscription
|
||||
|> where(user_id: ^user_id)
|
||||
|> preload(:token)
|
||||
|> Repo.all()
|
||||
|> Enum.filter(fn subscription ->
|
||||
get_in(subscription.data, ["alerts", type]) || false
|
||||
end)
|
||||
|> Enum.each(fn subscription ->
|
||||
sub = %{
|
||||
keys: %{
|
||||
p256dh: subscription.key_p256dh,
|
||||
auth: subscription.key_auth
|
||||
},
|
||||
endpoint: subscription.endpoint
|
||||
}
|
||||
|
||||
body =
|
||||
Jason.encode!(%{
|
||||
title: format_title(notification),
|
||||
access_token: subscription.token.token,
|
||||
body: format_body(notification, actor),
|
||||
notification_id: notification.id,
|
||||
notification_type: type,
|
||||
icon: User.avatar_url(actor),
|
||||
preferred_locale: "en"
|
||||
})
|
||||
|
||||
case WebPushEncryption.send_web_push(
|
||||
body,
|
||||
sub,
|
||||
Application.get_env(:web_push_encryption, :gcm_api_key)
|
||||
) do
|
||||
{:ok, %{status_code: code}} when 400 <= code and code < 500 ->
|
||||
Logger.debug("Removing subscription record")
|
||||
Repo.delete!(subscription)
|
||||
:ok
|
||||
|
||||
{:ok, %{status_code: code}} when 200 <= code and code < 300 ->
|
||||
:ok
|
||||
|
||||
{:ok, %{status_code: code}} ->
|
||||
Logger.error("Web Push Notification failed with code: #{code}")
|
||||
:error
|
||||
|
||||
_ ->
|
||||
Logger.error("Web Push Notification failed with unknown error")
|
||||
:error
|
||||
end
|
||||
end)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast({:send, _}, state) do
|
||||
Logger.warn("Unknown notification type")
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
defp format_title(%{activity: %{data: %{"type" => type}}}) do
|
||||
case type do
|
||||
"Create" -> "New Mention"
|
||||
"Follow" -> "New Follower"
|
||||
"Announce" -> "New Repeat"
|
||||
"Like" -> "New Favorite"
|
||||
end
|
||||
end
|
||||
|
||||
defp format_body(%{activity: %{data: %{"type" => type}}}, actor) do
|
||||
case type do
|
||||
"Create" -> "@#{actor.nickname} has mentioned you"
|
||||
"Follow" -> "@#{actor.nickname} has followed you"
|
||||
"Announce" -> "@#{actor.nickname} has repeated your post"
|
||||
"Like" -> "@#{actor.nickname} has favorited your post"
|
||||
end
|
||||
end
|
||||
end
|
||||
76
lib/pleroma/web/push/subscription.ex
Normal file
76
lib/pleroma/web/push/subscription.ex
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
defmodule Pleroma.Web.Push.Subscription do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Pleroma.{Repo, User}
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
alias Pleroma.Web.Push.Subscription
|
||||
|
||||
schema "push_subscriptions" do
|
||||
belongs_to(:user, User)
|
||||
belongs_to(:token, Token)
|
||||
field(:endpoint, :string)
|
||||
field(:key_p256dh, :string)
|
||||
field(:key_auth, :string)
|
||||
field(:data, :map, default: %{})
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@supported_alert_types ~w[follow favourite mention reblog]
|
||||
|
||||
defp alerts(%{"data" => %{"alerts" => alerts}}) do
|
||||
alerts = Map.take(alerts, @supported_alert_types)
|
||||
%{"alerts" => alerts}
|
||||
end
|
||||
|
||||
def create(
|
||||
%User{} = user,
|
||||
%Token{} = token,
|
||||
%{
|
||||
"subscription" => %{
|
||||
"endpoint" => endpoint,
|
||||
"keys" => %{"auth" => key_auth, "p256dh" => key_p256dh}
|
||||
}
|
||||
} = params
|
||||
) do
|
||||
Repo.insert(%Subscription{
|
||||
user_id: user.id,
|
||||
token_id: token.id,
|
||||
endpoint: endpoint,
|
||||
key_auth: ensure_base64_urlsafe(key_auth),
|
||||
key_p256dh: ensure_base64_urlsafe(key_p256dh),
|
||||
data: alerts(params)
|
||||
})
|
||||
end
|
||||
|
||||
def get(%User{id: user_id}, %Token{id: token_id}) do
|
||||
Repo.get_by(Subscription, user_id: user_id, token_id: token_id)
|
||||
end
|
||||
|
||||
def update(user, token, params) do
|
||||
get(user, token)
|
||||
|> change(data: alerts(params))
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
def delete(user, token) do
|
||||
Repo.delete(get(user, token))
|
||||
end
|
||||
|
||||
def delete_if_exists(user, token) do
|
||||
case get(user, token) do
|
||||
nil -> {:ok, nil}
|
||||
sub -> Repo.delete(sub)
|
||||
end
|
||||
end
|
||||
|
||||
# Some webpush clients (e.g. iOS Toot!) use an non urlsafe base64 as an encoding for the key.
|
||||
# However, the web push rfs specify to use base64 urlsafe, and the `web_push_encryption` library we use
|
||||
# requires the key to be properly encoded. So we just convert base64 to urlsafe base64.
|
||||
defp ensure_base64_urlsafe(string) do
|
||||
string
|
||||
|> String.replace("+", "-")
|
||||
|> String.replace("/", "_")
|
||||
|> String.replace("=", "")
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue