Merge branch 'develop' into gun

This commit is contained in:
Alexander Strizhakov 2020-02-29 11:34:50 +03:00
commit 814b275af7
No known key found for this signature in database
GPG Key ID: 022896A53AEF1381
208 changed files with 1680 additions and 429 deletions

View File

@ -76,6 +76,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- OAuth: admin scopes support (relevant setting: `[:auth, :enforce_oauth_admin_scope_usage]`).
- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma wont start. For hackney OTP update is not required.
- Add an option `authorized_fetch_mode` to require HTTP signatures for AP fetches.
- ActivityPub: support for `replies` collection (output for outgoing federation & fetching on incoming federation).
- Mix task to refresh counter cache (`mix pleroma.refresh_counter_cache`)
<details>
<summary>API Changes</summary>
@ -103,6 +105,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Configuration: `feed` option for user atom feed.
- Pleroma API: Add Emoji reactions
- Admin API: Add `/api/pleroma/admin/instances/:instance/statuses` - lists all statuses from a given instance
- Admin API: Add `/api/pleroma/admin/users/:nickname/statuses` - lists all statuses from a given user
- Admin API: `PATCH /api/pleroma/users/confirm_email` to confirm email for multiple users, `PATCH /api/pleroma/users/resend_confirmation_email` to resend confirmation email for multiple users
- ActivityPub: Configurable `type` field of the actors.
- Mastodon API: `/api/v1/accounts/:id` has `source/pleroma/actor_type` field.
@ -118,6 +121,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Tag feed: `/tags/:tag.rss` - list public statuses by hashtag.
- Mastodon API: Add `reacted` property to `emoji_reactions`
- Pleroma API: Add reactions for a single emoji.
- ActivityPub: `[:activitypub, :note_replies_output_limit]` setting sets the number of note self-replies to output on outgoing federation.
- Admin API: `GET /api/pleroma/admin/stats` to get status count by visibility scope
- Admin API: `GET /api/pleroma/admin/statuses` - list all statuses (accepts `godmode` and `local_only`)
</details>
### Fixed

View File

@ -198,6 +198,8 @@
max_expiration: 365 * 24 * 60 * 60
},
registrations_open: true,
invites_enabled: false,
account_activation_required: false,
federating: true,
federation_incoming_replies_max_depth: 100,
federation_reachability_timeout_days: 7,
@ -305,6 +307,7 @@
unfollow_blocked: true,
outgoing_blocks: true,
follow_handshake_timeout: 500,
note_replies_output_limit: 5,
sign_object_fetches: true,
authorized_fetch_mode: false
@ -378,6 +381,8 @@
config :phoenix, :json_library, Jason
config :phoenix, :filter_parameters, ["password", "confirm"]
config :pleroma, :gopher,
enabled: false,
ip: {0, 0, 0, 0},
@ -460,6 +465,7 @@
transmogrifier: 20,
scheduled_activities: 10,
background: 5,
remote_fetcher: 2,
attachments_cleanup: 5,
new_users_digest: 1
],

View File

@ -662,7 +662,7 @@
label: "Fed. incoming replies max depth",
type: :integer,
description:
"Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while" <>
"Max. depth of reply-to and reply activities fetching on incoming federation, to prevent out-of-memory situations while" <>
" fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.",
suggestions: [
100
@ -1615,160 +1615,6 @@
}
]
},
%{
group: :pleroma,
key: Pleroma.Web.Endpoint,
type: :group,
description: "Phoenix endpoint configuration",
children: [
%{
key: :http,
label: "HTTP",
type: {:keyword, :integer, :tuple},
description: "http protocol configuration",
suggestions: [
port: 8080,
ip: {127, 0, 0, 1}
],
children: [
%{
key: :dispatch,
type: {:list, :tuple},
description: "dispatch settings",
suggestions: [
{:_,
[
{"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
{"/websocket", Phoenix.Endpoint.CowboyWebSocket,
{Phoenix.Transports.WebSocket,
{Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}},
{:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
]}
# end copied from config.exs
]
},
%{
key: :ip,
label: "IP",
type: :tuple,
description: "ip",
suggestions: [
{0, 0, 0, 0}
]
},
%{
key: :port,
type: :integer,
description: "port",
suggestions: [
2020
]
}
]
},
%{
key: :url,
label: "URL",
type: {:keyword, :string, :integer},
description: "configuration for generating urls",
suggestions: [
host: "example.com",
port: 2020,
scheme: "https"
],
children: [
%{
key: :host,
type: :string,
description: "Host",
suggestions: [
"example.com"
]
},
%{
key: :port,
type: :integer,
description: "port",
suggestions: [
2020
]
},
%{
key: :scheme,
type: :string,
description: "Scheme",
suggestions: [
"https",
"https"
]
}
]
},
%{
key: :instrumenters,
type: {:list, :module},
suggestions: [Pleroma.Web.Endpoint.Instrumenter]
},
%{
key: :protocol,
type: :string,
suggestions: ["https"]
},
%{
key: :secret_key_base,
type: :string,
suggestions: ["aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl"]
},
%{
key: :signing_salt,
type: :string,
suggestions: ["CqaoopA2"]
},
%{
key: :render_errors,
type: :keyword,
suggestions: [view: Pleroma.Web.ErrorView, accepts: ~w(json)],
children: [
%{
key: :view,
type: :module,
suggestions: [Pleroma.Web.ErrorView]
},
%{
key: :accepts,
type: {:list, :string},
suggestions: ["json"]
}
]
},
%{
key: :pubsub,
type: :keyword,
suggestions: [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2],
children: [
%{
key: :name,
type: :module,
suggestions: [Pleroma.PubSub]
},
%{
key: :adapter,
type: :module,
suggestions: [Phoenix.PubSub.PG2]
}
]
},
%{
key: :secure_cookie_flag,
type: :boolean
},
%{
key: :extra_cookie_attrs,
type: {:list, :string},
suggestions: ["SameSite=Lax"]
}
]
},
%{
group: :pleroma,
key: :activitypub,
@ -1790,6 +1636,12 @@
type: :boolean,
description: "Sign object fetches with HTTP signatures"
},
%{
key: :note_replies_output_limit,
type: :integer,
description:
"The number of Note replies' URIs to be included with outgoing federation (`5` to match Mastodon hardcoded value, `0` to disable the output)."
},
%{
key: :follow_handshake_timeout,
type: :integer,
@ -2588,19 +2440,6 @@
}
]
},
%{
group: :pleroma,
key: :database,
type: :group,
description: "Database related settings",
children: [
%{
key: :rum_enabled,
type: :boolean,
description: "If RUM indexes should be used. Default: disabled"
}
]
},
%{
group: :pleroma,
key: :rate_limit,
@ -2764,20 +2603,6 @@
}
]
},
%{
group: :prometheus,
key: Pleroma.Web.Endpoint.MetricsExporter,
type: :group,
description: "Prometheus settings",
children: [
%{
key: :path,
type: :string,
description: "API endpoint with metrics",
suggestions: ["/api/pleroma/app_metrics"]
}
]
},
%{
group: :http_signatures,
type: :group,
@ -3045,7 +2870,7 @@
group: :pleroma,
key: :feed,
type: :group,
description: "Configure feed rendering.",
description: "Configure feed rendering",
children: [
%{
key: :post_title,
@ -3095,7 +2920,7 @@
group: :pleroma,
key: :modules,
type: :group,
description: "Custom Runtime Modules.",
description: "Custom Runtime Modules",
children: [
%{
key: :runtime_dir,
@ -3103,18 +2928,5 @@
description: "A path to custom Elixir modules (such as MRF policies)."
}
]
},
%{
group: :pleroma,
type: :group,
description: "Allow instance configuration from database.",
children: [
%{
key: :configurable_from_database,
type: :boolean,
description:
"Allow transferring configuration to DB with the subsequent customization from Admin api. Default: disabled"
}
]
}
]

View File

@ -260,10 +260,24 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- `nickname` or `id`
- *optional* `page_size`: number of statuses to return (default is `20`)
- *optional* `godmode`: `true`/`false` allows to see private statuses
- *optional* `with_reblogs`: `true`/`false` allows to see reblogs (default is false)
- Response:
- On failure: `Not found`
- On success: JSON array of user's latest statuses
## `GET /api/pleroma/admin/instances/:instance/statuses`
### Retrive instance's latest statuses
- Params:
- `instance`: instance name
- *optional* `page_size`: number of statuses to return (default is `20`)
- *optional* `godmode`: `true`/`false` allows to see private statuses
- *optional* `with_reblogs`: `true`/`false` allows to see reblogs (default is false)
- Response:
- On failure: `Not found`
- On success: JSON array of instance's latest statuses
## `POST /api/pleroma/admin/relay`
### Follow a Relay
@ -941,3 +955,20 @@ Loads json generated from `config/descriptions.exs`.
- Params:
- `nicknames`
- Response: Array of user nicknames
## `GET /api/pleroma/admin/stats`
### Stats
- Response:
```json
{
"status_visibility": {
"direct": 739,
"private": 9,
"public": 17,
"unlisted": 14
}
}
```

View File

@ -18,7 +18,9 @@
6. Run `sudo -Hu postgres pg_restore -d <pleroma_db> -v -1 </path/to/backup_location/pleroma.pgdump>`
7. If you installed a newer Pleroma version, you should run `mix ecto.migrate`[^1]. This task performs database migrations, if there were any.
8. Restart the Pleroma service.
9. After you've restarted Pleroma, you will notice that postgres will take up more cpu resources than usual. A lot in fact. To fix this you must do a VACUUM ANLAYZE. This can also be done while the instance is still running like so:
$ sudo -u postgres psql pleroma_database_name
pleroma=# VACUUM ANALYZE;
[^1]: Prefix with `MIX_ENV=prod` to run it using the production config file.
## Remove

View File

@ -0,0 +1,74 @@
# Theming your instance
To add a custom theme to your instance, you'll first need to get a custom theme, upload it to the server, make it available to the instance and eventually you can set it as default.
## Getting a custom theme
### Create your own theme
* You can create your own theme using the Pleroma FE by going to settings (gear on the top right) and choose the Theme tab. Here you have the options to create a personal theme.
* To download your theme, you can do Save preset
* If you want to upload a theme to customise it further, you can upload it using Load preset
This will only save the theme for you personally. To make it available to the whole instance, you'll need to upload it to the server.
### Get an existing theme
* You can download a theme from another instance by going to that instance, go to settings and make sure you have the theme selected that you want. Then you can do Save preset to download it.
* You can also find and download custom themes at <https://plthemes.vulpes.one/>
## Adding the custom theme to the instance
### Upload the theme to the server
Themes can be found in the [static directory](static_dir.md). Create `STATIC-DIR/static/themes/` if needed and copy your theme there. Next you need to add an entry for your theme to `STATIC-DIR/static/styles.json`. If you use a from source installation, you'll first need to copy the file from `priv/static/static/styles.json`.
Example of `styles.json` where we add our own `my-awesome-theme.json`
```json
{
"pleroma-dark": [ "Pleroma Dark", "#121a24", "#182230", "#b9b9ba", "#d8a070", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
"pleroma-light": [ "Pleroma Light", "#f2f4f6", "#dbe0e8", "#304055", "#f86f0f", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
"classic-dark": [ "Classic Dark", "#161c20", "#282e32", "#b9b9b9", "#baaa9c", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
"bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"],
"ir-black": [ "Ir Black", "#000000", "#242422", "#b5b3aa", "#ff6c60", "#FF6C60", "#A8FF60", "#96CBFE", "#FFFFB6" ],
"monokai": [ "Monokai", "#272822", "#383830", "#f8f8f2", "#f92672", "#F92672", "#a6e22e", "#66d9ef", "#f4bf75" ],
"redmond-xx": "/static/themes/redmond-xx.json",
"redmond-xx-se": "/static/themes/redmond-xx-se.json",
"redmond-xxi": "/static/themes/redmond-xxi.json",
"breezy-dark": "/static/themes/breezy-dark.json",
"breezy-light": "/static/themes/breezy-light.json",
"mammal": "/static/themes/mammal.json",
"my-awesome-theme": "/static/themes/my-awesome-theme.json"
}
```
Now you'll already be able to select the theme in Pleroma FE from the drop-down. You don't need to restart Pleroma because we only changed static served files. You may need to refresh the page in your browser. You'll notice however that the theme doesn't have a name, it's just an empty entry in the drop-down.
### Give the theme a name
When you open one of the themes that ship with Pleroma, you'll notice that the json has a `"name"` key. Add a key-value pair to your theme where the key name is `"name"` and the value the name you want to give your theme. After this you can refresh te page in your browser and the name should be visible in the drop-down.
Example of `my-awesome-theme.json` where we add the name "My Awesome Theme"
```json
{
"_pleroma_theme_version": 2,
"name": "My Awesome Theme",
"theme": {}
}
```
### Set as default theme
Now we can set the new theme as default in the [Pleroma FE configuration](General-tips-for-customizing-Pleroma-FE.md).
Example of adding the new theme in the back-end config files
```elixir
config :pleroma, :frontend_configurations,
pleroma_fe: %{
theme: "my-awesome-theme"
}
```
If you added it in the back-end configuration file, you'll need to restart your instance for the changes to take effect. If you don't see the changes, it's probably because the browser has cached the previous theme. In that case you'll want to clear browser caches. Alternatively you can use a private/incognito window just to see the changes.

View File

@ -41,7 +41,7 @@ On the top right you will also see a wrench icon. This opens your personal setti
This is where the interesting stuff happens!
Depending on the timeline you will see different statuses, but each status has a standard structure:
- Profile pic, name and link to profile. An optional left-arrow if it's a reply to another status (hovering will reveal the replied-to status). Clicking on the profile pic will uncollapse the user's profile.
- Profile pic, name and link to profile. An optional left-arrow if it's a reply to another status (hovering will reveal the reply-to status). Clicking on the profile pic will uncollapse the user's profile.
- A `+` button on the right allows you to Expand/Collapse an entire discussion thread. It also updates in realtime!
- An arrow icon allows you to open the status on the instance where it's originating from.
- The text of the status, including mentions and attachements. If you click on a mention, it will automatically open the profile page of that person.

View File

@ -185,11 +185,7 @@ def run(["gen-pack", src]) do
tmp_pack_dir = Path.join(System.tmp_dir!(), "emoji-pack-#{name}")
{:ok, _} =
:zip.unzip(
binary_archive,
cwd: tmp_pack_dir
)
{:ok, _} = :zip.unzip(binary_archive, cwd: String.to_charlist(tmp_pack_dir))
emoji_map = Pleroma.Emoji.Loader.make_shortcode_to_file_map(tmp_pack_dir, exts)

View File

@ -6,6 +6,8 @@ defmodule Mix.Tasks.Pleroma.Instance do
use Mix.Task
import Mix.Pleroma
alias Pleroma.Config
@shortdoc "Manages Pleroma instance"
@moduledoc File.read!("docs/administration/CLI_tasks/instance.md")
@ -63,7 +65,8 @@ def run(["gen" | rest]) do
get_option(
options,
:instance_name,
"What is the name of your instance? (e.g. Pleroma/Soykaf)"
"What is the name of your instance? (e.g. The Corndog Emporium)",
domain
)
email = get_option(options, :admin_email, "What is your admin email address?")
@ -153,6 +156,8 @@ def run(["gen" | rest]) do
Pleroma.Config.get([:instance, :static_dir])
)
Config.put([:instance, :static_dir], static_dir)
secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
jwt_secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
@ -202,8 +207,14 @@ def run(["gen" | rest]) do
write_robots_txt(indexable, template_dir)
shell_info(
"\n All files successfully written! Refer to the installation instructions for your platform for next steps"
"\n All files successfully written! Refer to the installation instructions for your platform for next steps."
)
if db_configurable? do
shell_info(
" Please transfer your config to the database after running database migrations. Refer to \"Transfering the config to/from the database\" section of the docs for more information."
)
end
else
shell_error(
"The task would have overwritten the following files:\n" <>

View File

@ -0,0 +1,46 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.RefreshCounterCache do
@shortdoc "Refreshes counter cache"
use Mix.Task
alias Pleroma.Activity
alias Pleroma.CounterCache
alias Pleroma.Repo
require Logger
import Ecto.Query
def run([]) do
Mix.Pleroma.start_pleroma()
["public", "unlisted", "private", "direct"]
|> Enum.each(fn visibility ->
count = status_visibility_count_query(visibility)
name = "status_visibility_#{visibility}"
CounterCache.set(name, count)
Mix.Pleroma.shell_info("Set #{name} to #{count}")
end)
Mix.Pleroma.shell_info("Done")
end
defp status_visibility_count_query(visibility) do
Activity
|> where(
[a],
fragment(
"activity_visibility(?, ?, ?) = ?",
a.actor,
a.recipients,
a.data,
^visibility
)
)
|> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data))
|> Repo.aggregate(:count, :id, timeout: :timer.minutes(30))
end
end

View File

@ -100,8 +100,7 @@ def run(["rm", nickname]) do
User.perform(:delete, user)
shell_info("User #{nickname} deleted.")
else
_ ->
shell_error("No local user #{nickname}")
_ -> shell_error("No local user #{nickname}")
end
end

View File

@ -310,7 +310,7 @@ def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
def restrict_deactivated_users(query) do
deactivated_users =
from(u in User.Query.build(deactivated: true), select: u.ap_id)
from(u in User.Query.build(%{deactivated: true}), select: u.ap_id)
|> Repo.all()
Activity.Queries.exclude_authors(query, deactivated_users)

View File

@ -7,7 +7,7 @@ defmodule Pleroma.Activity.Queries do
Contains queries for Activity.
"""
import Ecto.Query, only: [from: 2]
import Ecto.Query, only: [from: 2, where: 3]
@type query :: Ecto.Queryable.t() | Activity.t()
@ -30,7 +30,7 @@ def by_actor(query \\ Activity, actor) do
)
end
@spec by_author(query, String.t()) :: query
@spec by_author(query, User.t()) :: query
def by_author(query \\ Activity, %User{ap_id: ap_id}) do
from(a in query, where: a.actor == ^ap_id)
end
@ -63,6 +63,22 @@ def by_object_id(query, object_id) when is_binary(object_id) do
)
end
@spec by_object_in_reply_to_id(query, String.t(), keyword()) :: query
def by_object_in_reply_to_id(query, in_reply_to_id, opts \\ []) do
query =
if opts[:skip_preloading] do
Activity.with_joined_object(query)
else
Activity.with_preloaded_object(query)
end
where(
query,
[activity, object: o],
fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(in_reply_to_id))
)
end
@spec by_type(query, String.t()) :: query
def by_type(query \\ Activity, activity_type) do
from(

View File

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha do
@ -50,7 +50,7 @@ def handle_call(:new, _from, state) do
token = new_captcha[:token]
secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt")
sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign")
# Basicallty copy what Phoenix.Token does here, add the time to
# Basically copy what Phoenix.Token does here, add the time to
# the actual data and make it a binary to then encrypt it
encrypted_captcha_answer =
%{
@ -62,7 +62,7 @@ def handle_call(:new, _from, state) do
{
:reply,
# Repalce the answer with the encrypted answer
# Replace the answer with the encrypted answer
%{new_captcha | answer_data: encrypted_captcha_answer},
state
}
@ -82,7 +82,8 @@ def handle_call({:validate, token, captcha, answer_data}, _from, state) do
valid_if_after = DateTime.subtract!(DateTime.now_utc(), seconds_valid)
result =
with {:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret),
with false <- is_nil(answer_data),
{:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret),
%{at: at, answer_data: answer_md5} <- :erlang.binary_to_term(data) do
try do
if DateTime.before?(at, valid_if_after),

View File

@ -10,8 +10,8 @@ defmodule Pleroma.Captcha.Native do
@impl Service
def new do
case Captcha.get() do
{:timeout} ->
%{error: dgettext("errors", "Captcha timeout")}
:error ->
%{error: dgettext("errors", "Captcha error")}
{:ok, answer_data, img_binary} ->
%{

View File

@ -133,10 +133,8 @@ def restrict_recipients(query, user, %{"recipients" => user_ids}) do
[user.id | user_ids]
|> Enum.uniq()
|> Enum.reduce([], fn user_id, acc ->
case FlakeId.Ecto.CompatType.dump(user_id) do
{:ok, user_id} -> [user_id | acc]
_ -> acc
end
{:ok, user_id} = FlakeId.Ecto.CompatType.dump(user_id)
[user_id | acc]
end)
conversation_subquery =

View File

@ -0,0 +1,41 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.CounterCache do
alias Pleroma.CounterCache
alias Pleroma.Repo
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
schema "counter_cache" do
field(:name, :string)
field(:count, :integer)
end
def changeset(struct, params) do
struct
|> cast(params, [:name, :count])
|> validate_required([:name])
|> unique_constraint(:name)
end
def get_as_map(names) when is_list(names) do
CounterCache
|> where([cc], cc.name in ^names)
|> Repo.all()
|> Enum.group_by(& &1.name, & &1.count)
|> Map.new(fn {k, v} -> {k, hd(v)} end)
end
def set(name, count) do
%CounterCache{}
|> changeset(%{"name" => name, "count" => count})
|> Repo.insert(
on_conflict: [set: [count: count]],
returning: true,
conflict_target: :name
)
end
end

View File

@ -9,7 +9,7 @@ defmodule Pleroma.MIME do
@default "application/octet-stream"
@read_bytes 35
@spec file_mime_type(String.t()) ::
@spec file_mime_type(String.t(), String.t()) ::
{:ok, content_type :: String.t(), filename :: String.t()} | {:error, any()} | :error
def file_mime_type(path, filename) do
with {:ok, content_type} <- file_mime_type(path),

View File

@ -301,4 +301,26 @@ def update_data(%Object{data: data} = object, attrs \\ %{}) do
def local?(%Object{data: %{"id" => id}}) do
String.starts_with?(id, Pleroma.Web.base_url() <> "/")
end
def replies(object, opts \\ []) do
object = Object.normalize(object)
query =
Object
|> where(
[o],
fragment("(?)->>'inReplyTo' = ?", o.data, ^object.data["id"])
)
|> order_by([o], asc: o.id)
if opts[:self_only] do
actor = object.data["actor"]
where(query, [o], fragment("(?)->>'actor' = ?", o.data, ^actor))
else
query
end
end
def self_replies(object, opts \\ []),
do: replies(object, Keyword.put(opts, :self_only, true))
end

View File

@ -39,15 +39,8 @@ def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor)
defp compare_uris(_, %URI{scheme: "tag"}), do: :ok
end
defp compare_uris(%URI{} = id_uri, %URI{} = other_uri) do
if id_uri.host == other_uri.host do
:ok
else
:error
end
end
defp compare_uris(_, _), do: :error
defp compare_uris(%URI{host: host} = _id_uri, %URI{host: host} = _other_uri), do: :ok
defp compare_uris(_id_uri, _other_uri), do: :error
@doc """
Checks that an imported AP object's actor matches the domain it came from.

View File

@ -10,6 +10,7 @@ defmodule Pleroma.Object.Fetcher do
alias Pleroma.Signature
alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.Federator
require Logger
require Pleroma.Constants
@ -59,20 +60,23 @@ def refetch_object(%Object{data: %{"id" => id}} = object) do
end
end
# TODO:
# This will create a Create activity, which we need internally at the moment.
# Note: will create a Create activity, which we need internally at the moment.
def fetch_object_from_id(id, options \\ []) do
with {:fetch_object, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
{:fetch, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
{:normalize, nil} <- {:normalize, Object.normalize(data, false)},
with {_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
{_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])},
{_, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
{_, nil} <- {:normalize, Object.normalize(data, false)},
params <- prepare_activity_params(data),
{:containment, :ok} <- {:containment, Containment.contain_origin(id, params)},
{:transmogrifier, {:ok, activity}} <-
{_, :ok} <- {:containment, Containment.contain_origin(id, params)},
{_, {:ok, activity}} <-
{:transmogrifier, Transmogrifier.handle_incoming(params, options)},
{:object, _data, %Object{} = object} <-
{_, _data, %Object{} = object} <-
{:object, data, Object.normalize(activity, false)} do
{:ok, object}
else
{:allowed_depth, false} ->
{:error, "Max thread distance exceeded."}
{:containment, _} ->
{:error, "Object containment failed."}

View File

@ -4,6 +4,7 @@
defmodule Pleroma.Stats do
import Ecto.Query
alias Pleroma.CounterCache
alias Pleroma.Repo
alias Pleroma.User
@ -96,4 +97,21 @@ defp get_stat_data do
}
}
end
def get_status_visibility_count do
counter_cache =
CounterCache.get_as_map([
"status_visibility_public",
"status_visibility_private",
"status_visibility_unlisted",
"status_visibility_direct"
])
%{
public: counter_cache["status_visibility_public"] || 0,
unlisted: counter_cache["status_visibility_unlisted"] || 0,
private: counter_cache["status_visibility_private"] || 0,
direct: counter_cache["status_visibility_direct"] || 0
}
end
end

View File

@ -853,14 +853,14 @@ def get_followers_query(user, page) do
@spec get_followers_query(User.t()) :: Ecto.Query.t()
def get_followers_query(user), do: get_followers_query(user, nil)
@spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
@spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
def get_followers(user, page \\ nil) do
user
|> get_followers_query(page)
|> Repo.all()
end
@spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
@spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
def get_external_followers(user, page \\ nil) do
user
|> get_followers_query(page)
@ -1304,7 +1304,6 @@ def perform(:delete, %User{} = user) do
Repo.delete(user)
end
@spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:fetch_initial_posts, %User{} = user) do
pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
@ -1336,7 +1335,6 @@ def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
)
end
@spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
def perform(:follow_import, %User{} = follower, followed_identifiers)
when is_list(followed_identifiers) do
Enum.map(

View File

@ -48,7 +48,7 @@ defmodule Pleroma.User.Query do
followers: User.t(),
friends: User.t(),
recipients_from_activity: [String.t()],
nickname: [String.t()],
nickname: [String.t()] | String.t(),
ap_id: [String.t()],
order_by: term(),
select: term(),

View File

@ -33,9 +33,15 @@ defp format_query(query_string) do
# Strip the beginning @ off if there is a query
query_string = String.trim_leading(query_string, "@")
with [name, domain] <- String.split(query_string, "@"),
formatted_domain <- String.replace(domain, ~r/[!-\-|@|[-`|{-~|\/|:|\s]+/, "") do
name <> "@" <> to_string(:idna.encode(formatted_domain))
with [name, domain] <- String.split(query_string, "@") do
encoded_domain =
domain
|> String.replace(~r/[!-\-|@|[-`|{-~|\/|:|\s]+/, "")
|> String.to_charlist()
|> :idna.encode()
|> to_string()
name <> "@" <> encoded_domain
else
_ -> query_string
end

View File

@ -770,13 +770,18 @@ def fetch_user_activities(user, reading_user, params \\ %{}) do
|> Enum.reverse()
end
def fetch_instance_activities(params) do
def fetch_statuses(reading_user, params) do
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("instance", params["instance"])
fetch_activities([Pleroma.Constants.as_public()], params, :offset)
recipients =
user_activities_recipients(%{
"godmode" => params["godmode"],
"reading_user" => reading_user
})
fetch_activities(recipients, params, :offset)
|> Enum.reverse()
end

View File

@ -156,10 +156,11 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
when not is_nil(in_reply_to) do
in_reply_to_id = prepare_in_reply_to(in_reply_to)
object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
depth = (options[:depth] || 0) + 1
if Federator.allowed_incoming_reply_depth?(options[:depth]) do
if Federator.allowed_thread_distance?(depth) do
with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
%Activity{} = _ <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
%Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
object
|> Map.put("inReplyTo", replied_object.data["id"])
|> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
@ -312,7 +313,7 @@ def fix_type(object, options \\ [])
def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
when is_binary(reply_id) do
with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
with true <- Federator.allowed_thread_distance?(options[:depth]),
{:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
Map.put(object, "type", "Answer")
else
@ -406,8 +407,7 @@ def handle_incoming(
with nil <- Activity.get_create_by_object_ap_id(object["id"]),
{:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
object = fix_object(data["object"], options)
object = fix_object(object, options)
params = %{
to: data["to"],
@ -424,7 +424,20 @@ def handle_incoming(
])
}
ActivityPub.create(params)
with {:ok, created_activity} <- ActivityPub.create(params) do
reply_depth = (options[:depth] || 0) + 1
if Federator.allowed_thread_distance?(reply_depth) do
for reply_id <- replies(object) do
Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
"id" => reply_id,
"depth" => reply_depth
})
end
end
{:ok, created_activity}
end
else
%Activity{} = activity -> {:ok, activity}
_e -> :error
@ -442,7 +455,8 @@ def handle_incoming(
|> fix_addressing
with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
reply_depth = (options[:depth] || 0) + 1
options = Keyword.put(options, :depth, reply_depth)
object = fix_object(object, options)
params = %{
@ -903,6 +917,50 @@ def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_r
def set_reply_to_uri(obj), do: obj
@doc """
Serialized Mastodon-compatible `replies` collection containing _self-replies_.
Based on Mastodon's ActivityPub::NoteSerializer#replies.
"""
def set_replies(obj_data) do
replies_uris =
with limit when limit > 0 <-
Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
%Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
object
|> Object.self_replies()
|> select([o], fragment("?->>'id'", o.data))
|> limit(^limit)
|> Repo.all()
else
_ -> []
end
set_replies(obj_data, replies_uris)
end
defp set_replies(obj, []) do
obj
end
defp set_replies(obj, replies_uris) do
replies_collection = %{
"type" => "Collection",
"items" => replies_uris
}
Map.merge(obj, %{"replies" => replies_collection})
end
def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
items
end
def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
items
end
def replies(_), do: []
# Prepares the object of an outgoing create activity.
def prepare_object(object) do
object
@ -914,6 +972,7 @@ def prepare_object(object) do
|> prepare_attachments
|> set_conversation
|> set_reply_to_uri
|> set_replies
|> strip_internal_fields
|> strip_internal_tags
|> set_type

View File

@ -45,8 +45,8 @@ def normalize_params(params) do
Map.put(params, "actor", get_ap_id(params["actor"]))
end
@spec determine_explicit_mentions(map()) :: map()
def determine_explicit_mentions(%{"tag" => tag} = _) when is_list(tag) do
@spec determine_explicit_mentions(map()) :: [any]
def determine_explicit_mentions(%{"tag" => tag}) when is_list(tag) do
Enum.flat_map(tag, fn
%{"type" => "Mention", "href" => href} -> [href]
_ -> []
@ -427,7 +427,7 @@ defp fetch_likes(object) do
@doc """
Updates a follow activity's state (for locked accounts).
"""
@spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity} | {:error, any()}
@spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity | nil}
def update_follow_state_for_all(
%Activity{data: %{"actor" => actor, "object" => object}} = activity,
state

View File

@ -13,6 +13,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.ModerationLog
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.ReportNote
alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub
@ -98,7 +99,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug(
OAuthScopesPlug,
%{scopes: ["read"], admin: true}
when action in [:config_show, :list_log]
when action in [:config_show, :list_log, :stats]
)
plug(
@ -243,13 +244,15 @@ def user_show(conn, %{"nickname" => nickname}) do
end
def list_instance_statuses(conn, %{"instance" => instance} = params) do
with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
{page, page_size} = page_params(params)
activities =
ActivityPub.fetch_instance_activities(%{
ActivityPub.fetch_statuses(nil, %{
"instance" => instance,
"limit" => page_size,
"offset" => (page - 1) * page_size
"offset" => (page - 1) * page_size,
"exclude_reblogs" => !with_reblogs && "true"
})
conn
@ -258,6 +261,7 @@ def list_instance_statuses(conn, %{"instance" => instance} = params) do
end
def list_user_statuses(conn, %{"nickname" => nickname} = params) do
with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
godmode = params["godmode"] == "true" || params["godmode"] == true
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
@ -266,7 +270,8 @@ def list_user_statuses(conn, %{"nickname" => nickname} = params) do
activities =
ActivityPub.fetch_user_activities(user, nil, %{
"limit" => page_size,
"godmode" => godmode
"godmode" => godmode,
"exclude_reblogs" => !with_reblogs && "true"
})
conn
@ -740,6 +745,24 @@ def report_notes_delete(%{assigns: %{user: user}} = conn, %{
end
end
def list_statuses(%{assigns: %{user: admin}} = conn, params) do
godmode = params["godmode"] == "true" || params["godmode"] == true
local_only = params["local_only"] == "true" || params["local_only"] == true
{page, page_size} = page_params(params)
activities =
ActivityPub.fetch_statuses(admin, %{
"godmode" => godmode,
"local_only" => local_only,
"limit" => page_size,
"offset" => (page - 1) * page_size
})
conn
|> put_view(Pleroma.Web.AdminAPI.StatusView)
|> render("index.json", %{activities: activities, as: :activity})
end
def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
{:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
@ -953,6 +976,13 @@ def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" =
conn |> json("")
end
def stats(conn, _) do
count = Stats.get_status_visibility_count()
conn
|> json(%{"status_visibility" => count})
end
def errors(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)

View File

@ -18,7 +18,11 @@ defmacro not_empty_string(string) do
@spec user(map()) :: {:ok, [User.t()], pos_integer()}
def user(params \\ %{}) do
query = User.Query.build(params) |> order_by([u], u.nickname)
query =
params
|> Map.drop([:page, :page_size])
|> User.Query.build()
|> order_by([u], u.nickname)
paginated_query =
User.Query.paginate(query, params[:page] || 1, params[:page_size] || @page_size)

View File

@ -10,7 +10,7 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
alias Pleroma.User
def render("index.json", opts) do
render_many(opts.activities, __MODULE__, "show.json", opts)
safe_render_many(opts.activities, __MODULE__, "show.json", opts)
end
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do

View File

@ -15,13 +15,19 @@ defmodule Pleroma.Web.Federator do
require Logger
@doc "Addresses [memory leaks on recursive replies fetching](https://git.pleroma.social/pleroma/pleroma/issues/161)"
@doc """
Returns `true` if the distance to target object does not exceed max configured value.
Serves to prevent fetching of very long threads, especially useful on smaller instances.
Addresses [memory leaks on recursive replies fetching](https://git.pleroma.social/pleroma/pleroma/issues/161).
Applies to fetching of both ancestor (reply-to) and child (reply) objects.
"""
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
def allowed_incoming_reply_depth?(depth) do
max_replies_depth = Pleroma.Config.get([:instance, :federation_incoming_replies_max_depth])
def allowed_thread_distance?(distance) do
max_distance = Pleroma.Config.get([:instance, :federation_incoming_replies_max_depth])
if max_replies_depth do
(depth || 1) <= max_replies_depth
if max_distance && max_distance >= 0 do
# Default depth is 0 (an object has zero distance from itself in its thread)
(distance || 0) <= max_distance
else
true
end

View File

@ -92,9 +92,9 @@ def raw_nodeinfo do
openRegistrations: Config.get([:instance, :registrations_open]),
usage: %{
users: %{
total: stats.user_count || 0
total: Map.get(stats, :user_count, 0)
},
localPosts: stats.status_count || 0
localPosts: Map.get(stats, :status_count, 0)
},
metadata: %{
nodeName: Config.get([:instance, :name]),

View File

@ -323,7 +323,7 @@ def delete(conn, %{"name" => name}) do
{:ok, _} ->
conn |> json("ok")
{:error, _} ->
{:error, _, _} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "Couldn't delete the pack #{name}"})

View File

@ -192,6 +192,7 @@ defmodule Pleroma.Web.Router do
put("/statuses/:id", AdminAPIController, :status_update)
delete("/statuses/:id", AdminAPIController, :status_delete)
get("/statuses", AdminAPIController, :list_statuses)
get("/config", AdminAPIController, :config_show)
post("/config", AdminAPIController, :config_update)
@ -201,6 +202,7 @@ defmodule Pleroma.Web.Router do
get("/moderation_log", AdminAPIController, :list_log)
post("/reload_emoji", AdminAPIController, :reload_emoji)
get("/stats", AdminAPIController, :stats)
end
scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do

View File

@ -0,0 +1,20 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.RemoteFetcherWorker do
alias Pleroma.Object.Fetcher
use Pleroma.Workers.WorkerHelper, queue: "remote_fetcher"
@impl Oban.Worker
def perform(
%{
"op" => "fetch_remote",
"id" => id
} = args,
_job
) do
{:ok, _object} = Fetcher.fetch_object_from_id(id, depth: args["depth"])
end
end

View File

@ -0,0 +1,55 @@
defmodule Pleroma.Repo.Migrations.AddCounterCacheTable do
use Ecto.Migration
def up do
create_if_not_exists table(:counter_cache) do
add(:name, :string, null: false)
add(:count, :bigint, null: false, default: 0)
end
create_if_not_exists(unique_index(:counter_cache, [:name]))
"""
CREATE OR REPLACE FUNCTION update_status_visibility_counter_cache()
RETURNS TRIGGER AS
$$
DECLARE
BEGIN
IF TG_OP = 'INSERT' THEN
IF NEW.data->>'type' = 'Create' THEN
EXECUTE 'INSERT INTO counter_cache (name, count) VALUES (''status_visibility_' || activity_visibility(NEW.actor, NEW.recipients, NEW.data) || ''', 1) ON CONFLICT (name) DO UPDATE SET count = counter_cache.count + 1';
END IF;
RETURN NEW;
ELSIF TG_OP = 'UPDATE' THEN
IF (NEW.data->>'type' = 'Create') and (OLD.data->>'type' = 'Create') and activity_visibility(NEW.actor, NEW.recipients, NEW.data) != activity_visibility(OLD.actor, OLD.recipients, OLD.data) THEN
EXECUTE 'INSERT INTO counter_cache (name, count) VALUES (''status_visibility_' || activity_visibility(NEW.actor, NEW.recipients, NEW.data) || ''', 1) ON CONFLICT (name) DO UPDATE SET count = counter_cache.count + 1';
EXECUTE 'update counter_cache SET count = counter_cache.count - 1 where count > 0 and name = ''status_visibility_' || activity_visibility(OLD.actor, OLD.recipients, OLD.data) || ''';';
END IF;
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
IF OLD.data->>'type' = 'Create' THEN
EXECUTE 'update counter_cache SET count = counter_cache.count - 1 where count > 0 and name = ''status_visibility_' || activity_visibility(OLD.actor, OLD.recipients, OLD.data) || ''';';
END IF;
RETURN OLD;
END IF;
END;
$$
LANGUAGE 'plpgsql';
"""
|> execute()
"""
CREATE TRIGGER status_visibility_counter_cache_trigger BEFORE INSERT OR UPDATE of recipients, data OR DELETE ON activities
FOR EACH ROW
EXECUTE PROCEDURE update_status_visibility_counter_cache();
"""
|> execute()
end
def down do
execute("drop trigger if exists status_visibility_counter_cache_trigger on activities")
execute("drop function if exists update_status_visibility_counter_cache()")
drop_if_exists(unique_index(:counter_cache, [:name]))
drop_if_exists(table(:counter_cache))
end
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.

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.1abbc9b8.css rel=stylesheet><link href=chunk-libs.686b5876.css rel=stylesheet><link href=app.c836e084.css rel=stylesheet></head><body><div id=app></div><script type=text/javascript src=static/js/runtime.dfdeb6eb.js></script><script type=text/javascript src=static/js/chunk-elementUI.fba0efec.js></script><script type=text/javascript src=static/js/chunk-libs.b8c453ab.js></script><script type=text/javascript src=static/js/app.5f0094e3.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.686b5876.css rel=stylesheet><link href=app.c836e084.css rel=stylesheet></head><body><div id=app></div><script type=text/javascript src=static/js/runtime.ae93ea9f.js></script><script type=text/javascript src=static/js/chunk-elementUI.fba0efec.js></script><script type=text/javascript src=static/js/chunk-libs.b8c453ab.js></script><script type=text/javascript src=static/js/app.30262183.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.

View File

@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link href=/static/css/vendors~app.b2603a50868c68a1c192.css rel=stylesheet><link href=/static/css/app.ae04505b31bb0ee2765e.css rel=stylesheet><link href=/static/fontello.1581425930672.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.52ac194cbc427f97f06e.js></script><script type=text/javascript src=/static/js/app.f8af8a9b83e330e80903.js></script></body></html>
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link href=/static/css/vendors~app.b2603a50868c68a1c192.css rel=stylesheet><link href=/static/css/app.1055039ce3f2fe4dd110.css rel=stylesheet><link href=/static/fontello.1582927362782.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.c5bbd3734647f0cc7eef.js></script><script type=text/javascript src=/static/js/app.128bd8b808a3b5b6da6b.js></script></body></html>

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