Merge branch 'develop' into features/remote-follow-userpage-redirect
This commit is contained in:
commit
ae78059ff4
@ -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
|
||||
|
@ -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
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
79
docs/admin/config.md
Normal 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
|
||||
```
|
@ -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]
|
||||
```
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
422
lib/pleroma/config/config_db.ex
Normal file
422
lib/pleroma/config/config_db.ex
Normal 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
|
16
lib/pleroma/config/holder.ex
Normal file
16
lib/pleroma/config/holder.ex
Normal 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
|
59
lib/pleroma/config/loader.ex
Normal file
59
lib/pleroma/config/loader.ex
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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}"),
|
||||
|
@ -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()) ::
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
%{
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
88
lib/pleroma/workers/attachments_cleanup_worker.ex
Normal file
88
lib/pleroma/workers/attachments_cleanup_worker.ex
Normal 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
|
BIN
priv/static/adminfe/chunk-15fa.5a5f973d.css
Normal file
BIN
priv/static/adminfe/chunk-15fa.5a5f973d.css
Normal file
Binary file not shown.
BIN
priv/static/adminfe/chunk-18e1.dd09fe2e.css
Normal file
BIN
priv/static/adminfe/chunk-18e1.dd09fe2e.css
Normal file
Binary file not shown.
BIN
priv/static/adminfe/chunk-1ada.90dffac4.css
Normal file
BIN
priv/static/adminfe/chunk-1ada.90dffac4.css
Normal file
Binary file not shown.
BIN
priv/static/adminfe/chunk-2aa6.8ce63ac0.css
Normal file
BIN
priv/static/adminfe/chunk-2aa6.8ce63ac0.css
Normal file
Binary file not shown.
BIN
priv/static/adminfe/chunk-3d8e.48523459.css
Normal file
BIN
priv/static/adminfe/chunk-3d8e.48523459.css
Normal file
Binary file not shown.
BIN
priv/static/adminfe/chunk-4dc2.25d3bcab.css
Normal file
BIN
priv/static/adminfe/chunk-4dc2.25d3bcab.css
Normal file
Binary file not shown.
BIN
priv/static/adminfe/chunk-6b7b.1ab4da3b.css
Normal file
BIN
priv/static/adminfe/chunk-6b7b.1ab4da3b.css
Normal file
Binary file not shown.
BIN
priv/static/adminfe/chunk-6e77.57276c93.css
Normal file
BIN
priv/static/adminfe/chunk-6e77.57276c93.css
Normal file
Binary file not shown.
BIN
priv/static/adminfe/chunk-7f83.f9f73c8e.css
Normal file
BIN
priv/static/adminfe/chunk-7f83.f9f73c8e.css
Normal file
Binary file not shown.
BIN
priv/static/adminfe/chunk-7f8e.1d10e1c7.css
Normal file
BIN
priv/static/adminfe/chunk-7f8e.1d10e1c7.css
Normal file
Binary file not shown.
BIN
priv/static/adminfe/chunk-elementUI.1abbc9b8.css
Normal file
BIN
priv/static/adminfe/chunk-elementUI.1abbc9b8.css
Normal file
Binary file not shown.
Binary file not shown.
@ -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.
BIN
priv/static/adminfe/static/js/app.3da0f475.js
Normal file
BIN
priv/static/adminfe/static/js/app.3da0f475.js
Normal file
Binary file not shown.
BIN
priv/static/adminfe/static/js/app.3da0f475.js.map
Normal file
BIN
priv/static/adminfe/static/js/app.3da0f475.js.map
Normal file
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.
BIN
priv/static/adminfe/static/js/chunk-2aa6.be23b313.js
Normal file
BIN
priv/static/adminfe/static/js/chunk-2aa6.be23b313.js
Normal file
Binary file not shown.
BIN
priv/static/adminfe/static/js/chunk-2aa6.be23b313.js.map
Normal file
BIN
priv/static/adminfe/static/js/chunk-2aa6.be23b313.js.map
Normal file
Binary file not shown.
BIN
priv/static/adminfe/static/js/chunk-3d8e.916ea1c1.js
Normal file
BIN
priv/static/adminfe/static/js/chunk-3d8e.916ea1c1.js
Normal file
Binary file not shown.
BIN
priv/static/adminfe/static/js/chunk-3d8e.916ea1c1.js.map
Normal file
BIN
priv/static/adminfe/static/js/chunk-3d8e.916ea1c1.js.map
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
priv/static/adminfe/static/js/chunk-6b7b.e7ed7973.js
Normal file
BIN
priv/static/adminfe/static/js/chunk-6b7b.e7ed7973.js
Normal file
Binary file not shown.
BIN
priv/static/adminfe/static/js/chunk-6b7b.e7ed7973.js.map
Normal file
BIN
priv/static/adminfe/static/js/chunk-6b7b.e7ed7973.js.map
Normal file
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.
BIN
priv/static/adminfe/static/js/chunk-elementUI.2de79b84.js
Normal file
BIN
priv/static/adminfe/static/js/chunk-elementUI.2de79b84.js
Normal file
Binary file not shown.
BIN
priv/static/adminfe/static/js/chunk-elementUI.2de79b84.js.map
Normal file
BIN
priv/static/adminfe/static/js/chunk-elementUI.2de79b84.js.map
Normal file
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.
BIN
priv/static/adminfe/static/js/chunk-libs.680db3fc.js.map
Normal file
BIN
priv/static/adminfe/static/js/chunk-libs.680db3fc.js.map
Normal file
Binary file not shown.
BIN
priv/static/adminfe/static/js/runtime.cab03b3e.js
Normal file
BIN
priv/static/adminfe/static/js/runtime.cab03b3e.js
Normal file
Binary file not shown.
BIN
priv/static/adminfe/static/js/runtime.cab03b3e.js.map
Normal file
BIN
priv/static/adminfe/static/js/runtime.cab03b3e.js.map
Normal file
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.
BIN
priv/static/packs/core/auth.js
Normal file
BIN
priv/static/packs/core/auth.js
Normal file
Binary file not shown.
BIN
priv/static/packs/core/auth.js.map
Normal file
BIN
priv/static/packs/core/auth.js.map
Normal file
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
Loading…
Reference in New Issue
Block a user