Merge branch 'develop' into features/remote-follow-userpage-redirect

This commit is contained in:
Mark Felder 2020-01-22 15:05:39 -06:00
commit ae78059ff4
544 changed files with 3987 additions and 1564 deletions

View file

@ -95,6 +95,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Captcha: Enable by default
- Mastodon API: Add support for `account_id` param to filter notifications by the account
- Mastodon API: Add `emoji_reactions` property to Statuses
- Mastodon API: Change emoji reaction reply format
</details>
### Fixed

View file

@ -112,7 +112,6 @@
shortcode_globs: ["/emoji/custom/**/*.png"],
pack_extensions: [".png", ".gif"],
groups: [
# Put groups that have higher priority than defaults here. Example in `docs/config/custom_emoji.md`
Custom: ["/emoji/*.png", "/emoji/**/*.png"]
],
default_manifest: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json",
@ -265,7 +264,6 @@
remote_post_retention_days: 90,
skip_thread_containment: true,
limit_to_local_content: :unauthenticated,
dynamic_configuration: false,
user_bio_length: 5000,
user_name_length: 100,
max_account_fields: 10,
@ -502,7 +500,8 @@
mailer: 10,
transmogrifier: 20,
scheduled_activities: 10,
background: 5
background: 5,
attachments_cleanup: 5
]
config :pleroma, :workers,
@ -619,6 +618,8 @@
config :pleroma, :modules, runtime_dir: "instance/modules"
config :pleroma, configurable_from_database: false
config :swarm, node_blacklist: [~r/myhtml_.*$/]
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.

File diff suppressed because it is too large Load diff

View file

@ -6,6 +6,8 @@
config_path = System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs"
config :pleroma, release: true, config_path: config_path
if File.exists?(config_path) do
import_config config_path
else
@ -18,3 +20,12 @@
IO.puts(warning)
end
exported_config =
config_path
|> Path.dirname()
|> Path.join("prod.exported_from_db.secret.exs")
if File.exists?(exported_config) do
import_config exported_config
end

View file

@ -665,27 +665,16 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- 404 Not Found `"Not found"`
- On success: 200 OK `{}`
## `GET /api/pleroma/admin/config/migrate_to_db`
### Run mix task pleroma.config migrate_to_db
Copy settings on key `:pleroma` to DB.
- Params: none
- Response:
```json
{}
```
## `GET /api/pleroma/admin/config/migrate_from_db`
### Run mix task pleroma.config migrate_from_db
Copy all settings from DB to `config/prod.exported_from_db.secret.exs` with deletion from DB.
Copies all settings from database to `config/{env}.exported_from_db.secret.exs` with deletion from the table. Where `{env}` is the environment in which `pleroma` is running.
- Params: none
- Response:
- On failure:
- 400 Bad Request `"To use this endpoint you need to enable configuration from database."`
```json
{}
@ -693,20 +682,24 @@ Copy all settings from DB to `config/prod.exported_from_db.secret.exs` with dele
## `GET /api/pleroma/admin/config`
### List config settings
### Get list of merged default settings with saved in database.
List config settings only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`.
**Only works when configuration from database is enabled.**
- Params: none
- Params:
- `only_db`: true (*optional*, get only saved in database settings)
- Response:
- On failure:
- 400 Bad Request `"To use this endpoint you need to enable configuration from database."`
- 400 Bad Request `"To use configuration from database migrate your settings to database."`
```json
{
configs: [
{
"group": string,
"key": string or string with leading `:` for atoms,
"value": string or {} or [] or {"tuple": []}
"group": ":pleroma",
"key": "Pleroma.Upload",
"value": []
}
]
}
@ -716,44 +709,107 @@ List config settings only works with `:pleroma => :instance => :dynamic_configur
### Update config settings
Updating config settings only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`.
Module name can be passed as string, which starts with `Pleroma`, e.g. `"Pleroma.Upload"`.
Atom keys and values can be passed with `:` in the beginning, e.g. `":upload"`.
Tuples can be passed as `{"tuple": ["first_val", Pleroma.Module, []]}`.
`{"tuple": ["some_string", "Pleroma.Some.Module", []]}` will be converted to `{"some_string", Pleroma.Some.Module, []}`.
Keywords can be passed as lists with 2 child tuples, e.g.
`[{"tuple": ["first_val", Pleroma.Module]}, {"tuple": ["second_val", true]}]`.
**Only works when configuration from database is enabled.**
If value contains list of settings `[subkey: val1, subkey2: val2, subkey3: val3]`, it's possible to remove only subkeys instead of all settings passing `subkeys` parameter. E.g.:
{"group": "pleroma", "key": "some_key", "delete": "true", "subkeys": [":subkey", ":subkey3"]}.
Some modifications are necessary to save the config settings correctly:
Compile time settings (need instance reboot):
- all settings by this keys:
- strings which start with `Pleroma.`, `Phoenix.`, `Tesla.` or strings like `Oban`, `Ueberauth` will be converted to modules;
```
"Pleroma.Upload" -> Pleroma.Upload
"Oban" -> Oban
```
- strings starting with `:` will be converted to atoms;
```
":pleroma" -> :pleroma
```
- objects with `tuple` key and array value will be converted to tuples;
```
{"tuple": ["string", "Pleroma.Upload", []]} -> {"string", Pleroma.Upload, []}
```
- arrays with *tuple objects* will be converted to keywords;
```
[{"tuple": [":key1", "value"]}, {"tuple": [":key2", "value"]}] -> [key1: "value", key2: "value"]
```
Most of the settings will be applied in `runtime`, this means that you don't need to restart the instance. But some settings are applied in `compile time` and require a reboot of the instance, such as:
- all settings inside these keys:
- `:hackney_pools`
- `:chat`
- `Pleroma.Web.Endpoint`
- `Pleroma.Repo`
- part settings:
- `Pleroma.Captcha` -> `:seconds_valid`
- `Pleroma.Upload` -> `:proxy_remote`
- `:instance` -> `:upload_limit`
- partially settings inside these keys:
- `:seconds_valid` in `Pleroma.Captcha`
- `:proxy_remote` in `Pleroma.Upload`
- `:upload_limit` in `:instance`
- Params:
- `configs` => [
- `group` (string)
- `key` (string or string with leading `:` for atoms)
- `value` (string, [], {} or {"tuple": []})
- `delete` = true (optional, if parameter must be deleted)
- `subkeys` [(string with leading `:` for atoms)] (optional, works only if `delete=true` parameter is passed, otherwise will be ignored)
]
- `configs` - array of config objects
- config object params:
- `group` - string (**required**)
- `key` - string (**required**)
- `value` - string, [], {} or {"tuple": []} (**required**)
- `delete` - true (*optional*, if setting must be deleted)
- `subkeys` - array of strings (*optional*, only works when `delete=true` parameter is passed, otherwise will be ignored)
- Request (example):
*When a value have several nested settings, you can delete only some nested settings by passing a parameter `subkeys`, without deleting all settings by key.*
```
[subkey: val1, subkey2: val2, subkey3: val3] \\ initial value
{"group": ":pleroma", "key": "some_key", "delete": true, "subkeys": [":subkey", ":subkey3"]} \\ passing json for deletion
[subkey2: val2] \\ value after deletion
```
*Most of the settings can be partially updated through merge old values with new values, except settings value of which is list or is not keyword.*
Example of setting without keyword in value:
```elixir
config :tesla, :adapter, Tesla.Adapter.Hackney
```
List of settings which support only full update by key:
```elixir
@full_key_update [
{:pleroma, :ecto_repos},
{:quack, :meta},
{:mime, :types},
{:cors_plug, [:max_age, :methods, :expose, :headers]},
{:auto_linker, :opts},
{:swarm, :node_blacklist},
{:logger, :backends}
]
```
List of settings which support only full update by subkey:
```elixir
@full_subkey_update [
{:pleroma, :assets, :mascots},
{:pleroma, :emoji, :groups},
{:pleroma, :workers, :retries},
{:pleroma, :mrf_subchain, :match_actor},
{:pleroma, :mrf_keyword, :replace}
]
```
*Settings without explicit key must be sended in separate config object params.*
```elixir
config :quack,
level: :debug,
meta: [:all],
...
```
```json
{
configs: [
{"group": ":quack", "key": ":level", "value": ":debug"},
{"group": ":quack", "key": ":meta", "value": [":all"]},
...
]
}
```
- Request:
```json
{
configs: [
{
"group": "pleroma",
"group": ":pleroma",
"key": "Pleroma.Upload",
"value": [
{"tuple": [":uploader", "Pleroma.Uploaders.Local"]},
@ -763,7 +819,7 @@ Compile time settings (need instance reboot):
{"tuple": [":proxy_opts", [
{"tuple": [":redirect_on_failure", false]},
{"tuple": [":max_body_length", 1048576]},
{"tuple": [":http": [
{"tuple": [":http", [
{"tuple": [":follow_redirect", true]},
{"tuple": [":pool", ":upload"]},
]]}
@ -779,19 +835,53 @@ Compile time settings (need instance reboot):
```
- Response:
- On failure:
- 400 Bad Request `"To use this endpoint you need to enable configuration from database."`
```json
{
configs: [
{
"group": string,
"key": string or string with leading `:` for atoms,
"value": string or {} or [] or {"tuple": []}
"group": ":pleroma",
"key": "Pleroma.Upload",
"value": [...]
}
]
}
```
## ` GET /api/pleroma/admin/config/descriptions`
### Get JSON with config descriptions.
Loads json generated from `config/descriptions.exs`.
- Params: none
- Response:
```json
[{
"group": ":pleroma", // string
"key": "ModuleName", // string
"type": "group", // string or list with possible values,
"description": "Upload general settings", // string
"children": [
{
"key": ":uploader", // string or module name `Pleroma.Upload`
"type": "module",
"description": "Module which will be used for uploads",
"suggestions": ["module1", "module2"]
},
{
"key": ":filters",
"type": ["list", "module"],
"description": "List of filter modules for uploads",
"suggestions": [
"module1", "module2", "module3"
]
}
]
}]
```
## `GET /api/pleroma/admin/moderation_log`
### Get moderation log

View file

@ -29,7 +29,7 @@ Has these additional fields under the `pleroma` object:
- `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`
- `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire
- `thread_muted`: true if the thread the post belongs to is muted
- `emoji_reactions`: An object with all the emoji reactions with count. Contains no information about the reacting users, for that use the `emoji_reactions_by` endpoint.
- `emoji_reactions`: A list with emoji / reaction count tuples. Contains no information about the reacting users, for that use the `emoji_reactions_by` endpoint.
## Attachments

View file

@ -451,11 +451,11 @@ Emoji reactions work a lot like favourites do. They make it possible to react to
* Method: `GET`
* Authentication: optional
* Params: None
* Response: JSON, a map of emoji to account list mappings.
* Response: JSON, a list of emoji/account list tuples, sorted by emoji insertion date, in ascending order, e.g, the first emoji in the list is the oldest.
* Example Response:
```json
{
"😀" => [{"id" => "xyz.."...}, {"id" => "zyx..."}],
"🗡" => [{"id" => "abc..."}]
}
[
["😀", [{"id" => "xyz.."...}, {"id" => "zyx..."}]],
["☕", [{"id" => "abc..."}]]
]
```

79
docs/admin/config.md Normal file
View file

@ -0,0 +1,79 @@
# Configuring instance
You can configure your instance from admin interface. You need account with admin rights and little change in config file, which will allow settings configuration from database.
```elixir
config :pleroma, configurable_from_database: true
```
## How it works
Settings are stored in database and are applied in `runtime` after each change. Most of the settings take effect immediately, except some, which need instance reboot. These settings are needed in `compile time`, that's why settings are duplicated to the file.
File with duplicated settings is located in `config/{env}.exported_from_db.exs` if pleroma is runned from source. For prod env it will be `config/prod.exported_from_db.exs`.
For releases: `/etc/pleroma/prod.exported_from_db.secret.exs` or `PLEROMA_CONFIG_PATH/prod.exported_from_db.exs`.
## How to set it up
You need to migrate your existing settings to the database. This task will migrate only added by user settings.
For example you add settings to `prod.secret.exs` file, only these settings will be migrated to database. For release it will be `/etc/pleroma/config.exs` or `PLEROMA_CONFIG_PATH`.
You can do this with mix task (all config files will remain untouched):
```sh tab="OTP"
./bin/pleroma_ctl config migrate_to_db
```
```sh tab="From Source"
mix pleroma.config migrate_to_db
```
Now you can change settings in admin interface. After each save, settings from database are duplicated to the `config/{env}.exported_from_db.exs` file.
<span style="color:red">**ATTENTION**</span>
**<span style="color:red">Be careful while changing the settings. Every inaccurate configuration change can break the federation or the instance load.</span>**
*Compile time settings, which require instance reboot and can break instance loading:*
- all settings inside these keys:
- `:hackney_pools`
- `:chat`
- partially settings inside these keys:
- `:seconds_valid` in `Pleroma.Captcha`
- `:proxy_remote` in `Pleroma.Upload`
- `:upload_limit` in `:instance`
## How to dump settings from database to file
*Adding `-d` flag will delete migrated settings from database table.*
```sh tab="OTP"
./bin/pleroma_ctl config migrate_from_db [-d]
```
```sh tab="From Source"
mix pleroma.config migrate_from_db [-d]
```
## How to completely remove it
1. Truncate or delete all values from `config` table
```sql
TRUNCATE TABLE config;
```
2. Delete `config/{env}.exported_from_db.exs`.
For `prod` env:
```bash
cd /opt/pleroma
cp config/prod.exported_from_db.exs config/exported_from_db.back
rm -rf config/prod.exported_from_db.exs
```
*If you don't want to backup settings, you can skip step with `cp` command.*
3. Set configurable_from_database to `false`.
```elixir
config :pleroma, configurable_from_database: false
```
4. Restart pleroma instance
```bash
sudo service pleroma restart
```

View file

@ -18,11 +18,11 @@ mix pleroma.config migrate_to_db
## Transfer config from DB to `config/env.exported_from_db.secret.exs`
To delete transfered settings from database optional flag `-d` can be used. <env> is `prod` by default.
```sh tab="OTP"
./bin/pleroma_ctl config migrate_from_db <env>
./bin/pleroma_ctl config migrate_from_db [--env=<env>] [-d]
```
```sh tab="From Source"
mix pleroma.config migrate_from_db <env>
mix pleroma.config migrate_from_db [--env=<env>] [-d]
```

View file

@ -70,11 +70,6 @@ You shouldn't edit the base config directly to avoid breakages and merge conflic
* `account_field_value_length`: An account field value maximum length (default: `2048`).
* `external_user_synchronization`: Enabling following/followers counters synchronization for external users.
!!! danger
This is a Work In Progress, not usable just yet
* `dynamic_configuration`: Allow transferring configuration to DB with the subsequent customization from Admin api.
## Federation
### MRF policies
@ -355,7 +350,7 @@ Available caches:
* `proxy_url`: an upstream proxy to fetch posts and/or media with, (default: `nil`)
* `send_user_agent`: should we include a user agent with HTTP requests? (default: `true`)
* `user_agent`: what user agent should we use? (default: `:default`), must be string or `:default`
* `user_agent`: what user agent should we use? (default: `:default`), must be string or `:default`
* `adapter`: array of hackney options
@ -841,3 +836,7 @@ config :auto_linker,
## Custom Runtime Modules (`:modules`)
* `runtime_dir`: A path to custom Elixir modules (such as MRF policies).
## :configurable_from_database
Enable/disable configuration from database.

View file

@ -4,71 +4,144 @@
defmodule Mix.Tasks.Pleroma.Config do
use Mix.Task
import Mix.Pleroma
alias Pleroma.ConfigDB
alias Pleroma.Repo
alias Pleroma.Web.AdminAPI.Config
@shortdoc "Manages the location of the config"
@moduledoc File.read!("docs/administration/CLI_tasks/config.md")
def run(["migrate_to_db"]) do
start_pleroma()
if Pleroma.Config.get([:instance, :dynamic_configuration]) do
Application.get_all_env(:pleroma)
|> Enum.reject(fn {k, _v} -> k in [Pleroma.Repo, :env] end)
|> Enum.each(fn {k, v} ->
key = to_string(k) |> String.replace("Elixir.", "")
key =
if String.starts_with?(key, "Pleroma.") do
key
else
":" <> key
end
{:ok, _} = Config.update_or_create(%{group: "pleroma", key: key, value: v})
Mix.shell().info("#{key} is migrated.")
end)
Mix.shell().info("Settings migrated.")
else
Mix.shell().info(
"Migration is not allowed by config. You can change this behavior in instance settings."
)
end
migrate_to_db()
end
def run(["migrate_from_db", env, delete?]) do
def run(["migrate_from_db" | options]) do
start_pleroma()
delete? = if delete? == "true", do: true, else: false
if Pleroma.Config.get([:instance, :dynamic_configuration]) do
config_path = "config/#{env}.exported_from_db.secret.exs"
{:ok, file} = File.open(config_path, [:write, :utf8])
IO.write(file, "use Mix.Config\r\n")
Repo.all(Config)
|> Enum.each(fn config ->
IO.write(
file,
"config :#{config.group}, #{config.key}, #{
inspect(Config.from_binary(config.value), limit: :infinity)
}\r\n\r\n"
)
if delete? do
{:ok, _} = Repo.delete(config)
Mix.shell().info("#{config.key} deleted from DB.")
end
end)
File.close(file)
System.cmd("mix", ["format", config_path])
else
Mix.shell().info(
"Migration is not allowed by config. You can change this behavior in instance settings."
{opts, _} =
OptionParser.parse!(options,
strict: [env: :string, delete: :boolean],
aliases: [d: :delete]
)
migrate_from_db(opts)
end
@spec migrate_to_db(Path.t() | nil) :: any()
def migrate_to_db(file_path \\ nil) do
if Pleroma.Config.get([:configurable_from_database]) do
config_file =
if file_path do
file_path
else
if Pleroma.Config.get(:release) do
Pleroma.Config.get(:config_path)
else
"config/#{Pleroma.Config.get(:env)}.secret.exs"
end
end
do_migrate_to_db(config_file)
else
migration_error()
end
end
defp do_migrate_to_db(config_file) do
if File.exists?(config_file) do
custom_config =
config_file
|> read_file()
|> elem(0)
custom_config
|> Keyword.keys()
|> Enum.each(&create(&1, custom_config))
else
shell_info("To migrate settings, you must define custom settings in #{config_file}.")
end
end
defp create(group, settings) do
group
|> Pleroma.Config.Loader.filter_group(settings)
|> Enum.each(fn {key, value} ->
key = inspect(key)
{:ok, _} = ConfigDB.update_or_create(%{group: inspect(group), key: key, value: value})
shell_info("Settings for key #{key} migrated.")
end)
shell_info("Settings for group :#{group} migrated.")
end
defp migrate_from_db(opts) do
if Pleroma.Config.get([:configurable_from_database]) do
env = opts[:env] || "prod"
config_path =
if Pleroma.Config.get(:release) do
:config_path
|> Pleroma.Config.get()
|> Path.dirname()
else
"config"
end
|> Path.join("#{env}.exported_from_db.secret.exs")
file = File.open!(config_path, [:write, :utf8])
IO.write(file, config_header())
ConfigDB
|> Repo.all()
|> Enum.each(&write_and_delete(&1, file, opts[:delete]))
:ok = File.close(file)
System.cmd("mix", ["format", config_path])
else
migration_error()
end
end
defp migration_error do
shell_error(
"Migration is not allowed in config. You can change this behavior by setting `configurable_from_database` to true."
)
end
if Code.ensure_loaded?(Config.Reader) do
defp config_header, do: "import Config\r\n\r\n"
defp read_file(config_file), do: Config.Reader.read_imports!(config_file)
else
defp config_header, do: "use Mix.Config\r\n\r\n"
defp read_file(config_file), do: Mix.Config.eval!(config_file)
end
defp write_and_delete(config, file, delete?) do
config
|> write(file)
|> delete(delete?)
end
defp write(config, file) do
value =
config.value
|> ConfigDB.from_binary()
|> inspect(limit: :infinity)
IO.write(file, "config #{config.group}, #{config.key}, #{value}\r\n\r\n")
config
end
defp delete(config, true) do
{:ok, _} = Repo.delete(config)
shell_info("#{config.key} deleted from DB.")
end
defp delete(_config, _), do: :ok
end

View file

@ -28,7 +28,7 @@ def run(_) do
defp do_run(implementation) do
start_pleroma()
with {descriptions, _paths} <- Mix.Config.eval!("config/description.exs"),
with descriptions <- Pleroma.Config.Loader.load("config/description.exs"),
{:ok, file_path} <-
Pleroma.Docs.Generator.process(
implementation,

View file

@ -0,0 +1,422 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ConfigDB do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
import Pleroma.Web.Gettext
alias __MODULE__
alias Pleroma.Repo
@type t :: %__MODULE__{}
@full_key_update [
{:pleroma, :ecto_repos},
{:quack, :meta},
{:mime, :types},
{:cors_plug, [:max_age, :methods, :expose, :headers]},
{:auto_linker, :opts},
{:swarm, :node_blacklist},
{:logger, :backends}
]
@full_subkey_update [
{:pleroma, :assets, :mascots},
{:pleroma, :emoji, :groups},
{:pleroma, :workers, :retries},
{:pleroma, :mrf_subchain, :match_actor},
{:pleroma, :mrf_keyword, :replace}
]
@regex ~r/^~r(?'delimiter'[\/|"'([{<]{1})(?'pattern'.+)[\/|"')\]}>]{1}(?'modifier'[uismxfU]*)/u
@delimiters ["/", "|", "\"", "'", {"(", ")"}, {"[", "]"}, {"{", "}"}, {"<", ">"}]
schema "config" do
field(:key, :string)
field(:group, :string)
field(:value, :binary)
field(:db, {:array, :string}, virtual: true, default: [])
timestamps()
end
@spec get_all_as_keyword() :: keyword()
def get_all_as_keyword do
ConfigDB
|> select([c], {c.group, c.key, c.value})
|> Repo.all()
|> Enum.reduce([], fn {group, key, value}, acc ->
group = ConfigDB.from_string(group)
key = ConfigDB.from_string(key)
value = from_binary(value)
Keyword.update(acc, group, [{key, value}], &Keyword.merge(&1, [{key, value}]))
end)
end
@spec get_by_params(map()) :: ConfigDB.t() | nil
def get_by_params(params), do: Repo.get_by(ConfigDB, params)
@spec changeset(ConfigDB.t(), map()) :: Changeset.t()
def changeset(config, params \\ %{}) do
params = Map.put(params, :value, transform(params[:value]))
config
|> cast(params, [:key, :group, :value])
|> validate_required([:key, :group, :value])
|> unique_constraint(:key, name: :config_group_key_index)
end
@spec create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
def create(params) do
%ConfigDB{}
|> changeset(params)
|> Repo.insert()
end
@spec update(ConfigDB.t(), map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
def update(%ConfigDB{} = config, %{value: value}) do
config
|> changeset(%{value: value})
|> Repo.update()
end
@spec get_db_keys(ConfigDB.t()) :: [String.t()]
def get_db_keys(%ConfigDB{} = config) do
config.value
|> ConfigDB.from_binary()
|> get_db_keys(config.key)
end
@spec get_db_keys(keyword(), any()) :: [String.t()]
def get_db_keys(value, key) do
if Keyword.keyword?(value) do
value |> Keyword.keys() |> Enum.map(&convert(&1))
else
[convert(key)]
end
end
@spec merge_group(atom(), atom(), keyword(), keyword()) :: keyword()
def merge_group(group, key, old_value, new_value) do
new_keys = to_map_set(new_value)
intersect_keys =
old_value |> to_map_set() |> MapSet.intersection(new_keys) |> MapSet.to_list()
merged_value = ConfigDB.merge(old_value, new_value)
@full_subkey_update
|> Enum.map(fn
{g, k, subkey} when g == group and k == key ->
if subkey in intersect_keys, do: subkey, else: []
_ ->
[]
end)
|> List.flatten()
|> Enum.reduce(merged_value, fn subkey, acc ->
Keyword.put(acc, subkey, new_value[subkey])
end)
end
defp to_map_set(keyword) do
keyword
|> Keyword.keys()
|> MapSet.new()
end
@spec sub_key_full_update?(atom(), atom(), [Keyword.key()]) :: boolean()
def sub_key_full_update?(group, key, subkeys) do
Enum.any?(@full_subkey_update, fn {g, k, subkey} ->
g == group and k == key and subkey in subkeys
end)
end
@spec merge(keyword(), keyword()) :: keyword()
def merge(config1, config2) when is_list(config1) and is_list(config2) do
Keyword.merge(config1, config2, fn _, app1, app2 ->
if Keyword.keyword?(app1) and Keyword.keyword?(app2) do
Keyword.merge(app1, app2, &deep_merge/3)
else
app2
end
end)
end
defp deep_merge(_key, value1, value2) do
if Keyword.keyword?(value1) and Keyword.keyword?(value2) do
Keyword.merge(value1, value2, &deep_merge/3)
else
value2
end
end
@spec update_or_create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
def update_or_create(params) do
search_opts = Map.take(params, [:group, :key])
with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts),
{:partial_update, true, config} <-
{:partial_update, can_be_partially_updated?(config), config},
old_value <- from_binary(config.value),
transformed_value <- do_transform(params[:value]),
{:can_be_merged, true, config} <- {:can_be_merged, is_list(transformed_value), config},
new_value <-
merge_group(
ConfigDB.from_string(config.group),
ConfigDB.from_string(config.key),
old_value,
transformed_value
) do
ConfigDB.update(config, %{value: new_value})
else
{reason, false, config} when reason in [:partial_update, :can_be_merged] ->
ConfigDB.update(config, params)
nil ->
ConfigDB.create(params)
end
end
defp can_be_partially_updated?(%ConfigDB{} = config), do: not only_full_update?(config)
defp only_full_update?(%ConfigDB{} = config) do
config_group = ConfigDB.from_string(config.group)
config_key = ConfigDB.from_string(config.key)
Enum.any?(@full_key_update, fn
{group, key} when is_list(key) ->
config_group == group and config_key in key
{group, key} ->
config_group == group and config_key == key
end)
end
@spec delete(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
def delete(params) do
search_opts = Map.delete(params, :subkeys)
with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts),
{config, sub_keys} when is_list(sub_keys) <- {config, params[:subkeys]},
old_value <- from_binary(config.value),
keys <- Enum.map(sub_keys, &do_transform_string(&1)),
{:partial_remove, config, new_value} when new_value != [] <-
{:partial_remove, config, Keyword.drop(old_value, keys)} do
ConfigDB.update(config, %{value: new_value})
else
{:partial_remove, config, []} ->
Repo.delete(config)
{config, nil} ->
Repo.delete(config)
nil ->
err =
dgettext("errors", "Config with params %{params} not found", params: inspect(params))
{:error, err}
end
end
@spec from_binary(binary()) :: term()
def from_binary(binary), do: :erlang.binary_to_term(binary)
@spec from_binary_with_convert(binary()) :: any()
def from_binary_with_convert(binary) do
binary
|> from_binary()
|> do_convert()
end
@spec from_string(String.t()) :: atom() | no_return()
def from_string(":" <> entity), do: String.to_existing_atom(entity)
def from_string(entity) when is_binary(entity) do
if is_module_name?(entity) do
String.to_existing_atom("Elixir.#{entity}")
else
entity
end
end
@spec convert(any()) :: any()
def convert(entity), do: do_convert(entity)
defp do_convert(entity) when is_list(entity) do
for v <- entity, into: [], do: do_convert(v)
end
defp do_convert(%Regex{} = entity), do: inspect(entity)
defp do_convert(entity) when is_map(entity) do
for {k, v} <- entity, into: %{}, do: {do_convert(k), do_convert(v)}
end
defp do_convert({:proxy_url, {type, :localhost, port}}) do
%{"tuple" => [":proxy_url", %{"tuple" => [do_convert(type), "localhost", port]}]}
end
defp do_convert({:proxy_url, {type, host, port}}) when is_tuple(host) do
ip =
host
|> :inet_parse.ntoa()
|> to_string()
%{
"tuple" => [
":proxy_url",
%{"tuple" => [do_convert(type), ip, port]}
]
}
end
defp do_convert({:proxy_url, {type, host, port}}) do
%{
"tuple" => [
":proxy_url",
%{"tuple" => [do_convert(type), to_string(host), port]}
]
}
end
defp do_convert({:partial_chain, entity}), do: %{"tuple" => [":partial_chain", inspect(entity)]}
defp do_convert(entity) when is_tuple(entity) do
value =
entity
|> Tuple.to_list()
|> do_convert()
%{"tuple" => value}
end
defp do_convert(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity) do
entity
end
defp do_convert(entity)
when is_atom(entity) and entity in [:"tlsv1.1", :"tlsv1.2", :"tlsv1.3"] do
":#{entity}"
end
defp do_convert(entity) when is_atom(entity), do: inspect(entity)
defp do_convert(entity) when is_binary(entity), do: entity
@spec transform(any()) :: binary() | no_return()
def transform(entity) when is_binary(entity) or is_map(entity) or is_list(entity) do
entity
|> do_transform()
|> to_binary()
end
def transform(entity), do: to_binary(entity)
@spec transform_with_out_binary(any()) :: any()
def transform_with_out_binary(entity), do: do_transform(entity)
@spec to_binary(any()) :: binary()
def to_binary(entity), do: :erlang.term_to_binary(entity)
defp do_transform(%Regex{} = entity), do: entity
defp do_transform(%{"tuple" => [":proxy_url", %{"tuple" => [type, host, port]}]}) do
{:proxy_url, {do_transform_string(type), parse_host(host), port}}
end
defp do_transform(%{"tuple" => [":partial_chain", entity]}) do
{partial_chain, []} =
entity
|> String.replace(~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "")
|> Code.eval_string()
{:partial_chain, partial_chain}
end
defp do_transform(%{"tuple" => entity}) do
Enum.reduce(entity, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end)
end
defp do_transform(entity) when is_map(entity) do
for {k, v} <- entity, into: %{}, do: {do_transform(k), do_transform(v)}
end
defp do_transform(entity) when is_list(entity) do
for v <- entity, into: [], do: do_transform(v)
end
defp do_transform(entity) when is_binary(entity) do
entity
|> String.trim()
|> do_transform_string()
end
defp do_transform(entity), do: entity
defp parse_host("localhost"), do: :localhost
defp parse_host(host) do
charlist = to_charlist(host)
case :inet.parse_address(charlist) do
{:error, :einval} ->
charlist
{:ok, ip} ->
ip
end
end
defp find_valid_delimiter([], _string, _) do
raise(ArgumentError, message: "valid delimiter for Regex expression not found")
end
defp find_valid_delimiter([{leading, closing} = delimiter | others], pattern, regex_delimiter)
when is_tuple(delimiter) do
if String.contains?(pattern, closing) do
find_valid_delimiter(others, pattern, regex_delimiter)
else
{:ok, {leading, closing}}
end
end
defp find_valid_delimiter([delimiter | others], pattern, regex_delimiter) do
if String.contains?(pattern, delimiter) do
find_valid_delimiter(others, pattern, regex_delimiter)
else
{:ok, {delimiter, delimiter}}
end
end
defp do_transform_string("~r" <> _pattern = regex) do
with %{"modifier" => modifier, "pattern" => pattern, "delimiter" => regex_delimiter} <-
Regex.named_captures(@regex, regex),
{:ok, {leading, closing}} <- find_valid_delimiter(@delimiters, pattern, regex_delimiter),
{result, _} <- Code.eval_string("~r#{leading}#{pattern}#{closing}#{modifier}") do
result
end
end
defp do_transform_string(":" <> atom), do: String.to_atom(atom)
defp do_transform_string(value) do
if is_module_name?(value) do
String.to_existing_atom("Elixir." <> value)
else
value
end
end
@spec is_module_name?(String.t()) :: boolean()
def is_module_name?(string) do
Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Quack|Ueberauth)\./, string) or
string in ["Oban", "Ueberauth", "ExSyslogger"]
end
end

View file

@ -0,0 +1,16 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.Holder do
@config Pleroma.Config.Loader.load_and_merge()
@spec config() :: keyword()
def config, do: @config
@spec config(atom()) :: any()
def config(group), do: @config[group]
@spec config(atom(), atom()) :: any()
def config(group, key), do: @config[group][key]
end

View file

@ -0,0 +1,59 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.Loader do
@paths ["config/config.exs", "config/#{Mix.env()}.exs"]
@reject_keys [
Pleroma.Repo,
Pleroma.Web.Endpoint,
:env,
:configurable_from_database,
:database,
:swarm
]
if Code.ensure_loaded?(Config.Reader) do
@spec load(Path.t()) :: keyword()
def load(path), do: Config.Reader.read!(path)
defp do_merge(conf1, conf2), do: Config.Reader.merge(conf1, conf2)
else
# support for Elixir less than 1.9
@spec load(Path.t()) :: keyword()
def load(path) do
path
|> Mix.Config.eval!()
|> elem(0)
end
defp do_merge(conf1, conf2), do: Mix.Config.merge(conf1, conf2)
end
@spec load_and_merge() :: keyword()
def load_and_merge do
all_paths =
if Pleroma.Config.get(:release),
do: @paths ++ ["config/releases.exs"],
else: @paths
all_paths
|> Enum.map(&load(&1))
|> Enum.reduce([], &do_merge(&2, &1))
|> filter()
end
defp filter(configs) do
configs
|> Keyword.keys()
|> Enum.reduce([], &Keyword.put(&2, &1, filter_group(&1, configs)))
end
@spec filter_group(atom(), keyword()) :: keyword()
def filter_group(group, configs) do
Enum.reject(configs[group], fn {key, _v} ->
key in @reject_keys or (group == :phoenix and key == :serve_endpoints)
end)
end
end

View file

@ -4,56 +4,111 @@
defmodule Pleroma.Config.TransferTask do
use Task
alias Pleroma.Web.AdminAPI.Config
alias Pleroma.ConfigDB
alias Pleroma.Repo
require Logger
def start_link(_) do
load_and_update_env()
if Pleroma.Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Pleroma.Repo)
if Pleroma.Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo)
:ignore
end
def load_and_update_env do
if Pleroma.Config.get([:instance, :dynamic_configuration]) and
Ecto.Adapters.SQL.table_exists?(Pleroma.Repo, "config") do
for_restart =
Pleroma.Repo.all(Config)
|> Enum.map(&update_env(&1))
@spec load_and_update_env([ConfigDB.t()]) :: :ok | false
def load_and_update_env(deleted \\ []) do
with true <- Pleroma.Config.get(:configurable_from_database),
true <- Ecto.Adapters.SQL.table_exists?(Repo, "config"),
started_applications <- Application.started_applications() do
# We need to restart applications for loaded settings take effect
for_restart
|> Enum.reject(&(&1 in [:pleroma, :ok]))
|> Enum.each(fn app ->
Application.stop(app)
:ok = Application.start(app)
end)
in_db = Repo.all(ConfigDB)
with_deleted = in_db ++ deleted
with_deleted
|> Enum.map(&merge_and_update(&1))
|> Enum.uniq()
# TODO: some problem with prometheus after restart!
|> Enum.reject(&(&1 in [:pleroma, nil, :prometheus]))
|> Enum.each(&restart(started_applications, &1))
:ok
end
end
defp update_env(setting) do
defp merge_and_update(setting) do
try do
key =
if String.starts_with?(setting.key, "Pleroma.") do
"Elixir." <> setting.key
key = ConfigDB.from_string(setting.key)
group = ConfigDB.from_string(setting.group)
default = Pleroma.Config.Holder.config(group, key)
merged_value = merge_value(setting, default, group, key)
:ok = update_env(group, key, merged_value)
if group != :logger do
group
else
# change logger configuration in runtime, without restart
if Keyword.keyword?(merged_value) and
key not in [:compile_time_application, :backends, :compile_time_purge_matching] do
Logger.configure_backend(key, merged_value)
else
String.trim_leading(setting.key, ":")
Logger.configure([{key, merged_value}])
end
group = String.to_existing_atom(setting.group)
Application.put_env(
group,
String.to_existing_atom(key),
Config.from_binary(setting.value)
)
group
nil
end
rescue
e ->
require Logger
error ->
error_msg =
"updating env causes error, group: " <>
inspect(setting.group) <>
" key: " <>
inspect(setting.key) <>
" value: " <>
inspect(ConfigDB.from_binary(setting.value)) <> " error: " <> inspect(error)
Logger.warn(
"updating env causes error, key: #{inspect(setting.key)}, error: #{inspect(e)}"
)
Logger.warn(error_msg)
nil
end
end
defp merge_value(%{__meta__: %{state: :deleted}}, default, _group, _key), do: default
defp merge_value(setting, default, group, key) do
value = ConfigDB.from_binary(setting.value)
if can_be_merged?(default, value) do
ConfigDB.merge_group(group, key, default, value)
else
value
end
end
defp update_env(group, key, nil), do: Application.delete_env(group, key)
defp update_env(group, key, value), do: Application.put_env(group, key, value)
defp restart(started_applications, app) do
with {^app, _, _} <- List.keyfind(started_applications, app, 0),
:ok <- Application.stop(app) do
:ok = Application.start(app)
else
nil ->
Logger.warn("#{app} is not started.")
error ->
error
|> inspect()
|> Logger.warn()
end
end
defp can_be_merged?(val1, val2) when is_list(val1) and is_list(val2) do
Keyword.keyword?(val1) and Keyword.keyword?(val2)
end
defp can_be_merged?(_val1, _val2), do: false
end

View file

@ -6,68 +6,116 @@ def process(implementation, descriptions) do
implementation.process(descriptions)
end
@spec uploaders_list() :: [module()]
def uploaders_list do
{:ok, modules} = :application.get_key(:pleroma, :modules)
Enum.filter(modules, fn module ->
name_as_list = Module.split(module)
List.starts_with?(name_as_list, ["Pleroma", "Uploaders"]) and
List.last(name_as_list) != "Uploader"
end)
@spec list_modules_in_dir(String.t(), String.t()) :: [module()]
def list_modules_in_dir(dir, start) do
with {:ok, files} <- File.ls(dir) do
files
|> Enum.filter(&String.ends_with?(&1, ".ex"))
|> Enum.map(fn filename ->
module = filename |> String.trim_trailing(".ex") |> Macro.camelize()
String.to_existing_atom(start <> module)
end)
end
end
@spec filters_list() :: [module()]
def filters_list do
{:ok, modules} = :application.get_key(:pleroma, :modules)
Enum.filter(modules, fn module ->
name_as_list = Module.split(module)
List.starts_with?(name_as_list, ["Pleroma", "Upload", "Filter"])
end)
@doc """
Converts:
- atoms to strings with leading `:`
- module names to strings, without leading `Elixir.`
- add humanized labels to `keys` if label is not defined, e.g. `:instance` -> `Instance`
"""
@spec convert_to_strings([map()]) :: [map()]
def convert_to_strings(descriptions) do
Enum.map(descriptions, &format_entity(&1))
end
@spec mrf_list() :: [module()]
def mrf_list do
{:ok, modules} = :application.get_key(:pleroma, :modules)
Enum.filter(modules, fn module ->
name_as_list = Module.split(module)
List.starts_with?(name_as_list, ["Pleroma", "Web", "ActivityPub", "MRF"]) and
length(name_as_list) > 4
end)
defp format_entity(entity) do
entity
|> format_key()
|> Map.put(:group, atom_to_string(entity[:group]))
|> format_children()
end
@spec richmedia_parsers() :: [module()]
def richmedia_parsers do
{:ok, modules} = :application.get_key(:pleroma, :modules)
Enum.filter(modules, fn module ->
name_as_list = Module.split(module)
List.starts_with?(name_as_list, ["Pleroma", "Web", "RichMedia", "Parsers"]) and
length(name_as_list) == 5
end)
defp format_key(%{key: key} = entity) do
entity
|> Map.put(:key, atom_to_string(key))
|> Map.put(:label, entity[:label] || humanize(key))
end
defp format_key(%{group: group} = entity) do
Map.put(entity, :label, entity[:label] || humanize(group))
end
defp format_key(entity), do: entity
defp format_children(%{children: children} = entity) do
Map.put(entity, :children, Enum.map(children, &format_child(&1)))
end
defp format_children(entity), do: entity
defp format_child(%{suggestions: suggestions} = entity) do
entity
|> Map.put(:suggestions, format_suggestions(suggestions))
|> format_key()
|> format_group()
|> format_children()
end
defp format_child(entity) do
entity
|> format_key()
|> format_group()
|> format_children()
end
defp format_group(%{group: group} = entity) do
Map.put(entity, :group, format_suggestion(group))
end
defp format_group(entity), do: entity
defp atom_to_string(entity) when is_binary(entity), do: entity
defp atom_to_string(entity) when is_atom(entity), do: inspect(entity)
defp humanize(entity) do
string = inspect(entity)
if String.starts_with?(string, ":"),
do: Phoenix.Naming.humanize(entity),
else: string
end
defp format_suggestions([]), do: []
defp format_suggestions([suggestion | tail]) do
[format_suggestion(suggestion) | format_suggestions(tail)]
end
defp format_suggestion(entity) when is_atom(entity) do
atom_to_string(entity)
end
defp format_suggestion([head | tail] = entity) when is_list(entity) do
[format_suggestion(head) | format_suggestions(tail)]
end
defp format_suggestion(entity) when is_tuple(entity) do
format_suggestions(Tuple.to_list(entity)) |> List.to_tuple()
end
defp format_suggestion(entity), do: entity
end
defimpl Jason.Encoder, for: Tuple do
def encode(tuple, opts) do
Jason.Encode.list(Tuple.to_list(tuple), opts)
end
def encode(tuple, opts), do: Jason.Encode.list(Tuple.to_list(tuple), opts)
end
defimpl Jason.Encoder, for: [Regex, Function] do
def encode(term, opts) do
Jason.Encode.string(inspect(term), opts)
end
def encode(term, opts), do: Jason.Encode.string(inspect(term), opts)
end
defimpl String.Chars, for: Regex do
def to_string(term) do
inspect(term)
end
def to_string(term), do: inspect(term)
end

View file

@ -3,18 +3,22 @@ defmodule Pleroma.Docs.JSON do
@spec process(keyword()) :: {:ok, String.t()}
def process(descriptions) do
config_path = "docs/generate_config.json"
with {:ok, file} <- File.open(config_path, [:write, :utf8]),
json <- generate_json(descriptions),
with path <- "docs/generated_config.json",
{:ok, file} <- File.open(path, [:write, :utf8]),
formatted_descriptions <-
Pleroma.Docs.Generator.convert_to_strings(descriptions),
json <- Jason.encode!(formatted_descriptions),
:ok <- IO.write(file, json),
:ok <- File.close(file) do
{:ok, config_path}
{:ok, path}
end
end
@spec generate_json([keyword()]) :: String.t()
def generate_json(descriptions) do
Jason.encode!(descriptions)
def compile do
with config <- Pleroma.Config.Loader.load("config/description.exs") do
config[:pleroma][:config_description]
|> Pleroma.Docs.Generator.convert_to_strings()
|> Jason.encode!()
end
end
end

View file

@ -19,6 +19,8 @@ defmodule Pleroma.Object do
@type t() :: %__MODULE__{}
@derive {Jason.Encoder, only: [:data]}
schema "objects" do
field(:data, :map)
@ -180,85 +182,17 @@ def swap_object_with_tombstone(object) do
def delete(%Object{data: %{"id" => id}} = object) do
with {:ok, _obj} = swap_object_with_tombstone(object),
:ok <- delete_attachments(object),
deleted_activity = Activity.delete_all_by_object_ap_id(id),
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),
{:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do
{:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path),
{:ok, _} <-
Pleroma.Workers.AttachmentsCleanupWorker.enqueue("cleanup_attachments", %{
"object" => object
}) do
{:ok, object, deleted_activity}
end
end
defp delete_attachments(%{data: %{"attachment" => [_ | _] = attachments, "actor" => actor}}) do
hrefs =
Enum.flat_map(attachments, fn attachment ->
Enum.map(attachment["url"], & &1["href"])
end)
names = Enum.map(attachments, & &1["name"])
uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
# find all objects for copies of the attachments, name and actor doesn't matter here
delete_ids =
from(o in Object,
where:
fragment(
"to_jsonb(array(select jsonb_array_elements((?)#>'{url}') ->> 'href'))::jsonb \\?| (?)",
o.data,
^hrefs
)
)
|> Repo.all()
# we should delete 1 object for any given attachment, but don't delete files if
# there are more than 1 object for it
|> Enum.reduce(%{}, fn %{
id: id,
data: %{
"url" => [%{"href" => href}],
"actor" => obj_actor,
"name" => name
}
},
acc ->
Map.update(acc, href, %{id: id, count: 1}, fn val ->
case obj_actor == actor and name in names do
true ->
# set id of the actor's object that will be deleted
%{val | id: id, count: val.count + 1}
false ->
# another actor's object, just increase count to not delete file
%{val | count: val.count + 1}
end
end)
end)
|> Enum.map(fn {href, %{id: id, count: count}} ->
# only delete files that have single instance
with 1 <- count do
prefix =
case Pleroma.Config.get([Pleroma.Upload, :base_url]) do
nil -> "media"
_ -> ""
end
base_url = Pleroma.Config.get([__MODULE__, :base_url], Pleroma.Web.base_url())
file_path = String.trim_leading(href, "#{base_url}/#{prefix}")
uploader.delete_file(file_path)
end
id
end)
from(o in Object, where: o.id in ^delete_ids)
|> Repo.delete_all()
:ok
end
defp delete_attachments(%{data: _data}), do: :ok
def prune(%Object{data: %{"id" => id}} = object) do
with {:ok, object} <- Repo.delete(object),
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),

View file

@ -312,19 +312,12 @@ def make_emoji_reaction_data(user, object, emoji, activity_id) do
|> Map.put("content", emoji)
end
@spec update_element_in_object(String.t(), list(any), Object.t()) ::
@spec update_element_in_object(String.t(), list(any), Object.t(), integer() | nil) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def update_element_in_object(property, element, object) do
def update_element_in_object(property, element, object, count \\ nil) do
length =
if is_map(element) do
element
|> Map.values()
|> List.flatten()
|> length()
else
element
|> length()
end
count ||
length(element)
data =
Map.merge(
@ -344,29 +337,52 @@ def add_emoji_reaction_to_object(
%Activity{data: %{"content" => emoji, "actor" => actor}},
object
) do
reactions = object.data["reactions"] || %{}
emoji_actors = reactions[emoji] || []
new_emoji_actors = [actor | emoji_actors] |> Enum.uniq()
new_reactions = Map.put(reactions, emoji, new_emoji_actors)
update_element_in_object("reaction", new_reactions, object)
reactions = object.data["reactions"] || []
new_reactions =
case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do
nil ->
reactions ++ [[emoji, [actor]]]
index ->
List.update_at(
reactions,
index,
fn [emoji, users] -> [emoji, Enum.uniq([actor | users])] end
)
end
count = emoji_count(new_reactions)
update_element_in_object("reaction", new_reactions, object, count)
end
def emoji_count(reactions_list) do
Enum.reduce(reactions_list, 0, fn [_, users], acc -> acc + length(users) end)
end
def remove_emoji_reaction_from_object(
%Activity{data: %{"content" => emoji, "actor" => actor}},
object
) do
reactions = object.data["reactions"] || %{}
emoji_actors = reactions[emoji] || []
new_emoji_actors = List.delete(emoji_actors, actor)
reactions = object.data["reactions"] || []
new_reactions =
if new_emoji_actors == [] do
Map.delete(reactions, emoji)
else
Map.put(reactions, emoji, new_emoji_actors)
case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do
nil ->
reactions
index ->
List.update_at(
reactions,
index,
fn [emoji, users] -> [emoji, List.delete(users, actor)] end
)
|> Enum.reject(fn [_, users] -> Enum.empty?(users) end)
end
update_element_in_object("reaction", new_reactions, object)
count = emoji_count(new_reactions)
update_element_in_object("reaction", new_reactions, object, count)
end
@spec add_like_to_object(Activity.t(), Object.t()) ::

View file

@ -4,7 +4,11 @@
defmodule Pleroma.Web.AdminAPI.AdminAPIController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
alias Pleroma.Activity
alias Pleroma.ConfigDB
alias Pleroma.ModerationLog
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.ReportNote
@ -14,7 +18,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.Config
alias Pleroma.Web.AdminAPI.ConfigView
alias Pleroma.Web.AdminAPI.ModerationLogView
alias Pleroma.Web.AdminAPI.Report
@ -25,10 +28,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.Router
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
require Logger
@descriptions_json Pleroma.Docs.JSON.compile()
@users_page_size 50
plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"], admin: true}
@ -93,7 +97,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug(
OAuthScopesPlug,
%{scopes: ["read"], admin: true}
when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log]
when action in [:config_show, :migrate_from_db, :list_log]
)
plug(
@ -102,8 +106,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
when action == :config_update
)
@users_page_size 50
action_fallback(:errors)
def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
@ -785,49 +787,132 @@ def list_log(conn, params) do
|> render("index.json", %{log: log})
end
def migrate_to_db(conn, _params) do
Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
json(conn, %{})
def config_descriptions(conn, _params) do
conn
|> Plug.Conn.put_resp_content_type("application/json")
|> Plug.Conn.send_resp(200, @descriptions_json)
end
def migrate_from_db(conn, _params) do
Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "true"])
json(conn, %{})
with :ok <- configurable_from_database(conn) do
Mix.Tasks.Pleroma.Config.run([
"migrate_from_db",
"--env",
to_string(Pleroma.Config.get(:env)),
"-d"
])
json(conn, %{})
end
end
def config_show(conn, %{"only_db" => true}) do
with :ok <- configurable_from_database(conn) do
configs = Pleroma.Repo.all(ConfigDB)
if configs == [] do
errors(
conn,
{:error, "To use configuration from database migrate your settings to database."}
)
else
conn
|> put_view(ConfigView)
|> render("index.json", %{configs: configs})
end
end
end
def config_show(conn, _params) do
configs = Pleroma.Repo.all(Config)
with :ok <- configurable_from_database(conn) do
configs = ConfigDB.get_all_as_keyword()
conn
|> put_view(ConfigView)
|> render("index.json", %{configs: configs})
if configs == [] do
errors(
conn,
{:error, "To use configuration from database migrate your settings to database."}
)
else
merged =
Pleroma.Config.Holder.config()
|> ConfigDB.merge(configs)
|> Enum.map(fn {group, values} ->
Enum.map(values, fn {key, value} ->
db =
if configs[group][key] do
ConfigDB.get_db_keys(configs[group][key], key)
end
db_value = configs[group][key]
merged_value =
if !is_nil(db_value) and Keyword.keyword?(db_value) and
ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
ConfigDB.merge_group(group, key, value, db_value)
else
value
end
setting = %{
group: ConfigDB.convert(group),
key: ConfigDB.convert(key),
value: ConfigDB.convert(merged_value)
}
if db, do: Map.put(setting, :db, db), else: setting
end)
end)
|> List.flatten()
json(conn, %{configs: merged})
end
end
end
def config_update(conn, %{"configs" => configs}) do
updated =
if Pleroma.Config.get([:instance, :dynamic_configuration]) do
updated =
Enum.map(configs, fn
%{"group" => group, "key" => key, "delete" => "true"} = params ->
{:ok, config} = Config.delete(%{group: group, key: key, subkeys: params["subkeys"]})
config
with :ok <- configurable_from_database(conn) do
{_errors, results} =
Enum.map(configs, fn
%{"group" => group, "key" => key, "delete" => true} = params ->
ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
%{"group" => group, "key" => key, "value" => value} ->
{:ok, config} = Config.update_or_create(%{group: group, key: key, value: value})
config
end)
|> Enum.reject(&is_nil(&1))
%{"group" => group, "key" => key, "value" => value} ->
ConfigDB.update_or_create(%{group: group, key: key, value: value})
end)
|> Enum.split_with(fn result -> elem(result, 0) == :error end)
Pleroma.Config.TransferTask.load_and_update_env()
Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "false"])
updated
else
[]
end
{deleted, updated} =
results
|> Enum.map(fn {:ok, config} ->
Map.put(config, :db, ConfigDB.get_db_keys(config))
end)
|> Enum.split_with(fn config ->
Ecto.get_meta(config, :state) == :deleted
end)
conn
|> put_view(ConfigView)
|> render("index.json", %{configs: updated})
Pleroma.Config.TransferTask.load_and_update_env(deleted)
Mix.Tasks.Pleroma.Config.run([
"migrate_from_db",
"--env",
to_string(Pleroma.Config.get(:env))
])
conn
|> put_view(ConfigView)
|> render("index.json", %{configs: updated})
end
end
defp configurable_from_database(conn) do
if Pleroma.Config.get(:configurable_from_database) do
:ok
else
errors(
conn,
{:error, "To use this endpoint you need to enable configuration from database."}
)
end
end
def reload_emoji(conn, _params) do

View file

@ -1,182 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.Config do
use Ecto.Schema
import Ecto.Changeset
import Pleroma.Web.Gettext
alias __MODULE__
alias Pleroma.Repo
@type t :: %__MODULE__{}
schema "config" do
field(:key, :string)
field(:group, :string)
field(:value, :binary)
timestamps()
end
@spec get_by_params(map()) :: Config.t() | nil
def get_by_params(params), do: Repo.get_by(Config, params)
@spec changeset(Config.t(), map()) :: Changeset.t()
def changeset(config, params \\ %{}) do
config
|> cast(params, [:key, :group, :value])
|> validate_required([:key, :group, :value])
|> unique_constraint(:key, name: :config_group_key_index)
end
@spec create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()}
def create(params) do
%Config{}
|> changeset(Map.put(params, :value, transform(params[:value])))
|> Repo.insert()
end
@spec update(Config.t(), map()) :: {:ok, Config} | {:error, Changeset.t()}
def update(%Config{} = config, %{value: value}) do
config
|> change(value: transform(value))
|> Repo.update()
end
@spec update_or_create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()}
def update_or_create(params) do
with %Config{} = config <- Config.get_by_params(Map.take(params, [:group, :key])) do
Config.update(config, params)
else
nil -> Config.create(params)
end
end
@spec delete(map()) :: {:ok, Config.t()} | {:error, Changeset.t()}
def delete(params) do
with %Config{} = config <- Config.get_by_params(Map.delete(params, :subkeys)) do
if params[:subkeys] do
updated_value =
Keyword.drop(
:erlang.binary_to_term(config.value),
Enum.map(params[:subkeys], &do_transform_string(&1))
)
Config.update(config, %{value: updated_value})
else
Repo.delete(config)
{:ok, nil}
end
else
nil ->
err =
dgettext("errors", "Config with params %{params} not found", params: inspect(params))
{:error, err}
end
end
@spec from_binary(binary()) :: term()
def from_binary(binary), do: :erlang.binary_to_term(binary)
@spec from_binary_with_convert(binary()) :: any()
def from_binary_with_convert(binary) do
from_binary(binary)
|> do_convert()
end
defp do_convert(entity) when is_list(entity) do
for v <- entity, into: [], do: do_convert(v)
end
defp do_convert(%Regex{} = entity), do: inspect(entity)
defp do_convert(entity) when is_map(entity) do
for {k, v} <- entity, into: %{}, do: {do_convert(k), do_convert(v)}
end
defp do_convert({:dispatch, [entity]}), do: %{"tuple" => [":dispatch", [inspect(entity)]]}
defp do_convert({:partial_chain, entity}), do: %{"tuple" => [":partial_chain", inspect(entity)]}
defp do_convert(entity) when is_tuple(entity),
do: %{"tuple" => do_convert(Tuple.to_list(entity))}
defp do_convert(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity),
do: entity
defp do_convert(entity) when is_atom(entity) do
string = to_string(entity)
if String.starts_with?(string, "Elixir."),
do: do_convert(string),
else: ":" <> string
end
defp do_convert("Elixir." <> module_name), do: module_name
defp do_convert(entity) when is_binary(entity), do: entity
@spec transform(any()) :: binary()
def transform(entity) when is_binary(entity) or is_map(entity) or is_list(entity) do
:erlang.term_to_binary(do_transform(entity))
end
def transform(entity), do: :erlang.term_to_binary(entity)
defp do_transform(%Regex{} = entity), do: entity
defp do_transform(%{"tuple" => [":dispatch", [entity]]}) do
{dispatch_settings, []} = do_eval(entity)
{:dispatch, [dispatch_settings]}
end
defp do_transform(%{"tuple" => [":partial_chain", entity]}) do
{partial_chain, []} = do_eval(entity)
{:partial_chain, partial_chain}
end
defp do_transform(%{"tuple" => entity}) do
Enum.reduce(entity, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end)
end
defp do_transform(entity) when is_map(entity) do
for {k, v} <- entity, into: %{}, do: {do_transform(k), do_transform(v)}
end
defp do_transform(entity) when is_list(entity) do
for v <- entity, into: [], do: do_transform(v)
end
defp do_transform(entity) when is_binary(entity) do
String.trim(entity)
|> do_transform_string()
end
defp do_transform(entity), do: entity
defp do_transform_string("~r/" <> pattern) do
modificator = String.split(pattern, "/") |> List.last()
pattern = String.trim_trailing(pattern, "/" <> modificator)
case modificator do
"" -> ~r/#{pattern}/
"i" -> ~r/#{pattern}/i
"u" -> ~r/#{pattern}/u
"s" -> ~r/#{pattern}/s
end
end
defp do_transform_string(":" <> atom), do: String.to_atom(atom)
defp do_transform_string(value) do
if String.starts_with?(value, "Pleroma") or String.starts_with?(value, "Phoenix"),
do: String.to_existing_atom("Elixir." <> value),
else: value
end
defp do_eval(entity) do
cleaned_string = String.replace(entity, ~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "")
Code.eval_string(cleaned_string, [], requires: [], macros: [])
end
end

View file

@ -12,10 +12,16 @@ def render("index.json", %{configs: configs}) do
end
def render("show.json", %{config: config}) do
%{
map = %{
key: config.key,
group: config.group,
value: Pleroma.Web.AdminAPI.Config.from_binary_with_convert(config.value)
value: Pleroma.ConfigDB.from_binary_with_convert(config.value)
}
if config.db != [] do
Map.put(map, :db, config.db)
else
map
end
end
end

View file

@ -255,12 +255,11 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
emoji_reactions =
with %{data: %{"reactions" => emoji_reactions}} <- object do
Enum.map(emoji_reactions, fn {emoji, users} ->
{emoji, length(users)}
Enum.map(emoji_reactions, fn [emoji, users] ->
[emoji, length(users)]
end)
|> Enum.into(%{})
else
_ -> %{}
_ -> []
end
%{

View file

@ -43,21 +43,21 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
%Object{data: %{"reactions" => emoji_reactions}} <- Object.normalize(activity) do
%Object{data: %{"reactions" => emoji_reactions}} when is_list(emoji_reactions) <-
Object.normalize(activity) do
reactions =
emoji_reactions
|> Enum.map(fn {emoji, users} ->
|> Enum.map(fn [emoji, users] ->
users = Enum.map(users, &User.get_cached_by_ap_id/1)
{emoji, AccountView.render("index.json", %{users: users, for: user, as: :user})}
end)
|> Enum.into(%{})
conn
|> json(reactions)
else
_e ->
conn
|> json(%{})
|> json([])
end
end

View file

@ -195,7 +195,7 @@ defmodule Pleroma.Web.Router do
get("/config", AdminAPIController, :config_show)
post("/config", AdminAPIController, :config_update)
get("/config/migrate_to_db", AdminAPIController, :migrate_to_db)
get("/config/descriptions", AdminAPIController, :config_descriptions)
get("/config/migrate_from_db", AdminAPIController, :migrate_from_db)
get("/moderation_log", AdminAPIController, :list_log)

View file

@ -0,0 +1,88 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.AttachmentsCleanupWorker do
import Ecto.Query
alias Pleroma.Object
alias Pleroma.Repo
use Pleroma.Workers.WorkerHelper, queue: "attachments_cleanup"
@impl Oban.Worker
def perform(
%{"object" => %{"data" => %{"attachment" => [_ | _] = attachments, "actor" => actor}}},
_job
) do
hrefs =
Enum.flat_map(attachments, fn attachment ->
Enum.map(attachment["url"], & &1["href"])
end)
names = Enum.map(attachments, & &1["name"])
uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
# find all objects for copies of the attachments, name and actor doesn't matter here
delete_ids =
from(o in Object,
where:
fragment(
"to_jsonb(array(select jsonb_array_elements((?)#>'{url}') ->> 'href' where jsonb_typeof((?)#>'{url}') = 'array'))::jsonb \\?| (?)",
o.data,
o.data,
^hrefs
)
)
# The query above can be time consumptive on large instances until we
# refactor how uploads are stored
|> Repo.all(timout: :infinity)
# we should delete 1 object for any given attachment, but don't delete
# files if there are more than 1 object for it
|> Enum.reduce(%{}, fn %{
id: id,
data: %{
"url" => [%{"href" => href}],
"actor" => obj_actor,
"name" => name
}
},
acc ->
Map.update(acc, href, %{id: id, count: 1}, fn val ->
case obj_actor == actor and name in names do
true ->
# set id of the actor's object that will be deleted
%{val | id: id, count: val.count + 1}
false ->
# another actor's object, just increase count to not delete file
%{val | count: val.count + 1}
end
end)
end)
|> Enum.map(fn {href, %{id: id, count: count}} ->
# only delete files that have single instance
with 1 <- count do
prefix =
case Pleroma.Config.get([Pleroma.Upload, :base_url]) do
nil -> "media"
_ -> ""
end
base_url = Pleroma.Config.get([__MODULE__, :base_url], Pleroma.Web.base_url())
file_path = String.trim_leading(href, "#{base_url}/#{prefix}")
uploader.delete_file(file_path)
end
id
end)
from(o in Object, where: o.id in ^delete_ids)
|> Repo.delete_all()
end
def perform(%{"object" => _object}, _job), do: :ok
end

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1 +1 @@
<!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"><meta name=renderer content=webkit><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><title>Admin FE</title><link rel="shortcut icon" href=favicon.ico><link href=chunk-elementUI.a842fb0a.css rel=stylesheet><link href=chunk-libs.57fe98a3.css rel=stylesheet><link href=app.fdd73ce4.css rel=stylesheet></head><body><div id=app></div><script type=text/javascript src=static/js/runtime.d6d1aaab.js></script><script type=text/javascript src=static/js/chunk-elementUI.fa319e7b.js></script><script type=text/javascript src=static/js/chunk-libs.35c18287.js></script><script type=text/javascript src=static/js/app.19b7049e.js></script></body></html>
<!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"><meta name=renderer content=webkit><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><title>Admin FE</title><link rel="shortcut icon" href=favicon.ico><link href=chunk-elementUI.1abbc9b8.css rel=stylesheet><link href=chunk-libs.57fe98a3.css rel=stylesheet><link href=app.fdd73ce4.css rel=stylesheet></head><body><div id=app></div><script type=text/javascript src=static/js/runtime.cab03b3e.js></script><script type=text/javascript src=static/js/chunk-elementUI.2de79b84.js></script><script type=text/javascript src=static/js/chunk-libs.680db3fc.js></script><script type=text/javascript src=static/js/app.3da0f475.js></script></body></html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

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