commit
dd2b3a8da9
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"skip_files": [
|
"skip_files": [
|
||||||
"test/support",
|
"test/support",
|
||||||
"lib/mix/tasks/pleroma/benchmark.ex"
|
"lib/mix/tasks/pleroma/benchmark.ex",
|
||||||
|
"lib/credo/check/consistency/file_location.ex"
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
# Generate release environment file
|
||||||
|
|
||||||
|
```sh tab="OTP"
|
||||||
|
./bin/pleroma_ctl release_env gen
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh tab="From Source"
|
||||||
|
mix pleroma.release_env gen
|
||||||
|
```
|
@ -0,0 +1,136 @@
|
|||||||
|
# Configuring Ejabberd (XMPP Server) to use Pleroma for authentication
|
||||||
|
|
||||||
|
If you want to give your Pleroma users an XMPP (chat) account, you can configure [Ejabberd](https://github.com/processone/ejabberd) to use your Pleroma server for user authentication, automatically giving every local user an XMPP account.
|
||||||
|
|
||||||
|
In general, you just have to follow the configuration described at [https://docs.ejabberd.im/admin/configuration/authentication/#external-script](https://docs.ejabberd.im/admin/configuration/authentication/#external-script). Please read this section carefully.
|
||||||
|
|
||||||
|
Copy the script below to suitable path on your system and set owner and permissions. Also do not forget adjusting `PLEROMA_HOST` and `PLEROMA_PORT`, if necessary.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp pleroma_ejabberd_auth.py /etc/ejabberd/pleroma_ejabberd_auth.py
|
||||||
|
chown ejabberd /etc/ejabberd/pleroma_ejabberd_auth.py
|
||||||
|
chmod 700 /etc/ejabberd/pleroma_ejabberd_auth.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Set external auth params in ejabberd.yaml file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
auth_method: [external]
|
||||||
|
extauth_program: "python3 /etc/ejabberd/pleroma_ejabberd_auth.py"
|
||||||
|
extauth_instances: 3
|
||||||
|
auth_use_cache: false
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart / reload your ejabberd service.
|
||||||
|
|
||||||
|
After restarting your Ejabberd server, your users should now be able to connect with their Pleroma credentials.
|
||||||
|
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sys
|
||||||
|
import struct
|
||||||
|
import http.client
|
||||||
|
from base64 import b64encode
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
PLEROMA_HOST = "127.0.0.1"
|
||||||
|
PLEROMA_PORT = "4000"
|
||||||
|
AUTH_ENDPOINT = "/api/v1/accounts/verify_credentials"
|
||||||
|
USER_ENDPOINT = "/api/v1/accounts"
|
||||||
|
LOGFILE = "/var/log/ejabberd/pleroma_auth.log"
|
||||||
|
|
||||||
|
logging.basicConfig(filename=LOGFILE, level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
# Pleroma functions
|
||||||
|
def create_connection():
|
||||||
|
return http.client.HTTPConnection(PLEROMA_HOST, PLEROMA_PORT)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_credentials(user: str, password: str) -> bool:
|
||||||
|
user_pass_b64 = b64encode("{}:{}".format(
|
||||||
|
user, password).encode('utf-8')).decode("ascii")
|
||||||
|
params = {}
|
||||||
|
headers = {
|
||||||
|
"Authorization": "Basic {}".format(user_pass_b64)
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = create_connection()
|
||||||
|
conn.request("GET", AUTH_ENDPOINT, params, headers)
|
||||||
|
|
||||||
|
response = conn.getresponse()
|
||||||
|
if response.status == 200:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logging.info("Can not connect: %s", str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def does_user_exist(user: str) -> bool:
|
||||||
|
conn = create_connection()
|
||||||
|
conn.request("GET", "{}/{}".format(USER_ENDPOINT, user))
|
||||||
|
|
||||||
|
response = conn.getresponse()
|
||||||
|
if response.status == 200:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def auth(username: str, server: str, password: str) -> bool:
|
||||||
|
return verify_credentials(username, password)
|
||||||
|
|
||||||
|
|
||||||
|
def isuser(username, server):
|
||||||
|
return does_user_exist(username)
|
||||||
|
|
||||||
|
|
||||||
|
def read():
|
||||||
|
(pkt_size,) = struct.unpack('>H', bytes(sys.stdin.read(2), encoding='utf8'))
|
||||||
|
pkt = sys.stdin.read(pkt_size)
|
||||||
|
cmd = pkt.split(':')[0]
|
||||||
|
if cmd == 'auth':
|
||||||
|
username, server, password = pkt.split(':', 3)[1:]
|
||||||
|
write(auth(username, server, password))
|
||||||
|
elif cmd == 'isuser':
|
||||||
|
username, server = pkt.split(':', 2)[1:]
|
||||||
|
write(isuser(username, server))
|
||||||
|
elif cmd == 'setpass':
|
||||||
|
# u, s, p = pkt.split(':', 3)[1:]
|
||||||
|
write(False)
|
||||||
|
elif cmd == 'tryregister':
|
||||||
|
# u, s, p = pkt.split(':', 3)[1:]
|
||||||
|
write(False)
|
||||||
|
elif cmd == 'removeuser':
|
||||||
|
# u, s = pkt.split(':', 2)[1:]
|
||||||
|
write(False)
|
||||||
|
elif cmd == 'removeuser3':
|
||||||
|
# u, s, p = pkt.split(':', 3)[1:]
|
||||||
|
write(False)
|
||||||
|
else:
|
||||||
|
write(False)
|
||||||
|
|
||||||
|
|
||||||
|
def write(result):
|
||||||
|
if result:
|
||||||
|
sys.stdout.write('\x00\x02\x00\x01')
|
||||||
|
else:
|
||||||
|
sys.stdout.write('\x00\x02\x00\x00')
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logging.info("Starting pleroma ejabberd auth daemon...")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
read()
|
||||||
|
except Exception as e:
|
||||||
|
logging.info(
|
||||||
|
"Error while processing data from ejabberd %s", str(e))
|
||||||
|
pass
|
||||||
|
|
||||||
|
```
|
@ -0,0 +1,32 @@
|
|||||||
|
# Optional software packages needed for specific functionality
|
||||||
|
|
||||||
|
For specific Pleroma functionality (which is disabled by default) some or all of the below packages are required:
|
||||||
|
* `ImageMagic`
|
||||||
|
* `ffmpeg`
|
||||||
|
* `exiftool`
|
||||||
|
|
||||||
|
Please refer to documentation in `docs/installation` on how to install them on specific OS.
|
||||||
|
|
||||||
|
Note: the packages are not required with the current default settings of Pleroma.
|
||||||
|
|
||||||
|
## `ImageMagick`
|
||||||
|
|
||||||
|
`ImageMagick` is a set of tools to create, edit, compose, or convert bitmap images.
|
||||||
|
|
||||||
|
It is required for the following Pleroma features:
|
||||||
|
* `Pleroma.Upload.Filters.Mogrify`, `Pleroma.Upload.Filters.Mogrifun` upload filters (related config: `Plaroma.Upload/filters` in `config/config.exs`)
|
||||||
|
* Media preview proxy for still images (related config: `media_preview_proxy/enabled` in `config/config.exs`)
|
||||||
|
|
||||||
|
## `ffmpeg`
|
||||||
|
|
||||||
|
`ffmpeg` is software to record, convert and stream audio and video.
|
||||||
|
|
||||||
|
It is required for the following Pleroma features:
|
||||||
|
* Media preview proxy for videos (related config: `media_preview_proxy/enabled` in `config/config.exs`)
|
||||||
|
|
||||||
|
## `exiftool`
|
||||||
|
|
||||||
|
`exiftool` is media files metadata reader/writer.
|
||||||
|
|
||||||
|
It is required for the following Pleroma features:
|
||||||
|
* `Pleroma.Upload.Filters.Exiftool` upload filter (related config: `Plaroma.Upload/filters` in `config/config.exs`)
|
@ -0,0 +1,76 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Mix.Tasks.Pleroma.ReleaseEnv do
|
||||||
|
use Mix.Task
|
||||||
|
import Mix.Pleroma
|
||||||
|
|
||||||
|
@shortdoc "Generate Pleroma environment file."
|
||||||
|
@moduledoc File.read!("docs/administration/CLI_tasks/release_environments.md")
|
||||||
|
|
||||||
|
def run(["gen" | rest]) do
|
||||||
|
{options, [], []} =
|
||||||
|
OptionParser.parse(
|
||||||
|
rest,
|
||||||
|
strict: [
|
||||||
|
force: :boolean,
|
||||||
|
path: :string
|
||||||
|
],
|
||||||
|
aliases: [
|
||||||
|
p: :path,
|
||||||
|
f: :force
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
file_path =
|
||||||
|
get_option(
|
||||||
|
options,
|
||||||
|
:path,
|
||||||
|
"Environment file path",
|
||||||
|
"./config/pleroma.env"
|
||||||
|
)
|
||||||
|
|
||||||
|
env_path = Path.expand(file_path)
|
||||||
|
|
||||||
|
proceed? =
|
||||||
|
if File.exists?(env_path) do
|
||||||
|
get_option(
|
||||||
|
options,
|
||||||
|
:force,
|
||||||
|
"Environment file already exists. Do you want to overwrite the #{env_path} file? (y/n)",
|
||||||
|
"n"
|
||||||
|
) === "y"
|
||||||
|
else
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
if proceed? do
|
||||||
|
case do_generate(env_path) do
|
||||||
|
{:error, reason} ->
|
||||||
|
shell_error(
|
||||||
|
File.Error.message(%{action: "write to file", reason: reason, path: env_path})
|
||||||
|
)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
shell_info("\nThe file generated: #{env_path}.\n")
|
||||||
|
|
||||||
|
shell_info("""
|
||||||
|
WARNING: before start pleroma app please make sure to make the file read-only and non-modifiable.
|
||||||
|
Example:
|
||||||
|
chmod 0444 #{file_path}
|
||||||
|
chattr +i #{file_path}
|
||||||
|
""")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
shell_info("\nThe file is exist. #{env_path}.\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def do_generate(path) do
|
||||||
|
content = "RELEASE_COOKIE=#{Base.encode32(:crypto.strong_rand_bytes(32))}"
|
||||||
|
|
||||||
|
File.mkdir_p!(Path.dirname(path))
|
||||||
|
File.write(path, content)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,19 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Helpers.InetHelper do
|
||||||
|
def parse_address(ip) when is_tuple(ip) do
|
||||||
|
{:ok, ip}
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_address(ip) when is_binary(ip) do
|
||||||
|
ip
|
||||||
|
|> String.to_charlist()
|
||||||
|
|> parse_address()
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_address(ip) do
|
||||||
|
:inet.parse_address(ip)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,12 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.HTTP.WebPush do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
def post(url, payload, headers) do
|
||||||
|
list_headers = Map.to_list(headers)
|
||||||
|
Pleroma.HTTP.post(url, payload, list_headers)
|
||||||
|
end
|
||||||
|
end
|
@ -1,120 +0,0 @@
|
|||||||
# Pleroma: A lightweight social networking server
|
|
||||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
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(), 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(<<"RIFF", _::binary-size(4), "WAVE", _::binary>>) do
|
|
||||||
"audio/wav"
|
|
||||||
end
|
|
||||||
|
|
||||||
defp check_mime_type(<<"RIFF", _::binary-size(4), "WEBP", _::binary>>) do
|
|
||||||
"image/webp"
|
|
||||||
end
|
|
||||||
|
|
||||||
defp check_mime_type(<<"RIFF", _::binary-size(4), "AVI.", _::binary>>) do
|
|
||||||
"video/avi"
|
|
||||||
end
|
|
||||||
|
|
||||||
defp check_mime_type(_) do
|
|
||||||
@default
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,54 +0,0 @@
|
|||||||
# Pleroma: A lightweight social networking server
|
|
||||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
defmodule Pleroma.Plugs.RemoteIp do
|
|
||||||
@moduledoc """
|
|
||||||
This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import Plug.Conn
|
|
||||||
|
|
||||||
@behaviour Plug
|
|
||||||
|
|
||||||
@headers ~w[
|
|
||||||
x-forwarded-for
|
|
||||||
]
|
|
||||||
|
|
||||||
# https://en.wikipedia.org/wiki/Localhost
|
|
||||||
# https://en.wikipedia.org/wiki/Private_network
|
|
||||||
@reserved ~w[
|
|
||||||
127.0.0.0/8
|
|
||||||
::1/128
|
|
||||||
fc00::/7
|
|
||||||
10.0.0.0/8
|
|
||||||
172.16.0.0/12
|
|
||||||
192.168.0.0/16
|
|
||||||
]
|
|
||||||
|
|
||||||
def init(_), do: nil
|
|
||||||
|
|
||||||
def call(%{remote_ip: original_remote_ip} = conn, _) do
|
|
||||||
config = Pleroma.Config.get(__MODULE__, [])
|
|
||||||
|
|
||||||
if Keyword.get(config, :enabled, false) do
|
|
||||||
%{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts(config))
|
|
||||||
assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip)
|
|
||||||
else
|
|
||||||
conn
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp remote_ip_opts(config) do
|
|
||||||
headers = config |> Keyword.get(:headers, @headers) |> MapSet.new()
|
|
||||||
reserved = Keyword.get(config, :reserved, @reserved)
|
|
||||||
|
|
||||||
proxies =
|
|
||||||
config
|
|
||||||
|> Keyword.get(:proxies, [])
|
|
||||||
|> Enum.concat(reserved)
|
|
||||||
|> Enum.map(&InetCidr.parse/1)
|
|
||||||
|
|
||||||
{headers, proxies}
|
|
||||||
end
|
|
||||||
end
|
|
@ -0,0 +1,258 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.User.Backup do
|
||||||
|
use Ecto.Schema
|
||||||
|
|
||||||
|
import Ecto.Changeset
|
||||||
|
import Ecto.Query
|
||||||
|
import Pleroma.Web.Gettext
|
||||||
|
|
||||||
|
require Pleroma.Constants
|
||||||
|
|
||||||
|
alias Pleroma.Activity
|
||||||
|
alias Pleroma.Bookmark
|
||||||
|
alias Pleroma.Repo
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||||
|
alias Pleroma.Web.ActivityPub.UserView
|
||||||
|
alias Pleroma.Workers.BackupWorker
|
||||||
|
|
||||||
|
schema "backups" do
|
||||||
|
field(:content_type, :string)
|
||||||
|
field(:file_name, :string)
|
||||||
|
field(:file_size, :integer, default: 0)
|
||||||
|
field(:processed, :boolean, default: false)
|
||||||
|
|
||||||
|
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
def create(user, admin_id \\ nil) do
|
||||||
|
with :ok <- validate_email_enabled(),
|
||||||
|
:ok <- validate_user_email(user),
|
||||||
|
:ok <- validate_limit(user, admin_id),
|
||||||
|
{:ok, backup} <- user |> new() |> Repo.insert() do
|
||||||
|
BackupWorker.process(backup, admin_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def new(user) do
|
||||||
|
rand_str = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
|
||||||
|
datetime = Calendar.NaiveDateTime.Format.iso8601_basic(NaiveDateTime.utc_now())
|
||||||
|
name = "archive-#{user.nickname}-#{datetime}-#{rand_str}.zip"
|
||||||
|
|
||||||
|
%__MODULE__{
|
||||||
|
user_id: user.id,
|
||||||
|
content_type: "application/zip",
|
||||||
|
file_name: name
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete(backup) do
|
||||||
|
uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
|
||||||
|
|
||||||
|
with :ok <- uploader.delete_file(Path.join("backups", backup.file_name)) do
|
||||||
|
Repo.delete(backup)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_limit(_user, admin_id) when is_binary(admin_id), do: :ok
|
||||||
|
|
||||||
|
defp validate_limit(user, nil) do
|
||||||
|
case get_last(user.id) do
|
||||||
|
%__MODULE__{inserted_at: inserted_at} ->
|
||||||
|
days = Pleroma.Config.get([__MODULE__, :limit_days])
|
||||||
|
diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days)
|
||||||
|
|
||||||
|
if diff > days do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
{:error,
|
||||||
|
dngettext(
|
||||||
|
"errors",
|
||||||
|
"Last export was less than a day ago",
|
||||||
|
"Last export was less than %{days} days ago",
|
||||||
|
days,
|
||||||
|
days: days
|
||||||
|
)}
|
||||||
|
end
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_email_enabled do
|
||||||
|
if Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
{:error, dgettext("errors", "Backups require enabled email")}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_user_email(%User{email: nil}) do
|
||||||
|
{:error, dgettext("errors", "Email is required")}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_user_email(%User{email: email}) when is_binary(email), do: :ok
|
||||||
|
|
||||||
|
def get_last(user_id) do
|
||||||
|
__MODULE__
|
||||||
|
|> where(user_id: ^user_id)
|
||||||
|
|> order_by(desc: :id)
|
||||||
|
|> limit(1)
|
||||||
|
|> Repo.one()
|
||||||
|
end
|
||||||
|
|
||||||
|
def list(%User{id: user_id}) do
|
||||||
|
__MODULE__
|
||||||
|
|> where(user_id: ^user_id)
|
||||||
|
|> order_by(desc: :id)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_outdated(%__MODULE__{id: latest_id, user_id: user_id}) do
|
||||||
|
__MODULE__
|
||||||
|
|> where(user_id: ^user_id)
|
||||||
|
|> where([b], b.id != ^latest_id)
|
||||||
|
|> Repo.all()
|
||||||
|
|> Enum.each(&BackupWorker.delete/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get(id), do: Repo.get(__MODULE__, id)
|
||||||
|
|
||||||
|
def process(%__MODULE__{} = backup) do
|
||||||
|
with {:ok, zip_file} <- export(backup),
|
||||||
|
{:ok, %{size: size}} <- File.stat(zip_file),
|
||||||
|
{:ok, _upload} <- upload(backup, zip_file) do
|
||||||
|
backup
|
||||||
|
|> cast(%{file_size: size, processed: true}, [:file_size, :processed])
|
||||||
|
|> Repo.update()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json']
|
||||||
|
def export(%__MODULE__{} = backup) do
|
||||||
|
backup = Repo.preload(backup, :user)
|
||||||
|
name = String.trim_trailing(backup.file_name, ".zip")
|
||||||
|
dir = dir(name)
|
||||||
|
|
||||||
|
with :ok <- File.mkdir(dir),
|
||||||
|
:ok <- actor(dir, backup.user),
|
||||||
|
:ok <- statuses(dir, backup.user),
|
||||||
|
:ok <- likes(dir, backup.user),
|
||||||
|
:ok <- bookmarks(dir, backup.user),
|
||||||
|
{:ok, zip_path} <- :zip.create(String.to_charlist(dir <> ".zip"), @files, cwd: dir),
|
||||||
|
{:ok, _} <- File.rm_rf(dir) do
|
||||||
|
{:ok, to_string(zip_path)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def dir(name) do
|
||||||
|
dir = Pleroma.Config.get([__MODULE__, :dir]) || System.tmp_dir!()
|
||||||
|
Path.join(dir, name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def upload(%__MODULE__{} = backup, zip_path) do
|
||||||
|
uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
|
||||||
|
|
||||||
|
upload = %Pleroma.Upload{
|
||||||
|
name: backup.file_name,
|
||||||
|
tempfile: zip_path,
|
||||||
|
content_type: backup.content_type,
|
||||||
|
path: Path.join("backups", backup.file_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
with {:ok, _} <- Pleroma.Uploaders.Uploader.put_file(uploader, upload),
|
||||||
|
:ok <- File.rm(zip_path) do
|
||||||
|
{:ok, upload}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp actor(dir, user) do
|
||||||
|
with {:ok, json} <-
|
||||||
|
UserView.render("user.json", %{user: user})
|
||||||
|
|> Map.merge(%{"likes" => "likes.json", "bookmarks" => "bookmarks.json"})
|
||||||
|
|> Jason.encode() do
|
||||||
|
File.write(Path.join(dir, "actor.json"), json)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp write_header(file, name) do
|
||||||
|
IO.write(
|
||||||
|
file,
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"id": "#{name}.json",
|
||||||
|
"type": "OrderedCollection",
|
||||||
|
"orderedItems": [
|
||||||
|
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp write(query, dir, name, fun) do
|
||||||
|
path = Path.join(dir, "#{name}.json")
|
||||||
|
|
||||||
|
with {:ok, file} <- File.open(path, [:write, :utf8]),
|
||||||
|
:ok <- write_header(file, name) do
|
||||||
|
total =
|
||||||
|
query
|
||||||
|
|> Pleroma.Repo.chunk_stream(100)
|
||||||
|
|> Enum.reduce(0, fn i, acc ->
|
||||||
|
with {:ok, data} <- fun.(i),
|
||||||
|
{:ok, str} <- Jason.encode(data),
|
||||||
|
:ok <- IO.write(file, str <> ",\n") do
|
||||||
|
acc + 1
|
||||||
|
else
|
||||||
|
_ -> acc
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
with :ok <- :file.pwrite(file, {:eof, -2}, "\n],\n \"totalItems\": #{total}}") do
|
||||||
|
File.close(file)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp bookmarks(dir, %{id: user_id} = _user) do
|
||||||
|
Bookmark
|
||||||
|
|> where(user_id: ^user_id)
|
||||||
|
|> join(:inner, [b], activity in assoc(b, :activity))
|
||||||
|
|> select([b, a], %{id: b.id, object: fragment("(?)->>'object'", a.data)})
|
||||||
|
|> write(dir, "bookmarks", fn a -> {:ok, a.object} end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp likes(dir, user) do
|
||||||
|
user.ap_id
|
||||||
|
|> Activity.Queries.by_actor()
|
||||||
|
|> Activity.Queries.by_type("Like")
|
||||||
|
|> select([like], %{id: like.id, object: fragment("(?)->>'object'", like.data)})
|
||||||
|
|> write(dir, "likes", fn a -> {:ok, a.object} end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp statuses(dir, user) do
|
||||||
|
opts =
|
||||||
|
%{}
|
||||||
|
|> Map.put(:type, ["Create", "Announce"])
|
||||||
|
|> Map.put(:actor_id, user.ap_id)
|
||||||
|
|
||||||
|
[
|
||||||
|
[Pleroma.Constants.as_public(), user.ap_id],
|
||||||
|
User.following(user),
|
||||||
|
Pleroma.List.memberships(user)
|
||||||
|
]
|
||||||
|
|> Enum.concat()
|
||||||
|
|> ActivityPub.fetch_activities_query(opts)
|
||||||
|
|> write(dir, "outbox", fn a ->
|
||||||
|
with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do
|
||||||
|
{:ok, Map.delete(activity, "@context")}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue